Harden resolver and expand traffic runtime controls
This commit is contained in:
@@ -11,11 +11,13 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
trafficRulePrefMarkDirect = 11500
|
||||
trafficRulePrefMarkIngressReply = 11505
|
||||
trafficRulePrefMarkAppVPN = 11510
|
||||
trafficRulePrefDirectSubnetStart = 11600
|
||||
trafficRulePrefDirectUIDStart = 11680
|
||||
@@ -27,6 +29,13 @@ const (
|
||||
trafficRulePrefManagedMax = 12099
|
||||
trafficRulePerKindLimit = 70
|
||||
trafficAutoLocalDefault = true
|
||||
trafficIngressReplyDefault = false
|
||||
|
||||
trafficIngressPreroutingChain = "prerouting_ingress_reply"
|
||||
trafficIngressOutputChain = "output_ingress_reply"
|
||||
|
||||
trafficIngressCaptureComment = "svpn_ingress_reply_capture"
|
||||
trafficIngressRestoreComment = "svpn_ingress_reply_restore"
|
||||
)
|
||||
|
||||
var cgnatPrefix = netip.MustParsePrefix("100.64.0.0/10")
|
||||
@@ -199,6 +208,7 @@ func loadTrafficModeState() TrafficModeState {
|
||||
Mode TrafficMode `json:"mode"`
|
||||
PreferredIface string `json:"preferred_iface,omitempty"`
|
||||
AutoLocalBypass *bool `json:"auto_local_bypass,omitempty"`
|
||||
IngressReplyBypass *bool `json:"ingress_reply_bypass,omitempty"`
|
||||
ForceVPNSubnets []string `json:"force_vpn_subnets,omitempty"`
|
||||
ForceVPNUIDs []string `json:"force_vpn_uids,omitempty"`
|
||||
ForceVPNCGroups []string `json:"force_vpn_cgroups,omitempty"`
|
||||
@@ -214,6 +224,7 @@ func loadTrafficModeState() TrafficModeState {
|
||||
Mode: raw.Mode,
|
||||
PreferredIface: raw.PreferredIface,
|
||||
AutoLocalBypass: trafficAutoLocalDefault,
|
||||
IngressReplyBypass: trafficIngressReplyDefault,
|
||||
ForceVPNSubnets: append([]string(nil), raw.ForceVPNSubnets...),
|
||||
ForceVPNUIDs: append([]string(nil), raw.ForceVPNUIDs...),
|
||||
ForceVPNCGroups: append([]string(nil), raw.ForceVPNCGroups...),
|
||||
@@ -224,6 +235,9 @@ func loadTrafficModeState() TrafficModeState {
|
||||
if raw.AutoLocalBypass != nil {
|
||||
st.AutoLocalBypass = *raw.AutoLocalBypass
|
||||
}
|
||||
if raw.IngressReplyBypass != nil {
|
||||
st.IngressReplyBypass = *raw.IngressReplyBypass
|
||||
}
|
||||
return normalizeTrafficModeState(st)
|
||||
}
|
||||
|
||||
@@ -253,6 +267,7 @@ func inferTrafficModeState() TrafficModeState {
|
||||
Mode: mode,
|
||||
PreferredIface: iface,
|
||||
AutoLocalBypass: trafficAutoLocalDefault,
|
||||
IngressReplyBypass: trafficIngressReplyDefault,
|
||||
ForceVPNSubnets: nil,
|
||||
ForceVPNUIDs: nil,
|
||||
ForceVPNCGroups: nil,
|
||||
@@ -529,6 +544,116 @@ func applyAutoLocalBypass(vpnIface string) {
|
||||
}
|
||||
}
|
||||
|
||||
func nftObjectMissing(stdout, stderr string) bool {
|
||||
text := strings.ToLower(strings.TrimSpace(stdout + " " + stderr))
|
||||
return strings.Contains(text, "no such file") || strings.Contains(text, "not found")
|
||||
}
|
||||
|
||||
func ensureIngressReplyBypassChains() {
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "table", "inet", routesTableName())
|
||||
_, _, _, _ = runCommandTimeout(
|
||||
5*time.Second,
|
||||
"nft", "add", "chain", "inet", routesTableName(), trafficIngressPreroutingChain,
|
||||
"{", "type", "filter", "hook", "prerouting", "priority", "mangle;", "policy", "accept;", "}",
|
||||
)
|
||||
_, _, _, _ = runCommandTimeout(
|
||||
5*time.Second,
|
||||
"nft", "add", "chain", "inet", routesTableName(), trafficIngressOutputChain,
|
||||
"{", "type", "route", "hook", "output", "priority", "mangle;", "policy", "accept;", "}",
|
||||
)
|
||||
}
|
||||
|
||||
func flushIngressReplyBypassChains() error {
|
||||
for _, chain := range []string{trafficIngressPreroutingChain, trafficIngressOutputChain} {
|
||||
out, errOut, code, err := runCommandTimeout(5*time.Second, "nft", "flush", "chain", "inet", routesTableName(), chain)
|
||||
if err == nil && code == 0 {
|
||||
continue
|
||||
}
|
||||
if nftObjectMissing(out, errOut) {
|
||||
continue
|
||||
}
|
||||
if err == nil {
|
||||
err = fmt.Errorf("nft flush chain exited with %d", code)
|
||||
}
|
||||
return fmt.Errorf("flush %s failed: %w (%s %s)", chain, err, strings.TrimSpace(out), strings.TrimSpace(errOut))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func enableIngressReplyBypass(vpnIface string) error {
|
||||
vpnIface = strings.TrimSpace(vpnIface)
|
||||
if vpnIface == "" {
|
||||
return fmt.Errorf("empty vpn iface for ingress bypass")
|
||||
}
|
||||
|
||||
ensureIngressReplyBypassChains()
|
||||
if err := flushIngressReplyBypassChains(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addRule := func(chain string, args ...string) error {
|
||||
out, errOut, code, err := runCommandTimeout(5*time.Second, "nft", append([]string{"add", "rule", "inet", routesTableName(), chain}, args...)...)
|
||||
if err != nil || code != 0 {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("nft add rule exited with %d", code)
|
||||
}
|
||||
return fmt.Errorf("nft add rule %s failed: %w (%s %s)", chain, err, strings.TrimSpace(out), strings.TrimSpace(errOut))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EN: Mark inbound NEW connections (except loopback/VPN iface) so reply path can stay direct in full tunnel.
|
||||
// RU: Помечаем входящие NEW-соединения (кроме loopback/VPN iface), чтобы ответ шел напрямую в full tunnel.
|
||||
if err := addRule(
|
||||
trafficIngressPreroutingChain,
|
||||
"iifname", "!=", "lo",
|
||||
"iifname", "!=", vpnIface,
|
||||
"fib", "daddr", "type", "local",
|
||||
"ct", "state", "new",
|
||||
"ct", "mark", "set", MARK_INGRESS,
|
||||
"comment", trafficIngressCaptureComment,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
// EN: Restore fwmark from ct mark in prerouting for forwarded reply traffic.
|
||||
// RU: Восстанавливаем fwmark из ct mark в prerouting для forwarded-ответов.
|
||||
if err := addRule(
|
||||
trafficIngressPreroutingChain,
|
||||
"ct", "mark", MARK_INGRESS,
|
||||
"meta", "mark", "set", MARK_INGRESS,
|
||||
"comment", trafficIngressRestoreComment,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
// EN: Restore fwmark from ct mark in output for local-process replies.
|
||||
// RU: Восстанавливаем fwmark из ct mark в output для ответов локальных процессов.
|
||||
if err := addRule(
|
||||
trafficIngressOutputChain,
|
||||
"ct", "mark", MARK_INGRESS,
|
||||
"meta", "mark", "set", MARK_INGRESS,
|
||||
"comment", trafficIngressRestoreComment,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func disableIngressReplyBypass() error {
|
||||
ensureIngressReplyBypassChains()
|
||||
return flushIngressReplyBypassChains()
|
||||
}
|
||||
|
||||
func ingressReplyNftActive() bool {
|
||||
outPre, _, codePre, _ := runCommandTimeout(5*time.Second, "nft", "-a", "list", "chain", "inet", routesTableName(), trafficIngressPreroutingChain)
|
||||
outOut, _, codeOut, _ := runCommandTimeout(5*time.Second, "nft", "-a", "list", "chain", "inet", routesTableName(), trafficIngressOutputChain)
|
||||
if codePre != 0 || codeOut != 0 {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(outPre, trafficIngressCaptureComment) &&
|
||||
strings.Contains(outPre, trafficIngressRestoreComment) &&
|
||||
strings.Contains(outOut, trafficIngressRestoreComment)
|
||||
}
|
||||
|
||||
func prefStr(v int) string {
|
||||
return strconv.Itoa(v)
|
||||
}
|
||||
@@ -827,16 +952,22 @@ func ensureTrafficRouteBase(iface string, autoLocalBypass bool) error {
|
||||
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, st.AutoLocalBypass); err != nil {
|
||||
if err := ensureTrafficRouteBase(iface, autoLocalActive); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -852,6 +983,11 @@ func applyTrafficMode(st TrafficModeState, iface string) error {
|
||||
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
|
||||
}
|
||||
@@ -870,13 +1006,23 @@ func applyTrafficMode(st TrafficModeState, iface string) error {
|
||||
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
|
||||
}
|
||||
|
||||
type trafficRulesState struct {
|
||||
Mark bool
|
||||
Full bool
|
||||
Mark bool
|
||||
Full bool
|
||||
IngressReply bool
|
||||
}
|
||||
|
||||
func readTrafficRules() trafficRulesState {
|
||||
@@ -884,7 +1030,7 @@ func readTrafficRules() trafficRulesState {
|
||||
var st trafficRulesState
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
l := strings.ToLower(strings.TrimSpace(line))
|
||||
if l == "" || !strings.Contains(l, "lookup "+routesTableName()) {
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(l)
|
||||
@@ -895,9 +1041,17 @@ func readTrafficRules() trafficRulesState {
|
||||
pref, _ := strconv.Atoi(prefRaw)
|
||||
switch pref {
|
||||
case trafficRulePrefSelective:
|
||||
st.Mark = true
|
||||
if strings.Contains(l, "lookup "+routesTableName()) {
|
||||
st.Mark = true
|
||||
}
|
||||
case trafficRulePrefFull:
|
||||
st.Full = true
|
||||
if strings.Contains(l, "lookup "+routesTableName()) {
|
||||
st.Full = true
|
||||
}
|
||||
case trafficRulePrefMarkIngressReply:
|
||||
if strings.Contains(l, "fwmark "+strings.ToLower(MARK_INGRESS)) && strings.Contains(l, "lookup main") {
|
||||
st.IngressReply = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return st
|
||||
@@ -954,12 +1108,20 @@ func probeTrafficMode(mode TrafficMode, iface string) (bool, string) {
|
||||
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 st.AutoLocalBypass && (st.Mode != TrafficModeDirect || hasVPN) {
|
||||
if autoLocalActive && (st.Mode != TrafficModeDirect || hasVPN) {
|
||||
bypassCandidates = len(detectAutoLocalBypassRoutes(iface))
|
||||
}
|
||||
|
||||
@@ -976,7 +1138,11 @@ func evaluateTrafficMode(st TrafficModeState) TrafficModeStatusResponse {
|
||||
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...),
|
||||
@@ -991,6 +1157,8 @@ func evaluateTrafficMode(st TrafficModeState) TrafficModeStatusResponse {
|
||||
IfaceReason: reason,
|
||||
RuleMark: rules.Mark,
|
||||
RuleFull: rules.Full,
|
||||
IngressRulePresent: rules.IngressReply,
|
||||
IngressNftActive: ingressNft,
|
||||
TableDefault: tableDefault,
|
||||
}
|
||||
|
||||
@@ -1001,14 +1169,18 @@ func evaluateTrafficMode(st TrafficModeState) TrafficModeStatusResponse {
|
||||
// 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 && tableDefault && iface != "" && res.ProbeOK
|
||||
res.Healthy = !rules.Mark && !rules.Full && !rules.IngressReply && !ingressNft && tableDefault && iface != "" && res.ProbeOK
|
||||
} else {
|
||||
res.Healthy = !rules.Mark && !rules.Full && res.ProbeOK
|
||||
res.Healthy = !rules.Mark && !rules.Full && !rules.IngressReply && !ingressNft && res.ProbeOK
|
||||
}
|
||||
case TrafficModeFullTunnel:
|
||||
res.Healthy = rules.Full && !rules.Mark && tableDefault && iface != "" && res.ProbeOK
|
||||
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 && tableDefault && iface != "" && res.ProbeOK
|
||||
res.Healthy = rules.Mark && !rules.Full && !rules.IngressReply && !ingressNft && tableDefault && iface != "" && res.ProbeOK
|
||||
default:
|
||||
res.Healthy = false
|
||||
}
|
||||
@@ -1037,6 +1209,14 @@ func evaluateTrafficMode(st TrafficModeState) TrafficModeStatusResponse {
|
||||
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
|
||||
}
|
||||
@@ -1067,12 +1247,102 @@ func handleTrafficModeTest(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, evaluateTrafficMode(st))
|
||||
}
|
||||
|
||||
func acquireTrafficApplyLock() (*os.File, *TrafficModeStatusResponse) {
|
||||
lock, err := os.OpenFile(lockFile, os.O_CREATE|os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
msg := evaluateTrafficMode(loadTrafficModeState())
|
||||
msg.Message = "traffic lock open failed: " + err.Error()
|
||||
return nil, &msg
|
||||
}
|
||||
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
||||
_ = lock.Close()
|
||||
msg := evaluateTrafficMode(loadTrafficModeState())
|
||||
msg.Message = "traffic apply skipped: routes operation already running"
|
||||
return nil, &msg
|
||||
}
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
func handleTrafficAdvancedReset(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
lock, lockMsg := acquireTrafficApplyLock()
|
||||
if lockMsg != nil {
|
||||
writeJSON(w, http.StatusOK, *lockMsg)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = syscall.Flock(int(lock.Fd()), syscall.LOCK_UN)
|
||||
_ = lock.Close()
|
||||
}()
|
||||
|
||||
prev := normalizeTrafficModeState(loadTrafficModeState())
|
||||
next := prev
|
||||
next.AutoLocalBypass = false
|
||||
next.IngressReplyBypass = false
|
||||
|
||||
nextIface, _ := resolveTrafficIface(next.PreferredIface)
|
||||
if err := applyTrafficMode(next, nextIface); err != nil {
|
||||
prevIface, _ := resolveTrafficIface(prev.PreferredIface)
|
||||
_ = applyTrafficMode(prev, prevIface)
|
||||
msg := evaluateTrafficMode(prev)
|
||||
msg.Message = "advanced reset failed, rolled back: " + err.Error()
|
||||
writeJSON(w, http.StatusOK, msg)
|
||||
return
|
||||
}
|
||||
|
||||
if err := saveTrafficModeState(next); err != nil {
|
||||
prevIface, _ := resolveTrafficIface(prev.PreferredIface)
|
||||
_ = applyTrafficMode(prev, prevIface)
|
||||
_ = saveTrafficModeState(prev)
|
||||
msg := evaluateTrafficMode(prev)
|
||||
msg.Message = "advanced reset save failed, rolled back: " + err.Error()
|
||||
writeJSON(w, http.StatusOK, msg)
|
||||
return
|
||||
}
|
||||
|
||||
res := evaluateTrafficMode(next)
|
||||
if !res.Healthy {
|
||||
prevIface, _ := resolveTrafficIface(prev.PreferredIface)
|
||||
_ = applyTrafficMode(prev, prevIface)
|
||||
_ = saveTrafficModeState(prev)
|
||||
rolled := evaluateTrafficMode(prev)
|
||||
rolled.Message = "advanced reset verification failed, rolled back: " + res.Message
|
||||
writeJSON(w, http.StatusOK, rolled)
|
||||
return
|
||||
}
|
||||
|
||||
events.push("traffic_advanced_reset", map[string]any{
|
||||
"mode": res.Mode,
|
||||
"applied": res.AppliedMode,
|
||||
"active_iface": res.ActiveIface,
|
||||
"healthy": res.Healthy,
|
||||
"auto_local": res.AutoLocalBypass,
|
||||
"ingress_reply": res.IngressReplyBypass,
|
||||
"advanced_active": res.AdvancedActive,
|
||||
})
|
||||
res.Message = "advanced bypass reset"
|
||||
writeJSON(w, http.StatusOK, res)
|
||||
}
|
||||
|
||||
func handleTrafficMode(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
st := loadTrafficModeState()
|
||||
writeJSON(w, http.StatusOK, evaluateTrafficMode(st))
|
||||
case http.MethodPost:
|
||||
lock, lockMsg := acquireTrafficApplyLock()
|
||||
if lockMsg != nil {
|
||||
writeJSON(w, http.StatusOK, *lockMsg)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = syscall.Flock(int(lock.Fd()), syscall.LOCK_UN)
|
||||
_ = lock.Close()
|
||||
}()
|
||||
|
||||
prev := loadTrafficModeState()
|
||||
next := prev
|
||||
|
||||
@@ -1094,6 +1364,9 @@ func handleTrafficMode(w http.ResponseWriter, r *http.Request) {
|
||||
if body.AutoLocalBypass != nil {
|
||||
next.AutoLocalBypass = *body.AutoLocalBypass
|
||||
}
|
||||
if body.IngressReplyBypass != nil {
|
||||
next.IngressReplyBypass = *body.IngressReplyBypass
|
||||
}
|
||||
if body.ForceVPNSubnets != nil {
|
||||
next.ForceVPNSubnets = append([]string(nil), (*body.ForceVPNSubnets)...)
|
||||
}
|
||||
@@ -1127,21 +1400,12 @@ func handleTrafficMode(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err := saveTrafficModeState(next); err != nil {
|
||||
writeJSON(w, http.StatusOK, TrafficModeStatusResponse{
|
||||
Mode: next.Mode,
|
||||
DesiredMode: next.Mode,
|
||||
PreferredIface: next.PreferredIface,
|
||||
AutoLocalBypass: next.AutoLocalBypass,
|
||||
ForceVPNSubnets: append([]string(nil), next.ForceVPNSubnets...),
|
||||
ForceVPNUIDs: append([]string(nil), next.ForceVPNUIDs...),
|
||||
ForceVPNCGroups: append([]string(nil), next.ForceVPNCGroups...),
|
||||
ForceDirectSubnets: append([]string(nil), next.ForceDirectSubnets...),
|
||||
ForceDirectUIDs: append([]string(nil), next.ForceDirectUIDs...),
|
||||
ForceDirectCGroups: append([]string(nil), next.ForceDirectCGroups...),
|
||||
OverridesApplied: len(next.ForceVPNSubnets) + len(next.ForceVPNUIDs) + len(next.ForceDirectSubnets) + len(next.ForceDirectUIDs),
|
||||
Healthy: false,
|
||||
Message: "state save failed: " + err.Error(),
|
||||
})
|
||||
prevIface, _ := resolveTrafficIface(prev.PreferredIface)
|
||||
_ = applyTrafficMode(prev, prevIface)
|
||||
_ = saveTrafficModeState(prev)
|
||||
rolled := evaluateTrafficMode(prev)
|
||||
rolled.Message = "state save failed, rolled back: " + err.Error()
|
||||
writeJSON(w, http.StatusOK, rolled)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1161,7 +1425,11 @@ func handleTrafficMode(w http.ResponseWriter, r *http.Request) {
|
||||
"applied": res.AppliedMode,
|
||||
"active_iface": res.ActiveIface,
|
||||
"healthy": res.Healthy,
|
||||
"advanced_active": res.AdvancedActive,
|
||||
"auto_local_bypass": res.AutoLocalBypass,
|
||||
"auto_local_active": res.AutoLocalActive,
|
||||
"ingress_reply": res.IngressReplyBypass,
|
||||
"ingress_active": res.IngressReplyActive,
|
||||
"overrides_applied": res.OverridesApplied,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, res)
|
||||
|
||||
Reference in New Issue
Block a user