feat(proxy): accept transparent redirected connections

The proxy only accepted explicit clients, which announce their
destination with a CONNECT request. A client whose connection is
redirected at the kernel level speaks TLS immediately instead, so
http.Server tries to parse a TLS record as an HTTP request line and
drops the connection.

Demultiplex on the first byte of an accepted connection. A TLS
handshake record (0x16) cannot begin an HTTP method, so it separates
the two cleanly. Redirected connections have their destination
recovered from the ClientHello SNI and are served by synthesising the
CONNECT the client never sent, which keeps the MITM decision, cert
generation and interceptor chain on the existing code path.

Redirected connections deliberately bypass http.Server. It issues a
background read while a handler runs, which consumes the first byte of
the replayed ClientHello and corrupts the handshake. The CONNECT
response is also suppressed, since a client mid handshake expects a
ServerHello and would read those bytes as a malformed TLS record.

Off by default. Enabled with `pmg proxy start --transparent` or
proxy.server.transparent, and only useful alongside a redirect
mechanism such as an eBPF connect rewrite.
This commit is contained in:
Sahilb315
2026-07-27 12:31:24 +05:30
parent b8e12c27ae
commit 82ddbc70af
7 changed files with 540 additions and 5 deletions
+11 -4
View File
@@ -34,6 +34,8 @@ func newStartCommand() *cobra.Command {
cmd.Flags().BoolVarP(&daemonFlag, "daemon", "D", false, "Run the proxy as a detached background process")
cmd.Flags().StringVar(&srv.ListenHost, "host", srv.ListenHost, "Host to bind")
cmd.Flags().IntVar(&srv.ListenPort, "port", srv.ListenPort, "Port to bind (0 = a random free port)")
cmd.Flags().BoolVar(&srv.Transparent, "transparent", srv.Transparent,
"Also accept connections redirected to the proxy, recovering the destination from the TLS SNI")
cmd.Flags().StringVar(&logFileFlag, "log-file", "", "File for the daemon's output (default: <cache-dir>/proxy.log)")
cmd.Flags().BoolVar(&foregroundInternalFlag, "foreground-internal", false, "Internal: run the foreground server (used by --daemon)")
if err := cmd.Flags().MarkHidden("foreground-internal"); err != nil {
@@ -47,9 +49,10 @@ func runStart(cmd *cobra.Command, _ []string) error {
statePath := proxyserver.ResolveStatePath(stateFlag, cfg.CacheDir())
host := cfg.Config.Proxy.Server.ListenHost
port := cfg.Config.Proxy.Server.ListenPort
transparent := cfg.Config.Proxy.Server.Transparent
if daemonFlag && !foregroundInternalFlag {
if err := startDaemon(cmd, cfg, statePath, host, port); err != nil {
if err := startDaemon(cmd, cfg, statePath, host, port, transparent); err != nil {
ui.ErrorExit(err)
}
return nil
@@ -61,7 +64,7 @@ func runStart(cmd *cobra.Command, _ []string) error {
return nil
}
func startDaemon(cmd *cobra.Command, cfg *config.RuntimeConfig, statePath, host string, port int) error {
func startDaemon(cmd *cobra.Command, cfg *config.RuntimeConfig, statePath, host string, port int, transparent bool) error {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("resolve executable: %w", err)
@@ -77,7 +80,7 @@ func startDaemon(cmd *cobra.Command, cfg *config.RuntimeConfig, statePath, host
return fmt.Errorf("create daemon log dir: %w", err)
}
args := daemonArgs(cmd, statePath, host, port)
args := daemonArgs(cmd, statePath, host, port, transparent)
daemonCfg := proxyserver.ProxyDaemonConfig{
LogPath: logPath,
@@ -92,12 +95,16 @@ func startDaemon(cmd *cobra.Command, cfg *config.RuntimeConfig, statePath, host
return werr
}
func daemonArgs(cmd *cobra.Command, statePath, host string, port int) []string {
// daemonArgs passes the resolved values explicitly rather than relying on the
// child to re-derive them, since a value supplied by flag is not visible to the
// child's own config load.
func daemonArgs(cmd *cobra.Command, statePath, host string, port int, transparent bool) []string {
args := append([]string{}, config.ChangedConfigFlagArgs(cmd)...)
return append(args,
"proxy", "start", "--foreground-internal",
"--state", statePath,
"--host", host,
"--port", strconv.Itoa(port),
"--transparent="+strconv.FormatBool(transparent),
)
}
+24 -1
View File
@@ -17,7 +17,7 @@ func TestDaemonArgsPrependsChangedConfigFlags(t *testing.T) {
start := &cobra.Command{
Use: "start",
Run: func(cmd *cobra.Command, _ []string) {
got = daemonArgs(cmd, "/tmp/proxy-state.json", "127.0.0.1", 9000)
got = daemonArgs(cmd, "/tmp/proxy-state.json", "127.0.0.1", 9000, false)
},
}
proxyCmd := &cobra.Command{Use: "proxy"}
@@ -37,5 +37,28 @@ func TestDaemonArgsPrependsChangedConfigFlags(t *testing.T) {
"--state", "/tmp/proxy-state.json",
"--host", "127.0.0.1",
"--port", "9000",
"--transparent=false",
}, got)
}
// The daemon re-execs itself, so a transparent value supplied by flag must be
// carried across explicitly. The child's own config load cannot see it.
func TestDaemonArgsCarriesTransparentToChild(t *testing.T) {
root := &cobra.Command{Use: "pmg"}
config.ApplyCobraFlags(root)
var got []string
start := &cobra.Command{
Use: "start",
Run: func(cmd *cobra.Command, _ []string) {
got = daemonArgs(cmd, "/tmp/proxy-state.json", "127.0.0.1", 9000, true)
},
}
proxyCmd := &cobra.Command{Use: "proxy"}
proxyCmd.AddCommand(start)
root.AddCommand(proxyCmd)
root.SetArgs([]string{"proxy", "start"})
require.NoError(t, root.Execute())
assert.Contains(t, got, "--transparent=true")
}