Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@
All notable changes to this project will be documented in
this file. Format based on [Keep a Changelog](https://keepachangelog.com/).

## [0.2.2] - unreleased

### Added — wg-relay diagnostic surface

- **`--trace-forward-hashes` daemon flag**. When set, every wg-relay forward logs a SHA-256(payload) hash + length + source/destination peer pubkey prefix at two points: ingress (after the source peer matches and MAC1 verification, if applicable) and egress (just before `sendto` to the destination). Same hash on both lines proves the relay didn't mutate the frame; divergence flags a corrupting code path. Off by default — this is per-frame logging on the hot path and will tank throughput if left on. Aimed at diagnosing integrity-test failures where a single log line and aggregate counters aren't enough to triage.
- **Per-peer drop counters**. `wg peer list` now surfaces `peer.<i>.drop_no_link` and `peer.<i>.drop_pubkey_mismatch` alongside the existing per-peer byte counters. The aggregate counters in `wg show` are unchanged; these are the pair-attributable subset for diagnosing which peer's traffic is hitting which drop reason.
- **Per-source-IP drop histogram** for the three classes that aren't attributable to a known peer (`drop_unknown_src`, `drop_not_wg_shaped`, `drop_handshake_no_pubkey_match`). Surfaced via the new `wg show drop_sources` verb. Capped at 256 source IPs with FIFO eviction so spoofed-source storms don't grow the table without bound. Specifically targets the cloud-gcp-c4 internal-vs-external NAT-IP bug, where the runner traffic arrives at the relay from an IP not stamped on either peer and lights up `drop_handshake_no_pubkey_match` aggregately — now operators can see which IP it's coming from.

### Changed — wg-relay link-add error codes are differentiated

- **`link_limit_exceeded` is now its own error code**. Previously every `wg link add` failure mode (unknown peer, self-link, duplicate, iter-1 limit) collapsed into a single `wg_link_failed` response, so the benchmark runner couldn't tell "your star topology hit the one-link-per-peer limit" apart from "you typo'd a peer name." Each rejection reason now has its own code (`wg_peer_unknown`, `wg_link_self`, `link_limit_exceeded`, `wg_link_duplicate`); the iter-1 limit is documented in `wg link add --help`. Behaviour is unchanged — the rejection still happens at the same point in `LinkAddLocked` for the same reasons; only the surfaced error string changes.

The iter-1 invariant ("each peer is in at most one link") stands per the relay's design memory. Multi-link mesh routing is future work — it requires either per-link UDP ports or some form of in-packet introspection to disambiguate the destination from the source 4-tuple alone.

### Changed — wg-relay XDP attach is no longer silently fallback

- **Structured `xdp_attached` log line**. On successful attach the daemon now logs `xdp_attached iface=<nic> ifindex=N mode={drv,skb} driver=<gve|r8169|...> kernel=<release>` per attached NIC. The achieved mode is now legible from a single grep, instead of having to infer it from the older free-form "(ifindex=N, mode=native)" text.
- **`--xdp-mode={drv,skb,auto,off}` daemon flag**, default `drv`. Was previously hardcoded to "try DRV, fall back to SKB on failure" — a silent fallback that on the 0.2.1 cloud-gcp-c4 benchmark labelled rows "xdp" that were really running the userspace recv loop. Operators that want the historical behaviour pass `--xdp-mode=auto` explicitly. `off` skips XDP attach even when `--xdp-interface` is set, so the operator can leave the interface flag in their unit file but disable XDP at runtime without editing it.
- **Attach failure exits non-zero**. `XdpAttach` failure now logs `xdp_attach_failed iface=<nic> ifindex=N mode=<mode> driver=<drv> kernel=<release> reason=<errno-string>` and tears the relay down (`WgRelayStart` returns `nullptr` → `main.cc` exits non-zero). The runner gets a clear signal rather than a silent userspace fallback.

Note for cloud operators: gVNIC (Google) and vmxnet3 (VMware) do not advertise `XDP_DRV` mode on stock kernels through 6.12 — the daemon will fail attach with `reason=Operation not supported` under the default `--xdp-mode=drv`. Pass `--xdp-mode=auto` (or `=skb`) to opt into the generic-mode fallback explicitly.

## [0.2.1] - unreleased

### Added — wg-relay hardening
Expand Down
12 changes: 12 additions & 0 deletions include/hyper_derp/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ struct WgRelayConfig {
/// Path to the compiled BPF object. Defaults to a
/// CMake-installed location; rarely set explicitly.
std::string xdp_bpf_obj_path;
/// XDP attach mode override. "" / "drv" → native only
/// (XDP_FLAGS_DRV_MODE); "skb" → generic only; "auto" →
/// try drv, fall back to skb on EOPNOTSUPP / EINVAL;
/// "off" → skip XDP entirely. Surfaces an explicit
/// failure mode rather than silently falling through to
/// userspace when the operator expected drv mode.
std::string xdp_mode;
/// Per-frame trace logging for diagnosing integrity
/// failures. See WgRelay::trace_forward_hashes — off by
/// default; enabled via the `--trace-forward-hashes`
/// daemon flag.
bool trace_forward_hashes = false;
};

/// Connection level for a peer pair. Level 0 (DERP) is
Expand Down
86 changes: 86 additions & 0 deletions include/hyper_derp/wg_relay.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ struct WgRelayPeer {
/// Times this peer's `endpoint` was relearned via the
/// MAC1-driven roaming flow. Persisted to the roster.
uint64_t endpoint_relearn = 0;
/// Per-peer drop counters — incremented alongside the
/// aggregate `WgRelayStats` counters at sites where the
/// drop is attributable to a known source peer. The
/// other drop classes (drop_unknown_src,
/// drop_not_wg_shaped, drop_handshake_no_pubkey_match)
/// are by definition unattributable and stay aggregate.
uint64_t drop_no_link_peer = 0;
uint64_t drop_pubkey_mismatch_peer = 0;
/// Pending relearn-candidate, populated when an unknown
/// source presents a handshake with valid MAC1 against
/// this peer's link partner. Cleared on confirm (transport
Expand Down Expand Up @@ -214,6 +222,25 @@ struct WgXdpStats {
uint64_t drop_blocklisted = 0;
};

/// Per-source-IP histogram of drops that can't be attributed
/// to a registered peer (because, by definition, the source
/// didn't match any peer's endpoint). The brief asked for
/// per-pair breakdown of these but per-pair is meaningless
/// when the source is unknown — per-source-IP is the next
/// best granularity and exactly what's needed to diagnose
/// "the runner's traffic is arriving from an external NAT IP
/// that isn't stamped on the peer." Bounded to keep the map
/// from growing under spoofed source attacks (FIFO eviction
/// at the cap).
struct WgRelayDropBySrc {
uint64_t drop_unknown_src = 0;
uint64_t drop_not_wg_shaped = 0;
uint64_t drop_handshake_no_pubkey_match = 0;
/// Steady-clock ns of the most recent increment — used as
/// the FIFO eviction key when the map is at capacity.
uint64_t last_seen_ns = 0;
};

/// Strike record per source IP — incremented when a candidate
/// endpoint that source registered fails to confirm via
/// transport-data. Escalates the source onto the blocklist
Expand All @@ -237,6 +264,12 @@ struct WgRelay {
/// succeeds; escalated to wg_blocklist once the threshold
/// is crossed.
std::map<uint32_t, WgRelayStrike> strikes;
/// Per-source-IP drop histogram. Keyed by host-byte-order
/// uint32 (v4 only — v6 sources are skipped, since the
/// brief's diagnostic targets are v4 NAT bugs). Capped at
/// kDropBySrcMaxEntries; oldest `last_seen_ns` is evicted
/// when full so spoofed-source storms can't blow up RSS.
std::map<uint32_t, WgRelayDropBySrc> drop_by_src;
/// Blocked source IPs (host-byte-order uint32_t) → expiry
/// timestamp (steady_clock ns). Mirrors the BPF
/// wg_blocklist map for `wg blocklist list` / userspace
Expand All @@ -246,6 +279,14 @@ struct WgRelay {
std::atomic<bool> running{false};
std::thread loop_thread;
std::string roster_path;
/// When true, every forwarded frame is logged with
/// SHA-256(payload), length, and the source/destination
/// peer pubkey prefixes at both ingress and egress.
/// Off by default — this is per-frame logging on the hot
/// path and will tank throughput. Drive it from the
/// `--trace-forward-hashes` daemon flag for debugging
/// integrity-mismatch failures.
bool trace_forward_hashes = false;
/// XDP fast path. attached == true iff the BPF program
/// is live on a NIC. Map updates from `wg link add`
/// land here; the userspace recv loop still runs as the
Expand Down Expand Up @@ -276,6 +317,31 @@ bool WgRelayPeerNic(WgRelay* r, const std::string& name,
const std::string& nic);
bool WgRelayPeerRemove(WgRelay* r,
const std::string& name);
/// Outcome codes for `WgRelayLinkAdd` / `WgRelayLinkAddDetail`.
/// 0 means success; the non-zero values are surfaced by the
/// einheit channel as distinct error codes so the runner can
/// distinguish "you already used your one link slot" from
/// "you typo'd a peer name."
enum WgRelayLinkAddResult : int {
kWgLinkOk = 0,
kWgLinkUnknownPeer = 1,
kWgLinkSelfLink = 2,
/// Iteration-1 invariant: each peer is in at most one link
/// so the destination is unambiguous from the source 4-tuple
/// alone. A second link on either side is rejected here
/// rather than producing ambiguous forwarding.
kWgLinkLimitExceeded = 3,
kWgLinkDuplicate = 4,
};

/// Like WgRelayLinkAdd but returns the reason code. Use this
/// when the caller needs to differentiate failure modes (e.g.
/// the einheit channel mapping each onto a distinct error
/// string). The bool-returning wrapper below is kept for
/// callers that only need success/failure.
WgRelayLinkAddResult WgRelayLinkAddDetail(
WgRelay* r, const std::string& a, const std::string& b);

bool WgRelayLinkAdd(WgRelay* r, const std::string& a,
const std::string& b);
bool WgRelayLinkRemove(WgRelay* r, const std::string& a,
Expand All @@ -291,6 +357,12 @@ struct WgRelayPeerInfo {
uint64_t last_seen_ns;
uint64_t rx_bytes;
uint64_t fwd_bytes;
/// Per-peer drop counters. Aggregate counterparts in
/// WgRelayStatsSnapshot stay populated; these are the
/// pair-attributable subset for diagnosing which peer's
/// traffic is hitting which drop reason.
uint64_t drop_no_link;
uint64_t drop_pubkey_mismatch;
std::string linked_to; // name of peer this is linked to, or empty
};
std::vector<WgRelayPeerInfo> WgRelayListPeers(
Expand Down Expand Up @@ -335,6 +407,20 @@ struct WgBlocklistView {
std::vector<WgBlocklistView> WgRelayListBlocklist(
const WgRelay* r);

/// One row of `wg show drop_sources`. Provides the same three
/// drop counters the brief flagged as needing more granularity
/// than aggregate, attributed to the source IP (since they're
/// definitionally not attributable to a registered peer).
struct WgDropBySrcView {
std::string ip;
uint64_t drop_unknown_src;
uint64_t drop_not_wg_shaped;
uint64_t drop_handshake_no_pubkey_match;
uint64_t last_seen_ns;
};
std::vector<WgDropBySrcView> WgRelayListDropSources(
const WgRelay* r);

} // namespace hyper_derp

#endif // INCLUDE_HYPER_DERP_WG_RELAY_H_
85 changes: 78 additions & 7 deletions src/einheit_channel.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1843,6 +1843,10 @@ void WgPeerList(Server* s, const Request& /*req*/,
p.rx_bytes);
b += std::format("peer.{}.fwd_bytes={}\n", idx,
p.fwd_bytes);
b += std::format("peer.{}.drop_no_link={}\n", idx,
p.drop_no_link);
b += std::format("peer.{}.drop_pubkey_mismatch={}\n", idx,
p.drop_pubkey_mismatch);
idx++;
}
b += std::format("peer.count={}\n", idx);
Expand All @@ -1854,13 +1858,39 @@ void WgLinkAdd(Server* s, const Request& req,
if (!WgGate(s, r)) return;
if (!RequireArg(req, 0, "a", r)) return;
if (!RequireArg(req, 1, "b", r)) return;
if (!WgRelayLinkAdd(s->wg_relay, req.args[0],
req.args[1])) {
auto rc = WgRelayLinkAddDetail(s->wg_relay, req.args[0],
req.args[1]);
if (rc != kWgLinkOk) {
r->status = ResponseStatus::kError;
r->error = ErrorOf(
"wg_link_failed",
"unknown peer, self-link, duplicate link, or one "
"side already has a link (iteration-1 limit)");
switch (rc) {
case kWgLinkUnknownPeer:
r->error = ErrorOf("wg_peer_unknown",
"one or both peers are not "
"registered (use `wg peer add`)");
break;
case kWgLinkSelfLink:
r->error = ErrorOf("wg_link_self",
"a peer cannot link to itself");
break;
case kWgLinkLimitExceeded:
// The iter-1 invariant: each peer is in at most one
// link. Distinct from the other failure modes so the
// runner can detect a star-topology config that hits
// the limit and switch to disjoint pairs.
r->error = ErrorOf(
"link_limit_exceeded",
"iteration-1 limit: each peer may be in at most "
"one link (one side of this pair already is)");
break;
case kWgLinkDuplicate:
r->error = ErrorOf("wg_link_duplicate",
"this exact link already exists");
break;
default:
r->error = ErrorOf("wg_link_failed",
"link add failed");
break;
}
return;
}
SetBody(r, std::format("a={}\nb={}\n", req.args[0],
Expand Down Expand Up @@ -1948,6 +1978,37 @@ void WgBlocklistList(Server* s, const Request& /*req*/,
SetBody(r, b);
}

// Per-source-IP histogram of unattributable drops. The brief
// asked for per-pair breakdown of drop_unknown_src,
// drop_not_wg_shaped, and drop_handshake_no_pubkey_match —
// per-pair is meaningless when the source isn't a registered
// peer, but per-source-IP is exactly what the runner needs to
// diagnose internal/external NAT-IP mismatches.
void WgDropSources(Server* s, const Request& /*req*/,
Response* r) {
if (!WgGate(s, r)) return;
auto entries = WgRelayListDropSources(s->wg_relay);
if (entries.empty()) {
SetBody(r, "drop_sources=empty\n");
return;
}
std::string b;
for (size_t i = 0; i < entries.size(); ++i) {
b += std::format("src.{}.ip={}\n", i, entries[i].ip);
b += std::format("src.{}.drop_unknown_src={}\n", i,
entries[i].drop_unknown_src);
b += std::format("src.{}.drop_not_wg_shaped={}\n", i,
entries[i].drop_not_wg_shaped);
b += std::format(
"src.{}.drop_handshake_no_pubkey_match={}\n", i,
entries[i].drop_handshake_no_pubkey_match);
b += std::format("src.{}.last_seen_ns={}\n", i,
entries[i].last_seen_ns);
}
b += std::format("count={}\n", entries.size());
SetBody(r, b);
}

void WgShow(Server* s, const Request& /*req*/,
Response* r) {
if (!WgGate(s, r)) return;
Expand Down Expand Up @@ -2197,7 +2258,9 @@ Registry MakeRegistry() {
m["wg_link_add"] = {WgLinkAdd, Role::kOperator,
"wg link add",
"Allow A↔B forwarding between two "
"peers",
"peers. Iteration-1 limit: each peer "
"may appear in at most one link "
"(rejected as link_limit_exceeded)",
false, wg_link_args};
m["wg_link_remove"] = {WgLinkRemove, Role::kOperator,
"wg link remove",
Expand All @@ -2220,6 +2283,14 @@ Registry MakeRegistry() {
"Source IPs auto-blocked after repeated failed-confirm "
"strikes (forged-handshake protection)",
false, {}};
m["wg_show_drop_sources"] = {
WgDropSources, Role::kAny, "wg show drop_sources",
"Per-source-IP histogram for the three drop classes "
"that aren't attributable to a registered peer "
"(drop_unknown_src, drop_not_wg_shaped, "
"drop_handshake_no_pubkey_match). Capped at 256 IPs; "
"FIFO eviction on overflow",
false, {}};
return m;
}

Expand Down
27 changes: 27 additions & 0 deletions src/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ static void PrintUsage(const char* prog) {
"(default: 3478)\n"
" --xdp-interface <nic> Network interface for "
"XDP attachment\n"
" --xdp-mode <m> XDP attach mode: drv "
"(default), skb, auto, off.\n"
" Some virtual NICs (gVNIC, "
"vmxnet3) only ship XDP\n"
" in generic mode; use "
"--xdp-mode=skb or --xdp-mode=auto\n"
" to opt into the slower "
"fallback explicitly.\n"
" --trace-forward-hashes "
"Log SHA-256 of every forwarded\n"
" wg-relay frame at ingress + "
"egress. Per-frame\n"
" log; for diagnostics only — "
"do not enable in\n"
" production.\n"
" --help Show this help\n"
" --version Show version",
prog);
Expand Down Expand Up @@ -123,6 +138,8 @@ int main(int argc, char* argv[]) {
const char* hd_relay_key = nullptr;
const char* hd_enroll_mode = nullptr;
const char* xdp_interface = nullptr;
const char* xdp_mode = nullptr;
bool trace_forward_hashes = false;
int stun_port = -1;
int hd_relay_id = -1;
std::vector<std::string> seed_relays;
Expand Down Expand Up @@ -221,6 +238,10 @@ int main(int argc, char* argv[]) {
} else if (arg == "--xdp-interface"sv &&
i + 1 < argc) {
xdp_interface = argv[++i];
} else if (arg == "--xdp-mode"sv && i + 1 < argc) {
xdp_mode = argv[++i];
} else if (arg == "--trace-forward-hashes"sv) {
trace_forward_hashes = true;
} else {
std::println(stderr,
"error: unknown option '{}'", arg);
Expand Down Expand Up @@ -338,6 +359,12 @@ int main(int argc, char* argv[]) {
static_cast<uint16_t>(stun_port);
if (xdp_interface)
config.level2.xdp_interface = xdp_interface;
// wg-relay-mode-only flags: pass through whether or not
// wg-mode is selected (the relay only reads them when it's
// active; the daemon ignores them otherwise).
if (xdp_mode) config.wg.xdp_mode = xdp_mode;
if (trace_forward_hashes)
config.wg.trace_forward_hashes = true;

if (pin_spec) {
int n = ParsePinCores(pin_spec,
Expand Down
Loading
Loading