Skip to content

Latest commit

 

History

History
331 lines (258 loc) · 11.4 KB

persistent-forwarding-design.md

File metadata and controls

331 lines (258 loc) · 11.4 KB

Server-backed persistent forwarding design

This document records the M56 design for future server-owned forwarding. It is a documentation-only milestone: the existing adb-go forward command remains a foreground, process-scoped local TCP forward, and adb-gos still does not own ADB devices, transports, or forwards until later implementation milestones add that behavior.

Background

Today adb-go forwarding is intentionally direct and foreground-only. The CLI or library process binds the host listener, connects directly to one selected ADB target, and opens a fresh device tcp:PORT service for each accepted host connection. When that process exits, the listener and active bridges disappear.

Persistent forwarding needs a different owner. The only adb-go process designed to outlive a CLI invocation is adb-gos, so background forwards should be represented as server state and controlled through explicit server protocol commands. This keeps adb-go honest about lifetime: a forward is persistent only when the server owns the listener and the target connection policy.

Goals

  • Add a concrete design for server-owned forwards that can keep listening after adb-go forward --background ... exits.
  • Preserve the current foreground adb-go forward LOCAL REMOTE behavior.
  • Define how forwards are represented, listed, removed, and diagnosed.
  • Define server protocol extensions without implementing them yet.
  • Keep target selection explicit; do not introduce broad adb-server-compatible discovery or global transport ownership in this design.

Non-goals

  • No implementation in this milestone.
  • No compatibility promise for the official ADB server smart-socket protocol.
  • No reverse forwarding.
  • No host Unix sockets, JDWP, vsock, Android local socket namespaces, or raw advanced service targets in the first persistent-forwarding slice.
  • No server-managed authentication key discovery or persistence. Any future authenticated persistent forward must use explicit credentials or a separately designed auth store.

Forward representation

The server should store a small in-memory table of forwarding registrations. A registration describes desired listener state, not just one active connection.

Suggested logical fields:

{
  "id": "fwd_01J...",
  "state": "listening",
  "local": {"network":"tcp", "address":"127.0.0.1:9000"},
  "remote": {"service":"tcp:9000"},
  "target": {"transport":"tcp", "address":"127.0.0.1:5555"},
  "norebind": false,
  "createdAt": "2026-07-26T12:00:00Z",
  "activeConnections": 0,
  "lastError": ""
}

Definitions:

  • id is an adb-go-generated stable identifier for later removal. It should be opaque to callers.
  • local is the host-side listener. The first implementation should support TCP only and should bind loopback by default for tcp:PORT CLI syntax.
  • remote is the ADB service opened on the device for each accepted host connection. The first implementation should support only tcp:PORT.
  • target is the explicit ADB target selection needed to reconnect or open streams. Initially this can be TCP-only; USB persistence should be designed as a separate slice because device paths and permissions can change across reconnects.
  • state should distinguish at least listening, degraded, and stopped in server responses. Removed forwards do not need to stay in the table.
  • lastError is diagnostic text for the most recent listener, target, or bridge setup failure. It is informational and not a stable API field.

The table should be in-memory for the first server-backed implementation. Systemd can keep adb-gos running, but restarting the server intentionally drops all registered forwards unless a later milestone designs durable state.

CLI command shape

The existing foreground command remains unchanged:

adb-go forward --addr 127.0.0.1:5555 tcp:9000 tcp:9000

Persistent forwarding should be opt-in with an explicit background flag:

adb-go forward --background --addr 127.0.0.1:5555 tcp:9000 tcp:9000
adb-go forward --background --norebind --addr 127.0.0.1:5555 tcp:9000 tcp:9000

Behavior:

  • Without --background, the command continues to run in the foreground exactly as it does today.
  • With --background, the CLI sends a server request and exits after the server has bound the listener or returned a setup error.
  • --norebind rejects creation when another server-owned forward already uses the same local endpoint. Without --norebind, adb-go may replace an existing server-owned forward for that local endpoint, but it must never steal an unrelated listener owned by another process.
  • If the local port is tcp:0, the server binds an ephemeral port and returns the actual address so the CLI can print it.

Listing and removal should be server-backed commands, not foreground-client commands:

adb-go forward --list
adb-go forward --remove tcp:9000
adb-go forward --remove-id fwd_01J...
adb-go forward --remove-all

Listing output should be human-readable by default and include enough data for support requests:

ID          LOCAL             REMOTE    TARGET              STATE      ACTIVE
fwd_01JABC 127.0.0.1:9000    tcp:9000  tcp:127.0.0.1:5555  listening  2

Removal semantics:

  • --remove LOCAL removes the server-owned registration for the local endpoint.
  • --remove-id ID removes exactly one registration by server ID and is safest for scripts.
  • --remove-all closes all server-owned forwarding listeners.
  • Removing a forward closes its listener and all active bridged connections.
  • Removing a missing forward should return a clear not-found error.

The command should make server dependence visible. If adb-gos is unreachable, background/list/remove operations should fail with a hint to run adb-go server doctor or start/install the server service.

Server protocol additions

The server control protocol can remain newline-delimited JSON with version: 1 if these commands are added as optional request/response shapes. Older servers will return unknown_command, which the CLI can turn into a clear "server is too old" message.

Suggested commands:

  • forward_create
  • forward_list
  • forward_remove
  • forward_remove_all

forward_create

Request:

{
  "version": 1,
  "id": "c1",
  "command": "forward_create",
  "params": {
    "local": {"network":"tcp", "address":"127.0.0.1:9000"},
    "remote": {"service":"tcp:9000"},
    "target": {"transport":"tcp", "address":"127.0.0.1:5555"},
    "norebind": false
  }
}

Successful response:

{
  "version": 1,
  "id": "c1",
  "ok": true,
  "result": {
    "forward": {
      "id": "fwd_01JABC",
      "state": "listening",
      "local": {"network":"tcp", "address":"127.0.0.1:9000"},
      "remote": {"service":"tcp:9000"},
      "target": {"transport":"tcp", "address":"127.0.0.1:5555"},
      "activeConnections": 0
    }
  }
}

Creation should bind the local listener before returning success. A bind failure or target-selection validation failure returns ok:false and a stable error code such as address_in_use, unsupported_endpoint, bad_target, or rebind_disallowed.

forward_list

Request:

{"version":1,"id":"l1","command":"forward_list"}

Successful response includes all current server-owned forwards:

{
  "version": 1,
  "id": "l1",
  "ok": true,
  "result": {"forwards": []}
}

forward_remove

Request by ID:

{"version":1,"id":"r1","command":"forward_remove","params":{"id":"fwd_01JABC"}}

Request by local endpoint:

{
  "version": 1,
  "id": "r2",
  "command": "forward_remove",
  "params": {"local":{"network":"tcp", "address":"127.0.0.1:9000"}}
}

The server should require exactly one selector. Success means the listener has been closed and active bridges have been asked to close.

forward_remove_all

Request:

{"version":1,"id":"ra1","command":"forward_remove_all"}

The response should include a count of removed forwards.

Runtime flow

For each server-owned registration:

  1. The server binds the local TCP listener.
  2. For each accepted host connection, the server dials or reuses the selected ADB target according to the implementation slice.
  3. The server opens the configured remote service, such as tcp:9000.
  4. The server copies bytes in both directions until one side closes or errors.
  5. Connection-level failures close only that host connection. Listener-level failures move the registration to degraded or remove it, depending on the failure.

The first implementation should prefer simple correctness over pooling. Opening one ADB connection or stream per accepted host connection is easier to reason about and avoids server-wide device lifecycle ownership. Later milestones can add connection reuse if tests prove it is safe.

Lifecycle semantics

Server shutdown and restart

On graceful shutdown, adb-gos closes every forwarding listener and active bridge before exiting. Because the initial table is in-memory, a server restart starts with no forwards. This must be documented in CLI help and --list output should not imply durable state.

Systemd restart

A systemd user-service restart has the same effect as server restart: all forwards are dropped. Users who need forwards recreated after login or restart should use their own service units or scripts until adb-go explicitly designs persistent on-disk configuration.

Target disconnects

A failed per-connection target dial or ADB stream open should reject that host connection and update lastError, but the listener may remain active so later connections can succeed after the device returns. If repeated failures occur, the server may report state: degraded while keeping the listener bound.

Port conflicts

If another process owns the requested local address, creation fails with address_in_use. If another server-owned forward owns it:

  • norebind: true fails with rebind_disallowed.
  • norebind: false may replace the old server-owned registration after closing its listener and active bridges.

The server must not remove or alter listeners it does not own.

Security and supportability

  • Bind loopback for shorthand CLI endpoints. Users must opt in explicitly to wider bind addresses if a later CLI syntax supports them.
  • Treat remote services and target addresses as caller-controlled input; do not add allowlists beyond the endpoint families supported by the specific milestone.
  • Do not log forwarded payload bytes.
  • Include forwarding counts in server diagnostics, but keep detailed mappings and last setup errors in forward --list so server status remains concise.
  • Keep errors actionable and stable enough for CLI handling: unsupported endpoint, server too old, server unavailable, address in use, rebind disallowed, forward not found, and target connection failed.

Suggested implementation slices

  1. Add server protocol request/response types and tests for forwarding commands without opening listeners.
  2. Implement TCP listener ownership in adb-gos with create/list/remove/remove all over loopback TCP endpoints.
  3. Bridge accepted connections to direct TCP ADB targets and tcp:PORT remote services.
  4. Add CLI forward --background, --list, --remove, --remove-id, and --remove-all wired to the server protocol.
  5. Surface concise forwarding diagnostics in server status and server doctor while keeping the detailed table in forward --list.
  6. Revisit USB target persistence, broader endpoint families, and optional durable forward restoration as separate designs.