Skip to content

This page may contain unreviewed machine generated text. Use with care.

Pages

Control socket

Asking a running node questions, and the verbs that act on one.

--control /var/run/ranet3/control.sock is where the daemon answers, and is the default, so a node is askable without having been configured to be. --control "" turns it off. The socket is mode 0660 in the daemon's group, which the unit names, and that mode authorizes every path outside /v0/debug/. The verbs say what keeps it sufficient for them.

The debug paths are authorized by the caller's peer credentials instead. Their snapshots and the event stream are open to whoever can open the socket. debug probe, the only action among them, takes root or the daemon's own user. The daemon's --debug-access changes this. group opens the probe to whoever can open the socket, and off refuses every debug path, as debugging describes.

The daemon bounds each connection by an idle timeout. It also bounds the connections open at once across every caller. Each connection costs a descriptor, and a node out of descriptors cannot answer anyone. Past the limit, the daemon closes a new connection as it accepts it. That client learns at once that it was refused, and a shutdown never waits for a free place. Limits gives the values.

Socket directory

The socket takes the daemon's group on every platform. Each directory the daemon makes on the way to the socket gets the same group and mode 0750. Linux gives a new file the group of the process that makes it, outside a setgid directory. macOS gives a new file the group of its directory, which under /var/run is daemon. The daemon sets the group before the mode lets that group in.

On macOS, the record that link.underlay.bind keeps shares the default socket's directory, /var/run/ranet3. Whichever of the 2 the daemon writes first makes the directory.

A directory that already exists keeps its group and mode. On macOS, a directory in the group daemon keeps the socket out of reach of the daemon's group. Stop the daemon, give the directory the daemon's group with chgrp, and start the daemon again. A reboot works as well, because it clears /var/run and the daemon makes the directory again.

Removing the directory instead also removes the record of the routes link.underlay.bind wrote, and that record is the only way the daemon knows to withdraw them. Running it as a service gives the commands for the nix-darwin job. Under the NixOS module, systemd makes /run/ranet3 in the unit's group and removes it when the unit stops.

Who owns the path

An exclusive lock on a sibling file decides which process owns the path. The lock file is the socket's path with .lock appended. The daemon does not dial the socket to see whether anything answers. A live daemon out of descriptors fails to answer, as does one whose socket the caller cannot open, and both still own their path.

Under the lock, the daemon removes a socket a dead instance left behind. It refuses a path that is not a socket, naming it. A second daemon on the same path is refused, because the first holds the lock. A path named with --control that the daemon cannot bind refuses the startup. The default path warns and carries on instead, because it is on without having been asked for.

Version

The version a node reports is version.txt and the commit the binary was built from. ranet3 version reports the binary, without a node running. ranet3 version --daemon reports the running node, which tells the 2 apart after an upgrade.

Reading a node

The read subcommands print a table, or the wire form with --json. metrics prints the scrape, and bugreport prints JSON alone. Command line lists every command and its flags.

$ ranet3 status
Node        example/laptop
Version     2026.912.0+860393bf1c2e
Uptime      3h12m0s
Port        13000
Endpoints   0/ip6 1/ip4
TUN         ranet0 MTU 1400 queues 16
Role        initiator, responder, full mesh, no transit
Forwarding  IPv4 on IPv6 on
Registry    /etc/ranet/registry.json, 6 nodes in 2 organizations
Kernel      table 200 protocol 155, 14 installed, last pass 12 seconds ago
Egress      off, this node carries no traffic for others
Border      nftables table inet ranet3 chain border, inside ranet0 lo, 3 overlay prefixes, 1 SID, dropped 2 overlay sources, 0 routing headers, 0 SID destinations
Dialers     5 running
Sessions    2
Neighbors   2, 2 alive
Routes      14 prefixes, 11 selected, 3 originated
Originate   203.0.113.10/32 3fff:a::203:0:113:10/128 3fff:b:0:100::/60
Segments    3fff:b:0:106::1 End.DT46 (0 forwarded, 5 delivered, 0 dropped)
Steering    from 3fff:a::203:0:113:10/128 via 3fff:b:0:206::1 (12 steered, 2 with no route to their first segment)
Transit     0 forwarded, 5 delivered, 4 dropped (3 multicast, 1 forged)
ESP         41822931 in, 0 dropped, 14 refused

$ ranet3 neighbors
Peer                 State  Cost  Extra  Rx cost  RTT      Routes  Expires  Dropped  Failed
example/gateway:0@0  up     116   0      96       27.2ms   7       11.3s    0        0
other/relay:2@1      up     1218  1024   96       176.7ms  5       9.8s     0        0

$ ranet3 routes
Destination        From         Via                  Metric  Router ID         Seqno  Paths
::/0               3fff:a::/36  example/gateway:0@0  212     0a1b2c3d4e5f6071  42     2
3fff:b:0:100::/60               this node            0                         0      0

neighbors is the counterpart of birdc show babel neighbors. It shows the neighbor's reported Rx cost beside this node's cost, which tells a link that carries 1 way from one that carries neither. Extra is the part of the cost cap.babel.cost.extra charges the neighbor, which this node's IHUs add to the receive cost they report as well. It also shows 2 dataplane counters BIRD lacks.

routes is the counterpart of birdc show route. It reads the Babel route table, not the forwarding table, so a prefix every neighbor has retracted still shows as a row. A prefix this node announces and an address cap.table assigns it show as rows via this node. sessions is the counterpart of swanctl --list-sas. peers lists who this node dials, from the config file or from the trust document under dial.all, and whether it got there.

Naming a peer

A node is named organization/name. Each of its endpoints is named organization/name:serial, as configuration describes. status shows this node's own name in that form.

NameExampleNames
nodeexample/gatewaya node
endpointexample/gateway:01 endpoint of a node
sessionexample/gateway:0@0the session between the peer's endpoint and this node's endpoint 0, whichever end dialed
dialerexample/gateway@0the dialer of a node with no endpoint pinned, from this node's endpoint 0

peers shows a peer by the endpoint dial.to pins, or by its node. The session of a dialer with no endpoint pinned is named by the endpoint that answered.

  • rekey, debug probe and debug pmtu take a session by its name, its peer's endpoint, that endpoint's node, or the node's name alone. The name alone takes that name in every organization. A dialer's name matches only a session of exactly that name. To take the sessions of a dialer with no endpoint pinned, name their node.
  • redial takes the same names and a dialer by its name. The name of an endpoint or a session also wakes the dialer of that node with no endpoint pinned.
  • debug events --peer takes the same names as rekey and a dialer's name. A dialer's name selects a dial's events until its endpoint resolves.

A name matches whole. example/gateway takes no session of example/gateway2. A name with a serial and no organization, such as gateway:1, names nothing. For redial, rekey and debug probe, completion offers the peers the daemon dials and the nodes it holds a session with. For debug pmtu, it offers the nodes of its sessions. For debug events --peer, it offers the names the recorded events carry.

Questions

4 more commands answer questions instead of printing 1 subsystem:

  • whois <address> is a longest prefix match over the Babel route table. It prints the prefix that covers the address and the peer this node reaches it through, then what the address would fall back to. It names the peer, not the originating node, because Babel carries a router id and no name, and ranet3 gives each speaker a random router id. A prefix of this node's own is named as originated by this node, or as assigned to it for an address cap.table assigns.
  • ip prints this node's own mesh addresses, 1 per line, taken from the host prefixes it announces.
  • exit list prints the defaults the mesh advertises, with the 2 /1 halves of a family that 1 node advertises together, and which of them this node would take. It tells a withdrawn default from one that carries traffic.
  • bugreport prints every subsystem in 1 JSON object. A read that did not answer leaves its own line in the report, so a node that is half up still produces one.

metrics prints the same scrape the metrics listener serves, read over the control socket, so a node's counters are readable without it binding a port a fleet then has to firewall.

Endpoints

Every path starts with /v0. A read answers GET and HEAD, and a verb answers POST. The class says who may call a path. mode is anyone the socket's mode lets in. read and root are the debug classes described above.

PathMethodClassRequestAnswer
/v0/statusGET, HEADmodenonethe node, a JSON object
/v0/neighborsGET, HEADmodenoneBabel neighbors, a JSON list
/v0/routesGET, HEADmodenoneBabel routes, a JSON list
/v0/sessionsGET, HEADmodenoneIKE sessions, a JSON list
/v0/peersGET, HEADmodenonedialed peers, a JSON list
/v0/metricsGET, HEADmodenonethe Prometheus text format
/v0/disablePOSTmodesubsystema result
/v0/enablePOSTmodesubsystema result
/v0/redialPOSTmodepeer or alla result
/v0/rekeyPOSTmodepeer or alla result
/v0/reloadPOSTmodeempty bodya result
/v0/downPOSTmodeempty bodya result
/v0/upPOSTmodeempty bodya result
/v0/debug/eventsGET, HEADreadquery kind, peer, attr, since, followevents, 1 JSON object per line
/v0/debug/pmtuGET, HEADreadquery peerpath MTU view, a JSON object
/v0/debug/runtimeGET, HEADreadnoneprocess view, a JSON object
/v0/debug/updownGET, HEADreadnoneup and down view, a JSON object
/v0/debug/netmonGET, HEADreadnonenetwork watch view, a JSON object
/v0/debug/rulesGET, HEADreadnonerules view, a JSON object
/v0/debug/probePOSTrootpeer or alla result

Requests and answers

A verb's body is a JSON object with the fields subsystem, peer and all. Each verb reads the fields it needs and ignores the others. An empty body is the empty request, which reload, down and up take. A verb answers with a result. Its acted lists the subsystems or paths the verb reached, its missed each path the verb did not reach, by entry and reason, and its detail is the sentence the command prints. A command whose answer holds a missed exits 1 once it has printed the answer.

The reads answer JSON. A duration is a string such as "4s". A time follows RFC 3339. An address or a prefix is its text form. An optional field with no value is left out, such as down on a node that is up or err after a pass that succeeded. endpoints and originate are always present and read [] when empty, and every counter is always present. Other lists, such as segments, disabled, taken, acted and missed, are left out when empty.

AnswerFields
statusversion, node, port, endpoints (serial/family), tun, mtu, queues, started_at, uptime, responder (link.listen is on), full_mesh (dial.all is on), no_transit (cap.transit is off), forwards_ipv4, forwards_ipv6, registry, kernel, egress, border (absent without cap.border), counts, esp, originate, segments, steering, segment_counters, transit, disabled, down
registrypath, organizations, nodes, read_at
kernelenabled, where, table, protocol, metric, pass_at, installed, skipped, added, removed, cover, taken (each lookup another program's rule takes, as sharing a host describes: family, then prefix and the address asked inside it for an overlay prefix or endpoint for the underlay, and answer), err
egressenabled, where, ready (ipv4, ipv6, the families the last pass judged ready, by the route lookups where they ran and, on a node with egress segments, while the border filter reads back), unready (why a family it translates is not ready, such as a lookup that failed or left by another device, with the sysctls that explain it, or an address of its classes the host does not hold), skipped (the lookups the pass left out and why), translate (each class's from and to, none, auto or an address), return, advertise, announced, flows_per_source (the share in force), refused (ipv4, ipv6, the packets refused past it as of the last pass, from zero after a pass that rewrote the chains), dropped (ipv4, ipv6, the packets dropped with no tracked flow, out of the mesh under a class that translates and into it under return, counted as refused is), pass_at, installed, flows, bytes, untranslated (the connections a class of none left under their source), conntrack_count and conntrack_max (the host's connection table, each left out where the pass could not read it), conflicts, err
borderwhere, inside, overlay, sids, ready, unready, pass_at, installed, dropped (overlay_source_ipv4, overlay_source_ipv6, routing_header, sid_destination, each in packets as of the last pass), err
countsdialers, sessions, neighbors, neighbors_alive, prefixes, selected, originated
espinbound_packets, inbound_dropped, receive_dropped, receive_refused, keepalives
originatea list of prefix and from
segment_countersforwarded, delivered, dropped, steered, unsteered, unrouted, answered, egressed, refused, not_ready
transitforwarded, delivered, expired, no_route, held, refused, invalid, martian, source_route, unrouted_source, not_ready, too_big, multicast, forged, routing_header, sid, unowned_source, nested, looped, answered, unanswered, as metrics counts them
downsince, for
neighborpeer, address, alive, cost, extra, reported_cost (absent until an IHU arrives), rtt (absent until a timestamped IHU arrives), routes, expires, dropped, send_failed
routedestination, from, via (absent for a prefix held unreachable and for this node's own), metric, router_id, seqno, candidates, originated (this node announces it), assigned (an address cap.table assigns, announced only where originated is set too)
sessionpath, peer (the peer's node), remote (address and port), preferred, active, local_spi, remote_spi, ike_cipher, ike_prf and ike_kex (the IKE SA's transforms by their cap.crypto names, ike_kex its whole kex entry, such as x25519+mlkem768), esp_cipher and esp_kex (the Child SA's, esp_kex the kex entry of its own key exchanges, absent for a Child SA keyed from the IKE SA alone), ike_fragments (whether both ends said they support the IKE fragmentation of RFC 7383) and, where they did, ike_fragment_threshold (the path's threshold, the largest IP datagram this node cuts a new IKE message to fit, its family's floor from a step that cut a request down to it, taken on the path the request was readied or last cut on, which every send of it went on since, the step's own included, until a send or this read finds that the peer moved or that the path MTU the host reads changed, or the session is asked to probe its path, and which an answer cut within the datagrams of the request or repeat it answers goes below), started_at, age, idle
peerpath, peer, connected, generated (taken from the trust document under dial.all)
resultacted, missed (entry, reason), detail

Status codes

CodeWhen
200The read or the verb succeeded.
400The node refused a verb, a body or query does not parse, or a query names more kinds or attributes than the limits allow. The body says why.
403The caller may not use this debug path. The body names the rule and the caller's uid.
404No such path. Under --debug-access off, an unknown debug path answers 403.
405The method does not fit the path. Allow names the one that does.
500The daemon could not encode its answer. The body says why.
501The program serving the socket does not implement that verb or debug view. The daemon implements them all.
503A 5th event stream asked to follow while 4 are open.

Limits

LimitValue
Connections open at once32, and the daemon closes the next one as it accepts it
Idle connection between requestsclosed after 30 seconds
Request headers5 seconds
Writing an answer, a stream excepted30 seconds
Verb body the daemon reads64 KiB
Event streams open at once4
1 stream write waiting on its reader5 seconds, then the stream ends
Heartbeat on a quiet streamevery 15 seconds
Kinds in 1 events query16
Attributes in 1 events query16
Socket path103 bytes
1 exchange of ranet3 with the daemon, a stream excepted10 seconds
Wait of rekey for the rekeys it asked for5 seconds
Answer ranet3 reads64 MiB
1 stream line ranet3 reads1 MiB
Events in 1 stream write64

Acting on a running node

7 subcommands change what a node is doing right now. disable and enable stop and start a subsystem, which is reconciler, steering or responder. redial and rekey take a peer or --all. rekey asks each session to replace its Child SA, all at once, and answers when every rekey has ended, after 5 seconds at most, or once its caller has gone away. Its acted lists the sessions that rekeyed, and its missed each that did not with the error the session's rekey gave, such as the peer's refusal, a rekey already under way, a rekey still running at 5 seconds, or a failed Child SA rekey whose replacement waits for the retry the session has scheduled, as cap.crypto.rekey.retry sets. A rekey still running goes on, and ike.rekey.completed or ike.rekey.failed tells how it ended. reload does what SIGHUP does, so a supervisor is not the only way to ask. down takes the node off the mesh and up puts it back, as taking a node off the mesh describes. debug probe acts as well, under the debug rules above.

None of them changes the configuration. A node's configuration is its file, and that stays its only entry point. What these act on is operational state the file already decides: cap.table, cap.segment's steering, link.listen, the peers list and the file itself. down asks for no more than a file without peers, link.listen, cap.table or cap.egress would. Whoever may edit that file could already ask for every one of them, by editing and restarting or by editing and sending SIGHUP. The socket's mode therefore grants nothing the file's permissions did not. A verb that reached past what the file can express would break that, and is refused on those grounds. The subsystem names are a closed set for the same reason. The 2 halves are told apart by method rather than by path: a read answers GET and HEAD and nothing else, and a verb answers POST alone.

The state lives in the process, apart from the flag down leaves. A restart starts every subsystem the file names. A reload leaves a stopped subsystem stopped. The trust document changes whenever a node joins, and a reload that started a subsystem again would undo an operator's decision.

A node running less than its file says reports it on the Disabled line of ranet3 status, and in the disabled field of its JSON. No other read shows that difference.

disable and enable refuse a name outside those 3. They also refuse a subsystem the file never turned on:

  • reconciler on a node without cap.table
  • steering where cap.segment names no steer policy
  • responder while link.listen is off

A subsystem already in the state asked for is reported as such, and the command succeeds. enable is refused while the node is down.

Each verb acts as follows:

  • disable reconciler withdraws every route, address and rule the reconciler installed, as birdc disable does to a kernel protocol, instead of freezing a table nobody maintains. A withdrawal the kernel refuses in part is retried on the backoff of a failed pass until it completes. The Kernel line of ranet3 status carries its error until then.
  • disable steering leaves the policies loaded and stops acting on them, so a diagnostic still reports what was stopped.
  • disable responder refuses the next handshake and leaves the sessions this node already holds alone.
  • redial is for a peer that dials this node and cannot be dialed back. Such a peer does not retry on its own. redial drops what this node holds for that peer and sets its dialers going at once, instead of after the reconnect delay.
  • redial --all does the same for every session and every dialer. It is for a network change the node's watch cannot see, such as a captive portal that changes no address or route.

redial, rekey and debug probe refuse a call that names a peer and --all, and a call that names neither.

reload re-reads the file the daemon was started with. When the daemon refuses the reload, the command prints the refusal and exits 1. After SIGHUP, the same refusal goes to the log and to the err of the daemon.reload event. Reloading says what a reload applies.

Taking a node off the mesh

ranet3 down takes a node off the mesh and leaves its daemon running. Every dialer stops and no new one starts, the responder stops answering IKE where link.listen is on, every session is closed with an IKE Delete, and the Babel speaker stops. Where cap.table is set, the reconciler withdraws the routes, rules and tun addresses it installed, and on an exit node the translator withdraws its tables and its advertisement. The daemon, the tun, the control socket, the metrics and the debug views stay, and the node still answers every read. A ranet3 peer drops its routes through the node when the Delete arrives, rather than when the Babel neighbor would have timed out.

down writes the flag down in the daemon's state directory before it acts, holding the moment the node went down, and a daemon that starts with the flag there comes up down: its tun, control socket and metrics open, and nothing else of the mesh starts. A node taken off the mesh to be worked on stays off through the restarts and reboots the work brings, which makes down the only verb whose state outlives the process. --state-dir names the directory. It defaults to /var/lib/ranet3 on Linux and /var/db/ranet3 on macOS, and the daemon makes it at mode 0700 when it is missing. The flag is written beside its final name, synced and renamed into place, and the directory is synced after it, so a crash leaves the flag as it was or as asked.

ranet3 up removes the flag before it acts. It then puts back the addresses, rules and routes as the reconciler does at start, and on an exit node the translator's tables and advertisement. It starts every dialer, the responder and the speaker. A subsystem somebody disabled stays disabled. A down on a node that is down and an up on one that is up succeed and say so. While the node is down, redial, rekey, enable and debug probe are refused with an error saying it is down. A reload applies the file and the trust document and leaves the node down, and a peer the reload adds is dialed after up.

$ ranet3 down
This node is off the mesh with 2 session(s) closed, and it stays off through a restart until ranet3 up
  example/bravo:0@0
  example/charlie:0@0

$ ranet3 status
Node        example/gateway
Down        since 2026-10-12 21:04:05 UTC, 3 minutes ago, until ranet3 up
Version     2026.912.0+860393bf1c2e
Uptime      26h0m0s
Port        13000
Endpoints   0/ip4 1/ip6
TUN         ranet3 MTU 1400 queues 4
Role        initiator, responder, transit
Forwarding  IPv4 on IPv6 on
Registry    /etc/ranet3/registry.json, 12 nodes in 1 organizations
Kernel      table 100 protocol 155, 0 installed, last pass 3 minutes ago
Egress      off, this node carries no traffic for others
Dialers     0 running
Sessions    0
Neighbors   0, 0 alive
Routes      0 prefixes, 0 selected, 1 originated
Originate   2001:db8:a::/48
Segments    off
Steering    off
Transit     0 forwarded, 0 delivered, 0 dropped
ESP         0 in, 0 dropped, 0 refused

$ ranet3 up
This node is back on the mesh

ranet3 status carries the Down line under the node's name for as long as the node is down. ranet3 debug updown adds where the flag is and whether it is there. It also shows the last move between up and down with its cause, and the Babel router id and seqno in use, with the seqno limit babel.json holds. Each move is recorded as a node.down or node.up event, as debugging describes.

The state directory also holds babel.json, the node's Babel router id and a seqno limit, which a restart starts from, as running it as a service describes. A down and an up keep the router id, and the speaker the up starts sends a seqno past every one it sent before the down.

ranet3 has no login flow. A node's identity is its private key and its entry in the trust document, both in place before the daemon starts.

Updated