396 lines
11 KiB
Go
396 lines
11 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestBuildTransportRuntimeObservabilityItemsAggregatesInterfaces(t *testing.T) {
|
|
now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
|
|
ifaces := []TransportInterface{
|
|
{
|
|
ID: transportDefaultIfaceID,
|
|
Name: "Shared interface",
|
|
Mode: TransportInterfaceModeShared,
|
|
RuntimeIface: "tun-shared",
|
|
},
|
|
{
|
|
ID: "edge-a",
|
|
Name: "Edge A",
|
|
Mode: TransportInterfaceModeDedicated,
|
|
RuntimeIface: "tun-edge",
|
|
NetnsName: "svpn-edge-a",
|
|
RoutingTable: "agvpn_if_edge_a",
|
|
},
|
|
}
|
|
clients := []TransportClient{
|
|
{
|
|
ID: "sb-up",
|
|
Kind: TransportClientSingBox,
|
|
Enabled: true,
|
|
IfaceID: "edge-a",
|
|
Iface: "tun-edge0",
|
|
Status: TransportClientUp,
|
|
Health: TransportClientHealth{
|
|
LastCheck: "2026-03-16T11:59:00Z",
|
|
LatencyMS: 81,
|
|
},
|
|
},
|
|
{
|
|
ID: "dn-down",
|
|
Kind: TransportClientDNSTT,
|
|
Enabled: false,
|
|
IfaceID: "edge-a",
|
|
Status: TransportClientDown,
|
|
},
|
|
{
|
|
ID: "ph-shared",
|
|
Kind: TransportClientPhoenix,
|
|
Enabled: true,
|
|
IfaceID: transportDefaultIfaceID,
|
|
Iface: "tun-shared0",
|
|
Status: TransportClientDegraded,
|
|
Health: TransportClientHealth{
|
|
LastCheck: "2026-03-16T11:58:00Z",
|
|
LatencyMS: 320,
|
|
LastError: "upstream timeout",
|
|
},
|
|
},
|
|
}
|
|
plan := TransportPolicyCompilePlan{
|
|
Interfaces: []TransportPolicyCompileInterface{
|
|
{IfaceID: "edge-a", RuleCount: 3},
|
|
{IfaceID: transportDefaultIfaceID, RuleCount: 1},
|
|
},
|
|
}
|
|
egressByClient := map[string]EgressIdentity{
|
|
"sb-up": {
|
|
Scope: "transport:sb-up",
|
|
Source: "transport",
|
|
SourceID: "sb-up",
|
|
IP: "203.0.113.10",
|
|
CountryCode: "SG",
|
|
},
|
|
"ph-shared": {
|
|
Scope: "transport:ph-shared",
|
|
Source: "transport",
|
|
SourceID: "ph-shared",
|
|
IP: "198.51.100.44",
|
|
CountryCode: "NL",
|
|
},
|
|
}
|
|
|
|
items := buildTransportRuntimeObservabilityItems(
|
|
ifaces,
|
|
clients,
|
|
plan,
|
|
func(clientID string) EgressIdentity { return egressByClient[clientID] },
|
|
now,
|
|
)
|
|
if len(items) != 2 {
|
|
t.Fatalf("expected 2 items, got %d", len(items))
|
|
}
|
|
|
|
edge := items[0]
|
|
if edge.IfaceID != "edge-a" {
|
|
t.Fatalf("expected edge-a item first, got %q", edge.IfaceID)
|
|
}
|
|
if edge.ClientID != "sb-up" {
|
|
t.Fatalf("unexpected edge-a primary client: %#v", edge)
|
|
}
|
|
if edge.ActiveIface != "tun-edge0" || edge.RuntimeIface != "tun-edge" {
|
|
t.Fatalf("unexpected edge-a iface binding: %#v", edge)
|
|
}
|
|
if edge.Status != string(TransportClientUp) {
|
|
t.Fatalf("unexpected edge-a status: %#v", edge)
|
|
}
|
|
if edge.Counters.ClientCount != 2 || edge.Counters.UpCount != 1 || edge.Counters.DownCount != 1 || edge.Counters.RuleCount != 3 {
|
|
t.Fatalf("unexpected edge-a counters: %#v", edge.Counters)
|
|
}
|
|
if edge.Egress.IP != "203.0.113.10" || edge.Egress.CountryCode != "SG" {
|
|
t.Fatalf("unexpected edge-a egress: %#v", edge.Egress)
|
|
}
|
|
if len(edge.EngineCounts) != 2 || edge.EngineCounts[0].Kind != "dnstt" || edge.EngineCounts[1].Kind != "singbox" {
|
|
t.Fatalf("unexpected edge-a engine counts: %#v", edge.EngineCounts)
|
|
}
|
|
|
|
shared := items[1]
|
|
if shared.IfaceID != transportDefaultIfaceID {
|
|
t.Fatalf("expected shared item second, got %q", shared.IfaceID)
|
|
}
|
|
if shared.ClientID != "ph-shared" || shared.Status != string(TransportClientDegraded) {
|
|
t.Fatalf("unexpected shared snapshot: %#v", shared)
|
|
}
|
|
if shared.LatencyMS != 320 || shared.LastError != "upstream timeout" || shared.LastCheck != "2026-03-16T11:58:00Z" {
|
|
t.Fatalf("unexpected shared health snapshot: %#v", shared)
|
|
}
|
|
if shared.Counters.ClientCount != 1 || shared.Counters.DegradedCount != 1 || shared.Counters.RuleCount != 1 {
|
|
t.Fatalf("unexpected shared counters: %#v", shared.Counters)
|
|
}
|
|
}
|
|
|
|
func TestBuildTransportRuntimeObservabilityItemsKeepsActiveClientAndAggregateError(t *testing.T) {
|
|
now := time.Date(2026, time.March, 16, 12, 5, 0, 0, time.UTC)
|
|
ifaces := []TransportInterface{
|
|
{
|
|
ID: "edge-a",
|
|
Name: "Edge A",
|
|
Mode: TransportInterfaceModeDedicated,
|
|
RuntimeIface: "tun-edge",
|
|
},
|
|
}
|
|
clients := []TransportClient{
|
|
{
|
|
ID: "sb-up",
|
|
Kind: TransportClientSingBox,
|
|
Enabled: true,
|
|
IfaceID: "edge-a",
|
|
Iface: "tun-edge0",
|
|
Status: TransportClientUp,
|
|
Health: TransportClientHealth{
|
|
LastCheck: "2026-03-16T12:04:00Z",
|
|
LatencyMS: 55,
|
|
},
|
|
},
|
|
{
|
|
ID: "ph-bad",
|
|
Kind: TransportClientPhoenix,
|
|
Enabled: true,
|
|
IfaceID: "edge-a",
|
|
Status: TransportClientDegraded,
|
|
Health: TransportClientHealth{
|
|
LastCheck: "2026-03-16T12:04:30Z",
|
|
LastError: "probe failed",
|
|
},
|
|
},
|
|
}
|
|
|
|
items := buildTransportRuntimeObservabilityItems(
|
|
ifaces,
|
|
clients,
|
|
TransportPolicyCompilePlan{},
|
|
func(clientID string) EgressIdentity {
|
|
if clientID == "sb-up" {
|
|
return EgressIdentity{IP: "203.0.113.55", CountryCode: "DE"}
|
|
}
|
|
return EgressIdentity{}
|
|
},
|
|
now,
|
|
)
|
|
if len(items) != 1 {
|
|
t.Fatalf("expected 1 item, got %d", len(items))
|
|
}
|
|
item := items[0]
|
|
if item.ClientID != "sb-up" {
|
|
t.Fatalf("expected active client to stay sb-up, got %#v", item)
|
|
}
|
|
if item.Status != string(TransportClientDegraded) {
|
|
t.Fatalf("expected aggregate degraded status, got %#v", item)
|
|
}
|
|
if item.LatencyMS != 55 {
|
|
t.Fatalf("expected latency from active client, got %#v", item)
|
|
}
|
|
if item.LastError != "probe failed" {
|
|
t.Fatalf("expected aggregate last_error from degraded peer, got %#v", item)
|
|
}
|
|
if item.LastCheck != "2026-03-16T12:04:00Z" {
|
|
t.Fatalf("expected active client last_check to stay stable, got %#v", item)
|
|
}
|
|
}
|
|
|
|
func TestHandleTransportRuntimeObservability(t *testing.T) {
|
|
withTransportPolicyMutationTestPaths(t)
|
|
|
|
prevEgress := egressIdentitySWR
|
|
egressIdentitySWR = newEgressIdentityService(1)
|
|
t.Cleanup(func() {
|
|
egressIdentitySWR = prevEgress
|
|
})
|
|
|
|
if err := saveTransportClientsState(transportClientsState{
|
|
Version: transportStateVersion,
|
|
Items: []TransportClient{
|
|
{
|
|
ID: "sb-one",
|
|
Kind: TransportClientSingBox,
|
|
Enabled: true,
|
|
IfaceID: "edge-a",
|
|
Iface: "tun-edge0",
|
|
Status: TransportClientUp,
|
|
Health: TransportClientHealth{
|
|
LastCheck: "2026-03-16T12:09:00Z",
|
|
LatencyMS: 64,
|
|
},
|
|
},
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("save clients state: %v", err)
|
|
}
|
|
if err := saveTransportInterfacesState(transportInterfacesState{
|
|
Version: transportStateVersion,
|
|
Items: []TransportInterface{
|
|
{
|
|
ID: "edge-a",
|
|
Name: "Edge A",
|
|
Mode: TransportInterfaceModeDedicated,
|
|
RuntimeIface: "tun-edge",
|
|
RoutingTable: "agvpn_if_edge_a",
|
|
},
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("save interfaces state: %v", err)
|
|
}
|
|
if err := saveTransportPolicyState(TransportPolicyState{
|
|
Version: transportStateVersion,
|
|
Revision: 5,
|
|
UpdatedAt: "2026-03-16T12:00:00Z",
|
|
Intents: []TransportPolicyIntent{
|
|
{SelectorType: "domain", SelectorValue: "example.org", ClientID: "sb-one"},
|
|
{SelectorType: "cidr", SelectorValue: "10.0.0.0/24", ClientID: "sb-one"},
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("save policy state: %v", err)
|
|
}
|
|
if err := saveTransportPolicyCompilePlan(TransportPolicyCompilePlan{
|
|
GeneratedAt: "2026-03-16T12:00:00Z",
|
|
PolicyRevision: 5,
|
|
InterfaceCount: 1,
|
|
RuleCount: 2,
|
|
Interfaces: []TransportPolicyCompileInterface{
|
|
{IfaceID: "edge-a", RuleCount: 2, ClientIDs: []string{"sb-one"}},
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("save policy plan: %v", err)
|
|
}
|
|
|
|
egressIdentitySWR.mu.Lock()
|
|
egressIdentitySWR.entries["transport:sb-one"] = &egressIdentityEntry{
|
|
item: EgressIdentity{
|
|
Scope: "transport:sb-one",
|
|
Source: "transport",
|
|
SourceID: "sb-one",
|
|
IP: "198.51.100.8",
|
|
CountryCode: "NL",
|
|
},
|
|
}
|
|
egressIdentitySWR.mu.Unlock()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/transport/runtime/observability", nil)
|
|
rec := httptest.NewRecorder()
|
|
handleTransportRuntimeObservability(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp TransportRuntimeObservabilityResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if !resp.OK || resp.Count < 2 || resp.Count != len(resp.Items) || resp.GeneratedAt == "" {
|
|
t.Fatalf("unexpected response envelope: %#v", resp)
|
|
}
|
|
var item TransportRuntimeObservabilityItem
|
|
found := false
|
|
for _, current := range resp.Items {
|
|
if current.IfaceID != "edge-a" {
|
|
continue
|
|
}
|
|
item = current
|
|
found = true
|
|
break
|
|
}
|
|
if !found {
|
|
t.Fatalf("edge-a snapshot not found: %#v", resp.Items)
|
|
}
|
|
if item.IfaceID != "edge-a" || item.ClientID != "sb-one" {
|
|
t.Fatalf("unexpected runtime item identity: %#v", item)
|
|
}
|
|
if item.ActiveIface != "tun-edge0" || item.Counters.RuleCount != 2 {
|
|
t.Fatalf("unexpected runtime item details: %#v", item)
|
|
}
|
|
if item.Egress.IP != "198.51.100.8" || item.Egress.CountryCode != "NL" {
|
|
t.Fatalf("unexpected runtime item egress: %#v", item.Egress)
|
|
}
|
|
}
|
|
|
|
func TestBuildTransportRuntimeObservabilityItemsAddsVirtualAdGuardRow(t *testing.T) {
|
|
now := time.Date(2026, time.March, 23, 19, 10, 0, 0, time.UTC)
|
|
ifaces := []TransportInterface{
|
|
{
|
|
ID: transportDefaultIfaceID,
|
|
Name: "Shared interface",
|
|
Mode: TransportInterfaceModeShared,
|
|
},
|
|
}
|
|
virtual := buildTransportPolicyAdGuardTargetFromObservation(
|
|
"active",
|
|
"CONNECTED",
|
|
"after connect: CONNECTED; raw: Connected to HELSINKI in TUN mode, running on tun0",
|
|
now,
|
|
)
|
|
clients := []TransportClient{
|
|
{
|
|
ID: "sb-one",
|
|
Kind: TransportClientSingBox,
|
|
Enabled: true,
|
|
IfaceID: transportDefaultIfaceID,
|
|
Status: TransportClientDown,
|
|
},
|
|
virtual,
|
|
}
|
|
plan := TransportPolicyCompilePlan{
|
|
Interfaces: []TransportPolicyCompileInterface{
|
|
{
|
|
IfaceID: transportDefaultIfaceID,
|
|
RuleCount: 2,
|
|
Rules: []TransportPolicyCompileRule{
|
|
{ClientID: "sb-one"},
|
|
{ClientID: "adguardvpn"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
items := buildTransportRuntimeObservabilityItems(
|
|
ifaces,
|
|
clients,
|
|
plan,
|
|
func(clientID string) EgressIdentity {
|
|
if clientID == "adguardvpn" {
|
|
return EgressIdentity{Scope: "adguardvpn", IP: "185.77.216.28", CountryCode: "EE"}
|
|
}
|
|
return EgressIdentity{}
|
|
},
|
|
now,
|
|
)
|
|
if len(items) != 2 {
|
|
t.Fatalf("expected 2 rows, got %d", len(items))
|
|
}
|
|
|
|
var found bool
|
|
for _, item := range items {
|
|
if item.IfaceID != "adguardvpn" {
|
|
continue
|
|
}
|
|
found = true
|
|
if item.ClientID != "adguardvpn" || item.Status != string(TransportClientUp) {
|
|
t.Fatalf("unexpected virtual row identity: %#v", item)
|
|
}
|
|
if item.RuntimeIface != "tun0" || item.ActiveIface != "tun0" {
|
|
t.Fatalf("unexpected virtual iface binding: %#v", item)
|
|
}
|
|
if item.Counters.ClientCount != 1 || item.Counters.RuleCount != 1 {
|
|
t.Fatalf("unexpected virtual counters: %#v", item.Counters)
|
|
}
|
|
if item.Egress.IP != "185.77.216.28" || item.Egress.CountryCode != "EE" {
|
|
t.Fatalf("unexpected virtual egress: %#v", item.Egress)
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("virtual adguard row not found: %#v", items)
|
|
}
|
|
}
|