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

143 lines
4.8 KiB
Go

package app
import (
"fmt"
trafficmodepkg "selective-vpn-api/app/trafficmode"
"strings"
)
type trafficRulesState = trafficmodepkg.RulesState
func readTrafficRules() trafficRulesState {
return trafficmodepkg.ReadRules(trafficModeRulesConfig(), runCommand)
}
func detectAppliedTrafficMode(rules trafficRulesState) TrafficMode {
return TrafficMode(trafficmodepkg.DetectAppliedMode(trafficModeRulesConfig(), rules))
}
func probeTrafficMode(mode TrafficMode, iface string) (bool, string) {
return trafficmodepkg.ProbeMode(
trafficModeRulesConfig(),
string(normalizeTrafficMode(mode)),
strings.TrimSpace(iface),
runCommand,
)
}
func evaluateTrafficMode(st TrafficModeState) TrafficModeStatusResponse {
st = normalizeTrafficModeState(st)
eff := buildEffectiveOverrides(st)
advancedActive := st.Mode == TrafficModeFullTunnel
autoLocalActive := advancedActive && st.AutoLocalBypass
ingressDesired := st.IngressReplyBypass
ingressExpected := advancedActive && ingressDesired
hasVPN := len(eff.VPNSubnets) > 0 || len(eff.VPNUIDs) > 0
iface, reason := resolveTrafficIface(st.PreferredIface)
rules := readTrafficRules()
applied := detectAppliedTrafficMode(rules)
ingressNft := false
if rules.IngressReply || st.Mode == TrafficModeFullTunnel || st.IngressReplyBypass {
ingressNft = ingressReplyNftActive()
}
bypassCandidates := 0
if autoLocalActive && (st.Mode != TrafficModeDirect || hasVPN) {
bypassCandidates = len(detectAutoLocalBypassRoutes(iface))
}
overridesApplied := len(eff.VPNSubnets) + len(eff.VPNUIDs) + len(eff.DirectSubnets) + len(eff.DirectUIDs)
tableDefault := false
if iface != "" && (st.Mode != TrafficModeDirect || hasVPN) {
ok, _ := checkPolicyRoute(iface, routesTableName())
tableDefault = ok
}
res := TrafficModeStatusResponse{
Mode: st.Mode,
DesiredMode: st.Mode,
AppliedMode: applied,
PreferredIface: st.PreferredIface,
AdvancedActive: advancedActive,
AutoLocalBypass: st.AutoLocalBypass,
AutoLocalActive: autoLocalActive,
IngressReplyBypass: ingressDesired,
IngressReplyActive: rules.IngressReply && ingressNft,
BypassCandidates: bypassCandidates,
ForceVPNSubnets: append([]string(nil), st.ForceVPNSubnets...),
ForceVPNUIDs: append([]string(nil), st.ForceVPNUIDs...),
ForceVPNCGroups: append([]string(nil), st.ForceVPNCGroups...),
ForceDirectSubnets: append([]string(nil), st.ForceDirectSubnets...),
ForceDirectUIDs: append([]string(nil), st.ForceDirectUIDs...),
ForceDirectCGroups: append([]string(nil), st.ForceDirectCGroups...),
OverridesApplied: overridesApplied,
CgroupResolvedUIDs: eff.CgroupResolvedUIDs,
CgroupWarning: eff.CgroupWarning,
ActiveIface: iface,
IfaceReason: reason,
RuleMark: rules.Mark,
RuleFull: rules.Full,
IngressRulePresent: rules.IngressReply,
IngressNftActive: ingressNft,
TableDefault: tableDefault,
}
res.ProbeOK, res.ProbeMessage = probeTrafficMode(st.Mode, iface)
switch st.Mode {
case TrafficModeDirect:
// direct mode can still be healthy when vpn overrides exist
// (base full/selective rules must be absent).
if hasVPN {
res.Healthy = !rules.Mark && !rules.Full && !rules.IngressReply && !ingressNft && tableDefault && iface != "" && res.ProbeOK
} else {
res.Healthy = !rules.Mark && !rules.Full && !rules.IngressReply && !ingressNft && res.ProbeOK
}
case TrafficModeFullTunnel:
if ingressExpected {
res.Healthy = rules.Full && !rules.Mark && rules.IngressReply && ingressNft && tableDefault && iface != "" && res.ProbeOK
} else {
res.Healthy = rules.Full && !rules.Mark && !rules.IngressReply && !ingressNft && tableDefault && iface != "" && res.ProbeOK
}
case TrafficModeSelective:
res.Healthy = rules.Mark && !rules.Full && !rules.IngressReply && !ingressNft && tableDefault && iface != "" && res.ProbeOK
default:
res.Healthy = false
}
if res.Healthy {
res.Message = "traffic mode applied"
return res
}
if iface == "" && (st.Mode != TrafficModeDirect || hasVPN) {
res.Message = "vpn interface not found"
return res
}
if st.Mode != applied {
res.Message = fmt.Sprintf("desired=%s applied=%s mismatch", st.Mode, applied)
return res
}
if (st.Mode != TrafficModeDirect || hasVPN) && !tableDefault {
res.Message = "policy table default route is missing"
return res
}
if !res.ProbeOK {
res.Message = res.ProbeMessage
return res
}
if rules.Mark && rules.Full {
res.Message = "conflicting traffic rules detected"
return res
}
if ingressExpected && (!rules.IngressReply || !ingressNft) {
res.Message = "ingress-reply bypass rule is not active"
return res
}
if !ingressExpected && (rules.IngressReply || ingressNft) {
res.Message = "stale ingress-reply bypass rule is active"
return res
}
res.Message = "traffic mode check failed"
return res
}