feat: add VerdictRerouteToSplitTun verdict type

Add a new verdict (value 8) for routing connections through the split
tunnel. This prepares the infrastructure for the upcoming split-tunneling
feature without implementing the full feature yet.

Changes:
- Define VerdictRerouteToSplitTun in network/status.go with String() and Verb()
- Add RerouteToSplitTun() to the Packet interface and InfoPacket stub
- Implement RerouteToSplitTun() for windowskext (v1) and windowskext2 (v2) packets
- Map VerdictRerouteToSplitTun to KextVerdict 11 in kextinterface and kext2
- Handle the verdict in packet_handler.go dispatch, connection.go, api.go,
  metrics.go and nameserver.go
- Add VerdictRerouteToSplitTun = 8 to Angular Verdict enum and update
  stats counting, filter queries and verdict CSS class

(WIP) Note: Linux (nfq) implementation not updated yet. Therefore Linux build will fail.
This commit is contained in:
Alexandr Stelnykovych 2026-04-17 20:48:48 +03:00
parent 52bfe1750f
commit 933323d5f9
18 changed files with 51 additions and 13 deletions

View file

@ -459,6 +459,7 @@ export class Netquery {
case Verdict.Accept:
case Verdict.RerouteToNs:
case Verdict.RerouteToTunnel:
case Verdict.RerouteToSplitTun:
case Verdict.Undeterminable:
stats.size += res.totalCount
stats.countAllowed += res.totalCount;

View file

@ -8,7 +8,8 @@ export enum Verdict {
Drop = 4,
RerouteToNs = 5,
RerouteToTunnel = 6,
Failed = 7
Failed = 7,
RerouteToSplitTun = 8
}
export enum IPProtocol {

View file

@ -1206,7 +1206,8 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit {
$in: [
Verdict.Accept,
Verdict.RerouteToNs,
Verdict.RerouteToTunnel
Verdict.RerouteToTunnel,
Verdict.RerouteToSplitTun
],
}
},

View file

@ -28,7 +28,8 @@ span.verdict {
&.accept,
&.reroutetons,
&.reroutetotunnel {
&.reroutetotunnel,
&.reroutetosplittun {
--bg-color: theme('colors.info.green');
}

View file

@ -65,3 +65,8 @@ func (p *tracedPacket) RerouteToTunnel() error {
defer p.markServed("reroute-tunnel")
return p.Packet.RerouteToTunnel()
}
func (p *tracedPacket) RerouteToSplitTun() error {
defer p.markServed("reroute-splittun")
return p.Packet.RerouteToSplitTun()
}

View file

@ -135,3 +135,11 @@ func (pkt *Packet) RerouteToTunnel() error {
}
return nil
}
// RerouteToSplitTun permanently reroutes the connection to the split tunnel (and the current packet).
func (pkt *Packet) RerouteToSplitTun() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, network.VerdictRerouteToSplitTun)
}
return nil
}

View file

@ -183,6 +183,8 @@ func getKextVerdictFromConnection(conn *network.Connection) kextinterface.KextVe
return kextinterface.VerdictRerouteToNameserver
case network.VerdictRerouteToTunnel:
return kextinterface.VerdictRerouteToTunnel
case network.VerdictRerouteToSplitTun:
return kextinterface.VerdictRerouteToSplitTun
case network.VerdictFailed:
return kextinterface.VerdictFailed
}

View file

@ -140,3 +140,11 @@ func (pkt *Packet) RerouteToTunnel() error {
}
return nil
}
// RerouteToSplitTun permanently reroutes the connection to the local split tunnel entrypoint (and the current packet).
func (pkt *Packet) RerouteToSplitTun() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, kextinterface.VerdictRerouteToSplitTun)
}
return nil
}

View file

@ -844,6 +844,8 @@ func issueVerdict(conn *network.Connection, pkt packet.Packet, verdict network.V
err = pkt.RerouteToNameserver()
case network.VerdictRerouteToTunnel:
err = pkt.RerouteToTunnel()
case network.VerdictRerouteToSplitTun:
err = pkt.RerouteToSplitTun()
case network.VerdictFailed:
atomic.AddUint64(packetsFailed, 1)
err = pkt.Drop()

View file

@ -205,7 +205,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
switch conn.Verdict {
// We immediately save blocked, dropped or failed verdicts so
// they pop up in the UI.
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed, network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel:
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed,
network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel, network.VerdictRerouteToSplitTun:
conn.Save()
// For undecided or accepted connections we don't save them yet, because

View file

@ -167,6 +167,7 @@ func AddNetworkDebugData(di *debug.Info, profile, where string) {
switch conn.Verdict { //nolint:exhaustive
case VerdictAccept,
VerdictRerouteToNameserver,
VerdictRerouteToSplitTun,
VerdictRerouteToTunnel:
accepted++

View file

@ -795,7 +795,7 @@ func (conn *Connection) Save() {
// nolint:exhaustive
switch conn.Verdict {
case VerdictAccept, VerdictRerouteToNameserver:
case VerdictAccept, VerdictRerouteToNameserver, VerdictRerouteToSplitTun:
conn.ConnectionEstablished = true
case VerdictRerouteToTunnel:
// this is already handled when the connection tunnel has been

View file

@ -216,10 +216,10 @@ func (conn *Connection) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns
return nil // Do not respond to request.
case VerdictFailed:
return nsutil.BlockIP().ReplyWithDNS(ctx, request)
case VerdictUndecided, VerdictUndeterminable,
VerdictAccept, VerdictRerouteToNameserver, VerdictRerouteToTunnel:
fallthrough
default:
// ReplyWithDNS is called when a DNS response to a DNS message is
// crafted because the request is either denied or blocked.
// So, other verdicts are not expected here.
reply := nsutil.ServerFailure().ReplyWithDNS(ctx, request)
nsutil.AddMessagesToReply(ctx, reply, log.ErrorLevel, "INTERNAL ERROR: incorrect use of Connection DNS Responder")
return reply
@ -233,10 +233,6 @@ func (conn *Connection) GetExtraRRs(ctx context.Context, request *dns.Msg) []dns
switch conn.Verdict {
case VerdictFailed:
level = log.ErrorLevel
case VerdictUndecided, VerdictUndeterminable,
VerdictAccept, VerdictBlock, VerdictDrop,
VerdictRerouteToNameserver, VerdictRerouteToTunnel:
fallthrough
default:
level = log.InfoLevel
}

View file

@ -145,7 +145,7 @@ func (conn *Connection) addToMetrics() {
blockedOutConnCounter.Inc()
conn.addedToMetrics = true
return
case VerdictAccept, VerdictRerouteToTunnel:
case VerdictAccept, VerdictRerouteToTunnel, VerdictRerouteToSplitTun:
// Continue to next section.
default:
// Connection is not counted.

View file

@ -74,4 +74,8 @@ func (pkt *InfoPacket) RerouteToTunnel() error {
return ErrInfoOnlyPacket
}
func (pkt *InfoPacket) RerouteToSplitTun() error {
return ErrInfoOnlyPacket
}
var _ Packet = &InfoPacket{}

View file

@ -231,6 +231,7 @@ type Packet interface {
PermanentDrop() error
RerouteToNameserver() error
RerouteToTunnel() error
RerouteToSplitTun() error
FastTrackedByIntegration() bool
InfoOnly() bool
ExpectInfo() bool

View file

@ -15,6 +15,7 @@ const (
VerdictRerouteToNameserver Verdict = 5
VerdictRerouteToTunnel Verdict = 6
VerdictFailed Verdict = 7
VerdictRerouteToSplitTun Verdict = 8
)
func (v Verdict) String() string {
@ -33,6 +34,8 @@ func (v Verdict) String() string {
return "RerouteToNameserver"
case VerdictRerouteToTunnel:
return "RerouteToTunnel"
case VerdictRerouteToSplitTun:
return "RerouteToSplitTun"
case VerdictFailed:
return "Failed"
default:
@ -57,6 +60,8 @@ func (v Verdict) Verb() string {
return "redirected to nameserver"
case VerdictRerouteToTunnel:
return "tunneled"
case VerdictRerouteToSplitTun:
return "split tunneled"
case VerdictFailed:
return "failed"
default:

View file

@ -36,6 +36,7 @@ const (
VerdictRerouteToNameserver KextVerdict = 8
VerdictRerouteToTunnel KextVerdict = 9
VerdictFailed KextVerdict = 10
VerdictRerouteToSplitTun KextVerdict = 11
)
type Verdict struct {