package app import ( "encoding/json" "io" "net/http" "time" ) func handleTransportNetnsToggle(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var body TransportNetnsToggleRequest if r.Body != nil { defer r.Body.Close() if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil && err != io.EOF { http.Error(w, "bad json", http.StatusBadRequest) return } } lockIDs := resolveTransportNetnsToggleLockIDs(body) resp := TransportNetnsToggleResponse{} withTransportIfaceLocks(lockIDs, func() { resp = executeTransportNetnsToggleLocked(body, time.Now().UTC()) }) if resp.SuccessCount > 0 { publishTransportRuntimeObservabilitySnapshotChanged("transport_netns_toggled", nil, lockIDs) } writeJSON(w, http.StatusOK, resp) } func resolveTransportNetnsToggleLockIDs(req TransportNetnsToggleRequest) []string { now := time.Now().UTC() transportMu.Lock() clientsState := loadTransportClientsState() ifacesState := loadTransportInterfacesState() ifacesSnapshot := captureTransportInterfacesStateSnapshot(clientsState, ifacesState) transportMu.Unlock() normIfaces, changed := normalizeTransportInterfacesState(ifacesState, clientsState.Items) if changed { _ = saveTransportInterfacesIfSnapshotCurrent(ifacesSnapshot, normIfaces) } indexes, _ := resolveTransportNetnsToggleTargets(clientsState.Items, req.ClientIDs) lockIDs := make([]string, 0, len(indexes)) targetEnabled := transportNetnsToggleTarget(req.Enabled, clientsState.Items, indexes) provisionEnabled := true if req.Provision != nil { provisionEnabled = *req.Provision } restartRunning := true if req.RestartRunning != nil { restartRunning = *req.RestartRunning } if !provisionEnabled { restartRunning = false } plannedItems := make([]TransportClient, len(clientsState.Items)) copy(plannedItems, clientsState.Items) for _, idx := range indexes { if idx < 0 || idx >= len(clientsState.Items) { continue } lockIDs = append(lockIDs, clientsState.Items[idx].IfaceID) _, updated := prepareTransportNetnsToggleClientLocked(plannedItems[idx], normIfaces, targetEnabled, now) plannedItems[idx] = updated lockIDs = append(lockIDs, updated.IfaceID) } if restartRunning { for _, idx := range indexes { if idx < 0 || idx >= len(plannedItems) { continue } if normalizeTransportStatus(clientsState.Items[idx].Status) != TransportClientUp { continue } _, peers := matchTransportSingBoxPeersInSameNetnsForLock(plannedItems, idx, normIfaces) for _, peer := range peers { lockIDs = append(lockIDs, peer.Binding.IfaceID) } } } return lockIDs } func executeTransportNetnsToggleLocked(req TransportNetnsToggleRequest, now time.Time) TransportNetnsToggleResponse { transportMu.Lock() st := loadTransportClientsState() rawIfaces := loadTransportInterfacesState() ifacesSnapshot := captureTransportInterfacesOnlySnapshot(rawIfaces) ifaces, ifacesChanged := normalizeTransportInterfacesState(rawIfaces, st.Items) resp := TransportNetnsToggleResponse{} var plans []transportNetnsToggleClientPlan var missing []string indexes, missing := resolveTransportNetnsToggleTargets(st.Items, req.ClientIDs) targetEnabled := transportNetnsToggleTarget(req.Enabled, st.Items, indexes) provisionEnabled := true if req.Provision != nil { provisionEnabled = *req.Provision } restartRunning := true if req.RestartRunning != nil { restartRunning = *req.RestartRunning } if !provisionEnabled { restartRunning = false } resp = TransportNetnsToggleResponse{ OK: true, Enabled: targetEnabled, Items: make([]TransportNetnsToggleItem, 0, len(indexes)+len(missing)), } plans = make([]transportNetnsToggleClientPlan, 0, len(indexes)) for _, idx := range indexes { if idx < 0 || idx >= len(st.Items) { continue } item, updated := prepareTransportNetnsToggleClientLocked(st.Items[idx], ifaces, targetEnabled, now) st.Items[idx] = updated plans = append(plans, transportNetnsToggleClientPlan{ Item: item, NeedsProvision: item.OK && provisionEnabled, NeedsRestart: item.OK && restartRunning && item.StatusBefore == TransportClientUp, }) } if len(indexes) > 0 { if err := saveTransportClientsState(st); err != nil { transportMu.Unlock() resp.Items = append(resp.Items, markTransportNetnsTogglePlansSaveFailed(plans, "save failed: "+err.Error())...) for _, missingID := range missing { resp.Items = append(resp.Items, TransportNetnsToggleItem{ OK: false, ClientID: missingID, Code: "TRANSPORT_CLIENT_NOT_FOUND", Message: "not found", NetnsEnabled: targetEnabled, }) } return finalizeTransportNetnsToggleResponse(resp) } } transportMu.Unlock() if ifacesChanged { _ = saveTransportInterfacesIfUnchanged(ifacesSnapshot, ifaces) } for _, plan := range plans { resp.Items = append(resp.Items, applyTransportNetnsToggleClientPlanLocked(plan)) } for _, missingID := range missing { resp.Items = append(resp.Items, TransportNetnsToggleItem{ OK: false, ClientID: missingID, Code: "TRANSPORT_CLIENT_NOT_FOUND", Message: "not found", NetnsEnabled: targetEnabled, }) } refreshTransportNetnsToggleFinalStatuses(&resp) return finalizeTransportNetnsToggleResponse(resp) } func resolveTransportNetnsToggleTargets(items []TransportClient, clientIDs []string) ([]int, []string) { if len(clientIDs) == 0 { out := make([]int, 0, len(items)) for i := range items { if items[i].Kind == TransportClientSingBox { out = append(out, i) } } return out, nil } out := make([]int, 0, len(clientIDs)) missing := make([]string, 0, 2) seen := make(map[int]struct{}, len(clientIDs)) for _, raw := range clientIDs { id := sanitizeID(raw) if id == "" { continue } idx := findTransportClientIndex(items, id) if idx < 0 { missing = append(missing, id) continue } if _, ok := seen[idx]; ok { continue } seen[idx] = struct{}{} out = append(out, idx) } return out, missing } func transportNetnsToggleTarget(enabled *bool, items []TransportClient, indexes []int) bool { if enabled != nil { return *enabled } any := false allEnabled := true for _, idx := range indexes { if idx < 0 || idx >= len(items) { continue } it := items[idx] if it.Kind != TransportClientSingBox { continue } any = true if !transportNetnsEnabled(it) { allEnabled = false } } if !any { return false } return !allEnabled }