Files
elmprodvpn/selective-vpn-api/app/transport_handlers_netns.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
}