Skip to content

fix: choose our own home DERP from netcheck, and announce before moving - #36

Open
timmills wants to merge 14 commits into
Csontikka:mainfrom
timmills:pr/derp-home-selection-v055
Open

fix: choose our own home DERP from netcheck, and announce before moving#36
timmills wants to merge 14 commits into
Csontikka:mainfrom
timmills:pr/derp-home-selection-v055

Conversation

@timmills

@timmills timmills commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Rebased onto v0.5.5. This is findings #1 and #2 combined, as you asked in #33.

#1 — the home DERP region is chosen by a loop that echoes itself

NetInfo.PreferredDERP was populated from derp_region_default, which is a value that came from the control plane — and the control plane echoes back whatever the client sends. So the first region is self-sustaining:

control echoes X  ->  we adopt X  ->  we send X  ->  control echoes X ...

ML_DERP_REGION 4 (Frankfurt) is a compile-time constant and the only fallback, so a device that never breaks the loop stays on Frankfurt permanently, everywhere in the world.

Netcheck measures every region correctly the whole time. The measurements simply had no path to the decision — the override plumbing exists in the C but nothing exposed a way to reach it, and config.preferred_derp_region was always 0.

Observed here: two ESP32-S3 boards in Australia sat on fra at 682-927 ms while Sydney answered in ~10 ms, surviving reboots. The practical cost was an OTA taking 498 s that takes 25.8 s on the correct region.

This PR reports our own netcheck choice and selects on best-recent latency across several netchecks rather than a single probe. Movement requires both an absolute margin (>=10 ms) and a proportional one (<=2/3 of the incumbent), so near-equal regions do not ping-pong — modelled on net/netcheck/netcheck.go:1427-1500 and wgengine/magicsock/derp.go:197.

Verified on hardware: Home DERP changing 4 (254ms) -> 5 (52ms), one change then stable.

Related fixes in the series, each its own commit:

  • A lost STUN probe is not proof the home region is down.
  • Discard latency history when the DERPMap region set changes — stale RTTs for regions that no longer exist.
  • Never move an established home DERP without a live control connection.
  • Don't let the server echo overwrite a region we have chosen.
  • Home-DERP changes now log at WARN; they are rare and consequential.

#2 — announce the change before moving to it

The ordering matters and is easy to get backwards: if the device moves to the new home region and then tells the control plane, there is a window where peers are still being told to reach it at the old region while it is no longer listening there. Announce first, then move.

I have put that reasoning in a comment at the site rather than only in the commit message, since it is exactly the kind of thing that gets "simplified" back out by someone reading the code alone.

Please note — this branch also carries finding #6

The idle-peer probe gating (#6) is entangled with the DERP work in this series: it shares ml_wg_mgr.c changes with the DERP PeerGone handling and the probe-table saturation logging. I have left it in rather than attempt a split that I could not build-test cleanly. Happy to separate it if you would rather review it on its own — say the word and I will rebase it out.

Also included

netcheck_override is honoured again (it had been left unreachable), and a duplicate define is dropped.

Testing

Two ESP32-S3 boards, ~85-node Tailscale SaaS tailnet, Australia — which is what made the Frankfurt pinning so visible. The offer stands to field-test against the symmetric-NAT site for this one specifically, since you mentioned your bench can't produce honest cross-continent latency.

timmills added 13 commits August 6, 2026 23:35
…ntrol plane

My boards are in Australia and were sitting on the Frankfurt relay at 254-927 ms
while Sydney answered in 10-52 ms, across reboots, with no way to move them. I
went looking for why netcheck was not helping, and I think there is a loop here.

NetInfo.PreferredDERP is populated from ml->derp_region_default, but that value
originates from the control plane, which echoes back whatever we send as
Node.HomeDERP - as the log line at the register path already points out. So we
report X, control echoes X, we adopt X and report it again. Whichever region we
start on is self-sustaining, and since ML_DERP_REGION is 4, that is Frankfurt
for every device that has not been given an explicit region.

Netcheck measures every region correctly the whole time; the result just has no
route into the decision. And with netcheck_override enabled it changed only the
local home region - NetInfo.PreferredDERP still reported the old one, so peers
were being told to reach us at a region we were not using.

The Go client has no notion of a control-assigned home region: it picks from its
own report and tells control (magicsock/derp.go:197).

    preferredDERP = report.PreferredDERP
    if preferredDERP == 0 { preferredDERP = c.pickDERPFallback() }

Selection now follows netcheck.go:1427-1500:

  - choose on best-recent latency across the last few netchecks rather than the
    newest probe (netcheck.go:1525), so one lossy sample cannot move home;
  - require both an absolute and a proportional improvement before switching
    (preferredDERPAbsoluteDiff = 10 ms, and new <= old * 2/3), since a home
    change has to be announced to every peer;
  - fall through to the best reachable region if the current one stops
    answering.

On my bench board this moved 4 (254 ms) -> 5 (52 ms) on its own and then stayed
put - one change in a 1600-line log, no reconnect.

Happy to split the selection quality changes out from the reporting fix if you
would rather take them separately.
A home-region change is rare and has to be announced to every peer, so it is
worth seeing without enabling debug logging. The much more frequent 'staying on
the current region' line remains at INFO.
The no-hysteresis path (previous region unreachable -> take the best available)
was reachable from a single lost STUN round, which is the most plausible way
home-DERP selection could churn.

The reference treats the old region as accessible if it has EITHER a latency
this round OR has been heard from by a non-STUN route recently
(net/netcheck/netcheck.go:1475-1484, PreferredDERPFrameTime = 8s):

    heardFromOldRegionRecently = prevRegionLastHeard.After(rs.start) ||
        prevRegionLastHeard.After(now.Add(-PreferredDERPFrameTime))
    oldRegionIsAccessible := oldRegionCurLatency != 0 || heardFromOldRegionRecently

Our equivalent is an established DERP session to that same region which has
received traffic within 8s - the same signal the liveness watchdog already
uses, and one that cannot self-satisfy because DERP servers send keepalives.

When the old region is alive but has no measured latency, the margin test sees
a negative difference and keeps it, matching the reference (netcheck.go:1487,
where oldRegionCurLatency of 0 makes the absolute-difference test true).
The previous commit fixed what we REPORT (NetInfo.PreferredDERP now comes from
our own netcheck), but the self-node parse still adopted Node.HomeDERP
unconditionally. Node.HomeDERP is the control plane echoing back the
PreferredDERP we sent - not an independent suggestion, as the code's own log
line at the register path already says.

So after netcheck picked a nearer region, the very next netmap still carried
the old value (control has not processed our report yet) and flipped the
ACTIVE region - ml->derp_home_region, the value ml_derp.c connects with - back
again. That reproduces the exact mismatch this series set out to remove, only
inverted: we would report the good region while connecting to the stale one.

Now treated as a seed when we have no region at all, matching the guard the
register path already uses, and logged when it disagrees with our choice.

Note this still only takes effect on the next DERP connection; the reference
actively moves its home connection when the preference changes
(magicsock/derp.go:203+). That is a separate change.
derp_rtt_hist slots are indexed by position in derp_regions[], so they are only
meaningful while that array's contents are unchanged. A DERPMap that adds,
removes or reorders regions would silently attribute one region's latency to
another - and that history is what home selection is based on.

Checksum the region IDs on each netcheck and clear the history when it changes.
The reference does not have this problem because it prunes by time and keys by
region ID (netcheck.go:1405-1410, :1525); this is the cheap equivalent for a
fixed-size array.

Also softens a claim in the previous commit: control cannot dictate the home
region, but it can bias the choice via DERPMap.HomeParams.RegionScore, which the
reference multiplies into every latency (netcheck.go:1430-1439). We do not
implement that yet, and the comment now says so.
…ction

Found the hard way. A relay-only board picked a nearer region on boot, and
became unreachable to every peer for as long as I watched it - not because the
choice was wrong, but because peers reach a node at the HomeDERP the control
plane advertises for it. A home change we cannot report does not improve
connectivity, it removes it.

The reference refuses in exactly this situation
(wgengine/magicsock/derp.go:177-190):

    connectedToControl = c.health.GetInPollNetMap()
    if !connectedToControl && !force {
        if myDerp != 0 { return myDerp }   // keep what we have
        // else fall through: any DERP beats none
    }

GetInPollNetMap is documented as 'whether the client has an open HTTP long poll
to the control plane' (health/health.go:770-779). ctrl_stream_rx_ms is our
equivalent - it is fed by every frame on the map stream and already backs the
stream-liveness watchdog. Having no home region at all remains the documented
exception: any relay beats none.

Second half: when the region does change, report it immediately rather than
waiting for the next STUN completion to trigger an endpoint update. Until
control has the new PreferredDERP, every peer is still dialling the old region,
so the delay is measured in lost reachability. Retries on the next loop
iteration if the update fails.

I had noted this guard as 'not implemented' when adding home selection. That
was the wrong call: on a device whose only path is a relay, it is the
difference between a latency improvement and losing the device.
Two frames the relay sends us that we were dropping.

PeerGone (0x08) carries a 32-byte peer key AND a one-byte reason
(derp.go:88). We logged four bytes of the key and ignored the rest. Reason 0x01
is PeerGoneReasonNotHere - the relay has no path to that peer at all - and yet
the WireGuard fallback loop keeps driving a handshake through that same relay
every 30 s indefinitely. On a board whose peer is genuinely unreachable I
watched this reach 53 attempts with lastrx never advancing: pure load on the
relay and on a device that has little to spare.

The reference removes its DERP route for the peer on any PeerGone reason
(magicsock/derp.go:652-665). There is no route table here, so the equivalent is
to stop pushing handshakes through a relay that has just said it cannot deliver
them: a 60 s backoff, cleared immediately if we hear from the peer, so a peer
that comes back is picked up at once rather than waiting out a timer.

FrameHealth (0x14) was falling through to the default case. Its only current
use upstream is duplicate-connection detection (derp.go:118-123) - two clients
sharing a node key and fighting over one relay session, which from the outside
looks exactly like an unreachable node. That is worth seeing, so it is logged
rather than dropped.
…e fault

Two problems that only appear on a relay-only device, which is why a bench
board with direct paths never showed them.

Every known peer is probed forever regardless of whether we are talking to it.
Where a peer is reachable only through a relay, each probe costs a full relay
round trip, so the 32-slot probe table saturates permanently: I measured
active_probes pinned at 32/32 with just 4 peers, oldest_probe_age sitting above
the 5 s timeout, and returning pongs unmatchable.

The reference does not do this. It ends heartbeats for an idle session
(magicsock/endpoint.go:835, sessionActiveTimeout = 45 s) rather than probing
peers it has no traffic with. WireGuard already tracks last_rx/last_tx per
peer, so the same gate is cheap here: no probes for a session with no traffic
in 45 s. Trust-expiry bookkeeping still runs; this gates new probes only.

The second problem is worse in practice. With the table saturated, the
'probe table full' warning fires many times a second, and on this device the
logging itself blocked the main loop - 'logger took a long time (3050 ms)' -
which starves the ESPHome API and makes the node look unreachable while it is
running fine. The diagnostic became the outage. Now rate-limited to one line
per 10 s with a suppressed count.

Sizing the probe table larger would not have fixed either: the load is
proportional to peers times round-trip time, and the reference's answer is to
stop generating it.
Missing prototype - ml_derp.c called it with only an implicit declaration.
The caller and prototype landed but the function body did not, so the link
failed. Restored.
Two problems found reviewing the branches together rather than separately.

netcheck_override is exposed in the YAML by the DERP retry series and
documented as opt-in, but this branch had removed its only consumer - so the
flag became a documented safety knob that does nothing. The branches merge
cleanly, so nothing failed; a user setting netcheck_override: false simply got
netcheck-driven home selection anyway. That is the worst shape a defect can
take.

The flag now gates the CHOOSING again, as it always did. What is deliberately
NOT gated is the REPORTING: NetInfo.PreferredDERP always carries the region we
are actually using rather than the control plane's echo of it, because that
echo loop is a bug in either mode - with the override off it would still pin
every device permanently to whatever region it first landed on.

netcheck_override_threshold_ms is superseded by the reference's two-part margin
(absolute >= 10 ms AND proportional <= 2/3), which is strictly more
conservative than one absolute threshold.

Also: ML_DISCO_SESSION_ACTIVE_MS was defined twice. It turns out v0.5.4 already
defined it - unused, exactly like derp.last_recv_ms before the liveness
watchdog - and my edit added a second copy rather than annotating the existing
one. Duplicate removed, and the comment now notes it was already there, since
that supports the same 'intended and never finished' reading.
derp.go:118-123 -> 117-122.

And the idle gate is not equivalent to the reference: it gates on
max(last_rx, last_tx) where the reference uses lastSendExt, time since we last
sent (endpoint.go:835). A peer that keeps sending to us stays probed here. That
is more conservative, but the previous wording implied equivalence.
Ordering bug, found by watching a relay-only board go dark for minutes after
picking a nearer region.

Peers reach a node at the HomeDERP the control plane advertises, which control
derives from the PreferredDERP we report. The previous code set
derp_home_region immediately and reported afterwards, so the device stopped
listening on the region everyone was still calling and only announced the new
one later. The improvement in latency was real but it cost a multi-minute
outage every time the region changed.

The Go client can move immediately because magicsock holds connections to
several DERP regions and prunes idle ones later. We hold exactly one, so the
equivalent is to keep the old relay until control has been told:

  1. selection decides a new region -> derp_preferred_region, and a pending
     report is flagged. derp_home_region is deliberately NOT touched.
  2. the coord loop sends the endpoint update carrying the new PreferredDERP.
  3. only on success does derp_home_region move and a DERP reconnect fire.

If the update fails we stay put and retry, because moving without announcing is
strictly worse than staying on a slower relay that peers can still reach.
@timmills

timmills commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Build-tested against v0.5.5 before you spend time on it, since CI can't run on a fork PR without your approval:

ESPHome 2026.6.5 · ESP-IDF · esp32-s3-devkitc-1
SUCCESS — 104.16 s, 1194 compile/link steps
no errors or warnings from ml_derp.c / ml_coord.c / ml_netcheck.c / ml_wg_mgr.c
firmware.bin 1,455,936 bytes

The rebase onto v0.5.5 was conflict-free, and I checked your v0.5.5 work survived it rather than trusting that: ml_derp.c retains the backoff, RX-liveness and TLS-teardown changes intact, and the #32 peer sweep in ml_coord.c is complete.

One thing worth knowing when you read the diff: the ~35 lines this removes from ml_coord.c are the v0.5.4-era netcheck-override block, including the cJSON_AddNumberToObject(netinfo, "PreferredDERP", ml->derp_region_default) line that forms the echo loop. I confirmed v0.5.5 didn't author any of that — your ml_coord.c changes are entirely the peer sweep — so this supersedes old code rather than overwriting anything you've just written.

Also deliberate: this does not add a netcheck_override YAML option. Our own fork carries one for testing, but the config surface here is unchanged from v0.5.5 — the commit only makes the existing microlink_config_t field actually honoured.

@Csontikka

Copy link
Copy Markdown
Owner

Full review + hardware cycle done on this one too — and unlike #35, I can't merge it yet. The DERP/netcheck half holds up well (source-checked against the reference; a build of the first three commits survived everything I threw at it), but the series as a whole has two independent boot-killers on a SaaS tailnet with a populated peer table, plus one wiring problem on this repo's side. Everything below is measured, not speculated.

The crash

Repro environment: ESP32-S3, ordinary SaaS tailnet, ~14 NVS-cached peers, boot straight into a PeerGone burst (we connect to our home relay at ~7–8 s and immediately learn most peers aren't on it). In every capture the first DERP PeerGone is the last log line before the device dies.

Build Result
full series dies ~6–15 s after boot, 3/3 runs — even with zero unmatched PONGs (no probe-table flood involved)
ml_wg_mgr_notify_derp_gone call commented out dies
idle-gate body #if 0 dies
both disabled together survives 300 s under active DISCO fire (108 unmatched pongs)
current main, same conditions survives
first three commits only survives

So the two changes are independently fatal, and it's not the flood: Reset Reason: task watchdog (read via a debug build on the rollback partition — not a panic, no illegal access; something starves the watchdog). The fast death also lands before OTA app-verify, so the bootloader silently rolls back to the previous partition — if you've seen a board "mysteriously running the old firmware" during your own testing, that's why.

We didn't finish root-causing the exact starvation mechanism (timing coincides with the boot window where the NVS peer-cache is being written heavily; flash-write vs PSRAM/cache contention is our working theory). Happy to dig further together, but the isolation is solid either way.

Requests for the respin

  1. PeerGone → wg_mgr via its message queue, not a cross-task call from the DERP RX task. The queue and its handling pattern already exist for peer updates; a PeerGone burst is normal on a relay whose peers live elsewhere, so this path must be cheap and single-owner. Rate-limit its log line too (one per N seconds with a suppressed-count, like your probe-table fix) — during the burst it fires for every frame.
  2. Idle-gate: derive idleness from wg_mgr's own bookkeeping rather than reaching into wireguard_device from the probe loop. The fields [WG_SNAP] already dumps show wg_mgr tracks what's needed. Also worth stating in the PR text: the gate is inherently blind at boot (last_rx/tx == 0 → nothing is "idle"), so it does not protect against boot-time probe storms — the rate-limited saturation log is what helps there.
  3. netcheck_override_enabled is never set by this repo's component — there's no YAML knob here and the config struct zero-inits it false, so home-DERP selection would be dead code for every user of this repo (I believe the YAML exposure you referenced lives in your fork). Once the respin lands, we'll wire a component-side YAML option for it (selection on by default, your flag as the kill switch — matching the intent of your d454228 message).

The home-DERP selection logic itself, the echo-loop break, the announce-before-move ordering, the RTT-history design, PeerGone reason parsing and the HEALTH frame handler all read correctly against the reference and I'd like all of them in. The bench + repro harness is standing — send the respin and it goes straight through the full cycle, including your symmetric-NAT field test for the selection half.

Csontikka added a commit that referenced this pull request Aug 7, 2026
Two layers, both bench-measured under sustained fire from one aggressive
peer (which reboot-looped an unpatched v0.5.5 within ~5 minutes):

- Rate-limit the per-event 'DISCO PONG unmatched' and 'DISCO probe table
  full' warnings to one line per 10 s with a suppressed-event count
  (throttle pattern adopted from timmills' #36 series).
- Demote the hot-path per-packet chatter (PONG sent, DERP TX: SendPacket,
  Probe registered) from INFO to DEBUG - it fires for every relayed
  packet during a storm and only matters with the debug switch on.

This removes the log-driven part of the starvation and measurably
extends survival; a residual stall in the component status path under
the same extreme conditions is identified and tracked separately.
…uard_device

Addresses the two boot-killers found in review (task-watchdog starvation on a
warm-cache SaaS tailnet booting into a PeerGone burst).

1. PeerGone -> dedicated derp_gone_queue (DERP RX task enqueues a 32-byte key,
   no peer scan / no log on that task). wg_mgr, the sole owner of ml->peers,
   drains it, applies the backoff, and logs rate-limited (one line / 10s with a
   suppressed count, like the DISCO probe-table-full log). Drop-if-full: the
   backoff is advisory and the next PeerGone re-arms it.

2. DISCO idle-gate now reads wg_mgr's OWN bookkeeping instead of struct
   wireguard_peer from the probe loop: a new last_data_recv_ms stamped in
   process_wg_packet (peer resolved via pkt->src_pubkey) plus the existing
   last_pong_recv_ms. Gate = idle if we have not HEARD from the peer (data or
   pong) in ML_DISCO_SESSION_ACTIVE_MS.

Deliberate deviation from "max(last_send_ms, last_recv_ms)": wg_mgr is not on
the data-tx path, and stamping our own DISCO sends would make the gate
self-sustaining (a peer we keep probing would never look idle), so it gates on
last-heard. At boot all timestamps are 0 => nothing is idle (the gate is blind
then; the rate-limited log is the boot-storm protection).

Compile-verified on esp32-s3-devkitc-1 / esp-idf (esphome 2026.6.5). NOT yet
validated on hardware with a warm cache + default watchdog timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@timmills

Copy link
Copy Markdown
Contributor Author

Hardware validation of the respin, on the warm-cache repro you described.

Setup: esp32-s3-devkitc-1 on an ordinary SaaS tailnet (~7 NVS-cached peers), booting straight into a PeerGone burst (most cached peers live off the home relay), behind symmetric NAT. Built with this branch and, crucially, CONFIG_ESP_TASK_WDT_TIMEOUT_S dropped back to the ESP-IDF default. This board had been carrying a 30 override that was silently masking the starvation, which is why it "survived" before.

Result: it survives at the stock watchdog. 90s+ uptime, 0 task-watchdog resets, peers=7 linkup=5, well past the 6-15s death window. The new PeerGone path is confirmed live by its rate-limited log:

Relay reports no path to <peer> - pausing DERP handshakes for 60s (6 more suppressed in the last 10s)

So request #1 (PeerGone routed to wg_mgr via a dedicated queue, drained + rate-limited on the wg_mgr task, off the DERP RX task) and request #2 (idle-gate reads wg_mgr's own bookkeeping — a new last_data_recv_ms stamped in process_wg_packet via pkt->src_pubkey, plus last_pong_recv_ms — instead of struct wireguard_peer) resolve the starvation itself rather than the WDT bump hiding it.

One honest caveat: this run had the DERP selector off (the netcheck_override YAML knob isn't on this isolated branch's schema, so I dropped it from the base for the test). The two boot-killers you isolated are exercised by the PeerGone burst regardless of the selector, so they're covered — happy to also do a full-series run with netcheck_override on if useful.

Re request #3: acknowledged, netcheck_override_enabled is dead code in this repo (the YAML exposure is only in my fork); happy for you to wire the component-side option after this lands.

@Csontikka

Copy link
Copy Markdown
Owner

Ran the respin through the bench. The starvation fix works — confirmed independently on my side, and the queue/bookkeeping shapes are right. Two things still block the merge, one of them a real hole in the new idle-gate.

The good part, measured here

Same SaaS repro that killed the previous revision (warm NVS cache, boot straight into a PeerGone burst):

Build Result
previous revision died at 6–15 s, 3/3 runs
this respin survived the full 300 s run, 89 unmatched pongs, 0 resets

Worth stating explicitly: this branch is based on v0.5.5, so it does not include the log-throttle work now on main — the respin carried that run on its own. The new PeerGone path is visible and behaving on a Headscale bench too, rate-limited exactly as intended:

Relay reports no path to esp32-458730 - pausing DERP handshakes for 60s (4 more suppressed in the last 10s)

Routing PeerGone through its own queue and draining it on wg_mgr is the right shape, and so is deriving idleness from wg_mgr's own state instead of struct wireguard_peer.

Blocker: the idle-gate can't see traffic on a direct path

last_data_recv_ms is stamped from a find_peer_by_key(ml, pkt->src_pubkey) lookup — but src_pubkey is only populated on the DERP path (ml_derp.c:311; the field is even documented as "Source peer key (for DERP packets)"). Packets arriving on a direct path are built in ml_net_io.c:66-72 with a designated initializer that omits src_pubkey, so it is all-zero and the lookup never matches. On a direct path the stamp is therefore never written, and the only thing keeping a peer non-idle is last_pong_recv_ms.

That closes a loop you were careful to avoid in the other direction:

  1. pongs stop arriving for 45 s (radio contention, a burst of loss — the exact conditions the 2026-05-24 "throughput-collapse" comment in this file describes),
  2. the gate marks the peer idle, so heartbeat pings stop,
  3. no pings → no pongs → last_pong_recv_ms never advances → idle is now self-sustaining,
  4. trust_until_ms (60 s) expires, the direct path is torn down and the peer falls back to DERP — while data was flowing over that direct path the whole time.

I could not reproduce this on the bench: my direct peer's pong chain never broke (pong_age stayed at 161–2155 ms across a 5-minute run), so this is a code-path argument, not a field observation — but the missing stamp is not in doubt, and step 4 is the failure mode this file already carries a fix and a warning for.

Two ways to close it, either is fine by me:

  • stamp on the direct path too, resolving the peer by src_ip/src_port against its known endpoint when !pkt->via_derp; or
  • also stamp when we receive a DISCO ping (in the PONG-sending path, where the peer index is already resolved) — cheap, and it covers the direct path since DISCO pings arrive there as well.

Belt and braces would be both. Note last_send_ms looks like the natural third input but is dead — declared and zeroed, never written — so it can't carry the gate today.

Minor: the new queue isn't in the creation check

microlink.c:355 still validates only the original six queues, so a derp_gone_queue that fails to allocate goes unnoticed. It won't crash — both call sites null-check — the PeerGone backoff just silently stops working, which is the harder failure to diagnose.

Merge mechanics

This branch is based on v0.5.5 and conflicts with current main in ml_wg_mgr.c: main now has its own throttle on the probe-table-full warning (same pattern, credited to your series) plus a demotion of the per-packet hot-path logs to DEBUG. Rebasing needs one deliberate resolution — keep one throttle, not two. Happy to do that rebase myself when you're ready, so you don't have to chase our merges.

Once the direct-path stamp lands I'll rerun the full cycle, and then wire the component-side netcheck_override YAML option so the DERP selector is actually reachable for our users — I'll do a full-series run with it on at that point, which also covers the caveat you flagged.

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.

2 participants