@marcel91200 / caddy Mirror · read-only / commits / a6acb390
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, 14:45 UTC

proxyproto: Generated test coverage (#7540)

Francis Lavoie committed Mar 3, 2026 at 17:08 UTC a6acb3902cb6453153db0738bd8210e093449ce1
1 file changed +595
caddytest/integration/proxyprotocol_test.go new
+595
@@ -0,0 +1,595 @@
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 +}