mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
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.
111 lines
3.7 KiB
Go
111 lines
3.7 KiB
Go
package proxy
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"github.com/safedep/pmg/config"
|
|
"github.com/safedep/pmg/internal/proxyserver"
|
|
"github.com/safedep/pmg/internal/ui"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
daemonFlag bool
|
|
logFileFlag string
|
|
foregroundInternalFlag bool
|
|
)
|
|
|
|
func newStartCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "start",
|
|
Short: "Start the persistent PMG proxy server",
|
|
RunE: runStart,
|
|
}
|
|
|
|
// Bind --host/--port directly onto the config fields with the loaded config
|
|
// values as defaults, matching PMG's flag pattern (see config/cobra.go): a
|
|
// supplied flag overwrites the field, otherwise the config value stands.
|
|
// Precedence: flag > env > config file > default.
|
|
srv := &config.Get().Config.Proxy.Server
|
|
|
|
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 {
|
|
panic(err)
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func runStart(cmd *cobra.Command, _ []string) error {
|
|
cfg := config.Get()
|
|
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, transparent); err != nil {
|
|
ui.ErrorExit(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := proxyserver.Run(cmd.Context(), cfg, statePath, host, port); err != nil {
|
|
ui.ErrorExit(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Daemon log: the --log-file flag if set, else <cache-dir>/proxy.log. The
|
|
// caller owns this path, so ensure its parent directory exists here.
|
|
logPath := logFileFlag
|
|
if logPath == "" {
|
|
logPath = filepath.Join(cfg.CacheDir(), "proxy.log")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(logPath), 0o700); err != nil {
|
|
return fmt.Errorf("create daemon log dir: %w", err)
|
|
}
|
|
|
|
args := daemonArgs(cmd, statePath, host, port, transparent)
|
|
|
|
daemonCfg := proxyserver.ProxyDaemonConfig{
|
|
LogPath: logPath,
|
|
ReadyTimeout: proxyserver.DefaultDaemonReadyTimeout,
|
|
}
|
|
state, err := proxyserver.Daemonize(daemonCfg, statePath, exe, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, werr := fmt.Fprintf(os.Stdout, "PMG proxy daemon started on %s (pid %d)\n", state.Addr, state.PID)
|
|
return werr
|
|
}
|
|
|
|
// 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),
|
|
)
|
|
}
|