From cfed0a24b5597f353dc3f8175041442bfbb0aadf Mon Sep 17 00:00:00 2001 From: Jonty Wareing Date: Thu, 31 Mar 2016 14:06:24 +0100 Subject: [PATCH] Add optional support for the PROXY protocol This adds support for the commonly implemented PROXY protocol, allowing TCP proxies to pass along upstream client information. When this is enabled gorouter will read the PROXY preamble and inject the upstream information into the `X-Forwarded-For` header. http://blog.haproxy.com/haproxy/proxy-protocol/ --- Godeps/Godeps.json | 4 + .../github.com/armon/go-proxyproto/.gitignore | 2 + .../github.com/armon/go-proxyproto/LICENSE | 21 ++ .../github.com/armon/go-proxyproto/README.md | 36 ++++ .../armon/go-proxyproto/protocol.go | 194 ++++++++++++++++++ config/config.go | 12 +- config/config_test.go | 10 + router/router.go | 17 +- router/router_test.go | 29 +++ 9 files changed, 316 insertions(+), 9 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/armon/go-proxyproto/.gitignore create mode 100644 Godeps/_workspace/src/github.com/armon/go-proxyproto/LICENSE create mode 100644 Godeps/_workspace/src/github.com/armon/go-proxyproto/README.md create mode 100644 Godeps/_workspace/src/github.com/armon/go-proxyproto/protocol.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index ee3cfc35f..8a1ef6b00 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -10,6 +10,10 @@ "Comment": "v1.0.9", "Rev": "574d117d5095925c53a2c86e6283800118dbfff7" }, + { + "ImportPath": "github.com/armon/go-proxyproto", + "Rev": "609d6338d3a76ec26ac3fe7045a164d9a58436e7" + }, { "ImportPath": "github.com/bmizerany/pat", "Rev": "b8a35001b773c267eb260a691f4e5499a3531600" diff --git a/Godeps/_workspace/src/github.com/armon/go-proxyproto/.gitignore b/Godeps/_workspace/src/github.com/armon/go-proxyproto/.gitignore new file mode 100644 index 000000000..dd2440d55 --- /dev/null +++ b/Godeps/_workspace/src/github.com/armon/go-proxyproto/.gitignore @@ -0,0 +1,2 @@ +*.test +*~ diff --git a/Godeps/_workspace/src/github.com/armon/go-proxyproto/LICENSE b/Godeps/_workspace/src/github.com/armon/go-proxyproto/LICENSE new file mode 100644 index 000000000..3ed5f4302 --- /dev/null +++ b/Godeps/_workspace/src/github.com/armon/go-proxyproto/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Armon Dadgar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/armon/go-proxyproto/README.md b/Godeps/_workspace/src/github.com/armon/go-proxyproto/README.md new file mode 100644 index 000000000..25a779cca --- /dev/null +++ b/Godeps/_workspace/src/github.com/armon/go-proxyproto/README.md @@ -0,0 +1,36 @@ +# proxyproto + +This library provides the `proxyproto` package which can be used for servers +listening behind HAProxy of Amazon ELB load balancers. Those load balancers +support the use of a proxy protocol (http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt), +which provides a simple mechansim for the server to get the address of the client +instead of the load balancer. + +This library provides both a net.Listener and net.Conn implementation that +can be used to handle situation in which you may be using the proxy protocol. +Only proxy protocol version 1, the human-readable form, is understood. + +The only caveat is that we check for the "PROXY " prefix to determine if the protocol +is being used. If that string may occur as part of your input, then it is ambiguous +if the protocol is being used and you may have problems. + +# Documentation + +Full documentation can be found [here](http://godoc.org/github.com/armon/go-proxyproto). + +# Examples + +Using the library is very simple: + +``` + +// Create a listener +list, err := net.Listen("tcp", "...") + +// Wrap listener in a proxyproto listener +proxyList := &proxyproto.Listener{list} +conn, err :=proxyList.Accept() + +... +``` + diff --git a/Godeps/_workspace/src/github.com/armon/go-proxyproto/protocol.go b/Godeps/_workspace/src/github.com/armon/go-proxyproto/protocol.go new file mode 100644 index 000000000..2fc1dfc01 --- /dev/null +++ b/Godeps/_workspace/src/github.com/armon/go-proxyproto/protocol.go @@ -0,0 +1,194 @@ +package proxyproto + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "net" + "strconv" + "strings" + "sync" + "time" +) + +var ( + // prefix is the string we look for at the start of a connection + // to check if this connection is using the proxy protocol + prefix = []byte("PROXY ") + prefixLen = len(prefix) +) + +// Listener is used to wrap an underlying listener, +// whose connections may be using the HAProxy Proxy Protocol (version 1). +// If the connection is using the protocol, the RemoteAddr() will return +// the correct client address. +type Listener struct { + Listener net.Listener +} + +// Conn is used to wrap and underlying connection which +// may be speaking the Proxy Protocol. If it is, the RemoteAddr() will +// return the address of the client instead of the proxy address. +type Conn struct { + bufReader *bufio.Reader + conn net.Conn + dstAddr *net.TCPAddr + srcAddr *net.TCPAddr + once sync.Once +} + +// Accept waits for and returns the next connection to the listener. +func (p *Listener) Accept() (net.Conn, error) { + // Get the underlying connection + conn, err := p.Listener.Accept() + if err != nil { + return nil, err + } + return NewConn(conn), nil +} + +// Close closes the underlying listener. +func (p *Listener) Close() error { + return p.Listener.Close() +} + +// Addr returns the underlying listener's network address. +func (p *Listener) Addr() net.Addr { + return p.Listener.Addr() +} + +// NewConn is used to wrap a net.Conn that may be speaking +// the proxy protocol into a proxyproto.Conn +func NewConn(conn net.Conn) *Conn { + pConn := &Conn{ + bufReader: bufio.NewReader(conn), + conn: conn, + } + return pConn +} + +// Read is check for the proxy protocol header when doing +// the initial scan. If there is an error parsing the header, +// it is returned and the socket is closed. +func (p *Conn) Read(b []byte) (int, error) { + var err error + p.once.Do(func() { err = p.checkPrefix() }) + if err != nil { + return 0, err + } + return p.bufReader.Read(b) +} + +func (p *Conn) Write(b []byte) (int, error) { + return p.conn.Write(b) +} + +func (p *Conn) Close() error { + return p.conn.Close() +} + +func (p *Conn) LocalAddr() net.Addr { + return p.conn.LocalAddr() +} + +// RemoteAddr returns the address of the client if the proxy +// protocol is being used, otherwise just returns the address of +// the socket peer. If there is an error parsing the header, the +// address of the client is not returned, and the socket is closed. +// Once implication of this is that the call could block if the +// client is slow. Using a Deadline is recommended if this is called +// before Read() +func (p *Conn) RemoteAddr() net.Addr { + p.once.Do(func() { + if err := p.checkPrefix(); err != nil && err != io.EOF { + log.Printf("[ERR] Failed to read proxy prefix: %v", err) + } + }) + if p.srcAddr != nil { + return p.srcAddr + } + return p.conn.RemoteAddr() +} + +func (p *Conn) SetDeadline(t time.Time) error { + return p.conn.SetDeadline(t) +} + +func (p *Conn) SetReadDeadline(t time.Time) error { + return p.conn.SetReadDeadline(t) +} + +func (p *Conn) SetWriteDeadline(t time.Time) error { + return p.conn.SetWriteDeadline(t) +} + +func (p *Conn) checkPrefix() error { + // Incrementally check each byte of the prefix + for i := 1; i <= prefixLen; i++ { + inp, err := p.bufReader.Peek(i) + if err != nil { + return err + } + + // Check for a prefix mis-match, quit early + if !bytes.Equal(inp, prefix[:i]) { + return nil + } + } + + // Read the header line + header, err := p.bufReader.ReadString('\n') + if err != nil { + p.conn.Close() + return err + } + + // Strip the carriage return and new line + header = header[:len(header)-2] + + // Split on spaces, should be (PROXY ) + parts := strings.Split(header, " ") + if len(parts) != 6 { + p.conn.Close() + return fmt.Errorf("Invalid header line: %s", header) + } + + // Verify the type is known + switch parts[1] { + case "TCP4": + case "TCP6": + default: + p.conn.Close() + return fmt.Errorf("Unhandled address type: %s", parts[1]) + } + + // Parse out the source address + ip := net.ParseIP(parts[2]) + if ip == nil { + p.conn.Close() + return fmt.Errorf("Invalid source ip: %s", parts[2]) + } + port, err := strconv.Atoi(parts[4]) + if err != nil { + p.conn.Close() + return fmt.Errorf("Invalid source port: %s", parts[4]) + } + p.srcAddr = &net.TCPAddr{IP: ip, Port: port} + + // Parse out the destination address + ip = net.ParseIP(parts[3]) + if ip == nil { + p.conn.Close() + return fmt.Errorf("Invalid destination ip: %s", parts[3]) + } + port, err = strconv.Atoi(parts[5]) + if err != nil { + p.conn.Close() + return fmt.Errorf("Invalid destination port: %s", parts[5]) + } + p.dstAddr = &net.TCPAddr{IP: ip, Port: port} + + return nil +} diff --git a/config/config.go b/config/config.go index 2dd345d19..920d45cce 100644 --- a/config/config.go +++ b/config/config.go @@ -87,6 +87,7 @@ type Config struct { AccessLog AccessLog `yaml:"access_log"` EnableAccessLogStreaming bool `yaml:"enable_access_log_streaming"` DebugAddr string `yaml:"debug_addr"` + EnablePROXY bool `yaml:"enable_proxy"` EnableSSL bool `yaml:"enable_ssl"` SSLPort uint16 `yaml:"ssl_port"` SSLCertPath string `yaml:"ssl_cert_path"` @@ -141,11 +142,12 @@ var defaultConfig = Config{ Nats: []NatsConfig{defaultNatsConfig}, Logging: defaultLoggingConfig, - Port: 8081, - Index: 0, - GoMaxProcs: -1, - EnableSSL: false, - SSLPort: 443, + Port: 8081, + Index: 0, + GoMaxProcs: -1, + EnablePROXY: false, + EnableSSL: false, + SSLPort: 443, EndpointTimeoutInSeconds: 60, RouteServiceTimeoutInSeconds: 60, diff --git a/config/config_test.go b/config/config_test.go index ec6760152..ad411b144 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -265,6 +265,16 @@ token_fetcher_expiration_buffer_time: 40 Expect(config.TokenFetcherRetryIntervalInSeconds).To(Equal(5)) Expect(config.TokenFetcherExpirationBufferTimeInSeconds).To(Equal(int64(30))) }) + + It("sets proxy protocol", func() { + var b = []byte(` +enable_proxy: true +`) + + config.Initialize(b) + + Expect(config.EnablePROXY).To(Equal(true)) + }) }) Describe("Process", func() { diff --git a/router/router.go b/router/router.go index 66256f14f..aa94f02a5 100644 --- a/router/router.go +++ b/router/router.go @@ -7,6 +7,7 @@ import ( "sync" "syscall" + "github.com/armon/go-proxyproto" "github.com/apcera/nats" "github.com/cloudfoundry/dropsonde" vcap "github.com/cloudfoundry/gorouter/common" @@ -267,10 +268,14 @@ func (r *Router) serveHTTPS(server *http.Server, errChan chan error) error { } r.tlsListener = tlsListener - r.logger.Info(fmt.Sprintf("Listening on %s", tlsListener.Addr())) + if r.config.EnablePROXY { + r.tlsListener = &proxyproto.Listener{tlsListener} + } + + r.logger.Info(fmt.Sprintf("Listening on %s", r.tlsListener.Addr())) go func() { - err := server.Serve(tlsListener) + err := server.Serve(r.tlsListener) r.stopLock.Lock() if !r.stopping { errChan <- err @@ -290,10 +295,14 @@ func (r *Router) serveHTTP(server *http.Server, errChan chan error) error { } r.listener = listener - r.logger.Info(fmt.Sprintf("Listening on %s", listener.Addr())) + if r.config.EnablePROXY { + r.listener = &proxyproto.Listener{listener} + } + + r.logger.Info(fmt.Sprintf("Listening on %s", r.listener.Addr())) go func() { - err := server.Serve(listener) + err := server.Serve(r.listener) r.stopLock.Lock() if !r.stopping { errChan <- err diff --git a/router/router_test.go b/router/router_test.go index 1a5b54003..53fae11f0 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -79,6 +79,7 @@ var _ = Describe("Router", func() { config.SSLPort = 4443 + uint16(gConfig.GinkgoConfig.ParallelNode) config.SSLCertificate = cert config.CipherSuites = []uint16{tls.TLS_RSA_WITH_AES_256_CBC_SHA} + config.EnablePROXY = true // set pid file f, err := ioutil.TempFile("", "gorouter-test-pidfile-") @@ -586,6 +587,34 @@ var _ = Describe("Router", func() { Expect(string(body)).To(MatchRegexp(".*1\\.2\\.3\\.4:1234.*\n")) }) + It("handles the PROXY protocol", func() { + app := test.NewTestApp([]route.Uri{"proxy.vcap.me"}, config.Port, mbusClient, nil, "") + + rCh := make(chan string) + app.AddHandler("/", func(w http.ResponseWriter, r *http.Request) { + rCh <- r.Header.Get("X-Forwarded-For") + }) + app.Listen() + Eventually(func() bool { + return appRegistered(registry, app) + }).Should(BeTrue()) + + host := fmt.Sprintf("proxy.vcap.me:%d", config.Port) + conn, err := net.DialTimeout("tcp", host, 10*time.Second) + Expect(err).ToNot(HaveOccurred()) + defer conn.Close() + + fmt.Fprintf(conn, "PROXY TCP4 192.168.0.1 192.168.0.2 12345 80\r\n"+ + "GET / HTTP/1.0\r\n"+ + "Host: %s\r\n"+ + "\r\n", host) + + var rr string + Eventually(rCh).Should(Receive(&rr)) + Expect(rr).ToNot(BeNil()) + Expect(rr).To(Equal("192.168.0.1")) + }) + Context("HTTP keep-alive", func() { It("reuses the same connection on subsequent calls", func() { app := test.NewGreetApp([]route.Uri{"keepalive.vcap.me"}, config.Port, mbusClient, nil)