├── .github ├── dependabot.yml └── workflows │ ├── ci-build.yml │ ├── ci-release.yml │ └── codeql-analysis.yml ├── .gitignore ├── Dockerfile ├── LICENCE ├── Makefile ├── README.md ├── go.mod ├── log.go ├── main.go └── proxyprotocol ├── http.go ├── proxy.go └── proxyline ├── LICENSE ├── parser.go ├── parser_test.go └── proxy-protocol.txt /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "docker" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | name: Build and Test 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Build 18 | run: | 19 | make build 20 | 21 | - name: Change compiled binary capabilities 22 | run: | 23 | sudo setcap 'cap_net_bind_service=+ep' ./tiny-ssl-reverse-proxy 24 | 25 | - name: Generate TLS certificate 26 | run: | 27 | make fakecert 28 | 29 | - name: Run and test redirect 30 | run: | 31 | ./tiny-ssl-reverse-proxy -key key.pem -cert crt.pem & 32 | curl --cacert crt.pem https://localhost:443 | grep "Backend Unavailable" 33 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: Upload Release Asset 7 | 8 | jobs: 9 | build: 10 | name: Upload Release Asset 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | - name: Build project 16 | run: | 17 | make && mv tiny-ssl-reverse-proxy tiny-ssl-reverse-proxy_linux_amd64 18 | - name: Create Release 19 | id: create_release 20 | uses: actions/create-release@v1 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | tag_name: ${{ github.ref }} 25 | release_name: ${{ github.ref }} 26 | draft: true 27 | prerelease: false 28 | - name: Upload Release Asset 29 | id: upload-release-asset 30 | uses: actions/upload-release-asset@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | upload_url: ${{ steps.create_release.outputs.upload_url }} 35 | asset_path: ./tiny-ssl-reverse-proxy_linux_amd64 36 | asset_name: tiny-ssl-reverse-proxy_linux_amd64 37 | asset_content_type: application/octet-stream 38 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 17 * * 2' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['go'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /tiny-ssl-reverse-proxy 3 | /*.pem 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.1-alpine 2 | 3 | # Turn off cgo for a more static binary. 4 | # Specify cache directory so that we can run as nobody to build the binary. 5 | ENV CGO_ENABLED=0 XDG_CACHE_HOME=/tmp/.cache 6 | 7 | USER nobody:nogroup 8 | 9 | WORKDIR /go/src/github.com/sensiblecodeio/tiny-ssl-reverse-proxy 10 | 11 | COPY go.mod ./ 12 | RUN go mod download 13 | 14 | COPY . . 15 | RUN go install -v -buildvcs=false 16 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, The Sensible Code Company Limited 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DIST_NAME=tiny-ssl-reverse-proxy 2 | VERSION?=$(shell git describe --tags --always --dirty) 3 | 4 | all: build 5 | 6 | build: 7 | docker build -t tiny-ssl-reverse-proxy . 8 | docker run --rm tiny-ssl-reverse-proxy cat /go/bin/tiny-ssl-reverse-proxy > tiny-ssl-reverse-proxy 9 | chmod u+x tiny-ssl-reverse-proxy 10 | 11 | install: 12 | go install 13 | 14 | dist: dist/$(DIST_NAME)_darwin_amd64 dist/$(DIST_NAME)_linux_amd64 15 | 16 | dist/$(DIST_NAME)_darwin_amd64: 17 | GOOS=darwin GOARCH=amd64 go build -o $@ 18 | 19 | dist/$(DIST_NAME)_linux_amd64: 20 | GOOS=linux GOARCH=amd64 go build -o $@ 21 | 22 | rel: dist 23 | hub release create -a dist $(VERSION) 24 | 25 | fakecert: .FORCE 26 | openssl req \ 27 | -x509 \ 28 | -nodes \ 29 | -sha256 \ 30 | -newkey rsa:2048 \ 31 | -keyout key.pem \ 32 | -out crt.pem \ 33 | -subj '/L=Earth/O=Fake Certificate/CN=localhost/' \ 34 | -days 365 35 | 36 | release: 37 | hub release create -a tiny-ssl-reverse-proxy_linux_amd64 -a tiny-ssl-reverse-proxy_darwin_amd64 $(shell git describe --tags --exact-match) 38 | 39 | 40 | .FORCE: 41 | 42 | .PHONY: all build install rel 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A tiny SSL reverse proxy 2 | ======================== 3 | 4 | Did you ever want to protect your docker container with SSL, but 5 | didn't want to have to pull the whole of nginx? Well now you can! 6 | 7 | Usage: 8 | ------ 9 | 10 | ``` 11 | tiny-ssl-reverse-proxy 12 | 13 | Usage of tiny-ssl-reverse-proxy: 14 | -behind-tcp-proxy 15 | running behind TCP proxy (such as ELB or HAProxy) 16 | -cert string 17 | Path to PEM certificate (default "/etc/ssl/private/cert.pem") 18 | -flush-interval duration 19 | minimum duration between flushes to the client (default: off) 20 | -key string 21 | Path to PEM key (default "/etc/ssl/private/key.pem") 22 | -listen string 23 | Bind address to listen on (default ":443") 24 | -logging 25 | log requests (default true) 26 | -tls 27 | accept HTTPS connections (default true) 28 | -where string 29 | Place to forward connections to (default "http://localhost:80") 30 | ``` 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sensiblecodeio/tiny-ssl-reverse-proxy 2 | 3 | go 1.23.1 4 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | type LoggingMiddleware struct { 15 | http.Handler 16 | } 17 | 18 | type ResponseRecorder struct { 19 | ResponseWriter http.ResponseWriter 20 | response int 21 | *WriteCounter 22 | } 23 | 24 | func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder { 25 | return &ResponseRecorder{w, 0, &WriteCounter{w, 0}} 26 | } 27 | 28 | func (r *ResponseRecorder) Header() http.Header { 29 | return r.ResponseWriter.Header() 30 | } 31 | 32 | func (r *ResponseRecorder) WriteHeader(n int) { 33 | r.ResponseWriter.WriteHeader(n) 34 | r.response = n 35 | } 36 | 37 | func (r *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { 38 | hijacker, ok := r.ResponseWriter.(http.Hijacker) 39 | if !ok { 40 | return nil, nil, fmt.Errorf("Not a Hijacker: %T", r.ResponseWriter) 41 | } 42 | return hijacker.Hijack() 43 | } 44 | 45 | func (r *ResponseRecorder) Flush() { 46 | flusher, ok := r.ResponseWriter.(http.Flusher) 47 | if !ok { 48 | return 49 | } 50 | flusher.Flush() 51 | } 52 | 53 | type WriteCounter struct { 54 | io.Writer 55 | nBytes int 56 | } 57 | 58 | func (r *WriteCounter) Write(bs []byte) (n int, err error) { 59 | if r.Writer != nil { 60 | n, err = r.Writer.Write(bs) 61 | } else { 62 | n = len(bs) 63 | } 64 | r.nBytes += n 65 | return n, err 66 | } 67 | 68 | func (x *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { 69 | recorder := NewResponseRecorder(w) 70 | 71 | uploaded := &WriteCounter{Writer: ioutil.Discard} 72 | r.Body = struct { 73 | io.Reader 74 | io.Closer 75 | }{io.TeeReader(r.Body, uploaded), r.Body} 76 | 77 | start := time.Now() 78 | x.Handler.ServeHTTP(recorder, r) 79 | duration := time.Since(start) 80 | 81 | log.Printf("%21v %3d %10d %10d %7.1fms %4v %v%-30v %v", 82 | r.RemoteAddr, 83 | recorder.response, 84 | uploaded.nBytes, 85 | recorder.nBytes, 86 | duration.Seconds()*1000, 87 | r.Method, 88 | r.URL.Host, 89 | r.URL.EscapedPath(), 90 | r.Header.Get("User-Agent")) 91 | } 92 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "net/http" 11 | "net/http/httputil" 12 | "net/url" 13 | "os" 14 | "time" 15 | 16 | "github.com/sensiblecodeio/tiny-ssl-reverse-proxy/proxyprotocol" 17 | ) 18 | 19 | // Version number 20 | const Version = "0.24.0" 21 | 22 | var message = ` 23 | 24 |
25 | 26 |Sorry, we‘re having a brief problem. You can retry.
41 |If the problem persists, please get in touch.
42 | 43 | ` 44 | 45 | type ConnectionErrorHandler struct{ http.RoundTripper } 46 | 47 | func (c *ConnectionErrorHandler) RoundTrip(req *http.Request) (*http.Response, error) { 48 | resp, err := c.RoundTripper.RoundTrip(req) 49 | if err != nil { 50 | log.Printf("Error: backend request failed for %v: %v", 51 | req.RemoteAddr, err) 52 | } 53 | if _, ok := err.(*net.OpError); ok { 54 | r := &http.Response{ 55 | StatusCode: http.StatusServiceUnavailable, 56 | Body: ioutil.NopCloser(bytes.NewBufferString(message)), 57 | } 58 | return r, nil 59 | } 60 | return resp, err 61 | } 62 | 63 | func main() { 64 | var ( 65 | listen, cert, key, where string 66 | useTLS, useLogging, behindTCPProxy bool 67 | flushInterval time.Duration 68 | ) 69 | flag.StringVar(&listen, "listen", ":443", "Bind address to listen on") 70 | flag.StringVar(&key, "key", "/etc/ssl/private/key.pem", "Path to PEM key") 71 | flag.StringVar(&cert, "cert", "/etc/ssl/private/cert.pem", "Path to PEM certificate") 72 | flag.StringVar(&where, "where", "http://localhost:80", "Place to forward connections to") 73 | flag.BoolVar(&useTLS, "tls", true, "accept HTTPS connections") 74 | flag.BoolVar(&useLogging, "logging", true, "log requests") 75 | flag.BoolVar(&behindTCPProxy, "behind-tcp-proxy", false, "running behind TCP proxy (such as ELB or HAProxy)") 76 | flag.DurationVar(&flushInterval, "flush-interval", 0, "minimum duration between flushes to the client (default: off)") 77 | oldUsage := flag.Usage 78 | flag.Usage = func() { 79 | fmt.Fprintf(os.Stderr, "\n%v version %v\n\n", os.Args[0], Version) 80 | oldUsage() 81 | } 82 | flag.Parse() 83 | 84 | url, err := url.Parse(where) 85 | if err != nil { 86 | log.Fatalln("Fatal parsing -where:", err) 87 | } 88 | 89 | httpProxy := httputil.NewSingleHostReverseProxy(url) 90 | httpProxy.Transport = &ConnectionErrorHandler{http.DefaultTransport} 91 | httpProxy.FlushInterval = flushInterval 92 | 93 | var handler http.Handler 94 | 95 | handler = httpProxy 96 | 97 | originalHandler := handler 98 | handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 99 | if r.URL.Path == "/_version" { 100 | w.Header().Add("X-Tiny-SSL-Version", Version) 101 | } 102 | r.Header.Set("X-Forwarded-Proto", "https") 103 | originalHandler.ServeHTTP(w, r) 104 | }) 105 | 106 | if useLogging { 107 | handler = &LoggingMiddleware{handler} 108 | } 109 | 110 | server := &http.Server{Addr: listen, Handler: handler} 111 | 112 | switch { 113 | case useTLS && behindTCPProxy: 114 | err = proxyprotocol.BehindTCPProxyListenAndServeTLS(server, cert, key) 115 | case behindTCPProxy: 116 | err = proxyprotocol.BehindTCPProxyListenAndServe(server) 117 | case useTLS: 118 | err = server.ListenAndServeTLS(cert, key) 119 | default: 120 | err = server.ListenAndServe() 121 | } 122 | 123 | log.Fatalln(err) 124 | } 125 | -------------------------------------------------------------------------------- /proxyprotocol/http.go: -------------------------------------------------------------------------------- 1 | package proxyprotocol 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http" 7 | ) 8 | 9 | func BehindTCPProxyListenAndServeTLS(srv *http.Server, certFile, keyFile string) error { 10 | // Begin copied almost verbatim from net/http 11 | addr := srv.Addr 12 | if addr == "" { 13 | addr = ":https" 14 | } 15 | 16 | // Ensure we don't modify *TLSConfig, in case it is reused. 17 | srv.TLSConfig = cloneTLSClientConfig(srv.TLSConfig) 18 | 19 | srv.TLSConfig.NextProtos = append(srv.TLSConfig.NextProtos, "h2") 20 | 21 | var err error 22 | srv.TLSConfig.Certificates = make([]tls.Certificate, 1) 23 | srv.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | ln, err := net.Listen("tcp", addr) 29 | if err != nil { 30 | return err 31 | } 32 | // End copied almost verbatim from net/http 33 | 34 | // Wrap the listener with one understanding the PROXY protocol 35 | var listener net.Listener 36 | listener = ln.(*net.TCPListener) 37 | listener = NewListener(listener) 38 | listener = tls.NewListener(listener, srv.TLSConfig) 39 | return srv.Serve(listener) 40 | } 41 | 42 | // BehindTCPProxyListenAndServe listens on the TCP network address srv.Addr and then 43 | // calls Serve to handle requests on incoming connections. If 44 | // srv.Addr is blank, ":http" is used. 45 | func BehindTCPProxyListenAndServe(srv *http.Server) error { 46 | // Begin copied verbatim from net/http 47 | addr := srv.Addr 48 | if addr == "" { 49 | addr = ":http" 50 | } 51 | ln, err := net.Listen("tcp", addr) 52 | if err != nil { 53 | return err 54 | } 55 | // End copied verbatim from net/http 56 | 57 | // Wrap the listener with one understanding the PROXY protocol 58 | listener := NewListener(ln.(*net.TCPListener)) 59 | return srv.Serve(listener) 60 | } 61 | 62 | // cloneTLSClientConfig is like cloneTLSConfig but omits 63 | // the fields SessionTicketsDisabled and SessionTicketKey. 64 | // This makes it safe to call cloneTLSClientConfig on a config 65 | // in active use by a server. 66 | // COPIED FROM net/http/transport.go 67 | func cloneTLSClientConfig(cfg *tls.Config) *tls.Config { 68 | if cfg == nil { 69 | return &tls.Config{} 70 | } 71 | return &tls.Config{ 72 | Rand: cfg.Rand, 73 | Time: cfg.Time, 74 | Certificates: cfg.Certificates, 75 | NameToCertificate: cfg.NameToCertificate, 76 | GetCertificate: cfg.GetCertificate, 77 | RootCAs: cfg.RootCAs, 78 | NextProtos: cfg.NextProtos, 79 | ServerName: cfg.ServerName, 80 | ClientAuth: cfg.ClientAuth, 81 | ClientCAs: cfg.ClientCAs, 82 | InsecureSkipVerify: cfg.InsecureSkipVerify, 83 | CipherSuites: cfg.CipherSuites, 84 | PreferServerCipherSuites: cfg.PreferServerCipherSuites, 85 | ClientSessionCache: cfg.ClientSessionCache, 86 | MinVersion: cfg.MinVersion, 87 | MaxVersion: cfg.MaxVersion, 88 | CurvePreferences: cfg.CurvePreferences, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /proxyprotocol/proxy.go: -------------------------------------------------------------------------------- 1 | package proxyprotocol 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "log" 8 | "net" 9 | "sync" 10 | 11 | "github.com/sensiblecodeio/tiny-ssl-reverse-proxy/proxyprotocol/proxyline" 12 | ) 13 | 14 | type Accept struct { 15 | c net.Conn 16 | err error 17 | } 18 | 19 | type Listener struct { 20 | net.Listener 21 | wg sync.WaitGroup 22 | accepts <-chan Accept 23 | done chan<- struct{} 24 | } 25 | 26 | func NewListener(underlying net.Listener) net.Listener { 27 | done := make(chan struct{}) 28 | accepts := make(chan Accept, 10) 29 | 30 | l := &Listener{ 31 | underlying, 32 | sync.WaitGroup{}, 33 | accepts, 34 | done, 35 | } 36 | 37 | l.wg.Add(1) 38 | go func() { 39 | defer l.wg.Done() 40 | for { 41 | // underlying 42 | c, err := underlying.Accept() 43 | if err != nil { 44 | accepts <- Accept{c, err} 45 | continue 46 | } 47 | 48 | // Asynchronously process a PROXY instruction before passing it off 49 | // to the consumer's accept loop. 50 | go func() { 51 | // Process the "PROXY" string 52 | buf := bufio.NewReader(c) 53 | p, err := proxyline.ConsumeProxyLine(buf) 54 | if err != nil { 55 | log.Printf("proxyprotocol failed to parse PROXY header: "+ 56 | "%v", err) 57 | // Failed to read the proxy string, drop the connection. 58 | 59 | _ = c.Close() // Ignore the error. 60 | return 61 | } 62 | 63 | // Wrap the connection with a reader which first reads whatever 64 | // remains in the buffer, followed by the rest of the 65 | // connection. 66 | // Because we're using buf.Buffered(), can ignore the error. 67 | bufbytes, _ := buf.Peek(buf.Buffered()) 68 | buffered := bytes.NewReader(bufbytes) 69 | r := io.MultiReader(buffered, c) 70 | 71 | wrapped := &Conn{Reader: r, Conn: c} 72 | if p != nil { 73 | ra := &net.TCPAddr{p.SrcAddr.IP, p.SrcPort, p.SrcAddr.Zone} 74 | la := &net.TCPAddr{p.DstAddr.IP, p.DstPort, p.DstAddr.Zone} 75 | wrapped.remoteAddr = ra 76 | wrapped.localAddr = la 77 | } 78 | 79 | accepts <- Accept{wrapped, err} 80 | }() 81 | 82 | select { 83 | case <-done: 84 | return 85 | default: 86 | } 87 | } 88 | }() 89 | 90 | return l 91 | } 92 | 93 | func (l *Listener) Accept() (net.Conn, error) { 94 | accept, ok := <-l.accepts 95 | if !ok { 96 | return nil, io.ErrClosedPipe 97 | } 98 | 99 | return accept.c, accept.err 100 | } 101 | 102 | func (l *Listener) Close() error { 103 | close(l.done) 104 | err := l.Close() 105 | l.wg.Wait() 106 | return err 107 | } 108 | 109 | type Conn struct { 110 | io.Reader 111 | net.Conn 112 | remoteAddr, localAddr net.Addr 113 | } 114 | 115 | func (c *Conn) Read(bs []byte) (int, error) { 116 | return c.Reader.Read(bs) 117 | } 118 | 119 | // LocalAddr returns the specified local addr, if there is one. 120 | func (c *Conn) LocalAddr() net.Addr { 121 | if c.localAddr != nil { 122 | return c.localAddr 123 | } 124 | return c.Conn.LocalAddr() 125 | } 126 | 127 | // RemoteAddr returns the specified remote addr, if there is one. 128 | func (c *Conn) RemoteAddr() net.Addr { 129 | if c.remoteAddr != nil { 130 | return c.remoteAddr 131 | } 132 | return c.Conn.RemoteAddr() 133 | } 134 | -------------------------------------------------------------------------------- /proxyprotocol/proxyline/LICENSE: -------------------------------------------------------------------------------- 1 | This uses code from https://github.com/racker/go-proxy-protocol 2 | 3 | Copyright 2013 Rackspace 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. -------------------------------------------------------------------------------- /proxyprotocol/proxyline/parser.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Rackspace 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | // Packet proxyProtocol implements Proxy Protocol parser and writer. 19 | package proxyline 20 | 21 | import ( 22 | "bufio" 23 | "bytes" 24 | "errors" 25 | "fmt" 26 | "io" 27 | "net" 28 | "strconv" 29 | "strings" 30 | ) 31 | 32 | // INET protocol and family 33 | const ( 34 | TCP4 = "TCP4" // TCP over IPv4 35 | TCP6 = "TCP6" // TCP over IPv6 36 | UNKNOWN = "UNKNOWN" // Unsupported or unknown protocols 37 | ) 38 | 39 | var ( 40 | InvalidProxyLine = errors.New("Invalid proxy line") 41 | UnmatchedIPAddress = errors.New("IP address(es) unmatched with protocol") 42 | InvalidPortNum = errors.New(fmt.Sprintf("Invalid port number parsed. (expected [%d..%d])", _port_lower, _port_upper)) 43 | ) 44 | 45 | var ( 46 | _proxy = []byte{'P', 'R', 'O', 'X', 'Y'} 47 | _CRLF = "\r\n" 48 | _sep = " " 49 | _port_lower = 0 50 | _port_upper = 65535 51 | ) 52 | 53 | type ProxyLine struct { 54 | Protocol string 55 | SrcAddr *net.IPAddr 56 | DstAddr *net.IPAddr 57 | SrcPort int 58 | DstPort int 59 | } 60 | 61 | // ConsumeProxyLine looks for PROXY line in the reader and try to parse it if found. 62 | // 63 | // If first 5 bytes in reader is "PROXY", the function reads one line (until first '\n') from reader and try to parse it as ProxyLine. A newly allocated ProxyLine is returned if parsing secceeds. If parsing fails, a nil and an error is returned; 64 | // 65 | // If first 5 bytes in reader is not "PROXY", the function simply returns (nil, nil), leaving reader intact (nothing from reader is consumed). 66 | // 67 | // If the being parsed PROXY line is using an unknown protocol, ConsumeProxyLine parses remaining fields as same syntax as a supported protocol assuming IP is used in layer 3, and reports error if failed. 68 | func ConsumeProxyLine(reader *bufio.Reader) (*ProxyLine, error) { 69 | word, err := reader.Peek(5) 70 | if !bytes.Equal(word, _proxy) { 71 | return nil, nil 72 | } 73 | line, err := reader.ReadString('\n') 74 | if !strings.HasSuffix(line, _CRLF) { 75 | return nil, InvalidProxyLine 76 | } 77 | tokens := strings.Split(line[:len(line)-2], _sep) 78 | ret := new(ProxyLine) 79 | if len(tokens) < 6 { 80 | return nil, InvalidProxyLine 81 | } 82 | switch tokens[1] { 83 | case TCP4: 84 | ret.Protocol = TCP4 85 | case TCP6: 86 | ret.Protocol = TCP6 87 | default: 88 | ret.Protocol = UNKNOWN 89 | } 90 | ret.SrcAddr, err = parseIPAddr(ret.Protocol, tokens[2]) 91 | if err != nil { 92 | return nil, err 93 | } 94 | ret.DstAddr, err = parseIPAddr(ret.Protocol, tokens[3]) 95 | if err != nil { 96 | return nil, err 97 | } 98 | ret.SrcPort, err = parsePortNumber(tokens[4]) 99 | if err != nil { 100 | return nil, err 101 | } 102 | ret.DstPort, err = parsePortNumber(tokens[5]) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return ret, nil 107 | } 108 | 109 | // WriteProxyLine formats p as valid PROXY line into w 110 | func (p *ProxyLine) WriteProxyLine(w io.Writer) (err error) { 111 | _, err = fmt.Fprintf(w, "PROXY %s %s %s %d %d\r\n", p.Protocol, p.SrcAddr.String(), p.DstAddr.String(), p.SrcPort, p.DstPort) 112 | return 113 | } 114 | 115 | func parsePortNumber(portStr string) (port int, err error) { 116 | port, err = strconv.Atoi(portStr) 117 | if err == nil { 118 | if port < _port_lower || port > _port_upper { 119 | err = InvalidPortNum 120 | } 121 | } 122 | return 123 | } 124 | 125 | func parseIPAddr(protocol string, addrStr string) (addr *net.IPAddr, err error) { 126 | proto := "ip" 127 | if protocol == TCP4 { 128 | proto = "ip4" 129 | } else if protocol == TCP6 { 130 | proto = "ip6" 131 | } 132 | addr, err = net.ResolveIPAddr(proto, addrStr) 133 | if err == nil { 134 | tryV4 := addr.IP.To4() 135 | if (protocol == TCP4 && tryV4 == nil) || (protocol == TCP6 && tryV4 != nil) { 136 | err = UnmatchedIPAddress 137 | } 138 | } 139 | return 140 | } 141 | -------------------------------------------------------------------------------- /proxyprotocol/proxyline/parser_test.go: -------------------------------------------------------------------------------- 1 | package proxyline 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var ( 11 | fixtureTCP4 = "PROXY TCP4 127.0.0.1 127.0.0.1 65533 65533\r\n" 12 | fixtureTCP6 = "PROXY TCP6 2001:4801:7817:72:d4d9:211d:ff10:1631 2001:4801:7817:72:d4d9:211d:ff10:1631 65533 65533\r\n" 13 | 14 | v4addr, _ = net.ResolveIPAddr("ip", "127.0.0.1") 15 | v6addr, _ = net.ResolveIPAddr("ip", "2001:4801:7817:72:d4d9:211d:ff10:1631") 16 | pTCP4 = &ProxyLine{Protocol: TCP4, SrcAddr: v4addr, DstAddr: v4addr, SrcPort: 65533, DstPort: 65533} 17 | pTCP6 = &ProxyLine{Protocol: TCP6, SrcAddr: v6addr, DstAddr: v6addr, SrcPort: 65533, DstPort: 65533} 18 | 19 | invalidProxyLines = []string{ 20 | "PROXY TCP4 127.0.0.1 127.0.0.1 65533 65533", // no CRLF 21 | "PROXY \r\n", // not enough fields 22 | "PROXY TCP6 127.0.0.1 127.0.0.1 65533 65533\r\n,", // unmatched protocol addr 23 | "PROXY TCP4 2001:4801:7817:72:d4d9:211d:ff10:1631 2001:4801:7817:72:d4d9:211d:ff10:1631 65533 65533\r\n", // unmatched protocol addr 24 | } 25 | noneProxyLine = "There is no spoon." 26 | ) 27 | 28 | func TestParseTCP4(t *testing.T) { 29 | reader := bufio.NewReader(strings.NewReader(fixtureTCP4)) 30 | p, err := ConsumeProxyLine(reader) 31 | if err != nil { 32 | t.Fatalf("Parsing TCP4 failed: %v\n", err) 33 | } 34 | if !p.EqualTo(pTCP4) { 35 | t.Fatalf("Expected ProxyLine %v, got %v\n", pTCP4, p) 36 | } 37 | } 38 | 39 | func TestParseTCP6(t *testing.T) { 40 | reader := bufio.NewReader(strings.NewReader(fixtureTCP6)) 41 | p, err := ConsumeProxyLine(reader) 42 | if err != nil { 43 | t.Fatalf("Parsing TCP6 failed: %v\n", err) 44 | } 45 | if !p.EqualTo(pTCP6) { 46 | t.Fatalf("Expected ProxyLine %v, got %v\n", pTCP6, p) 47 | } 48 | } 49 | 50 | func TestParseNonProxyLine(t *testing.T) { 51 | reader := bufio.NewReader(strings.NewReader(noneProxyLine)) 52 | p, err := ConsumeProxyLine(reader) 53 | if err != nil || p != nil { 54 | t.Fatalf("Parsing none PROXY line failed. Expected nil, nil; got %q, %q\n", p, err) 55 | } 56 | } 57 | 58 | func TestInvalidProxyLines(t *testing.T) { 59 | for _, str := range invalidProxyLines { 60 | reader := bufio.NewReader(strings.NewReader(str)) 61 | _, err := ConsumeProxyLine(reader) 62 | if err == nil { 63 | t.Fatalf("Parsing an invalid PROXY line %q fails to fail\n", str) 64 | } 65 | } 66 | } 67 | 68 | func (p *ProxyLine) EqualTo(q *ProxyLine) bool { 69 | return p.Protocol == q.Protocol && 70 | p.SrcAddr.String() == q.SrcAddr.String() && 71 | p.DstAddr.String() == q.DstAddr.String() && 72 | p.SrcPort == q.SrcPort && 73 | p.DstPort == q.DstPort 74 | } 75 | -------------------------------------------------------------------------------- /proxyprotocol/proxyline/proxy-protocol.txt: -------------------------------------------------------------------------------- 1 | The PROXY protocol 2 | Willy Tarreau 3 | 2011/03/20 4 | 5 | Abstract 6 | 7 | The PROXY protocol provides a convenient way to safely transport connection 8 | information such as a client's address across multiple layers of NAT or TCP 9 | proxies. It is designed to require little changes to existing components and 10 | to limit the performance impact caused by the processing of the transported 11 | information. 12 | 13 | 14 | Revision history 15 | 16 | 2010/10/29 - first version 17 | 2011/03/20 - update: implementation and security considerations 18 | 19 | 20 | 1. Background 21 | 22 | Relaying TCP connections through proxies generally involves a loss of the 23 | original TCP connection parameters such as source and destination addresses, 24 | ports, and so on. Some protocols make it a little bit easier to transfer such 25 | information. For SMTP, Postfix authors have proposed the XCLIENT protocol which 26 | received broad adoption and is particularly suited to mail exchanges. In HTTP, 27 | we have the non-standard but omnipresent X-Forwarded-For header which relays 28 | information about the original source address, and the less common 29 | X-Original-To which relays information about the destination address. 30 | 31 | However, both mechanisms require a knowledge of the underlying protocol to be 32 | implemented in intermediaries. 33 | 34 | Then comes a new class of products which we'll call "dumb proxies", not because 35 | they don't do anything, but because they're processing protocol-agnostic data. 36 | Stunnel is an example of such a "dumb proxy". It talks raw TCP on one side, and 37 | raw SSL on the other one, and does that reliably. 38 | 39 | The problem with such a proxy when it is combined with another one such as 40 | haproxy is to adapt it to talk the higher level protocol. A patch is available 41 | for Stunnel to make it capable to insert an X-Forwarded-For header in the first 42 | HTTP request of each incoming connection. Haproxy is able not to add another 43 | one when the connection comes from Stunnel, so that it's possible to hide it 44 | from the servers. 45 | 46 | The typical architecture becomes the following one : 47 | 48 | 49 | +--------+ HTTP :80 +----------+ 50 | | client | --------------------------------> | | 51 | | | | haproxy, | 52 | +--------+ +---------+ | 1 or 2 | 53 | / / HTTPS | stunnel | HTTP :81 | listening| 54 | <________/ ---------> | (server | ---------> | ports | 55 | | mode) | | | 56 | +---------+ +----------+ 57 | 58 | 59 | The problem appears when haproxy runs with keep-alive on the side towards the 60 | client. The Stunnel patch will only add the X-Forwarded-For header to the first 61 | request of each connection and all subsequent requests will not have it. One 62 | solution could be to improve the patch to make it support keep-alive and parse 63 | all forwarded data, whether they're announced with a Content-Length or with a 64 | Transfer-Encoding, taking care of special methods such as HEAD which announce 65 | data without transfering them, etc... In fact, it would require implementing a 66 | full HTTP stack in Stunnel. It would then become a lot more complex, a lot less 67 | reliable and would not anymore be the "dumb proxy" that fits every purposes. 68 | 69 | In practice, we don't need to add a header for each request because we'll emit 70 | the exact same information every time : the information related to the client 71 | side connection. We could then cache that information in haproxy and use it for 72 | every other request. But that becomes dangerous and is still limited to HTTP 73 | only. 74 | 75 | Another approach would be to prepend each connection with a line reporting the 76 | characteristics of the other side's connection. This method is a lot simpler to 77 | implement, does not require any protocol-specific knowledge on either side, and 78 | completely fits the purpose. That's finally what we did with a small patch to 79 | Stunnel and another one to haproxy. We have called this protocol the PROXY 80 | protocol. 81 | 82 | 83 | 2. The PROXY protocol 84 | 85 | The PROXY protocol's goal is to fill the receiver's internal structures with 86 | the information it could have found itself if it performed the accept from the 87 | client. Thus right now we're supporting the following : 88 | - INET protocol and family (TCP over IPv4 or IPv6) 89 | - layer 3 source and destination addresses 90 | - layer 4 source and destination ports if any 91 | 92 | Unlike the XCLIENT protocol, the PROXY protocol was designed with limited 93 | extensibility in order to help the receiver parse it very fast, while keeping 94 | it human-readable for better debugging possibilities. So it consists in exactly 95 | the following block prepended before any data flowing from the dumb proxy to 96 | the next hop : 97 | 98 | - a string identifying the protocol : "PROXY" ( \x50 \x52 \x4F \x58 \x59 ) 99 | 100 | - exactly one space : " " ( \x20 ) 101 | 102 | - a string indicating the proxied INET protocol and family. At the moment, 103 | only "TCP4" ( \x54 \x43 \x50 \x34 ) for TCP over IPv4, and "TCP6" 104 | ( \x54 \x43 \x50 \x36 ) for TCP over IPv6 are allowed. Unsupported or 105 | unknown protocols must be reported with the name "UNKNOWN" ( \x55 \x4E \x4B 106 | \x4E \x4F \x57 \x4E). The remaining fields of the line are then optional 107 | and may be ignored, until the CRLF is found. 108 | 109 | - exactly one space : " " ( \x20 ) 110 | 111 | - the layer 3 source address in its canonical format. IPv4 addresses must be 112 | indicated as a series of exactly 4 integers in the range [0..255] inclusive 113 | written in decimal representation separated by exactly one dot between each 114 | other. Heading zeroes are not permitted in front of numbers in order to 115 | avoid any possible confusion with octal numbers. IPv6 addresses must be 116 | indicated as series of 4 hexadecimal digits (upper or lower case) delimited 117 | by colons between each other, with the acceptance of one double colon 118 | sequence to replace the largest acceptable range of consecutive zeroes. The 119 | total number of decoded bits must exactly be 128. The advertised protocol 120 | family dictates what format to use. 121 | 122 | - exactly one space : " " ( \x20 ) 123 | 124 | - the layer 3 destination address in its canonical format. It is the same 125 | format as the layer 3 source address and matches the same family. 126 | 127 | - exactly one space : " " ( \x20 ) 128 | 129 | - the TCP source port represented as a decimal integer in the range 130 | [0..65535] inclusive. Heading zeroes are not permitted in front of numbers 131 | in order to avoid any possible confusion with octal numbers. 132 | 133 | - exactly one space : " " ( \x20 ) 134 | 135 | - the TCP destination port represented as a decimal integer in the range 136 | [0..65535] inclusive. Heading zeroes are not permitted in front of numbers 137 | in order to avoid any possible confusion with octal numbers. 138 | 139 | - the CRLF sequence ( \x0D \x0A ) 140 | 141 | The receiver MUST be configured to only receive this protocol and MUST not try 142 | to guess whether the line is prepended or not. That means that the protocol 143 | explicitly prevents port sharing between public and private access. Otherwise 144 | it would become a big security issue. The receiver should ensure proper access 145 | filtering so that only trusted proxies are allowed to use this protocol. The 146 | receiver must wait for the CRLF sequence to decode the addresses in order to 147 | ensure they are complete. Any sequence which does not exactly match the 148 | protocol must be discarded and cause a connection abort. It is recommended 149 | to abort the connection as soon as possible to that the emitter notices the 150 | anomaly. 151 | 152 | If the announced transport protocol is "UNKNOWN", then the receiver knows that 153 | the emitter talks the correct protocol, and may or may not decide to accept the 154 | connection and use the real connection's parameters as if there was no such 155 | protocol on the wire. 156 | 157 | An example of such a line before an HTTP request would look like this (CR 158 | marked as "\r" and LF marked as "\n") : 159 | 160 | PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n 161 | GET / HTTP/1.1\r\n 162 | Host: 192.168.0.11\r\n 163 | \r\n 164 | 165 | For the emitter, the line is easy to put into the output buffers once the 166 | connection is established. For the receiver, once the line is parsed, it's 167 | easy to skip it from the input buffers. 168 | 169 | 170 | 3. Implementations 171 | 172 | Haproxy 1.5 implements the PROXY protocol on both sides : 173 | - the listening sockets accept the protocol when the "accept-proxy" setting 174 | is passed to the "bind" keyword. Connections accepted on such listeners 175 | will behave just as if the source really was the one advertised in the 176 | protocol. This is true for logging, ACLs, content filtering, transparent 177 | proxying, etc... 178 | 179 | - the protocol may be used to connect to servers if the "send-proxy" setting 180 | is present on the "server" line. It is enabled on a per-server basis, so it 181 | is possible to have it enabled for remote servers only and still have local 182 | ones behave differently. If the incoming connection was accepted with the 183 | "accept-proxy", then the relayed information is the one advertised in this 184 | connection's PROXY line. 185 | 186 | We have a patch available for recent versions of Stunnel that brings it the 187 | ability to be an emitter. The feature is called "sendproxy" there. 188 | 189 | The protocol is so simple that it is expected that other implementations will 190 | appear, especially in environments such as SMTP, IMAP, FTP, RDP where the 191 | client's address is an important piece of information for the server and some 192 | intermediaries. 193 | 194 | Proxy developers are encouraged to implement this protocol, because it will 195 | make their products much more transparent in complex infrastructures, and will 196 | get rid of a number of issues related to logging and access control. 197 | 198 | 199 | 4. Security considerations 200 | 201 | The protocol was designed so as to be distinguishable from HTTP. It will not 202 | parse as a valid HTTP request and an HTTP request will not parse as a valid 203 | proxy request. That makes it easier to enfore its use certain connections. 204 | Implementers should be very careful about not trying to automatically detect 205 | whether they have to decode the line or not, but rather to only rely on a 206 | configuration parameter. Indeed, if the opportunity is left to a normal client 207 | to use the protocol, he will be able to hide his activities or make them appear 208 | as coming from someone else. However, accepting the line only from a number of 209 | known sources should be safe. 210 | 211 | 212 | 5. Future developments 213 | 214 | It is possible that the protocol may slightly evolve to present other 215 | information such as the incoming network interface, or the origin addresses in 216 | case of network address translation happening before the first proxy, but this 217 | is not identified as a requirement right now. Suggestions on improvements are 218 | welcome. 219 | 220 | 221 | 6. Contacts 222 | 223 | Please use w@1wt.eu to send any comments to the author. 224 | 225 | --------------------------------------------------------------------------------