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:
parent
920ba3db39
commit
ff2e70f4ce
|
@ -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.
|
||||
```
|
|
@ -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{
|
||||
{
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
ARG CONSUL_IMAGE_VERSION=latest
|
||||
FROM consul:${CONSUL_IMAGE_VERSION}
|
||||
RUN apk update && apk add iptables
|
||||
COPY consul /bin/consul
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
`
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -38,6 +38,7 @@ Subcommands:
|
|||
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
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue