Skip to content

Egress: per-task attribution for SOCKS5 raw-TCP audit events #360

Description

@naveen-kurra

Problem

SOCKS5 raw-TCP egress events (introduced in #337 / PR #355) carry host:port + decision + mode=socks5 but no task_id / correlation_id. HTTP CONNECT audits carry those fields (recovered from Proxy-Authorization per #338) — SOCKS5 doesn't, because SOCKS5v5 no-auth has no channel for per-request credentials.

For the enterprise audit story that #337 sells ("which task reached the database?"), an unattributed raw-TCP event is a real observability gap. The signal is currently "something inside the agent reached db.internal:5432 at T" — usable, but weaker than the HTTP path.

Design sketch

SOCKS5 supports authentication as part of the protocol (RFC 1929 username/password sub-negotiation). The pattern would mirror #338's HTTP approach:

  1. Change the injected env from socks5h://127.0.0.1:PORT to socks5h://<b64u(task_id)>:<b64u(correlation_id)>@127.0.0.1:PORT. Go's x/net/proxy.SOCKS5 accepts a Auth{User, Password} object; a proxy URL with userinfo drives that.
  2. Handshake path in socks5.go — currently offers only methodNoAuth (0x00). Extend to offer 0x02 (username/password) when the client requests it, fall back to no-auth otherwise. On 0x02, read the sub-negotiation payload per RFC 1929, base64url-decode both halves, stash on the connection.
  3. Thread through ValidateAndDial — currently p.ValidateAndDial(context.Background(), host, port). Change to validateAndDialWithIdentity(ctx, host, port, auditDomain, egressIdentity{...}) and pass the decoded task/correlation.
  4. Backward compat — no-auth stays the default when the client doesn't ask for auth (curl, one-off tools). Only Forge's own subprocess env-injection triggers the auth path.

Client support caveats

  • Go clients (x/net/proxy.SOCKS5) — native support, just needs the URL userinfo.
  • curl --socks5-hostname — passes user:pass when the URL carries it.
  • psql / redis-cli etc. via proxychains-ng — proxychains has its own [ProxyList] config for user/pass; the LD_PRELOAD path would need updating. Docs.
  • Language SDKs (PySocks, socks-proxy-agent) — support user/pass; docs example needs the auth form.

Scope / non-goals

  • Not doing GSSAPI (SOCKS5 method 0x01) — overkill for internal task attribution.
  • Not doing UDP (UDP ASSOCIATE) — separate issue when a UDP use case appears.
  • Rejecting a malformed sub-negotiation should fail closed (deny the CONNECT).

Acceptance

  • SkillCommandExecutor injects task/correlation into ALL_PROXY URL userinfo, matching the HTTP-side Egress: subprocess proxy audit events missing task_id / correlation_id #338 pattern.
  • SOCKS5 handshake accepts both 0x00 (no-auth) and 0x02 (RFC 1929), base64url-decodes both halves.
  • handleSOCKS5 threads the recovered identity into validateAndDialWithIdentity so OnAttempt events carry TaskID + CorrelationID for SOCKS5 dials.
  • Test coverage: authed CONNECT round-trip records identity; no-auth still works; malformed auth fails closed; mismatched user/pass length rejected per RFC.
  • Docs updated in docs/security/egress-control.md (raw-TCP task-attribution section + client support matrix updated).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestforge-coreAffects the forge-core library (runtime, security, types, llm, mcp, auth)securitySecurity vulnerability fixes

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions