From 85db0efc59a007f5f78aa13918ff673b64d0ff1b Mon Sep 17 00:00:00 2001 From: Umbrix Developer Date: Tue, 20 Jan 2026 17:38:25 +0300 Subject: [PATCH] feat: Add per-app proxy (Split Tunneling) for desktop platforms - Add PerAppProxyOptions struct with Mode, IncludedApplications, ExcludedApplications - Implement routing rules for include/exclude modes - Include mode: selected apps use VPN, others go direct - Exclude mode: selected apps bypass VPN, others use VPN - Only active on non-Android platforms (Windows, Linux, macOS) - Logging added for debugging per-app routing decisions Part of v1.7.6 Split Tunneling feature --- config/config.go | 48 ++++++++++++++++++++++++++++++++++++++++ config/hiddify_option.go | 7 ++++++ 2 files changed, 55 insertions(+) diff --git a/config/config.go b/config/config.go index 1cabed2..75933a5 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ import ( "time" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" dns "github.com/sagernet/sing-dns" ) @@ -494,6 +495,53 @@ func setRoutingOptions(options *option.Options, opt *HiddifyOptions) { }, ) } + + // Per-App Proxy for Desktop platforms (Windows, Linux, macOS) + if runtime.GOOS != "android" { + if opt.PerAppProxyOptions.Mode == "include" && len(opt.PerAppProxyOptions.IncludedApplications) > 0 { + // Mode: Only selected apps use VPN + log.Info("[Per-App] Mode: include - ", len(opt.PerAppProxyOptions.IncludedApplications), " apps will use VPN") + + // Rule 1: Selected apps → VPN + routeRules = append( + routeRules, + option.Rule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + ProcessName: opt.PerAppProxyOptions.IncludedApplications, + Outbound: OutboundSelectTag, + }, + }, + ) + + // Rule 2: All other apps → Direct + routeRules = append( + routeRules, + option.Rule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + Outbound: OutboundDirectTag, + }, + }, + ) + } else if opt.PerAppProxyOptions.Mode == "exclude" && len(opt.PerAppProxyOptions.ExcludedApplications) > 0 { + // Mode: Excluded apps DON'T use VPN + log.Info("[Per-App] Mode: exclude - ", len(opt.PerAppProxyOptions.ExcludedApplications), " apps will bypass VPN") + + // Rule: Excluded apps → Direct (rest goes through VPN by default) + routeRules = append( + routeRules, + option.Rule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + ProcessName: opt.PerAppProxyOptions.ExcludedApplications, + Outbound: OutboundDirectTag, + }, + }, + ) + } + } + routeRules = append(routeRules, option.Rule{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ diff --git a/config/hiddify_option.go b/config/hiddify_option.go index 5b13242..9aab5ea 100644 --- a/config/hiddify_option.go +++ b/config/hiddify_option.go @@ -26,6 +26,13 @@ type HiddifyOptions struct { InboundOptions URLTestOptions RouteOptions + PerAppProxyOptions +} + +type PerAppProxyOptions struct { + Mode string `json:"per-app-proxy-mode"` // "off", "include", "exclude" + IncludedApplications []string `json:"included-applications"` // ["chrome.exe", "firefox.exe"] + ExcludedApplications []string `json:"excluded-applications"` // ["steam.exe", "uTorrent.exe"] } type DNSOptions struct {