Support Proxy protocol (#12527)

This PR adds functionality to allow Gitea to sit behind an
HAProxy and HAProxy protocolled connections directly.

Fix #7508

Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
zeripath 2022-08-21 19:20:43 +01:00 committed by GitHub
parent 0b4c166e8a
commit 943753f560
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 787 additions and 74 deletions

View file

@ -76,7 +76,7 @@ func runHTTPRedirector() {
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
})
err := runHTTP("tcp", source, "HTTP Redirector", handler)
err := runHTTP("tcp", source, "HTTP Redirector", handler, setting.RedirectorUseProxyProtocol)
if err != nil {
log.Fatal("Failed to start port redirection: %v", err)
}
@ -231,40 +231,38 @@ func listen(m http.Handler, handleRedirector bool) error {
if handleRedirector {
NoHTTPRedirector()
}
err = runHTTP("tcp", listenAddr, "Web", m)
err = runHTTP("tcp", listenAddr, "Web", m, setting.UseProxyProtocol)
case setting.HTTPS:
if setting.EnableAcme {
err = runACME(listenAddr, m)
break
} else {
if handleRedirector {
if setting.RedirectOtherPort {
go runHTTPRedirector()
} else {
NoHTTPRedirector()
}
}
err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m)
}
if handleRedirector {
if setting.RedirectOtherPort {
go runHTTPRedirector()
} else {
NoHTTPRedirector()
}
}
err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m, setting.UseProxyProtocol, setting.ProxyProtocolTLSBridging)
case setting.FCGI:
if handleRedirector {
NoHTTPRedirector()
}
err = runFCGI("tcp", listenAddr, "FCGI Web", m)
err = runFCGI("tcp", listenAddr, "FCGI Web", m, setting.UseProxyProtocol)
case setting.HTTPUnix:
if handleRedirector {
NoHTTPRedirector()
}
err = runHTTP("unix", listenAddr, "Web", m)
err = runHTTP("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
case setting.FCGIUnix:
if handleRedirector {
NoHTTPRedirector()
}
err = runFCGI("unix", listenAddr, "Web", m)
err = runFCGI("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
default:
log.Fatal("Invalid protocol: %s", setting.Protocol)
}
if err != nil {
log.Critical("Failed to start server: %v", err)
}

View file

@ -113,14 +113,14 @@ func runACME(listenAddr string, m http.Handler) error {
log.Info("Running Let's Encrypt handler on %s", setting.HTTPAddr+":"+setting.PortToRedirect)
// all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validation happens here)
err := runHTTP("tcp", setting.HTTPAddr+":"+setting.PortToRedirect, "Let's Encrypt HTTP Challenge", myACME.HTTPChallengeHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)))
err := runHTTP("tcp", setting.HTTPAddr+":"+setting.PortToRedirect, "Let's Encrypt HTTP Challenge", myACME.HTTPChallengeHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)), setting.RedirectorUseProxyProtocol)
if err != nil {
log.Fatal("Failed to start the Let's Encrypt handler on port %s: %v", setting.PortToRedirect, err)
}
}()
}
return runHTTPSWithTLSConfig("tcp", listenAddr, "Web", tlsConfig, m)
return runHTTPSWithTLSConfig("tcp", listenAddr, "Web", tlsConfig, m, setting.UseProxyProtocol, setting.ProxyProtocolTLSBridging)
}
func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {

View file

@ -15,8 +15,8 @@ import (
"code.gitea.io/gitea/modules/setting"
)
func runHTTP(network, listenAddr, name string, m http.Handler) error {
return graceful.HTTPListenAndServe(network, listenAddr, name, m)
func runHTTP(network, listenAddr, name string, m http.Handler, useProxyProtocol bool) error {
return graceful.HTTPListenAndServe(network, listenAddr, name, m, useProxyProtocol)
}
// NoHTTPRedirector tells our cleanup routine that we will not be using a fallback http redirector
@ -36,7 +36,7 @@ func NoInstallListener() {
graceful.GetManager().InformCleanup()
}
func runFCGI(network, listenAddr, name string, m http.Handler) error {
func runFCGI(network, listenAddr, name string, m http.Handler, useProxyProtocol bool) error {
// This needs to handle stdin as fcgi point
fcgiServer := graceful.NewServer(network, listenAddr, name)
@ -47,7 +47,7 @@ func runFCGI(network, listenAddr, name string, m http.Handler) error {
}
m.ServeHTTP(resp, req)
}))
})
}, useProxyProtocol)
if err != nil {
log.Fatal("Failed to start FCGI main server: %v", err)
}

View file

@ -129,14 +129,14 @@ var (
defaultCiphersChaChaFirst = append(defaultCiphersChaCha, defaultCiphersAES...)
)
// runHTTPs listens on the provided network address and then calls
// runHTTPS listens on the provided network address and then calls
// Serve to handle requests on incoming TLS connections.
//
// Filenames containing a certificate and matching private key for the server must
// be provided. If the certificate is signed by a certificate authority, the
// certFile should be the concatenation of the server's certificate followed by the
// CA's certificate.
func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler) error {
func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
tlsConfig := &tls.Config{}
if tlsConfig.NextProtos == nil {
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
@ -184,9 +184,9 @@ func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handle
return err
}
return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m)
return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m, useProxyProtocol, proxyProtocolTLSBridging)
}
func runHTTPSWithTLSConfig(network, listenAddr, name string, tlsConfig *tls.Config, m http.Handler) error {
return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m)
func runHTTPSWithTLSConfig(network, listenAddr, name string, tlsConfig *tls.Config, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m, useProxyProtocol, proxyProtocolTLSBridging)
}

View file

@ -29,6 +29,18 @@ RUN_MODE = ; prod
;; The protocol the server listens on. One of 'http', 'https', 'unix' or 'fcgi'. Defaults to 'http'
;PROTOCOL = http
;;
;; Expect PROXY protocol headers on connections
;USE_PROXY_PROTOCOL = false
;;
;; Use PROXY protocol in TLS Bridging mode
;PROXY_PROTOCOL_TLS_BRIDGING = false
;;
; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
;PROXY_PROTOCOL_HEADER_TIMEOUT=5s
;;
; Accept PROXY protocol headers with UNKNOWN type
;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false
;;
;; Set the domain for the server
;DOMAIN = localhost
;;
@ -51,6 +63,8 @@ RUN_MODE = ; prod
;REDIRECT_OTHER_PORT = false
;PORT_TO_REDIRECT = 80
;;
;; expect PROXY protocol header on connections to https redirector.
;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)
;; Minimum and maximum supported TLS versions
;SSL_MIN_VERSION=TLSv1.2
;SSL_MAX_VERSION=
@ -76,13 +90,19 @@ RUN_MODE = ; prod
;; Do not set this variable if PROTOCOL is set to 'unix'.
;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/
;;
;; When making local connections pass the PROXY protocol header.
;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)
;;
;; Disable SSH feature when not available
;DISABLE_SSH = false
;;
;; Whether to use the builtin SSH server or not.
;START_SSH_SERVER = false
;;
;; Username to use for the builtin SSH server.
;; Expect PROXY protocol header on connections to the built-in SSH server
;SSH_SERVER_USE_PROXY_PROTOCOL = false
;;
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s
;;
;; Domain name to be exposed in clone URL

View file

@ -238,6 +238,10 @@ The following configuration set `Content-Type: application/vnd.android.package-a
## Server (`server`)
- `PROTOCOL`: **http**: \[http, https, fcgi, http+unix, fcgi+unix\]
- `USE_PROXY_PROTOCOL`: **false**: Expect PROXY protocol headers on connections
- `PROXY_PROTOCOL_TLS_BRIDGING`: **false**: When protocol is https, expect PROXY protocol headers after TLS negotiation.
- `PROXY_PROTOCOL_HEADER_TIMEOUT`: **5s**: Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
- `PROXY_PROTOCOL_ACCEPT_UNKNOWN`: **false**: Accept PROXY protocol headers with Unknown type.
- `DOMAIN`: **localhost**: Domain name of this server.
- `ROOT_URL`: **%(PROTOCOL)s://%(DOMAIN)s:%(HTTP\_PORT)s/**:
Overwrite the automatically generated public URL.
@ -262,12 +266,15 @@ The following configuration set `Content-Type: application/vnd.android.package-a
most cases you do not need to change the default value. Alter it only if
your SSH server node is not the same as HTTP node. Do not set this variable
if `PROTOCOL` is set to `http+unix`.
- `LOCAL_USE_PROXY_PROTOCOL`: **%(USE_PROXY_PROTOCOL)**: When making local connections pass the PROXY protocol header.
This should be set to false if the local connection will go through the proxy.
- `PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the connection. (Set to -1 to
disable all timeouts.)
- `PER_WRITE_PER_KB_TIMEOUT`: **10s**: Timeout per Kb written to connections.
- `DISABLE_SSH`: **false**: Disable SSH feature when it's not available.
- `START_SSH_SERVER`: **false**: When enabled, use the built-in SSH server.
- `SSH_SERVER_USE_PROXY_PROTOCOL`: **false**: Expect PROXY protocol header on connections to the built-in SSH Server.
- `BUILTIN_SSH_SERVER_USER`: **%(RUN_USER)s**: Username to use for the built-in SSH Server.
- `SSH_USER`: **%(BUILTIN_SSH_SERVER_USER)**: SSH username displayed in clone URLs. This is only for people who configure the SSH server themselves; in most cases, you want to leave this blank and modify the `BUILTIN_SSH_SERVER_USER`.
- `SSH_DOMAIN`: **%(DOMAIN)s**: Domain name of this server, used for displayed clone URL.
@ -313,6 +320,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `LFS_LOCKS_PAGING_NUM`: **50**: Maximum number of LFS Locks returned per page.
- `REDIRECT_OTHER_PORT`: **false**: If true and `PROTOCOL` is https, allows redirecting http requests on `PORT_TO_REDIRECT` to the https port Gitea listens on.
- `REDIRECTOR_USE_PROXY_PROTOCOL`: **%(USE_PROXY_PROTOCOL)**: expect PROXY protocol header on connections to https redirector.
- `PORT_TO_REDIRECT`: **80**: Port for the http redirection service to listen on. Used when `REDIRECT_OTHER_PORT` is true.
- `SSL_MIN_VERSION`: **TLSv1.2**: Set the minimum version of ssl support.
- `SSL_MAX_VERSION`: **\<empty\>**: Set the maximum version of ssl support.

View file

@ -16,6 +16,7 @@ import (
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/proxyprotocol"
"code.gitea.io/gitea/modules/setting"
)
@ -79,16 +80,27 @@ func NewServer(network, address, name string) *Server {
// ListenAndServe listens on the provided network address and then calls Serve
// to handle requests on incoming connections.
func (srv *Server) ListenAndServe(serve ServeFunction) error {
func (srv *Server) ListenAndServe(serve ServeFunction, useProxyProtocol bool) error {
go srv.awaitShutdown()
l, err := GetListener(srv.network, srv.address)
listener, err := GetListener(srv.network, srv.address)
if err != nil {
log.Error("Unable to GetListener: %v", err)
return err
}
srv.listener = newWrappedListener(l, srv)
// we need to wrap the listener to take account of our lifecycle
listener = newWrappedListener(listener, srv)
// Now we need to take account of ProxyProtocol settings...
if useProxyProtocol {
listener = &proxyprotocol.Listener{
Listener: listener,
ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout,
AcceptUnknown: setting.ProxyProtocolAcceptUnknown,
}
}
srv.listener = listener
srv.BeforeBegin(srv.network, srv.address)
@ -97,22 +109,44 @@ func (srv *Server) ListenAndServe(serve ServeFunction) error {
// ListenAndServeTLSConfig listens on the provided network address and then calls
// Serve to handle requests on incoming TLS connections.
func (srv *Server) ListenAndServeTLSConfig(tlsConfig *tls.Config, serve ServeFunction) error {
func (srv *Server) ListenAndServeTLSConfig(tlsConfig *tls.Config, serve ServeFunction, useProxyProtocol, proxyProtocolTLSBridging bool) error {
go srv.awaitShutdown()
if tlsConfig.MinVersion == 0 {
tlsConfig.MinVersion = tls.VersionTLS12
}
l, err := GetListener(srv.network, srv.address)
listener, err := GetListener(srv.network, srv.address)
if err != nil {
log.Error("Unable to get Listener: %v", err)
return err
}
wl := newWrappedListener(l, srv)
srv.listener = tls.NewListener(wl, tlsConfig)
// we need to wrap the listener to take account of our lifecycle
listener = newWrappedListener(listener, srv)
// Now we need to take account of ProxyProtocol settings... If we're not bridging then we expect that the proxy will forward the connection to us
if useProxyProtocol && !proxyProtocolTLSBridging {
listener = &proxyprotocol.Listener{
Listener: listener,
ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout,
AcceptUnknown: setting.ProxyProtocolAcceptUnknown,
}
}
// Now handle the tls protocol
listener = tls.NewListener(listener, tlsConfig)
// Now if we're bridging then we need the proxy to tell us who we're bridging for...
if useProxyProtocol && proxyProtocolTLSBridging {
listener = &proxyprotocol.Listener{
Listener: listener,
ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout,
AcceptUnknown: setting.ProxyProtocolAcceptUnknown,
}
}
srv.listener = listener
srv.BeforeBegin(srv.network, srv.address)
return srv.Serve(serve)

View file

@ -28,14 +28,14 @@ func newHTTPServer(network, address, name string, handler http.Handler) (*Server
// HTTPListenAndServe listens on the provided network address and then calls Serve
// to handle requests on incoming connections.
func HTTPListenAndServe(network, address, name string, handler http.Handler) error {
func HTTPListenAndServe(network, address, name string, handler http.Handler, useProxyProtocol bool) error {
server, lHandler := newHTTPServer(network, address, name, handler)
return server.ListenAndServe(lHandler)
return server.ListenAndServe(lHandler, useProxyProtocol)
}
// HTTPListenAndServeTLSConfig listens on the provided network address and then calls Serve
// to handle requests on incoming connections.
func HTTPListenAndServeTLSConfig(network, address, name string, tlsConfig *tls.Config, handler http.Handler) error {
func HTTPListenAndServeTLSConfig(network, address, name string, tlsConfig *tls.Config, handler http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
server, lHandler := newHTTPServer(network, address, name, handler)
return server.ListenAndServeTLSConfig(tlsConfig, lHandler)
return server.ListenAndServeTLSConfig(tlsConfig, lHandler, useProxyProtocol, proxyProtocolTLSBridging)
}

View file

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/proxyprotocol"
"code.gitea.io/gitea/modules/setting"
)
@ -50,7 +51,32 @@ func newInternalRequest(ctx context.Context, url, method string) *httplib.Reques
req.SetTransport(&http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", setting.HTTPAddr)
conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr)
if err != nil {
return conn, err
}
if setting.LocalUseProxyProtocol {
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
_ = conn.Close()
return nil, err
}
}
return conn, err
},
})
} else if setting.LocalUseProxyProtocol {
req.SetTransport(&http.Transport{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer
conn, err := d.DialContext(ctx, network, address)
if err != nil {
return conn, err
}
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
_ = conn.Close()
return nil, err
}
return conn, err
},
})
}

View file

@ -0,0 +1,506 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package proxyprotocol
import (
"bufio"
"bytes"
"encoding/binary"
"io"
"net"
"strconv"
"strings"
"sync"
"time"
"code.gitea.io/gitea/modules/log"
)
var (
// v1Prefix is the string we look for at the start of a connection
// to check if this connection is using the proxy protocol
v1Prefix = []byte("PROXY ")
v1PrefixLen = len(v1Prefix)
v2Prefix = []byte("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A")
v2PrefixLen = len(v2Prefix)
)
// Conn is used to wrap and underlying connection which is speaking the
// Proxy Protocol. RemoteAddr() will return the address of the client
// instead of the proxy address.
type Conn struct {
bufReader *bufio.Reader
conn net.Conn
localAddr net.Addr
remoteAddr net.Addr
once sync.Once
proxyHeaderTimeout time.Duration
acceptUnknown bool
}
// NewConn is used to wrap a net.Conn speaking the proxy protocol into
// a proxyprotocol.Conn
func NewConn(conn net.Conn, timeout time.Duration) *Conn {
pConn := &Conn{
bufReader: bufio.NewReader(conn),
conn: conn,
proxyHeaderTimeout: timeout,
}
return pConn
}
// Read reads data from the connection.
// It will initially read the proxy protocol header.
// If there is an error parsing the header, it is returned and the socket is closed.
func (p *Conn) Read(b []byte) (int, error) {
if err := p.readProxyHeaderOnce(); err != nil {
return 0, err
}
return p.bufReader.Read(b)
}
// ReadFrom reads data from a provided reader and copies it to the connection.
func (p *Conn) ReadFrom(r io.Reader) (int64, error) {
if err := p.readProxyHeaderOnce(); err != nil {
return 0, err
}
if rf, ok := p.conn.(io.ReaderFrom); ok {
return rf.ReadFrom(r)
}
return io.Copy(p.conn, r)
}
// WriteTo reads data from the connection and writes it to the writer.
// It will initially read the proxy protocol header.
// If there is an error parsing the header, it is returned and the socket is closed.
func (p *Conn) WriteTo(w io.Writer) (int64, error) {
if err := p.readProxyHeaderOnce(); err != nil {
return 0, err
}
return p.bufReader.WriteTo(w)
}
// Write writes data to the connection.
// Write can be made to time out and return an error after a fixed
// time limit; see SetDeadline and SetWriteDeadline.
func (p *Conn) Write(b []byte) (int, error) {
if err := p.readProxyHeaderOnce(); err != nil {
return 0, err
}
return p.conn.Write(b)
}
// Close closes the connection.
// Any blocked Read or Write operations will be unblocked and return errors.
func (p *Conn) Close() error {
return p.conn.Close()
}
// LocalAddr returns the local network address.
func (p *Conn) LocalAddr() net.Addr {
_ = p.readProxyHeaderOnce()
if p.localAddr != nil {
return p.localAddr
}
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.
// One 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.readProxyHeaderOnce()
if p.remoteAddr != nil {
return p.remoteAddr
}
return p.conn.RemoteAddr()
}
// SetDeadline sets the read and write deadlines associated
// with the connection. It is equivalent to calling both
// SetReadDeadline and SetWriteDeadline.
//
// A deadline is an absolute time after which I/O operations
// fail instead of blocking. The deadline applies to all future
// and pending I/O, not just the immediately following call to
// Read or Write. After a deadline has been exceeded, the
// connection can be refreshed by setting a deadline in the future.
//
// If the deadline is exceeded a call to Read or Write or to other
// I/O methods will return an error that wraps os.ErrDeadlineExceeded.
// This can be tested using errors.Is(err, os.ErrDeadlineExceeded).
// The error's Timeout method will return true, but note that there
// are other possible errors for which the Timeout method will
// return true even if the deadline has not been exceeded.
//
// An idle timeout can be implemented by repeatedly extending
// the deadline after successful Read or Write calls.
//
// A zero value for t means I/O operations will not time out.
func (p *Conn) SetDeadline(t time.Time) error {
return p.conn.SetDeadline(t)
}
// SetReadDeadline sets the deadline for future Read calls
// and any currently-blocked Read call.
// A zero value for t means Read will not time out.
func (p *Conn) SetReadDeadline(t time.Time) error {
return p.conn.SetReadDeadline(t)
}
// SetWriteDeadline sets the deadline for future Write calls
// and any currently-blocked Write call.
// Even if write times out, it may return n > 0, indicating that
// some of the data was successfully written.
// A zero value for t means Write will not time out.
func (p *Conn) SetWriteDeadline(t time.Time) error {
return p.conn.SetWriteDeadline(t)
}
// readProxyHeaderOnce will ensure that the proxy header has been read
func (p *Conn) readProxyHeaderOnce() (err error) {
p.once.Do(func() {
if err = p.readProxyHeader(); err != nil && err != io.EOF {
log.Error("Failed to read proxy prefix: %v", err)
p.Close()
p.bufReader = bufio.NewReader(p.conn)
}
})
return err
}
func (p *Conn) readProxyHeader() error {
if p.proxyHeaderTimeout != 0 {
readDeadLine := time.Now().Add(p.proxyHeaderTimeout)
_ = p.conn.SetReadDeadline(readDeadLine)
defer func() {
_ = p.conn.SetReadDeadline(time.Time{})
}()
}
inp, err := p.bufReader.Peek(v1PrefixLen)
if err != nil {
return err
}
if bytes.Equal(inp, v1Prefix) {
return p.readV1ProxyHeader()
}
inp, err = p.bufReader.Peek(v2PrefixLen)
if err != nil {
return err
}
if bytes.Equal(inp, v2Prefix) {
return p.readV2ProxyHeader()
}
return &ErrBadHeader{inp}
}
func (p *Conn) readV2ProxyHeader() error {
// The binary header format starts with a constant 12 bytes block containing the
// protocol signature :
//
// \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
//
// Note that this block contains a null byte at the 5th position, so it must not
// be handled as a null-terminated string.
if _, err := p.bufReader.Discard(v2PrefixLen); err != nil {
// This shouldn't happen as we have already asserted that there should be enough in the buffer
return err
}
// The next byte (the 13th one) is the protocol version and command.
version, err := p.bufReader.ReadByte()
if err != nil {
return err
}
// The 14th byte contains the transport protocol and address family.otocol.
familyByte, err := p.bufReader.ReadByte()
if err != nil {
return err
}
// The 15th and 16th bytes is the address length in bytes in network endian order.
var addressLen uint16
if err := binary.Read(p.bufReader, binary.BigEndian, &addressLen); err != nil {
return err
}
// Now handle the version byte: (14th byte).
// The highest four bits contains the version. As of this specification, it must
// always be sent as \x2 and the receiver must only accept this value.
if version>>4 != 0x2 {
return &ErrBadHeader{append(v2Prefix, version, familyByte, uint8(addressLen>>8), uint8(addressLen&0xff))}
}
// The lowest four bits represents the command :
switch version & 0xf {
case 0x0:
// - \x0 : LOCAL : the connection was established on purpose by the proxy
// without being relayed. The connection endpoints are the sender and the
// receiver. Such connections exist when the proxy sends health-checks to the
// server. The receiver must accept this connection as valid and must use the
// real connection endpoints and discard the protocol block including the
// family which is ignored.
// We therefore ignore the 14th, 15th and 16th bytes
p.remoteAddr = p.conn.LocalAddr()
p.localAddr = p.conn.RemoteAddr()
return nil
case 0x1:
// - \x1 : PROXY : the connection was established on behalf of another node,
// and reflects the original connection endpoints. The receiver must then use
// the information provided in the protocol block to get original the address.
default:
// - other values are unassigned and must not be emitted by senders. Receivers
// must drop connections presenting unexpected values here.
return &ErrBadHeader{append(v2Prefix, version, familyByte, uint8(addressLen>>8), uint8(addressLen&0xff))}
}
// Now handle the familyByte byte: (15th byte).
// The highest 4 bits contain the address family, the lowest 4 bits contain the protocol
// The address family maps to the original socket family without necessarily
// matching the values internally used by the system. It may be one of :
//
// - 0x0 : AF_UNSPEC : the connection is forwarded for an unknown, unspecified
// or unsupported protocol. The sender should use this family when sending
// LOCAL commands or when dealing with unsupported protocol families. The
// receiver is free to accept the connection anyway and use the real endpoint
// addresses or to reject it. The receiver should ignore address information.
//
// - 0x1 : AF_INET : the forwarded connection uses the AF_INET address family
// (IPv4). The addresses are exactly 4 bytes each in network byte order,
// followed by transport protocol information (typically ports).
//
// - 0x2 : AF_INET6 : the forwarded connection uses the AF_INET6 address family
// (IPv6). The addresses are exactly 16 bytes each in network byte order,
// followed by transport protocol information (typically ports).
//
// - 0x3 : AF_UNIX : the forwarded connection uses the AF_UNIX address family
// (UNIX). The addresses are exactly 108 bytes each.
//
// - other values are unspecified and must not be emitted in version 2 of this
// protocol and must be rejected as invalid by receivers.
// The transport protocol is specified in the lowest 4 bits of the 14th byte :
//
// - 0x0 : UNSPEC : the connection is forwarded for an unknown, unspecified
// or unsupported protocol. The sender should use this family when sending
// LOCAL commands or when dealing with unsupported protocol families. The
// receiver is free to accept the connection anyway and use the real endpoint
// addresses or to reject it. The receiver should ignore address information.
//
// - 0x1 : STREAM : the forwarded connection uses a SOCK_STREAM protocol (eg:
// TCP or UNIX_STREAM). When used with AF_INET/AF_INET6 (TCP), the addresses
// are followed by the source and destination ports represented on 2 bytes
// each in network byte order.
//
// - 0x2 : DGRAM : the forwarded connection uses a SOCK_DGRAM protocol (eg:
// UDP or UNIX_DGRAM). When used with AF_INET/AF_INET6 (UDP), the addresses
// are followed by the source and destination ports represented on 2 bytes
// each in network byte order.
//
// - other values are unspecified and must not be emitted in version 2 of this
// protocol and must be rejected as invalid by receivers.
if familyByte>>4 == 0x0 || familyByte&0xf == 0x0 {
// - hi 0x0 : AF_UNSPEC : the connection is forwarded for an unknown address type
// or
// - lo 0x0 : UNSPEC : the connection is forwarded for an unspecified protocol
if !p.acceptUnknown {
p.conn.Close()
return &ErrBadHeader{append(v2Prefix, version, familyByte, uint8(addressLen>>8), uint8(addressLen&0xff))}
}
p.remoteAddr = p.conn.LocalAddr()
p.localAddr = p.conn.RemoteAddr()
_, err = p.bufReader.Discard(int(addressLen))
return err
}
// other address or protocol
if (familyByte>>4) > 0x3 || (familyByte&0xf) > 0x2 {
return &ErrBadHeader{append(v2Prefix, version, familyByte, uint8(addressLen>>8), uint8(addressLen&0xff))}
}
// Handle AF_UNIX addresses
if familyByte>>4 == 0x3 {
// - \x31 : UNIX stream : the forwarded connection uses SOCK_STREAM over the
// AF_UNIX protocol family. Address length is 2*108 = 216 bytes.
// - \x32 : UNIX datagram : the forwarded connection uses SOCK_DGRAM over the
// AF_UNIX protocol family. Address length is 2*108 = 216 bytes.
if addressLen != 216 {
return &ErrBadHeader{append(v2Prefix, version, familyByte, uint8(addressLen>>8), uint8(addressLen&0xff))}
}
remoteName := make([]byte, 108)
localName := make([]byte, 108)
if _, err := p.bufReader.Read(remoteName); err != nil {
return err
}
if _, err := p.bufReader.Read(localName); err != nil {
return err
}
protocol := "unix"
if familyByte&0xf == 2 {
protocol = "unixgram"
}
p.remoteAddr = &net.UnixAddr{
Name: string(remoteName),
Net: protocol,
}
p.localAddr = &net.UnixAddr{
Name: string(localName),
Net: protocol,
}
return nil
}
var remoteIP []byte
var localIP []byte
var remotePort uint16
var localPort uint16
if familyByte>>4 == 0x1 {
// AF_INET
// - \x11 : TCP over IPv4 : the forwarded connection uses TCP over the AF_INET
// protocol family. Address length is 2*4 + 2*2 = 12 bytes.
// - \x12 : UDP over IPv4 : the forwarded connection uses UDP over the AF_INET
// protocol family. Address length is 2*4 + 2*2 = 12 bytes.
if addressLen != 12 {
return &ErrBadHeader{append(v2Prefix, version, familyByte, uint8(addressLen>>8), uint8(addressLen&0xff))}
}
remoteIP = make([]byte, 4)
localIP = make([]byte, 4)
} else {
// AF_INET6
// - \x21 : TCP over IPv6 : the forwarded connection uses TCP over the AF_INET6
// protocol family. Address length is 2*16 + 2*2 = 36 bytes.
// - \x22 : UDP over IPv6 : the forwarded connection uses UDP over the AF_INET6
// protocol family. Address length is 2*16 + 2*2 = 36 bytes.
if addressLen != 36 {
return &ErrBadHeader{append(v2Prefix, version, familyByte, uint8(addressLen>>8), uint8(addressLen&0xff))}
}
remoteIP = make([]byte, 16)
localIP = make([]byte, 16)
}
if _, err := p.bufReader.Read(remoteIP); err != nil {
return err
}
if _, err := p.bufReader.Read(localIP); err != nil {
return err
}
if err := binary.Read(p.bufReader, binary.BigEndian, &remotePort); err != nil {
return err
}
if err := binary.Read(p.bufReader, binary.BigEndian, &localPort); err != nil {
return err
}
if familyByte&0xf == 1 {
p.remoteAddr = &net.TCPAddr{
IP: remoteIP,
Port: int(remotePort),
}
p.localAddr = &net.TCPAddr{
IP: localIP,
Port: int(localPort),
}
} else {
p.remoteAddr = &net.UDPAddr{
IP: remoteIP,
Port: int(remotePort),
}
p.localAddr = &net.UDPAddr{
IP: localIP,
Port: int(localPort),
}
}
return nil
}
func (p *Conn) readV1ProxyHeader() error {
// Read until a newline
header, err := p.bufReader.ReadString('\n')
if err != nil {
p.conn.Close()
return err
}
if header[len(header)-2] != '\r' {
return &ErrBadHeader{[]byte(header)}
}
// Strip the carriage return and new line
header = header[:len(header)-2]
// Split on spaces, should be (PROXY <type> <remote addr> <local addr> <remote port> <local port>)
parts := strings.Split(header, " ")
if len(parts) < 2 {
p.conn.Close()
return &ErrBadHeader{[]byte(header)}
}
// Verify the type is known
switch parts[1] {
case "UNKNOWN":
if !p.acceptUnknown || len(parts) != 2 {
p.conn.Close()
return &ErrBadHeader{[]byte(header)}
}
p.remoteAddr = p.conn.LocalAddr()
p.localAddr = p.conn.RemoteAddr()
return nil
case "TCP4":
case "TCP6":
default:
p.conn.Close()
return &ErrBadAddressType{parts[1]}
}
if len(parts) != 6 {
p.conn.Close()
return &ErrBadHeader{[]byte(header)}
}
// Parse out the remote address
ip := net.ParseIP(parts[2])
if ip == nil {
p.conn.Close()
return &ErrBadRemote{parts[2], parts[4]}
}
port, err := strconv.Atoi(parts[4])
if err != nil {
p.conn.Close()
return &ErrBadRemote{parts[2], parts[4]}
}
p.remoteAddr = &net.TCPAddr{IP: ip, Port: port}
// Parse out the destination address
ip = net.ParseIP(parts[3])
if ip == nil {
p.conn.Close()
return &ErrBadLocal{parts[3], parts[5]}
}
port, err = strconv.Atoi(parts[5])
if err != nil {
p.conn.Close()
return &ErrBadLocal{parts[3], parts[5]}
}
p.localAddr = &net.TCPAddr{IP: ip, Port: port}
return nil
}

View file

@ -0,0 +1,45 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package proxyprotocol
import "fmt"
// ErrBadHeader is an error demonstrating a bad proxy header
type ErrBadHeader struct {
Header []byte
}
func (e *ErrBadHeader) Error() string {
return fmt.Sprintf("Unexpected proxy header: %v", e.Header)
}
// ErrBadAddressType is an error demonstrating a bad proxy header with bad Address type
type ErrBadAddressType struct {
Address string
}
func (e *ErrBadAddressType) Error() string {
return fmt.Sprintf("Unexpected proxy header address type: %s", e.Address)
}
// ErrBadRemote is an error demonstrating a bad proxy header with bad Remote
type ErrBadRemote struct {
IP string
Port string
}
func (e *ErrBadRemote) Error() string {
return fmt.Sprintf("Unexpected proxy header remote IP and port: %s %s", e.IP, e.Port)
}
// ErrBadLocal is an error demonstrating a bad proxy header with bad Local
type ErrBadLocal struct {
IP string
Port string
}
func (e *ErrBadLocal) Error() string {
return fmt.Sprintf("Unexpected proxy header local IP and port: %s %s", e.IP, e.Port)
}

View file

@ -0,0 +1,47 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package proxyprotocol
import (
"net"
"time"
)
// Listener is used to wrap an underlying listener,
// whose connections may be using the HAProxy Proxy Protocol (version 1 or 2).
// 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
AcceptUnknown bool // allow PROXY UNKNOWN
}
// Accept implements the Accept method in the Listener interface
// it waits for the next call and returns a wrapped Conn.
func (p *Listener) Accept() (net.Conn, error) {
// Get the underlying connection
conn, err := p.Listener.Accept()
if err != nil {
return nil, err
}
newConn := NewConn(conn, p.ProxyHeaderTimeout)
newConn.acceptUnknown = p.AcceptUnknown
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()
}

View file

@ -0,0 +1,15 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package proxyprotocol
import "io"
var localHeader = append(v2Prefix, '\x20', '\x00', '\x00', '\x00', '\x00')
// WriteLocalHeader will write the ProxyProtocol Header for a local connection to the provided writer
func WriteLocalHeader(w io.Writer) error {
_, err := w.Write(localHeader)
return err
}

View file

@ -94,45 +94,52 @@ var (
LocalURL string
// Server settings
Protocol Scheme
Domain string
HTTPAddr string
HTTPPort string
RedirectOtherPort bool
PortToRedirect string
OfflineMode bool
CertFile string
KeyFile string
StaticRootPath string
StaticCacheTime time.Duration
EnableGzip bool
LandingPageURL LandingPage
LandingPageCustom string
UnixSocketPermission uint32
EnablePprof bool
PprofDataPath string
EnableAcme bool
AcmeTOS bool
AcmeLiveDirectory string
AcmeEmail string
AcmeURL string
AcmeCARoot string
SSLMinimumVersion string
SSLMaximumVersion string
SSLCurvePreferences []string
SSLCipherSuites []string
GracefulRestartable bool
GracefulHammerTime time.Duration
StartupTimeout time.Duration
PerWriteTimeout = 30 * time.Second
PerWritePerKbTimeout = 10 * time.Second
StaticURLPrefix string
AbsoluteAssetURL string
Protocol Scheme
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
ProxyProtocolHeaderTimeout time.Duration
ProxyProtocolAcceptUnknown bool
Domain string
HTTPAddr string
HTTPPort string
LocalUseProxyProtocol bool
RedirectOtherPort bool
RedirectorUseProxyProtocol bool
PortToRedirect string
OfflineMode bool
CertFile string
KeyFile string
StaticRootPath string
StaticCacheTime time.Duration
EnableGzip bool
LandingPageURL LandingPage
LandingPageCustom string
UnixSocketPermission uint32
EnablePprof bool
PprofDataPath string
EnableAcme bool
AcmeTOS bool
AcmeLiveDirectory string
AcmeEmail string
AcmeURL string
AcmeCARoot string
SSLMinimumVersion string
SSLMaximumVersion string
SSLCurvePreferences []string
SSLCipherSuites []string
GracefulRestartable bool
GracefulHammerTime time.Duration
StartupTimeout time.Duration
PerWriteTimeout = 30 * time.Second
PerWritePerKbTimeout = 10 * time.Second
StaticURLPrefix string
AbsoluteAssetURL string
SSH = struct {
Disabled bool `ini:"DISABLE_SSH"`
StartBuiltinServer bool `ini:"START_SSH_SERVER"`
BuiltinServerUser string `ini:"BUILTIN_SSH_SERVER_USER"`
UseProxyProtocol bool `ini:"SSH_SERVER_USE_PROXY_PROTOCOL"`
Domain string `ini:"SSH_DOMAIN"`
Port int `ini:"SSH_PORT"`
User string `ini:"SSH_USER"`
@ -717,6 +724,10 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
}
}
UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false)
ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false)
ProxyProtocolHeaderTimeout = sec.Key("PROXY_PROTOCOL_HEADER_TIMEOUT").MustDuration(5 * time.Second)
ProxyProtocolAcceptUnknown = sec.Key("PROXY_PROTOCOL_ACCEPT_UNKNOWN").MustBool(false)
GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true)
GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second)
StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(0 * time.Second)
@ -770,8 +781,10 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
}
LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL)
LocalURL = strings.TrimRight(LocalURL, "/") + "/"
LocalUseProxyProtocol = sec.Key("LOCAL_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol)
RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false)
PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80")
RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol)
OfflineMode = sec.Key("OFFLINE_MODE").MustBool()
DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool()
if len(StaticRootPath) == 0 {
@ -836,6 +849,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
SSH.KeygenPath = sec.Key("SSH_KEYGEN_PATH").MustString("ssh-keygen")
SSH.Port = sec.Key("SSH_PORT").MustInt(22)
SSH.ListenPort = sec.Key("SSH_LISTEN_PORT").MustInt(SSH.Port)
SSH.UseProxyProtocol = sec.Key("SSH_SERVER_USE_PROXY_PROTOCOL").MustBool(false)
// When disable SSH, start builtin server value is ignored.
if SSH.Disabled {

View file

@ -17,7 +17,7 @@ func listen(server *ssh.Server) {
gracefulServer.PerWriteTimeout = setting.SSH.PerWriteTimeout
gracefulServer.PerWritePerKbTimeout = setting.SSH.PerWritePerKbTimeout
err := gracefulServer.ListenAndServe(server.Serve)
err := gracefulServer.ListenAndServe(server.Serve, setting.SSH.UseProxyProtocol)
if err != nil {
select {
case <-graceful.GetManager().IsShutdown():