Files
elmprodvpn/selective-vpn-api/app/transport_client_runtime_netns.go

289 lines
7.4 KiB
Go

package app
import (
"strings"
"time"
)
type transportNetnsPeerMatch struct {
Index int
Binding transportIfaceBinding
}
type transportLifecyclePeerStopPlan struct {
ClientID string
Client TransportClient
}
type transportLifecyclePeerStopExecution struct {
ClientID string
BackendID string
Result transportBackendActionResult
}
type transportLifecycleStateChange struct {
ClientID string
From TransportClientStatus
To TransportClientStatus
}
func matchTransportSingBoxPeersInSameNetns(
items []TransportClient,
targetIdx int,
ifaces transportInterfacesState,
) (transportIfaceBinding, []transportNetnsPeerMatch) {
if targetIdx < 0 || targetIdx >= len(items) {
return transportIfaceBinding{}, nil
}
target := items[targetIdx]
targetBinding := resolveTransportIfaceBinding(target, ifaces)
if target.Kind != TransportClientSingBox || !transportNetnsEnabled(target) {
return targetBinding, nil
}
targetNS := strings.TrimSpace(targetBinding.NetnsName)
if targetNS == "" {
return targetBinding, nil
}
peers := make([]transportNetnsPeerMatch, 0, 2)
for i := range items {
if i == targetIdx {
continue
}
peer := items[i]
if peer.Kind != TransportClientSingBox {
continue
}
if !transportNetnsEnabled(peer) {
continue
}
if normalizeTransportStatus(peer.Status) == TransportClientDown {
continue
}
peerBinding := resolveTransportIfaceBinding(peer, ifaces)
if strings.TrimSpace(peerBinding.NetnsName) != targetNS {
continue
}
peers = append(peers, transportNetnsPeerMatch{
Index: i,
Binding: peerBinding,
})
}
return targetBinding, peers
}
func matchTransportSingBoxPeersInSameNetnsForLock(
items []TransportClient,
targetIdx int,
ifaces transportInterfacesState,
) (transportIfaceBinding, []transportNetnsPeerMatch) {
if targetIdx < 0 || targetIdx >= len(items) {
return transportIfaceBinding{}, nil
}
target := items[targetIdx]
targetBinding := resolveTransportIfaceBinding(target, ifaces)
if target.Kind != TransportClientSingBox || !transportNetnsEnabled(target) {
return targetBinding, nil
}
targetNS := strings.TrimSpace(targetBinding.NetnsName)
if targetNS == "" {
return targetBinding, nil
}
peers := make([]transportNetnsPeerMatch, 0, 2)
for i := range items {
if i == targetIdx {
continue
}
peer := items[i]
if peer.Kind != TransportClientSingBox {
continue
}
if !transportNetnsEnabled(peer) {
continue
}
peerBinding := resolveTransportIfaceBinding(peer, ifaces)
if strings.TrimSpace(peerBinding.NetnsName) != targetNS {
continue
}
peers = append(peers, transportNetnsPeerMatch{
Index: i,
Binding: peerBinding,
})
}
return targetBinding, peers
}
func planTransportSingBoxPeerStops(
items []TransportClient,
targetIdx int,
ifaces transportInterfacesState,
now time.Time,
) []transportLifecyclePeerStopPlan {
_, peers := matchTransportSingBoxPeersInSameNetns(items, targetIdx, ifaces)
if len(peers) == 0 {
return nil
}
plans := make([]transportLifecyclePeerStopPlan, 0, len(peers))
for _, peerMatch := range peers {
if peerMatch.Index < 0 || peerMatch.Index >= len(items) {
continue
}
peer := items[peerMatch.Index]
if bound, changed := applyTransportIfaceBinding(peer, ifaces, now); changed {
peer = bound
}
plans = append(plans, transportLifecyclePeerStopPlan{
ClientID: peer.ID,
Client: peer,
})
}
return plans
}
func executeTransportLifecyclePeerStops(
plans []transportLifecyclePeerStopPlan,
) []transportLifecyclePeerStopExecution {
if len(plans) == 0 {
return nil
}
results := make([]transportLifecyclePeerStopExecution, 0, len(plans))
for _, plan := range plans {
backend := selectTransportBackend(plan.Client)
stopRes := backend.Action(plan.Client, "stop")
results = append(results, transportLifecyclePeerStopExecution{
ClientID: plan.ClientID,
BackendID: backend.ID(),
Result: stopRes,
})
if !stopRes.OK {
break
}
}
return results
}
func summarizeTransportLifecyclePeerStops(
results []transportLifecyclePeerStopExecution,
) transportBackendActionResult {
if len(results) == 0 {
return transportBackendActionResult{OK: true, ExitCode: 0}
}
stopped := make([]string, 0, len(results))
aggOut := make([]string, 0, len(results))
aggErr := make([]string, 0, len(results))
for _, res := range results {
if s := strings.TrimSpace(res.Result.Stdout); s != "" {
aggOut = append(aggOut, res.ClientID+": "+s)
}
if s := strings.TrimSpace(res.Result.Stderr); s != "" {
aggErr = append(aggErr, res.ClientID+": "+s)
}
if !res.Result.OK {
msg := strings.TrimSpace(res.Result.Message)
if msg == "" {
msg = "stop conflicting peer failed: " + res.ClientID
}
return transportBackendActionResult{
OK: false,
Code: res.Result.Code,
Message: msg,
ExitCode: res.Result.ExitCode,
Stdout: strings.Join(aggOut, "\n"),
Stderr: strings.Join(aggErr, "\n"),
}
}
stopped = append(stopped, res.ClientID)
}
if len(stopped) == 0 {
return transportBackendActionResult{OK: true, ExitCode: 0}
}
return transportBackendActionResult{
OK: true,
ExitCode: 0,
Message: "stopped conflicting peers: " + strings.Join(stopped, ", "),
Stdout: strings.Join(aggOut, "\n"),
Stderr: strings.Join(aggErr, "\n"),
}
}
func applyTransportLifecyclePeerStopExecutionsLocked(
st *transportClientsState,
ifaces transportInterfacesState,
now time.Time,
results []transportLifecyclePeerStopExecution,
) []transportLifecycleStateChange {
if st == nil || len(results) == 0 {
return nil
}
changes := make([]transportLifecycleStateChange, 0, len(results))
for _, res := range results {
idx := findTransportClientIndex(st.Items, res.ClientID)
if idx < 0 {
continue
}
peer := st.Items[idx]
if bound, changed := applyTransportIfaceBinding(peer, ifaces, now); changed {
peer = bound
}
prev := peer.Status
if res.Result.OK {
applyTransportLifecycleAction(&peer, "stop", now)
peer.Runtime.Backend = res.BackendID
} else {
applyTransportLifecycleFailure(&peer, "stop", now, res.BackendID, res.Result)
}
st.Items[idx] = peer
if prev != peer.Status {
changes = append(changes, transportLifecycleStateChange{
ClientID: peer.ID,
From: prev,
To: peer.Status,
})
}
}
return changes
}
func joinNonEmptyLines(parts ...string) string {
lines := make([]string, 0, len(parts))
for _, part := range parts {
v := strings.TrimSpace(part)
if v == "" {
continue
}
lines = append(lines, v)
}
return strings.Join(lines, "\n")
}
func stopTransportSingBoxPeersInSameNetnsLocked(
st *transportClientsState,
targetIdx int,
now time.Time,
) transportBackendActionResult {
if st == nil || targetIdx < 0 || targetIdx >= len(st.Items) {
return transportBackendActionResult{
OK: false,
Code: "TRANSPORT_CLIENT_NOT_FOUND",
Message: "target client not found",
ExitCode: -1,
}
}
ifaces, err := syncTransportInterfacesWithClientsLocked(st.Items)
if err != nil {
return transportBackendActionResult{
OK: false,
Code: "TRANSPORT_INTERFACES_SAVE_FAILED",
Message: "interfaces sync failed: " + err.Error(),
ExitCode: -1,
}
}
plans := planTransportSingBoxPeerStops(st.Items, targetIdx, ifaces, now)
results := executeTransportLifecyclePeerStops(plans)
summary := summarizeTransportLifecyclePeerStops(results)
applyTransportLifecyclePeerStopExecutionsLocked(st, ifaces, now, results)
return summary
}