// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package iptables import ( "errors" "fmt" "strconv" ) const ( // ProxyInboundChain is the chain to intercept inbound traffic. ProxyInboundChain = "CONSUL_PROXY_INBOUND" // ProxyInboundRedirectChain is the chain to redirect inbound traffic to the proxy. ProxyInboundRedirectChain = "CONSUL_PROXY_IN_REDIRECT" // ProxyOutputChain is the chain to intercept outbound traffic. ProxyOutputChain = "CONSUL_PROXY_OUTPUT" // ProxyOutputRedirectChain is the chain to redirect outbound traffic to the proxy ProxyOutputRedirectChain = "CONSUL_PROXY_REDIRECT" // DNSChain is the chain to redirect outbound DNS traffic to Consul DNS. DNSChain = "CONSUL_DNS_REDIRECT" DefaultTProxyOutboundPort = 15001 ) // Config is used to configure which traffic interception and redirection // rules should be applied with the iptables commands. type Config struct { // ConsulDNSIP is the IP for Consul DNS to direct DNS queries to. ConsulDNSIP string // ConsulDNSPort is the port for Consul DNS to direct DNS queries to. ConsulDNSPort int // ProxyUserID is the user ID of the proxy process. ProxyUserID string // ProxyInboundPort is the port of the proxy's inbound listener. ProxyInboundPort int // ProxyInboundPort is the port of the proxy's outbound listener. ProxyOutboundPort int // ExcludeInboundPorts is the list of ports that should be excluded // from inbound traffic redirection. ExcludeInboundPorts []string // ExcludeOutboundPorts is the list of ports that should be excluded // from outbound traffic redirection. ExcludeOutboundPorts []string // ExcludeOutboundCIDRs is the list of IP CIDRs that should be excluded // from outbound traffic redirection. ExcludeOutboundCIDRs []string // ExcludeUIDs is the list of additional user IDs to exclude // from traffic redirection. ExcludeUIDs []string // NetNS is the network namespace where the traffic redirection rules // should be applied. This must be a path to the network namespace, // e.g. /var/run/netns/foo. NetNS string // IptablesProvider is the Provider that will apply iptables rules. IptablesProvider Provider } // AdditionalRulesFn can be implemented by the caller to // add environment specific rules (like ECS) that needs to // be executed for traffic redirection to work properly. // // This gets called by the Setup function after all the // first class iptable rules are added. The implemented // function should only call the `AddRule` and optionally // the `Rules` method of the provider. type AdditionalRulesFn func(iptablesProvider Provider) // Provider is an interface for executing iptables rules. type Provider interface { // AddRule adds a rule without executing it. AddRule(name string, args ...string) // ApplyRules executes rules that have been added via AddRule. // This operation is currently not atomic, and if there's an error applying rules, // you may be left in a state where partial rules were applied. // ApplyRules should not be called twice on the same instance in order to avoid // duplicate rule application. ApplyRules() error // Rules returns the list of rules that have been added (including those not yet // applied). Rules() []string } // Setup will set up iptables interception and redirection rules // based on the configuration provided in cfg. func Setup(cfg Config) error { return SetupWithAdditionalRules(cfg, nil) } // SetupWithAdditionalRules will set up iptables interception and redirection rules // based on the configuration provided in cfg. The additionalRulesFn will be applied // after the normal set of rules. This implementation was inspired by // https://github.com/openservicemesh/osm/blob/650a1a1dcf081ae90825f3b5dba6f30a0e532725/pkg/injector/iptables.go func SetupWithAdditionalRules(cfg Config, additionalRulesFn AdditionalRulesFn) error { if cfg.IptablesProvider == nil { cfg.IptablesProvider = &iptablesExecutor{cfg: cfg} } err := validateConfig(cfg) if err != nil { return err } // Set the default outbound port if it's not already set. if cfg.ProxyOutboundPort == 0 { cfg.ProxyOutboundPort = DefaultTProxyOutboundPort } // Create chains we will use for redirection. chains := []string{ProxyInboundChain, ProxyInboundRedirectChain, ProxyOutputChain, ProxyOutputRedirectChain, DNSChain} for _, chain := range chains { cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-N", chain) } // Configure outbound rules. { // Redirects outbound TCP traffic hitting PROXY_REDIRECT chain to Envoy's outbound listener port. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", ProxyOutputRedirectChain, "-p", "tcp", "-j", "REDIRECT", "--to-port", strconv.Itoa(cfg.ProxyOutboundPort)) // The DNS rules are applied before the rules that directs all TCP traffic, so that the traffic going to port 53 goes through this rule first. if cfg.ConsulDNSIP != "" && cfg.ConsulDNSPort == 0 { // Traffic in the DNSChain is directed to the Consul DNS Service IP. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", DNSChain, "-p", "udp", "--dport", "53", "-j", "DNAT", "--to-destination", cfg.ConsulDNSIP) cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", DNSChain, "-p", "tcp", "--dport", "53", "-j", "DNAT", "--to-destination", cfg.ConsulDNSIP) // For outbound TCP and UDP traffic going to port 53 (DNS), jump to the DNSChain. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "udp", "--dport", "53", "-j", DNSChain) cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "tcp", "--dport", "53", "-j", DNSChain) } else if cfg.ConsulDNSPort != 0 { consulDNSIP := "127.0.0.1" if cfg.ConsulDNSIP != "" { consulDNSIP = cfg.ConsulDNSIP } consulDNSHostPort := fmt.Sprintf("%s:%d", consulDNSIP, cfg.ConsulDNSPort) // Traffic in the DNSChain is directed to the Consul DNS Service IP. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", DNSChain, "-p", "udp", "-d", consulDNSIP, "--dport", "53", "-j", "DNAT", "--to-destination", consulDNSHostPort) cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", DNSChain, "-p", "tcp", "-d", consulDNSIP, "--dport", "53", "-j", "DNAT", "--to-destination", consulDNSHostPort) // For outbound TCP and UDP traffic going to port 53 (DNS), jump to the DNSChain. Only redirect traffic that's going to consul's DNS IP. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "udp", "-d", consulDNSIP, "--dport", "53", "-j", DNSChain) cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "tcp", "-d", consulDNSIP, "--dport", "53", "-j", DNSChain) } // For outbound TCP traffic jump from OUTPUT chain to PROXY_OUTPUT chain. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "tcp", "-j", ProxyOutputChain) // Don't redirect proxy traffic back to itself, return it to the next chain for processing. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", ProxyOutputChain, "-m", "owner", "--uid-owner", cfg.ProxyUserID, "-j", "RETURN") // Skip localhost traffic, doesn't need to be routed via the proxy. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", ProxyOutputChain, "-d", "127.0.0.1/32", "-j", "RETURN") // Redirect remaining outbound traffic to Envoy. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", ProxyOutputChain, "-j", ProxyOutputRedirectChain) // We are using "insert" (-I) instead of "append" (-A) so the provided rules take precedence over default ones. for _, outboundPort := range cfg.ExcludeOutboundPorts { cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-I", ProxyOutputChain, "-p", "tcp", "--dport", outboundPort, "-j", "RETURN") } for _, outboundIP := range cfg.ExcludeOutboundCIDRs { cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-I", ProxyOutputChain, "-d", outboundIP, "-j", "RETURN") } for _, uid := range cfg.ExcludeUIDs { cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-I", ProxyOutputChain, "-m", "owner", "--uid-owner", uid, "-j", "RETURN") } } // Configure inbound rules. { // Redirects inbound TCP traffic hitting the PROXY_IN_REDIRECT chain to Envoy's inbound listener port. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", ProxyInboundRedirectChain, "-p", "tcp", "-j", "REDIRECT", "--to-port", strconv.Itoa(cfg.ProxyInboundPort)) // For inbound traffic jump from PREROUTING chain to PROXY_INBOUND chain. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", "PREROUTING", "-p", "tcp", "-j", ProxyInboundChain) // Redirect remaining inbound traffic to Envoy. cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-A", ProxyInboundChain, "-p", "tcp", "-j", ProxyInboundRedirectChain) for _, inboundPort := range cfg.ExcludeInboundPorts { cfg.IptablesProvider.AddRule("iptables", "-t", "nat", "-I", ProxyInboundChain, "-p", "tcp", "--dport", inboundPort, "-j", "RETURN") } } // Call function to add any additional rules passed on by the caller if additionalRulesFn != nil { additionalRulesFn(cfg.IptablesProvider) } return cfg.IptablesProvider.ApplyRules() } func validateConfig(cfg Config) error { if cfg.ProxyUserID == "" { return errors.New("ProxyUserID is required to set up traffic redirection") } if cfg.ProxyInboundPort == 0 { return errors.New("ProxyInboundPort is required to set up traffic redirection") } return nil }