GitHub

Your machines, on one private network, anywhere. Rayfish is a peer-to-peer mesh VPN that lets your laptop, phone, server, and your friends' machines talk to each other as if they were all plugged into the same router, even when they're scattered across the world behind different NATs.

There's nothing to host and nothing to sign up for. You don't rent a server, open a port, or hand out IP addresses. One person runs a command, shares a code, and the network exists.

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
ping alice.gaming.ray      # you reach each other by name

That's the whole idea. The rest of this README is the details.

License: MPL 2.0 Status: experimental

Jump to: Why · How it works · Features · Quick start · Managing your network · Who can join · Firewall · Provisioning · Permissions · Custom relay & DNS · Troubleshooting · How it compares · FAQ


Why Rayfish

  • No infrastructure. There's no control server to host or trust. Peers find each other through a DHT and connect directly. The only "server" is whoever ran ray create, and they can be offline once everyone's admitted.
  • Identity, not IP. Every machine has a cryptographic identity, and its addresses are derived from that identity: stable, collision-free, and assigned without any coordinator handing them out.
  • Private by default. Networks are closed unless you say otherwise. The code you share to discover a network isn't enough to join it.
  • Works over NAT. Hole-punching and end-to-end encryption come from iroh, including automatic port mapping (UPnP/NAT-PMP/PCP). When a direct path isn't 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) you can manually forward to guarantee a direct path. 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.
  • Reach peers by name. Magic DNS gives you name.network.ray so you never memorize a virtual IP.

How it works

Each machine runs a small daemon (comparable to Tailscale's tailscaled) that creates a TUN device, captures IP packets, and tunnels them over iroh's QUIC connections. Everything else (create, join, status, file sharing) is an unprivileged command that talks to the daemon over a local socket.

  1. Create. 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.
  2. Join. On a closed network a peer gets in with a one-time invite code (ray invite) or by requesting approval (ray requests). The coordinator is the gatekeeper.
  3. Mesh. Every peer derives its own stable virtual IPv4 (100.64.0.0/10) and IPv6 (200::/7) from its identity, then connects directly to every other peer.
  4. Use it. Any TCP/UDP app works, addressed by IP or by name.network.ray.

Features

Each of these has a fuller treatment further down; this is the one-line tour.

  • 🔒 Closed-by-default networks. One-time invites, reusable fleet keys, or live approval, with --open for public ones. See Who can join.
  • 🤝 Direct 2-peer links. ray connect <contact-id> ties you to one person with no room id or invite, approved like a friend request. See Direct 2-peer connections.
  • 🌐 Magic DNS. Reach peers at name.network.ray, updated live as they join, leave, or rename.
  • 🧱 Per-device firewall. A userspace firewall for mesh traffic, secure by default, layered on top of your host firewall. See Firewall.
  • 🔑 Mesh SSH, no keys. Log in over the mesh with a stock ssh client; peers authenticate by identity. See SSH, no keys.
  • 🌍 Exit nodes. Route all your internet traffic through a peer that offers itself as a gateway: ray exit-node allow on the gateway, ray exit-node use on the client. Offering a gateway works on Linux, macOS and FreeBSD; using one works on Linux and macOS.
  • 📜 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.
  • 👥 Multi-device identity. Pair your laptop and phone under one identity, with encrypted key backup (optionally to 1Password). See Pairing your own devices.
  • 📁 File sharing. ray send file.zip bob, with optional auto-accept for transfers from your own paired devices.
  • 📡 mDNS local discovery, and optional Tor transport.
  • 🛠 Operator model. Run day-to-day commands without sudo, Tailscale-style. See Permissions.

Quick start

Here's the whole tour: install once, create a network, invite a friend, and reach each other by name. Two machines, about five minutes.

Rayfish runs on Linux and macOS (Android is early and experimental). Building from source needs a Rust toolchain (2024 edition, Rust 1.85+); see Building. Once the service is installed, ray update keeps it current without rebuilding.

1. Install & start

Install the latest release, then bring the VPN up:

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

The installer drops the ray binary in /usr/local/bin (override with INSTALL_DIR) and verifies its checksum. It's install.sh in this repo, so you can read it before you run it. To build from source instead, cargo build and see Building.

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

Three levels of "on":

  • ray up / ray down toggle the VPN itself. down is a quick standby: it drops the data plane (the tunnel and DNS) but keeps your peer connections warm, so up comes back near-instantly and needs no root.
  • sudo ray stop / sudo ray start turn the whole daemon off and on. stop goes fully offline and closes every connection; start brings it all back. sudo ray restart is the two in one step (handy after changing config).

To update later, sudo ray update grabs the latest stable release, or sudo ray update --nightly follows the bleeding edge. Both swap the binary and restart the daemon for you. More on this just below.

Updating

ray --version            # show the installed version + git sha (also `ray version`)
ray update --check       # report current vs the latest GitHub release
ray update --list        # list available releases (newest first)
sudo ray update          # download + verify the latest stable release, swap the binary, restart the daemon
sudo ray update --nightly        # track the rolling nightly (rebuilt on every commit to master)
sudo ray update --version 0.1.0  # install a specific release (downgrades allowed)
sudo ray install --auto-update   # enable automatic stable updates at install time
ray config set auto-update on    # or toggle it any time (takes effect on `sudo ray restart`)

ray update fetches a release from GitHub, verifies its SHA-256, atomically replaces the running ray binary, and (if the system service is installed) restarts the daemon onto the new version. By default it tracks the latest stable release; --nightly follows the rolling pre-release built from every commit, and --version X pins a specific release. There is no persisted channel: each run picks its target from the flag. It needs root when the installed binary lives in a system path (so use sudo ray update); ray --version, ray update --check, and ray update --list do not.

Automatic updates are opt-in (off by default). Enable them with sudo ray install --auto-update or ray config set auto-update on: the daemon then checks for a newer stable release about every 6 hours and, when one exists, downloads + verifies + swaps the binary and restarts itself onto it. Nightlies are never auto-installed. Because applying an update restarts the daemon, it briefly drops the VPN (peers reconnect automatically), so it stays opt-in. ray status shows when auto-update is on.

2. Create a network

ray create --hostname alice          # closed by default; add --open for a public network
# ✓ network created  gentle-amber-fox
#   IPv4  100.64.23.142
#   IPv6  200:ab3f:d92c:1e4a::1

3. Invite someone

ray invite gentle-amber-fox          # mint a single-use, expiring code
# ✓ invite ab3f9c01
#   <invite-code>
#   single-use, expires in 7d

Hand the code to a friend. On a closed network they can also run ray join <room-id> to land in your approval queue (see Who can join).

4. Join from another machine

ray join <invite-code> --name gaming --hostname bob
# ✓ joined gaming
#   IPv4  100.64.7.201
#   IPv6  200:7c10:5e8b:33a1::1

5. Reach each other

ray status               # networks, peers, and traffic
ping alice.gaming.ray    # by name
ping bob.ray             # flat lookup
ping 100.64.23.142       # or just the IP
ray ping alice           # mesh probe: RTT, loss, and direct-vs-relay path
ray netcheck             # your own bound port, relay, and reachability

ray ping is a mesh-aware probe: it sends live echo probes over the encrypted connection and reports per-probe latency, packet loss, and whether traffic is flowing direct (hole-punched) or via a relay, so you can tell at a glance why a link is slow. ray netcheck reports your own node's conditions: the bound UDP port (and whether it is the fixed, forwardable port), your home relay and its latency, your public addresses, and whether UDP is getting through.

ray status is also where the daemon nudges you about things that need attention. A pending block at the bottom lists anything waiting on you, each row telling you the exact command to deal with it:

  rayfish  ● up      mDNS on      endpoint k7f2…9abc
  gentle-amber-fox  coordinator   alice   100.64.23.142   members 2/3
    ● bob      100.64.7.201   direct   12ms   ↑ 1.2 MB   ↓ 3.4 MB
    ○ carol    100.64.9.14
    join  <room-id>
  pending
    (1)  join request           ray requests gentle-amber-fox
    (1)  file offer             ray files

So if a friend runs ray join <room-id> and is sitting in your approval queue, you don't have to go looking: ray status shows (1) join request and points you straight at ray requests gentle-amber-fox. The same block surfaces incoming file offers, connection requests, and coordinator firewall suggestions.

6. Leave or pause

ray leave gaming         # leave a network
ray kick gaming alice    # coordinator only: remove a member from a closed network (disconnects them mesh-wide)
ray ephemeral gaming 7d  # coordinator only: auto-remove members offline longer than 7d (off | show to disable/print)
ray down                 # standby: data plane (TUN + DNS) off, still connected to peers
ray up                   # reactivate (no root needed, near-instant: connections were kept)
sudo ray stop            # fully offline: daemon exits, peer connections close
sudo ray start           # back online: daemon restarts with both planes on

Run ray --help to discover the rest: invite, requests/accept/deny, firewall, exit-node, apply, send, pair, mdns, and more.

Prefer buttons and forms? Run ray gui to open a local browser GUI. It wraps the same CLI commands, so anything available in ray --help is available there too; commands that need root still need the GUI to be launched with sudo.

Tab completion

Installed for you: the installer and sudo ray up write the completion scripts into the directories bash, zsh and fish already search, so there is nothing to source and no rc file to edit. Open a new shell and press tab.

It is not a static script. Each tab asks the running ray, so the answers are the networks and peers you have right now:

$ ray leave <TAB>
gaming  homelab
$ ray ping <TAB>
alice   100.64.3.1, active
nas     100.64.9.4, idle
$ ray exit-node use homelab <TAB>
gateway 100.64.2.7, active

A tab never starts the daemon and never blocks: with the service stopped it answers from your saved config where it can, and offers nothing where it can't.

If you installed the binary by hand, ray completions --install sets it up (system-wide under sudo, otherwise for your user only). ray completions zsh prints the script instead, for placing yourself.

Managing your network

Once a network exists, running it is a handful of commands. Here are the ones you'll actually reach for.

Public or private

ray create makes a private (closed) network by default. People can discover it by its room id, but the id alone won't get them in: you decide who joins. Pass --open for a public network that anyone with the room id can enter.

ray create                    # private: you approve every join
ray create --open             # public: anyone with the room id joins directly

Pick private for a homelab or a circle of friends, public for a community network you're happy to leave the door open on. The three ways into a private network (one-time invite, reusable key, live approval) are covered under Who can join.

Adding and removing people

ray invite <network>                 # mint a one-time code to hand to one person
ray requests <network>               # see who's asking to join
ray requests <network> accept <id>   # let them in ("deny <id>" to refuse)
ray kick <network> <member>          # remove someone for good; they drop mesh-wide
ray ephemeral <network> 7d           # auto-remove members offline longer than 7d

ray kick is the one to reach for when someone should lose access: it removes them from the network's signed roster and every other member disconnects them. ray invite, ray requests, and ray kick are coordinator actions, so you run them on the machine that created the network (or on any co-coordinator you've added with ray admin add).

Pairing your own devices

Pairing puts several of your own machines (laptop, desktop, phone) under a single identity. They then share every network you belong to and show up as you, not as separate members.

On a device that's already set up, start pairing:

ray pair                      # prints a ticket and a QR code, then waits

On the new device, scan the QR or paste the ticket:

ray pair <ticket>            # join your identity
ray pair list                # list the devices under your identity
ray unpair <device>          # revoke one later

Once paired, both devices are members of everything you've joined, and transfers between them can land automatically (ray files auto-accept <net> on). Back the shared identity key up with ray pair backup (optionally into 1Password) and bring it onto a fresh machine with ray pair restore.

Who can join

The room id (a network's public key) is a discovery key. It's published so peers can find the network, but on a closed network it is not an admission credential. Admission is always the coordinator's job:

  • Closed (default) has three ways in:
    • Invite code. ray invite <network> mints a single-use, expiring code. The holder runs ray join <code>; the coordinator verifies and burns it. Manage with ray invite <network> list / revoke <id>.
    • Reusable key. ray invite <network> --reusable mints a multi-use, expiring key for unattended fleets. Its hash rides the network's signed record, so it admits many machines and revoke propagates to every key-holder. A server joins non-interactively with ray join <key> --hostname web --auto-accept-firewall. The name isn't authoritative, so two servers asking for web become web and web-1. For stable per-host names give each a unique --hostname (e.g. a cloud instance id), and prefer the * wildcard subject for fleet firewall suggestions (a rule keyed to one hostname can retarget as servers come and go). Key expiry is not member expiry: expiry/revoke only blocks new joins; machines already admitted stay members.
    • Live approval. The holder of just the room id runs ray join <room-id> and lands in a queue. The coordinator runs ray requests <network>, then ray requests <network> accept <id> (or deny <id>).
  • Open (ray create --open) lets anyone with the room id join directly. Good for public or community networks.

Either gate runs through a coordinator. The full coordinator set is published in the network's signed record (Member.is_coordinator), so a fresh joiner dials the invite minter first, then falls back across the other coordinators. Admission survives any one coordinator being offline. Once admitted, a member reconnects by cryptographic identity and no coordinator needs to be online.

Direct 2-peer connections

To link up with one person, skip room ids and invite codes entirely. Everyone has a standing contact id (ray contact id, also shown at the top of ray status): a rotatable handle, separate from your network identity, that you can share like a phone number.

ray connect <their-contact-id>     # ask to connect; you wait, pending
ray connect                        # they see the request…
ray connect approve <id>           # …and approve it

Approval creates a private 2-peer network automatically (shown as [direct] in ray status). It's a real network, so firewall rules, Magic DNS, and the mesh all work the same. Approval is recipient-only: the requester consents by asking, the recipient consents by approving. Rotate your contact id anytime with ray contact rotate to stop new requests (existing links keep working). To stay unreachable, don't share the id.

Finding peers on your LAN

Nodes on the same local network announce themselves over mDNS (on by default, ray mdns on|off). That normally just keeps connections direct instead of relayed, but you can also look at what it found:

ray mdns scan                      # rayfish nodes seen on this LAN
ray connect <peer>                 # link up with one, using the id from the scan

ray status also lists LAN neighbours you're not connected to yet, under nearby, so you don't have to go looking.

A sighting grants nothing. The scan marks which neighbours you already share a network with, and connecting to one still needs their ray connect approve. Because the scan gives you an id you can dial directly, the pair does not need the DHT: two machines can link up on a LAN with no internet. The flip side is that anyone on your LAN can send you a connect request without knowing your contact id, so ray contact rotate doesn't stop local requests (your approval still does).

Firewall

Rayfish ships a small userspace firewall that governs mesh traffic only. It sits on top of your host/kernel firewall (a packet has to clear both), and it's secure by default: unsolicited inbound TCP and UDP are denied, while inbound ICMP (ping) and all outbound traffic are allowed. A stateful conntrack lets the return traffic for connections you started back in.

So out of the box you can reach out to peers, but nothing reaches a port on your machine until you open it. To expose a service you run, add an inbound allow rule:

ray firewall add in allow -p tcp --port 22                   # let peers reach your sshd
ray firewall add in allow -p tcp --port 8080 --peer alice    # only alice, only 8080
ray firewall reject on                                       # blocked connections fail fast instead of hanging
ray firewall                                                 # show the current rules

Rules are directional, per-port, and per-network; --peer scopes a rule to a single peer. Don't want a second firewall at all? ray firewall off disables it on that device. Full model: ray firewall --help.

Coordinator suggestions. A network's coordinator can suggest firewall rules that ride the signed network record (a * subject targets every host). Each node sees the pending suggestions in ray status and applies them, or opts into auto-install with --auto-accept-firewall. Suggestions are advisory: your local rules are never overwritten.

SSH, no keys

ray firewall ssh on runs an embedded SSH server bound to your mesh IPs, and ray firewall ssh allow <network> <peer> authorizes a peer to log in. Connect with a stock client:

ssh user@host.ray
scp file user@host.ray:            # scp and sftp need an sftp-server on the host
ssh -L 8080:localhost:80 host.ray  # forwarding works in both directions, -A too

The peer is authenticated by its mesh identity, so there are no authorized_keys to distribute (the same model as Tailscale SSH). An allow rule grants any non-root user by default; -u <users> names the accounts a peer may log in as, and -u '*' includes root.

Sessions, scp/sftp, port and unix-socket forwarding (-L, -D, -R, ProxyJump) and agent forwarding (-A) all work with a stock client. X11 forwarding does not.

Interactive sessions are handed to the host's login(1), so PAM applies (a locked or expired account is refused), the session shows up in who / last / loginctl, and you get the motd. Root sessions and ssh host <cmd> spawn the shell directly, since login won't take a root session on a pseudo-terminal.

Declarative provisioning

For fleets and repeatable setups, ray apply deploy.yaml reconciles your networks against a YAML spec instead of running commands by hand. It creates any missing networks and publishes their firewall suggestions, so the spec is the source of truth you can keep in git.

A spec has three top-level keys, all optional except networks::

# aliases: give a user a name that expands to all of their devices.
# Copy the identity from `ray identityof <net> <host>`.
aliases:
  alice: 7f3a9c01...          # alice, on every device she's paired
# groups: bundle aliases and/or literal hostnames under one name.
groups:
  admins: [alice, jumpbox]    # alice's devices plus a host named "jumpbox"
# networks: the real payload. Each network maps hostnames (the "subject") to
# the firewall rules other peers get toward that host.
networks:
  infra:
    "*":                      # "*" subject = every node in the network
      allows:
        admins: "tcp:22"      # the admins group may reach SSH on every host
  minecraft:
    "*":
      allows:
        "*": "tcp:6969"       # "*" peer = anyone; open 6969 mesh-wide
  gaming:
    alice:                    # a named host, not a wildcard
      allows:
        bob: "tcp:9000,tcp:8123"   # comma-separated proto:port tokens
      denies:
        eve: "icmp"
    carol: {}                 # empty subject = fully open, no rules

A few rules of thumb:

  • Subject and peer keys are hostnames (or an alias/group that expands to them). * as a subject means every node; * as a peer means any peer.
  • If a subject has an allows: list, it's an allow-list: only the listed peers get through, everything else is denied. denies: carves exceptions out.
  • Aliases and groups are coordinator-side shorthand, expanded before publishing. They never travel over the mesh. An alias only resolves once that user has joined; literal hostnames work before anyone joins.

Run it, and iterate safely:

ray apply --example              # print a fully-commented starter spec
ray apply deploy.yaml --dry-run  # show what would change, apply nothing
ray apply deploy.yaml            # create missing networks, publish suggestions
ray apply deploy.yaml --invite-missing   # also mint invites for expected-but-absent hosts
ray apply deploy.yaml --prune            # drop suggestions for hosts no longer in the spec

Suggestions are still advisory on the receiving end: each node queues them for ray firewall accept, or auto-installs them if it joined with --auto-accept-firewall. ray apply never joins a node for you and never edits a peer's local rules. To seed a spec's aliases: from a machine you're on, ray alias <net> set <host> <name> saves the alias locally (it also shows inline in ray status) so you don't have to paste the identity by hand.

Permissions

Like Tailscale, the daemon authorizes each command by the caller's UID, not by file permissions:

  • Read-only commands (status, *… show, files) are open to any local user.
  • Mutating commands need root or the configured operator.
  • The user who installs the service (sudo ray up / ray install) becomes the operator automatically, so they keep working without sudo. Authorize someone else with sudo ray set-operator <user>.

Only a handful of commands need root, because they manage the system service itself:

sudo ray install | restart | uninstall   # manage the service unit / launchd plist
sudo ray start | stop                    # start / stop the service. stop = fully offline (closes peer connections); start = back online
sudo ray set-operator <user>              # let a user run ray without sudo

Custom relay & DNS

By default rayfish uses iroh's public infrastructure for relay fallback and peer discovery. You can point it at your own servers (or the rayfish-operated ones) with ray config:

ray config                                   # show current settings
ray config set relay rayfish                 # use the rayfish relay (keeps n0 as fallback)
ray config set relay https://r1,https://r2   # multiple custom relays
ray config set discovery-dns rayfish         # custom discovery / pkarr server
ray config set dns-upstreams 1.1.1.1,8.8.8.8 # forwarders for non-.ray names
ray config set relay https://r1 --replace    # drop the n0 defaults entirely
ray config unset relay                        # back to defaults

Keys: relay, discovery-dns, dns-upstreams. Values are a comma list of presets (rayfish, n0), URLs, or IPv4 addresses. By default custom servers are added alongside the defaults; --replace swaps them out (a bad custom server with no fallback can isolate the node). Settings are saved to settings.toml and take effect on sudo ray restart.

Running alongside another VPN

Tailscale (and anything else built on CGNAT space) routes all of 100.64.0.0/10, the same range rayfish derives its IPv4 from, so on a host running both, one of the two loses its IPv4 half. The IPv6 ranges don't overlap, so rayfish steps aside and uses only its own.

This needs no setup: the daemon looks for another VPN on that range at startup and switches itself, logging why and showing ipv6-only on (auto) in ray status. Nothing is written to your config, so the mode ends when the other VPN does. To decide it yourself:

ray config set ipv6-only on    # always, even with no other VPN present
ray config set ipv6-only off   # never; refuse to start on such a host instead
ray config set ipv6-only auto  # the default: let the daemon decide at startup
sudo ray restart               # the mode is fixed when the tunnel is built

Peers, mesh SSH, file transfer, and .ray names all keep working over IPv6; .ray answers AAAA only, and peers are told not to use your mesh IPv4 either. Magic DNS moves to 200::53 in this mode, because the usual 100.100.100.53 sits in the range the other VPN is filtering (Tailscale drops anything sourced from 100.64.0.0/10 that doesn't arrive on tailscale0, which includes our own DNS replies).

The one thing that doesn't work is ray exit-node use: the full tunnel is IPv4 policy routing, so it's refused in this mode. Serving as an exit node for others is unaffected. NetworkManager's DNS backend is skipped too (it can only carry an IPv4 nameserver); the next backend down takes over.

If both VPNs manage /etc/resolv.conf directly (no systemd-resolved), rayfish shares the file rather than fighting over it or giving it up. It writes its own resolver in first, keeps the other VPN's behind it, and keeps both sets of search domains. It answers .ray and declines everything else, so your system resolver moves straight on to the other VPN's server, whose own DNS setup then applies as usual. Both meshes resolve, in either start order, and it holds whichever way the two write the file. Rayfish rewrites at most once a minute, so the two daemons can't spin against each other, and once the other VPN is gone it goes back to managing the file alone.

Shutting rayfish down removes only its own lines and leaves the other VPN's DNS as it found it. dig @200::53 <host>.ray answers throughout, since Magic DNS is reached through the tunnel and not through resolv.conf. (Use @200::53, not the v4 100.100.100.53: a host sharing the CGNAT range with another VPN is in IPv6-only mode, where the v4 magic address is exactly what the mode gives up.)

With resolvconf in the path both VPNs do get to register, but the system tries the resolvers in order and stops at the first that answers, so whichever sorts second never sees its own names. rayfish says which resolver is ahead of it in the log if that happens; fix it by giving rayfish's stanza priority in resolvconf's interface order.

On Android the same mode is a switch under You > IPv6-only mode. Two VPNs can't run at once there, so the reason to reach for it is a carrier that gives the phone a 100.64.x.x address of its own. Flipping it reconnects.

Troubleshooting

ray logs                 # today's daemon log, in your pager
ray logs --since 2h30m   # just the last two and a half hours
ray logs -f              # keep streaming new lines, like tail -f
ray report               # bundle logs + metrics, open a pre-filled GitHub issue

The daemon writes rolling logs to /var/log/rayfish/ (Linux) or /Library/Logs/rayfish/ (macOS). They're root-owned, so ray logs reads them through the daemon rather than off disk: no sudo, and it pages through $PAGER on a terminal but writes straight through when piped or following (ray logs | grep peer).

ray report collects those logs, current metrics, and a sanitized status snapshot (no private keys) into a .tgz, then opens a pre-filled GitHub issue for you to attach. The bundle is written locally first, so you can review it before sharing.

How it compares

Rayfish sits closest to Tailscale, but without a coordination server: there's no account, no control plane, and nothing to self-host. The network's signed record on a public DHT is the only shared state. Unlike raw WireGuard, you don't hand-manage keys, IPs, or peer configs. Unlike Nebula, there's no certificate authority to run; identity is the key.

Status

Rayfish is experimental, pre-1.0 software and has not had an independent security audit. The wire format and on-disk config may still change between releases. Please file issues, but don't rely on it for anything critical yet.

Changelog

See CHANGELOG.md for the full history, or the releases page for per-version notes. ray update --list shows available releases and ray update --check reports what a pending upgrade brings.

Building

cargo build                  # debug build
cargo build --features tor   # optional Tor transport
cargo build --features otel  # optional OTLP span export

Requires the Rust 2024 edition (Rust 1.85+).

FAQ

"You use LLMs? 🤯"

Yes. Heavily. Claude and GLM-5.2 wrote a lot of this repo.

"So it's all vibe-coded slop."

The idea is over 4 years old: a trustless, decentralized network with no central entity that can censor who you connect with. I sketched it for years. What an LLM changed was the speed: the prototype came together in a day. The trigger was the release of iroh v1.

"Is it actually trustless and serverless?"

Yes. By using iroh.

☝️🤓 actually, it is wrong to describe these P2P products as server-less. In order to connect two peers over WAN it needs a form of coordination server.

Who cares? To connect to the internet you need to go to through your ISP, the FBI, the NSA, the IRS, and any other 3 letter agency you can imagine in the world. But you care about n0 computer's servers... Sure.

You can also host your own relay and pkarr servers. Check ansible-iroh. Your data is encrypted anyway. You own your identity.

"Is rayfish production ready?"

NO. Don't use it in your company (yet!). Don't ditch tailscale or anything else to use it.

Use it in your homelab, with friends on a discord server or just to connect your android (in very alpha stage) and your laptop.

Contributing & security

See CONTRIBUTING.md for the development workflow and SECURITY.md to report vulnerabilities privately.

License

Rayfish is licensed under the Mozilla Public License 2.0 (MPL-2.0).

Read the original on github.com ↗