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