Skip to content

Commit ae0a340

Browse files
committed
pesto: add target address mapping to CLI args
Fixes: podman-container-tools/podman#28770 Signed-off-by: Jan Rodák <hony.com@seznam.cz>
1 parent b17b0a4 commit ae0a340

2 files changed

Lines changed: 138 additions & 49 deletions

File tree

common/libnetwork/pasta/pesto_linux.go

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44
// Used by rootless bridge networking: pesto incrementally adds or deletes
55
// port forwarding rules for individual containers.
66
//
7-
// Passt only forwards traffic from the host into the rootless netns.
8-
// Netavark handles the final DNAT to the container IP:ContainerPort
9-
// inside the netns. Each mapping uses HostPort as both source and
10-
// destination so traffic arrives at the port netavark expects.
7+
// Each mapping specifies both the host binding and container target
8+
// address, so pasta forwards traffic directly to the correct
9+
// container IP:ContainerPort.
1110
//
1211
// When no HostIP is specified, pesto binds both IPv4 (0.0.0.0) and
1312
// IPv6 ([::]) so dual-stack networks work out of the box.
1413
//
1514
// Limitations:
16-
// - TCP and UDP only (SCTP is silently skipped)
15+
// - TCP and UDP only (unsupported protocols return an error)
1716

1817
package pasta
1918

@@ -32,31 +31,35 @@ const PestoBinaryName = "pesto"
3231

3332
// PestoAddPorts adds port forwarding rules to the running pasta instance
3433
// via -A/--add. Idempotent: adding already-active ports is a no-op.
35-
func PestoAddPorts(conf *config.Config, socketPath string, ports []types.PortMapping) error {
34+
// containerIPv4 and containerIPv6 are the container's addresses inside the
35+
// network namespace; they are embedded in the target side of each mapping.
36+
func PestoAddPorts(conf *config.Config, socketPath string, ports []types.PortMapping, containerIPv4, containerIPv6 string) error {
3637
if socketPath == "" {
3738
return errors.New("pesto control socket not available")
3839
}
3940
logrus.Debugf("pesto: adding %d port mappings", len(ports))
40-
return pestoModifyPorts(conf, socketPath, ports, "--add")
41+
return pestoModifyPorts(conf, socketPath, ports, "--add", containerIPv4, containerIPv6)
4142
}
4243

4344
// PestoDeletePorts removes port forwarding rules from the running pasta
4445
// instance via -D/--delete.
45-
func PestoDeletePorts(conf *config.Config, socketPath string, ports []types.PortMapping) error {
46+
// containerIPv4 and containerIPv6 are the container's addresses inside the
47+
// network namespace; they are embedded in the target side of each mapping.
48+
func PestoDeletePorts(conf *config.Config, socketPath string, ports []types.PortMapping, containerIPv4, containerIPv6 string) error {
4649
if socketPath == "" {
4750
return nil
4851
}
4952
logrus.Debugf("pesto: deleting %d port mappings", len(ports))
50-
return pestoModifyPorts(conf, socketPath, ports, "--delete")
53+
return pestoModifyPorts(conf, socketPath, ports, "--delete", containerIPv4, containerIPv6)
5154
}
5255

53-
func pestoModifyPorts(conf *config.Config, socketPath string, ports []types.PortMapping, mode string) error {
56+
func pestoModifyPorts(conf *config.Config, socketPath string, ports []types.PortMapping, mode, containerIPv4, containerIPv6 string) error {
5457
pestoPath, err := conf.FindHelperBinary(PestoBinaryName, true)
5558
if err != nil {
5659
return fmt.Errorf("could not find pesto binary: %w", err)
5760
}
5861

59-
pestoArgs, err := portMappingsToPestoArgs(ports)
62+
pestoArgs, err := portMappingsToPestoArgs(ports, containerIPv4, containerIPv6)
6063
if err != nil {
6164
return err
6265
}
@@ -77,23 +80,42 @@ func pestoModifyPorts(conf *config.Config, socketPath string, ports []types.Port
7780
return nil
7881
}
7982

80-
// portMappingsToPestoArgs converts PortMappings into pesto CLI arguments.
83+
// portMappingsToPestoArgs converts PortMappings into pesto CLI arguments
84+
// using the target mapping syntax: -t hostIP/hostPort:containerIP/containerPort.
8185
//
82-
// When HostIP is set, a single binding is created (e.g. "-t 127.0.0.1/8080").
83-
// When HostIP is empty, both IPv4 and IPv6 bindings are created so that
84-
// dual-stack networks work: "-t 0.0.0.0/8080 -t [::]/8080".
85-
func portMappingsToPestoArgs(ports []types.PortMapping) ([]string, error) {
86+
// IPv6 host addresses are bracketed (e.g. [::]) to disambiguate colons
87+
// from the mapping ":" separator; container addresses are never bracketed
88+
// because they appear after the ":" where there is no ambiguity.
89+
//
90+
// When HostIP is empty, dual-stack bindings are created using 0.0.0.0 with
91+
// containerIPv4, and (if containerIPv6 is non-empty) [::] with containerIPv6.
92+
// When HostIP is set, the address-family-matched container IP is used.
93+
func portMappingsToPestoArgs(ports []types.PortMapping, containerIPv4, containerIPv6 string) ([]string, error) {
94+
type addrPair struct {
95+
host, container string
96+
}
97+
8698
var args []string
8799

88100
for _, p := range ports {
89-
var addrs []string
101+
var pairs []addrPair
102+
90103
switch {
91104
case p.HostIP == "":
92-
addrs = []string{"0.0.0.0/", "[::]/"}
105+
if containerIPv4 != "" {
106+
pairs = append(pairs, addrPair{"0.0.0.0", containerIPv4})
107+
}
108+
if containerIPv6 != "" {
109+
pairs = append(pairs, addrPair{"[::]", containerIPv6})
110+
}
93111
case strings.Contains(p.HostIP, ":"):
94-
addrs = []string{"[" + p.HostIP + "]/"}
112+
if containerIPv6 != "" {
113+
pairs = append(pairs, addrPair{"[" + p.HostIP + "]", containerIPv6})
114+
}
95115
default:
96-
addrs = []string{p.HostIP + "/"}
116+
if containerIPv4 != "" {
117+
pairs = append(pairs, addrPair{p.HostIP, containerIPv4})
118+
}
97119
}
98120

99121
for protocol := range strings.SplitSeq(p.Protocol, ",") {
@@ -112,12 +134,14 @@ func portMappingsToPestoArgs(ports []types.PortMapping) ([]string, error) {
112134
portRange = 1
113135
}
114136

115-
for _, addr := range addrs {
137+
for _, pair := range pairs {
116138
var arg string
117139
if portRange == 1 {
118-
arg = fmt.Sprintf("%s%d", addr, p.HostPort)
140+
arg = fmt.Sprintf("%s/%d:%s/%d", pair.host, p.HostPort, pair.container, p.ContainerPort)
119141
} else {
120-
arg = fmt.Sprintf("%s%d-%d", addr, p.HostPort, p.HostPort+portRange-1)
142+
arg = fmt.Sprintf("%s/%d-%d:%s/%d-%d",
143+
pair.host, p.HostPort, p.HostPort+portRange-1,
144+
pair.container, p.ContainerPort, p.ContainerPort+portRange-1)
121145
}
122146
args = append(args, flag, arg)
123147
}

common/libnetwork/pasta/pesto_linux_test.go

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import (
99

1010
func Test_portMappingsToPestoArgs(t *testing.T) {
1111
tests := []struct {
12-
name string
13-
ports []types.PortMapping
14-
want []string
15-
wantErr string
12+
name string
13+
ports []types.PortMapping
14+
containerIPv4 string
15+
containerIPv6 string
16+
want []string
17+
wantErr string
1618
}{
1719
{
1820
name: "no ports returns nil",
@@ -29,79 +31,98 @@ func Test_portMappingsToPestoArgs(t *testing.T) {
2931
ports: []types.PortMapping{
3032
{HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1},
3133
},
32-
want: []string{"-t", "0.0.0.0/8080", "-t", "[::]/8080"},
34+
containerIPv4: "10.0.0.2",
35+
containerIPv6: "fd00::2",
36+
want: []string{"-t", "0.0.0.0/8080:10.0.0.2/80", "-t", "[::]/8080:fd00::2/80"},
3337
},
3438
{
3539
name: "single udp port dual-stack",
3640
ports: []types.PortMapping{
3741
{HostPort: 53, ContainerPort: 53, Protocol: "udp", Range: 1},
3842
},
39-
want: []string{"-u", "0.0.0.0/53", "-u", "[::]/53"},
43+
containerIPv4: "10.0.0.2",
44+
containerIPv6: "fd00::2",
45+
want: []string{"-u", "0.0.0.0/53:10.0.0.2/53", "-u", "[::]/53:fd00::2/53"},
4046
},
4147
{
4248
name: "tcp and udp port dual-stack",
4349
ports: []types.PortMapping{
4450
{HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1},
4551
{HostPort: 53, ContainerPort: 53, Protocol: "udp", Range: 1},
4652
},
47-
want: []string{"-t", "0.0.0.0/80", "-t", "[::]/80", "-u", "0.0.0.0/53", "-u", "[::]/53"},
53+
containerIPv4: "10.0.0.2",
54+
containerIPv6: "fd00::2",
55+
want: []string{"-t", "0.0.0.0/80:10.0.0.2/80", "-t", "[::]/80:fd00::2/80", "-u", "0.0.0.0/53:10.0.0.2/53", "-u", "[::]/53:fd00::2/53"},
4856
},
4957
{
5058
name: "dual protocol on single mapping",
5159
ports: []types.PortMapping{
5260
{HostPort: 80, ContainerPort: 80, Protocol: "tcp,udp", Range: 1},
5361
},
54-
want: []string{"-t", "0.0.0.0/80", "-t", "[::]/80", "-u", "0.0.0.0/80", "-u", "[::]/80"},
62+
containerIPv4: "10.0.0.2",
63+
containerIPv6: "fd00::2",
64+
want: []string{"-t", "0.0.0.0/80:10.0.0.2/80", "-t", "[::]/80:fd00::2/80", "-u", "0.0.0.0/80:10.0.0.2/80", "-u", "[::]/80:fd00::2/80"},
5565
},
5666
{
57-
name: "port range expands to host port range",
67+
name: "port range maps both host and container ranges",
5868
ports: []types.PortMapping{
5969
{HostPort: 8000, ContainerPort: 80, Protocol: "tcp", Range: 5},
6070
},
61-
want: []string{"-t", "0.0.0.0/8000-8004", "-t", "[::]/8000-8004"},
71+
containerIPv4: "10.0.0.2",
72+
containerIPv6: "fd00::2",
73+
want: []string{"-t", "0.0.0.0/8000-8004:10.0.0.2/80-84", "-t", "[::]/8000-8004:fd00::2/80-84"},
6274
},
6375
{
6476
name: "range of zero treated as single port",
6577
ports: []types.PortMapping{
6678
{HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 0},
6779
},
68-
want: []string{"-t", "0.0.0.0/80", "-t", "[::]/80"},
80+
containerIPv4: "10.0.0.2",
81+
containerIPv6: "fd00::2",
82+
want: []string{"-t", "0.0.0.0/80:10.0.0.2/80", "-t", "[::]/80:fd00::2/80"},
6983
},
7084
{
7185
name: "range of two",
7286
ports: []types.PortMapping{
7387
{HostPort: 3000, ContainerPort: 3000, Protocol: "tcp", Range: 2},
7488
},
75-
want: []string{"-t", "0.0.0.0/3000-3001", "-t", "[::]/3000-3001"},
89+
containerIPv4: "10.0.0.2",
90+
containerIPv6: "fd00::2",
91+
want: []string{"-t", "0.0.0.0/3000-3001:10.0.0.2/3000-3001", "-t", "[::]/3000-3001:fd00::2/3000-3001"},
7692
},
7793
{
7894
name: "explicit IPv4 host IP",
7995
ports: []types.PortMapping{
8096
{HostIP: "127.0.0.1", HostPort: 443, ContainerPort: 443, Protocol: "tcp", Range: 1},
8197
},
82-
want: []string{"-t", "127.0.0.1/443"},
98+
containerIPv4: "10.0.0.2",
99+
want: []string{"-t", "127.0.0.1/443:10.0.0.2/443"},
83100
},
84101
{
85102
name: "IPv6 host IP gets brackets",
86103
ports: []types.PortMapping{
87104
{HostIP: "::1", HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1},
88105
},
89-
want: []string{"-t", "[::1]/8080"},
106+
containerIPv6: "fd00::2",
107+
want: []string{"-t", "[::1]/8080:fd00::2/80"},
90108
},
91109
{
92110
name: "full-form IPv6 host IP",
93111
ports: []types.PortMapping{
94112
{HostIP: "fd00::1", HostPort: 80, ContainerPort: 80, Protocol: "udp", Range: 1},
95113
},
96-
want: []string{"-u", "[fd00::1]/80"},
114+
containerIPv6: "fd00::2",
115+
want: []string{"-u", "[fd00::1]/80:fd00::2/80"},
97116
},
98117
{
99118
name: "multiple tcp ports dual-stack",
100119
ports: []types.PortMapping{
101120
{HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1},
102121
{HostPort: 443, ContainerPort: 443, Protocol: "tcp", Range: 1},
103122
},
104-
want: []string{"-t", "0.0.0.0/80", "-t", "[::]/80", "-t", "0.0.0.0/443", "-t", "[::]/443"},
123+
containerIPv4: "10.0.0.2",
124+
containerIPv6: "fd00::2",
125+
want: []string{"-t", "0.0.0.0/80:10.0.0.2/80", "-t", "[::]/80:fd00::2/80", "-t", "0.0.0.0/443:10.0.0.2/443", "-t", "[::]/443:fd00::2/443"},
105126
},
106127
{
107128
name: "unsupported protocol returns error",
@@ -123,43 +144,52 @@ func Test_portMappingsToPestoArgs(t *testing.T) {
123144
ports: []types.PortMapping{
124145
{HostIP: "10.0.0.1", HostPort: 3000, ContainerPort: 3000, Protocol: "udp", Range: 1},
125146
},
126-
want: []string{"-u", "10.0.0.1/3000"},
147+
containerIPv4: "10.0.0.2",
148+
want: []string{"-u", "10.0.0.1/3000:10.0.0.2/3000"},
127149
},
128150
{
129-
name: "container port does not appear in args",
151+
name: "host and container ports differ",
130152
ports: []types.PortMapping{
131153
{HostPort: 9090, ContainerPort: 3000, Protocol: "tcp", Range: 1},
132154
},
133-
want: []string{"-t", "0.0.0.0/9090", "-t", "[::]/9090"},
155+
containerIPv4: "10.0.0.2",
156+
containerIPv6: "fd00::2",
157+
want: []string{"-t", "0.0.0.0/9090:10.0.0.2/3000", "-t", "[::]/9090:fd00::2/3000"},
134158
},
135159
{
136160
name: "host IP with range",
137161
ports: []types.PortMapping{
138162
{HostIP: "10.0.0.1", HostPort: 3000, ContainerPort: 3000, Protocol: "udp", Range: 3},
139163
},
140-
want: []string{"-u", "10.0.0.1/3000-3002"},
164+
containerIPv4: "10.0.0.2",
165+
want: []string{"-u", "10.0.0.1/3000-3002:10.0.0.2/3000-3002"},
141166
},
142167
{
143168
name: "range with dual protocol",
144169
ports: []types.PortMapping{
145170
{HostPort: 5000, ContainerPort: 5000, Protocol: "tcp,udp", Range: 3},
146171
},
147-
want: []string{"-t", "0.0.0.0/5000-5002", "-t", "[::]/5000-5002", "-u", "0.0.0.0/5000-5002", "-u", "[::]/5000-5002"},
172+
containerIPv4: "10.0.0.2",
173+
containerIPv6: "fd00::2",
174+
want: []string{"-t", "0.0.0.0/5000-5002:10.0.0.2/5000-5002", "-t", "[::]/5000-5002:fd00::2/5000-5002", "-u", "0.0.0.0/5000-5002:10.0.0.2/5000-5002", "-u", "[::]/5000-5002:fd00::2/5000-5002"},
148175
},
149176
{
150177
name: "IPv6 host IP with range",
151178
ports: []types.PortMapping{
152179
{HostIP: "::1", HostPort: 5000, ContainerPort: 5000, Protocol: "tcp", Range: 4},
153180
},
154-
want: []string{"-t", "[::1]/5000-5003"},
181+
containerIPv6: "fd00::2",
182+
want: []string{"-t", "[::1]/5000-5003:fd00::2/5000-5003"},
155183
},
156184
{
157185
name: "mixed explicit and default host IPs",
158186
ports: []types.PortMapping{
159187
{HostIP: "10.0.0.1", HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1},
160188
{HostPort: 443, ContainerPort: 443, Protocol: "tcp", Range: 1},
161189
},
162-
want: []string{"-t", "10.0.0.1/80", "-t", "0.0.0.0/443", "-t", "[::]/443"},
190+
containerIPv4: "10.0.0.2",
191+
containerIPv6: "fd00::2",
192+
want: []string{"-t", "10.0.0.1/80:10.0.0.2/80", "-t", "0.0.0.0/443:10.0.0.2/443", "-t", "[::]/443:fd00::2/443"},
163193
},
164194
{
165195
name: "triple protocol with unsupported in middle returns error",
@@ -173,14 +203,16 @@ func Test_portMappingsToPestoArgs(t *testing.T) {
173203
ports: []types.PortMapping{
174204
{HostIP: "192.168.1.1", HostPort: 80, ContainerPort: 80, Protocol: "tcp,udp", Range: 1},
175205
},
176-
want: []string{"-t", "192.168.1.1/80", "-u", "192.168.1.1/80"},
206+
containerIPv4: "10.0.0.2",
207+
want: []string{"-t", "192.168.1.1/80:10.0.0.2/80", "-u", "192.168.1.1/80:10.0.0.2/80"},
177208
},
178209
{
179210
name: "dual protocol with explicit IPv6",
180211
ports: []types.PortMapping{
181212
{HostIP: "fd00::1", HostPort: 80, ContainerPort: 80, Protocol: "tcp,udp", Range: 1},
182213
},
183-
want: []string{"-t", "[fd00::1]/80", "-u", "[fd00::1]/80"},
214+
containerIPv6: "fd00::2",
215+
want: []string{"-t", "[fd00::1]/80:fd00::2/80", "-u", "[fd00::1]/80:fd00::2/80"},
184216
},
185217
{
186218
name: "all unsupported protocols returns error",
@@ -189,11 +221,44 @@ func Test_portMappingsToPestoArgs(t *testing.T) {
189221
},
190222
wantErr: "pesto: unsupported protocol sctp",
191223
},
224+
{
225+
name: "IPv4 only when no IPv6 container address",
226+
ports: []types.PortMapping{
227+
{HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1},
228+
},
229+
containerIPv4: "10.0.0.2",
230+
want: []string{"-t", "0.0.0.0/8080:10.0.0.2/80"},
231+
},
232+
{
233+
name: "IPv6 host IP skipped when no container IPv6",
234+
ports: []types.PortMapping{
235+
{HostIP: "::1", HostPort: 9999, ContainerPort: 9999, Protocol: "tcp", Range: 1},
236+
},
237+
containerIPv4: "10.0.0.2",
238+
want: nil,
239+
},
240+
{
241+
name: "IPv4 host IP skipped when no container IPv4",
242+
ports: []types.PortMapping{
243+
{HostIP: "127.0.0.1", HostPort: 9999, ContainerPort: 9999, Protocol: "tcp", Range: 1},
244+
},
245+
containerIPv6: "fd00::2",
246+
want: nil,
247+
},
248+
{
249+
name: "different host and container port ranges",
250+
ports: []types.PortMapping{
251+
{HostPort: 9000, ContainerPort: 3000, Protocol: "tcp", Range: 3},
252+
},
253+
containerIPv4: "10.0.0.2",
254+
containerIPv6: "fd00::2",
255+
want: []string{"-t", "0.0.0.0/9000-9002:10.0.0.2/3000-3002", "-t", "[::]/9000-9002:fd00::2/3000-3002"},
256+
},
192257
}
193258

194259
for _, tt := range tests {
195260
t.Run(tt.name, func(t *testing.T) {
196-
got, err := portMappingsToPestoArgs(tt.ports)
261+
got, err := portMappingsToPestoArgs(tt.ports, tt.containerIPv4, tt.containerIPv6)
197262
if tt.wantErr != "" {
198263
assert.EqualError(t, err, tt.wantErr)
199264
return

0 commit comments

Comments
 (0)