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)