Repository navigation
feat: keel mesh invite and join --dry-run, the keel1: token (0048) - #72
Merged
Merged
Conversation
added 2 commits
October 3, 2026 13:30
…efuses Spec validation compared peer keys as text, for this node's own key and for a key declared twice. Both now go through one helper, wireguard.key_bytes and same_key. A key whose last base64 character sets the two spare bits decodes to the same bytes in Python but is refused by wg pubkey; key_bytes refuses that spelling too, so is_key no longer takes a key wg-quick would reject.
First part of handbook decision 0048, joining the mesh with one command. keel mesh invite prints keel mesh join keel1:<token>, valid one hour and once, and reserves the new node's overlay address, a random free one of the prefix (secrets), so two members inviting at once before etcd exists almost never collide, in a pending invite under /var/lib/keel/mesh (0700 directories, 0600 files, atomic writes, one lock). The invite keeps an HMAC key derived from the token's secret, never the secret, and the invite's own certificate, whose SHA-256 the token carries for pinning. Consuming an invite marks it used and keeps its address reserved until it expires. keel mesh join TOKEN|- --dry-run checks the token (length, prefix, checksum, layout, values, expiry) and prints the address and peer entry it makes in the spec, validated as the spec would be after the join; it applies nothing. The token already carries the HTTPS port, the mesh identity and the etcd state, so the listener, the join, accept, create and remove need no format change. Removing expired invites at boot comes with the listener. New exit codes 23 and 24. docs/mesh.md. keel 0.16.0.
marcos-mendez
force-pushed
the
feat/mesh-token
branch
from
October 3, 2026 13:38
23ae216 to
16a26f8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First part of handbook decision 0048 (Keel-Linux/handbook#42), "joining the mesh with one command": the
keel1:token, the address allocator, the pending-invite store, andkeel mesh invite/keel mesh join --dry-run. The HTTPS listener, the real join,accept,createandremovecome in the next PRs; the token and the store are laid out so they need no format change.Seams under test
Written down before the tests; the tests exercise these and nothing behind them.
keel.mesh.token):encode(Token) -> strandparse(text, now) -> Token, raisingTokenError. Tested by round trip and by feedingparsedamaged, truncated, unknown-version, inconsistent and expired tokens.repr(Token)and every error message are checked not to hold the secret.keel.mesh.allocate):free_address(own, taken, randbelow) -> str, raisingAllocationErrorwhen the prefix is full;randbelowis injected so the tests are deterministic.keel.mesh.invites):reserve(root, now, make),find(root, id, now),consume(root, id, now, verify),expire(root, now), against a scratch root: modes, single use, expiry, no secret on disk.keel mesh invite,keel mesh join --dry-run, throughkeel.cli.main): the printed line, the YAML change, the exit codes, the state written or not.Not tested on their own: the byte layout, the lock, the checksum function; they are behind seam 1 and 3.
What it does
keel1:token (keel/mesh/token.py): fixed big-endian layout, base64url without padding, 4-byte SHA-256 checksum over prefix and fields; input over 1 KiB is refused before decoding. Carries the inviter's key, endpoints (IPv6 first, IPv4 optional, at least one), WireGuard port, HTTPS port, certificate fingerprint, the inviter's overlay address and prefix length, the assigned address, the mesh identity, the etcd state (and port when running), the invite id (derived from the secret and checked against it), the 256-bit secret and the expiry. 250 characters with one IPv6 endpoint, 256 with both.encoderefuses whatparsewould refuse. The secret isrepr=Falseand no error message holds it or the token text.keel/mesh/allocate.py): a random free address of the prefix (secrets), avoiding the node's own, the peers'allowed_ips(whole prefixes), pending invites and the subnet-router anycast; redraws on a collision, and after 16 searches from the last draw so a nearly full prefix still gives its last address and a full one is refused. Random so two members inviting before etcd exists almost never collide.keel/mesh/invites.py):/var/lib/keel/mesh/invites/<id>.json, 0600 in 0700 directories (re-tightened if widened), written throughmarker.write_privateand under a flock as/var/lib/keel/networkis. Holds the address, expiry, HTTPS port, the invite's certificate and TLS key, andhmac_key(secret), never the secret.reserveallocates and writes in one lock;consumetakes averifycallback so the listener can check the HMAC and spend the invite in one step; a consumed invite is marked used and keeps its address reserved until it expires. Removing invites that expired while the machine was down, at boot, comes with the listener.keel mesh invite: reads the key withwg pubkey, the endpoints from--endpointor the static addresses ofnetwork.interfaces, makes the invite's certificate withopenssl req -x509, makes the mesh identity once (/var/lib/keel/mesh/identity), reserves the address and prints the line alone on stdout.--portsets the HTTPS port (default 51820).keel mesh join TOKEN|- --dry-run: prints theaddressand the peer entry as YAML, refuses a node in another mesh or at another address of it, and validates the spec as it would be after the join. Without--dry-runit exits 9.MESH_TOKEN_INVALIDand 24MESH_REFUSED. keel 0.16.0.docs/mesh.md, linked fromdocs/spec.md, README.The two LOW findings
validate_overlaynow compares keys throughwireguard.key_bytes/same_key(own key, and the duplicate-key check). While checking it I found thatwg pubkeyrefuses a key whose last base64 character sets the two spare bits, whichis_keyaccepted;key_bytesnow refuses that spelling too, so validation no longer accepts a key wg-quick would reject.Points of 0048 read one way here
allowed_ips, in the fallback above all.parsechecks that they agree.hmac_key(secret)only.ipv4_addressis not carried; docs/mesh.md says so.Tests
Full suite: 2448 passed, 4 skipped; coverage 99 %,
keel/mesh100 %. CI installs wireguard-tools, so the invite tests run the realwgandopenssl.