143 lines
4.8 KiB
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
|
|
}
|