FEAT: accept TLS connections on the proxy listener - #27
Merged
Conversation
A client talks to the proxy in the clear by default, which puts the Proxy-Authorization credentials and the host name of every CONNECT request on the local network. WithListenerTLS makes the HTTP frontend accept TLS instead, so the client speaks the proxy protocol inside a TLS connection: what a browser calls an HTTPS proxy. Most of this already worked, since Serve takes the listener from its caller and goproxy hijacks a tls.Conn as happily as any other. What was missing was a way to ask for it without knowing that, a way to configure it from a file, and somewhere for the certificate to live. The option copies the configuration it is given and applies TLS 1.2 as a floor when none is set, so a caller cannot accidentally offer 1.0. Serve and ListenAndServe wrap the listener themselves. The command grows tls_cert_file and tls_key_file, which are needed together or not at all, and holds the pair behind GetCertificate so that a renewal is picked up on USR2 without restarting: the listener is already up, the connections already established keep the certificate they started with, and a pair that cannot be read leaves the one in force alone rather than taking the listener down. This decrypts nothing. What a client sends through CONNECT stays opaque; it is the hop to the proxy that is protected, not the traffic inside it. The SOCKS frontend is untouched, since SOCKS5 has no TLS convention, and so is the health endpoint.
There was a problem hiding this comment.
Pull request overview
Adds first-class support for running the HTTP proxy listener over TLS (“HTTPS proxy”), including library options, CLI configuration, certificate reload on USR2, and documentation/tests to validate the behavior.
Changes:
- Introduces
WithListenerTLSand wraps the proxy listener in TLS when configured (TLS 1.2 default floor, config cloning). - Extends the
microproxycommand/config withtls_cert_file+tls_key_fileand implements certificate hot-reload viaGetCertificate. - Adds end-to-end TLS proxy tests plus certificate reload and config validation tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
tls_test.go |
New integration tests for TLS proxy listener behavior (plain + CONNECT + auth + downgrade refusal + config copy). |
server.go |
Wraps the provided listener with tls.NewListener when listenerTLS is configured; improves startup logging. |
options.go |
Adds WithListenerTLS option (clone config + enforce TLS 1.2 floor when unset) and clarifies TLS client/server distinction in docs. |
README.md |
Documents HTTPS-proxy mode, config keys, and runtime reload behavior; includes Go usage example. |
cmd/microproxy/main.go |
Wires TLS listener configuration into the CLI and reloads certs on USR2 via a reloader. |
cmd/microproxy/config.go |
Adds tls_cert_file / tls_key_file settings and validates they’re provided together. |
cmd/microproxy/certs.go |
Implements a certificateReloader using tls.Config.GetCertificate with safe concurrent swapping. |
cmd/microproxy/certs_test.go |
Tests certificate hot-reload and config validation for partial TLS configuration. |
Suppressed comments (1)
tls_test.go:113
- After removing
InsecureSkipVerify, the test needs to explicitly trust thehttptest.NewTLSServercertificate as well (the client will validate the target TLS cert after CONNECT). Add the target server cert to the sameRootCAspool used for both the proxy TLS hop and the tunnelled target.
certificate, pool := selfSigned(t, "127.0.0.1")
_, client := newTLSProxy(t,
Config{AllowedConnectPorts: []int{portOf(t, secure.URL)}},
&tls.Config{Certificates: []tls.Certificate{certificate}}, //nolint:gosec // MinVersion is applied by WithListenerTLS
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The client these tests proxy through set InsecureSkipVerify alongside RootCAs, which switches verification off entirely and leaves the pool doing nothing. The comment claiming the certificate was pinned was wrong, and the tests would have passed against any certificate at all: they showed that bytes flowed, not that TLS was done correctly. Verification is on now. It was off because one tls.Config covers both hops and the tunnelled target is self-signed too, so the fix is to trust the target's issuer as well rather than to trust nothing: httptest exposes it through Server.Certificate. A test is added for the property that was missing, where the client is given a pool that trusts a different issuer and has to refuse the connection with an unknown-authority error. With InsecureSkipVerify put back it fails, so it pins what it claims to. Reported by Copilot on #27.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A client talks to the proxy in the clear by default, which puts the Proxy-Authorization credentials and the host name of every CONNECT request on the local network. WithListenerTLS makes the HTTP frontend accept TLS instead, so the client speaks the proxy protocol inside a TLS connection: what a browser calls an HTTPS proxy.
Most of this already worked, since Serve takes the listener from its caller and goproxy hijacks a tls.Conn as happily as any other. What was missing was a way to ask for it without knowing that, a way to configure it from a file, and somewhere for the certificate to live.
The option copies the configuration it is given and applies TLS 1.2 as a floor when none is set, so a caller cannot accidentally offer 1.0. Serve and ListenAndServe wrap the listener themselves.
The command grows tls_cert_file and tls_key_file, which are needed together or not at all, and holds the pair behind GetCertificate so that a renewal is picked up on USR2 without restarting: the listener is already up, the connections already established keep the certificate they started with, and a pair that cannot be read leaves the one in force alone rather than taking the listener down.
This decrypts nothing. What a client sends through CONNECT stays opaque; it is the hop to the proxy that is protected, not the traffic inside it. The SOCKS frontend is untouched, since SOCKS5 has no TLS convention, and so is the health endpoint.