Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 04:15 UTC
proxyprotocol: Add PROXY protocol support to `reverse_proxy`, add HTTP listener wrapper (#5424)
Co-authored-by: WeidiDeng <weidi_deng@icloud.com> Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Corin Langosch committed
Mar 31, 2023 at 23:44 UTC
b6fe5d4b41d07e70a502ed58d40e8b0e75067db5
9 files changed
+245
-1
go.mod
+1
@@ -14,6 +14,7 @@ require (
14
github.com/google/uuid v1.3.0
15
github.com/klauspost/compress v1.16.0
16
github.com/klauspost/cpuid/v2 v2.2.4
17
+ github.com/mastercactapus/proxyprotocol v0.0.4
18
github.com/mholt/acmez v1.1.0
19
github.com/prometheus/client_golang v1.14.0
20
github.com/quic-go/quic-go v0.33.0
go.sum
+2
@@ -443,6 +443,8 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q
443
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
444
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
445
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
446
+github.com/mastercactapus/proxyprotocol v0.0.4 h1:qSY75IZF30ZqIU9iW1ip3I7gTnm8wRAnGWqPxCBVgq0=
447
+github.com/mastercactapus/proxyprotocol v0.0.4/go.mod h1:X8FRVEDZz9FkrIoL4QYTBF4Ka4ELwTv0sah0/5NxCPw=
448
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
449
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
450
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
modules/caddyhttp/proxyprotocol/listenerwrapper.go
new
+68
@@ -0,0 +1,68 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package proxyprotocol
16
+
17
+import (
18
+ "fmt"
19
+ "net"
20
+ "time"
21
+
22
+ "github.com/caddyserver/caddy/v2"
23
+ "github.com/mastercactapus/proxyprotocol"
24
+)
25
+
26
+// ListenerWrapper provides PROXY protocol support to Caddy by implementing
27
+// the caddy.ListenerWrapper interface. It must be loaded before the `tls` listener.
28
+//
29
+// Credit goes to https://github.com/mastercactapus/caddy2-proxyprotocol for having
30
+// initially implemented this as a plugin.
31
+type ListenerWrapper struct {
32
+ // Timeout specifies an optional maximum time for
33
+ // the PROXY header to be received.
34
+ // If zero, timeout is disabled. Default is 5s.
35
+ Timeout caddy.Duration `json:"timeout,omitempty"`
36
+
37
+ // Allow is an optional list of CIDR ranges to
38
+ // allow/require PROXY headers from.
39
+ Allow []string `json:"allow,omitempty"`
40
+
41
+ rules []proxyprotocol.Rule
42
+}
43
+
44
+// Provision sets up the listener wrapper.
45
+func (pp *ListenerWrapper) Provision(ctx caddy.Context) error {
46
+ rules := make([]proxyprotocol.Rule, 0, len(pp.Allow))
47
+ for _, s := range pp.Allow {
48
+ _, n, err := net.ParseCIDR(s)
49
+ if err != nil {
50
+ return fmt.Errorf("invalid subnet '%s': %w", s, err)
51
+ }
52
+ rules = append(rules, proxyprotocol.Rule{
53
+ Timeout: time.Duration(pp.Timeout),
54
+ Subnet: n,
55
+ })
56
+ }
57
+
58
+ pp.rules = rules
59
+
60
+ return nil
61
+}
62
+
63
+// WrapListener adds PROXY protocol support to the listener.
64
+func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener {
65
+ pl := proxyprotocol.NewListener(l, time.Duration(pp.Timeout))
66
+ pl.SetFilter(pp.rules)
67
+ return pl
68
+}
modules/caddyhttp/proxyprotocol/module.go
new
+75
@@ -0,0 +1,75 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package proxyprotocol
16
+
17
+import (
18
+ "github.com/caddyserver/caddy/v2"
19
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
20
+)
21
+
22
+func init() {
23
+ caddy.RegisterModule(ListenerWrapper{})
24
+}
25
+
26
+func (ListenerWrapper) CaddyModule() caddy.ModuleInfo {
27
+ return caddy.ModuleInfo{
28
+ ID: "caddy.listeners.proxy_protocol",
29
+ New: func() caddy.Module { return new(ListenerWrapper) },
30
+ }
31
+}
32
+
33
+// UnmarshalCaddyfile sets up the listener Listenerwrapper from Caddyfile tokens. Syntax:
34
+//
35
+// proxy_protocol {
36
+// timeout <duration>
37
+// allow <IPs...>
38
+// }
39
+func (w *ListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
40
+ for d.Next() {
41
+ // No same-line options are supported
42
+ if d.NextArg() {
43
+ return d.ArgErr()
44
+ }
45
+
46
+ for d.NextBlock(0) {
47
+ switch d.Val() {
48
+ case "timeout":
49
+ if !d.NextArg() {
50
+ return d.ArgErr()
51
+ }
52
+ dur, err := caddy.ParseDuration(d.Val())
53
+ if err != nil {
54
+ return d.Errf("parsing proxy_protocol timeout duration: %v", err)
55
+ }
56
+ w.Timeout = caddy.Duration(dur)
57
+
58
+ case "allow":
59
+ w.Allow = append(w.Allow, d.RemainingArgs()...)
60
+
61
+ default:
62
+ return d.ArgErr()
63
+ }
64
+ }
65
+ }
66
+ return nil
67
+}
68
+
69
+// Interface guards
70
+var (
71
+ _ caddy.Provisioner = (*ListenerWrapper)(nil)
72
+ _ caddy.Module = (*ListenerWrapper)(nil)
73
+ _ caddy.ListenerWrapper = (*ListenerWrapper)(nil)
74
+ _ caddyfile.Unmarshaler = (*ListenerWrapper)(nil)
75
+)
modules/caddyhttp/reverseproxy/caddyfile.go
+11
@@ -918,6 +918,17 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
918
}
919
h.MaxResponseHeaderSize = int64(size)
920
921
+ case "proxy_protocol":
922
+ if !d.NextArg() {
923
+ return d.ArgErr()
924
+ }
925
+ switch proxyProtocol := d.Val(); proxyProtocol {
926
+ case "v1", "v2":
927
+ h.ProxyProtocol = proxyProtocol
928
+ default:
929
+ return d.Errf("invalid proxy protocol version '%s'", proxyProtocol)
930
+ }
931
+
932
case "dial_timeout":
933
if !d.NextArg() {
934
return d.ArgErr()
modules/caddyhttp/reverseproxy/hosts.go
+11
@@ -19,6 +19,7 @@ import (
19
"fmt"
20
"net"
21
"net/http"
22
+ "net/netip"
23
"strconv"
24
"sync/atomic"
25
@@ -259,3 +260,13 @@ var hosts = caddy.NewUsagePool()
260
// dialInfoVarKey is the key used for the variable that holds
261
// the dial info for the upstream connection.
262
const dialInfoVarKey = "reverse_proxy.dial_info"
263
+
264
+// proxyProtocolInfoVarKey is the key used for the variable that holds
265
+// the proxy protocol info for the upstream connection.
266
+const proxyProtocolInfoVarKey = "reverse_proxy.proxy_protocol_info"
267
+
268
+// ProxyProtocolInfo contains information needed to write proxy protocol to a
269
+// connection to an upstream host.
270
+type ProxyProtocolInfo struct {
271
+ AddrPort netip.AddrPort
272
+}
modules/caddyhttp/reverseproxy/httptransport.go
+65
@@ -29,7 +29,9 @@ import (
29
"time"
30
31
"github.com/caddyserver/caddy/v2"
32
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
33
"github.com/caddyserver/caddy/v2/modules/caddytls"
34
+ "github.com/mastercactapus/proxyprotocol"
35
"go.uber.org/zap"
36
"golang.org/x/net/http2"
37
)
@@ -64,6 +66,10 @@ type HTTPTransport struct {
66
// Maximum number of connections per host. Default: 0 (no limit)
67
MaxConnsPerHost int `json:"max_conns_per_host,omitempty"`
68
69
+ // If non-empty, which PROXY protocol version to send when
70
+ // connecting to an upstream. Default: off.
71
+ ProxyProtocol string `json:"proxy_protocol,omitempty"`
72
+
73
// How long to wait before timing out trying to connect to
74
// an upstream. Default: `3s`.
75
DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
@@ -195,6 +201,57 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
201
return nil, DialError{err}
202
}
203
204
+ if h.ProxyProtocol != "" {
205
+ proxyProtocolInfo, ok := caddyhttp.GetVar(ctx, proxyProtocolInfoVarKey).(ProxyProtocolInfo)
206
+ if !ok {
207
+ return nil, fmt.Errorf("failed to get proxy protocol info from context")
208
+ }
209
+
210
+ // The src and dst have to be of the some address family. As we don't know the original
211
+ // dst address (it's kind of impossible to know) and this address is generelly of very
212
+ // little interest, we just set it to all zeros.
213
+ var destIP net.IP
214
+ switch {
215
+ case proxyProtocolInfo.AddrPort.Addr().Is4():
216
+ destIP = net.IPv4zero
217
+ case proxyProtocolInfo.AddrPort.Addr().Is6():
218
+ destIP = net.IPv6zero
219
+ default:
220
+ return nil, fmt.Errorf("unexpected remote addr type in proxy protocol info")
221
+ }
222
+
223
+ // TODO: We should probably migrate away from net.IP to use netip.Addr,
224
+ // but due to the upstream dependency, we can't do that yet.
225
+ switch h.ProxyProtocol {
226
+ case "v1":
227
+ header := proxyprotocol.HeaderV1{
228
+ SrcIP: net.IP(proxyProtocolInfo.AddrPort.Addr().AsSlice()),
229
+ SrcPort: int(proxyProtocolInfo.AddrPort.Port()),
230
+ DestIP: destIP,
231
+ DestPort: 0,
232
+ }
233
+ caddyCtx.Logger().Debug("sending proxy protocol header v1", zap.Any("header", header))
234
+ _, err = header.WriteTo(conn)
235
+ case "v2":
236
+ header := proxyprotocol.HeaderV2{
237
+ Command: proxyprotocol.CmdProxy,
238
+ Src: &net.TCPAddr{IP: net.IP(proxyProtocolInfo.AddrPort.Addr().AsSlice()), Port: int(proxyProtocolInfo.AddrPort.Port())},
239
+ Dest: &net.TCPAddr{IP: destIP, Port: 0},
240
+ }
241
+ caddyCtx.Logger().Debug("sending proxy protocol header v2", zap.Any("header", header))
242
+ _, err = header.WriteTo(conn)
243
+ default:
244
+ return nil, fmt.Errorf("unexpected proxy protocol version")
245
+ }
246
+
247
+ if err != nil {
248
+ // identify this error as one that occurred during
249
+ // dialing, which can be important when trying to
250
+ // decide whether to retry a request
251
+ return nil, DialError{err}
252
+ }
253
+ }
254
+
255
// if read/write timeouts are configured and this is a TCP connection,
256
// enforce the timeouts by wrapping the connection with our own type
257
if tcpConn, ok := conn.(*net.TCPConn); ok && (h.ReadTimeout > 0 || h.WriteTimeout > 0) {
@@ -239,6 +296,14 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
296
rt.IdleConnTimeout = time.Duration(h.KeepAlive.IdleConnTimeout)
297
}
298
299
+ // The proxy protocol header can only be sent once right after opening the connection.
300
+ // So single connection must not be used for multiple requests, which can potentially
301
+ // come from different clients.
302
+ if !rt.DisableKeepAlives && h.ProxyProtocol != "" {
303
+ caddyCtx.Logger().Warn("disabling keepalives, they are incompatible with using PROXY protocol")
304
+ rt.DisableKeepAlives = true
305
+ }
306
+
307
if h.Compression != nil {
308
rt.DisableCompression = !*h.Compression
309
}
modules/caddyhttp/reverseproxy/reverseproxy.go
+11
-1
@@ -688,8 +688,18 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
688
req.Header.Set("Upgrade", reqUpType)
689
}
690
691
+ // Set up the PROXY protocol info
692
+ address := caddyhttp.GetVar(req.Context(), caddyhttp.ClientIPVarKey).(string)
693
+ addrPort, err := netip.ParseAddrPort(address)
694
+ if err != nil {
695
+ // OK; probably didn't have a port
696
+ addrPort, _ = netip.ParseAddrPort(address + ":0")
697
+ }
698
+ proxyProtocolInfo := ProxyProtocolInfo{AddrPort: addrPort}
699
+ caddyhttp.SetVar(req.Context(), proxyProtocolInfoVarKey, proxyProtocolInfo)
700
+
701
// Add the supported X-Forwarded-* headers
692
- err := h.addForwardedHeaders(req)
702
+ err = h.addForwardedHeaders(req)
703
if err != nil {
704
return nil, err
705
}
modules/caddyhttp/standard/imports.go
+1
@@ -11,6 +11,7 @@ import (
11
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
12
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
13
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/map"
14
+ _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/proxyprotocol"
15
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/push"
16
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
17
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"