Files
elmprodvpn/selective-vpn-api/app/traffic_mode_apply.go

153 lines
5.0 KiB
Go

package app
import (
"fmt"
trafficmodepkg "selective-vpn-api/app/trafficmode"
"strings"
)
type effectiveTrafficOverrides struct {
VPNSubnets []string
VPNUIDs []string
DirectSubnets []string
DirectUIDs []string
CgroupResolvedUIDs int
CgroupWarning string
}
func buildEffectiveOverrides(st TrafficModeState) effectiveTrafficOverrides {
st = normalizeTrafficModeState(st)
e := effectiveTrafficOverrides{
VPNSubnets: append([]string(nil), st.ForceVPNSubnets...),
VPNUIDs: append([]string(nil), st.ForceVPNUIDs...),
DirectSubnets: append([]string(nil), st.ForceDirectSubnets...),
DirectUIDs: append([]string(nil), st.ForceDirectUIDs...),
}
vpnUIDsFromCG, warnVPN := trafficmodepkg.ResolveCgroupUIDRanges(st.ForceVPNCGroups, cgroupRootPath)
directUIDsFromCG, warnDirect := trafficmodepkg.ResolveCgroupUIDRanges(st.ForceDirectCGroups, cgroupRootPath)
e.CgroupResolvedUIDs = len(vpnUIDsFromCG) + len(directUIDsFromCG)
e.VPNUIDs = normalizeUIDList(append(e.VPNUIDs, vpnUIDsFromCG...))
e.DirectUIDs = normalizeUIDList(append(e.DirectUIDs, directUIDsFromCG...))
warns := make([]string, 0, 2)
if strings.TrimSpace(warnVPN) != "" {
warns = append(warns, strings.TrimSpace(warnVPN))
}
if strings.TrimSpace(warnDirect) != "" {
warns = append(warns, strings.TrimSpace(warnDirect))
}
e.CgroupWarning = strings.Join(warns, "; ")
return e
}
func applyRule(pref int, args ...string) error {
return trafficmodepkg.ApplyRule(pref, runCommand, args...)
}
func applyTrafficOverrides(e effectiveTrafficOverrides) (int, error) {
return trafficmodepkg.ApplyOverrides(
trafficModeOverrideConfig(),
trafficmodepkg.EffectiveOverrides{
VPNSubnets: append([]string(nil), e.VPNSubnets...),
VPNUIDs: append([]string(nil), e.VPNUIDs...),
DirectSubnets: append([]string(nil), e.DirectSubnets...),
DirectUIDs: append([]string(nil), e.DirectUIDs...),
},
applyRule,
)
}
func ensureTrafficRouteBase(iface string, autoLocalBypass bool) error {
iface = strings.TrimSpace(iface)
if iface == "" {
return fmt.Errorf("empty interface")
}
if !ifaceExists(iface) {
return fmt.Errorf("interface not found: %s", iface)
}
ensureRoutesTableEntry()
if _, _, code, err := runCommand("ip", "-4", "route", "replace", "default", "dev", iface, "table", routesTableName(), "mtu", policyRouteMTU); err != nil || code != 0 {
if err == nil {
err = fmt.Errorf("ip route replace default exited with %d", code)
}
return err
}
if autoLocalBypass {
applyAutoLocalBypass(iface)
}
return nil
}
func applyTrafficMode(st TrafficModeState, iface string) error {
st = normalizeTrafficModeState(st)
eff := buildEffectiveOverrides(st)
advancedActive := st.Mode == TrafficModeFullTunnel
autoLocalActive := advancedActive && st.AutoLocalBypass
ingressReplyActive := advancedActive && st.IngressReplyBypass
removeTrafficRulesForTable()
// EN: Ensure the policy table name exists even in direct mode so mark-based rules can be installed.
// RU: Гарантируем наличие имени policy-table даже в direct режиме, чтобы можно было ставить mark-правила.
ensureRoutesTableEntry()
if err := disableIngressReplyBypass(); err != nil {
return err
}
needVPNTable := st.Mode != TrafficModeDirect || len(eff.VPNSubnets) > 0 || len(eff.VPNUIDs) > 0
if needVPNTable {
if err := ensureTrafficRouteBase(iface, autoLocalActive); err != nil {
return err
}
}
if _, err := applyTrafficOverrides(eff); err != nil {
return err
}
// EN: Mark-based per-app routing support (cgroup-based marking in nftables).
// EN: These rules are safe even when no packets are marked with MARK_APP/MARK_DIRECT.
// RU: Поддержка per-app маршрутизации по mark (cgroup-based marking в nftables).
// RU: Эти правила безопасны, если пакеты не помечаются MARK_APP/MARK_DIRECT.
if err := applyRule(trafficRulePrefMarkDirect, "fwmark", MARK_DIRECT, "lookup", "main"); err != nil {
return err
}
if ingressReplyActive {
if err := applyRule(trafficRulePrefMarkIngressReply, "fwmark", MARK_INGRESS, "lookup", "main"); err != nil {
return err
}
}
if err := applyRule(trafficRulePrefMarkAppVPN, "fwmark", MARK_APP, "lookup", routesTableName()); err != nil {
return err
}
switch st.Mode {
case TrafficModeFullTunnel:
if err := applyRule(trafficRulePrefFull, "lookup", routesTableName()); err != nil {
return err
}
case TrafficModeSelective:
if err := applyRule(trafficRulePrefSelective, "fwmark", MARK, "lookup", routesTableName()); err != nil {
return err
}
case TrafficModeDirect:
// direct mode relies only on optional direct/vpn overrides.
default:
return fmt.Errorf("unknown traffic mode: %s", st.Mode)
}
if ingressReplyActive {
if err := enableIngressReplyBypass(iface); err != nil {
return err
}
}
if err := restoreAppMarksFromState(); err != nil {
appendTraceLine("traffic", fmt.Sprintf("appmarks restore warning: %v", err))
}
return nil
}