Skip to content

Commit 04f836d

Browse files
vsiravarswagatbora90
authored andcommitted
Check if port is used in publish flag
Signed-off-by: Swagat Bora <[email protected]> Co-authored-by: Vishwas Siravara <[email protected]>
1 parent 32077c5 commit 04f836d

File tree

5 files changed

+131
-26
lines changed

5 files changed

+131
-26
lines changed

cmd/nerdctl/container/container_run_network_linux_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,61 @@ func TestUniqueHostPortAssignement(t *testing.T) {
349349
}
350350
}
351351

352+
func TestHostPortAlreadyInUse(t *testing.T) {
353+
testCases := []struct {
354+
hostPort string
355+
containerPort string
356+
}{
357+
{
358+
hostPort: "5000",
359+
containerPort: "80/tcp",
360+
},
361+
{
362+
hostPort: "5000",
363+
containerPort: "80/tcp",
364+
},
365+
{
366+
hostPort: "5000",
367+
containerPort: "80/udp",
368+
},
369+
{
370+
hostPort: "5000",
371+
containerPort: "80/sctp",
372+
},
373+
}
374+
375+
tID := testutil.Identifier(t)
376+
377+
for i, tc := range testCases {
378+
tc := tc
379+
tcName := fmt.Sprintf("%+v", tc)
380+
t.Run(tcName, func(t *testing.T) {
381+
if strings.Contains(tc.containerPort, "sctp") && rootlessutil.IsRootless() {
382+
t.Skip("sctp is not supported in rootless mode")
383+
}
384+
testContainerName1 := fmt.Sprintf("%s-%d-1", tID, i)
385+
testContainerName2 := fmt.Sprintf("%s-%d-2", tID, i)
386+
base := testutil.NewBase(t)
387+
t.Cleanup(func() {
388+
base.Cmd("rm", "-f", testContainerName1, testContainerName2).AssertOK()
389+
})
390+
pFlag := fmt.Sprintf("%s:%s", tc.hostPort, tc.containerPort)
391+
cmd1 := base.Cmd("run", "-d",
392+
"--name", testContainerName1, "-p",
393+
pFlag,
394+
testutil.NginxAlpineImage)
395+
396+
cmd2 := base.Cmd("run", "-d",
397+
"--name", testContainerName2, "-p",
398+
pFlag,
399+
testutil.NginxAlpineImage)
400+
401+
cmd1.AssertOK()
402+
cmd2.AssertFail()
403+
})
404+
}
405+
}
406+
352407
func TestRunPort(t *testing.T) {
353408
baseTestRunPort(t, testutil.NginxAlpineImage, testutil.NginxAlpineIndexHTMLSnippet, true)
354409
}

pkg/portutil/port_allocate_linux.go

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ import (
2525

2626
const (
2727
// This port range is compatible with Docker, FYI https://github.com/moby/moby/blob/eb9e42a09ee123af1d95bf7d46dd738258fa2109/libnetwork/portallocator/portallocator_unix.go#L7-L12
28-
allocateEnd = 60999
28+
allocateEnd = uint64(60999)
29+
30+
tcpTimeWait = 6 //TIME_WAIT state is represented by the value 6 in /proc/net/tcp
31+
tcpCloseWait = 8 //CLOSE_WAIT state is represented by the value 8 in /proc/net/tcp
2932
)
3033

3134
var (
32-
allocateStart = 49153
35+
allocateStart = uint64(49153)
3336
)
3437

3538
func filter(ss []procnet.NetworkDetail, filterFunc func(detail procnet.NetworkDetail) bool) (ret []procnet.NetworkDetail) {
@@ -42,24 +45,56 @@ func filter(ss []procnet.NetworkDetail, filterFunc func(detail procnet.NetworkDe
4245
}
4346

4447
func portAllocate(protocol string, ip string, count uint64) (uint64, uint64, error) {
45-
netprocData, err := procnet.ReadStatsFileData(protocol)
48+
usedPorts, err := getUsedPorts(ip, protocol)
4649
if err != nil {
4750
return 0, 0, err
4851
}
49-
netprocItems := procnet.Parse(netprocData)
52+
53+
start := allocateStart
54+
if count > allocateEnd-allocateStart+1 {
55+
return 0, 0, fmt.Errorf("can not allocate %d ports", count)
56+
}
57+
for start < allocateEnd {
58+
needReturn := true
59+
for i := start; i < start+count; i++ {
60+
if _, ok := usedPorts[i]; ok {
61+
needReturn = false
62+
break
63+
}
64+
}
65+
if needReturn {
66+
allocateStart = start + count
67+
return start, start + count - 1, nil
68+
}
69+
start += count
70+
}
71+
return 0, 0, fmt.Errorf("there is not enough %d free ports", count)
72+
}
73+
74+
func getUsedPorts(ip string, protocol string) (map[uint64]bool, error) {
75+
netprocItems := []procnet.NetworkDetail{}
76+
77+
if protocol == "tcp" || protocol == "udp" {
78+
netprocData, err := procnet.ReadStatsFileData(protocol)
79+
if err != nil {
80+
return nil, err
81+
}
82+
netprocItems = append(netprocItems, procnet.Parse(netprocData)...)
83+
}
84+
5085
// In some circumstances, when we bind address like "0.0.0.0:80", we will get the formation of ":::80" in /proc/net/tcp6.
5186
// So we need some trick to process this situation.
5287
if protocol == "tcp" {
5388
tempTCPV6Data, err := procnet.ReadStatsFileData("tcp6")
5489
if err != nil {
55-
return 0, 0, err
90+
return nil, err
5691
}
5792
netprocItems = append(netprocItems, procnet.Parse(tempTCPV6Data)...)
5893
}
5994
if protocol == "udp" {
6095
tempUDPV6Data, err := procnet.ReadStatsFileData("udp6")
6196
if err != nil {
62-
return 0, 0, err
97+
return nil, err
6398
}
6499
netprocItems = append(netprocItems, procnet.Parse(tempUDPV6Data)...)
65100
}
@@ -73,36 +108,26 @@ func portAllocate(protocol string, ip string, count uint64) (uint64, uint64, err
73108

74109
usedPort := make(map[uint64]bool)
75110
for _, value := range netprocItems {
111+
// Skip ports in TIME_WAIT or CLOSE_WAIT state
112+
if protocol == "tcp" && (value.State == tcpTimeWait || value.State == tcpCloseWait) {
113+
// In rootless mode, Rootlesskit creates extra socket connections to proxy traffic from the host network namespace
114+
// to the container namespace. Proxy TCP connections can remain in TIME_WAIT state for 10-20 seconds even when the
115+
// container is stopped/removed, which is standard TCP behavior. These ports are actually available for allocation
116+
// despite appearing in /proc/net/tcp.
117+
continue
118+
}
76119
usedPort[value.LocalPort] = true
77120
}
78121

79122
ipTableItems, err := iptable.ReadIPTables("nat")
80123
if err != nil {
81-
return 0, 0, err
124+
return nil, err
82125
}
83126
destinationPorts := iptable.ParseIPTableRules(ipTableItems)
84127

85128
for _, port := range destinationPorts {
86129
usedPort[port] = true
87130
}
88131

89-
start := uint64(allocateStart)
90-
if count > uint64(allocateEnd-allocateStart+1) {
91-
return 0, 0, fmt.Errorf("can not allocate %d ports", count)
92-
}
93-
for start < allocateEnd {
94-
needReturn := true
95-
for i := start; i < start+count; i++ {
96-
if _, ok := usedPort[i]; ok {
97-
needReturn = false
98-
break
99-
}
100-
}
101-
if needReturn {
102-
allocateStart = int(start + count)
103-
return start, start + count - 1, nil
104-
}
105-
start += count
106-
}
107-
return 0, 0, fmt.Errorf("there is not enough %d free ports", count)
132+
return usedPort, nil
108133
}

pkg/portutil/port_allocate_other.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ import "fmt"
2323
func portAllocate(protocol string, ip string, count uint64) (uint64, uint64, error) {
2424
return 0, 0, fmt.Errorf("auto port allocate are not support Non-Linux platform yet")
2525
}
26+
27+
func getUsedPorts(ip string, protocol string) (map[uint64]bool, error) {
28+
return nil, nil
29+
}

pkg/portutil/portutil.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ func ParseFlagP(s string) ([]cni.PortMapping, error) {
101101
if err != nil {
102102
return nil, fmt.Errorf("invalid hostPort: %s", hostPort)
103103
}
104+
var usedPorts map[uint64]bool
105+
usedPorts, err = getUsedPorts(ip, proto)
106+
if err != nil {
107+
return nil, err
108+
}
109+
for i := startHostPort; i <= endHostPort; i++ {
110+
if usedPorts[i] {
111+
return nil, fmt.Errorf("bind for %s:%d failed: port is already allocated", ip, i)
112+
}
113+
}
104114
}
105115
if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
106116
if endPort != startPort {

pkg/portutil/procnet/procnet.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
type NetworkDetail struct {
2828
LocalIP net.IP
2929
LocalPort uint64
30+
State int
3031
}
3132

3233
func Parse(data []string) (results []NetworkDetail) {
@@ -37,9 +38,19 @@ func Parse(data []string) (results []NetworkDetail) {
3738
if err != nil {
3839
continue
3940
}
41+
42+
state := 0
43+
if len(lineData) > 2 {
44+
stateHex, err := strconv.ParseInt(lineData[3], 16, 32)
45+
if err == nil {
46+
state = int(stateHex)
47+
}
48+
}
49+
4050
results = append(results, NetworkDetail{
4151
LocalIP: ip,
4252
LocalPort: uint64(port),
53+
State: state,
4354
})
4455
}
4556
return results

0 commit comments

Comments
 (0)