@marcel91200 / caddy Mirror · read-only / commits / b6fe5d4b
Fork 0

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"