mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 01:27:32 +00:00
Merge branch 'maint'
# Conflicts: # CHANGELOG.md # src/bin/fips.rs
This commit is contained in:
@@ -138,15 +138,92 @@ jobs:
|
||||
run: cargo install cargo-zigbuild --version 0.19.8 --locked
|
||||
|
||||
- name: Install zig (required by cargo-zigbuild)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ZIG_VERSION="0.13.0"
|
||||
# Each arch carries the expected SHA-256 of its upstream tarball,
|
||||
# taken from ziglang.org's own https://ziglang.org/download/index.json,
|
||||
# field .["<version>"]["<arch>-linux"].shasum:
|
||||
# jq -r '.["0.13.0"]["x86_64-linux"].shasum' index.json
|
||||
# Upstream publishes no .sha256 sidecar and no SHA256SUMS, so
|
||||
# index.json is the only checksum document offered, and it lists only
|
||||
# recent releases: once a version ages out of it the pin can no longer
|
||||
# be re-derived upstream. Bumping ZIG_VERSION means replacing every
|
||||
# hash below, and adding an arch means adding its hash here too.
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ZIG_ARCH="x86_64" ;;
|
||||
aarch64|arm64) ZIG_ARCH="aarch64" ;;
|
||||
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||
x86_64|amd64)
|
||||
ZIG_ARCH="x86_64"
|
||||
ZIG_SHA256="d45312e61ebcc48032b77bc4cf7fd6915c11fa16e4aad116b66c9468211230ea"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
ZIG_ARCH="aarch64"
|
||||
ZIG_SHA256="041ac42323837eb5624068acd8b00cd5777dac4cf91179e8dad7a7e90dd0c556"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" | sudo tar xJ -C /opt
|
||||
if [ -z "${ZIG_SHA256:-}" ]; then
|
||||
echo "No SHA-256 pinned for zig ${ZIG_VERSION} on ${ZIG_ARCH}."
|
||||
echo "Add one to the case above, from https://ziglang.org/download/index.json:"
|
||||
echo " jq -r '.[\"${ZIG_VERSION}\"][\"${ZIG_ARCH}-linux\"].shasum'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NAME="zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz"
|
||||
URL="https://ziglang.org/download/${ZIG_VERSION}/${NAME}"
|
||||
# Stage outside the checkout so a failed attempt cannot leave a stray
|
||||
# tarball in the working tree.
|
||||
ZIG_TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$ZIG_TMP"' EXIT
|
||||
TARBALL="${ZIG_TMP}/${NAME}"
|
||||
|
||||
# Download to a file and check it before anything consumes it: piping
|
||||
# curl straight into tar let a truncated transfer reach the extractor,
|
||||
# which is how this step failed. curl's own --retry does not cover a
|
||||
# short read (exit 18), and a checksum mismatch needs a fresh download
|
||||
# anyway, so the retry is an explicit bounded loop.
|
||||
verified=""
|
||||
previous=""
|
||||
for attempt in 1 2 3; do
|
||||
rm -f "$TARBALL"
|
||||
if curl -fsSL -o "$TARBALL" "$URL" && [ -s "$TARBALL" ]; then
|
||||
actual="$(sha256sum < "$TARBALL" | cut -d' ' -f1)"
|
||||
if [ "$actual" = "$ZIG_SHA256" ]; then
|
||||
echo "zig tarball matches its pinned SHA-256 (${actual})"
|
||||
verified=yes
|
||||
break
|
||||
fi
|
||||
echo "zig tarball failed its checksum on attempt ${attempt}:"
|
||||
echo " expected ${ZIG_SHA256}"
|
||||
echo " actual ${actual}"
|
||||
echo " size $(wc -c < "$TARBALL") bytes"
|
||||
if [ "$actual" = "$previous" ]; then
|
||||
echo "Two attempts fetched byte-identical content, so retrying is not"
|
||||
echo "going to help: the pin is stale, upstream re-published, or the"
|
||||
echo "source is serving the same bad file every time."
|
||||
break
|
||||
fi
|
||||
previous="$actual"
|
||||
else
|
||||
echo "zig tarball download failed on attempt ${attempt}"
|
||||
fi
|
||||
if [ "$attempt" -lt 3 ]; then
|
||||
sleep $((attempt * 10))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$verified" ]; then
|
||||
echo "zig ${ZIG_VERSION} (${ZIG_ARCH}) did not download with its pinned"
|
||||
echo "SHA-256 after ${attempt} attempt(s). Refusing to extract a tarball"
|
||||
echo "that does not match the pin; failing the build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo tar xJ -C /opt -f "$TARBALL"
|
||||
sudo ln -sf /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig
|
||||
zig version
|
||||
|
||||
|
||||
+101
@@ -86,6 +86,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
folded into the new tables with a one-time deprecation warning; migrate your
|
||||
`fips.yaml` to the new keys.
|
||||
|
||||
- Config validation now rejects two `node.rekey` settings that appear to
|
||||
disable the trigger and in fact fire it continuously. `after_messages` of
|
||||
zero makes the message-count arm true on every poll, because the trigger
|
||||
compares the counter with greater-or-equal. `after_secs` at or below the
|
||||
per-session jitter bound is the same trap on the timer arm: each session
|
||||
offsets the interval by a random value within plus or minus that bound, so a
|
||||
smaller interval saturates to zero on a negative draw and rekeys on sight,
|
||||
for roughly half of sessions. Both are checked whether or not rekey is
|
||||
enabled, so switching it on later cannot surface the error at a surprising
|
||||
moment, and neither gains an upper bound — a very large value remains the
|
||||
supported way to disable one arm. A config carrying either setting now fails
|
||||
to load instead of starting a node that rekeys constantly.
|
||||
|
||||
- Peer bloom filters are computed for every recipient in one prefix and suffix
|
||||
union sweep rather than rebuilt per recipient. Announcing to R peers
|
||||
previously did R full map builds and R by T merges; at 240 peers that was
|
||||
20.6 ms per tick, roughly half the tick body, with a median per-interval
|
||||
maximum of 34.5 ms. The result is exactly equal rather than approximately:
|
||||
merging is a bytewise OR, so regrouping the unions cannot change it. The
|
||||
trade-off, measured rather than assumed, is that the sweep does its full work
|
||||
regardless of how many peers are ready, so a tick announcing to one or two
|
||||
peers now costs about twice what it did; break-even is around three ready
|
||||
peers. Cadence, the debounce, the sequence rule and the fill-ratio cap are
|
||||
unchanged.
|
||||
|
||||
- Each peer's npub is derived once at construction instead of once per tick.
|
||||
The per-tick stats snapshot ran a bech32 encode for every tracked peer, and a
|
||||
second one for the common peer with no hosts-file entry and no alias, since
|
||||
the display-name fallback bottoms out in the same encode: 14.1 ms per tick at
|
||||
240 peers. The display name itself is deliberately not cached, because the
|
||||
alias map and the host map both mutate at runtime.
|
||||
|
||||
- The peer-retry tick no longer awaits the Nostr advert refetch. It ran inline
|
||||
on the 1-second rx-loop tick, awaiting a fetch with a 2-second timeout for
|
||||
each due peer and discarding the result; with up to sixteen due peers the
|
||||
timeouts stacked, and field profiling measured single 2.00 s stalls as the
|
||||
common case and a worst tick of 12.4 s against a 1 s period, delaying every
|
||||
other rx-loop arm by as much as 4.2 s. The refetch is now spawned, so a dial
|
||||
uses the advert cached at that moment and the refreshed one lands for that
|
||||
peer's next retry.
|
||||
|
||||
- The `Adopted NAT traversal socket` log line now carries the transport id and
|
||||
the local address alongside the peer npub. Without the local address an
|
||||
operator cannot join a host socket table against adoption events, and without
|
||||
the transport id several peers sharing one adopted transport are
|
||||
indistinguishable from several separate adopted transports.
|
||||
|
||||
- Inbound msg1 is classified before it is rate limited, and rekey or restart
|
||||
msg1 arriving on an established link now draws on its own token bucket
|
||||
instead of competing with stranger admission for a single shared one. On a
|
||||
@@ -116,6 +163,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Nostr NAT traversal signals are now sent only to relays the client pool
|
||||
actually holds. A signal is addressed to the merge of the peer's NIP-17 inbox
|
||||
relays, the relays its advert nominates for signaling, and our own DM relays,
|
||||
but the pool is built once at startup from the configured relays and the send
|
||||
is rejected outright, before anything is contacted, if any single URL in that
|
||||
list is outside it. One unconfigured relay anywhere in the merge therefore
|
||||
killed the whole attempt, including the sends to relays both sides shared. On
|
||||
a public node in open mode this made discovery non-functional: 309 traversal
|
||||
attempts, 290 explicit failures, zero successes, every failure on `relay not
|
||||
found`. Configured peers were unaffected, since they run a matching relay
|
||||
set. Comparison is on the normalized relay URL rather than the raw string, so
|
||||
a configured relay spelled with a trailing slash or different host case is
|
||||
not discarded. Two smaller fixes ride along: the responder resolves its
|
||||
relays before binding a socket and running STUN, rather than spending a STUN
|
||||
round trip and holding an offer slot only to find it has nowhere to answer,
|
||||
and it gained the empty-relay-list guard the initiator already had.
|
||||
|
||||
- A failed log write can no longer panic the thread or task that logged. The
|
||||
subscriber was built with the default internal-error reporting, which sends a
|
||||
failed write to `eprintln!`, and that macro panics when stderr has also
|
||||
failed. The shipped supervisor configurations make that a single condition
|
||||
rather than two: the macOS plist points both standard streams at one
|
||||
unrotated file, and the systemd units route both to journald, so one full
|
||||
disk fails both sinks together. In the daemon a crypto worker was the case
|
||||
that mattered — it logs a warning on send backpressure, and a worker that
|
||||
dies takes its share of the peer space with it permanently, while the panic
|
||||
message is discarded along the same broken path. In `fips-gateway`, which
|
||||
built its subscriber the same way, the casualty is a spawned task: the DNS
|
||||
resolver, the control accept loop or the pool tick, none of which is observed
|
||||
until shutdown, so the process would keep running and reporting healthy with
|
||||
mesh name resolution or lease expiry and NAT cleanup silently stopped.
|
||||
|
||||
- macOS: `peers.allow`, `peers.deny`, and the `hosts` file are now read
|
||||
from `/usr/local/etc/fips/`, matching the install layout the macOS
|
||||
packaging ships (`packaging/macos/`). The default-path constants were
|
||||
@@ -193,6 +272,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
counter now charges at the node that makes the decision rather than at the
|
||||
hop after it.
|
||||
|
||||
### Security
|
||||
|
||||
- The FSP session address is now bound to the peer key the Noise handshake
|
||||
authenticated, on both the initial and the rekey path. The responder recorded
|
||||
a session under the source address carried in the datagram without ever
|
||||
checking that address against the static key it had just authenticated, so a
|
||||
peer could complete a genuine handshake while claiming another node's
|
||||
address, and the identity cache, the session map and the address the IPv6
|
||||
shim reconstructs on delivery would all attribute its traffic to the node it
|
||||
named. The address is now derived from the authenticated key at the point it
|
||||
first becomes available in msg3, and a mismatch drops the half-open session
|
||||
without recording either the identity or the session. The rekey responder
|
||||
needed its own check: it returns before that code is reached and never read
|
||||
the peer's static key at all, so a rekey could complete under an established
|
||||
session with a different key than the one that opened it. It now requires the
|
||||
key to be unchanged and abandons the rekey while leaving the existing session
|
||||
intact, rather than tearing the session down, which would have handed an
|
||||
attacker a way to kill established sessions. Both comparisons are on x-only
|
||||
keys, because a stored key may carry a synthesized parity while the handshake
|
||||
learns the true point. The two rejections are counted separately in the
|
||||
session reject statistics.
|
||||
|
||||
## [0.4.1] - 2026-07-19
|
||||
|
||||
### Changed
|
||||
|
||||
+10
-1
@@ -67,7 +67,16 @@ async fn main() {
|
||||
)
|
||||
.from_env_lossy();
|
||||
|
||||
fmt().with_env_filter(filter).with_target(true).init();
|
||||
// As in the daemon: a failed log write must not panic whoever logged. The
|
||||
// default reports write failures with `eprintln!`, which panics when stderr
|
||||
// fails too, and both units send stdout and stderr to journald. Here the
|
||||
// casualty is a spawned task — the DNS resolver or the pool tick — whose
|
||||
// handle nothing observes until shutdown.
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(true)
|
||||
.log_internal_errors(false)
|
||||
.init();
|
||||
|
||||
info!("fips-gateway {} starting", version::short_version());
|
||||
|
||||
|
||||
@@ -101,10 +101,17 @@ async fn run_daemon(
|
||||
|
||||
// ANSI color only when stdout is a terminal — under a supervisor
|
||||
// (daemon(8), systemd) escape codes would litter the log file.
|
||||
//
|
||||
// Never let a failed log write panic the thread that logged. The default
|
||||
// is to report a write failure with `eprintln!`, which itself panics when
|
||||
// stderr fails too — and the shipped supervisor configs point stdout and
|
||||
// stderr at the same place, so one full disk satisfies both. A worker
|
||||
// thread killed that way takes its share of the peer space with it.
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(true)
|
||||
.with_ansi(std::io::IsTerminal::is_terminal(&std::io::stdout()))
|
||||
.log_internal_errors(false)
|
||||
.init();
|
||||
|
||||
info!("FIPS {} starting", version::short_version());
|
||||
|
||||
Reference in New Issue
Block a user