Merge pull request #469 from AlbanBedel/portmap-hairpin-subnet

portmap: Apply the DNAT hairpin to the whole subnet
This commit is contained in:
Piotr Skamruk 2020-04-22 17:22:14 +02:00 committed by GitHub
commit 5af9ff493e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 48 additions and 41 deletions

View File

@ -72,7 +72,7 @@ will masquerade traffic as needed.
The DNAT rule rewrites the destination port and address of new connections. The DNAT rule rewrites the destination port and address of new connections.
There is a top-level chain, `CNI-HOSTPORT-DNAT` which is always created and There is a top-level chain, `CNI-HOSTPORT-DNAT` which is always created and
never deleted. Each plugin execution creates an additional chain for ease never deleted. Each plugin execution creates an additional chain for ease
of cleanup. So, if a single container exists on IP 172.16.30.2 with ports of cleanup. So, if a single container exists on IP 172.16.30.2/24 with ports
8080 and 8043 on the host forwarded to ports 80 and 443 in the container, the 8080 and 8043 on the host forwarded to ports 80 and 443 in the container, the
rules look like this: rules look like this:
@ -86,10 +86,10 @@ rules look like this:
- `-j MARK --set-xmark 0x2000/0x2000` - `-j MARK --set-xmark 0x2000/0x2000`
`CNI-DN-xxxxxx` chain: `CNI-DN-xxxxxx` chain:
- `-p tcp -s 172.16.30.2 --dport 8080 -j CNI-HOSTPORT-SETMARK` (masquerade hairpin traffic) - `-p tcp -s 172.16.30.0/24 --dport 8080 -j CNI-HOSTPORT-SETMARK` (masquerade hairpin traffic)
- `-p tcp -s 127.0.0.1 --dport 8080 -j CNI-HOSTPORT-SETMARK` (masquerade localhost traffic) - `-p tcp -s 127.0.0.1 --dport 8080 -j CNI-HOSTPORT-SETMARK` (masquerade localhost traffic)
- `-p tcp --dport 8080 -j DNAT --to-destination 172.16.30.2:80` (rewrite destination) - `-p tcp --dport 8080 -j DNAT --to-destination 172.16.30.2:80` (rewrite destination)
- `-p tcp -s 172.16.30.2 --dport 8043 -j CNI-HOSTPORT-SETMARK` - `-p tcp -s 172.16.30.0/24 --dport 8043 -j CNI-HOSTPORT-SETMARK`
- `-p tcp -s 127.0.0.1 --dport 8043 -j CNI-HOSTPORT-SETMARK` - `-p tcp -s 127.0.0.1 --dport 8043 -j CNI-HOSTPORT-SETMARK`
- `-p tcp --dport 8043 -j DNAT --to-destination 172.16.30.2:443` - `-p tcp --dport 8043 -j DNAT --to-destination 172.16.30.2:443`

View File

@ -61,8 +61,8 @@ type PortMapConf struct {
// These are fields parsed out of the config or the environment; // These are fields parsed out of the config or the environment;
// included here for convenience // included here for convenience
ContainerID string `json:"-"` ContainerID string `json:"-"`
ContIPv4 net.IP `json:"-"` ContIPv4 net.IPNet `json:"-"`
ContIPv6 net.IP `json:"-"` ContIPv6 net.IPNet `json:"-"`
} }
// The default mark bit to signal that masquerading is required // The default mark bit to signal that masquerading is required
@ -85,13 +85,13 @@ func cmdAdd(args *skel.CmdArgs) error {
netConf.ContainerID = args.ContainerID netConf.ContainerID = args.ContainerID
if netConf.ContIPv4 != nil { if netConf.ContIPv4.IP != nil {
if err := forwardPorts(netConf, netConf.ContIPv4); err != nil { if err := forwardPorts(netConf, netConf.ContIPv4); err != nil {
return err return err
} }
} }
if netConf.ContIPv6 != nil { if netConf.ContIPv6.IP != nil {
if err := forwardPorts(netConf, netConf.ContIPv6); err != nil { if err := forwardPorts(netConf, netConf.ContIPv6); err != nil {
return err return err
} }
@ -138,13 +138,13 @@ func cmdCheck(args *skel.CmdArgs) error {
conf.ContainerID = args.ContainerID conf.ContainerID = args.ContainerID
if conf.ContIPv4 != nil { if conf.ContIPv4.IP != nil {
if err := checkPorts(conf, conf.ContIPv4); err != nil { if err := checkPorts(conf, conf.ContIPv4); err != nil {
return err return err
} }
} }
if conf.ContIPv6 != nil { if conf.ContIPv6.IP != nil {
if err := checkPorts(conf, conf.ContIPv6); err != nil { if err := checkPorts(conf, conf.ContIPv6); err != nil {
return err return err
} }
@ -205,9 +205,9 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er
if conf.PrevResult != nil { if conf.PrevResult != nil {
for _, ip := range result.IPs { for _, ip := range result.IPs {
if ip.Version == "6" && conf.ContIPv6 != nil { if ip.Version == "6" && conf.ContIPv6.IP != nil {
continue continue
} else if ip.Version == "4" && conf.ContIPv4 != nil { } else if ip.Version == "4" && conf.ContIPv4.IP != nil {
continue continue
} }
@ -223,9 +223,9 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er
} }
switch ip.Version { switch ip.Version {
case "6": case "6":
conf.ContIPv6 = ip.Address.IP conf.ContIPv6 = ip.Address
case "4": case "4":
conf.ContIPv4 = ip.Address.IP conf.ContIPv4 = ip.Address
} }
} }
} }

View File

@ -48,9 +48,9 @@ const MarkMasqChainName = "CNI-HOSTPORT-MASQ"
const OldTopLevelSNATChainName = "CNI-HOSTPORT-SNAT" const OldTopLevelSNATChainName = "CNI-HOSTPORT-SNAT"
// forwardPorts establishes port forwarding to a given container IP. // forwardPorts establishes port forwarding to a given container IP.
// containerIP can be either v4 or v6. // containerNet.IP can be either v4 or v6.
func forwardPorts(config *PortMapConf, containerIP net.IP) error { func forwardPorts(config *PortMapConf, containerNet net.IPNet) error {
isV6 := (containerIP.To4() == nil) isV6 := (containerNet.IP.To4() == nil)
var ipt *iptables.IPTables var ipt *iptables.IPTables
var err error var err error
@ -86,7 +86,7 @@ func forwardPorts(config *PortMapConf, containerIP net.IP) error {
if !isV6 { if !isV6 {
// Set the route_localnet bit on the host interface, so that // Set the route_localnet bit on the host interface, so that
// 127/8 can cross a routing boundary. // 127/8 can cross a routing boundary.
hostIfName := getRoutableHostIF(containerIP) hostIfName := getRoutableHostIF(containerNet.IP)
if hostIfName != "" { if hostIfName != "" {
if err := enableLocalnetRouting(hostIfName); err != nil { if err := enableLocalnetRouting(hostIfName); err != nil {
return fmt.Errorf("unable to enable route_localnet: %v", err) return fmt.Errorf("unable to enable route_localnet: %v", err)
@ -104,7 +104,7 @@ func forwardPorts(config *PortMapConf, containerIP net.IP) error {
dnatChain := genDnatChain(config.Name, config.ContainerID) dnatChain := genDnatChain(config.Name, config.ContainerID)
// First, idempotently tear down this chain in case there was some // First, idempotently tear down this chain in case there was some
// sort of collision or bad state. // sort of collision or bad state.
fillDnatRules(&dnatChain, config, containerIP) fillDnatRules(&dnatChain, config, containerNet)
if err := dnatChain.setup(ipt); err != nil { if err := dnatChain.setup(ipt); err != nil {
return fmt.Errorf("unable to setup DNAT: %v", err) return fmt.Errorf("unable to setup DNAT: %v", err)
} }
@ -112,10 +112,10 @@ func forwardPorts(config *PortMapConf, containerIP net.IP) error {
return nil return nil
} }
func checkPorts(config *PortMapConf, containerIP net.IP) error { func checkPorts(config *PortMapConf, containerNet net.IPNet) error {
dnatChain := genDnatChain(config.Name, config.ContainerID) dnatChain := genDnatChain(config.Name, config.ContainerID)
fillDnatRules(&dnatChain, config, containerIP) fillDnatRules(&dnatChain, config, containerNet)
ip4t := maybeGetIptables(false) ip4t := maybeGetIptables(false)
ip6t := maybeGetIptables(true) ip6t := maybeGetIptables(true)
@ -180,8 +180,8 @@ func genDnatChain(netName, containerID string) chain {
// dnatRules generates the destination NAT rules, one per port, to direct // dnatRules generates the destination NAT rules, one per port, to direct
// traffic from hostip:hostport to podip:podport // traffic from hostip:hostport to podip:podport
func fillDnatRules(c *chain, config *PortMapConf, containerIP net.IP) { func fillDnatRules(c *chain, config *PortMapConf, containerNet net.IPNet) {
isV6 := (containerIP.To4() == nil) isV6 := (containerNet.IP.To4() == nil)
comment := trimComment(fmt.Sprintf(`dnat name: "%s" id: "%s"`, config.Name, config.ContainerID)) comment := trimComment(fmt.Sprintf(`dnat name: "%s" id: "%s"`, config.Name, config.ContainerID))
entries := config.RuntimeConfig.PortMaps entries := config.RuntimeConfig.PortMaps
setMarkChainName := SetMarkChainName setMarkChainName := SetMarkChainName
@ -249,7 +249,7 @@ func fillDnatRules(c *chain, config *PortMapConf, containerIP net.IP) {
copy(hpRule, ruleBase) copy(hpRule, ruleBase)
hpRule = append(hpRule, hpRule = append(hpRule,
"-s", containerIP.String(), "-s", containerNet.String(),
"-j", setMarkChainName, "-j", setMarkChainName,
) )
c.rules = append(c.rules, hpRule) c.rules = append(c.rules, hpRule)
@ -272,7 +272,7 @@ func fillDnatRules(c *chain, config *PortMapConf, containerIP net.IP) {
copy(dnatRule, ruleBase) copy(dnatRule, ruleBase)
dnatRule = append(dnatRule, dnatRule = append(dnatRule,
"-j", "DNAT", "-j", "DNAT",
"--to-destination", fmtIpPort(containerIP, entry.ContainerPort), "--to-destination", fmtIpPort(containerNet.IP, entry.ContainerPort),
) )
c.rules = append(c.rules, dnatRule) c.rules = append(c.rules, dnatRule)
} }

View File

@ -16,7 +16,8 @@ package main
import ( import (
"fmt" "fmt"
"net"
"github.com/containernetworking/cni/pkg/types"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
@ -77,8 +78,10 @@ var _ = Describe("portmapping configuration", func() {
Expect(c.SNAT).To(Equal(&fvar)) Expect(c.SNAT).To(Equal(&fvar))
Expect(c.Name).To(Equal("test")) Expect(c.Name).To(Equal("test"))
Expect(c.ContIPv4).To(Equal(net.ParseIP("10.0.0.2"))) n, err := types.ParseCIDR("10.0.0.2/24")
Expect(c.ContIPv6).To(Equal(net.ParseIP("2001:db8:1::2"))) Expect(c.ContIPv4).To(Equal(*n))
n, err = types.ParseCIDR("2001:db8:1::2/64")
Expect(c.ContIPv6).To(Equal(*n))
}) })
It("Correctly parses a DEL config", func() { It("Correctly parses a DEL config", func() {
@ -186,7 +189,8 @@ var _ = Describe("portmapping configuration", func() {
entryChains: []string{"CNI-HOSTPORT-DNAT"}, entryChains: []string{"CNI-HOSTPORT-DNAT"},
})) }))
fillDnatRules(&ch, conf, net.ParseIP("10.0.0.2")) n, err := types.ParseCIDR("10.0.0.2/24")
fillDnatRules(&ch, conf, *n)
Expect(ch.entryRules).To(Equal([][]string{ Expect(ch.entryRules).To(Equal([][]string{
{"-m", "comment", "--comment", {"-m", "comment", "--comment",
@ -204,16 +208,16 @@ var _ = Describe("portmapping configuration", func() {
})) }))
Expect(ch.rules).To(Equal([][]string{ Expect(ch.rules).To(Equal([][]string{
{"-p", "tcp", "--dport", "8080", "-s", "10.0.0.2", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "tcp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "tcp", "--dport", "8080", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"}, {"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "tcp", "--dport", "8081", "-s", "10.0.0.2", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "tcp", "--dport", "8081", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "tcp", "--dport", "8081", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80"}, {"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "udp", "--dport", "8080", "-s", "10.0.0.2", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "udp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "udp", "--dport", "8080", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:81"}, {"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:81"},
{"-p", "udp", "--dport", "8082", "-s", "10.0.0.2", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "udp", "--dport", "8082", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "udp", "--dport", "8082", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "10.0.0.2:82"}, {"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "10.0.0.2:82"},
})) }))
@ -221,16 +225,17 @@ var _ = Describe("portmapping configuration", func() {
ch.rules = nil ch.rules = nil
ch.entryRules = nil ch.entryRules = nil
fillDnatRules(&ch, conf, net.ParseIP("2001:db8::2")) n, err = types.ParseCIDR("2001:db8::2/64")
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{ Expect(ch.rules).To(Equal([][]string{
{"-p", "tcp", "--dport", "8080", "-s", "2001:db8::2", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "tcp", "--dport", "8080", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "[2001:db8::2]:80"}, {"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "[2001:db8::2]:80"},
{"-p", "tcp", "--dport", "8081", "-s", "2001:db8::2", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "tcp", "--dport", "8081", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "[2001:db8::2]:80"}, {"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "[2001:db8::2]:80"},
{"-p", "udp", "--dport", "8080", "-s", "2001:db8::2", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "udp", "--dport", "8080", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "[2001:db8::2]:81"}, {"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "[2001:db8::2]:81"},
{"-p", "udp", "--dport", "8082", "-s", "2001:db8::2", "-j", "CNI-HOSTPORT-SETMARK"}, {"-p", "udp", "--dport", "8082", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "[2001:db8::2]:82"}, {"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "[2001:db8::2]:82"},
})) }))
@ -240,7 +245,8 @@ var _ = Describe("portmapping configuration", func() {
fvar := false fvar := false
conf.SNAT = &fvar conf.SNAT = &fvar
fillDnatRules(&ch, conf, net.ParseIP("10.0.0.2")) n, err = types.ParseCIDR("10.0.0.2/24")
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{ Expect(ch.rules).To(Equal([][]string{
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"}, {"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80"}, {"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
@ -276,9 +282,10 @@ var _ = Describe("portmapping configuration", func() {
conf.ContainerID = containerID conf.ContainerID = containerID
ch = genDnatChain(conf.Name, containerID) ch = genDnatChain(conf.Name, containerID)
fillDnatRules(&ch, conf, net.ParseIP("10.0.0.2")) n, err := types.ParseCIDR("10.0.0.2/24")
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{ Expect(ch.rules).To(Equal([][]string{
{"-p", "tcp", "--dport", "8080", "-s", "10.0.0.2", "-j", "PLZ-SET-MARK"}, {"-p", "tcp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "PLZ-SET-MARK"},
{"-p", "tcp", "--dport", "8080", "-s", "127.0.0.1", "-j", "PLZ-SET-MARK"}, {"-p", "tcp", "--dport", "8080", "-s", "127.0.0.1", "-j", "PLZ-SET-MARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"}, {"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
})) }))