mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
12 KiB
445 lines
12 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
|
|
package proxy |
|
|
|
import ( |
|
"flag" |
|
"fmt" |
|
"log" |
|
"net" |
|
"net/http" |
|
_ "net/http/pprof" // Expose pprof if configured |
|
"os" |
|
"sort" |
|
"strconv" |
|
"strings" |
|
|
|
"github.com/hashicorp/consul/api" |
|
"github.com/hashicorp/consul/command/flags" |
|
proxyImpl "github.com/hashicorp/consul/connect/proxy" |
|
"github.com/hashicorp/go-hclog" |
|
|
|
"github.com/hashicorp/consul/logging" |
|
"github.com/mitchellh/cli" |
|
) |
|
|
|
func New(ui cli.Ui, shutdownCh <-chan struct{}) *cmd { |
|
ui = &cli.PrefixedUi{ |
|
OutputPrefix: "==> ", |
|
InfoPrefix: " ", |
|
ErrorPrefix: "==> ", |
|
Ui: ui, |
|
} |
|
|
|
c := &cmd{UI: ui, shutdownCh: shutdownCh} |
|
c.init() |
|
return c |
|
} |
|
|
|
type cmd struct { |
|
UI cli.Ui |
|
flags *flag.FlagSet |
|
http *flags.HTTPFlags |
|
help string |
|
|
|
shutdownCh <-chan struct{} |
|
|
|
logger hclog.Logger |
|
|
|
// flags |
|
logLevel string |
|
logJSON bool |
|
cfgFile string |
|
proxyID string |
|
sidecarFor string |
|
pprofAddr string |
|
service string |
|
serviceAddr string |
|
upstreams map[string]proxyImpl.UpstreamConfig |
|
listen string |
|
register bool |
|
registerId string |
|
|
|
// test flags |
|
testNoStart bool // don't start the proxy, just exit 0 |
|
} |
|
|
|
func (c *cmd) init() { |
|
c.flags = flag.NewFlagSet("", flag.ContinueOnError) |
|
|
|
c.flags.StringVar(&c.proxyID, "proxy-id", "", |
|
"The proxy's ID on the local agent.") |
|
|
|
c.flags.StringVar(&c.sidecarFor, "sidecar-for", "", |
|
"The ID of a service instance on the local agent that this proxy should "+ |
|
"become a sidecar for. It requires that the proxy service is registered "+ |
|
"with the agent as a connect-proxy with Proxy.DestinationServiceID set "+ |
|
"to this value. If more than one such proxy is registered it will fail.") |
|
|
|
c.flags.StringVar(&c.logLevel, "log-level", "INFO", |
|
"Specifies the log level.") |
|
|
|
c.flags.BoolVar(&c.logJSON, "log-json", false, |
|
"Output logs in JSON format.") |
|
|
|
c.flags.StringVar(&c.pprofAddr, "pprof-addr", "", |
|
"Enable debugging via pprof. Providing a host:port (or just ':port') "+ |
|
"enables profiling HTTP endpoints on that address.") |
|
|
|
c.flags.StringVar(&c.service, "service", "", |
|
"Name of the service this proxy is representing.") |
|
|
|
c.flags.Var((*FlagUpstreams)(&c.upstreams), "upstream", |
|
"Upstream service to support connecting to. The format should be "+ |
|
"'name:addr', such as 'db:8181'. This will make 'db' available "+ |
|
"on port 8181. This can be repeated multiple times.") |
|
|
|
c.flags.StringVar(&c.serviceAddr, "service-addr", "", |
|
"Address of the local service to proxy. Only useful if -listen "+ |
|
"and -service are both set.") |
|
|
|
c.flags.StringVar(&c.listen, "listen", "", |
|
"Address to listen for inbound connections to the proxied service. "+ |
|
"Must be specified with -service and -service-addr.") |
|
|
|
c.flags.BoolVar(&c.register, "register", false, |
|
"Self-register with the local Consul agent. Only useful with "+ |
|
"-listen.") |
|
|
|
c.flags.StringVar(&c.registerId, "register-id", "", |
|
"ID suffix for the service. Use this to disambiguate with other proxies.") |
|
|
|
c.http = &flags.HTTPFlags{} |
|
flags.Merge(c.flags, c.http.ClientFlags()) |
|
flags.Merge(c.flags, c.http.ServerFlags()) |
|
c.help = flags.Usage(help, c.flags) |
|
} |
|
|
|
func (c *cmd) Run(args []string) int { |
|
if err := c.flags.Parse(args); err != nil { |
|
return 1 |
|
} |
|
if len(c.flags.Args()) > 0 { |
|
c.UI.Error(fmt.Sprintf("Should have no non-flag arguments.")) |
|
return 1 |
|
} |
|
|
|
// Load the proxy ID and token from env vars if they're set |
|
if c.proxyID == "" { |
|
c.proxyID = os.Getenv("CONNECT_PROXY_ID") |
|
} |
|
if c.sidecarFor == "" { |
|
c.sidecarFor = os.Getenv("CONNECT_SIDECAR_FOR") |
|
} |
|
|
|
// Setup the log outputs |
|
logConfig := logging.Config{ |
|
LogLevel: c.logLevel, |
|
Name: logging.Proxy, |
|
LogJSON: c.logJSON, |
|
} |
|
|
|
logGate := logging.GatedWriter{Writer: &cli.UiWriter{Ui: c.UI}} |
|
|
|
logger, err := logging.Setup(logConfig, &logGate) |
|
if err != nil { |
|
c.UI.Error(err.Error()) |
|
return 1 |
|
} |
|
c.logger = logger |
|
|
|
// Enable Pprof if needed |
|
if c.pprofAddr != "" { |
|
go func() { |
|
c.UI.Output(fmt.Sprintf("Starting pprof HTTP endpoints on "+ |
|
"http://%s/debug/pprof", c.pprofAddr)) |
|
log.Fatal(http.ListenAndServe(c.pprofAddr, nil)) |
|
}() |
|
} |
|
|
|
// Setup Consul client |
|
client, err := c.http.APIClient() |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) |
|
return 1 |
|
} |
|
|
|
// Output this first since the config watcher below will output |
|
// other information. |
|
c.UI.Output("Consul Connect proxy starting...") |
|
|
|
// Get the proper configuration watcher |
|
cfgWatcher, err := c.configWatcher(client) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error preparing configuration: %s", err)) |
|
return 1 |
|
} |
|
|
|
p, err := proxyImpl.New(client, cfgWatcher, c.logger) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Failed initializing proxy: %s", err)) |
|
return 1 |
|
} |
|
|
|
// Hook the shutdownCh up to close the proxy |
|
go func() { |
|
<-c.shutdownCh |
|
p.Close() |
|
}() |
|
|
|
// Register the service if we requested it |
|
if c.register { |
|
monitor, err := c.registerMonitor(client) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Failed initializing registration: %s", err)) |
|
return 1 |
|
} |
|
|
|
go monitor.Run() |
|
defer monitor.Close() |
|
} |
|
|
|
c.UI.Info("") |
|
c.UI.Output("Log data will now stream in as it occurs:\n") |
|
logGate.Flush() |
|
|
|
// Run the proxy unless our tests require we don't |
|
if !c.testNoStart { |
|
if err := p.Serve(); err != nil { |
|
c.UI.Error(fmt.Sprintf("Failed running proxy: %s", err)) |
|
} |
|
} |
|
|
|
c.UI.Output("Consul Connect proxy shutdown") |
|
return 0 |
|
} |
|
|
|
func (c *cmd) lookupServiceForSidecar(client *api.Client) (*api.AgentService, error) { |
|
return LookupServiceForSidecar(client, c.sidecarFor) |
|
} |
|
|
|
// LookupServiceForSidecar finds candidate local proxy registrations that are a |
|
// sidecar for the given service. It will return that service if and only if there |
|
// is exactly one registered connect proxy with `Proxy.DestinationServiceID` set to |
|
// the specified service ID. |
|
// |
|
// This is exported to share it with the connect envoy command. |
|
func LookupServiceForSidecar(client *api.Client, sidecarFor string) (*api.AgentService, error) { |
|
svcs, err := client.Agent().Services() |
|
if err != nil { |
|
return nil, fmt.Errorf("Failed looking up sidecar proxy info for %s: %s", |
|
sidecarFor, err) |
|
} |
|
|
|
var matched []*api.AgentService |
|
var matchedProxyIDs []string |
|
for _, svc := range svcs { |
|
if svc.Kind == api.ServiceKindConnectProxy && svc.Proxy != nil && |
|
strings.EqualFold(svc.Proxy.DestinationServiceID, sidecarFor) { |
|
matched = append(matched, svc) |
|
matchedProxyIDs = append(matchedProxyIDs, svc.ID) |
|
} |
|
} |
|
|
|
if len(matched) == 0 { |
|
return nil, fmt.Errorf("No sidecar proxy registered for %s", sidecarFor) |
|
} |
|
if len(matched) > 1 { |
|
return nil, fmt.Errorf("More than one sidecar proxy registered for %s.\n"+ |
|
" Start proxy with -proxy-id and one of the following IDs: %s", |
|
sidecarFor, strings.Join(matchedProxyIDs, ", ")) |
|
} |
|
return matched[0], nil |
|
} |
|
|
|
// LookupGatewayProxy finds the gateway service registered with the local |
|
// agent. If exactly one gateway exists it will be returned, otherwise an error |
|
// is returned. |
|
func LookupGatewayProxy(client *api.Client, kind api.ServiceKind) (*api.AgentService, error) { |
|
svcs, err := client.Agent().ServicesWithFilter(fmt.Sprintf("Kind == `%s`", kind)) |
|
if err != nil { |
|
return nil, fmt.Errorf("Failed looking up %s instances: %v", kind, err) |
|
} |
|
|
|
switch len(svcs) { |
|
case 0: |
|
return nil, fmt.Errorf("No %s services registered with this agent", kind) |
|
case 1: |
|
for _, svc := range svcs { |
|
return svc, nil |
|
} |
|
return nil, fmt.Errorf("This should be unreachable") |
|
default: |
|
return nil, fmt.Errorf("Cannot lookup the %s's proxy ID because multiple are registered with the agent", kind) |
|
} |
|
} |
|
|
|
func (c *cmd) configWatcher(client *api.Client) (proxyImpl.ConfigWatcher, error) { |
|
// Use the configured proxy ID |
|
if c.proxyID != "" { |
|
c.UI.Info("Configuration mode: Agent API") |
|
c.UI.Info(fmt.Sprintf(" Proxy ID: %s", c.proxyID)) |
|
return proxyImpl.NewAgentConfigWatcher(client, c.proxyID, c.logger) |
|
} |
|
|
|
if c.sidecarFor != "" { |
|
// Running as a sidecar, we need to find the proxy-id for the requested |
|
// service |
|
var err error |
|
svc, err := c.lookupServiceForSidecar(client) |
|
if err != nil { |
|
return nil, err |
|
} |
|
c.proxyID = svc.ID |
|
|
|
c.UI.Info("Configuration mode: Agent API") |
|
c.UI.Info(fmt.Sprintf(" Sidecar for ID: %s", c.sidecarFor)) |
|
c.UI.Info(fmt.Sprintf(" Proxy ID: %s", c.proxyID)) |
|
return proxyImpl.NewAgentConfigWatcher(client, c.proxyID, c.logger) |
|
} |
|
|
|
// Otherwise, we're representing a manually specified service. |
|
if c.service == "" { |
|
return nil, fmt.Errorf( |
|
"-service or -proxy-id must be specified so that proxy can " + |
|
"configure itself.") |
|
} |
|
|
|
c.UI.Info("Configuration mode: Flags") |
|
c.UI.Info(fmt.Sprintf(" Service: %s", c.service)) |
|
|
|
// Convert our upstreams to a slice of configurations. We do this |
|
// deterministically by alphabetizing the upstream keys. We do this so |
|
// that tests can compare the upstream values. |
|
upstreamKeys := make([]string, 0, len(c.upstreams)) |
|
for k := range c.upstreams { |
|
upstreamKeys = append(upstreamKeys, k) |
|
} |
|
sort.Strings(upstreamKeys) |
|
upstreams := make([]proxyImpl.UpstreamConfig, 0, len(c.upstreams)) |
|
for _, k := range upstreamKeys { |
|
config := c.upstreams[k] |
|
|
|
addr := config.LocalBindSocketPath |
|
if addr == "" { |
|
addr = fmt.Sprintf( |
|
"%s:%d", |
|
config.LocalBindAddress, config.LocalBindPort) |
|
} |
|
|
|
c.UI.Info(fmt.Sprintf( |
|
" Upstream: %s => %s", |
|
k, addr)) |
|
upstreams = append(upstreams, config) |
|
} |
|
|
|
// Parse out our listener if we have one |
|
var listener proxyImpl.PublicListenerConfig |
|
if c.listen != "" { |
|
host, port, err := c.listenParts() |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
if c.serviceAddr == "" { |
|
return nil, fmt.Errorf( |
|
"-service-addr must be specified with -listen so the proxy " + |
|
"knows the backend service address.") |
|
} |
|
|
|
c.UI.Info(fmt.Sprintf(" Public listener: %s:%d => %s", host, port, c.serviceAddr)) |
|
listener.BindAddress = host |
|
listener.BindPort = port |
|
listener.LocalServiceAddress = c.serviceAddr |
|
} else { |
|
c.UI.Info(fmt.Sprintf(" Public listener: Disabled")) |
|
} |
|
|
|
return proxyImpl.NewStaticConfigWatcher(&proxyImpl.Config{ |
|
ProxiedServiceName: c.service, |
|
PublicListener: listener, |
|
Upstreams: upstreams, |
|
}), nil |
|
} |
|
|
|
// registerMonitor returns the registration monitor ready to be started. |
|
func (c *cmd) registerMonitor(client *api.Client) (*RegisterMonitor, error) { |
|
if c.service == "" || c.listen == "" { |
|
return nil, fmt.Errorf("-register may only be specified with -service and -listen") |
|
} |
|
|
|
host, port, err := c.listenParts() |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
m := NewRegisterMonitor(c.logger) |
|
m.Logger = c.logger |
|
m.Client = client |
|
m.Service = c.service |
|
m.IDSuffix = c.registerId |
|
m.LocalAddress = host |
|
m.LocalPort = port |
|
return m, nil |
|
} |
|
|
|
// listenParts returns the host and port parts of the -listen flag. The |
|
// -listen flag must be non-empty prior to calling this. |
|
func (c *cmd) listenParts() (string, int, error) { |
|
host, portRaw, err := net.SplitHostPort(c.listen) |
|
if err != nil { |
|
return "", 0, err |
|
} |
|
|
|
port, err := strconv.ParseInt(portRaw, 0, 0) |
|
if err != nil { |
|
return "", 0, err |
|
} |
|
|
|
return host, int(port), nil |
|
} |
|
|
|
func (c *cmd) Synopsis() string { |
|
return synopsis |
|
} |
|
|
|
func (c *cmd) Help() string { |
|
return c.help |
|
} |
|
|
|
const synopsis = "Runs a Consul Connect proxy" |
|
const help = ` |
|
Usage: consul connect proxy [options] |
|
|
|
Starts a Consul Connect proxy and runs until an interrupt is received. |
|
The proxy can be used to accept inbound connections for a service, |
|
wrap outbound connections to upstream services, or both. This enables |
|
a non-Connect-aware application to use Connect. |
|
|
|
The proxy requires service:write permissions for the service it represents. |
|
The token may be passed via the CLI or the CONSUL_HTTP_TOKEN environment |
|
variable. |
|
|
|
Consul can automatically start and manage this proxy by specifying the |
|
"proxy" configuration within your service definition. |
|
|
|
The example below shows how to start a local proxy for establishing outbound |
|
connections to "db" representing the frontend service. Once running, any |
|
process that creates a TCP connection to the specified port (8181) will |
|
establish a mutual TLS connection to "db" identified as "frontend". |
|
|
|
$ consul connect proxy -service frontend -upstream db:8181 |
|
|
|
The next example starts a local proxy that also accepts inbound connections |
|
on port 8443, authorizes the connection, then proxies it to port 8080: |
|
|
|
$ consul connect proxy \ |
|
-service frontend \ |
|
-service-addr 127.0.0.1:8080 \ |
|
-listen ':8443' |
|
|
|
A proxy can accept both inbound connections as well as proxy to upstream |
|
services by specifying both the "-listen" and "-upstream" flags. |
|
|
|
`
|
|
|