Skip to content

yzs-lab/fswire

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fswire

Tunnel a TCP port across two hosts that share a filesystem but have no direct network path.

Some machines can't talk over the network — different VLANs, an air gap, a one-way DMZ — yet they happen to mount the same shared filesystem (NFS, SMB, a synced folder, a cloud drive). fswire turns that shared directory into a wire: it carries a real TCP connection between the two hosts, byte for byte.

  • Zero dependencies. Pure Node.js (net, fs, path). No build step. npx fswire and go.
  • NFS-safe. Doesn't rely on append/tail (which NFS close-to-open consistency makes unreliable). Each direction is a stream of discrete, monotonically-numbered message files published with write-tmp → fsync → rename (atomic within a directory) and consumed strictly in order.
  • Transparent. It moves raw bytes, so anything TCP works through it — HTTP, WebSocket, SSH, databases, RPC. The application needs no changes; just point it at the local port.
   client host (no network to target)      shared FS          server host
 ┌──────────────────────────────┐    ┌──────────────────┐  ┌────────────────────────┐
 │ your app  ──▶ 127.0.0.1:PORT │    │ <SHARED>/fswire/ │  │ fswire server          │
 │ fswire client  ──────────────┼───▶│   <channel>/     │◀─┼── dials TARGET_HOST:PORT│
 │   --listen-port PORT         │    │     conns/<id>/  │  │                         │
 └──────────────────────────────┘    └──────────────────┘  │ real TCP service (:PORT)│
                                                            └────────────────────────┘

Install / Run

No install needed:

# on the host that CAN reach the real service (e.g. a web server on :3000)
npx fswire server --shared-dir /mnt/share --target-port 3000

# on the host that shares the filesystem but can't reach it directly
npx fswire client --shared-dir /mnt/share --listen-port 3000

# now, on the client host:
curl http://127.0.0.1:3000     # tunnels to the server host's :3000

Or install globally: npm i -g fswire, then fswire server … / fswire client ….

--shared-dir is each host's own mount point of the same shared filesystem. The two paths may differ between hosts (e.g. /mnt/share vs /data/share); each side just points at its own view.

Which side is which?

  • server runs where the real TCP service lives. It dials --target-port.
  • client runs where your application is. It listens on --listen-port.

Mnemonic: the client is where you connect from; the server is where the service is.

Options

Every option is a CLI flag or an environment variable (flag wins).

Flag Env Default Side Meaning
--shared-dir FSWIRE_SHARED_DIR (required) both local path to the shared FS root
--channel FSWIRE_CHANNEL default both namespace; client & server must match
--poll-ms FSWIRE_POLL_MS 15 both recv directory poll interval (ms)
--hb-ms FSWIRE_HB_MS 1000 both heartbeat write interval (ms)
--peer-timeout FSWIRE_PEER_TIMEOUT 15000 both tear down if peer heartbeat stale (ms)
--max-backlog FSWIRE_MAX_BACKLOG 256 both pause socket above this many pending files
--verbose off both log to stderr
--target-host FSWIRE_TARGET_HOST 127.0.0.1 server host of the real service to dial
--target-port FSWIRE_TARGET_PORT (required) server port of the real service to dial
--scan-ms FSWIRE_SCAN_MS 50 server new-connection scan interval (ms)
--listen-host FSWIRE_LISTEN_HOST 127.0.0.1 client local bind host
--listen-port FSWIRE_LISTEN_PORT (required) client local TCP port your app connects to

Run multiple independent tunnels over one shared dir by giving each a distinct --channel.

Programmatic API

import { runServer, runClient } from 'fswire'

// both return a promise that stays pending while the tunnel runs
runServer({ sharedDir: '/mnt/share', targetPort: 3000, verbose: true })
runClient({ sharedDir: '/mnt/share', listenPort: 3000, verbose: true })

How it works (wire protocol)

Per connection, under <shared>/fswire/<channel>/conns/<connId>/:

meta.json          # client announces the connection (written last)
c2s/<seq>.msg      # client → server bytes (≤64 KiB each, tmp+rename)
s2c/<seq>.msg      # server → client bytes
c2s.eof / s2c.eof  # half-close: that writer is done sending
close              # either side requests teardown
client.hb / server.hb  # liveness heartbeats (epoch ms)

Readers poll their recv subdir, read complete files in seq order (a gap blocks until the missing file appears — never reorders), then delete them. A stale heartbeat (--peer-timeout) or a close marker tears the connection down and closes the paired TCP socket. Backpressure: the local socket is paused when unconsumed outbound files exceed --max-backlog.

Notes & limits

  • Latency is bounded by --poll-ms (default 15 ms per hop). Great for APIs, control channels, and bulk transfer; interactive typing over the tunnel feels slightly laggier than a direct link. Lower --poll-ms to trade CPU for latency.
  • Security. The tunnel carries the same bytes as a direct TCP link — including any credentials your protocol sends. Anyone who can read the shared directory can read the traffic. Protect it with filesystem permissions, and prefer protocols with their own encryption for sensitive data.
  • Cleanup. Connection dirs are reaped on teardown and on server startup. Leftover dirs from a hard-killed host are harmless and get cleaned when their heartbeat goes stale or on the next server start.

Test

npm test     # byte-exact round-trips (incl. 2 MiB chunked) + concurrent isolation

License

MIT

About

Tunnel a TCP port across hosts that share a filesystem but have no direct network path. Zero-dependency, NFS-safe.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages