platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
226
selective-vpn-api/app/transport_handlers_netns.go
Normal file
226
selective-vpn-api/app/transport_handlers_netns.go
Normal file
@@ -0,0 +1,226 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user