Start-guard covers a static detour chain but stops at a selector (its chosen member is runtime-resolved). This adds the runtime half: in Selector.SelectOutbound, BEFORE committing the switch, if the new member reaches a wireguard endpoint, walk up the reverse-dependency ledger (OutboundManager.ConsumersOf) and SuspendAmneziaWG() every AmneziaWG consumer of the group — device down, started=false. Suspending before s.selected.Store closes the race: by the time the group points at the WG member, the consumer is down and a reconnect fails with "not ready" instead of sending a junk handshake into WireGuard. New adapter.AmneziaWGSuspendable marker + OutboundManager.ConsumersOf let protocol/group act without importing protocol/wireguard. Plain-WG and non-AWG consumers are left untouched. Variant B throughout. Refs #2
94 lines
3.4 KiB
Go
94 lines
3.4 KiB
Go
// lx:begin awg
|
|
|
|
package group
|
|
|
|
import (
|
|
"github.com/sagernet/sing-box/adapter"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
)
|
|
|
|
// suspendAmneziaWGConsumersOnWireGuardSwitch is called from Selector.SelectOutbound
|
|
// BEFORE the switch is committed. If the member about to be selected is — or chains
|
|
// down via detour to — a WireGuard-based endpoint (type "wireguard", covering plain
|
|
// WG and AmneziaWG), it walks UP from this group to every AmneziaWG endpoint that
|
|
// detours through it and suspends each one (brings its device down). Rationale:
|
|
// AmneziaWG traffic encapsulated inside a WireGuard tunnel hangs the kernel on
|
|
// Android; the static Start-guard cannot cover this because a selector's chosen
|
|
// member is only known at runtime.
|
|
//
|
|
// Called before s.selected is updated, so the race is closed: once the group
|
|
// points at the WireGuard member, the AmneziaWG consumers are already suspended
|
|
// (started=false) and a concurrent reconnect fails with "not ready" instead of
|
|
// sending a junk handshake into WireGuard.
|
|
func suspendAmneziaWGConsumersOnWireGuardSwitch(outboundManager adapter.OutboundManager, groupTag string, selected adapter.Outbound) {
|
|
if outboundManager == nil || groupTag == "" {
|
|
return
|
|
}
|
|
if !chainReachesWireGuard(outboundManager, selected, make(map[string]bool)) {
|
|
return
|
|
}
|
|
suspendAmneziaWGConsumers(outboundManager, groupTag, make(map[string]bool))
|
|
}
|
|
|
|
// chainReachesWireGuard reports whether outbound is — or transitively detours
|
|
// down to, or (being a group) contains a member that is — a WireGuard-based
|
|
// endpoint. visited guards against cycles.
|
|
func chainReachesWireGuard(outboundManager adapter.OutboundManager, outbound adapter.Outbound, visited map[string]bool) bool {
|
|
if outbound == nil {
|
|
return false
|
|
}
|
|
tag := outbound.Tag()
|
|
if tag != "" {
|
|
if visited[tag] {
|
|
return false
|
|
}
|
|
visited[tag] = true
|
|
}
|
|
if outbound.Type() == C.TypeWireGuard {
|
|
return true
|
|
}
|
|
// Down the detour chain (vless -> ... -> wireguard).
|
|
for _, dependency := range outbound.Dependencies() {
|
|
if member, loaded := outboundManager.Outbound(dependency); loaded {
|
|
if chainReachesWireGuard(outboundManager, member, visited) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
// A nested group: any member reaching WireGuard counts.
|
|
if group, isGroup := outbound.(adapter.OutboundGroup); isGroup {
|
|
for _, memberTag := range group.All() {
|
|
if member, loaded := outboundManager.Outbound(memberTag); loaded {
|
|
if chainReachesWireGuard(outboundManager, member, visited) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// suspendAmneziaWGConsumers walks UP from tag via the reverse-dependency ledger
|
|
// (ConsumersOf) and suspends every AmneziaWG endpoint that detours through it,
|
|
// directly or transitively (e.g. AWG -> vless -> group). visited guards cycles.
|
|
func suspendAmneziaWGConsumers(outboundManager adapter.OutboundManager, tag string, visited map[string]bool) {
|
|
for _, consumerTag := range outboundManager.ConsumersOf(tag) {
|
|
if visited[consumerTag] {
|
|
continue
|
|
}
|
|
visited[consumerTag] = true
|
|
consumer, loaded := outboundManager.Outbound(consumerTag)
|
|
if !loaded {
|
|
continue
|
|
}
|
|
if awg, isAWG := consumer.(adapter.AmneziaWGSuspendable); isAWG && awg.IsAmneziaWG() {
|
|
awg.SuspendAmneziaWG()
|
|
}
|
|
// Keep walking up: a non-AWG hop (vless) or a parent group may itself have
|
|
// an AmneziaWG consumer above it.
|
|
suspendAmneziaWGConsumers(outboundManager, consumerTag, visited)
|
|
}
|
|
}
|
|
|
|
// lx:end awg
|