Skip to content

Commit 943753f

Browse files
authored
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>
1 parent 0b4c166 commit 943753f

File tree

15 files changed

+786
-73
lines changed

15 files changed

+786
-73
lines changed

Diff for: cmd/web.go

+12-14
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func runHTTPRedirector() {
7676
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
7777
})
7878

79-
err := runHTTP("tcp", source, "HTTP Redirector", handler)
79+
err := runHTTP("tcp", source, "HTTP Redirector", handler, setting.RedirectorUseProxyProtocol)
8080
if err != nil {
8181
log.Fatal("Failed to start port redirection: %v", err)
8282
}
@@ -231,40 +231,38 @@ func listen(m http.Handler, handleRedirector bool) error {
231231
if handleRedirector {
232232
NoHTTPRedirector()
233233
}
234-
err = runHTTP("tcp", listenAddr, "Web", m)
234+
err = runHTTP("tcp", listenAddr, "Web", m, setting.UseProxyProtocol)
235235
case setting.HTTPS:
236236
if setting.EnableAcme {
237237
err = runACME(listenAddr, m)
238238
break
239-
} else {
240-
if handleRedirector {
241-
if setting.RedirectOtherPort {
242-
go runHTTPRedirector()
243-
} else {
244-
NoHTTPRedirector()
245-
}
239+
}
240+
if handleRedirector {
241+
if setting.RedirectOtherPort {
242+
go runHTTPRedirector()
243+
} else {
244+
NoHTTPRedirector()
246245
}
247-
err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m)
248246
}
247+
err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m, setting.UseProxyProtocol, setting.ProxyProtocolTLSBridging)
249248
case setting.FCGI:
250249
if handleRedirector {
251250
NoHTTPRedirector()
252251
}
253-
err = runFCGI("tcp", listenAddr, "FCGI Web", m)
252+
err = runFCGI("tcp", listenAddr, "FCGI Web", m, setting.UseProxyProtocol)
254253
case setting.HTTPUnix:
255254
if handleRedirector {
256255
NoHTTPRedirector()
257256
}
258-
err = runHTTP("unix", listenAddr, "Web", m)
257+
err = runHTTP("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
259258
case setting.FCGIUnix:
260259
if handleRedirector {
261260
NoHTTPRedirector()
262261
}
263-
err = runFCGI("unix", listenAddr, "Web", m)
262+
err = runFCGI("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
264263
default:
265264
log.Fatal("Invalid protocol: %s", setting.Protocol)
266265
}
267-
268266
if err != nil {
269267
log.Critical("Failed to start server: %v", err)
270268
}

Diff for: cmd/web_acme.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,14 @@ func runACME(listenAddr string, m http.Handler) error {
113113

114114
log.Info("Running Let's Encrypt handler on %s", setting.HTTPAddr+":"+setting.PortToRedirect)
115115
// all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validation happens here)
116-
err := runHTTP("tcp", setting.HTTPAddr+":"+setting.PortToRedirect, "Let's Encrypt HTTP Challenge", myACME.HTTPChallengeHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)))
116+
err := runHTTP("tcp", setting.HTTPAddr+":"+setting.PortToRedirect, "Let's Encrypt HTTP Challenge", myACME.HTTPChallengeHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)), setting.RedirectorUseProxyProtocol)
117117
if err != nil {
118118
log.Fatal("Failed to start the Let's Encrypt handler on port %s: %v", setting.PortToRedirect, err)
119119
}
120120
}()
121121
}
122122

123-
return runHTTPSWithTLSConfig("tcp", listenAddr, "Web", tlsConfig, m)
123+
return runHTTPSWithTLSConfig("tcp", listenAddr, "Web", tlsConfig, m, setting.UseProxyProtocol, setting.ProxyProtocolTLSBridging)
124124
}
125125

126126
func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {

Diff for: cmd/web_graceful.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import (
1515
"code.gitea.io/gitea/modules/setting"
1616
)
1717

18-
func runHTTP(network, listenAddr, name string, m http.Handler) error {
19-
return graceful.HTTPListenAndServe(network, listenAddr, name, m)
18+
func runHTTP(network, listenAddr, name string, m http.Handler, useProxyProtocol bool) error {
19+
return graceful.HTTPListenAndServe(network, listenAddr, name, m, useProxyProtocol)
2020
}
2121

2222
// NoHTTPRedirector tells our cleanup routine that we will not be using a fallback http redirector
@@ -36,7 +36,7 @@ func NoInstallListener() {
3636
graceful.GetManager().InformCleanup()
3737
}
3838

39-
func runFCGI(network, listenAddr, name string, m http.Handler) error {
39+
func runFCGI(network, listenAddr, name string, m http.Handler, useProxyProtocol bool) error {
4040
// This needs to handle stdin as fcgi point
4141
fcgiServer := graceful.NewServer(network, listenAddr, name)
4242

@@ -47,7 +47,7 @@ func runFCGI(network, listenAddr, name string, m http.Handler) error {
4747
}
4848
m.ServeHTTP(resp, req)
4949
}))
50-
})
50+
}, useProxyProtocol)
5151
if err != nil {
5252
log.Fatal("Failed to start FCGI main server: %v", err)
5353
}

Diff for: cmd/web_https.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,14 @@ var (
129129
defaultCiphersChaChaFirst = append(defaultCiphersChaCha, defaultCiphersAES...)
130130
)
131131

132-
// runHTTPs listens on the provided network address and then calls
132+
// runHTTPS listens on the provided network address and then calls
133133
// Serve to handle requests on incoming TLS connections.
134134
//
135135
// Filenames containing a certificate and matching private key for the server must
136136
// be provided. If the certificate is signed by a certificate authority, the
137137
// certFile should be the concatenation of the server's certificate followed by the
138138
// CA's certificate.
139-
func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler) error {
139+
func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
140140
tlsConfig := &tls.Config{}
141141
if tlsConfig.NextProtos == nil {
142142
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
@@ -184,9 +184,9 @@ func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handle
184184
return err
185185
}
186186

187-
return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m)
187+
return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m, useProxyProtocol, proxyProtocolTLSBridging)
188188
}
189189

190-
func runHTTPSWithTLSConfig(network, listenAddr, name string, tlsConfig *tls.Config, m http.Handler) error {
191-
return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m)
190+
func runHTTPSWithTLSConfig(network, listenAddr, name string, tlsConfig *tls.Config, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
191+
return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m, useProxyProtocol, proxyProtocolTLSBridging)
192192
}

Diff for: custom/conf/app.example.ini

+21-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ RUN_MODE = ; prod
2929
;; The protocol the server listens on. One of 'http', 'https', 'unix' or 'fcgi'. Defaults to 'http'
3030
;PROTOCOL = http
3131
;;
32+
;; Expect PROXY protocol headers on connections
33+
;USE_PROXY_PROTOCOL = false
34+
;;
35+
;; Use PROXY protocol in TLS Bridging mode
36+
;PROXY_PROTOCOL_TLS_BRIDGING = false
37+
;;
38+
; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
39+
;PROXY_PROTOCOL_HEADER_TIMEOUT=5s
40+
;;
41+
; Accept PROXY protocol headers with UNKNOWN type
42+
;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false
43+
;;
3244
;; Set the domain for the server
3345
;DOMAIN = localhost
3446
;;
@@ -51,6 +63,8 @@ RUN_MODE = ; prod
5163
;REDIRECT_OTHER_PORT = false
5264
;PORT_TO_REDIRECT = 80
5365
;;
66+
;; expect PROXY protocol header on connections to https redirector.
67+
;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)
5468
;; Minimum and maximum supported TLS versions
5569
;SSL_MIN_VERSION=TLSv1.2
5670
;SSL_MAX_VERSION=
@@ -76,13 +90,19 @@ RUN_MODE = ; prod
7690
;; Do not set this variable if PROTOCOL is set to 'unix'.
7791
;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/
7892
;;
93+
;; When making local connections pass the PROXY protocol header.
94+
;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)
95+
;;
7996
;; Disable SSH feature when not available
8097
;DISABLE_SSH = false
8198
;;
8299
;; Whether to use the builtin SSH server or not.
83100
;START_SSH_SERVER = false
84101
;;
85-
;; Username to use for the builtin SSH server.
102+
;; Expect PROXY protocol header on connections to the built-in SSH server
103+
;SSH_SERVER_USE_PROXY_PROTOCOL = false
104+
;;
105+
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
86106
;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s
87107
;;
88108
;; Domain name to be exposed in clone URL

Diff for: docs/content/doc/advanced/config-cheat-sheet.en-us.md

+8
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ The following configuration set `Content-Type: application/vnd.android.package-a
238238
## Server (`server`)
239239

240240
- `PROTOCOL`: **http**: \[http, https, fcgi, http+unix, fcgi+unix\]
241+
- `USE_PROXY_PROTOCOL`: **false**: Expect PROXY protocol headers on connections
242+
- `PROXY_PROTOCOL_TLS_BRIDGING`: **false**: When protocol is https, expect PROXY protocol headers after TLS negotiation.
243+
- `PROXY_PROTOCOL_HEADER_TIMEOUT`: **5s**: Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
244+
- `PROXY_PROTOCOL_ACCEPT_UNKNOWN`: **false**: Accept PROXY protocol headers with Unknown type.
241245
- `DOMAIN`: **localhost**: Domain name of this server.
242246
- `ROOT_URL`: **%(PROTOCOL)s://%(DOMAIN)s:%(HTTP\_PORT)s/**:
243247
Overwrite the automatically generated public URL.
@@ -262,12 +266,15 @@ The following configuration set `Content-Type: application/vnd.android.package-a
262266
most cases you do not need to change the default value. Alter it only if
263267
your SSH server node is not the same as HTTP node. Do not set this variable
264268
if `PROTOCOL` is set to `http+unix`.
269+
- `LOCAL_USE_PROXY_PROTOCOL`: **%(USE_PROXY_PROTOCOL)**: When making local connections pass the PROXY protocol header.
270+
This should be set to false if the local connection will go through the proxy.
265271
- `PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the connection. (Set to -1 to
266272
disable all timeouts.)
267273
- `PER_WRITE_PER_KB_TIMEOUT`: **10s**: Timeout per Kb written to connections.
268274

269275
- `DISABLE_SSH`: **false**: Disable SSH feature when it's not available.
270276
- `START_SSH_SERVER`: **false**: When enabled, use the built-in SSH server.
277+
- `SSH_SERVER_USE_PROXY_PROTOCOL`: **false**: Expect PROXY protocol header on connections to the built-in SSH Server.
271278
- `BUILTIN_SSH_SERVER_USER`: **%(RUN_USER)s**: Username to use for the built-in SSH Server.
272279
- `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`.
273280
- `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
313320
- `LFS_LOCKS_PAGING_NUM`: **50**: Maximum number of LFS Locks returned per page.
314321

315322
- `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.
323+
- `REDIRECTOR_USE_PROXY_PROTOCOL`: **%(USE_PROXY_PROTOCOL)**: expect PROXY protocol header on connections to https redirector.
316324
- `PORT_TO_REDIRECT`: **80**: Port for the http redirection service to listen on. Used when `REDIRECT_OTHER_PORT` is true.
317325
- `SSL_MIN_VERSION`: **TLSv1.2**: Set the minimum version of ssl support.
318326
- `SSL_MAX_VERSION`: **\<empty\>**: Set the maximum version of ssl support.

Diff for: modules/graceful/server.go

+41-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"time"
1717

1818
"code.gitea.io/gitea/modules/log"
19+
"code.gitea.io/gitea/modules/proxyprotocol"
1920
"code.gitea.io/gitea/modules/setting"
2021
)
2122

@@ -79,16 +80,27 @@ func NewServer(network, address, name string) *Server {
7980

8081
// ListenAndServe listens on the provided network address and then calls Serve
8182
// to handle requests on incoming connections.
82-
func (srv *Server) ListenAndServe(serve ServeFunction) error {
83+
func (srv *Server) ListenAndServe(serve ServeFunction, useProxyProtocol bool) error {
8384
go srv.awaitShutdown()
8485

85-
l, err := GetListener(srv.network, srv.address)
86+
listener, err := GetListener(srv.network, srv.address)
8687
if err != nil {
8788
log.Error("Unable to GetListener: %v", err)
8889
return err
8990
}
9091

91-
srv.listener = newWrappedListener(l, srv)
92+
// we need to wrap the listener to take account of our lifecycle
93+
listener = newWrappedListener(listener, srv)
94+
95+
// Now we need to take account of ProxyProtocol settings...
96+
if useProxyProtocol {
97+
listener = &proxyprotocol.Listener{
98+
Listener: listener,
99+
ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout,
100+
AcceptUnknown: setting.ProxyProtocolAcceptUnknown,
101+
}
102+
}
103+
srv.listener = listener
92104

93105
srv.BeforeBegin(srv.network, srv.address)
94106

@@ -97,22 +109,44 @@ func (srv *Server) ListenAndServe(serve ServeFunction) error {
97109

98110
// ListenAndServeTLSConfig listens on the provided network address and then calls
99111
// Serve to handle requests on incoming TLS connections.
100-
func (srv *Server) ListenAndServeTLSConfig(tlsConfig *tls.Config, serve ServeFunction) error {
112+
func (srv *Server) ListenAndServeTLSConfig(tlsConfig *tls.Config, serve ServeFunction, useProxyProtocol, proxyProtocolTLSBridging bool) error {
101113
go srv.awaitShutdown()
102114

103115
if tlsConfig.MinVersion == 0 {
104116
tlsConfig.MinVersion = tls.VersionTLS12
105117
}
106118

107-
l, err := GetListener(srv.network, srv.address)
119+
listener, err := GetListener(srv.network, srv.address)
108120
if err != nil {
109121
log.Error("Unable to get Listener: %v", err)
110122
return err
111123
}
112124

113-
wl := newWrappedListener(l, srv)
114-
srv.listener = tls.NewListener(wl, tlsConfig)
125+
// we need to wrap the listener to take account of our lifecycle
126+
listener = newWrappedListener(listener, srv)
127+
128+
// 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
129+
if useProxyProtocol && !proxyProtocolTLSBridging {
130+
listener = &proxyprotocol.Listener{
131+
Listener: listener,
132+
ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout,
133+
AcceptUnknown: setting.ProxyProtocolAcceptUnknown,
134+
}
135+
}
136+
137+
// Now handle the tls protocol
138+
listener = tls.NewListener(listener, tlsConfig)
139+
140+
// Now if we're bridging then we need the proxy to tell us who we're bridging for...
141+
if useProxyProtocol && proxyProtocolTLSBridging {
142+
listener = &proxyprotocol.Listener{
143+
Listener: listener,
144+
ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout,
145+
AcceptUnknown: setting.ProxyProtocolAcceptUnknown,
146+
}
147+
}
115148

149+
srv.listener = listener
116150
srv.BeforeBegin(srv.network, srv.address)
117151

118152
return srv.Serve(serve)

Diff for: modules/graceful/server_http.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ func newHTTPServer(network, address, name string, handler http.Handler) (*Server
2828

2929
// HTTPListenAndServe listens on the provided network address and then calls Serve
3030
// to handle requests on incoming connections.
31-
func HTTPListenAndServe(network, address, name string, handler http.Handler) error {
31+
func HTTPListenAndServe(network, address, name string, handler http.Handler, useProxyProtocol bool) error {
3232
server, lHandler := newHTTPServer(network, address, name, handler)
33-
return server.ListenAndServe(lHandler)
33+
return server.ListenAndServe(lHandler, useProxyProtocol)
3434
}
3535

3636
// HTTPListenAndServeTLSConfig listens on the provided network address and then calls Serve
3737
// to handle requests on incoming connections.
38-
func HTTPListenAndServeTLSConfig(network, address, name string, tlsConfig *tls.Config, handler http.Handler) error {
38+
func HTTPListenAndServeTLSConfig(network, address, name string, tlsConfig *tls.Config, handler http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
3939
server, lHandler := newHTTPServer(network, address, name, handler)
40-
return server.ListenAndServeTLSConfig(tlsConfig, lHandler)
40+
return server.ListenAndServeTLSConfig(tlsConfig, lHandler, useProxyProtocol, proxyProtocolTLSBridging)
4141
}

Diff for: modules/private/internal.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"code.gitea.io/gitea/modules/httplib"
1515
"code.gitea.io/gitea/modules/json"
1616
"code.gitea.io/gitea/modules/log"
17+
"code.gitea.io/gitea/modules/proxyprotocol"
1718
"code.gitea.io/gitea/modules/setting"
1819
)
1920

@@ -50,7 +51,32 @@ func newInternalRequest(ctx context.Context, url, method string) *httplib.Reques
5051
req.SetTransport(&http.Transport{
5152
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
5253
var d net.Dialer
53-
return d.DialContext(ctx, "unix", setting.HTTPAddr)
54+
conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr)
55+
if err != nil {
56+
return conn, err
57+
}
58+
if setting.LocalUseProxyProtocol {
59+
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
60+
_ = conn.Close()
61+
return nil, err
62+
}
63+
}
64+
return conn, err
65+
},
66+
})
67+
} else if setting.LocalUseProxyProtocol {
68+
req.SetTransport(&http.Transport{
69+
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
70+
var d net.Dialer
71+
conn, err := d.DialContext(ctx, network, address)
72+
if err != nil {
73+
return conn, err
74+
}
75+
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
76+
_ = conn.Close()
77+
return nil, err
78+
}
79+
return conn, err
5480
},
5581
})
5682
}

0 commit comments

Comments
 (0)