Skip to content

phase 6c+6d: xpc tun + xpc ghidra/ida lifecycle#3

Merged
nficano merged 4 commits into
mainfrom
phase-6/tun
May 9, 2026
Merged

phase 6c+6d: xpc tun + xpc ghidra/ida lifecycle#3
nficano merged 4 commits into
mainfrom
phase-6/tun

Conversation

@nficano

@nficano nficano commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary

Builds on PR #2. Two new capability layers, both driven by the same
session-open + tool.invoke flow.

Phase 6c — xpc tun -L localPort:vmHost:vmPort

First subcommand to exercise both directions of ARCP streaming on a
single job.

  • Agent (agent/agent.py):
    • New tun.connect tool opens a VM-side TCP socket and pumps bytes
      to the host via stream.chunk(delta_b64).
    • Connection._dispatch now routes client-sourced stream.chunk /
      stream.close envelopes to the matching job's tun_socket. Non-tun
      jobs drop silently.
  • Host (internal/cli/tun.go): reader goroutine + forwarder goroutine
    • write mutex; forwarder waits on a jobReady channel so its first
      chunk doesn't race ahead of job.accepted.
  • Real-VM verified (docs/sessions/phase-6c-tun.md): xpctl JSON ping
    round-tripped through xpc tun -L 19578:127.0.0.1:9578.

Phase 6d — xpc ghidra / xpc ida lifecycle

Thin wrappers built on Phase 6c.

  • xpc ghidra start [--binary] [--port] [--repo] /
    xpc ghidra stop -- detached-spawn ghidraSvr.bat on the VM via the
    os.dup2-to-NUL + DETACHED_PROCESS trick; PID at
    C:\xpc\ghidra.runlog.pid. Stop kills java.exe whose command line
    matches %ghidra%.
  • xpc ida start [--binary] [--port] / xpc ida stop -- same
    pattern; default targets win32_remote.exe on port 23946.
  • Tunnel deliberately decoupled: users run
    xpc tun -L <port>:127.0.0.1:<port> to expose the server.

Live verification of ghidra/ida waits until those tools are installed
on the VM; dry-runs verified locally.

Test plan

  • go test -race ./... green
  • golangci-lint run clean (0 issues)
  • Python agent suite: 42 passed, 2 skipped corpus indices
  • Real-VM xpc tun xpctl-ping round-trip
  • xpc ghidra start --dry-run and xpc ida start --dry-run

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • xpc tun: local TCP forwarding (-L) with bidirectional streaming and agent-side tunnel support
    • xpc ghidra / xpc ida: detached start/stop commands to manage remote servers
  • Reliability

    • Improved tunnel/connection synchronization to reduce races and increase stability
  • Documentation

    • Expanded Phase 6c docs and updated changelog/TASKS with verification notes and next steps

Review Change Stack

nficano and others added 3 commits May 9, 2026 12:42
First subcommand to exercise both directions of ARCP streaming on a single
job. Forwards local TCP through the agent: each accepted connection on
127.0.0.1:<localPort> spawns a fresh ARCP session, invokes tun.connect on
the agent, then bidirectionally pipes bytes via stream.chunk(delta_b64).

Agent additions (agent/agent.py):

* tool_tun_connect: opens a VM-side TCP socket to arguments.host:port,
  stores it on the running Job, pumps VM->host bytes via stream.chunk on
  a fresh upstream stream id. On EOF / cancel, closes and emits
  stream.close.

* Connection._dispatch now routes stream.chunk and stream.close envelopes
  from the client. They look up the matching Job by job_id and either
  sendall the base64-decoded delta into job.tun_socket or
  shutdown(SHUT_WR). Non-tun jobs drop these envelopes silently.

Host additions (internal/cli/tun.go):

* xpc tun -L localPort:vmHost:vmPort cobra command.
* Reader goroutine: decodes envelopes, routes upstream stream.chunk to
  local conn (decode delta_b64 -> Write), stream.close to local
  CloseWrite, terminal types to clean up.
* Forwarder goroutine: waits for job.accepted via a jobReady channel
  before pumping local.Read -> stream.chunk envelopes on a downstream
  stream id. On local EOF, sends stream.close. The jobReady gate fixes
  a race where the first chunk would otherwise go out with empty job_id
  and be silently dropped by the agent.
* Single writeMu mutex serialises all envelope writes so the forwarder
  and cancel sender don't interleave bytes on the TLS conn.

Real-VM verification (docs/sessions/phase-6c-tun.md):
  $ ./bin/xpc tun -L 19578:127.0.0.1:9578 &
  $ python3 -c "send xpctl ping over local TCP; receive pong"
  xpctl response: {'pong': True, 'uptime': 72791.625, ...}

Agent log:
  tun.connect [job=job_ryp...] -> 127.0.0.1:9578

This pattern is the foundation for xpc ghidra/ida server-tunnel commands
that the master prompt §10 schedules later.

Deferred: xpc tun -R (reverse), xpc daemon, xpc dbg/trace/ghidra/ida, snap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin lifecycle wrappers built on top of Phase 6c's xpc tun. Both commands
share the same pattern: detached-spawn the RE server on the VM, save its
PID to a runlog, and tell the user how to expose it via xpc tun -L. The
tunnel stays a separate command so users can pick any local port and re-
tunnel without restarting the server.

* internal/cli/ghidra.go:
  - xpc ghidra start [--binary] [--port] [--repo]
    Default --binary: C:\ghidra\support\ghidraSvr.bat
    Default --port:   13100
    Spawns detached via the same os.dup2-to-NUL + DETACHED_PROCESS trick
    used by xpc bootstrap's manage.py. PID saved to
    C:\xpc\ghidra.runlog.pid for later stop / introspection.
  - xpc ghidra stop
    WMIC where name='java.exe' and commandline like '%ghidra%'; taskkill
    each match. Won't touch unrelated java processes.
  - Shared helpers buildDetachedSpawnPy and killByCmdLineMatch are reused
    by xpc ida and ready for future server-lifecycle commands.

* internal/cli/ida.go:
  - xpc ida start [--binary] [--port]
    Default --binary: C:\IDA\dbgsrv\win32_remote.exe
    Default --port:   23946 (IDA convention)
  - xpc ida stop
    Matches both win32_remote.exe and dbgsrv.exe on the off-chance the
    user installs the older binary name.

Sample run (dry-run; live verification pending until Ghidra / IDA are
actually installed on the VM):
    $ xpc ghidra start --dry-run
    (dry-run) start C:\ghidra\support\ghidraSvr.bat with port 13100 ...
    $ xpc ida start --dry-run
    (dry-run) start C:\IDA\dbgsrv\win32_remote.exe --port 23946

Deferred: xpc dbg (long-running debugger sessions), xpc trace (procmon),
xpc snap (Proxmox), xpc daemon, xpc tun -R, TOFU SSH host-key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backfills the CHANGELOG entry that didn't make it into 1dbbb7e because the
file was externally re-touched between read and edit. Content matches what
was already captured in TASKS.md §6.14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 0279f4a7-0815-493d-9617-0cb9551f9414

📥 Commits

Reviewing files that changed from the base of the PR and between b64c5c7 and 3d58226.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml

📝 Walkthrough

Walkthrough

This PR implements Phase 6c ARCP bidirectional streaming for TCP tunneling and Phase 6d remote debugger lifecycle management. The agent exposes a tun.connect tool that streams VM-side TCP socket data through stream.chunk/stream.close envelopes. The client xpc tun -L command listens locally and forwards traffic bidirectionally via ARCP. New xpc ghidra and xpc ida commands manage remote analyzer processes through detached spawn and kill-by-command-line operations.

Changes

Bidirectional ARCP Tunnel and Debugger Lifecycle

Layer / File(s) Summary
Feature Overview & Metadata
CHANGELOG.md, TASKS.md, docs/sessions/phase-6c-tun.md
Changelog, task updates, and Phase 6c documentation added describing ARCP tunnel behavior, verification transcript, and Phase 6d lifecycle notes.
Agent-side Tunnel Tool & Stream Routing
agent/agent.py
Adds tool_tun_connect, registers it in TOOLS, imports base64, and extends Connection dispatch to handle stream.chunk and stream.close, routing by job_id into job tun_socket.
Client-side Tunnel Listener & ARCP Session
internal/cli/tun.go
Adds tun command with -L parsing, local listener, per-connection ARCP session, tun.connect invocation, concurrent reader/forwarder goroutines, job-ready synchronization, and serialized ARCP writes.
Remote Debugger Lifecycle
internal/cli/ghidra.go, internal/cli/ida.go
Adds ghidra and ida commands with start (detached spawn via generated Python, PID/log capture) and stop (wmic/taskkill by command-line match); introduces buildDetachedSpawnPy and killByCmdLineMatch helpers.
CLI Command Integration
internal/cli/root.go
Registers new tun, ghidra, and ida subcommands in the root CLI.
CI Workflow
.github/workflows/ci.yml
Updates Go test invocation to go test -race ./..., removes coverage profile output and documents coverage tooling notes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A tunnel now flows through the ARCP stream,
With chunks and closures, a bidirectional dream!
Ghidra and IDA wake, spawn, and stop with a click,
tun.connect hums while the bytes do the trick.
Rabbit hops, logs in paw, celebrates this neat pick.


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

Triggers `go: no such tool "covdata"` on Go 1.22 + actions/setup-go@v5
when test compilation runs for packages without test files (cmd/gen-corpus,
cmd/xpc-exec, cmd/xpc-roundtrip, internal/cli). Tests themselves pass and
report coverage on packages that do have tests; the cover profile output
just isn't uploaded anywhere. Dropping the flag is the cleanest fix.

Locally and on the previous CI runs this didn't repro because Go's covdata
tool is part of the cache that setup-go@v5 sometimes restores partially.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nficano nficano merged commit 51009a6 into main May 9, 2026
5 checks passed
@nficano nficano deleted the phase-6/tun branch May 9, 2026 16:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b64c5c7670

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cli/tun.go
Comment on lines +252 to +253
case <-time.After(5 * time.Second):
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove fixed 5s wait for job acceptance

The forwarder exits after a hard-coded 5-second jobReady wait, which can silently break new tunnels when the agent/session is slow to acknowledge job.accepted (e.g., loaded VM or transient network delay). In that case the reader goroutine may stay alive, but no local bytes are ever forwarded, so the connection appears hung with no actionable error. This should wait on context/connection lifecycle (or surface an explicit timeout error) instead of unconditionally returning at 5 seconds.

Useful? React with 👍 / 👎.

Comment thread internal/cli/tun.go
Comment on lines +283 to +285
closeEv.Payload = map[string]interface{}{"reason": "host_eof"}
_ = send(closeEv)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel tunnel job after local EOF to prevent leaked sessions

On local read termination, the code only sends stream.close and returns, but does not cancel the remote job. If the VM-side target keeps the socket open after receiving EOF (valid TCP half-close behavior), tun.connect can continue blocking on recv and the host-side handler can remain stuck waiting for terminal envelopes, leaking a long-lived session per connection. Sending a cancel (or otherwise forcing full remote teardown) on local close avoids this hang path.

Useful? React with 👍 / 👎.

nficano added a commit that referenced this pull request May 9, 2026
… tun -R stub (#4)

Closes the remaining deferred items from MASTER.md §10. Each command is
contained and useful in v0 without further user input.

* TOFU SSH host-key (internal/sshlife/ssh.go + tofu_test.go):
  Dial() now defaults to TOFUHostKey(~/.xpc/known_hosts) -- writes
  <host> <key-type> <base64-key> on first contact, byte-matches on
  subsequent contacts, refuses on key change with a clear MITM warning.
  4 unit tests cover the four edge cases.

* xpc trace start/stop/pull (internal/cli/trace.go):
  Sysinternals procmon.exe wrapper. start /accepteula /quiet /minimized
  /backingfile <out> [/runtime N] via the same DETACHED_PROCESS spawn
  trick used by xpc bootstrap. stop calls procmon.exe /Terminate via a
  python subprocess (no cmd.exe quirks). pull is an alias for xpc cp.

* xpc dbg run/analyze (internal/cli/dbg.go):
  One-shot cdb wrappers. run <target> [--command] auto-detects .dmp
  files (uses -z) and appends ;q so cdb exits cleanly. analyze is the
  shorthand for !analyze -v against a minidump.

* xpc snap list/create/restore/delete (internal/cli/snap.go):
  Proxmox PVE HTTP API client at https://<host>:8006/api2/json/ with
  PVEAPIToken auth. Profile fields proxmox_host / proxmox_user are
  honored as defaults; the secret is expected via --proxmox-token or
  $XPC_PROXMOX_TOKEN. --proxmox-{host,user,token,node,vmid,insecure}
  flags layer on top.

* xpc daemon start/stop/status/exec (internal/cli/daemon.go):
  Long-lived host-side process holding warm TLS+session connections per
  profile. IPC over ~/.xpc/run/daemon.sock with one-line JSON requests
  and stdout_b64/stderr_b64 chunked responses. Smoke verified end-to-
  end: 'xpc daemon exec ver' through the warm session prints
  'Microsoft Windows XP [Version 5.1.2600]'. The CLI doesn't auto-route
  through it yet; opt-in for now.

* xpc tun -R: stub. Returns a clear 'not yet implemented' error
  pointing at TASKS.md. Real reverse forwarding needs an agent->host
  tool.invoke primitive; tracked as deferred.

CI fix: drop -coverprofile from go test (covdata tool missing on the
setup-go@v5 runners) -- already merged via PR #3 commit 3d58226.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant