From 9fca2dbbbc439e0ae090e7912cd24fbc71dc31bb Mon Sep 17 00:00:00 2001 From: Robert Markovski <5818108+Roshan931@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:58:58 +0000 Subject: [PATCH] fix: Health check config collides between two targets sharing the... Signed-off-by: Robert Markovski <5818108+Roshan931@users.noreply.github.com> --- newt/connect.go | 11 +++++++- newt/newt_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/newt/connect.go b/newt/connect.go index 1413abd..b431021 100644 --- a/newt/connect.go +++ b/newt/connect.go @@ -23,6 +23,15 @@ import ( "golang.zx2c4.com/wireguard/tun/netstack" ) +func (n *Newt) applyWgData(jsonData []byte) error { + var wgData WgData + if err := json.Unmarshal(jsonData, &wgData); err != nil { + return err + } + n.wgData = wgData + return nil +} + func (n *Newt) handleConnect(ctx context.Context, msg websocket.WSMessage) { logger.Debug("Received registration message") regResult := "success" @@ -63,7 +72,7 @@ func (n *Newt) handleConnect(ctx context.Context, msg websocket.WSMessage) { return } - if err := json.Unmarshal(jsonData, &n.wgData); err != nil { + if err := n.applyWgData(jsonData); err != nil { logger.Info("Error unmarshaling target data: %v", err) regResult = "failure" return diff --git a/newt/newt_test.go b/newt/newt_test.go index 16c25d2..3db0c20 100644 --- a/newt/newt_test.go +++ b/newt/newt_test.go @@ -8,6 +8,8 @@ import ( "sync/atomic" "testing" "time" + + "github.com/fosrl/newt/healthcheck" ) func TestWatchBlueprintFile_WriteTriggersSend(t *testing.T) { @@ -353,6 +355,71 @@ func TestPingNilNetstackReturnsError(t *testing.T) { } } +func TestApplyWgDataReplacesPreviousPayload(t *testing.T) { + n := &Newt{wgData: WgData{ + HealthCheckTargets: []healthcheck.Config{ + {ID: 2, Status: 401, Path: "/api/mcp"}, + {ID: 1, Status: 200, Path: "/manifest.json"}, + }, + }} + + payload := []byte(`{ + "healthCheckTargets": [ + {"id": 1, "hcPath": "/manifest.json", "hcStatus": null}, + {"id": 2, "hcPath": "/api/mcp", "hcStatus": 401} + ] + }`) + + if err := n.applyWgData(payload); err != nil { + t.Fatalf("applyWgData returned error: %v", err) + } + + got := n.wgData.HealthCheckTargets + if len(got) != 2 { + t.Fatalf("expected 2 health check targets, got %d", len(got)) + } + if got[0].ID != 1 || got[0].Status != 0 { + t.Errorf("target 1 = {ID: %d, Status: %d}, want {ID: 1, Status: 0}", got[0].ID, got[0].Status) + } + if got[1].ID != 2 || got[1].Status != 401 { + t.Errorf("target 2 = {ID: %d, Status: %d}, want {ID: 2, Status: 401}", got[1].ID, got[1].Status) + } +} + +func TestApplyWgDataClearsStaleFields(t *testing.T) { + n := &Newt{wgData: WgData{ + TunnelIP: "10.0.0.2", + RemoteExitNodeSubnets: []string{"10.1.0.0/24"}, + HealthCheckTargets: []healthcheck.Config{{ID: 1, Status: 200}}, + }} + + if err := n.applyWgData([]byte(`{"tunnelIP": "10.0.0.3"}`)); err != nil { + t.Fatalf("applyWgData returned error: %v", err) + } + + if n.wgData.TunnelIP != "10.0.0.3" { + t.Errorf("TunnelIP = %q, want %q", n.wgData.TunnelIP, "10.0.0.3") + } + if len(n.wgData.RemoteExitNodeSubnets) != 0 { + t.Errorf("RemoteExitNodeSubnets = %v, want empty", n.wgData.RemoteExitNodeSubnets) + } + if len(n.wgData.HealthCheckTargets) != 0 { + t.Errorf("HealthCheckTargets = %v, want empty", n.wgData.HealthCheckTargets) + } +} + +func TestApplyWgDataKeepsExistingDataOnDecodeError(t *testing.T) { + n := &Newt{wgData: WgData{TunnelIP: "10.0.0.2"}} + + if err := n.applyWgData([]byte(`{"tunnelIP":`)); err == nil { + t.Fatal("expected error for malformed payload, got nil") + } + + if n.wgData.TunnelIP != "10.0.0.2" { + t.Errorf("TunnelIP = %q, want %q", n.wgData.TunnelIP, "10.0.0.2") + } +} + // TestShouldFireRecovery is the regression guard for the broken trigger gate // that prevented data-plane recovery from ever firing under default settings // (fosrl/newt#284, #310, pangolin#1004).