package app import ( "fmt" "net/http" "strings" "time" ) func handleTransportVirtualClientAction(w http.ResponseWriter, r *http.Request, id, action string) bool { cid := sanitizeID(id) if !isTransportPolicyVirtualClientID(cid) { return false } switch action { case "health": handleTransportVirtualClientHealthAction(w, r, cid) case "metrics": handleTransportVirtualClientMetricsAction(w, r, cid) case "provision": handleTransportVirtualClientProvisionAction(w, r, cid) case "start", "stop", "restart": handleTransportVirtualClientLifecycleAction(w, r, cid, action) default: http.NotFound(w, r) } return true } func handleTransportVirtualClientCardGet(w http.ResponseWriter, r *http.Request, id string) bool { cid := sanitizeID(id) if !isTransportPolicyVirtualClientID(cid) { return false } if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return true } item := transportVirtualAdGuardSnapshot() writeJSON(w, http.StatusOK, TransportClientsResponse{OK: true, Message: "ok", Item: &item}) return true } func transportVirtualClientReadOnlyResponse() (int, TransportClientsResponse) { return http.StatusOK, TransportClientsResponse{ OK: false, Message: "virtual control-plane client is read-only", } } func transportVirtualAdGuardSnapshot() TransportClient { if item, ok := resolveTransportPolicyVirtualClient(transportPolicyTargetAdGuardID); ok { return item } now := time.Now().UTC() item := buildTransportPolicyAdGuardTargetFromObservation("inactive", "DISCONNECTED", "", now) item.Health.LastError = "adguard autoloop state unavailable" item.Runtime.LastError = TransportClientError{ Code: "BACKEND_RUNTIME_ERROR", Message: item.Health.LastError, Retryable: true, At: item.Health.LastCheck, } item.Runtime.LastAction = "observe" item.Runtime.LastActionAt = item.Health.LastCheck item.UpdatedAt = item.Health.LastCheck return item } func handleTransportVirtualClientHealthAction(w http.ResponseWriter, r *http.Request, id string) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } item := transportVirtualAdGuardSnapshot() resp := buildTransportHealthResponse(item, time.Now().UTC()) if normalizeTransportStatus(item.Status) == TransportClientDown { resp.Code = "TRANSPORT_CLIENT_DOWN" } writeJSON(w, http.StatusOK, resp) } func handleTransportVirtualClientMetricsAction(w http.ResponseWriter, r *http.Request, id string) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } item := transportVirtualAdGuardSnapshot() writeJSON(w, http.StatusOK, buildTransportMetricsResponse(item, time.Now().UTC())) } func handleTransportVirtualClientProvisionAction(w http.ResponseWriter, r *http.Request, id string) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } item := transportVirtualAdGuardSnapshot() now := time.Now().UTC() msg := "virtual adapter uses existing adguard autoloop runtime; explicit provision skipped" writeJSON(w, http.StatusOK, TransportClientLifecycleResponse{ OK: true, Message: msg, Code: "", ClientID: item.ID, Kind: item.Kind, Action: "provision", StatusBefore: item.Status, StatusAfter: item.Status, Health: item.Health, Runtime: transportRuntimeSnapshot(item, now), }) } func handleTransportVirtualClientLifecycleAction(w http.ResponseWriter, r *http.Request, id, action string) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } status, resp := runTransportVirtualClientLifecycleAction(id, action) writeJSON(w, status, resp) } func runTransportVirtualClientLifecycleAction(id, action string) (int, TransportClientLifecycleResponse) { status := http.StatusOK resp := TransportClientLifecycleResponse{} withTransportIfaceLock(transportPolicyTargetAdGuardIfaceID, func() { status, resp = executeTransportVirtualClientLifecycleAction(id, action) }) return status, resp } func executeTransportVirtualClientLifecycleAction(id, action string) (int, TransportClientLifecycleResponse) { before := transportVirtualAdGuardSnapshot() beforeStatus := normalizeTransportStatus(before.Status) now := time.Now().UTC() cmdAction, ok := transportVirtualAdGuardSystemdAction(action) if !ok { return http.StatusNotFound, TransportClientLifecycleResponse{ OK: false, Message: "unknown action", Code: "TRANSPORT_ACTION_UNKNOWN", } } stdout, stderr, exitCode, err := runCommand("systemctl", cmdAction, adgvpnUnit) actionOK := err == nil && exitCode == 0 after := transportVirtualAdGuardSnapshot() afterStatus := normalizeTransportStatus(after.Status) if !actionOK && afterStatus == beforeStatus { afterStatus = TransportClientDegraded after.Status = afterStatus } msg := strings.TrimSpace(stdout) if msg == "" { msg = strings.TrimSpace(stderr) } if msg == "" && err != nil { msg = strings.TrimSpace(err.Error()) } if msg == "" { msg = fmt.Sprintf("adguardvpn %s done", cmdAction) } code := "" if !actionOK { code = "TRANSPORT_ADGUARD_ACTION_FAILED" } events.push("transport_client_state_changed", map[string]any{ "id": id, "from": beforeStatus, "to": afterStatus, }) publishTransportRuntimeObservabilitySnapshotChanged( "transport_client_state_changed", []string{id}, []string{transportPolicyTargetAdGuardIfaceID}, ) _, _ = egressIdentitySWR.queueRefresh([]string{"adguardvpn", "system"}, true) return http.StatusOK, TransportClientLifecycleResponse{ OK: actionOK, Message: msg, Code: code, ExitCode: exitCode, Stdout: stdout, Stderr: stderr, ClientID: id, Kind: after.Kind, Action: action, StatusBefore: beforeStatus, StatusAfter: afterStatus, Health: after.Health, Runtime: transportRuntimeSnapshot(after, now), } } func transportVirtualAdGuardSystemdAction(action string) (string, bool) { switch strings.ToLower(strings.TrimSpace(action)) { case "start": return "start", true case "stop": return "stop", true case "restart": return "restart", true default: return "", false } }