From 5755c97bc7ebbd2c584dbae52d7e006821306720 Mon Sep 17 00:00:00 2001 From: Iryna Shustava Date: Fri, 9 Apr 2021 11:48:10 -0700 Subject: [PATCH] cli: Add new `consul connect redirect-traffic` command for applying traffic redirection rules when Transparent Proxy is enabled. (#9910) * Add new consul connect redirect-traffic command for applying traffic redirection rules when Transparent Proxy is enabled. * Add new iptables package for applying traffic redirection rules with iptables. --- .changelog/9910.txt | 6 + agent/xds/listeners.go | 9 +- build-support/docker/Consul-Dev.dockerfile | 1 + command/commands_oss.go | 6 +- .../redirecttraffic/redirect_traffic.go | 168 +++++++++++ .../redirecttraffic/redirect_traffic_test.go | 280 ++++++++++++++++++ sdk/go.mod | 2 +- sdk/iptables/iptables.go | 120 ++++++++ sdk/iptables/iptables_executor_linux.go | 46 +++ sdk/iptables/iptables_executor_unsupported.go | 18 ++ sdk/iptables/iptables_test.go | 123 ++++++++ website/content/commands/connect/envoy.mdx | 7 +- website/content/commands/connect/index.mdx | 9 +- .../commands/connect/redirect-traffic.mdx | 73 +++++ 14 files changed, 852 insertions(+), 16 deletions(-) create mode 100644 .changelog/9910.txt create mode 100644 command/connect/redirecttraffic/redirect_traffic.go create mode 100644 command/connect/redirecttraffic/redirect_traffic_test.go create mode 100644 sdk/iptables/iptables.go create mode 100644 sdk/iptables/iptables_executor_linux.go create mode 100644 sdk/iptables/iptables_executor_unsupported.go create mode 100644 sdk/iptables/iptables_test.go create mode 100644 website/content/commands/connect/redirect-traffic.mdx diff --git a/.changelog/9910.txt b/.changelog/9910.txt new file mode 100644 index 0000000000..4e3f6ceba1 --- /dev/null +++ b/.changelog/9910.txt @@ -0,0 +1,6 @@ +```release-note:feature +cli: Add new `consul connect redirect-traffic` command for applying traffic redirection rules when Transparent Proxy is enabled. +``` +```release-note:feature +sdk: Add new `iptables` package for applying traffic redirection rules with iptables. +``` diff --git a/agent/xds/listeners.go b/agent/xds/listeners.go index 6ec12b8d63..a8ea5f5c8c 100644 --- a/agent/xds/listeners.go +++ b/agent/xds/listeners.go @@ -18,6 +18,7 @@ import ( envoy_tcp_proxy_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/hashicorp/consul/sdk/iptables" "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" @@ -32,11 +33,6 @@ import ( "github.com/hashicorp/consul/logging" ) -const ( - // TODO (freddy) Make this configurable - TProxyOutboundPort = 15001 -) - // listenersFromSnapshot returns the xDS API representation of the "listeners" in the snapshot. func (s *Server) listenersFromSnapshot(cInfo connectionInfo, cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) { if cfgSnap == nil { @@ -75,7 +71,8 @@ func (s *Server) listenersFromSnapshotConnectProxy(cInfo connectionInfo, cfgSnap var outboundListener *envoy_listener_v3.Listener if cfgSnap.Proxy.TransparentProxy { - outboundListener = makeListener(OutboundListenerName, "127.0.0.1", TProxyOutboundPort, envoy_core_v3.TrafficDirection_OUTBOUND) + // TODO (freddy) Make DefaultTProxyOutboundPort configurable + outboundListener = makeListener(OutboundListenerName, "127.0.0.1", iptables.DefaultTProxyOutboundPort, envoy_core_v3.TrafficDirection_OUTBOUND) outboundListener.FilterChains = make([]*envoy_listener_v3.FilterChain, 0) outboundListener.ListenerFilters = []*envoy_listener_v3.ListenerFilter{ { diff --git a/build-support/docker/Consul-Dev.dockerfile b/build-support/docker/Consul-Dev.dockerfile index 2ecddb8c1b..ea4723a02c 100644 --- a/build-support/docker/Consul-Dev.dockerfile +++ b/build-support/docker/Consul-Dev.dockerfile @@ -1,3 +1,4 @@ ARG CONSUL_IMAGE_VERSION=latest FROM consul:${CONSUL_IMAGE_VERSION} +RUN apk update && apk add iptables COPY consul /bin/consul diff --git a/command/commands_oss.go b/command/commands_oss.go index 781f744124..facdb51193 100644 --- a/command/commands_oss.go +++ b/command/commands_oss.go @@ -54,6 +54,7 @@ import ( pipebootstrap "github.com/hashicorp/consul/command/connect/envoy/pipe-bootstrap" "github.com/hashicorp/consul/command/connect/expose" "github.com/hashicorp/consul/command/connect/proxy" + "github.com/hashicorp/consul/command/connect/redirecttraffic" "github.com/hashicorp/consul/command/debug" "github.com/hashicorp/consul/command/event" "github.com/hashicorp/consul/command/exec" @@ -77,8 +78,8 @@ import ( kvput "github.com/hashicorp/consul/command/kv/put" "github.com/hashicorp/consul/command/leave" "github.com/hashicorp/consul/command/lock" - login "github.com/hashicorp/consul/command/login" - logout "github.com/hashicorp/consul/command/logout" + "github.com/hashicorp/consul/command/login" + "github.com/hashicorp/consul/command/logout" "github.com/hashicorp/consul/command/maint" "github.com/hashicorp/consul/command/members" "github.com/hashicorp/consul/command/monitor" @@ -173,6 +174,7 @@ func init() { Register("connect envoy", func(ui cli.Ui) (cli.Command, error) { return envoy.New(ui), nil }) Register("connect envoy pipe-bootstrap", func(ui cli.Ui) (cli.Command, error) { return pipebootstrap.New(ui), nil }) Register("connect expose", func(ui cli.Ui) (cli.Command, error) { return expose.New(ui), nil }) + Register("connect redirect-traffic", func(ui cli.Ui) (cli.Command, error) { return redirecttraffic.New(ui), nil }) Register("debug", func(ui cli.Ui) (cli.Command, error) { return debug.New(ui, MakeShutdownCh()), nil }) Register("event", func(ui cli.Ui) (cli.Command, error) { return event.New(ui), nil }) Register("exec", func(ui cli.Ui) (cli.Command, error) { return exec.New(ui, MakeShutdownCh()), nil }) diff --git a/command/connect/redirecttraffic/redirect_traffic.go b/command/connect/redirecttraffic/redirect_traffic.go new file mode 100644 index 0000000000..6156a55cdc --- /dev/null +++ b/command/connect/redirecttraffic/redirect_traffic.go @@ -0,0 +1,168 @@ +package redirecttraffic + +import ( + "flag" + "fmt" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/sdk/iptables" + "github.com/mitchellh/cli" + "github.com/mitchellh/mapstructure" +) + +func New(ui cli.Ui) *cmd { + ui = &cli.PrefixedUi{ + OutputPrefix: "==> ", + InfoPrefix: " ", + ErrorPrefix: "==> ", + Ui: ui, + } + + c := &cmd{ + UI: ui, + } + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + client *api.Client + + // Flags. + proxyUID string + proxyID string + proxyInboundPort int + proxyOutboundPort int +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.proxyUID, "proxy-uid", "", "The user ID of the proxy to exclude from traffic redirection.") + c.flags.StringVar(&c.proxyID, "proxy-id", "", "The service ID of the proxy service registered with Consul.") + c.flags.IntVar(&c.proxyInboundPort, "proxy-inbound-port", 0, "The inbound port that the proxy is listening on.") + c.flags.IntVar(&c.proxyOutboundPort, "proxy-outbound-port", iptables.DefaultTProxyOutboundPort, + "The outbound port that the proxy is listening on. When not provided, 15001 is used by default.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.NamespaceFlags()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) + return 1 + } + + if c.proxyUID == "" { + c.UI.Error("-proxy-uid is required") + return 1 + } + + if c.proxyID == "" && c.proxyInboundPort == 0 { + c.UI.Error("either -proxy-id or -proxy-inbound-port are required") + return 1 + } + + if c.proxyID != "" && (c.proxyInboundPort != 0 || c.proxyOutboundPort != iptables.DefaultTProxyOutboundPort) { + c.UI.Error("-proxy-inbound-port or -proxy-outbound-port cannot be provided together with -proxy-id. " + + "Proxy's inbound and outbound ports are retrieved from the proxy's configuration instead.") + return 1 + } + + cfg, err := c.generateConfigFromFlags() + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to create configuration to apply traffic redirection rules: %s", err)) + return 1 + } + + err = iptables.Setup(cfg) + if err != nil { + c.UI.Error(fmt.Sprintf("Error setting up traffic redirection rules: %s", err.Error())) + return 1 + } + + c.UI.Info("Successfully applied traffic redirection rules") + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return c.help +} + +// trafficRedirectProxyConfig is a snippet of xds/config.go +// with only the configuration values that we need to parse from Proxy.Config +// to apply traffic redirection rules. +type trafficRedirectProxyConfig struct { + BindPort int `mapstructure:"bind_port"` +} + +// generateConfigFromFlags generates iptables.Config based on command flags. +func (c *cmd) generateConfigFromFlags() (iptables.Config, error) { + cfg := iptables.Config{ProxyUserID: c.proxyUID} + + // When proxyID is provided, we set up cfg with values + // from proxy's service registration in Consul. + if c.proxyID != "" { + var err error + if c.client == nil { + c.client, err = c.http.APIClient() + if err != nil { + return iptables.Config{}, fmt.Errorf("error creating Consul API client: %s", err) + } + } + + svc, _, err := c.client.Agent().Service(c.proxyID, nil) + if err != nil { + return iptables.Config{}, fmt.Errorf("failed to fetch proxy service from Consul Agent: %s", err) + } + + if svc.Proxy == nil { + return iptables.Config{}, fmt.Errorf("service %s is not a proxy service", c.proxyID) + } + + cfg.ProxyInboundPort = svc.Port + var trCfg trafficRedirectProxyConfig + if err := mapstructure.WeakDecode(svc.Proxy.Config, &trCfg); err != nil { + return iptables.Config{}, fmt.Errorf("failed parsing Proxy.Config: %s", err) + } + + if trCfg.BindPort != 0 { + cfg.ProxyInboundPort = trCfg.BindPort + } + + // todo: Change once it's configurable + cfg.ProxyOutboundPort = iptables.DefaultTProxyOutboundPort + } else { + cfg.ProxyInboundPort = c.proxyInboundPort + cfg.ProxyOutboundPort = c.proxyOutboundPort + } + + return cfg, nil +} + +const synopsis = "Applies iptables rules for traffic redirection" +const help = ` +Usage: consul connect redirect-traffic [options] + + Applies iptables rules for inbound and outbound traffic redirection. + + Requires that the iptables command line utility is installed. + + Examples: + + $ consul connect redirect-traffic -proxy-uid 1234 -proxy-id web + + $ consul connect redirect-traffic -proxy-uid 1234 -proxy-inbound-port 20000 +` diff --git a/command/connect/redirecttraffic/redirect_traffic_test.go b/command/connect/redirecttraffic/redirect_traffic_test.go new file mode 100644 index 0000000000..77f5d88ed8 --- /dev/null +++ b/command/connect/redirecttraffic/redirect_traffic_test.go @@ -0,0 +1,280 @@ +package redirecttraffic + +import ( + "testing" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/iptables" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" +) + +func TestRun_FlagValidation(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + expError string + }{ + { + "-proxy-uid is missing", + nil, + "-proxy-uid is required", + }, + { + "-proxy-id and -proxy-inbound-port are missing", + []string{"-proxy-uid=1234"}, + "either -proxy-id or -proxy-inbound-port are required", + }, + { + "-proxy-id and -proxy-inbound-port are provided", + []string{"-proxy-uid=1234", "-proxy-id=test", "-proxy-inbound-port=15000"}, + "-proxy-inbound-port or -proxy-outbound-port cannot be provided together with -proxy-id.", + }, + { + "-proxy-id and -proxy-outbound-port are provided", + []string{"-proxy-uid=1234", "-proxy-id=test", "-proxy-outbound-port=15000"}, + "-proxy-inbound-port or -proxy-outbound-port cannot be provided together with -proxy-id.", + }, + { + "-proxy-id, -proxy-inbound-port and non-default -proxy-outbound-port are provided", + []string{"-proxy-uid=1234", "-proxy-id=test", "-proxy-inbound-port=15000", "-proxy-outbound-port=15001"}, + "-proxy-inbound-port or -proxy-outbound-port cannot be provided together with -proxy-id.", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(c.args) + require.Equal(t, code, 1) + require.Contains(t, ui.ErrorWriter.String(), c.expError) + }) + } + +} + +func TestGenerateConfigFromFlags(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + command func() cmd + proxyService *api.AgentServiceRegistration + expCfg iptables.Config + expError string + }{ + { + "proxyID with service port provided", + func() cmd { + var c cmd + c.init() + c.proxyUID = "1234" + c.proxyID = "test-proxy-id" + return c + }, + &api.AgentServiceRegistration{ + Kind: api.ServiceKindConnectProxy, + ID: "test-proxy-id", + Name: "test-proxy", + Port: 20000, + Address: "1.1.1.1", + Proxy: &api.AgentServiceConnectProxyConfig{ + DestinationServiceName: "foo", + }, + }, + iptables.Config{ + ProxyUserID: "1234", + ProxyInboundPort: 20000, + ProxyOutboundPort: iptables.DefaultTProxyOutboundPort, + }, + "", + }, + { + "proxyID with bind_port(int) provided", + func() cmd { + var c cmd + c.init() + c.proxyUID = "1234" + c.proxyID = "test-proxy-id" + return c + }, + &api.AgentServiceRegistration{ + Kind: api.ServiceKindConnectProxy, + ID: "test-proxy-id", + Name: "test-proxy", + Port: 20000, + Address: "1.1.1.1", + Proxy: &api.AgentServiceConnectProxyConfig{ + DestinationServiceName: "foo", + Config: map[string]interface{}{ + "bind_port": 21000, + }, + }, + }, + iptables.Config{ + ProxyUserID: "1234", + ProxyInboundPort: 21000, + ProxyOutboundPort: iptables.DefaultTProxyOutboundPort, + }, + "", + }, + { + "proxyID with bind_port(string) provided", + func() cmd { + var c cmd + c.init() + c.proxyUID = "1234" + c.proxyID = "test-proxy-id" + return c + }, + &api.AgentServiceRegistration{ + Kind: api.ServiceKindConnectProxy, + ID: "test-proxy-id", + Name: "test-proxy", + Port: 20000, + Address: "1.1.1.1", + Proxy: &api.AgentServiceConnectProxyConfig{ + DestinationServiceName: "foo", + Config: map[string]interface{}{ + "bind_port": "21000", + }, + }, + }, + iptables.Config{ + ProxyUserID: "1234", + ProxyInboundPort: 21000, + ProxyOutboundPort: iptables.DefaultTProxyOutboundPort, + }, + "", + }, + { + "proxyID with bind_port(invalid type) provided", + func() cmd { + var c cmd + c.init() + c.proxyUID = "1234" + c.proxyID = "test-proxy-id" + return c + }, + &api.AgentServiceRegistration{ + Kind: api.ServiceKindConnectProxy, + ID: "test-proxy-id", + Name: "test-proxy", + Port: 20000, + Address: "1.1.1.1", + Proxy: &api.AgentServiceConnectProxyConfig{ + DestinationServiceName: "foo", + Config: map[string]interface{}{ + "bind_port": "invalid", + }, + }, + }, + iptables.Config{}, + "failed parsing Proxy.Config: 1 error(s) decoding:\n\n* cannot parse 'bind_port' as int:", + }, + { + "proxyID provided, but Consul is not reachable", + func() cmd { + var c cmd + c.init() + c.proxyUID = "1234" + c.proxyID = "test-proxy-id" + return c + }, + nil, + iptables.Config{}, + "failed to fetch proxy service from Consul Agent: ", + }, + { + "proxyID of a non-proxy service", + func() cmd { + var c cmd + c.init() + c.proxyUID = "1234" + c.proxyID = "test-proxy-id" + return c + }, + &api.AgentServiceRegistration{ + ID: "test-proxy-id", + Name: "test-proxy", + Port: 20000, + Address: "1.1.1.1", + }, + iptables.Config{}, + "service test-proxy-id is not a proxy service", + }, + { + "only proxy inbound port is provided", + func() cmd { + var c cmd + c.init() + c.proxyUID = "1234" + c.proxyInboundPort = 15000 + return c + }, + nil, + iptables.Config{ + ProxyUserID: "1234", + ProxyInboundPort: 15000, + ProxyOutboundPort: iptables.DefaultTProxyOutboundPort, + }, + "", + }, + { + "proxy inbound and outbound ports are provided", + func() cmd { + var c cmd + c.init() + c.proxyUID = "1234" + c.proxyInboundPort = 15000 + c.proxyOutboundPort = 16000 + return c + }, + nil, + iptables.Config{ + ProxyUserID: "1234", + ProxyInboundPort: 15000, + ProxyOutboundPort: 16000, + }, + "", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cmd := c.command() + if c.proxyService != nil { + testServer, err := testutil.NewTestServerConfigT(t, nil) + require.NoError(t, err) + defer testServer.Stop() + + client, err := api.NewClient(&api.Config{Address: testServer.HTTPAddr}) + require.NoError(t, err) + + err = client.Agent().ServiceRegister(c.proxyService) + require.NoError(t, err) + + cmd.client = client + } else { + client, err := api.NewClient(&api.Config{Address: "not-reachable"}) + require.NoError(t, err) + cmd.client = client + } + + cfg, err := cmd.generateConfigFromFlags() + + if c.expError == "" { + require.NoError(t, err) + require.Equal(t, c.expCfg, cfg) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), c.expError) + } + }) + } +} diff --git a/sdk/go.mod b/sdk/go.mod index 339d1973ef..ecee2ecd42 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -11,7 +11,7 @@ require ( github.com/mattn/go-isatty v0.0.12 // indirect github.com/mitchellh/go-testing-interface v1.0.0 github.com/pkg/errors v0.8.1 - github.com/stretchr/testify v1.4.0 // indirect + github.com/stretchr/testify v1.4.0 golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect diff --git a/sdk/iptables/iptables.go b/sdk/iptables/iptables.go new file mode 100644 index 0000000000..e371cf9915 --- /dev/null +++ b/sdk/iptables/iptables.go @@ -0,0 +1,120 @@ +package iptables + +import ( + "errors" + "strconv" +) + +const ( + // Chain to intercept inbound traffic + ProxyInboundChain = "CONSUL_PROXY_INBOUND" + + // Chain to redirect inbound traffic to the proxy + ProxyInboundRedirectChain = "CONSUL_PROXY_IN_REDIRECT" + + // Chain to intercept outbound traffic + ProxyOutputChain = "CONSUL_PROXY_OUTPUT" + + // Chain to redirect outbound traffic to the proxy + ProxyOutputRedirectChain = "CONSUL_PROXY_REDIRECT" + + DefaultTProxyOutboundPort = 15001 +) + +// Config is used to configure which traffic interception and redirection +// rules should be applied with the iptables commands. +type Config struct { + // 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 + + // IptablesProvider is the Provider that will apply iptables rules. + 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() error + // Rules returns the list of rules that have been added but not applied yet. + Rules() []string +} + +// Setup will set up iptables interception and redirection rules +// based on the configuration provided in cfg. +// This implementation was inspired by +// https://github.com/openservicemesh/osm/blob/650a1a1dcf081ae90825f3b5dba6f30a0e532725/pkg/injector/iptables.go +func Setup(cfg Config) error { + if cfg.IptablesProvider == nil { + cfg.IptablesProvider = &iptablesExecutor{} + } + + 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} + 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)) + + // 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) + } + + // 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) + } + + 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 +} diff --git a/sdk/iptables/iptables_executor_linux.go b/sdk/iptables/iptables_executor_linux.go new file mode 100644 index 0000000000..cbd9a79152 --- /dev/null +++ b/sdk/iptables/iptables_executor_linux.go @@ -0,0 +1,46 @@ +// +build linux + +package iptables + +import ( + "bytes" + "fmt" + "os/exec" +) + +// iptablesExecutor implements IptablesProvider using exec.Cmd. +type iptablesExecutor struct { + commands []*exec.Cmd +} + +func (i *iptablesExecutor) AddRule(name string, args ...string) { + i.commands = append(i.commands, exec.Command(name, args...)) +} + +func (i *iptablesExecutor) ApplyRules() error { + _, err := exec.LookPath("iptables") + if err != nil { + return err + } + + for _, cmd := range i.commands { + var cmdOutput bytes.Buffer + cmd.Stdout = &cmdOutput + cmd.Stderr = &cmdOutput + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to run command: %s, err: %v, output: %s", cmd.String(), err, string(cmdOutput.Bytes())) + } + } + + return nil +} + +func (i *iptablesExecutor) Rules() []string { + var rules []string + for _, cmd := range i.commands { + rules = append(rules, cmd.String()) + } + + return rules +} diff --git a/sdk/iptables/iptables_executor_unsupported.go b/sdk/iptables/iptables_executor_unsupported.go new file mode 100644 index 0000000000..ba2e75ff77 --- /dev/null +++ b/sdk/iptables/iptables_executor_unsupported.go @@ -0,0 +1,18 @@ +// +build !linux + +package iptables + +import "errors" + +// iptablesExecutor implements IptablesProvider and errors out on any non-linux OS. +type iptablesExecutor struct{} + +func (i *iptablesExecutor) AddRule(_ string, _ ...string) {} + +func (i *iptablesExecutor) ApplyRules() error { + return errors.New("applying traffic redirection rules with 'iptables' is not supported on this operating system; only linux OS is supported") +} + +func (i *iptablesExecutor) Rules() []string { + return nil +} diff --git a/sdk/iptables/iptables_test.go b/sdk/iptables/iptables_test.go new file mode 100644 index 0000000000..e3206c3c39 --- /dev/null +++ b/sdk/iptables/iptables_test.go @@ -0,0 +1,123 @@ +package iptables + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSetup(t *testing.T) { + cases := []struct { + name string + cfg Config + expectedRules []string + }{ + { + "no proxy outbound port provided", + Config{ + ProxyUserID: "123", + ProxyInboundPort: 20000, + IptablesProvider: &fakeIptablesProvider{}, + }, + []string{ + "iptables -t nat -N CONSUL_PROXY_INBOUND", + "iptables -t nat -N CONSUL_PROXY_IN_REDIRECT", + "iptables -t nat -N CONSUL_PROXY_OUTPUT", + "iptables -t nat -N CONSUL_PROXY_REDIRECT", + "iptables -t nat -A CONSUL_PROXY_REDIRECT -p tcp -j REDIRECT --to-port 15001", + "iptables -t nat -A OUTPUT -p tcp -j CONSUL_PROXY_OUTPUT", + "iptables -t nat -A CONSUL_PROXY_OUTPUT -m owner --uid-owner 123 -j RETURN", + "iptables -t nat -A CONSUL_PROXY_OUTPUT -d 127.0.0.1/32 -j RETURN", + "iptables -t nat -A CONSUL_PROXY_OUTPUT -j CONSUL_PROXY_REDIRECT", + "iptables -t nat -A CONSUL_PROXY_IN_REDIRECT -p tcp -j REDIRECT --to-port 20000", + "iptables -t nat -A PREROUTING -p tcp -j CONSUL_PROXY_INBOUND", + "iptables -t nat -A CONSUL_PROXY_INBOUND -p tcp -j CONSUL_PROXY_IN_REDIRECT", + }, + }, + { + "proxy outbound port is provided", + Config{ + ProxyUserID: "123", + ProxyInboundPort: 20000, + ProxyOutboundPort: 21000, + IptablesProvider: &fakeIptablesProvider{}, + }, + []string{ + "iptables -t nat -N CONSUL_PROXY_INBOUND", + "iptables -t nat -N CONSUL_PROXY_IN_REDIRECT", + "iptables -t nat -N CONSUL_PROXY_OUTPUT", + "iptables -t nat -N CONSUL_PROXY_REDIRECT", + "iptables -t nat -A CONSUL_PROXY_REDIRECT -p tcp -j REDIRECT --to-port 21000", + "iptables -t nat -A OUTPUT -p tcp -j CONSUL_PROXY_OUTPUT", + "iptables -t nat -A CONSUL_PROXY_OUTPUT -m owner --uid-owner 123 -j RETURN", + "iptables -t nat -A CONSUL_PROXY_OUTPUT -d 127.0.0.1/32 -j RETURN", + "iptables -t nat -A CONSUL_PROXY_OUTPUT -j CONSUL_PROXY_REDIRECT", + "iptables -t nat -A CONSUL_PROXY_IN_REDIRECT -p tcp -j REDIRECT --to-port 20000", + "iptables -t nat -A PREROUTING -p tcp -j CONSUL_PROXY_INBOUND", + "iptables -t nat -A CONSUL_PROXY_INBOUND -p tcp -j CONSUL_PROXY_IN_REDIRECT", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := Setup(c.cfg) + require.NoError(t, err) + require.Equal(t, c.expectedRules, c.cfg.IptablesProvider.Rules()) + }) + } + +} + +func TestSetup_errors(t *testing.T) { + cases := []struct { + name string + cfg Config + expErr string + }{ + { + "no proxy UID", + Config{ + IptablesProvider: &iptablesExecutor{}, + }, + "ProxyUserID is required to set up traffic redirection", + }, + { + "no proxy inbound port", + Config{ + ProxyUserID: "123", + ProxyOutboundPort: 21000, + IptablesProvider: &iptablesExecutor{}, + }, + "ProxyInboundPort is required to set up traffic redirection", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := Setup(c.cfg) + require.EqualError(t, err, c.expErr) + }) + } +} + +type fakeIptablesProvider struct { + rules []string +} + +func (f *fakeIptablesProvider) AddRule(name string, args ...string) { + var rule []string + rule = append(rule, name) + rule = append(rule, args...) + + f.rules = append(f.rules, strings.Join(rule, " ")) +} + +func (f *fakeIptablesProvider) ApplyRules() error { + return nil +} + +func (f *fakeIptablesProvider) Rules() []string { + return f.rules +} diff --git a/website/content/commands/connect/envoy.mdx b/website/content/commands/connect/envoy.mdx index 396fe275fc..9492dbc683 100644 --- a/website/content/commands/connect/envoy.mdx +++ b/website/content/commands/connect/envoy.mdx @@ -1,7 +1,8 @@ --- layout: commands -page_title: 'Commands: Connect Proxy' -description: The connect proxy subcommand is used to run the Envoy proxy for Connect. +page_title: 'Commands: Connect Envoy' +sidebar_title: envoy +description: The connect envoy subcommand is used to generate a bootstrap configuration for Envoy. --- # Consul Connect Envoy @@ -90,7 +91,7 @@ proxy configuration needed. - `-prometheus-scrape-path` - Sets the path where Envoy will expose metrics on the `envoy_prometheus_bind_addr` listener. Default is `/metrics`. For example, if `envoy_prometheus_bind_addr` - is `0.0.0.0:20200`, and this flag is set to `/scrape-metrics`, prometheus metrics would + is `0.0.0.0:20200`, and this flag is set to `/scrape-metrics`, prometheus metrics would be scrapable at `0.0.0.0:20200/scrape-metrics`. Only applicable when `envoy_prometheus_bind_addr` is set in proxy config. diff --git a/website/content/commands/connect/index.mdx b/website/content/commands/connect/index.mdx index 767ac41a63..380cbb14e1 100644 --- a/website/content/commands/connect/index.mdx +++ b/website/content/commands/connect/index.mdx @@ -34,10 +34,11 @@ Usage: consul connect [options] [args] For more examples, ask for subcommand help or view the documentation. Subcommands: - ca Interact with the Consul Connect Certificate Authority (CA) - envoy Runs or Configures Envoy as a Connect proxy - expose Expose a Connect-enabled service through an Ingress gateway - proxy Runs a Consul Connect proxy + ca Interact with the Consul Connect Certificate Authority (CA) + envoy Runs or Configures Envoy as a Connect proxy + expose Expose a Connect-enabled service through an Ingress gateway + proxy Runs a Consul Connect proxy + redirect-traffic Applies iptables rules for traffic redirection ``` For more information, examples, and usage about a subcommand, click on the name diff --git a/website/content/commands/connect/redirect-traffic.mdx b/website/content/commands/connect/redirect-traffic.mdx new file mode 100644 index 0000000000..618f155f78 --- /dev/null +++ b/website/content/commands/connect/redirect-traffic.mdx @@ -0,0 +1,73 @@ +--- +layout: commands +page_title: 'Commands: Connect Redirect Traffic' +sidebar_title: redirect-traffic +description: > + The connect redirect-traffic subcommand is used to apply traffic redirection rules + when using Connect in Transparent Proxy mode. +--- + +# Consul Connect Redirect Traffic + +Command: `consul connect redirect-traffic` + +The connect redirect-traffic command is used to apply traffic redirection rules to enforce +all traffic to go through the [Envoy proxy](https://envoyproxy.io) when using [Consul +Service Mesh](/docs/connect/) in the Transparent Proxy mode. + +This command requires `iptables` command line utility to be installed, +and as a result, this command can currently only run on linux. +The user running the command needs to have `NET_ADMIN` capability. + +By default, this command will apply rules to intercept and redirect all inbound and outbound +TCP traffic to the Envoy's inbound and outbound ports accordingly. + +When `proxy-id` is specified, additional exclusion rules will be applied based on proxy's +configuration stored in the local Consul agent. This includes redirecting to the proxy's +inbound and outbound ports specified in the service registration. + +## Usage + +Usage: `consul connect redirect-traffic [options]` + +#### API Options + + @include 'http_api_options_client.mdx' + +#### Options for Traffic Redirection Rules + +- `-proxy-id` - The [proxy service](/docs/connect/registration/service-registration) ID. + This service ID must already be registered with the local agent. + +- `-proxy-inbound-port` - The inbound port that the proxy is listening on. + +- `-proxy-outbound-port` - The outbound port that the proxy is listening on. When not provided, 15001 is used by default. + +- `-proxy-uid` - The user ID of the proxy to exclude from traffic redirection. + +#### Enterprise Options + +@include 'http_api_namespace_options.mdx' + +## Examples + +### Basic Rules + +The default traffic redirection rules can be applied with: + +```shell-session +$ consul connect redirect-traffic \ + -proxy-uid 1234 \ + -proxy-inbound-port 20000 +``` + +### Using Registered Proxy Configuration + +To automatically apply rules based on proxy's service registration, use the following command: + +```shell-session +$ consul connect redirect-traffic -proxy-uid 1234 -proxy-id web +``` + +This command assumes that the proxy service is registered with the local agent +and that the local agent is reachable.