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
+}