# Rayfish: a P2P mesh VPN built on iroh, with no coordinator to host

> Rayfish is an experimental Rust mesh VPN that derives peer addresses from cryptographic identity and tunnels IP packets over iroh's QUIC connections. It removes the control server but keeps a coordinator role, and the README is honest about the relay fallback.

**rayfish/rayfish** — P2P mesh VPN powered by iroh

- Repository: https://github.com/rayfish/rayfish
- Website: https://rayfish.xyz
- Stars: 708 · Forks: 33
- Language: Rust
- License: MPL-2.0
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/rayfish-rayfish

## The problem Rayfish removes: no rented control plane

Most mesh VPNs ask you to trust or run a coordination service. Rayfish's README states the opposite: there is no control server to host or trust, peers find each other through a DHT and connect directly, and the only "server" is whoever ran ray create, who can be offline once everyone's admitted. That is the whole pitch, and it is a real architectural decision rather than a marketing line. The intended user is someone with a handful of machines scattered behind different NATs who does not want to rent a VPS, open a port, or hand out IP addresses. The README frames the workflow as one person running a command, sharing a code, and the network existing. It is aimed at the same audience that reaches for Tailscale or WireGuard plus a coordination layer, but with the coordination layer moved into a DHT and the addresses derived from keys instead of assigned by a coordinator. The project is explicit that it is experimental, both in the README badge and in the release naming, so the target user is someone willing to run pre-1.0 software on machines they control.

## How the mesh is built: identities, a coordinator, and QUIC tunnels

Each machine runs a daemon that creates a TUN device, captures IP packets, and tunnels them over iroh's QUIC connections. Everything else, including create, join, status and file sharing, is an unprivileged command that talks to the daemon over a local socket. That split is the first thing to understand, because it explains the permission model later.

The lifecycle has four steps. One peer starts a network and becomes its coordinator. The network's public key is its room id: it lets others discover the network but, on a closed network, is not enough to get in. Joining on a closed network happens either with a one-time invite code from ray invite or by requesting approval through ray requests, and the coordinator is the gatekeeper. Every peer then derives its own stable virtual IPv6 address in 200::/7 from its identity and connects directly to every other peer. After that, any TCP or UDP application works, addressed by IP or by name.network.ray.

The address derivation is the part that differs most from conventional mesh VPNs. There is no address pool and no allocator; the address is a function of the key. The README calls the result stable and collision-free. That also means you cannot renumber a peer without changing its identity, which is a trade-off the README does not discuss. The coordinator role is worth noting too: Rayfish removes the hosted control plane, not the notion of a first peer who admits others. If that coordinator's identity is lost, the README does not document a recovery path.

## Installing Rayfish and creating a first network

The README gives a one-line installer plus a daemon start. The installer drops the ray binary in /usr/local/bin, verifies its checksum, and can be redirected with INSTALL_DIR. It is install.sh in the repository, so it can be read before it is run.

```bash
curl -fsSL https://rayfish.xyz/install.sh | sh
sudo ray up    # installs the system service if needed, then activates the VPN
```

Only this first ray up needs root. It installs the background daemon that owns the network device and does the tunneling. From then on, everything runs as your normal user, including ray up and ray down.

The README describes three levels of "on": ray up and ray down toggle the VPN, and down is a quick standby that drops the data plane, meaning the tunnel and DNS, while keeping peer connections warm so up comes back quickly.

With the daemon running, the network itself is three commands. ray create makes you the coordinator of a private network. ray invite mints a one-time code to hand out, and the example uses the name gaming as the network name. A second machine joins with that code.

```bash
ray create                 # you now have a private network of your own
ray invite gaming          # mint a one-time code to hand out
ray join <invite-code>     # a friend joins with the code
```

After that, peers are reachable by name. The README's example is ping alice.gaming.ray, where alice is the peer name and gaming is the network name. Magic DNS keeps that name resolution current as peers join, leave, or rename. If the name does not resolve, the first thing to check is whether the daemon is up, since the DNS part of the data plane is dropped by ray down.

Building from source is also supported. The Cargo.toml targets edition 2024 with rust-version 1.91, and the README states that building needs a Rust toolchain, 2024 edition, Rust 1.85 or newer. The binary target is named ray and is gated behind the desktop feature, so a --no-default-features build skips it and compiles only the library, which is what the Android build does.

## Closed by default, and what the invite code does not give you

The security model is the most interesting design choice here. Networks are closed unless you say otherwise, and the README is precise about why: the code you share to discover a network is not enough to join it. Discovery and admission are separate. The room id, which is the network's public key, only lets a peer find the network. Getting in requires a one-time invite from ray invite, a reusable fleet key, or live approval through ray requests. There is an --open flag for public networks, which flips that default and should be treated as an explicit decision rather than a convenience.

On top of admission control there is a per-device userspace firewall for mesh traffic, described as secure by default and layered on top of the host firewall. The README also documents declarative provisioning: ray apply deploy.yaml stands up networks and firewall rules from a YAML spec, with reusable aliases and groups instead of repeated hostnames. That is the feature that makes the firewall practical at more than a few nodes, because hand-maintained rule sets across a mesh drift quickly.

There is a second, narrower connection type worth knowing about. ray connect <contact-id> ties two peers directly with no room id and no invite, approved like a friend request. That is useful for a single persistent link between two people, but it sits outside the network abstraction, so it does not participate in the network's DNS namespace or its firewall groups in the same way.

## NAT traversal, the relay fallback, and the 41383 UDP port

Hole punching and end-to-end encryption come from iroh, including automatic port mapping through UPnP, NAT-PMP and PCP. The README gives one number that matters for planning: when a direct path is not possible, roughly 10% of the time, traffic falls back to encrypted relays. That is a real dependency on relay infrastructure, and the README does not say who operates those relays or what their capacity and availability characteristics are. If your threat model excludes third-party relays, or your latency budget cannot absorb a relayed path, this is the part to investigate before deploying.

For routers that block automatic port mapping, the daemon listens on a fixed UDP port, 41383, which can be manually forwarded to guarantee a direct path. There is an important caveat in the README: a manual forward maps the port to one machine, so only one node per LAN benefits. The others still use automatic traversal and relay fallback. That makes the manual forward a targeted fix for a single well-connected node, not a general solution for an office behind a hostile router.

The README also lists mDNS local discovery and an optional Tor transport. Neither is described in enough detail in the available text to say how they interact with the relay path, so treat them as options to verify against the source rather than as documented guarantees.

## Where Rayfish is the wrong tool

The README labels the project experimental, and the release list backs that up: a nightly tag sits alongside v0.4.0 and v0.4.1, with the nightly published before the stable releases. Anyone who needs a versioned, stable CLI contract for automation should look elsewhere until the project says otherwise.

Platform coverage is uneven by the README's own account. Rayfish runs on Linux and macOS. Windows x64 is experimental, uses a LocalSystem service, signed Wintun and named-pipe IPC, and requires running ray up from an elevated terminal once to register the operator SID. Critically, the README states that Windows SSH and PTY commands are not available in this first port, so ray.exe must be used in terminals and scripts. Android is described as early and experimental. If your fleet is mixed and you depend on the mesh SSH feature, the Windows gap is disqualifying today.

Exit nodes are also asymmetric. Offering a gateway works on Linux, macOS and FreeBSD, but using one works only on Linux and macOS. A FreeBSD machine can be the exit but cannot be a client of one.

Finally, the coordinator model has an operational cost the README does not resolve. The coordinator can be offline once everyone is admitted, which is the good case. What is not documented is what happens when the coordinator's identity is lost, or how to hand the coordinator role to another peer. If your network depends on one person's laptop, that is an unaddressed single point of failure.

## How it compares to Tailscale and plain WireGuard

The README itself makes the Tailscale comparison, noting that the daemon is comparable to Tailscale's tailscaled and that day-to-day commands run without sudo in a Tailscale-style operator model. The difference in approach is where coordination lives. Tailscale relies on a hosted coordination service and an identity provider; Rayfish replaces that with a DHT for discovery and a coordinator peer for admission, with addresses derived from keys rather than allocated. If you cannot or will not depend on a hosted control plane, that is the substantive difference.

Against plain WireGuard the gap is larger. WireGuard gives you an encrypted interface and a peer configuration file; it does not do NAT traversal, name resolution, admission control, or a per-device firewall. Rayfish bundles all four. The cost of that bundling is a running daemon, a TUN device, a DHT dependency, and a relay fallback path, none of which a static WireGuard configuration needs. For two servers with stable public addresses, WireGuard is simpler and has fewer moving parts. Rayfish earns its complexity when peers are behind NATs and the set of peers changes.

The README does not name a specific competitor beyond the tailscaled comparison, so anything beyond that is inference rather than a documented positioning.

## Licence, maintenance, and what upgrading costs

Rayfish is MPL-2.0. That is a file-level copyleft licence: modifications to files already covered by the licence must be made available under the same licence, while larger works that combine covered files with other code can be distributed under other terms. This is not legal advice, and anyone embedding the daemon or the ray-proto crate in a product should read the licence text and the SECURITY.md file in the repository rather than rely on a summary.

The repository was not archived as of the last push on 2026-09-17, and v0.4.1 was released on 2026-09-01, with v0.4.0 on 2026-08-26. The project is still pre-1.0, so expect CLI and configuration changes between minor versions. The repository contains a CHANGELOG.md and a cliff.toml, which suggests release notes are generated, so the changelog is the place to check before upgrading.

Upgrade cost is lower than for many daemons because the README states that once the service is installed, ray update keeps it current without rebuilding. That does not remove the need to read the changelog: a change to the firewall defaults or the invite semantics would be a configuration change, not a binary swap. On a source build the toolchain floor is real, since Cargo.toml sets rust-version to 1.91 while the README's build instructions say Rust 1.85 or newer. The manifest is the stricter of the two, so plan for 1.91.

## Conclusion

Try Rayfish if you want a small private network between your own machines and a few friends, you are comfortable with an experimental project, and you are on Linux or macOS. Do not adopt it if you need Windows parity today: the README states the Windows x64 port is experimental and that SSH and PTY commands are not available in it. Do not adopt it either if you need a stable, versioned CLI contract, since the release history in the repository shows a nightly tag alongside v0.4.x. Before rolling it out, read install.sh, confirm the daemon's fixed UDP port 41383 is reachable or that relay fallback is acceptable for your traffic, and verify the permissions model by running ray status as your normal user rather than as root.

## FAQ

### What is Rayfish?

Rayfish is a peer-to-peer mesh VPN written in Rust and powered by iroh. It lets machines behind different NATs reach each other as if they were on the same router, with addresses derived from each peer's cryptographic identity rather than assigned by a server.

### How do you install Rayfish?

The README gives a curl command that pipes https://rayfish.xyz/install.sh into sh, followed by sudo ray up to install the system service and activate the VPN. The installer places the ray binary in /usr/local/bin and verifies its checksum.

### Does Rayfish work on Windows?

Windows x64 is described as experimental. It uses a LocalSystem service, signed Wintun and named-pipe IPC, and requires running ray up from an elevated terminal once to register the operator SID. The README states that Windows SSH and PTY commands are not available in this first port.

### What happens if a direct peer-to-peer connection is not possible in Rayfish?

The README states that when a direct path is not possible, roughly 10% of the time, traffic falls back to encrypted relays. For routers that block automatic port mapping, the daemon listens on a fixed UDP port, 41383, which can be manually forwarded, though a manual forward benefits only one node per LAN.

## Sources

- [License: MPL-2.0](https://github.com/rayfish/rayfish/blob/master/LICENSE)
- [Project website](https://rayfish.xyz)
- [rayfish/rayfish on GitHub](https://github.com/rayfish/rayfish)
- [README](https://github.com/rayfish/rayfish/blob/master/README.md)
- [Releases](https://github.com/rayfish/rayfish/releases)

---

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