opstunnel 1.0.0 Remote Ops Workspace - a plain TCP port forwarder with per-connection accounting =============================================================================== WHAT IT IS opstunnel listens on a local address and proxies every byte to a target host:port, in both directions, concurrently. A team can then reach a service through one agreed hop instead of a dozen ad-hoc tunnels - and every connection that passes through leaves an auditable record: who connected, when, for how long, how many bytes moved in each direction, and why the connection ended. That ledger is the point. A forwarder that moves bytes is easy; a forwarder you can hand to an auditor afterwards is the useful one. READ THIS FIRST - NO ENCRYPTION, NO AUTHENTICATION -------------------------------------------------- opstunnel forwards PLAIN TCP. It does not encrypt anything. It does not authenticate anybody. There is no password, no key, no certificate, no handshake of any kind. * Anyone who can reach the listening address can use the tunnel, unless you restrict them with --allow, and --allow is an IP address filter only - IP addresses can be spoofed and NAT can put strangers behind a trusted one. * Everything that crosses the tunnel crosses it in whatever form the client sent it. If the client speaks cleartext, the bytes are cleartext on the wire, on both hops. * The connection log records addresses, byte counts and timings. It does NOT record payloads - but it is still sensitive metadata; protect the file. Therefore: use opstunnel ONLY on a network you already trust, or INSIDE an existing secure channel (an SSH tunnel, a WireGuard/VPN link, a service mesh, a private VPC subnet). Do not expose the listener to the public internet. Do not use it as a substitute for TLS. If the traffic needs confidentiality, the confidentiality has to come from the protocol inside the tunnel or from the transport around it - opstunnel provides neither. INSTALL go build -o opstunnel . # Go 1.24+, standard library only Prebuilt binaries are in dist/: dist/opstunnel-linux-amd64 dist/opstunnel-darwin-amd64 dist/opstunnel-darwin-arm64 dist/opstunnel-windows-amd64.exe COMMANDS opstunnel forward --listen --target [flags] opstunnel status --log [--json] opstunnel check --target [--timeout 3s] opstunnel help | --help | -h opstunnel version | --version | -v Every command accepts -h / --help / help and exits 0. A bad invocation prints the error and the usage to stderr and exits 1. forward - proxy and account --------------------------- opstunnel forward --listen 127.0.0.1:15432 --target db.internal:5432 \ --allow 10.0.0.0/8 --max-conns 50 --idle-timeout 90s \ --log conns.jsonl --listen Local address to accept on, host:port (required). Use 127.0.0.1:PORT to keep it on this machine. --target Where connections are forwarded (required) --allow Only accept clients inside this CIDR. Repeatable, and comma-separated lists are accepted. Default: any client --max-conns Refuse connections beyond n simultaneous (0 = unlimited) --idle-timeout Close a connection that has moved no bytes in EITHER direction for this long (default 60s, 0 = never) --dial-timeout Timeout when dialling the target (default 5s) --grace After SIGINT/SIGTERM: how long in-flight connections may keep running before they are closed (default 5s) --log Append one JSON record per connection (created if absent) --json Emit JSON events on stdout instead of human text Behaviour: * Both directions are copied concurrently, so a slow reader in one direction never stalls the other. * Half-close is preserved. A client that closes only its write side (the common "request finished, now send me the response" pattern) still receives everything the target sends afterwards. The connection ends when BOTH directions are done. * A refused connection - outside --allow, over --max-conns, or a target that cannot be dialled - is closed immediately and still written to the log with the reason, so refusals are auditable too. * SIGINT/SIGTERM stops accepting, lets in-flight connections drain for --grace, closes anything still open (reason "shutdown"), and releases the port. * Every log line is fsynced as it is written, so a signal or a crash cannot leave a half-written record. LOG FORMAT (one JSON object per line, append-only) {"id":7,"client":"10.2.0.9:51544","target":"db.internal:5432", "start":"2026-08-11T06:44:34.091028907Z","end":"2026-08-11T06:44:34.111976172Z", "duration_seconds":0.020947247, "bytes_client_to_target":4194304,"bytes_target_to_client":4194304, "reason":"client_closed"} id per-run sequence number client remote address of the client target address it was forwarded to start / end RFC3339 timestamps, UTC duration_seconds end - start bytes_client_to_target bytes actually delivered upstream bytes_target_to_client bytes actually delivered back reason why the connection ended (below) detail optional extra text (error string, denial detail) reason values: client_closed the client closed its side first target_closed the target closed its side first idle_timeout no bytes in either direction for --idle-timeout shutdown still open when the post-signal grace period expired denied_not_allowed client address matched no --allow CIDR denied_max_conns --max-conns was already reached target_unreachable the target could not be dialled read_error the transfer failed while reading write_error the transfer failed while writing status - what happened ---------------------- opstunnel status --log conns.jsonl opstunnel status --log conns.jsonl --json Reports the connection count (proxied vs refused), total bytes in each direction, total connected time, the longest connection, the busiest client, a per-client table, and a breakdown of how connections ended. The numbers are a straight sum over the log, so they can be re-derived by anyone with the file. check - pre-flight ------------------ opstunnel check --target db.internal:5432 --timeout 3s Dials the target once and closes immediately; nothing is sent. Exit 0 when the connection is established, exit 1 with the reason when it is not. Use it before starting a forwarder, or from a health script. TYPICAL USE # 1. is the service even up? opstunnel check --target 10.0.3.14:5432 # 2. one agreed hop for the team, restricted and accounted opstunnel forward --listen 0.0.0.0:15432 --target 10.0.3.14:5432 \ --allow 10.1.0.0/16 --max-conns 25 --log ops.jsonl # 3. afterwards, who used it and how much opstunnel status --log ops.jsonl LIMITS * TCP only. No UDP, no SOCKS, no HTTP awareness. * One target per process. Run one forwarder per service. * --allow filters by IP address, which is not authentication. * The log grows without bound; rotate it yourself (it is plain JSONL, so moving the file aside between runs is enough). * Connection ids restart at 1 on every run; combine with "start" for a globally unique key. ROADMAP * TLS: terminate or originate TLS at the tunnel, with certificate pinning, so the hop itself is confidential instead of relying on the network. * Authentication: per-client credentials (token or mutual TLS) recorded in the ledger, so the log says WHO connected rather than only which IP. * Multiplexed reverse tunnels: a single outbound control connection from a machine behind NAT, carrying many streams back to it, so services with no inbound route can still be reached through the same accounted hop. EXIT CODES 0 success (including all help output) 1 bad invocation, unreachable target for `check`, or a runtime error