diff --git a/README.md b/README.md index cb2a5ae..eb14aa9 100644 --- a/README.md +++ b/README.md @@ -229,14 +229,18 @@ log_path = "/home/user/.hermes-node/audit.log" # One of: debug, info, warn, error log_level = "debug" -# Reconnect backoff (defaults shown) +# Proxy: optional HTTP(S) proxy for the WebSocket connection. +# When set, overrides HTTPS_PROXY / HTTP_PROXY env vars. +proxy_url = "http://proxy.corp.example:8080" + +# Reconnect backoff: fine-tune the exponential backoff for +# transient network drops. The defaults work for most setups. backoff_initial = "1s" # default; Go duration, e.g. "500ms", "5s" backoff_max = "60s" # default; maximum delay between retries backoff_factor = 2.0 # default; multiplier per retry -# Proxy: hermes-node respects HTTPS_PROXY / HTTP_PROXY / NO_PROXY -# env vars automatically. No config field needed — just set them -# in the shell before running `hermes-node run`. +# Proxy: set proxy_url above, or use HTTPS_PROXY / HTTP_PROXY / NO_PROXY +# env vars for env-level config. ``` ### `[server]` section @@ -333,7 +337,7 @@ Quick summary: - **Q: Can I reload config without restarting?** A: Send `SIGHUP` to the daemon process to reload `log_level`. Other changes require a restart. -- **Q: Does the node support HTTP proxies?** A: Yes. The WebSocket client respects the standard `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`, `http_proxy`, and `NO_PROXY` environment variables automatically. For Basic auth, include credentials in the URL: `http://user:password@proxy:port`. For NTLM/Kerberos proxies, use a local authenticating proxy bridge (e.g. `cntlm`). +- **Q: Does the node support HTTP proxies?** A: Yes. Set `proxy_url` in `config.toml` under `[node]`, or use the standard `HTTPS_PROXY`/`HTTP_PROXY` env vars. For Basic auth, include credentials in the URL: `http://user:password@proxy:port`. For NTLM/Kerberos proxies, use a local authenticating proxy bridge (e.g. `cntlm`). - **Q: What does `--version` show?** A: The version, Go version, commit SHA, and build date. Example: `hermes-node v0.1.0 go1.26.3 abc12345 2026-06-22`. diff --git a/cmd/hermes-node/main.go b/cmd/hermes-node/main.go index d6e9dbb..df986e9 100644 --- a/cmd/hermes-node/main.go +++ b/cmd/hermes-node/main.go @@ -32,6 +32,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "os/exec" "os/signal" @@ -699,6 +700,21 @@ func runValidate(args []string, configPath string, stdout, stderr io.Writer) int } // If no TLS settings are configured, that's fine — no check needed. + // 5. Proxy URL — validate the URL when set. + if cfg.Node.ProxyURL != "" { + proxyURL, err := url.Parse(cfg.Node.ProxyURL) + if err != nil { + fmt.Fprintf(stdout, " [FAIL] proxy_url: %v\n", err) + failed++ + } else if proxyURL.Scheme != "http" && proxyURL.Scheme != "https" { + fmt.Fprintf(stdout, " [FAIL] proxy_url: scheme %q must be http or https\n", proxyURL.Scheme) + failed++ + } else { + fmt.Fprintf(stdout, " [OK] proxy_url: %s\n", cfg.Node.ProxyURL) + passed++ + } + } + if failed == 0 { fmt.Fprintf(stdout, "\nhermes-node: config is valid (%d checks passed).\n", passed) return 0 @@ -809,6 +825,7 @@ func runRun(ctx context.Context, configPath string, stdout, stderr io.Writer) in Arch: runtime.GOARCH, Capabilities: []string{"exec", "read", "write"}, TLSConfig: tlsCfg, + ProxyURL: cfg.Node.ProxyURL, }) }, // Setup is invoked once per (re)connect. We build a fresh diff --git a/internal/config/config.go b/internal/config/config.go index d1ba672..26f420b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,6 +35,7 @@ type NodeConfig struct { AllowedPaths []string `toml:"allowed_paths"` LogPath string `toml:"log_path"` LogLevel string `toml:"log_level"` + ProxyURL string `toml:"proxy_url"` BackoffInitial string `toml:"backoff_initial"` BackoffMax string `toml:"backoff_max"` BackoffFactor float64 `toml:"backoff_factor"` diff --git a/internal/wire/client.go b/internal/wire/client.go index 22c8738..09312f7 100644 --- a/internal/wire/client.go +++ b/internal/wire/client.go @@ -10,6 +10,7 @@ import ( "crypto/tls" "errors" "fmt" + "net/http" "net/url" "time" @@ -97,6 +98,13 @@ type DialOptions struct { // handed over. A future maintainer should not construct a // per-call closure here. TLSConfig *tls.Config + + // ProxyURL is an optional HTTP(S) proxy URL for the WebSocket + // dialer. When set, it overrides the HTTP_PROXY / HTTPS_PROXY + // environment variables. Leave empty to use the default + // proxy-from-environment behaviour (ProxyFromEnvironment). + // Example: "http://proxy.corp.example:8080". + ProxyURL string } // withDefaults returns a copy of opts with zero-valued fields filled @@ -166,6 +174,13 @@ func Connect(ctx context.Context, opts DialOptions) (*Client, error) { if opts.TLSConfig != nil { dialer.TLSClientConfig = opts.TLSConfig } + if opts.ProxyURL != "" { + proxyURL, err := url.Parse(opts.ProxyURL) + if err != nil { + return nil, fmt.Errorf("wire: parse proxy_url: %w", err) + } + dialer.Proxy = http.ProxyURL(proxyURL) + } conn, _, err := dialer.DialContext(wsCtx, opts.ServerURL, nil) if err != nil { return nil, fmt.Errorf("wire: dial %s: %w", opts.ServerURL, err)