Skip to content

release: netcode.rs 1.1.2 - #28

Merged
rowan-claude merged 1 commit into
mainfrom
rowan/release-next
Sep 14, 2026
Merged

rowan-claude merged 1 commit into
mainfrom
rowan/release-next

Conversation

@rowan-claude

Copy link
Copy Markdown
Contributor

Release preparation for netcode.rs 1.1.2, the connect-token reuse fix from #27 (fixes #24). No code change beyond the version bump. Do not merge until the notes below are edited to Glenn's satisfaction; tagging, the GitHub release and the crates.io publish are all separate and deliberate steps after this.

Version chosen: 1.1.2

The crate numbers itself; it does not track the C reference's release number. The deciding sentence is in CLAUDE.md's PUBLISHING block:

Verified 2026-07-26: crates.io has netcode-official 1.0.0 while this repo is at 1.1.0.

At that date the C reference was already on 1.4.x, so the crate's own line (1.0.0 published, 1.1.0 in tree) is independent of it. The HOT block's first sentence ("from-scratch Rust port of netcode 1.02") names the wire protocol version, not a release number, and the C reference this now matches is 1.4.8. The predecessor releases are v1.1.1 (2026-08-08) and v1.1.0, so the next patch is 1.1.2. netcode.go did the same for the same fix: it tagged v1.1.4, not v1.4.8.

Version sites

Site Before After
Cargo.toml version 1.1.1 1.1.2

That is the only one. Three candidate sites were checked and deliberately left alone:

  • Cargo.lock — not bumped, and must not be added. CLAUDE.md has a block headed "CARGO.LOCK IS GITIGNORED HERE ON PURPOSE": .gitignore lists it by decision, this is a library crate, and "Do not 'fix' the inconsistency by adding a lock here". There is no lock in the tree to update.
  • src/lib.rs VERSION_INFO — not touched. pub(crate) const VERSION_INFO: [u8; 13] = *b"NETCODE 1.02\0" is the wire protocol version written into every unencrypted packet header and mixed into the AEAD associated data. It is asserted byte-for-byte by wire_compat.rs::version_info_matches_c and by the golden vectors, and C 1.4.8 still sends NETCODE 1.02. Moving it to "1.4.8" would break every other netcode implementation. netcode.go draws the same line explicitly: its versionInfo "never changes", while its separate VersionFull constant carries the C release number.
  • A C-reference version constant — this port does not have one. netcode.go has VersionFull/VersionMajor/VersionMinor/VersionPatch reporting 1.4.8; grep over src/, tests/, examples/, README.md and Cargo.toml finds no equivalent here, so there is nothing to set to 1.4.8. Adding one would be a new public API in a patch release and is out of scope for this PR — worth a follow-up issue if we want the ports to match.

README.md carries no version string.

Gates

Run locally on this branch at 7a4d7a0, as .github/workflows/ci.yml runs them:

$ cargo fmt --check
(no output)

$ cargo clippy --all-targets -- -D warnings
    Checking netcode-official v1.1.2 (…/netcode.rs)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.41s

$ cargo build --all-targets
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.58s

$ cargo test
test result: ok. 56 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.42s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

$ RUSTDOCFLAGS="-D warnings" cargo doc --no-deps
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.33s

$ rustup run 1.85 cargo check          # MSRV
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.85s

$ NETCODE_C_SERVER=… NETCODE_C_CLIENT=… \
    cargo test --test c_interop -- --ignored --test-threads=1 --nocapture
test c_client_connects_to_rust_server ... ok
test rust_client_connects_to_c_server ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 17.80s

The 2 ignored tests in the cargo test run are the interop pair, which is exactly the trap CLAUDE.md names; they are run separately above. The C client and server were built with CMake from mas-bandwidth/netcode at tip 47a156b, which is tagged v1.4.8 — so the live interop leg is against the same C release whose connect-token lifecycle #27 ported.

Not run locally: cargo deny and the fuzz smoke leg. CI covers both.

Release notes (draft, Rowan edits)

netcode.rs 1.1.2: a connect token cannot be reused after its client disconnects

A connect token now encrypts exactly one session. Before this release the Rust server admitted a token again once its client had gone away, as long as the request came from the same address the token first arrived from. A second session under the same token repeats AEAD nonces under the same keys, which is the flaw published against the C library as GHSA-v29p-3vj4-vg4f and fixed there in netcode 1.4.5. This port carried the pre-1.4.5 behaviour until now; it is brought up to the connect-token lifecycle of the C reference at netcode 1.4.8, the version it is tested against.

What changed. The server's token history tracks each entry as pending or consumed. While an entry is pending, retransmitted connection requests from the token's original address are admitted, so an ordinary handshake that loses a packet still completes. The entry is consumed at the moment the client is installed in a slot, and a consumed token is refused from every address. A full history refuses new tokens instead of evicting an unexpired entry, so a flood of tokens can no longer make room for a spent one. Entries are reclaimed only after their tokens expire. On top of that, a server that has just started refuses any token whose expiry predates the start, so a restart does not reopen tokens issued to the process that came before.

What operators need to do. The restart guard is measured against a configured maximum connect-token lifetime, 30 seconds by default. That setting must be at least as long as the longest lifetime your backend issues. If it is shorter, tokens your backend legitimately issued can survive a restart unrefused and the guard is unsafe. If it is longer, nothing is unsafe: the server simply waits longer after a restart before it will admit the oldest tokens. Set it with the new ServerConfig and Server::new_with_config:

let config = netcode::ServerConfig { max_connect_token_lifetime: 60 };
let mut server = netcode::Server::new_with_config(
    server_address, protocol_id, &private_key, config, time,
).expect("failed to create server");

Server::new is unchanged and keeps the 30 second default.

Nothing on the wire changes. No packet layout, field or byte differs from the previous release; the golden vector tests and live interoperability against the C reference client and server pass unchanged. A client does not need updating, and mixed fleets of old and new servers interoperate.

The finding was reported as netcode.rs#24 and tracked as security#39; the same class of fix shipped for the Go port as netcode.go 1.1.4.

One thing to check before this goes out, Glenn: the notes say the C fix landed in 1.4.5, not 1.4.8. That is what issue #24 and the GHSA say (C netcode 1.4.5 … spends a connect token when the client is installed), and the C v1.4.8 release notes are about libsodium nonnull attributes, not tokens. 1.4.8 is the C release this port is now tested and aligned against, which is how netcode.go's notes use it too. If you would rather the notes name only 1.4.8, say so and I will reword.

🤖 Generated with Claude Code

Bump the crate version for the connect-token reuse fix (#27, fixes #24).
The crate numbers itself independently of the C reference, so this is 1.1.2
and not 1.4.8. Cargo.toml is the only version site: Cargo.lock is gitignored
here on purpose, and VERSION_INFO ("NETCODE 1.02") is the wire protocol
version, not a release number, so it does not move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rowan-claude

Copy link
Copy Markdown
Contributor Author

Merge on the record: macOS legs substituted by local macOS runs

At this head every hosted check passes except the macOS leg(s), queued at zero seconds on GitHub's macOS runner pool. Glenn, live at 00:03Z: "Same for the other five, local runs on the record."

Substitute, run on the Studio (macOS) at this exact head by the cold readers: cargo fmt --check, clippy -D warnings, cargo test 56+10+1 passed, cargo doc -D warnings, MSRV 1.85 check, C interop 2/2 against C v1.4.8 (the release-prep builder at 7a4d7a0). Reads: Rowan's own read of the one-line diff (Cargo.toml 1.1.1 to 1.1.2); the code it releases was read by Fable, Opus and Alex at 2404f58.

An exception named per Glenn's flexibility rule, not the path; the hosted macOS legs run on main after the merge and are read back there.

@rowan-claude
rowan-claude merged commit 25d97cc into main Sep 14, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port still has the pre-1.4.5 connect-token reuse (GHSA-v29p-3vj4-vg4f)

1 participant