From 933323d5f9659f371b174a19802a52113a393c85 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Fri, 17 Apr 2026 20:48:48 +0300 Subject: [PATCH] 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. --- .../safing/portmaster-api/src/lib/netquery.service.ts | 1 + .../safing/portmaster-api/src/lib/network.types.ts | 3 ++- .../src/app/shared/netquery/netquery.component.ts | 3 ++- desktop/angular/src/theme/_verdict.scss | 3 ++- service/firewall/interception/packet_tracer.go | 5 +++++ service/firewall/interception/windowskext/packet.go | 8 ++++++++ service/firewall/interception/windowskext2/kext.go | 2 ++ service/firewall/interception/windowskext2/packet.go | 8 ++++++++ service/firewall/packet_handler.go | 2 ++ service/nameserver/nameserver.go | 3 ++- service/network/api.go | 1 + service/network/connection.go | 2 +- service/network/dns.go | 10 +++------- service/network/metrics.go | 2 +- service/network/packet/info_only.go | 4 ++++ service/network/packet/packet.go | 1 + service/network/status.go | 5 +++++ windows_kext/kextinterface/command.go | 1 + 18 files changed, 51 insertions(+), 13 deletions(-) diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts index c0b1ec88..9769d454 100644 --- a/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts @@ -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; diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts index 6cdef998..35a75861 100644 --- a/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts @@ -8,7 +8,8 @@ export enum Verdict { Drop = 4, RerouteToNs = 5, RerouteToTunnel = 6, - Failed = 7 + Failed = 7, + RerouteToSplitTun = 8 } export enum IPProtocol { diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.ts b/desktop/angular/src/app/shared/netquery/netquery.component.ts index 5442b02b..170d8500 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.ts +++ b/desktop/angular/src/app/shared/netquery/netquery.component.ts @@ -1206,7 +1206,8 @@ export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { $in: [ Verdict.Accept, Verdict.RerouteToNs, - Verdict.RerouteToTunnel + Verdict.RerouteToTunnel, + Verdict.RerouteToSplitTun ], } }, diff --git a/desktop/angular/src/theme/_verdict.scss b/desktop/angular/src/theme/_verdict.scss index 9f4a2730..f5e480c1 100644 --- a/desktop/angular/src/theme/_verdict.scss +++ b/desktop/angular/src/theme/_verdict.scss @@ -28,7 +28,8 @@ span.verdict { &.accept, &.reroutetons, - &.reroutetotunnel { + &.reroutetotunnel, + &.reroutetosplittun { --bg-color: theme('colors.info.green'); } diff --git a/service/firewall/interception/packet_tracer.go b/service/firewall/interception/packet_tracer.go index b90dfbf7..d7f466d1 100644 --- a/service/firewall/interception/packet_tracer.go +++ b/service/firewall/interception/packet_tracer.go @@ -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() +} diff --git a/service/firewall/interception/windowskext/packet.go b/service/firewall/interception/windowskext/packet.go index 9145926c..4b8ead53 100644 --- a/service/firewall/interception/windowskext/packet.go +++ b/service/firewall/interception/windowskext/packet.go @@ -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 +} diff --git a/service/firewall/interception/windowskext2/kext.go b/service/firewall/interception/windowskext2/kext.go index 43be43cd..50559454 100644 --- a/service/firewall/interception/windowskext2/kext.go +++ b/service/firewall/interception/windowskext2/kext.go @@ -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 } diff --git a/service/firewall/interception/windowskext2/packet.go b/service/firewall/interception/windowskext2/packet.go index 00d95036..fcba82f6 100644 --- a/service/firewall/interception/windowskext2/packet.go +++ b/service/firewall/interception/windowskext2/packet.go @@ -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 +} diff --git a/service/firewall/packet_handler.go b/service/firewall/packet_handler.go index b2ff3358..6e96cb28 100644 --- a/service/firewall/packet_handler.go +++ b/service/firewall/packet_handler.go @@ -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() diff --git a/service/nameserver/nameserver.go b/service/nameserver/nameserver.go index 1d346220..b181f35b 100644 --- a/service/nameserver/nameserver.go +++ b/service/nameserver/nameserver.go @@ -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 diff --git a/service/network/api.go b/service/network/api.go index 5c18bcfd..601d4bb3 100644 --- a/service/network/api.go +++ b/service/network/api.go @@ -167,6 +167,7 @@ func AddNetworkDebugData(di *debug.Info, profile, where string) { switch conn.Verdict { //nolint:exhaustive case VerdictAccept, VerdictRerouteToNameserver, + VerdictRerouteToSplitTun, VerdictRerouteToTunnel: accepted++ diff --git a/service/network/connection.go b/service/network/connection.go index 2cdf12e7..b673fbe7 100644 --- a/service/network/connection.go +++ b/service/network/connection.go @@ -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 diff --git a/service/network/dns.go b/service/network/dns.go index e8cdbb63..2eb6ac10 100644 --- a/service/network/dns.go +++ b/service/network/dns.go @@ -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 } diff --git a/service/network/metrics.go b/service/network/metrics.go index e64ed163..bad739f3 100644 --- a/service/network/metrics.go +++ b/service/network/metrics.go @@ -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. diff --git a/service/network/packet/info_only.go b/service/network/packet/info_only.go index cc7ef9aa..09e349e5 100644 --- a/service/network/packet/info_only.go +++ b/service/network/packet/info_only.go @@ -74,4 +74,8 @@ func (pkt *InfoPacket) RerouteToTunnel() error { return ErrInfoOnlyPacket } +func (pkt *InfoPacket) RerouteToSplitTun() error { + return ErrInfoOnlyPacket +} + var _ Packet = &InfoPacket{} diff --git a/service/network/packet/packet.go b/service/network/packet/packet.go index 18aa7eb2..57bf1eb2 100644 --- a/service/network/packet/packet.go +++ b/service/network/packet/packet.go @@ -231,6 +231,7 @@ type Packet interface { PermanentDrop() error RerouteToNameserver() error RerouteToTunnel() error + RerouteToSplitTun() error FastTrackedByIntegration() bool InfoOnly() bool ExpectInfo() bool diff --git a/service/network/status.go b/service/network/status.go index 1cd633fe..2223b629 100644 --- a/service/network/status.go +++ b/service/network/status.go @@ -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: diff --git a/windows_kext/kextinterface/command.go b/windows_kext/kextinterface/command.go index b40be18c..7d675fd9 100644 --- a/windows_kext/kextinterface/command.go +++ b/windows_kext/kextinterface/command.go @@ -36,6 +36,7 @@ const ( VerdictRerouteToNameserver KextVerdict = 8 VerdictRerouteToTunnel KextVerdict = 9 VerdictFailed KextVerdict = 10 + VerdictRerouteToSplitTun KextVerdict = 11 ) type Verdict struct {