@@ -8,10 +8,26 @@ import (
88 "fmt"
99 "sync"
1010 "sync/atomic"
11+ "time"
1112
1213 "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
1314)
1415
16+ const (
17+ // Debounce parameters for network-change notifications, borrowed from
18+ // WireGuard for Windows (tunnel/defaultroutemonitor.go): coalesce bursts
19+ // for 150ms, but never delay a re-detection beyond 2s if the burst keeps
20+ // going (interface storms during docking/undocking).
21+ debounceInterval = 150 * time .Millisecond
22+ debounceBurstMax = 2 * time .Second
23+
24+ // sweepInterval paces the registry liveness sweep. On a stable network
25+ // re-apply (the other cleanup point) may not run for weeks; the sweep
26+ // guarantees closed sockets don't stay pinned by their RawConn either
27+ // way. The registry holds a handful of entries, so this is nearly free.
28+ sweepInterval = 2 * time .Minute
29+ )
30+
1531// Manager owns the Windows OS network state behind AWL's VPN gateway feature:
1632// the always-on socket marking (IP_UNICAST_IF binding to the physical uplink
1733// NIC — mechanism and lifecycle in sockmark_windows.go) and the runtime state
@@ -33,11 +49,11 @@ type Manager struct {
3349 index6 atomic.Uint32
3450
3551 // registry tracks the live long-lived UDP sockets for re-binding on
36- // uplink changes; kickCh feeds debounced network-change notifications to
37- // the watch goroutine. Both belong to the marking machinery in
38- // sockmark_windows.go .
39- registry sockRegistry
40- kickCh chan struct {}
52+ // uplink changes (marking machinery, sockmark_windows.go); netChangeCh
53+ // carries network-change notifications from the OS callbacks to the
54+ // watch goroutine, which debounces them .
55+ registry sockRegistry
56+ netChangeCh chan struct {}
4157
4258 mu sync.Mutex
4359 routeState * routeState
@@ -47,7 +63,7 @@ type Manager struct {
4763// NewManager returns the Manager for Windows. The uplink indexes stay zero
4864// (marking is a no-op) until Start performs the initial detection.
4965func NewManager () * Manager {
50- return & Manager {kickCh : make (chan struct {}, 1 )}
66+ return & Manager {netChangeCh : make (chan struct {}, 1 )}
5167}
5268
5369// Start synchronously detects the current uplink and launches the
@@ -58,14 +74,18 @@ func NewManager() *Manager {
5874// registration itself can fail — and that deliberately fails Init (unlike
5975// the Linux Start, whose netlink monitor is best-effort staleness tracking):
6076// the notifications drive socket marking itself, which never recovers
61- // without them. Must be called before the first libp2p socket is created.
77+ // without them. Called before the first libp2p socket is created (Init
78+ // guarantees this); the ordering matters mostly for TCP — a UDP socket
79+ // created earlier is registered and re-bound by the initial uplink
80+ // re-detection anyway, but a TCP dial made before Start would stay unmarked
81+ // for its lifetime.
6282func (m * Manager ) Start (ctx context.Context ) error {
63- m .redetect ()
83+ m .redetectUplinks ()
6484
6585 routeCb , err := winipcfg .RegisterRouteChangeCallback (func (_ winipcfg.MibNotificationType , route * winipcfg.MibIPforwardRow2 ) {
6686 // Only default-route changes can change the uplink choice.
6787 if route != nil && route .DestinationPrefix .PrefixLength == 0 {
68- m .kick ()
88+ m .notifyNetChange ()
6989 }
7090 })
7191 if err != nil {
@@ -75,7 +95,7 @@ func (m *Manager) Start(ctx context.Context) error {
7595 // Parameter changes cover interface metric flips, which reorder
7696 // default routes without touching the route table itself.
7797 if notificationType == winipcfg .MibParameterNotification {
78- m .kick ()
98+ m .notifyNetChange ()
7999 }
80100 })
81101 if err != nil {
@@ -93,6 +113,83 @@ func (m *Manager) Start(ctx context.Context) error {
93113 return nil
94114}
95115
116+ // notifyNetChange signals the watch goroutine that the network changed, which
117+ // reacts after a debounce (onNetworkChange). Non-blocking and coalescing:
118+ // called from OS notification threads.
119+ func (m * Manager ) notifyNetChange () {
120+ select {
121+ case m .netChangeCh <- struct {}{}:
122+ default :
123+ }
124+ }
125+
126+ // watch is the Manager's background goroutine: the debounced reaction to
127+ // network change notifications plus the periodic registry sweep. cleanup
128+ // unregisters the OS callbacks when ctx dies.
129+ func (m * Manager ) watch (ctx context.Context , cleanup func ()) {
130+ defer cleanup ()
131+
132+ coalesce := time .NewTimer (time .Hour )
133+ if ! coalesce .Stop () {
134+ <- coalesce .C
135+ }
136+ defer coalesce .Stop ()
137+ sweep := time .NewTicker (sweepInterval )
138+ defer sweep .Stop ()
139+
140+ stopCoalesce := func () {
141+ if ! coalesce .Stop () {
142+ select {
143+ case <- coalesce .C :
144+ default :
145+ }
146+ }
147+ }
148+
149+ var burstStart time.Time
150+ pending := false
151+ for {
152+ select {
153+ case <- ctx .Done ():
154+ return
155+ case <- m .netChangeCh :
156+ now := time .Now ()
157+ switch {
158+ case ! pending :
159+ burstStart = now
160+ pending = true
161+ coalesce .Reset (debounceInterval )
162+ case now .Sub (burstStart ) >= debounceBurstMax :
163+ // The burst has been going on too long — don't starve the
164+ // re-detection, run it now.
165+ stopCoalesce ()
166+ pending = false
167+ m .onNetworkChange ()
168+ default :
169+ stopCoalesce ()
170+ coalesce .Reset (debounceInterval )
171+ }
172+ case <- coalesce .C :
173+ pending = false
174+ m .onNetworkChange ()
175+ case <- sweep .C :
176+ if evicted := m .registry .sweep (); evicted > 0 {
177+ logger .Debugf ("registry sweep evicted %d closed sockets" , evicted )
178+ }
179+ }
180+ }
181+ }
182+
183+ // onNetworkChange runs the debounced consumers of a network-change event:
184+ // socket-marking re-detection first (lock-free — atomics + the registry's own
185+ // lock), then the exit node's forwarding re-sync, which takes m.mu.
186+ // EnableServerNAT can hold m.mu for seconds of PowerShell, and must never
187+ // delay socket re-binding — only the re-sync itself, which is harmless.
188+ func (m * Manager ) onNetworkChange () {
189+ m .redetectUplinks ()
190+ m .resyncServerForwarding ()
191+ }
192+
96193// EnableClientRoutes installs the gateway client routes on the TUN (the
97194// default-route capture plus the IPv6 fail-closed fence). Idempotent: a
98195// second call while routes are installed is a no-op. It refuses while no
0 commit comments