From c8c31aea62e79a505e022c2d37b96d0bb8ae2bbd Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Fri, 25 Aug 2017 21:32:03 +0200 Subject: [PATCH] Add proxy protocol --- README.md | 3 +- build.Dockerfile | 11 +- configuration/configuration.go | 15 +- docs/toml.md | 6 + glide.lock | 6 +- glide.yaml | 2 + server/server.go | 46 ++-- server/server_test.go | 2 +- traefik.sample.toml | 6 + vendor/github.com/armon/go-proxyproto/LICENSE | 21 ++ .../armon/go-proxyproto/protocol.go | 244 ++++++++++++++++++ 11 files changed, 333 insertions(+), 29 deletions(-) create mode 100644 vendor/github.com/armon/go-proxyproto/LICENSE create mode 100644 vendor/github.com/armon/go-proxyproto/protocol.go diff --git a/README.md b/README.md index 49f573b55..a3f0e8f83 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ Run it and forget it! - Websocket, HTTP/2, GRPC ready - Access Logs (JSON, CLF) - [Let's Encrypt](https://letsencrypt.org) support (Automatic HTTPS with renewal) -- High Availability with cluster mode +- [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support +- High Availability with cluster mode (beta) ## Supported backends diff --git a/build.Dockerfile b/build.Dockerfile index 244716c35..d85036b9c 100644 --- a/build.Dockerfile +++ b/build.Dockerfile @@ -1,11 +1,8 @@ -FROM golang:1.8 +FROM golang:1.9-alpine -# Install a more recent version of mercurial to avoid mismatching results -# between glide run on a decently updated host system and the build container. -RUN awk '$1 ~ "^deb" { $3 = $3 "-backports"; print; exit }' /etc/apt/sources.list > /etc/apt/sources.list.d/backports.list && \ - DEBIAN_FRONTEND=noninteractive apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -t jessie-backports --yes --no-install-recommends mercurial=3.9.1-1~bpo8+1 && \ - rm -fr /var/lib/apt/lists/ +RUN apk --update upgrade \ +&& apk --no-cache --no-progress add git mercurial bash gcc musl-dev curl tar \ +&& rm -rf /var/cache/apk/* RUN go get github.com/jteeuwen/go-bindata/... \ && go get github.com/golang/lint/golint \ diff --git a/configuration/configuration.go b/configuration/configuration.go index 2f3b6f02b..a8494436a 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -193,7 +193,7 @@ func (ep *EntryPoints) String() string { // Set's argument is a string to be parsed to set the flag. // It's a comma-separated list, so we split it. func (ep *EntryPoints) Set(value string) error { - regex := regexp.MustCompile(`(?:Name:(?P\S*))\s*(?:Address:(?P
\S*))?\s*(?:TLS:(?P\S*))?\s*((?PTLS))?\s*(?:CA:(?P\S*))?\s*(?:Redirect.EntryPoint:(?P\S*))?\s*(?:Redirect.Regex:(?P\\S*))?\s*(?:Redirect.Replacement:(?P\S*))?\s*(?:Compress:(?P\S*))?\s*(?:WhiteListSourceRange:(?P\S*))?`) + regex := regexp.MustCompile(`(?:Name:(?P\S*))\s*(?:Address:(?P
\S*))?\s*(?:TLS:(?P\S*))?\s*((?PTLS))?\s*(?:CA:(?P\S*))?\s*(?:Redirect.EntryPoint:(?P\S*))?\s*(?:Redirect.Regex:(?P\\S*))?\s*(?:Redirect.Replacement:(?P\S*))?\s*(?:Compress:(?P\S*))?\s*(?:WhiteListSourceRange:(?P\S*))?\s*(?:ProxyProtocol:(?P\S*))?`) match := regex.FindAllStringSubmatch(value, -1) if match == nil { return fmt.Errorf("bad EntryPoints format: %s", value) @@ -234,7 +234,9 @@ func (ep *EntryPoints) Set(value string) error { compress := false if len(result["Compress"]) > 0 { - compress = strings.EqualFold(result["Compress"], "enable") || strings.EqualFold(result["Compress"], "on") + compress = strings.EqualFold(result["Compress"], "true") || + strings.EqualFold(result["Compress"], "enable") || + strings.EqualFold(result["Compress"], "on") } whiteListSourceRange := []string{} @@ -242,12 +244,20 @@ func (ep *EntryPoints) Set(value string) error { whiteListSourceRange = strings.Split(result["WhiteListSourceRange"], ",") } + proxyprotocol := false + if len(result["ProxyProtocol"]) > 0 { + proxyprotocol = strings.EqualFold(result["ProxyProtocol"], "true") || + strings.EqualFold(result["ProxyProtocol"], "enable") || + strings.EqualFold(result["ProxyProtocol"], "on") + } + (*ep)[result["Name"]] = &EntryPoint{ Address: result["Address"], TLS: configTLS, Redirect: redirect, Compress: compress, WhitelistSourceRange: whiteListSourceRange, + ProxyProtocol: proxyprotocol, } return nil @@ -277,6 +287,7 @@ type EntryPoint struct { Auth *types.Auth WhitelistSourceRange []string Compress bool + ProxyProtocol bool } // Redirect configures a redirection of an entry point to another, or to an URL diff --git a/docs/toml.md b/docs/toml.md index 859b45fc1..fb55fca88 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -292,6 +292,12 @@ To write JSON format logs, specify `json` as the format: # address = ":80" # whiteListSourceRange = ["127.0.0.1/32"] +# To enable ProxyProtocol support (https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt): +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# proxyprotocol = true + [entryPoints] [entryPoints.http] address = ":80" diff --git a/glide.lock b/glide.lock index a6ddf84b2..ec701f9d6 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 3d5a06016b7b56be08120ed653406a1e8d4ade7e69b4fbc37b31683cb4e9a519 -updated: 2017-08-21T14:15:06.346751095+02:00 +hash: 2b042ce06e9c4aed4606f2b8ced5d6c3de537d1254316e8c6611e78d934a024a +updated: 2017-08-24T14:24:42.04425168+02:00 imports: - name: cloud.google.com/go version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c @@ -10,6 +10,8 @@ imports: version: 0ddd408d5d60ea76e320503cc7dd091992dee608 - name: github.com/aokoli/goutils version: 3391d3790d23d03408670993e957e8f408993c34 +- name: github.com/armon/go-proxyproto + version: 48572f11356f1843b694f21a290d4f1006bc5e47 - name: github.com/ArthurHlt/go-eureka-client version: 9d0a49cbd39aa3634ae1977e9f519a262b10adaf subpackages: diff --git a/glide.yaml b/glide.yaml index ba1b19d9d..3dd18b441 100644 --- a/glide.yaml +++ b/glide.yaml @@ -202,6 +202,8 @@ import: - spew - package: github.com/Masterminds/sprig version: e039e20e500c2c025d9145be375e27cf42a94174 +- package: github.com/armon/go-proxyproto + version: 48572f11356f1843b694f21a290d4f1006bc5e47 testImport: - package: github.com/stvp/go-udp-testing - package: github.com/docker/libcompose diff --git a/server/server.go b/server/server.go index 4c7faa44b..1069588ca 100644 --- a/server/server.go +++ b/server/server.go @@ -18,6 +18,7 @@ import ( "sync" "time" + "github.com/armon/go-proxyproto" "github.com/containous/mux" "github.com/containous/traefik/cluster" "github.com/containous/traefik/configuration" @@ -65,6 +66,7 @@ type serverEntryPoints map[string]*serverEntryPoint type serverEntryPoint struct { httpServer *http.Server + listener net.Listener httpRouter *middlewares.HandlerSwitcher } @@ -259,7 +261,7 @@ func (server *Server) startHTTPServers() { for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints { serverEntryPoint := server.setupServerEntryPoint(newServerEntryPointName, newServerEntryPoint) - go server.startServer(serverEntryPoint.httpServer, server.globalConfiguration) + go server.startServer(serverEntryPoint, server.globalConfiguration) } } @@ -296,12 +298,13 @@ func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newS } serverMiddlewares = append(serverMiddlewares, ipWhitelistMiddleware) } - newSrv, err := server.prepareServer(newServerEntryPointName, server.globalConfiguration.EntryPoints[newServerEntryPointName], newServerEntryPoint.httpRouter, serverMiddlewares...) + newSrv, listener, err := server.prepareServer(newServerEntryPointName, server.globalConfiguration.EntryPoints[newServerEntryPointName], newServerEntryPoint.httpRouter, serverMiddlewares...) if err != nil { log.Fatal("Error preparing server: ", err) } serverEntryPoint := server.serverEntryPoints[newServerEntryPointName] serverEntryPoint.httpServer = newSrv + serverEntryPoint.listener = listener return serverEntryPoint } @@ -611,20 +614,20 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *configur return config, nil } -func (server *Server) startServer(srv *http.Server, globalConfiguration configuration.GlobalConfiguration) { - log.Infof("Starting server on %s", srv.Addr) +func (server *Server) startServer(serverEntryPoint *serverEntryPoint, globalConfiguration configuration.GlobalConfiguration) { + log.Infof("Starting server on %s", serverEntryPoint.httpServer.Addr) var err error - if srv.TLSConfig != nil { - err = srv.ListenAndServeTLS("", "") + if serverEntryPoint.httpServer.TLSConfig != nil { + err = serverEntryPoint.httpServer.ServeTLS(serverEntryPoint.listener, "", "") } else { - err = srv.ListenAndServe() + err = serverEntryPoint.httpServer.Serve(serverEntryPoint.listener) } if err != nil { log.Error("Error creating server: ", err) } } -func (server *Server) prepareServer(entryPointName string, entryPoint *configuration.EntryPoint, router *middlewares.HandlerSwitcher, middlewares ...negroni.Handler) (*http.Server, error) { +func (server *Server) prepareServer(entryPointName string, entryPoint *configuration.EntryPoint, router *middlewares.HandlerSwitcher, middlewares ...negroni.Handler) (*http.Server, net.Listener, error) { readTimeout, writeTimeout, idleTimeout := buildServerTimeouts(server.globalConfiguration) log.Infof("Preparing server %s %+v with readTimeout=%s writeTimeout=%s idleTimeout=%s", entryPointName, entryPoint, readTimeout, writeTimeout, idleTimeout) @@ -638,17 +641,28 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura tlsConfig, err := server.createTLSConfig(entryPointName, entryPoint.TLS, router) if err != nil { log.Errorf("Error creating TLS config: %s", err) - return nil, err + return nil, nil, err + } + + listener, err := net.Listen("tcp", entryPoint.Address) + if err != nil { + log.Error("Error opening listener ", err) + } + + if entryPoint.ProxyProtocol { + listener = &proxyproto.Listener{Listener: listener} } return &http.Server{ - Addr: entryPoint.Address, - Handler: n, - TLSConfig: tlsConfig, - ReadTimeout: readTimeout, - WriteTimeout: writeTimeout, - IdleTimeout: idleTimeout, - }, nil + Addr: entryPoint.Address, + Handler: n, + TLSConfig: tlsConfig, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + IdleTimeout: idleTimeout, + }, + listener, + nil } func buildServerTimeouts(globalConfig configuration.GlobalConfiguration) (readTimeout, writeTimeout, idleTimeout time.Duration) { diff --git a/server/server_test.go b/server/server_test.go index 78c2e2ee4..a6fb23e1d 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -100,7 +100,7 @@ func TestPrepareServerTimeouts(t *testing.T) { router := middlewares.NewHandlerSwitcher(mux.NewRouter()) srv := NewServer(test.globalConfig) - httpServer, err := srv.prepareServer(entryPointName, entryPoint, router) + httpServer, _, err := srv.prepareServer(entryPointName, entryPoint, router) if err != nil { t.Fatalf("Unexpected error when preparing srv: %s", err) } diff --git a/traefik.sample.toml b/traefik.sample.toml index ba29c548b..a30417562 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -344,6 +344,12 @@ # address = ":80" # whiteListSourceRange = ["127.0.0.1/32"] +# To enable ProxyProtocol support (https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt): +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# proxyprotocol = true + # Enable retry sending request if network error # # Optional diff --git a/vendor/github.com/armon/go-proxyproto/LICENSE b/vendor/github.com/armon/go-proxyproto/LICENSE new file mode 100644 index 000000000..3ed5f4302 --- /dev/null +++ b/vendor/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/vendor/github.com/armon/go-proxyproto/protocol.go b/vendor/github.com/armon/go-proxyproto/protocol.go new file mode 100644 index 000000000..576507294 --- /dev/null +++ b/vendor/github.com/armon/go-proxyproto/protocol.go @@ -0,0 +1,244 @@ +package proxyproto + +import ( + "bufio" + "bytes" + "errors" + "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) + + ErrInvalidUpstream = errors.New("upstream connection address not trusted for PROXY information") +) + +// SourceChecker can be used to decide whether to trust the PROXY info or pass +// the original connection address through. If set, the connecting address is +// passed in as an argument. If the function returns an error due to the source +// being disallowed, it should return ErrInvalidUpstream. +// +// Behavior is as follows: +// * If error is not nil, the call to Accept() will fail. If the reason for +// triggering this failure is due to a disallowed source, it should return +// ErrInvalidUpstream. +// * If bool is true, the PROXY-set address is used. +// * If bool is false, the connection's remote address is used, rather than the +// address claimed in the PROXY info. +type SourceChecker func(net.Addr) (bool, error) + +// 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. +// +// Optionally define ProxyHeaderTimeout to set a maximum time to +// receive the Proxy Protocol Header. Zero means no timeout. +type Listener struct { + Listener net.Listener + ProxyHeaderTimeout time.Duration + SourceCheck SourceChecker +} + +// 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 + useConnRemoteAddr bool + once sync.Once + proxyHeaderTimeout time.Duration +} + +// 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 + } + var useConnRemoteAddr bool + if p.SourceCheck != nil { + allowed, err := p.SourceCheck(conn.RemoteAddr()) + if err != nil { + return nil, err + } + if !allowed { + useConnRemoteAddr = true + } + } + newConn := NewConn(conn, p.ProxyHeaderTimeout) + newConn.useConnRemoteAddr = useConnRemoteAddr + return newConn, 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, timeout time.Duration) *Conn { + pConn := &Conn{ + bufReader: bufio.NewReader(conn), + conn: conn, + proxyHeaderTimeout: timeout, + } + 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) + p.Close() + p.bufReader = bufio.NewReader(p.conn) + } + }) + if p.srcAddr != nil && !p.useConnRemoteAddr { + 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 { + if p.proxyHeaderTimeout != 0 { + readDeadLine := time.Now().Add(p.proxyHeaderTimeout) + p.conn.SetReadDeadline(readDeadLine) + defer p.conn.SetReadDeadline(time.Time{}) + } + + // Incrementally check each byte of the prefix + for i := 1; i <= prefixLen; i++ { + inp, err := p.bufReader.Peek(i) + + if err != nil { + if neterr, ok := err.(net.Error); ok && neterr.Timeout() { + return nil + } else { + 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 +}