@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 595 lines 18.7 KB
Raw
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 // Integration tests for Caddy's PROXY protocol support, covering two distinct
16 // roles that Caddy can play:
17 //
18 // 1. As a PROXY protocol *sender* (reverse proxy outbound transport):
19 // Caddy receives an inbound request from a test client and the
20 // reverse_proxy handler forwards it to an upstream with a PROXY protocol
21 // header (v1 or v2) prepended to the connection. A lightweight backend
22 // built with go-proxyproto validates that the header was received and
23 // carries the correct client address.
24 //
25 // Transport versions tested:
26 // - "1.1" -> plain HTTP/1.1 to the upstream
27 // - "h2c" -> HTTP/2 cleartext (h2c) to the upstream (regression for #7529)
28 // - "2" -> HTTP/2 over TLS (h2) to the upstream
29 //
30 // For each transport version both PROXY protocol v1 and v2 are exercised.
31 //
32 // HTTP/3 (h3) is not included because it uses QUIC/UDP and therefore
33 // bypasses the TCP-level dialContext that injects PROXY protocol headers;
34 // there is no meaningful h3 + proxy protocol sender combination to test.
35 //
36 // 2. As a PROXY protocol *receiver* (server-side listener wrapper):
37 // A raw TCP client dials Caddy directly, injects a PROXY v2 header
38 // spoofing a source address, and sends a normal HTTP/1.1 request. The
39 // Caddy server is configured with the proxy_protocol listener wrapper and
40 // is expected to surface the spoofed address via the
41 // {http.request.remote.host} placeholder.
42
43 package integration
44
45 import (
46 "crypto/tls"
47 "encoding/json"
48 "fmt"
49 "net"
50 "net/http"
51 "net/http/httptest"
52 "slices"
53 "strings"
54 "sync"
55 "testing"
56
57 goproxy "github.com/pires/go-proxyproto"
58 "golang.org/x/net/http2"
59 "golang.org/x/net/http2/h2c"
60
61 "github.com/caddyserver/caddy/v2/caddytest"
62 )
63
64 // proxyProtoBackend is a minimal HTTP server that sits behind a
65 // go-proxyproto listener and records the source address that was
66 // delivered in the PROXY header for each request.
67 type proxyProtoBackend struct {
68 mu sync.Mutex
69 headerAddrs []string // host:port strings extracted from each PROXY header
70
71 ln net.Listener
72 srv *http.Server
73 }
74
75 // newProxyProtoBackend starts a TCP listener wrapped with go-proxyproto on a
76 // random local port and serves requests with a simple "OK" body. The PROXY
77 // header source addresses are accumulated in headerAddrs so tests can
78 // inspect them.
79 func newProxyProtoBackend(t *testing.T) *proxyProtoBackend {
80 t.Helper()
81
82 b := &proxyProtoBackend{}
83
84 rawLn, err := net.Listen("tcp", "127.0.0.1:0")
85 if err != nil {
86 t.Fatalf("backend: listen: %v", err)
87 }
88
89 // Wrap with go-proxyproto so the PROXY header is stripped and parsed
90 // before the HTTP server sees the connection. We use REQUIRE so that a
91 // missing header returns an error instead of silently passing through.
92 pLn := &goproxy.Listener{
93 Listener: rawLn,
94 Policy: func(_ net.Addr) (goproxy.Policy, error) {
95 return goproxy.REQUIRE, nil
96 },
97 }
98 b.ln = pLn
99
100 // Wrap the handler with h2c support so the backend can speak HTTP/2
101 // cleartext (h2c) as well as plain HTTP/1.1. Without this, Caddy's
102 // reverse proxy would receive a 'frame too large' error when the
103 // upstream transport is configured to use h2c.
104 h2Server := &http2.Server{}
105 handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
106 // go-proxyproto has already updated the net.Conn's remote
107 // address to the value from the PROXY header; the HTTP server
108 // surfaces it in r.RemoteAddr.
109 b.mu.Lock()
110 b.headerAddrs = append(b.headerAddrs, r.RemoteAddr)
111 b.mu.Unlock()
112 w.WriteHeader(http.StatusOK)
113 _, _ = fmt.Fprint(w, "OK")
114 })
115
116 b.srv = &http.Server{
117 Handler: h2c.NewHandler(handlerFn, h2Server),
118 }
119
120 go b.srv.Serve(pLn) //nolint:errcheck
121 t.Cleanup(func() {
122 _ = b.srv.Close()
123 _ = rawLn.Close()
124 })
125
126 return b
127 }
128
129 // addr returns the listening address (host:port) of the backend.
130 func (b *proxyProtoBackend) addr() string {
131 return b.ln.Addr().String()
132 }
133
134 // recordedAddrs returns a snapshot of all PROXY-header source addresses seen
135 // so far.
136 func (b *proxyProtoBackend) recordedAddrs() []string {
137 b.mu.Lock()
138 defer b.mu.Unlock()
139 cp := make([]string, len(b.headerAddrs))
140 copy(cp, b.headerAddrs)
141 return cp
142 }
143
144 // tlsProxyProtoBackend is a TLS-enabled backend that sits behind a
145 // go-proxyproto listener. The PROXY header is stripped before the TLS
146 // handshake so the layer order on a connection is:
147 //
148 // raw TCP → go-proxyproto (strips PROXY header) → TLS handshake → HTTP/2
149 type tlsProxyProtoBackend struct {
150 mu sync.Mutex
151 headerAddrs []string
152
153 srv *httptest.Server
154 }
155
156 // newTLSProxyProtoBackend starts a TLS listener that first reads and strips
157 // PROXY protocol headers (go-proxyproto, REQUIRE policy) and then performs a
158 // TLS handshake. The backend speaks HTTP/2 over TLS (h2).
159 //
160 // The certificate is the standard self-signed certificate generated by
161 // httptest.Server; the Caddy transport must be configured with
162 // insecure_skip_verify: true to trust it.
163 func newTLSProxyProtoBackend(t *testing.T) *tlsProxyProtoBackend {
164 t.Helper()
165
166 b := &tlsProxyProtoBackend{}
167
168 handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
169 b.mu.Lock()
170 b.headerAddrs = append(b.headerAddrs, r.RemoteAddr)
171 b.mu.Unlock()
172 w.WriteHeader(http.StatusOK)
173 _, _ = fmt.Fprint(w, "OK")
174 })
175
176 rawLn, err := net.Listen("tcp", "127.0.0.1:0")
177 if err != nil {
178 t.Fatalf("tlsBackend: listen: %v", err)
179 }
180
181 // Wrap with go-proxyproto so the PROXY header is consumed before TLS.
182 pLn := &goproxy.Listener{
183 Listener: rawLn,
184 Policy: func(_ net.Addr) (goproxy.Policy, error) {
185 return goproxy.REQUIRE, nil
186 },
187 }
188
189 // httptest.NewUnstartedServer lets us replace the listener before
190 // calling StartTLS(), which wraps our proxyproto listener with
191 // tls.NewListener. This gives us the right layer order.
192 b.srv = httptest.NewUnstartedServer(handlerFn)
193 b.srv.Listener = pLn
194
195 // StartTLS enables HTTP/2 on the server automatically.
196 b.srv.StartTLS()
197
198 t.Cleanup(func() {
199 b.srv.Close()
200 })
201
202 return b
203 }
204
205 // addr returns the listening address (host:port) of the TLS backend.
206 func (b *tlsProxyProtoBackend) addr() string {
207 return b.srv.Listener.Addr().String()
208 }
209
210 // tlsConfig returns the *tls.Config used by the backend server.
211 // Tests can use it to verify cert details if needed.
212 func (b *tlsProxyProtoBackend) tlsConfig() *tls.Config {
213 return b.srv.TLS
214 }
215
216 // recordedAddrs returns a snapshot of all PROXY-header source addresses.
217 func (b *tlsProxyProtoBackend) recordedAddrs() []string {
218 b.mu.Lock()
219 defer b.mu.Unlock()
220 cp := make([]string, len(b.headerAddrs))
221 copy(cp, b.headerAddrs)
222 return cp
223 }
224
225 // proxyProtoTLSConfig builds a Caddy JSON configuration that proxies to a TLS
226 // upstream with PROXY protocol. The transport uses insecure_skip_verify so
227 // the self-signed certificate generated by httptest.Server is accepted.
228 func proxyProtoTLSConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string {
229 versionsJSON, _ := json.Marshal(transportVersions)
230 return fmt.Sprintf(`{
231 "admin": {
232 "listen": "localhost:2999"
233 },
234 "apps": {
235 "pki": {
236 "certificate_authorities": {
237 "local": {
238 "install_trust": false
239 }
240 }
241 },
242 "http": {
243 "grace_period": 1,
244 "servers": {
245 "proxy": {
246 "listen": [":%d"],
247 "automatic_https": {
248 "disable": true
249 },
250 "routes": [
251 {
252 "handle": [
253 {
254 "handler": "reverse_proxy",
255 "upstreams": [{"dial": "%s"}],
256 "transport": {
257 "protocol": "http",
258 "proxy_protocol": "%s",
259 "versions": %s,
260 "tls": {
261 "insecure_skip_verify": true
262 }
263 }
264 }
265 ]
266 }
267 ]
268 }
269 }
270 }
271 }
272 }`, listenPort, backendAddr, ppVersion, string(versionsJSON))
273 }
274
275 // testTLSProxyProtocolMatrix is the shared implementation for TLS-based proxy
276 // protocol tests. It mirrors testProxyProtocolMatrix but uses a TLS backend.
277 func testTLSProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) {
278 t.Helper()
279
280 backend := newTLSProxyProtoBackend(t)
281 listenPort := freePort(t)
282
283 tester := caddytest.NewTester(t)
284 tester.WithDefaultOverrides(caddytest.Config{
285 AdminPort: 2999,
286 })
287 cfg := proxyProtoTLSConfig(listenPort, backend.addr(), ppVersion, transportVersions)
288 tester.InitServer(cfg, "json")
289
290 proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort)
291
292 for i := 0; i < numRequests; i++ {
293 resp, err := tester.Client.Get(proxyURL)
294 if err != nil {
295 t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err)
296 }
297 resp.Body.Close()
298 if resp.StatusCode != http.StatusOK {
299 t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode)
300 }
301 }
302
303 addrs := backend.recordedAddrs()
304 if len(addrs) == 0 {
305 t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)")
306 }
307
308 for i, addr := range addrs {
309 host, _, err := net.SplitHostPort(addr)
310 if err != nil {
311 t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err)
312 continue
313 }
314 if host != "127.0.0.1" {
315 t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host)
316 }
317 }
318 }
319
320 // proxyProtoConfig builds a Caddy JSON configuration that:
321 // - listens on listenPort for inbound HTTP requests
322 // - proxies them to backendAddr with PROXY protocol ppVersion ("v1"/"v2")
323 // - uses the given transport versions (e.g. ["1.1"] or ["h2c"])
324 func proxyProtoConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string {
325 versionsJSON, _ := json.Marshal(transportVersions)
326 return fmt.Sprintf(`{
327 "admin": {
328 "listen": "localhost:2999"
329 },
330 "apps": {
331 "pki": {
332 "certificate_authorities": {
333 "local": {
334 "install_trust": false
335 }
336 }
337 },
338 "http": {
339 "grace_period": 1,
340 "servers": {
341 "proxy": {
342 "listen": [":%d"],
343 "automatic_https": {
344 "disable": true
345 },
346 "routes": [
347 {
348 "handle": [
349 {
350 "handler": "reverse_proxy",
351 "upstreams": [{"dial": "%s"}],
352 "transport": {
353 "protocol": "http",
354 "proxy_protocol": "%s",
355 "versions": %s
356 }
357 }
358 ]
359 }
360 ]
361 }
362 }
363 }
364 }
365 }`, listenPort, backendAddr, ppVersion, string(versionsJSON))
366 }
367
368 // freePort returns a free local TCP port by binding briefly and releasing it.
369 func freePort(t *testing.T) int {
370 t.Helper()
371 ln, err := net.Listen("tcp", "127.0.0.1:0")
372 if err != nil {
373 t.Fatalf("freePort: %v", err)
374 }
375 port := ln.Addr().(*net.TCPAddr).Port
376 _ = ln.Close()
377 return port
378 }
379
380 // TestProxyProtocolV1WithH1 verifies that PROXY protocol v1 headers are sent
381 // correctly when the transport uses HTTP/1.1 to the upstream.
382 func TestProxyProtocolV1WithH1(t *testing.T) {
383 testProxyProtocolMatrix(t, "v1", []string{"1.1"}, 1)
384 }
385
386 // TestProxyProtocolV2WithH1 verifies that PROXY protocol v2 headers are sent
387 // correctly when the transport uses HTTP/1.1 to the upstream.
388 func TestProxyProtocolV2WithH1(t *testing.T) {
389 testProxyProtocolMatrix(t, "v2", []string{"1.1"}, 1)
390 }
391
392 // TestProxyProtocolV1WithH2C verifies that PROXY protocol v1 headers are sent
393 // correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream.
394 func TestProxyProtocolV1WithH2C(t *testing.T) {
395 testProxyProtocolMatrix(t, "v1", []string{"h2c"}, 1)
396 }
397
398 // TestProxyProtocolV2WithH2C verifies that PROXY protocol v2 headers are sent
399 // correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream.
400 // This is the primary regression test for github.com/caddyserver/caddy/issues/7529:
401 // before the fix, the h2 transport opened a new TCP connection per request
402 // (because req.URL.Host was mangled differently for each request due to the
403 // varying client port), which caused file-descriptor exhaustion under load.
404 func TestProxyProtocolV2WithH2C(t *testing.T) {
405 testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 1)
406 }
407
408 // TestProxyProtocolV2WithH2CMultipleRequests sends several sequential requests
409 // through the h2c + PROXY-protocol path and confirms that:
410 // 1. Every request receives a 200 response (no connection exhaustion).
411 // 2. The backend received at least one PROXY header (connection was reused).
412 //
413 // This is the core regression guard for issue #7529: without the fix, a new
414 // TCP connection was opened per request, quickly exhausting file descriptors.
415 func TestProxyProtocolV2WithH2CMultipleRequests(t *testing.T) {
416 testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 5)
417 }
418
419 // TestProxyProtocolV1WithH2 verifies that PROXY protocol v1 headers are sent
420 // correctly when the transport uses HTTP/2 over TLS (h2) to the upstream.
421 func TestProxyProtocolV1WithH2(t *testing.T) {
422 testTLSProxyProtocolMatrix(t, "v1", []string{"2"}, 1)
423 }
424
425 // TestProxyProtocolV2WithH2 verifies that PROXY protocol v2 headers are sent
426 // correctly when the transport uses HTTP/2 over TLS (h2) to the upstream.
427 func TestProxyProtocolV2WithH2(t *testing.T) {
428 testTLSProxyProtocolMatrix(t, "v2", []string{"2"}, 1)
429 }
430
431 // TestProxyProtocolServerAndProxy is an end-to-end matrix test that exercises
432 // all combinations of PROXY protocol version x transport version.
433 func TestProxyProtocolServerAndProxy(t *testing.T) {
434 plainTests := []struct {
435 name string
436 ppVersion string
437 transportVersions []string
438 numRequests int
439 }{
440 {"h1-v1", "v1", []string{"1.1"}, 3},
441 {"h1-v2", "v2", []string{"1.1"}, 3},
442 {"h2c-v1", "v1", []string{"h2c"}, 3},
443 {"h2c-v2", "v2", []string{"h2c"}, 3},
444 }
445 for _, tc := range plainTests {
446 t.Run(tc.name, func(t *testing.T) {
447 testProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests)
448 })
449 }
450
451 tlsTests := []struct {
452 name string
453 ppVersion string
454 transportVersions []string
455 numRequests int
456 }{
457 {"h2-v1", "v1", []string{"2"}, 3},
458 {"h2-v2", "v2", []string{"2"}, 3},
459 }
460 for _, tc := range tlsTests {
461 t.Run(tc.name, func(t *testing.T) {
462 testTLSProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests)
463 })
464 }
465 }
466
467 // testProxyProtocolMatrix is the shared implementation for the proxy protocol
468 // tests. It:
469 // 1. Starts a go-proxyproto-wrapped backend.
470 // 2. Configures Caddy as a reverse proxy with the given PROXY protocol
471 // version and transport versions.
472 // 3. Sends numRequests GET requests through Caddy and asserts 200 OK each time.
473 // 4. Asserts the backend recorded at least one PROXY header whose source host
474 // is 127.0.0.1 (the loopback address used by the test client).
475 func testProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) {
476 t.Helper()
477
478 backend := newProxyProtoBackend(t)
479 listenPort := freePort(t)
480
481 tester := caddytest.NewTester(t)
482 tester.WithDefaultOverrides(caddytest.Config{
483 AdminPort: 2999,
484 })
485 cfg := proxyProtoConfig(listenPort, backend.addr(), ppVersion, transportVersions)
486 tester.InitServer(cfg, "json")
487
488 // If the test is h2c-only (no "1.1" in versions), reconfigure the test
489 // client transport to use unencrypted HTTP/2 so we actually exercise the
490 // h2c code path through Caddy.
491 if slices.Contains(transportVersions, "h2c") && !slices.Contains(transportVersions, "1.1") {
492 tr, ok := tester.Client.Transport.(*http.Transport)
493 if ok {
494 tr.Protocols = new(http.Protocols)
495 tr.Protocols.SetHTTP1(false)
496 tr.Protocols.SetUnencryptedHTTP2(true)
497 }
498 }
499
500 proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort)
501
502 for i := 0; i < numRequests; i++ {
503 resp, err := tester.Client.Get(proxyURL)
504 if err != nil {
505 t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err)
506 }
507 resp.Body.Close()
508 if resp.StatusCode != http.StatusOK {
509 t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode)
510 }
511 }
512
513 // The backend must have seen at least one PROXY header. For h1, there is
514 // one per request; for h2c, requests share the same connection so only one
515 // header is written at connection establishment.
516 addrs := backend.recordedAddrs()
517 if len(addrs) == 0 {
518 t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)")
519 }
520
521 // Every PROXY-decoded source address must be the loopback address since
522 // the test client always connects from 127.0.0.1.
523 for i, addr := range addrs {
524 host, _, err := net.SplitHostPort(addr)
525 if err != nil {
526 t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err)
527 continue
528 }
529 if host != "127.0.0.1" {
530 t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host)
531 }
532 }
533 }
534
535 // TestProxyProtocolListenerWrapper verifies that Caddy's
536 // caddy.listeners.proxy_protocol listener wrapper can successfully parse
537 // incoming PROXY protocol headers.
538 //
539 // The test dials Caddy's listening port directly, injects a raw PROXY v2
540 // header spoofing source address 10.0.0.1:1234, then sends a normal
541 // HTTP/1.1 GET request. The Caddy server is configured to echo back the
542 // remote address ({http.request.remote.host}). The test asserts that the
543 // echoed address is the spoofed 10.0.0.1.
544 func TestProxyProtocolListenerWrapper(t *testing.T) {
545 tester := caddytest.NewTester(t)
546 tester.InitServer(`{
547 skip_install_trust
548 admin localhost:2999
549 http_port 9080
550 https_port 9443
551 grace_period 1ns
552 servers :9080 {
553 listener_wrappers {
554 proxy_protocol {
555 timeout 5s
556 allow 127.0.0.0/8
557 }
558 }
559 }
560 }
561 http://localhost:9080 {
562 respond "{http.request.remote.host}"
563 }`, "caddyfile")
564
565 // Dial the Caddy listener directly and inject a PROXY v2 header that
566 // claims the connection originates from 10.0.0.1:1234.
567 conn, err := net.Dial("tcp", "127.0.0.1:9080")
568 if err != nil {
569 t.Fatalf("dial: %v", err)
570 }
571 defer conn.Close()
572
573 spoofedSrc := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1234}
574 spoofedDst := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9080}
575 hdr := goproxy.HeaderProxyFromAddrs(2, spoofedSrc, spoofedDst)
576 if _, err := hdr.WriteTo(conn); err != nil {
577 t.Fatalf("write proxy header: %v", err)
578 }
579
580 // Write a minimal HTTP/1.1 GET request.
581 _, err = fmt.Fprintf(conn,
582 "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
583 if err != nil {
584 t.Fatalf("write HTTP request: %v", err)
585 }
586
587 // Read the raw response and look for the spoofed address in the body.
588 buf := make([]byte, 4096)
589 n, _ := conn.Read(buf)
590 raw := string(buf[:n])
591
592 if !strings.Contains(raw, "10.0.0.1") {
593 t.Errorf("expected spoofed address 10.0.0.1 in response body; full response:\n%s", raw)
594 }
595 }