Skip to content

Commit 3d27709

Browse files
l1b0ksqueed
authored andcommitted
fix(portmap): correct masquerading source address handling
- Use 0.0.0.0/0 or ::/0 as source address when MasqAll is true for full traffic match Signed-off-by: l1b0k <[email protected]>
1 parent 025aca1 commit 3d27709

2 files changed

Lines changed: 272 additions & 4 deletions

File tree

plugins/meta/portmap/portmap_nftables.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,16 +228,32 @@ func (pmNFT *portMapperNFTables) forwardPorts(config *PortMapConf, containerNet
228228
// In theory we should validate that the original dst IP and port are as
229229
// expected, but *any* traffic matching one of these patterns would need
230230
// to be masqueraded to be able to work correctly anyway.
231+
232+
var masqSrcAddr string
233+
if config.MasqAll {
234+
// MasqAll: match traffic from any source IP
235+
if isV6 {
236+
masqSrcAddr = "::/0"
237+
} else {
238+
masqSrcAddr = "0.0.0.0/0"
239+
}
240+
} else {
241+
// Default: only match traffic from container's own IP (hairpin)
242+
masqSrcAddr = containerNet.IP.String()
243+
}
244+
231245
tx.Add(&knftables.Rule{
232246
Chain: masqueradingChain,
233247
Rule: knftables.Concat(
234-
ipX, "saddr", containerNet.IP,
248+
ipX, "saddr", masqSrcAddr,
235249
ipX, "daddr", containerNet.IP,
236250
"masquerade",
237251
),
238252
Comment: &config.ContainerID,
239253
})
240-
if !isV6 {
254+
if !isV6 && !config.MasqAll {
255+
// Only add localhost rule when MasqAll is false
256+
// (when MasqAll is true, 0.0.0.0/0 already covers 127.0.0.1)
241257
tx.Add(&knftables.Rule{
242258
Chain: masqueradingChain,
243259
Rule: knftables.Concat(
@@ -275,8 +291,12 @@ func (pmNFT *portMapperNFTables) checkPorts(config *PortMapConf, containerNet ne
275291
}
276292
if *config.SNAT {
277293
masqueradings = 1
278-
if !isV6 {
279-
masqueradings *= 2
294+
// When MasqAll is false and IPv4, we have 2 rules:
295+
// 1. hairpin rule (container IP -> container IP)
296+
// 2. localhost rule (127.0.0.1 -> container IP)
297+
// When MasqAll is true, we only have 1 rule (0.0.0.0/0 -> container IP)
298+
if !isV6 && !config.MasqAll {
299+
masqueradings = 2
280300
}
281301
}
282302

plugins/meta/portmap/portmap_nftables_test.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,5 +224,253 @@ add rule ip6 cni_hostport prerouting c d fib daddr type local jump hostports_all
224224
Expect(err).To(HaveOccurred())
225225
})
226226
})
227+
228+
Describe("MasqAll configuration", func() {
229+
var pmNFT *portMapperNFTables
230+
var ipv4Fake, ipv6Fake *knftables.Fake
231+
BeforeEach(func() {
232+
ipv4Fake = knftables.NewFake(knftables.IPv4Family, tableName)
233+
ipv6Fake = knftables.NewFake(knftables.IPv6Family, tableName)
234+
pmNFT = &portMapperNFTables{
235+
ipv4: ipv4Fake,
236+
ipv6: ipv6Fake,
237+
}
238+
})
239+
240+
It(fmt.Sprintf("[%s] generates correct rules with masqAll=true for IPv4", ver), func() {
241+
configTmpl := `{
242+
"name": "test",
243+
"type": "portmap",
244+
"cniVersion": "%s",
245+
"backend": "nftables",
246+
"runtimeConfig": {
247+
"portMappings": [
248+
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
249+
]
250+
},
251+
"snat": true,
252+
"masqAll": true
253+
}`
254+
configBytes := []byte(fmt.Sprintf(configTmpl, ver))
255+
256+
conf, _, err := parseConfig(configBytes, "foo")
257+
Expect(err).NotTo(HaveOccurred())
258+
conf.ContainerID = containerID
259+
260+
err = pmNFT.forwardPorts(conf, *containerNet4)
261+
Expect(err).NotTo(HaveOccurred())
262+
263+
// With masqAll=true, should have only 1 masquerading rule with saddr 0.0.0.0/0
264+
expectedRules := strings.TrimSpace(`
265+
add table ip cni_hostport { comment "CNI portmap plugin" ; }
266+
add chain ip cni_hostport hostip_hostports
267+
add chain ip cni_hostport hostports
268+
add chain ip cni_hostport hostports_all
269+
add chain ip cni_hostport masquerading { type nat hook postrouting priority 100 ; }
270+
add chain ip cni_hostport output { type nat hook output priority -100 ; }
271+
add chain ip cni_hostport prerouting { type nat hook prerouting priority -100 ; }
272+
add rule ip cni_hostport hostports tcp dport 8080 dnat to 10.0.0.2:80 comment "icee6giejonei6so"
273+
add rule ip cni_hostport hostports_all jump hostip_hostports
274+
add rule ip cni_hostport hostports_all jump hostports
275+
add rule ip cni_hostport masquerading ip saddr 0.0.0.0/0 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so"
276+
add rule ip cni_hostport output fib daddr type local jump hostports_all
277+
add rule ip cni_hostport prerouting fib daddr type local jump hostports_all
278+
`)
279+
actualRules := strings.TrimSpace(ipv4Fake.Dump())
280+
Expect(actualRules).To(Equal(expectedRules))
281+
282+
// Check should pass with 1 masquerading rule
283+
err = pmNFT.checkPorts(conf, *containerNet4)
284+
Expect(err).NotTo(HaveOccurred())
285+
})
286+
287+
It(fmt.Sprintf("[%s] generates correct rules with masqAll=false for IPv4", ver), func() {
288+
configTmpl := `{
289+
"name": "test",
290+
"type": "portmap",
291+
"cniVersion": "%s",
292+
"backend": "nftables",
293+
"runtimeConfig": {
294+
"portMappings": [
295+
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
296+
]
297+
},
298+
"snat": true,
299+
"masqAll": false
300+
}`
301+
configBytes := []byte(fmt.Sprintf(configTmpl, ver))
302+
303+
conf, _, err := parseConfig(configBytes, "foo")
304+
Expect(err).NotTo(HaveOccurred())
305+
conf.ContainerID = containerID
306+
307+
err = pmNFT.forwardPorts(conf, *containerNet4)
308+
Expect(err).NotTo(HaveOccurred())
309+
310+
// With masqAll=false, should have 2 masquerading rules: hairpin + localhost
311+
expectedRules := strings.TrimSpace(`
312+
add table ip cni_hostport { comment "CNI portmap plugin" ; }
313+
add chain ip cni_hostport hostip_hostports
314+
add chain ip cni_hostport hostports
315+
add chain ip cni_hostport hostports_all
316+
add chain ip cni_hostport masquerading { type nat hook postrouting priority 100 ; }
317+
add chain ip cni_hostport output { type nat hook output priority -100 ; }
318+
add chain ip cni_hostport prerouting { type nat hook prerouting priority -100 ; }
319+
add rule ip cni_hostport hostports tcp dport 8080 dnat to 10.0.0.2:80 comment "icee6giejonei6so"
320+
add rule ip cni_hostport hostports_all jump hostip_hostports
321+
add rule ip cni_hostport hostports_all jump hostports
322+
add rule ip cni_hostport masquerading ip saddr 10.0.0.2 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so"
323+
add rule ip cni_hostport masquerading ip saddr 127.0.0.1 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so"
324+
add rule ip cni_hostport output fib daddr type local jump hostports_all
325+
add rule ip cni_hostport prerouting fib daddr type local jump hostports_all
326+
`)
327+
actualRules := strings.TrimSpace(ipv4Fake.Dump())
328+
Expect(actualRules).To(Equal(expectedRules))
329+
330+
// Check should pass with 2 masquerading rules
331+
err = pmNFT.checkPorts(conf, *containerNet4)
332+
Expect(err).NotTo(HaveOccurred())
333+
})
334+
335+
It(fmt.Sprintf("[%s] generates correct rules with masqAll=true for IPv6", ver), func() {
336+
configTmpl := `{
337+
"name": "test",
338+
"type": "portmap",
339+
"cniVersion": "%s",
340+
"backend": "nftables",
341+
"runtimeConfig": {
342+
"portMappings": [
343+
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
344+
]
345+
},
346+
"snat": true,
347+
"masqAll": true
348+
}`
349+
configBytes := []byte(fmt.Sprintf(configTmpl, ver))
350+
351+
conf, _, err := parseConfig(configBytes, "foo")
352+
Expect(err).NotTo(HaveOccurred())
353+
conf.ContainerID = containerID
354+
355+
err = pmNFT.forwardPorts(conf, *containerNet6)
356+
Expect(err).NotTo(HaveOccurred())
357+
358+
// With masqAll=true for IPv6, should have only 1 masquerading rule with saddr ::/0
359+
expectedRules := strings.TrimSpace(`
360+
add table ip6 cni_hostport { comment "CNI portmap plugin" ; }
361+
add chain ip6 cni_hostport hostip_hostports
362+
add chain ip6 cni_hostport hostports
363+
add chain ip6 cni_hostport hostports_all
364+
add chain ip6 cni_hostport masquerading { type nat hook postrouting priority 100 ; }
365+
add chain ip6 cni_hostport output { type nat hook output priority -100 ; }
366+
add chain ip6 cni_hostport prerouting { type nat hook prerouting priority -100 ; }
367+
add rule ip6 cni_hostport hostports tcp dport 8080 dnat to [2001:db8::2]:80 comment "icee6giejonei6so"
368+
add rule ip6 cni_hostport hostports_all jump hostip_hostports
369+
add rule ip6 cni_hostport hostports_all jump hostports
370+
add rule ip6 cni_hostport masquerading ip6 saddr ::/0 ip6 daddr 2001:db8::2 masquerade comment "icee6giejonei6so"
371+
add rule ip6 cni_hostport output fib daddr type local jump hostports_all
372+
add rule ip6 cni_hostport prerouting fib daddr type local jump hostports_all
373+
`)
374+
actualRules := strings.TrimSpace(ipv6Fake.Dump())
375+
Expect(actualRules).To(Equal(expectedRules))
376+
377+
// Check should pass with 1 masquerading rule
378+
err = pmNFT.checkPorts(conf, *containerNet6)
379+
Expect(err).NotTo(HaveOccurred())
380+
})
381+
382+
It(fmt.Sprintf("[%s] generates correct rules with masqAll=false for IPv6", ver), func() {
383+
configTmpl := `{
384+
"name": "test",
385+
"type": "portmap",
386+
"cniVersion": "%s",
387+
"backend": "nftables",
388+
"runtimeConfig": {
389+
"portMappings": [
390+
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
391+
]
392+
},
393+
"snat": true,
394+
"masqAll": false
395+
}`
396+
configBytes := []byte(fmt.Sprintf(configTmpl, ver))
397+
398+
conf, _, err := parseConfig(configBytes, "foo")
399+
Expect(err).NotTo(HaveOccurred())
400+
conf.ContainerID = containerID
401+
402+
err = pmNFT.forwardPorts(conf, *containerNet6)
403+
Expect(err).NotTo(HaveOccurred())
404+
405+
// With masqAll=false for IPv6, should have 1 masquerading rule (no localhost for IPv6)
406+
expectedRules := strings.TrimSpace(`
407+
add table ip6 cni_hostport { comment "CNI portmap plugin" ; }
408+
add chain ip6 cni_hostport hostip_hostports
409+
add chain ip6 cni_hostport hostports
410+
add chain ip6 cni_hostport hostports_all
411+
add chain ip6 cni_hostport masquerading { type nat hook postrouting priority 100 ; }
412+
add chain ip6 cni_hostport output { type nat hook output priority -100 ; }
413+
add chain ip6 cni_hostport prerouting { type nat hook prerouting priority -100 ; }
414+
add rule ip6 cni_hostport hostports tcp dport 8080 dnat to [2001:db8::2]:80 comment "icee6giejonei6so"
415+
add rule ip6 cni_hostport hostports_all jump hostip_hostports
416+
add rule ip6 cni_hostport hostports_all jump hostports
417+
add rule ip6 cni_hostport masquerading ip6 saddr 2001:db8::2 ip6 daddr 2001:db8::2 masquerade comment "icee6giejonei6so"
418+
add rule ip6 cni_hostport output fib daddr type local jump hostports_all
419+
add rule ip6 cni_hostport prerouting fib daddr type local jump hostports_all
420+
`)
421+
actualRules := strings.TrimSpace(ipv6Fake.Dump())
422+
Expect(actualRules).To(Equal(expectedRules))
423+
424+
// Check should pass with 1 masquerading rule
425+
err = pmNFT.checkPorts(conf, *containerNet6)
426+
Expect(err).NotTo(HaveOccurred())
427+
})
428+
429+
It(fmt.Sprintf("[%s] checkPorts validates correct rule count with masqAll", ver), func() {
430+
configTmpl := `{
431+
"name": "test",
432+
"type": "portmap",
433+
"cniVersion": "%s",
434+
"backend": "nftables",
435+
"runtimeConfig": {
436+
"portMappings": [
437+
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
438+
]
439+
},
440+
"snat": true,
441+
"masqAll": %t
442+
}`
443+
444+
// Test masqAll=true: expects 1 rule
445+
configBytes := []byte(fmt.Sprintf(configTmpl, ver, true))
446+
conf, _, err := parseConfig(configBytes, "foo")
447+
Expect(err).NotTo(HaveOccurred())
448+
conf.ContainerID = containerID
449+
450+
err = pmNFT.forwardPorts(conf, *containerNet4)
451+
Expect(err).NotTo(HaveOccurred())
452+
453+
// Should pass with 1 rule
454+
err = pmNFT.checkPorts(conf, *containerNet4)
455+
Expect(err).NotTo(HaveOccurred())
456+
457+
// Clear the fake nftables
458+
ipv4Fake = knftables.NewFake(knftables.IPv4Family, tableName)
459+
pmNFT.ipv4 = ipv4Fake
460+
461+
// Test masqAll=false: expects 2 rules
462+
configBytes = []byte(fmt.Sprintf(configTmpl, ver, false))
463+
conf, _, err = parseConfig(configBytes, "foo")
464+
Expect(err).NotTo(HaveOccurred())
465+
conf.ContainerID = containerID
466+
467+
err = pmNFT.forwardPorts(conf, *containerNet4)
468+
Expect(err).NotTo(HaveOccurred())
469+
470+
// Should pass with 2 rules
471+
err = pmNFT.checkPorts(conf, *containerNet4)
472+
Expect(err).NotTo(HaveOccurred())
473+
})
474+
})
227475
}
228476
})

0 commit comments

Comments
 (0)