227 lines
6.5 KiB
Go
227 lines
6.5 KiB
Go
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
|
|
}
|