# ruroco: triggering pre-configured server actions with a single UDP packet

> ruroco is a Rust tool that runs a whitelisted command on a server when it receives a single encrypted UDP packet. It carries no session and no traffic, so it grants a capability to act rather than network access, and the commands live in a root-only file the network-facing server process cannot read. It ships as four binaries with a client, a GUI, a server and a privileged commander.

**beac0n/ruroco** — Ruroco is a tool that lets you execute commands on a server by sending UDP packets. The commands are configured on the server side, so the client does not define what is going to be executed, it only picks from existing commands.

- Repository: https://github.com/beac0n/ruroco
- Website: https://beac0n.github.io/ruroco/
- Stars: 556 · Forks: 15
- Language: Rust
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/beac0n-ruroco

## A capability to act, not network access

ruroco is a tool that lets you execute commands on a server by sending UDP packets, and the readme is careful to frame it precisely.

It triggers a pre-configured action on the server, such as opening a firewall rule, restarting a service or running a script. It is not a tunnel or a VPN. There is no session, no connection, and no traffic carried: the client fires a single stateless packet and the server runs a whitelisted command.

The phrase the readme uses for this is the key idea. ruroco grants a capability to act without granting network access. That is a genuinely different security model from a remote shell, and the whole design follows from it.

With a shell, the compromise of the client credential means an attacker has whatever the remote user can do. With ruroco, the set of things that can be done is fixed on the server side, and the client can only pick from that set. The readme states it plainly: the commands are configured on the server side, so the client does not define what is going to be executed, it only picks from existing commands.

The mechanism is enforced by the process layout. The commands are saved on the server in a root-only file that the network-facing server process cannot read. So the unprivileged daemon that listens on the socket is not even able to discover what it is allowed to run, let alone accept an arbitrary command from the network. A packet can name a command, but it cannot introduce one.

That is why the tool suits a specific job: opening a hole in a firewall temporarily, bouncing a service, kicking off a script, without handing out shell access to a machine that might be on a network you do not want to expose.

The project is Rust, MIT licensed, at version 0.14.2 in the Cargo manifest with a rust-version floor of 1.92, and the last push was on 2026-07-26.

## Four binaries and what each one does

The tool consists of four binaries, and the split is the security model expressed in code.

ruroco-client runs on your notebook or computer and sends the UDP packets. ruroco-client-ui presents most of the functionality of ruroco-client in an easier to use user interface, and it is a graphical application rather than a terminal program.

ruroco-server receives the UDP packets and makes sure that they are valid. That is its entire job, and the word valid is doing a lot of work: the packet has to decrypt correctly, it has to not be a replay, it has to name a command that exists, and the source address has to pass whatever IP validation the server applies.

ruroco-commander runs the command encoded by the data of the UDP packet if it is valid. And this one runs with different privileges, which is the point. The commander is the process that can read the root-only command file and execute what is in it. The server cannot. So a compromise of the network-facing process does not immediately become arbitrary command execution.

The build system makes that separation explicit, because each binary is built with its own feature flag. The Makefile builds client with the with-client feature, client_ui with with-gui, server with with-server, and commander with with-commander, each targeting x86_64-unknown-linux-gnu and each with the other features turned off via no-default-features. So you can produce a server binary that contains no client code, no GUI code and no commander code, and the same the other way round.

The Cargo manifest shows the shape of the dependencies. rmp-serde for MessagePack serialisation, anyhow for error handling, base64, blake2 for hashing, zeroize with the derive feature for scrubbing key bytes from memory, chrono for timestamps, and clap with the derive and string features for the command line interface. Optional dependencies carry the heavier pieces: openssl, ureq with the json feature, tempfile, toml, and eframe, the GUI toolkit, configured with default-features off and the glow, wayland and x11 backends and default fonts.

The Android build has its own optional dependency block, with jni, ndk-context, android-activity and wgpu with the gles feature, and a comment noting that these are only ever built when targeting Android so that the all-features build stays buildable on desktop hosts.

## The security model in five lines

The security section of the readme is short, and each bullet answers a different question an attacker would ask.

The client sends a UDP packet to the server and the server never responds to it, so port-scanning does not help an adversary. That is a real property and a slightly unusual one. There is nothing to connect to, so a scanner looking for open ports gets nothing back. It also means the client gets no confirmation, which has its own consequences for reliability.

Data sent from the client to the server is encrypted symmetrically with AES-256-GCM-SIV using a shared key, and the readme is direct that symmetric is a deliberate trade-off, with the full reasoning in SECURITY.md. GCM-SIV is the nonce-misuse-resistant variant of GCM, which matters here because a nonce reused with the same key in standard GCM is catastrophic, and the choice indicates the author thought about that failure mode rather than picking a mode by default.

The client only defines a command to execute, and commands are saved on the server in a root-only file the network-facing server process cannot read. This is the bullet that stops the tool from being a remote shell.

Run the server software in such a way that it uses as little operating system rights as possible. That is the least specific bullet and the most easily ignored one, and it points at the systemd unit in the systemd/ directory at the repository root.

Replay protection works by adding every packet the server received to a blocklist. The client has a matching reseed command, reseed the replay-protection counter to the current timestamp, which is the mechanism for recovering from a state where the counter would otherwise need to catch up.

The readme points to SECURITY.md for the full threat model: in-scope attacks, accepted risks, the key lifecycle covering generation, rotation and revocation, and a supported-versions table. That last one matters for a security tool, because it tells you which releases still get fixes.

## Using the client, and the wizard

The client command set is small and each subcommand maps to one job.

```text
Usage: ruroco-client <COMMAND>

Commands:
  gen     Generate a shared AES key (base64 with embedded key id)
  send    Send a command to a specific address
  update  Update the client binary
  wizard  Run the wizard to set up the server side
  reseed  Reseed the replay-protection counter to the current timestamp
  help    Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

```

The gen command generates a shared AES key, base64 with an embedded key id. That key id is what lets a server accept a key and know which one it is, and it is why the readme talks about key rotation and revocation in SECURITY.md.

The wizard runs the wizard to set up the server side, and the flow around it is described carefully. The Generate Key action in the GUI, or ruroco-client gen, produces a base64-encoded shared key, and you copy that key into the server's .key files and reuse the same string with ruroco-client send. The readme suggests storing the key safely in a password manager, or using secret-tool to keep it in the local keyring, with the example secret-tool store --label="ruroco" token ruroco.

The send command takes an address, a key file and a command name, and it has a permissive flag for allowing permissive IP validation, so the source IP does not have to match the expected address. That is a per-invocation override of the source address check, and it is worth understanding before using it, since the check exists to stop a stolen key being used from somewhere else.

The server install instructions are worth reading for one specific reason. After running the install_server make goal you have to generate a shared .key file and copy it to the right place, set up the config.toml, and start the server with systemctl start ruroco.service. And the wizard deliberately leaves it stopped until a .key exists, since a keyless server exits at startup and Restart=always would crash-loop it into systemd's start limit.

That is a small piece of operational engineering that saves a genuinely confusing failure. The alternative is a service that refuses to start, restarts forever, and produces a wall of journal output that looks like a crash rather than a missing key.

## Installation paths, packaging and the repository layout

There are several ways to get the binaries, and the readme covers each of the four binaries separately.

You can download them from the releases page, or build them yourself by running make release, and then find the binaries in target/release/client, target/release/client_ui, target/release/server and target/release/commander. The names in the Cargo manifest match: the package is ruroco with four binaries named client, client_ui, server and commander.

For Arch Linux there are AUR packages, ruroco-client, ruroco-client-ui and ruroco-server. And there is a pre-built script that pulls the client out of the GitHub releases API and installs it into ~/.local/bin, then runs ruroco-client update --force.

The Android build is a separate track, with nix/android.nix, scripts/dev_ui_android.sh and scripts/release_android.sh, and the Makefile has a dev_ui_android target that runs nix-shell nix/android.nix --pure with the script.

The repository layout is extensive and tells you about the project's maturity. There is a SECURITY.md with the threat model, a CHANGELOG.md, a Cargo.lock, a Makefile, a version.sh, and a manifest.yaml. There is a config/ directory, a systemd/ directory for the unit file, a fail2ban/ directory, a fuzz/ directory, a tests/ directory, a docs/ directory that is built with mdbook, and a nix/ directory. There is a clippy.toml and a rustfmt.toml for linting and formatting configuration, a deny.toml which is the cargo-deny configuration for dependency policy, and a _typos.toml.

The release signing story is in the Makefile. The gen_signing_key target refuses to overwrite an existing keys/ruroco-release-ed25519.key, generates an ed25519 signing key with openssl, produces the public pem, and then prints the next step, adding the private key as the RUROCO_SIGNING_KEY GitHub Actions secret. The echo output says which file is private and gitignored and should be backed up offline, and which is public and committed and embedded into the client. That is release signing done properly, with the private key never in the repository.

The recent releases are v0.14.2 from 2026-05-30, v0.14.1 from 2026-05-29 and v0.13.1 from 2026-05-28, so the project ships often, and the changes in each are recorded in the changelog.

## Conclusion

Adopt ruroco if you need to trigger a small set of operational actions on a remote host, such as opening a firewall rule or restarting a service, and you want something narrower than a VPN. Do not adopt it as a remote shell, because it deliberately carries no traffic and has no session, and read SECURITY.md before deploying it, since the shared key model is a deliberate trade-off. Before you commit, run the wizard on the server, put the key in place first, and decide whether the extra privilege separation of the commander process is worth the operational complexity for your setup.

## FAQ

### What does ruroco do?

It is a Rust tool that lets you execute commands on a server by sending UDP packets. It triggers a pre-configured action on the server, such as opening a firewall rule, restarting a service or running a script, and is explicitly not a tunnel or a VPN: there is no session, no connection and no traffic carried.

### Why is ruroco not a remote shell?

Because the commands are configured on the server side, so the client can only pick from existing commands rather than define what runs. The commands are saved on the server in a root-only file that the network-facing server process cannot read, so a compromised listener still cannot introduce a new command.

### What are the four ruroco binaries?

ruroco-client sends the UDP packets, ruroco-client-ui is a graphical front end for most of that functionality, ruroco-server receives the packets and validates them, and ruroco-commander runs the command encoded by a valid packet. Each is built separately through its own cargo feature flag, so a server binary contains no client or GUI code.

### How does ruroco protect against replay and key compromise?

Packets are encrypted symmetrically with AES-256-GCM-SIV using a shared key, and replay protection works by adding every packet the server received to a blocklist, with a reseed command on the client to reset the counter to the current timestamp. The readme calls the symmetric design a deliberate trade-off and points to SECURITY.md for the full threat model, accepted risks and key lifecycle.

### How do I install ruroco?

Download the binaries from the releases page, build them with make release, or use the Arch Linux AUR packages. The server install also involves generating a shared .key file, placing it under /etc/ruroco/, editing the config.toml and starting ruroco.service with systemctl, and the wizard deliberately leaves the service stopped until the key file exists.

## Sources

- [beac0n/ruroco on GitHub](https://github.com/beac0n/ruroco)
- [License: MIT](https://github.com/beac0n/ruroco/blob/master/LICENSE)
- [Project website](https://beac0n.github.io/ruroco/)
- [README](https://github.com/beac0n/ruroco/blob/master/README.md)
- [Releases](https://github.com/beac0n/ruroco/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/beac0n-ruroco
