initial commit

This commit is contained in:
2026-08-27 12:00:08 -06:00
parent 46e778812a
commit 1dc9e36fc8
6 changed files with 778 additions and 0 deletions
+95
View File
@@ -1,2 +1,97 @@
# smallprox
**smallprox** is a lightweight, zero-dependency reverse proxy written in Go. It accepts incoming client connections over **TLS 1.2** (or configurable TLS versions) and forwards requests to backends running modern TLS protocols (such as TLS 1.3) or any SSL/TLS version.
## Key Features
- **TLS Protocol Bridging**: Solves the incompatibility where legacy clients (e.g. PowerShell scripts configured for TLS 1.2) need to access backends requiring TLS 1.3.
- **Zero Configuration HTTPS**: Generates an in-memory self-signed certificate on startup with localhost and IP Subject Alternative Names (SANs) if no cert/key files are provided.
- **Full Backend Trust**: Bypasses backend certificate checks (`InsecureSkipVerify: true`) to work with self-signed, untrusted, or internal certificates.
- **Virtual Host & SNI Support**: Automatically updates the `Host` header to match the destination backend (can be disabled with `-preserve-host`).
- **Zero External Dependencies**: Built entirely with Go standard library (`net/http`, `crypto/tls`, `crypto/x509`, `net/http/httputil`).
---
## Installation & Building
```powershell
# Clone and build
cd smallprox
go build -o smallprox.exe .
```
---
## Usage
```powershell
smallprox -backend <url> [options]
```
If no backend is provided, usage instructions are automatically printed.
### Command-line Options
| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `-backend` | `-b` | *(Required)* | Target backend URL (e.g. `https://tls13.example.com`) |
| `-listen` | `-l` | `:8443` | Address and port to bind |
| `-tls-min` | | `1.2` | Minimum incoming TLS version (`1.0`, `1.1`, `1.2`, `1.3`) |
| `-tls-max` | | `1.2` | Maximum incoming TLS version (`1.0`, `1.1`, `1.2`, `1.3`) |
| `-cert` | | | Path to custom TLS certificate PEM file (optional) |
| `-key` | | | Path to custom TLS private key PEM file (optional) |
| `-http` | | `false` | Listen in plain HTTP mode instead of HTTPS |
| `-preserve-host` | | `false` | Preserve incoming Host header instead of target host |
| `-version` | `-v` | | Print version and exit |
| `-help` | `-h` | | Show help message |
---
## Examples
### 1. Basic TLS 1.2 Proxy to TLS 1.3 Backend
```powershell
.\smallprox.exe -backend https://api.example.com
```
*Listens on `https://127.0.0.1:8443` with TLS 1.2 and proxies to `https://api.example.com`.*
### 2. Custom Port and TLS Range
```powershell
.\smallprox.exe -listen :9443 -backend https://api.example.com -tls-min 1.2 -tls-max 1.3
```
### 3. Using Custom Certificates
```powershell
.\smallprox.exe -backend https://api.example.com -cert server.crt -key server.key
```
### 4. Plain HTTP Ingress to HTTPS Backend
```powershell
.\smallprox.exe -http -listen :8080 -backend https://api.example.com
```
---
## PowerShell Client Example
To communicate through `smallprox` from a PowerShell session requiring TLS 1.2 and trusting self-signed certificates:
```powershell
# Enforce TLS 1.2 in PowerShell
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Trust local self-signed certificate
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
# Make request through proxy
$response = Invoke-RestMethod -Uri "https://localhost:8443/api/v1/resource" -Method Get
Write-Output $response
```
---
## Running Tests
```powershell
go test -v ./...
```
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"os"
"time"
)
// generateSelfSignedCert creates an in-memory self-signed TLS certificate.
func generateSelfSignedCert(extraHosts []string) (tls.Certificate, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to generate private key: %w", err)
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to generate serial number: %w", err)
}
dnsNames := []string{"localhost"}
ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
if hostname, err := os.Hostname(); err == nil && hostname != "" {
dnsNames = append(dnsNames, hostname)
}
for _, host := range extraHosts {
if host == "" {
continue
}
if ip := net.ParseIP(host); ip != nil {
ipAddresses = append(ipAddresses, ip)
} else {
dnsNames = append(dnsNames, host)
}
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"smallprox"},
CommonName: "smallprox-localhost",
},
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: dnsNames,
IPAddresses: ipAddresses,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to create certificate: %w", err)
}
cert := tls.Certificate{
Certificate: [][]byte{derBytes},
PrivateKey: priv,
}
return cert, nil
}
+3
View File
@@ -0,0 +1,3 @@
module code.stevenpolley.net/steven/smallprox
go 1.24
+247
View File
@@ -0,0 +1,247 @@
package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
var (
version = "1.0.0"
)
func parseTLSVersion(v string) (uint16, error) {
switch strings.TrimSpace(v) {
case "1.0", "tls1.0", "TLS1.0":
return tls.VersionTLS10, nil
case "1.1", "tls1.1", "TLS1.1":
return tls.VersionTLS11, nil
case "1.2", "tls1.2", "TLS1.2":
return tls.VersionTLS12, nil
case "1.3", "tls1.3", "TLS1.3":
return tls.VersionTLS13, nil
default:
return 0, fmt.Errorf("unknown TLS version %q (valid options: 1.0, 1.1, 1.2, 1.3)", v)
}
}
func printUsage() {
fmt.Fprintf(os.Stderr, `smallprox v%s - Miniature TLS 1.2 Reverse Proxy
Usage:
smallprox -backend <url> [options]
Options:
-backend, -b string
Target backend URL (required, e.g. https://tls13-service.example.com)
-listen, -l string
Address and port to bind to (default ":8443")
-tls-min string
Minimum incoming TLS version: 1.0, 1.1, 1.2, 1.3 (default "1.2")
-tls-max string
Maximum incoming TLS version: 1.0, 1.1, 1.2, 1.3 (default "1.2")
-cert string
Path to TLS certificate PEM file (optional; self-signed generated if omitted)
-key string
Path to TLS private key PEM file (optional; self-signed generated if omitted)
-http
Listen in plain HTTP mode instead of HTTPS (default false)
-preserve-host
Preserve incoming Host header instead of rewriting to backend host (default false)
-version, -v
Print version information and exit
-help, -h
Show this help message
Examples:
# Basic reverse proxy to a TLS 1.3 backend (PowerShell -> TLS 1.2 proxy:8443 -> TLS 1.3 backend)
smallprox -backend https://api.example.com
# Custom listen port and specific TLS range
smallprox -listen :9443 -backend https://api.example.com -tls-min 1.2 -tls-max 1.3
# Using custom certificates
smallprox -backend https://api.example.com -cert server.crt -key server.key
# PowerShell usage example:
# [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
# Invoke-RestMethod -Uri https://localhost:8443/api/endpoint
`, version)
}
func main() {
var (
backendFlag string
backendShort string
listenAddr string
listenShort string
certFile string
keyFile string
tlsMinStr string
tlsMaxStr string
plainHTTP bool
preserveHost bool
showVersion bool
showVersionShort bool
showHelp bool
showHelpShort bool
)
flag.StringVar(&backendFlag, "backend", "", "Target backend URL")
flag.StringVar(&backendShort, "b", "", "Target backend URL (short)")
flag.StringVar(&listenAddr, "listen", ":8443", "Listen address")
flag.StringVar(&listenShort, "l", ":8443", "Listen address (short)")
flag.StringVar(&certFile, "cert", "", "Path to TLS cert file")
flag.StringVar(&keyFile, "key", "", "Path to TLS key file")
flag.StringVar(&tlsMinStr, "tls-min", "1.2", "Minimum incoming TLS version")
flag.StringVar(&tlsMaxStr, "tls-max", "1.2", "Maximum incoming TLS version")
flag.BoolVar(&plainHTTP, "http", false, "Listen in plain HTTP mode")
flag.BoolVar(&preserveHost, "preserve-host", false, "Preserve incoming Host header")
flag.BoolVar(&showVersion, "version", false, "Print version")
flag.BoolVar(&showVersionShort, "v", false, "Print version (short)")
flag.BoolVar(&showHelp, "help", false, "Show help")
flag.BoolVar(&showHelpShort, "h", false, "Show help (short)")
flag.Usage = printUsage
flag.Parse()
if showHelp || showHelpShort {
printUsage()
os.Exit(0)
}
if showVersion || showVersionShort {
fmt.Printf("smallprox version %s\n", version)
os.Exit(0)
}
// Resolve flags with short versions
rawBackend := backendFlag
if rawBackend == "" {
rawBackend = backendShort
}
// Also check positional argument if no flag given
if rawBackend == "" && flag.NArg() > 0 {
rawBackend = flag.Arg(0)
}
if rawBackend == "" {
fmt.Fprintln(os.Stderr, "[ERROR] Backend URL is required.")
printUsage()
os.Exit(1)
}
targetURL, err := ParseTargetURL(rawBackend)
if err != nil {
log.Fatalf("[FATAL] %v", err)
}
// Determine listen address
bindAddr := listenAddr
if bindAddr == ":8443" && listenShort != ":8443" {
bindAddr = listenShort
}
// Parse TLS versions
minTLS, err := parseTLSVersion(tlsMinStr)
if err != nil {
log.Fatalf("[FATAL] Invalid -tls-min: %v", err)
}
maxTLS, err := parseTLSVersion(tlsMaxStr)
if err != nil {
log.Fatalf("[FATAL] Invalid -tls-max: %v", err)
}
if minTLS > maxTLS {
log.Fatalf("[FATAL] -tls-min (%s) cannot be greater than -tls-max (%s)", tlsMinStr, tlsMaxStr)
}
proxyHandler := NewProxyHandler(ProxyConfig{
TargetURL: targetURL,
PreserveHost: preserveHost,
})
server := &http.Server{
Addr: bindAddr,
Handler: proxyHandler,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
}
protocol := "HTTPS"
if plainHTTP {
protocol = "HTTP"
} else {
tlsConfig := &tls.Config{
MinVersion: minTLS,
MaxVersion: maxTLS,
}
if certFile != "" && keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatalf("[FATAL] Failed to load certificate/key: %v", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
log.Printf("[INFO] Loaded TLS certificate from %s and %s", certFile, keyFile)
} else {
hostPart, _, _ := net.SplitHostPort(bindAddr)
extraHosts := []string{}
if hostPart != "" {
extraHosts = append(extraHosts, hostPart)
}
cert, err := generateSelfSignedCert(extraHosts)
if err != nil {
log.Fatalf("[FATAL] Failed to generate self-signed certificate: %v", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
log.Printf("[INFO] Generated ephemeral self-signed TLS certificate")
}
server.TLSConfig = tlsConfig
}
// Channel to listen for shutdown signals
stopChan := make(chan os.Signal, 1)
signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)
go func() {
log.Printf("[INFO] smallprox v%s starting...", version)
log.Printf("[INFO] Listening on %s://%s (incoming TLS: %s - %s)", protocol, bindAddr, tlsVersionName(minTLS), tlsVersionName(maxTLS))
log.Printf("[INFO] Proxying to %s (backend TLS verification disabled)", targetURL.String())
var err error
if plainHTTP {
err = server.ListenAndServe()
} else {
// TLSConfig already contains the certificate
err = server.ListenAndServeTLS("", "")
}
if err != nil && err != http.ErrServerClosed {
log.Fatalf("[FATAL] Server listener error: %v", err)
}
}()
<-stopChan
log.Printf("[INFO] Shutting down smallprox gracefully...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("[ERROR] Server shutdown error: %v", err)
}
log.Printf("[INFO] smallprox stopped.")
}
+144
View File
@@ -0,0 +1,144 @@
package main
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
// ProxyConfig holds options for the reverse proxy.
type ProxyConfig struct {
TargetURL *url.URL
PreserveHost bool
Verbose bool
}
// loggingResponseWriter wraps http.ResponseWriter to capture the HTTP status code.
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// NewProxyHandler creates an http.Handler that reverse proxies requests to the target URL.
func NewProxyHandler(cfg ProxyConfig) http.Handler {
proxy := httputil.NewSingleHostReverseProxy(cfg.TargetURL)
// Transport configured with InsecureSkipVerify to trust all backend certificates
proxy.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS10,
MaxVersion: tls.VersionTLS13,
},
}
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
// Set Host header to target host unless preserve host is explicitly requested.
// This is vital for backend virtual hosting and SNI.
if !cfg.PreserveHost {
req.Host = cfg.TargetURL.Host
}
if clientProto := req.Header.Get("X-Forwarded-Proto"); clientProto == "" {
if req.TLS != nil {
req.Header.Set("X-Forwarded-Proto", "https")
} else {
req.Header.Set("X-Forwarded-Proto", "http")
}
}
}
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("[ERROR] %s %s -> Backend error: %v", r.Method, r.URL.RequestURI(), err)
http.Error(w, fmt.Sprintf("Bad Gateway (smallprox): %v", err), http.StatusBadGateway)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
proxy.ServeHTTP(lrw, r)
duration := time.Since(start)
tlsVer := "HTTP"
if r.TLS != nil {
tlsVer = tlsVersionName(r.TLS.Version)
}
log.Printf("[%s] %s | %s %s -> %d (%s)",
tlsVer,
r.RemoteAddr,
r.Method,
r.URL.RequestURI(),
lrw.statusCode,
duration.Round(time.Millisecond/10),
)
})
}
// ParseTargetURL cleans and parses the backend target URL string.
func ParseTargetURL(raw string) (*url.URL, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, fmt.Errorf("backend URL cannot be empty")
}
if !strings.Contains(raw, "://") {
raw = "https://" + raw
}
parsed, err := url.Parse(raw)
if err != nil {
return nil, fmt.Errorf("invalid backend URL %q: %w", raw, err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, fmt.Errorf("unsupported backend scheme %q (must be http or https)", parsed.Scheme)
}
if parsed.Host == "" {
return nil, fmt.Errorf("backend URL must have a host")
}
return parsed, nil
}
// tlsVersionName returns the human-readable string for a TLS version identifier.
func tlsVersionName(ver uint16) string {
switch ver {
case tls.VersionTLS10:
return "TLS 1.0"
case tls.VersionTLS11:
return "TLS 1.1"
case tls.VersionTLS12:
return "TLS 1.2"
case tls.VersionTLS13:
return "TLS 1.3"
default:
return fmt.Sprintf("TLS 0x%04x", ver)
}
}
+215
View File
@@ -0,0 +1,215 @@
package main
import (
"crypto/tls"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func TestParseTLSVersion(t *testing.T) {
tests := []struct {
input string
want uint16
wantErr bool
}{
{"1.0", tls.VersionTLS10, false},
{"TLS1.0", tls.VersionTLS10, false},
{"1.1", tls.VersionTLS11, false},
{"1.2", tls.VersionTLS12, false},
{"tls1.2", tls.VersionTLS12, false},
{"1.3", tls.VersionTLS13, false},
{"2.0", 0, true},
{"invalid", 0, true},
}
for _, tt := range tests {
got, err := parseTLSVersion(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseTLSVersion(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
continue
}
if got != tt.want {
t.Errorf("parseTLSVersion(%q) = %x, want %x", tt.input, got, tt.want)
}
}
}
func TestParseTargetURL(t *testing.T) {
tests := []struct {
input string
wantHost string
wantSch string
wantErr bool
}{
{"https://example.com", "example.com", "https", false},
{"http://localhost:9000", "localhost:9000", "http", false},
{"example.com:8443", "example.com:8443", "https", false},
{"", "", "", true},
{"ftp://example.com", "", "", true},
}
for _, tt := range tests {
u, err := ParseTargetURL(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseTargetURL(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
continue
}
if !tt.wantErr {
if u.Host != tt.wantHost || u.Scheme != tt.wantSch {
t.Errorf("ParseTargetURL(%q) = %s://%s, want %s://%s", tt.input, u.Scheme, u.Host, tt.wantSch, tt.wantHost)
}
}
}
}
func TestGenerateSelfSignedCert(t *testing.T) {
cert, err := generateSelfSignedCert([]string{"custom.local", "192.168.1.50"})
if err != nil {
t.Fatalf("generateSelfSignedCert() failed: %v", err)
}
if len(cert.Certificate) == 0 {
t.Fatal("generateSelfSignedCert() returned empty certificate chain")
}
if cert.PrivateKey == nil {
t.Fatal("generateSelfSignedCert() returned nil private key")
}
}
func TestProxy_TLS12_To_TLS13_Backend(t *testing.T) {
// 1. Setup backend server that ONLY accepts TLS 1.3
backendHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/test-endpoint" {
w.Header().Set("X-Backend-Received-Proto", r.Proto)
w.Header().Set("X-Backend-Received-Host", r.Host)
w.WriteHeader(http.StatusOK)
body, _ := io.ReadAll(r.Body)
w.Write([]byte("backend-response: " + string(body)))
return
}
http.NotFound(w, r)
})
backendServer := httptest.NewUnstartedServer(backendHandler)
backendServer.TLS = &tls.Config{
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
}
backendServer.StartTLS()
defer backendServer.Close()
backendURL, err := url.Parse(backendServer.URL)
if err != nil {
t.Fatalf("failed to parse backend URL: %v", err)
}
// 2. Setup smallprox handler pointing to the TLS 1.3 backend
proxyHandler := NewProxyHandler(ProxyConfig{
TargetURL: backendURL,
PreserveHost: false,
})
proxyCert, err := generateSelfSignedCert(nil)
if err != nil {
t.Fatalf("failed to generate proxy cert: %v", err)
}
proxyServer := httptest.NewUnstartedServer(proxyHandler)
// Proxy frontend only accepts TLS 1.2
proxyServer.TLS = &tls.Config{
Certificates: []tls.Certificate{proxyCert},
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
}
proxyServer.StartTLS()
defer proxyServer.Close()
// 3. Client configured strictly for TLS 1.2
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
},
},
}
// Make request through proxy
req, err := http.NewRequest(http.MethodPost, proxyServer.URL+"/test-endpoint", strings.NewReader("hello-from-tls12-client"))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("client request through proxy failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected status 200 OK, got %d", resp.StatusCode)
}
if resp.TLS == nil || resp.TLS.Version != tls.VersionTLS12 {
t.Errorf("expected client TLS version to be TLS 1.2 (0x0303), got %x", resp.TLS.Version)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read response body: %v", err)
}
expectedBody := "backend-response: hello-from-tls12-client"
if string(body) != expectedBody {
t.Errorf("expected body %q, got %q", expectedBody, string(body))
}
// Verify Host was rewritten to backend host
if receivedHost := resp.Header.Get("X-Backend-Received-Host"); receivedHost != backendURL.Host {
t.Errorf("expected backend to receive host %q, got %q", backendURL.Host, receivedHost)
}
}
func TestProxy_PreserveHost(t *testing.T) {
backendHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Received-Host", r.Host)
w.WriteHeader(http.StatusOK)
})
backendServer := httptest.NewServer(backendHandler)
defer backendServer.Close()
backendURL, err := url.Parse(backendServer.URL)
if err != nil {
t.Fatalf("failed to parse backend URL: %v", err)
}
proxyHandler := NewProxyHandler(ProxyConfig{
TargetURL: backendURL,
PreserveHost: true,
})
proxyServer := httptest.NewServer(proxyHandler)
defer proxyServer.Close()
req, err := http.NewRequest(http.MethodGet, proxyServer.URL+"/test", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Host = "custom-domain.net"
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if got := resp.Header.Get("X-Received-Host"); got != "custom-domain.net" {
t.Errorf("expected preserved host %q, got %q", "custom-domain.net", got)
}
}