End-to-End Flow
This chapter follows one command from the moment you press Enter on the client to the moment the shell command runs on the server. It is the single most important page for understanding how the modules fit together. Each step links to the leaf chapter that documents it in detail.
The whole journey at a glance
sequenceDiagram
autonumber
participant U as User (CLI or GUI)
participant C as client (src/client)
participant Net as UDP network
participant S as server (unprivileged)
participant Sock as Unix socket
participant Cmd as commander (root)
participant Sh as shell
U->>C: ruroco-client send -a host:port -k KEY -c open_port
Note over C: acquire single-instance lock
C->>C: resolve host to IP(s), filter by --ipv4/--ipv6
loop for each destination IP
C->>C: increment + persist counter (u128 ns)
C->>C: ClientData::create(cmd, strict, src_ip, dst_ip, counter)
C->>C: serialize to 58-byte plaintext
C->>C: encrypt: IV(12)+tag(16)+ct(58) = 86 bytes
C->>C: prepend 8-byte key_id => 94-byte packet
C->>Net: send one UDP datagram
C->>C: sleep send_delay_ms
end
Net->>S: 94-byte datagram arrives
S->>S: split key_id (8) + ciphertext (86)
S->>S: look up CryptoHandler by key_id
S->>S: RateLimiter::check(source_ip)
S->>S: decrypt + GCM tag verify
S->>S: deserialize ClientData (58 bytes)
S->>S: validate (replay, dst_ip, strict src_ip)
S->>S: persist new counter to blocklist
S->>Sock: send 24-byte CommanderData (cmd_hash + ip)
Note over S: server NEVER replies to the client
Sock->>Cmd: 24 bytes
Cmd->>Cmd: look up command string by hash
Cmd->>Sh: sh -c "<command>" with RUROCO_IP set
Sh-->>Cmd: exit status (logged, not returned)
Phase 1: the client builds and sends the packet
Driven by Sender::send (send/).
- Lock. The client acquires a PID-based single-instance lock at
<conf_dir>/client.lockso two runs cannot race the counter (lock.rs). - Resolve. The destination
--addressis resolved to one or more IPs, filtered by the--ipv4/--ipv6flags. The client then loops over each destination IP. - Counter. For each IP it increments the persistent counter and writes it to disk
immediately. The counter is a
u128nanosecond value, seeded to “now” on first use, and is strictly increasing (counter.rs). This is what makes replays impossible. - Build plaintext.
ClientData::createhashes the command name with Blake2b-64 and packsversion,cmd_hash,counter,strict,src_ip,dst_ipinto a fixed 58-byte layout (Wire Protocol). Note the inversion: the CLI flag is--permissive, but the packet carriesstrict = !permissive. - Encrypt. The 58 bytes are encrypted with AES-256-GCM-SIV using the shared key. A fresh random
IV is generated per packet. Output is
IV(12) || tag(16) || ciphertext(58)= 86 bytes (Cryptography). - Frame. The 8-byte
key_idis prepended, giving the final 94-byte packet. The key_id tells the server which shared key to use without revealing it. - Send. Exactly one UDP datagram goes out per destination IP, with
send_delay_msbetween IPs. The client does not wait for and does not expect a reply.
Phase 2: the server receives and validates
Driven by the server main loop and handler.rs (Server Overview,
handler.rs).
- Receive.
socket.rsreads a datagram into a 94-byte buffer. The socket is either inherited from systemd socket activation or bound to[::]as a fallback (socket.rs). - Decode frame. The first 8 bytes are the
key_id; the remaining 86 are the ciphertext blob. - Select key. The server loads every
*.keyfile in its config dir at startup; thekey_idselects the matchingCryptoHandler(keys.rs). - Rate limit.
RateLimiter::checkenforces a per-IP cap (default 2 requests/second). This is throttling, not security (rate_limiter.rs). - Decrypt. AES-256-GCM-SIV decrypts and verifies the tag. A bad key or tampered packet fails the tag check and is dropped silently.
- Deserialize. The 58-byte plaintext becomes a
ClientDatastruct. The leadingversionbyte is checked againstPROTOCOL_VERSION(it is authenticated, so this happens after the tag verifies). - Validate, in order (handler.rs):
- Replay: the counter must be strictly greater than the highest counter previously seen
for this
key_id(the blocklist floor). Equal counts as a replay (blocklist.rs). - Destination IP: the
dst_ipin the packet must be one of the server’s configured IPs. - Strict source IP: if the client set
strictand included asrc_ip, it must match the real source IP of the datagram.
- Replay: the counter must be strictly greater than the highest counter previously seen
for this
- Persist. On success the new counter becomes the blocklist floor and is written to disk, so the same packet can never be accepted again, even across restarts.
- Forward. The server sends a 24-byte
CommanderData(cmd_hash[0:8]+ip[8:24]) over the Unix socket. It then goes back to listening. It never replies to the client.
Phase 3: the commander executes
Driven by the top-level commander module (mod.rs + exec.rs) (commander).
- Receive. The commander reads the 24-byte
CommanderDatafrom the Unix socket. - Look up. It hashes each configured command name with Blake2b-64 and finds the one matching
cmd_hash. An unknown hash is logged and ignored. - Execute. It runs the configured shell string via
sh -c, with the environment variableRUROCO_IPset to the requesting client’s IP (so commands can reference$RUROCO_IP, for example to allow that exact IP through the firewall). - Done. The exit status is logged. Nothing is sent back to the server or the client.
Why this shape
- Two processes, one socket. Splitting
server(unprivileged, network-facing) fromcommander(privileged, local-only) means a bug in the parser cannot directly run privileged commands; it can only ever push 24 well-formed bytes through a Unix socket whose other end is the commander. - Counter written before send, floor written after accept. The client advances its counter before sending and the server advances its floor only after accepting. Combined with the strictly-greater check, this guarantees monotonic, gap-tolerant replay protection even if packets are lost or reordered.
- No response, ever. The absence of a reply is a feature. There is no oracle to probe and no packet for an attacker to elicit.
Continue with the Wire Protocol to see the exact bytes, or jump straight into a subsystem via Client Overview or Server Overview.