| // Copyright 2017 CNI authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // This is a post-setup plugin that establishes port forwarding - using iptables, |
| // from the host's network interface(s) to a pod's network interface. |
| // |
| // It is intended to be used as a chained CNI plugin, and determines the container |
| // IP from the previous result. If the result includes an IPv6 address, it will |
| // also be configured. (IPTables will not forward cross-family). |
| // |
| // This has one notable limitation: it does not perform any kind of reservation |
| // of the actual host port. If there is a service on the host, it will have all |
| // its traffic captured by the container. If another container also claims a given |
| // port, it will caputure the traffic - it is last-write-wins. |
| package main |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "log" |
| "net" |
| |
| "golang.org/x/sys/unix" |
| |
| "github.com/containernetworking/cni/pkg/skel" |
| "github.com/containernetworking/cni/pkg/types" |
| current "github.com/containernetworking/cni/pkg/types/100" |
| "github.com/containernetworking/cni/pkg/version" |
| "github.com/containernetworking/plugins/pkg/utils" |
| bv "github.com/containernetworking/plugins/pkg/utils/buildversion" |
| ) |
| |
| type PortMapper interface { |
| forwardPorts(config *PortMapConf, containerNet net.IPNet) error |
| checkPorts(config *PortMapConf, containerNet net.IPNet) error |
| unforwardPorts(config *PortMapConf) error |
| } |
| |
| // These are vars rather than consts so we can "&" them |
| var ( |
| iptablesBackend = "iptables" |
| nftablesBackend = "nftables" |
| ) |
| |
| // PortMapEntry corresponds to a single entry in the port_mappings argument, |
| // see CONVENTIONS.md |
| type PortMapEntry struct { |
| HostPort int `json:"hostPort"` |
| ContainerPort int `json:"containerPort"` |
| Protocol string `json:"protocol"` |
| HostIP string `json:"hostIP,omitempty"` |
| } |
| |
| type PortMapConf struct { |
| types.NetConf |
| |
| mapper PortMapper |
| |
| // Generic config |
| Backend *string `json:"backend,omitempty"` |
| SNAT *bool `json:"snat,omitempty"` |
| ConditionsV4 *[]string `json:"conditionsV4"` |
| ConditionsV6 *[]string `json:"conditionsV6"` |
| MasqAll bool `json:"masqAll,omitempty"` |
| MarkMasqBit *int `json:"markMasqBit"` |
| RuntimeConfig struct { |
| PortMaps []PortMapEntry `json:"portMappings,omitempty"` |
| } `json:"runtimeConfig,omitempty"` |
| |
| // iptables-backend-specific config |
| ExternalSetMarkChain *string `json:"externalSetMarkChain"` |
| |
| // These are fields parsed out of the config or the environment; |
| // included here for convenience |
| ContainerID string `json:"-"` |
| ContIPv4 net.IPNet `json:"-"` |
| ContIPv6 net.IPNet `json:"-"` |
| } |
| |
| // The default mark bit to signal that masquerading is required |
| // Kubernetes uses 14 and 15, Calico uses 20-31. |
| const DefaultMarkBit = 13 |
| |
| func cmdAdd(args *skel.CmdArgs) error { |
| netConf, _, err := parseConfig(args.StdinData, args.IfName) |
| if err != nil { |
| return fmt.Errorf("failed to parse config: %v", err) |
| } |
| |
| if netConf.PrevResult == nil { |
| return fmt.Errorf("must be called as chained plugin") |
| } |
| |
| if len(netConf.RuntimeConfig.PortMaps) == 0 { |
| return types.PrintResult(netConf.PrevResult, netConf.CNIVersion) |
| } |
| |
| netConf.ContainerID = args.ContainerID |
| |
| if netConf.ContIPv4.IP != nil { |
| if err := netConf.mapper.forwardPorts(netConf, netConf.ContIPv4); err != nil { |
| return err |
| } |
| // Delete conntrack entries for UDP to avoid conntrack blackholing traffic |
| // due to stale connections. We do that after the iptables rules are set, so |
| // the new traffic uses them. Failures are informative only. |
| if err := deletePortmapStaleConnections(netConf.RuntimeConfig.PortMaps, unix.AF_INET); err != nil { |
| log.Printf("failed to delete stale UDP conntrack entries for %s: %v", netConf.ContIPv4.IP, err) |
| } |
| |
| if *netConf.SNAT { |
| // Set the route_localnet bit on the host interface, so that |
| // 127/8 can cross a routing boundary. |
| hostIfName := getRoutableHostIF(netConf.ContIPv4.IP) |
| if hostIfName != "" { |
| if err := enableLocalnetRouting(hostIfName); err != nil { |
| return fmt.Errorf("unable to enable route_localnet: %v", err) |
| } |
| } |
| } |
| } |
| |
| if netConf.ContIPv6.IP != nil { |
| if err := netConf.mapper.forwardPorts(netConf, netConf.ContIPv6); err != nil { |
| return err |
| } |
| // Delete conntrack entries for UDP to avoid conntrack blackholing traffic |
| // due to stale connections. We do that after the iptables rules are set, so |
| // the new traffic uses them. Failures are informative only. |
| if err := deletePortmapStaleConnections(netConf.RuntimeConfig.PortMaps, unix.AF_INET6); err != nil { |
| log.Printf("failed to delete stale UDP conntrack entries for %s: %v", netConf.ContIPv6.IP, err) |
| } |
| } |
| |
| // Pass through the previous result |
| return types.PrintResult(netConf.PrevResult, netConf.CNIVersion) |
| } |
| |
| func cmdDel(args *skel.CmdArgs) error { |
| netConf, _, err := parseConfig(args.StdinData, args.IfName) |
| if err != nil { |
| return fmt.Errorf("failed to parse config: %v", err) |
| } |
| |
| if len(netConf.RuntimeConfig.PortMaps) == 0 { |
| return nil |
| } |
| |
| netConf.ContainerID = args.ContainerID |
| |
| // We don't need to parse out whether or not we're using v6 or snat, |
| // deletion is idempotent |
| return netConf.mapper.unforwardPorts(netConf) |
| } |
| |
| func main() { |
| skel.PluginMainFuncs(skel.CNIFuncs{ |
| Add: cmdAdd, |
| Check: cmdCheck, |
| Del: cmdDel, |
| /* FIXME GC */ |
| /* FIXME Status */ |
| }, version.All, bv.BuildString("portmap")) |
| } |
| |
| func cmdCheck(args *skel.CmdArgs) error { |
| conf, result, err := parseConfig(args.StdinData, args.IfName) |
| if err != nil { |
| return err |
| } |
| |
| // Ensure we have previous result. |
| if result == nil { |
| return fmt.Errorf("Required prevResult missing") |
| } |
| |
| if len(conf.RuntimeConfig.PortMaps) == 0 { |
| return nil |
| } |
| |
| conf.ContainerID = args.ContainerID |
| |
| if conf.ContIPv4.IP != nil { |
| if err := conf.mapper.checkPorts(conf, conf.ContIPv4); err != nil { |
| return err |
| } |
| } |
| |
| if conf.ContIPv6.IP != nil { |
| if err := conf.mapper.checkPorts(conf, conf.ContIPv6); err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| // parseConfig parses the supplied configuration (and prevResult) from stdin. |
| func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, error) { |
| conf := PortMapConf{} |
| |
| if err := json.Unmarshal(stdin, &conf); err != nil { |
| return nil, nil, fmt.Errorf("failed to parse network configuration: %v", err) |
| } |
| |
| // Parse previous result. |
| var result *current.Result |
| if conf.RawPrevResult != nil { |
| var err error |
| if err = version.ParsePrevResult(&conf.NetConf); err != nil { |
| return nil, nil, fmt.Errorf("could not parse prevResult: %v", err) |
| } |
| |
| result, err = current.NewResultFromResult(conf.PrevResult) |
| if err != nil { |
| return nil, nil, fmt.Errorf("could not convert result to current version: %v", err) |
| } |
| } |
| |
| conf.mapper = &portMapperIPTables{} |
| |
| if conf.SNAT == nil { |
| tvar := true |
| conf.SNAT = &tvar |
| } |
| |
| if conf.MarkMasqBit != nil && conf.ExternalSetMarkChain != nil { |
| return nil, nil, fmt.Errorf("Cannot specify externalSetMarkChain and markMasqBit") |
| } |
| |
| if conf.MarkMasqBit == nil { |
| bvar := DefaultMarkBit // go constants are "special" |
| conf.MarkMasqBit = &bvar |
| } |
| |
| if *conf.MarkMasqBit < 0 || *conf.MarkMasqBit > 31 { |
| return nil, nil, fmt.Errorf("MasqMarkBit must be between 0 and 31") |
| } |
| |
| err := ensureBackend(&conf) |
| if err != nil { |
| return nil, nil, err |
| } |
| switch *conf.Backend { |
| case iptablesBackend: |
| conf.mapper = &portMapperIPTables{} |
| |
| case nftablesBackend: |
| conf.mapper = &portMapperNFTables{} |
| |
| default: |
| return nil, nil, fmt.Errorf("unrecognized backend %q", *conf.Backend) |
| } |
| |
| // Reject invalid port numbers |
| for _, pm := range conf.RuntimeConfig.PortMaps { |
| if pm.ContainerPort <= 0 { |
| return nil, nil, fmt.Errorf("Invalid container port number: %d", pm.ContainerPort) |
| } |
| if pm.HostPort <= 0 { |
| return nil, nil, fmt.Errorf("Invalid host port number: %d", pm.HostPort) |
| } |
| } |
| |
| if conf.PrevResult != nil { |
| for _, ip := range result.IPs { |
| isIPv4 := ip.Address.IP.To4() != nil |
| if !isIPv4 && conf.ContIPv6.IP != nil { |
| continue |
| } else if isIPv4 && conf.ContIPv4.IP != nil { |
| continue |
| } |
| |
| // Skip known non-sandbox interfaces |
| if ip.Interface != nil { |
| intIdx := *ip.Interface |
| if intIdx >= 0 && |
| intIdx < len(result.Interfaces) && |
| (result.Interfaces[intIdx].Name != ifName || |
| result.Interfaces[intIdx].Sandbox == "") { |
| continue |
| } |
| } |
| if ip.Address.IP.To4() != nil { |
| conf.ContIPv4 = ip.Address |
| } else { |
| conf.ContIPv6 = ip.Address |
| } |
| } |
| } |
| |
| return &conf, result, nil |
| } |
| |
| // ensureBackend validates and/or sets conf.Backend |
| func ensureBackend(conf *PortMapConf) error { |
| backendConfig := make(map[string][]string) |
| |
| if conf.ExternalSetMarkChain != nil { |
| backendConfig[iptablesBackend] = append(backendConfig[iptablesBackend], "externalSetMarkChain") |
| } |
| if conditionsBackend := detectBackendOfConditions(conf.ConditionsV4); conditionsBackend != "" { |
| backendConfig[conditionsBackend] = append(backendConfig[conditionsBackend], "conditionsV4") |
| } |
| if conditionsBackend := detectBackendOfConditions(conf.ConditionsV6); conditionsBackend != "" { |
| backendConfig[conditionsBackend] = append(backendConfig[conditionsBackend], "conditionsV6") |
| } |
| |
| // If backend wasn't requested explicitly, default to iptables, unless it is not |
| // available (and nftables is). FIXME: flip this default at some point. |
| if conf.Backend == nil { |
| if !utils.SupportsIPTables() && utils.SupportsNFTables() { |
| conf.Backend = &nftablesBackend |
| } else { |
| conf.Backend = &iptablesBackend |
| } |
| } |
| |
| // Make sure we dont have config for the wrong backend |
| var wrongBackend string |
| if *conf.Backend == iptablesBackend { |
| wrongBackend = nftablesBackend |
| } else { |
| wrongBackend = iptablesBackend |
| } |
| if len(backendConfig[wrongBackend]) > 0 { |
| return fmt.Errorf("%s backend was requested but configuration contains %s-specific options %v", *conf.Backend, wrongBackend, backendConfig[wrongBackend]) |
| } |
| |
| // OK |
| return nil |
| } |
| |
| // detectBackendOfConditions returns "iptables" if conditions contains iptables |
| // conditions, "nftables" if it contains nftables conditions, and "" if it is empty. |
| func detectBackendOfConditions(conditions *[]string) string { |
| if conditions == nil || len(*conditions) == 0 || (*conditions)[0] == "" { |
| return "" |
| } |
| |
| // The first character of any iptables condition would either be an hyphen |
| // (e.g. "-d", "--sport", "-m") or an exclamation mark. |
| // No nftables condition would start that way. (An nftables condition might |
| // include a negative number, but not as the first token.) |
| if (*conditions)[0][0] == '-' || (*conditions)[0][0] == '!' { |
| return iptablesBackend |
| } |
| return nftablesBackend |
| } |