1package proxy
2
3import (
4	"flag"
5	"fmt"
6	"io"
7	"log"
8	"net"
9	"net/http"
10	_ "net/http/pprof" // Expose pprof if configured
11	"os"
12	"sort"
13	"strconv"
14	"strings"
15
16	proxyAgent "github.com/hashicorp/consul/agent/proxyprocess"
17	"github.com/hashicorp/consul/api"
18	"github.com/hashicorp/consul/command/flags"
19	proxyImpl "github.com/hashicorp/consul/connect/proxy"
20
21	"github.com/hashicorp/consul/logger"
22	"github.com/hashicorp/logutils"
23	"github.com/mitchellh/cli"
24)
25
26func New(ui cli.Ui, shutdownCh <-chan struct{}) *cmd {
27	ui = &cli.PrefixedUi{
28		OutputPrefix: "==> ",
29		InfoPrefix:   "    ",
30		ErrorPrefix:  "==> ",
31		Ui:           ui,
32	}
33
34	c := &cmd{UI: ui, shutdownCh: shutdownCh}
35	c.init()
36	return c
37}
38
39type cmd struct {
40	UI    cli.Ui
41	flags *flag.FlagSet
42	http  *flags.HTTPFlags
43	help  string
44
45	shutdownCh <-chan struct{}
46
47	logFilter *logutils.LevelFilter
48	logOutput io.Writer
49	logger    *log.Logger
50
51	// flags
52	logLevel    string
53	cfgFile     string
54	proxyID     string
55	sidecarFor  string
56	pprofAddr   string
57	service     string
58	serviceAddr string
59	upstreams   map[string]proxyImpl.UpstreamConfig
60	listen      string
61	register    bool
62	registerId  string
63
64	// test flags
65	testNoStart bool // don't start the proxy, just exit 0
66}
67
68func (c *cmd) init() {
69	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
70
71	c.flags.StringVar(&c.proxyID, "proxy-id", "",
72		"The proxy's ID on the local agent.")
73
74	c.flags.StringVar(&c.sidecarFor, "sidecar-for", "",
75		"The ID of a service instance on the local agent that this proxy should "+
76			"become a sidecar for. It requires that the proxy service is registered "+
77			"with the agent as a connect-proxy with Proxy.DestinationServiceID set "+
78			"to this value. If more than one such proxy is registered it will fail.")
79
80	c.flags.StringVar(&c.logLevel, "log-level", "INFO",
81		"Specifies the log level.")
82
83	c.flags.StringVar(&c.pprofAddr, "pprof-addr", "",
84		"Enable debugging via pprof. Providing a host:port (or just ':port') "+
85			"enables profiling HTTP endpoints on that address.")
86
87	c.flags.StringVar(&c.service, "service", "",
88		"Name of the service this proxy is representing.")
89
90	c.flags.Var((*FlagUpstreams)(&c.upstreams), "upstream",
91		"Upstream service to support connecting to. The format should be "+
92			"'name:addr', such as 'db:8181'. This will make 'db' available "+
93			"on port 8181. This can be repeated multiple times.")
94
95	c.flags.StringVar(&c.serviceAddr, "service-addr", "",
96		"Address of the local service to proxy. Only useful if -listen "+
97			"and -service are both set.")
98
99	c.flags.StringVar(&c.listen, "listen", "",
100		"Address to listen for inbound connections to the proxied service. "+
101			"Must be specified with -service and -service-addr.")
102
103	c.flags.BoolVar(&c.register, "register", false,
104		"Self-register with the local Consul agent. Only useful with "+
105			"-listen.")
106
107	c.flags.StringVar(&c.registerId, "register-id", "",
108		"ID suffix for the service. Use this to disambiguate with other proxies.")
109
110	c.http = &flags.HTTPFlags{}
111	flags.Merge(c.flags, c.http.ClientFlags())
112	flags.Merge(c.flags, c.http.ServerFlags())
113	c.help = flags.Usage(help, c.flags)
114}
115
116func (c *cmd) Run(args []string) int {
117	if err := c.flags.Parse(args); err != nil {
118		return 1
119	}
120	if len(c.flags.Args()) > 0 {
121		c.UI.Error(fmt.Sprintf("Should have no non-flag arguments."))
122		return 1
123	}
124
125	// Load the proxy ID and token from env vars if they're set
126	if c.proxyID == "" {
127		c.proxyID = os.Getenv(proxyAgent.EnvProxyID)
128	}
129	if c.sidecarFor == "" {
130		c.sidecarFor = os.Getenv(proxyAgent.EnvSidecarFor)
131	}
132	if c.http.Token() == "" {
133		c.http.SetToken(os.Getenv(proxyAgent.EnvProxyToken))
134	}
135
136	// Setup the log outputs
137	logConfig := &logger.Config{
138		LogLevel: c.logLevel,
139	}
140	logFilter, logGate, _, logOutput, ok := logger.Setup(logConfig, c.UI)
141	if !ok {
142		return 1
143	}
144	c.logFilter = logFilter
145	c.logOutput = logOutput
146	c.logger = log.New(logOutput, "", log.LstdFlags)
147
148	// Enable Pprof if needed
149	if c.pprofAddr != "" {
150		go func() {
151			c.UI.Output(fmt.Sprintf("Starting pprof HTTP endpoints on "+
152				"http://%s/debug/pprof", c.pprofAddr))
153			log.Fatal(http.ListenAndServe(c.pprofAddr, nil))
154		}()
155	}
156
157	// Setup Consul client
158	client, err := c.http.APIClient()
159	if err != nil {
160		c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
161		return 1
162	}
163
164	// Output this first since the config watcher below will output
165	// other information.
166	c.UI.Output("Consul Connect proxy starting...")
167
168	// Get the proper configuration watcher
169	cfgWatcher, err := c.configWatcher(client)
170	if err != nil {
171		c.UI.Error(fmt.Sprintf("Error preparing configuration: %s", err))
172		return 1
173	}
174
175	p, err := proxyImpl.New(client, cfgWatcher, c.logger)
176	if err != nil {
177		c.UI.Error(fmt.Sprintf("Failed initializing proxy: %s", err))
178		return 1
179	}
180
181	// Hook the shutdownCh up to close the proxy
182	go func() {
183		<-c.shutdownCh
184		p.Close()
185	}()
186
187	// Register the service if we requested it
188	if c.register {
189		monitor, err := c.registerMonitor(client)
190		if err != nil {
191			c.UI.Error(fmt.Sprintf("Failed initializing registration: %s", err))
192			return 1
193		}
194
195		go monitor.Run()
196		defer monitor.Close()
197	}
198
199	c.UI.Info("")
200	c.UI.Output("Log data will now stream in as it occurs:\n")
201	logGate.Flush()
202
203	// Run the proxy unless our tests require we don't
204	if !c.testNoStart {
205		if err := p.Serve(); err != nil {
206			c.UI.Error(fmt.Sprintf("Failed running proxy: %s", err))
207		}
208	}
209
210	c.UI.Output("Consul Connect proxy shutdown")
211	return 0
212}
213
214func (c *cmd) lookupProxyIDForSidecar(client *api.Client) (string, error) {
215	return LookupProxyIDForSidecar(client, c.sidecarFor)
216}
217
218// LookupProxyIDForSidecar finds candidate local proxy registrations that are a
219// sidecar for the given service. It will return an ID if and only if there is
220// exactly one registered connect proxy with `Proxy.DestinationServiceID` set to
221// the specified service ID.
222//
223// This is exported to share it with the connect envoy command.
224func LookupProxyIDForSidecar(client *api.Client, sidecarFor string) (string, error) {
225	svcs, err := client.Agent().Services()
226	if err != nil {
227		return "", fmt.Errorf("Failed looking up sidecar proxy info for %s: %s",
228			sidecarFor, err)
229	}
230
231	var proxyIDs []string
232	for _, svc := range svcs {
233		if svc.Kind == api.ServiceKindConnectProxy && svc.Proxy != nil &&
234			strings.ToLower(svc.Proxy.DestinationServiceID) == sidecarFor {
235			proxyIDs = append(proxyIDs, svc.ID)
236		}
237	}
238
239	if len(proxyIDs) == 0 {
240		return "", fmt.Errorf("No sidecar proxy registered for %s", sidecarFor)
241	}
242	if len(proxyIDs) > 1 {
243		return "", fmt.Errorf("More than one sidecar proxy registered for %s.\n"+
244			"    Start proxy with -proxy-id and one of the following IDs: %s",
245			sidecarFor, strings.Join(proxyIDs, ", "))
246	}
247	return proxyIDs[0], nil
248}
249
250func (c *cmd) configWatcher(client *api.Client) (proxyImpl.ConfigWatcher, error) {
251	// Use the configured proxy ID
252	if c.proxyID != "" {
253		c.UI.Info("Configuration mode: Agent API")
254		c.UI.Info(fmt.Sprintf("          Proxy ID: %s", c.proxyID))
255		return proxyImpl.NewAgentConfigWatcher(client, c.proxyID, c.logger)
256	}
257
258	if c.sidecarFor != "" {
259		// Running as a sidecar, we need to find the proxy-id for the requested
260		// service
261		var err error
262		c.proxyID, err = c.lookupProxyIDForSidecar(client)
263		if err != nil {
264			return nil, err
265		}
266
267		c.UI.Info("Configuration mode: Agent API")
268		c.UI.Info(fmt.Sprintf("    Sidecar for ID: %s", c.sidecarFor))
269		c.UI.Info(fmt.Sprintf("          Proxy ID: %s", c.proxyID))
270		return proxyImpl.NewAgentConfigWatcher(client, c.proxyID, c.logger)
271	}
272
273	// Otherwise, we're representing a manually specified service.
274	if c.service == "" {
275		return nil, fmt.Errorf(
276			"-service or -proxy-id must be specified so that proxy can " +
277				"configure itself.")
278	}
279
280	c.UI.Info("Configuration mode: Flags")
281	c.UI.Info(fmt.Sprintf("           Service: %s", c.service))
282
283	// Convert our upstreams to a slice of configurations. We do this
284	// deterministically by alphabetizing the upstream keys. We do this so
285	// that tests can compare the upstream values.
286	upstreamKeys := make([]string, 0, len(c.upstreams))
287	for k := range c.upstreams {
288		upstreamKeys = append(upstreamKeys, k)
289	}
290	sort.Strings(upstreamKeys)
291	upstreams := make([]proxyImpl.UpstreamConfig, 0, len(c.upstreams))
292	for _, k := range upstreamKeys {
293		config := c.upstreams[k]
294
295		c.UI.Info(fmt.Sprintf(
296			"          Upstream: %s => %s:%d",
297			k, config.LocalBindAddress, config.LocalBindPort))
298		upstreams = append(upstreams, config)
299	}
300
301	// Parse out our listener if we have one
302	var listener proxyImpl.PublicListenerConfig
303	if c.listen != "" {
304		host, port, err := c.listenParts()
305		if err != nil {
306			return nil, err
307		}
308
309		if c.serviceAddr == "" {
310			return nil, fmt.Errorf(
311				"-service-addr must be specified with -listen so the proxy " +
312					"knows the backend service address.")
313		}
314
315		c.UI.Info(fmt.Sprintf("   Public listener: %s:%d => %s", host, port, c.serviceAddr))
316		listener.BindAddress = host
317		listener.BindPort = port
318		listener.LocalServiceAddress = c.serviceAddr
319	} else {
320		c.UI.Info(fmt.Sprintf("   Public listener: Disabled"))
321	}
322
323	return proxyImpl.NewStaticConfigWatcher(&proxyImpl.Config{
324		ProxiedServiceName: c.service,
325		PublicListener:     listener,
326		Upstreams:          upstreams,
327	}), nil
328}
329
330// registerMonitor returns the registration monitor ready to be started.
331func (c *cmd) registerMonitor(client *api.Client) (*RegisterMonitor, error) {
332	if c.service == "" || c.listen == "" {
333		return nil, fmt.Errorf("-register may only be specified with -service and -listen")
334	}
335
336	host, port, err := c.listenParts()
337	if err != nil {
338		return nil, err
339	}
340
341	m := NewRegisterMonitor()
342	m.Logger = c.logger
343	m.Client = client
344	m.Service = c.service
345	m.IDSuffix = c.registerId
346	m.LocalAddress = host
347	m.LocalPort = port
348	return m, nil
349}
350
351// listenParts returns the host and port parts of the -listen flag. The
352// -listen flag must be non-empty prior to calling this.
353func (c *cmd) listenParts() (string, int, error) {
354	host, portRaw, err := net.SplitHostPort(c.listen)
355	if err != nil {
356		return "", 0, err
357	}
358
359	port, err := strconv.ParseInt(portRaw, 0, 0)
360	if err != nil {
361		return "", 0, err
362	}
363
364	return host, int(port), nil
365}
366
367func (c *cmd) Synopsis() string {
368	return synopsis
369}
370
371func (c *cmd) Help() string {
372	return c.help
373}
374
375const synopsis = "Runs a Consul Connect proxy"
376const help = `
377Usage: consul connect proxy [options]
378
379  Starts a Consul Connect proxy and runs until an interrupt is received.
380  The proxy can be used to accept inbound connections for a service,
381  wrap outbound connections to upstream services, or both. This enables
382  a non-Connect-aware application to use Connect.
383
384  The proxy requires service:write permissions for the service it represents.
385  The token may be passed via the CLI or the CONSUL_TOKEN environment
386  variable.
387
388  Consul can automatically start and manage this proxy by specifying the
389  "proxy" configuration within your service definition.
390
391  The example below shows how to start a local proxy for establishing outbound
392  connections to "db" representing the frontend service. Once running, any
393  process that creates a TCP connection to the specified port (8181) will
394  establish a mutual TLS connection to "db" identified as "frontend".
395
396    $ consul connect proxy -service frontend -upstream db:8181
397
398  The next example starts a local proxy that also accepts inbound connections
399  on port 8443, authorizes the connection, then proxies it to port 8080:
400
401    $ consul connect proxy \
402        -service frontend \
403        -service-addr 127.0.0.1:8080 \
404        -listen ':8443'
405
406  A proxy can accept both inbound connections as well as proxy to upstream
407  services by specifying both the "-listen" and "-upstream" flags.
408
409`
410