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.
This commit is contained in:
Iryna Shustava 2021-04-09 11:48:10 -07:00 committed by GitHub
parent 920ba3db39
commit ff2e70f4ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 852 additions and 16 deletions

6
.changelog/9910.txt Normal file
View File

@ -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.
```

View File

@ -18,6 +18,7 @@ import (
envoy_tcp_proxy_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" 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_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" 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/jsonpb"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
@ -32,11 +33,6 @@ import (
"github.com/hashicorp/consul/logging" "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. // listenersFromSnapshot returns the xDS API representation of the "listeners" in the snapshot.
func (s *Server) listenersFromSnapshot(cInfo connectionInfo, cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) { func (s *Server) listenersFromSnapshot(cInfo connectionInfo, cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
if cfgSnap == nil { if cfgSnap == nil {
@ -75,7 +71,8 @@ func (s *Server) listenersFromSnapshotConnectProxy(cInfo connectionInfo, cfgSnap
var outboundListener *envoy_listener_v3.Listener var outboundListener *envoy_listener_v3.Listener
if cfgSnap.Proxy.TransparentProxy { 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.FilterChains = make([]*envoy_listener_v3.FilterChain, 0)
outboundListener.ListenerFilters = []*envoy_listener_v3.ListenerFilter{ outboundListener.ListenerFilters = []*envoy_listener_v3.ListenerFilter{
{ {

View File

@ -1,3 +1,4 @@
ARG CONSUL_IMAGE_VERSION=latest ARG CONSUL_IMAGE_VERSION=latest
FROM consul:${CONSUL_IMAGE_VERSION} FROM consul:${CONSUL_IMAGE_VERSION}
RUN apk update && apk add iptables
COPY consul /bin/consul COPY consul /bin/consul

View File

@ -54,6 +54,7 @@ import (
pipebootstrap "github.com/hashicorp/consul/command/connect/envoy/pipe-bootstrap" pipebootstrap "github.com/hashicorp/consul/command/connect/envoy/pipe-bootstrap"
"github.com/hashicorp/consul/command/connect/expose" "github.com/hashicorp/consul/command/connect/expose"
"github.com/hashicorp/consul/command/connect/proxy" "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/debug"
"github.com/hashicorp/consul/command/event" "github.com/hashicorp/consul/command/event"
"github.com/hashicorp/consul/command/exec" "github.com/hashicorp/consul/command/exec"
@ -77,8 +78,8 @@ import (
kvput "github.com/hashicorp/consul/command/kv/put" kvput "github.com/hashicorp/consul/command/kv/put"
"github.com/hashicorp/consul/command/leave" "github.com/hashicorp/consul/command/leave"
"github.com/hashicorp/consul/command/lock" "github.com/hashicorp/consul/command/lock"
login "github.com/hashicorp/consul/command/login" "github.com/hashicorp/consul/command/login"
logout "github.com/hashicorp/consul/command/logout" "github.com/hashicorp/consul/command/logout"
"github.com/hashicorp/consul/command/maint" "github.com/hashicorp/consul/command/maint"
"github.com/hashicorp/consul/command/members" "github.com/hashicorp/consul/command/members"
"github.com/hashicorp/consul/command/monitor" "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", 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 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 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("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("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 }) Register("exec", func(ui cli.Ui) (cli.Command, error) { return exec.New(ui, MakeShutdownCh()), nil })

View File

@ -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
`

View File

@ -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)
}
})
}
}

View File

@ -11,7 +11,7 @@ require (
github.com/mattn/go-isatty v0.0.12 // indirect github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 github.com/mitchellh/go-testing-interface v1.0.0
github.com/pkg/errors v0.8.1 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 golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect

120
sdk/iptables/iptables.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,7 +1,8 @@
--- ---
layout: commands layout: commands
page_title: 'Commands: Connect Proxy' page_title: 'Commands: Connect Envoy'
description: The connect proxy subcommand is used to run the Envoy proxy for Connect. sidebar_title: envoy
description: The connect envoy subcommand is used to generate a bootstrap configuration for Envoy.
--- ---
# Consul Connect Envoy # Consul Connect Envoy
@ -90,7 +91,7 @@ proxy configuration needed.
- `-prometheus-scrape-path` - Sets the path where Envoy will expose metrics on the - `-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` `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`. be scrapable at `0.0.0.0:20200/scrape-metrics`.
Only applicable when `envoy_prometheus_bind_addr` is set in proxy config. Only applicable when `envoy_prometheus_bind_addr` is set in proxy config.

View File

@ -34,10 +34,11 @@ Usage: consul connect <subcommand> [options] [args]
For more examples, ask for subcommand help or view the documentation. For more examples, ask for subcommand help or view the documentation.
Subcommands: Subcommands:
ca Interact with the Consul Connect Certificate Authority (CA) ca Interact with the Consul Connect Certificate Authority (CA)
envoy Runs or Configures Envoy as a Connect proxy envoy Runs or Configures Envoy as a Connect proxy
expose Expose a Connect-enabled service through an Ingress gateway expose Expose a Connect-enabled service through an Ingress gateway
proxy Runs a Consul Connect proxy 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 For more information, examples, and usage about a subcommand, click on the name

View File

@ -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.