diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index f5ea208..d73979c 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -9,7 +9,6 @@ on: jobs: test: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v3 @@ -24,9 +23,6 @@ jobs: - name: Build run: go build - - name: Run unit tests with coverage - run: bash unit-test-coverage.sh - - name: Run integration tests - run: go test -v -tags=integration ./... - - name: Run smoke tests - run: bash smoke/smoke-test.sh + - name: Run all tests + run: go test -v -tags=router_test,integration,e2e ./... + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1c9d86f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM golang:1.25.4 AS builder +WORKDIR /src + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -o /nylon . + +FROM scratch + +# Copy binary from builder +COPY --from=builder /nylon /usr/local/bin/nylon + +WORKDIR /app/config + +ENTRYPOINT ["/usr/local/bin/nylon", "run"] + +FROM ubuntu:latest AS debug + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + iputils-ping \ + iperf3 \ + curl \ + iproute2 \ + net-tools \ + tcpdump \ + dnsutils \ + netcat-openbsd && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /nylon /usr/local/bin/nylon + +WORKDIR /app/config + +ENTRYPOINT ["/usr/local/bin/nylon", "run", "-v", "-w", "--dbg-trace-tc"] diff --git a/cmd/run.go b/cmd/run.go index dae1dad..afbdd89 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -37,6 +37,7 @@ func init() { runCmd.Flags().BoolVarP(&state.DBG_log_repo_updates, "dbg-repo", "", false, "Outputs repo updates to the console") runCmd.Flags().BoolVarP(&state.DBG_debug, "dbg-perf", "", false, "Enables performance debugging server on port 6060") runCmd.Flags().BoolVarP(&state.DBG_trace, "dbg-trace", "", false, "Enables trace to trace.out") + runCmd.Flags().BoolVarP(&state.DBG_trace_tc, "dbg-trace-tc", "", false, "Enables logging of packet routing") runCmd.Flags().StringP("config", "c", DefaultConfigPath, "Path to the config file") runCmd.Flags().StringP("node", "n", DefaultNodeConfigPath, "Path to the node config file") runCmd.Flags().StringP("log", "l", "", "Path to the log file (overrides config)") diff --git a/core/ipc.go b/core/ipc.go index 2791cf7..092bda1 100644 --- a/core/ipc.go +++ b/core/ipc.go @@ -100,6 +100,25 @@ func HandleNylonIPCGet(s *state.State, rw *bufio.ReadWriter) error { } slices.Sort(rt) sb.WriteString(strings.Join(rt, "\n") + "\n") + + // print forward table + sb.WriteString("\n\nForward Table:\n") + rt = make([]string, 0) + for prefix, route := range Get[*NylonRouter](s).ForwardTable.All() { + rt = append(rt, fmt.Sprintf(" - %s via %s", prefix, route.Nh)) + } + slices.Sort(rt) + sb.WriteString(strings.Join(rt, "\n") + "\n") + + // print exit table + sb.WriteString("\n\nExit Table:\n") + rt = make([]string, 0) + for prefix, route := range Get[*NylonRouter](s).ExitTable.All() { + rt = append(rt, fmt.Sprintf(" - %s via %s", prefix, route.Nh)) + } + slices.Sort(rt) + sb.WriteString(strings.Join(rt, "\n") + "\n") + sb.WriteRune(0) _, err = rw.WriteString(sb.String()) if err != nil { diff --git a/core/nylon.go b/core/nylon.go index 11d218f..be4866f 100644 --- a/core/nylon.go +++ b/core/nylon.go @@ -56,7 +56,7 @@ func (n *Nylon) Init(s *state.State) error { } stNeigh := &state.Neighbour{ Id: peer, - Routes: make(map[state.Source]state.NeighRoute), + Routes: make(map[netip.Prefix]state.NeighRoute), Eps: make([]state.Endpoint, 0), } cfg := s.GetRouter(peer) diff --git a/core/nylon_tc.go b/core/nylon_tc.go index fe1c6ff..0f6c05f 100644 --- a/core/nylon_tc.go +++ b/core/nylon_tc.go @@ -1,6 +1,8 @@ package core import ( + "net/netip" + "github.com/encodeous/nylon/polyamide/conn" "github.com/encodeous/nylon/polyamide/device" "github.com/encodeous/nylon/protocol" @@ -17,6 +19,27 @@ const ( func (n *Nylon) InstallTC(s *state.State) { r := Get[*NylonRouter](s) + if state.DBG_trace_tc { + n.Device.InstallFilter(func(dev *device.Device, packet *device.TCElement) (device.TCAction, error) { + if packet.Validate() { // make sure it's an IP packet + peer := packet.FromPeer + if peer == nil { + peer = packet.ToPeer + } + src := packet.GetSrc() + dst := packet.GetDst() + if src.IsValid() && + dst.IsValid() && + peer != nil && + src != netip.IPv4Unspecified() && src != netip.IPv6Unspecified() && + dst != netip.IPv4Unspecified() && dst != netip.IPv6Unspecified() { + dev.Log.Verbosef("Unhandled TC packet: %v -> %v, peer %s", packet.GetSrc(), packet.GetDst(), peer) + } + } + return device.TcPass, nil + }) + } + // bounce back packets if using system routing if n.env.UseSystemRouting { n.Device.InstallFilter(func(dev *device.Device, packet *device.TCElement) (device.TCAction, error) { @@ -32,7 +55,9 @@ func (n *Nylon) InstallTC(s *state.State) { entry, ok := r.ForwardTable.Lookup(packet.GetDst()) if ok && !packet.Incoming() { packet.ToPeer = entry.Peer - //dev.Log.Verbosef("Fwd packet: %v -> %v, via %s", packet.GetSrc(), packet.GetDst(), entry.Nh) + if state.DBG_trace_tc { + dev.Log.Verbosef("Fwd packet: %v -> %v, via %s", packet.GetSrc(), packet.GetDst(), entry.Nh) + } return device.TcForward, nil } return device.TcPass, nil @@ -43,6 +68,9 @@ func (n *Nylon) InstallTC(s *state.State) { entry, ok := r.ForwardTable.Lookup(packet.GetDst()) if ok { packet.ToPeer = entry.Peer + if state.DBG_trace_tc { + dev.Log.Verbosef("Fwd packet: %v -> %v, via %s", packet.GetSrc(), packet.GetDst(), entry.Nh) + } return device.TcForward, nil } return device.TcPass, nil @@ -58,6 +86,9 @@ func (n *Nylon) InstallTC(s *state.State) { packet.DecrementTTL() } if ttl == 0 { + if state.DBG_trace_tc { + dev.Log.Verbosef("TTL Expired: %v -> %v, via %s", packet.GetSrc(), packet.GetDst()) + } return device.TcBounce, nil } } diff --git a/core/nylon_wireguard.go b/core/nylon_wireguard.go index b5c8183..676c372 100644 --- a/core/nylon_wireguard.go +++ b/core/nylon_wireguard.go @@ -127,7 +127,7 @@ func (n *Nylon) cleanupWireGuard(s *state.State) error { } } // run pre-down commands - for _, cmd := range s.PreUp { + for _, cmd := range s.PreDown { err := ExecSplit(s.Log, cmd) if err != nil { s.Log.Error("failed to run pre-down command", "err", err) diff --git a/core/router.go b/core/router.go index 024919c..26dd1f0 100644 --- a/core/router.go +++ b/core/router.go @@ -197,7 +197,7 @@ func (r *NylonRouter) Init(s *state.State) error { func (r *NylonRouter) ComputeSysRouteTable() []netip.Prefix { prefixes := make([]netip.Prefix, 0) selectedSelf := make(map[netip.Prefix]struct{}) - for entry, v := range r.ForwardTable.All() { + for entry, v := range r.Routes { prefixes = append(prefixes, entry) if v.Nh == r.Id { selectedSelf[entry] = struct{}{} diff --git a/core/router_algo.go b/core/router_algo.go index 6ce3c3a..2fe954f 100644 --- a/core/router_algo.go +++ b/core/router_algo.go @@ -153,7 +153,7 @@ func RunGC(s *state.RouterState, r Router) { for src := range s.Sources { found := false for _, neigh := range s.Neighbours { - if _, ok := neigh.Routes[src]; ok { + if nSrc, ok := neigh.Routes[src.Prefix]; ok && nSrc.Source == src { found = true break } @@ -289,7 +289,7 @@ func HandleNeighbourUpdate(s *state.RouterState, r Router, neighId state.NodeId, n := s.GetNeighbour(neighId) - _, ok := n.Routes[adv.Source] + _, ok := n.Routes[adv.Prefix] if adv.Metric == state.INF { r.SendAckRetract(neighId, adv.Source.Prefix) @@ -317,7 +317,7 @@ func HandleNeighbourUpdate(s *state.RouterState, r Router, neighId state.NodeId, // metric carried by the update. // create the route - n.Routes[adv.Source] = state.NeighRoute{ + n.Routes[adv.Prefix] = state.NeighRoute{ PubRoute: adv, ExpireAt: time.Now().Add(state.RouteExpiryTime), } @@ -366,13 +366,13 @@ func HandleNeighbourUpdate(s *state.RouterState, r Router, neighId state.NodeId, // update (possibly a retraction) MUST be sent in a timely manner as // described in Section 3.7.2. - nr := n.Routes[adv.Source] + nr := n.Routes[adv.Prefix] nr.PubRoute = adv if adv.Metric != state.INF { nr.ExpireAt = time.Now().Add(state.RouteExpiryTime) } - n.Routes[adv.Source] = nr + n.Routes[adv.Prefix] = nr } } @@ -502,9 +502,7 @@ func ComputeRoutes(s *state.RouterState, r Router) { } // enumerate through neighbour advertisements - for S, adv := range neigh.Routes { - prefix := S.Prefix - + for prefix, adv := range neigh.Routes { // Cost(A, B) + Cost(S, B) totalCost := AddMetric(CAB, adv.Metric) @@ -568,7 +566,7 @@ func ComputeRoutes(s *state.RouterState, r Router) { for prefix, newRoute := range newTable { oldRoute, exists := s.Routes[prefix] - if !exists { + if !exists || oldRoute.Metric == state.INF { r.TableInsertRoute(prefix, newRoute) r.Log(RouteChanged, "inserted", "prefix", prefix, "new", newRoute) } else if oldRoute.Nh != newRoute.Nh { diff --git a/core/router_harness.go b/core/router_harness.go index e3cfec0..95a2a9c 100644 --- a/core/router_harness.go +++ b/core/router_harness.go @@ -178,7 +178,7 @@ func MakeNeighbours(ids ...state.NodeId) []*state.Neighbour { for _, id := range ids { neighs = append(neighs, &state.Neighbour{ Id: id, - Routes: make(map[state.Source]state.NeighRoute), + Routes: make(map[netip.Prefix]state.NeighRoute), }) } return neighs diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000..344f079 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1 @@ +runs diff --git a/e2e/connectivity_test.go b/e2e/connectivity_test.go new file mode 100644 index 0000000..b8711c3 --- /dev/null +++ b/e2e/connectivity_test.go @@ -0,0 +1,88 @@ +//go:build e2e + +package e2e + +import ( + "testing" + "time" + + "github.com/encodeous/nylon/state" +) + +func TestConnectivity(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + t.Parallel() + + // Use a specific subnet for this test to avoid conflicts + h := NewHarness(t) + + // Generate keys + node1Key := state.GenerateKey() + node2Key := state.GenerateKey() + node3Key := state.GenerateKey() + + // IPs in the docker network + node1IP := GetIP(h.Subnet, 10) + node2IP := GetIP(h.Subnet, 11) + node3IP := GetIP(h.Subnet, 12) + + // Internal Nylon IPs + node1NylonIP := "10.0.0.1" + node2NylonIP := "10.0.0.2" + node3NylonIP := "10.0.0.3" + + // Create config directory for this test run + configDir := h.SetupTestDir() + + // 1. Create Central Config + central := state.CentralCfg{ + Routers: []state.RouterCfg{ + SimpleRouter("node1", node1Key.Pubkey(), node1NylonIP, ""), + SimpleRouter("node2", node2Key.Pubkey(), node2NylonIP, node2IP), + SimpleRouter("node3", node3Key.Pubkey(), node3NylonIP, ""), + }, + Graph: []string{ + "node1, node2", + "node2, node3", + }, + Timestamp: time.Now().UnixNano(), + } + + centralPath := h.WriteConfig(configDir, "central.yaml", central) + + // 2. Create Node Configs + node1Cfg := SimpleLocal("node1", node1Key) + node1Path := h.WriteConfig(configDir, "node1.yaml", node1Cfg) + + node2Cfg := SimpleLocal("node2", node2Key) + node2Path := h.WriteConfig(configDir, "node2.yaml", node2Cfg) + + node3Cfg := SimpleLocal("node3", node3Key) + node3Path := h.WriteConfig(configDir, "node3.yaml", node3Cfg) + + // 4. Start Containers in Parallel + h.StartNodes( + NodeSpec{Name: "node1", IP: node1IP, CentralConfigPath: centralPath, NodeConfigPath: node1Path}, + NodeSpec{Name: "node2", IP: node2IP, CentralConfigPath: centralPath, NodeConfigPath: node2Path}, + NodeSpec{Name: "node3", IP: node3IP, CentralConfigPath: centralPath, NodeConfigPath: node3Path}, + ) + + // 5. Wait for convergence + t.Log("Waiting for convergence...") + h.WaitForLog("node3", "installing new route prefix=10.0.0.1/32") + h.WaitForLog("node1", "installing new route prefix=10.0.0.2/31") + + // 6. Test Connectivity + // Ping from node1 to node2's Nylon IP + t.Logf("Pinging %s from node1...", node2NylonIP) + stdout, stderr, err := h.Exec("node3", []string{"ping", "-c", "3", node1NylonIP}) + if err != nil { + h.PrintLogs("node1") + h.PrintLogs("node2") + h.PrintLogs("node3") + t.Fatalf("Ping failed: %v\nStdout: %s\nStderr: %s", err, stdout, stderr) + } + t.Logf("Ping output:\n%s", stdout) +} diff --git a/e2e/harness.go b/e2e/harness.go new file mode 100644 index 0000000..f1ecd72 --- /dev/null +++ b/e2e/harness.go @@ -0,0 +1,368 @@ +//go:build e2e + +package e2e + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/pkg/stdcopy" + "github.com/testcontainers/testcontainers-go" + tcnetwork "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + ImageName = "nylon-debug:latest" + AppPort = "57175/udp" + WaitTimeout = 2 * time.Minute +) + +type Harness struct { + t *testing.T + mu sync.Mutex + ctx context.Context + Network *testcontainers.DockerNetwork + Nodes map[string]testcontainers.Container + LogBuffers map[string]*LogBuffer + ImageName string + RootDir string + Subnet string + Gateway string +} +type LogBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (l *LogBuffer) Write(p []byte) (n int, err error) { + l.mu.Lock() + defer l.mu.Unlock() + return l.buf.Write(p) +} +func (l *LogBuffer) String() string { + l.mu.Lock() + defer l.mu.Unlock() + return l.buf.String() +} + +// NewHarness creates a test harness with a unique subnet +func NewHarness(t *testing.T) *Harness { + ctx := context.Background() + // Find root directory (assuming we are in e2e/) + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + // Traversing up to find go.mod + rootDir := wd + for { + if _, err := os.Stat(filepath.Join(rootDir, "go.mod")); err == nil { + break + } + parent := filepath.Dir(rootDir) + if parent == rootDir { + t.Fatal("could not find project root") + } + rootDir = parent + } + + subnet, gateway := GlobalNetworkAllocator.Allocate() + t.Logf("Allocated subnet: %s, gateway: %s", subnet, gateway) + + // Create network with specific subnet + newNetwork, err := tcnetwork.New(ctx, + tcnetwork.WithAttachable(), + tcnetwork.WithDriver("bridge"), + tcnetwork.WithIPAM(&network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{ + { + Subnet: subnet, + Gateway: gateway, + }, + }, + })) + if err != nil { + t.Fatal(err) + } + h := &Harness{ + t: t, + ctx: ctx, + Network: newNetwork, + Nodes: make(map[string]testcontainers.Container), + LogBuffers: make(map[string]*LogBuffer), + RootDir: rootDir, + Subnet: subnet, + Gateway: gateway, + } + // Image building is handled in MainTest + t.Cleanup(func() { + h.Cleanup() + }) + return h +} + +var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + +func StripAnsi(s string) string { + return ansiRegex.ReplaceAllString(s, "") +} + +type LogConsumer struct { + Name string + Buffer *LogBuffer +} + +func (g *LogConsumer) Accept(l testcontainers.Log) { + content := string(l.Content) + // Strip ANSI codes for easier processing and cleaner output + cleanContent := StripAnsi(content) + fmt.Printf("[%s] %s", g.Name, cleanContent) + if g.Buffer != nil { + g.Buffer.Write([]byte(cleanContent)) + } +} + +type NodeSpec struct { + Name string + IP string + CentralConfigPath string + NodeConfigPath string +} + +func (h *Harness) StartNodes(specs ...NodeSpec) { + var wg sync.WaitGroup + wg.Add(len(specs)) + for _, spec := range specs { + go func(s NodeSpec) { + defer wg.Done() + h.StartNode(s.Name, s.IP, s.CentralConfigPath, s.NodeConfigPath) + }(spec) + } + wg.Wait() +} +func (h *Harness) StartNode(name string, ip string, centralConfigPath, nodeConfigPath string) testcontainers.Container { + h.t.Logf("Starting node %s at %s", name, ip) + req := testcontainers.ContainerRequest{ + Image: ImageName, + Networks: []string{h.Network.Name}, + NetworkAliases: map[string][]string{ + h.Network.Name: {name}, + }, + Files: []testcontainers.ContainerFile{ + { + HostFilePath: centralConfigPath, + ContainerFilePath: "/app/config/central.yaml", + FileMode: 0644, + }, + { + HostFilePath: nodeConfigPath, + ContainerFilePath: "/app/config/node.yaml", + FileMode: 0644, + }, + }, + Cmd: nil, // Entrypoint already handles "run -v" + Env: map[string]string{ + "NYLON_LOG_LEVEL": "debug", + }, + WaitingFor: wait.ForLog("Nylon has been initialized").WithStartupTimeout(15 * time.Second), + HostConfigModifier: func(hostConfig *container.HostConfig) { + hostConfig.Privileged = true + hostConfig.CapAdd = []string{"NET_ADMIN"} + }, + EndpointSettingsModifier: func(m map[string]*network.EndpointSettings) { + if ip != "" { + if s, ok := m[h.Network.Name]; ok { + s.IPAMConfig = &network.EndpointIPAMConfig{ + IPv4Address: ip, + } + } + } + }, + Name: h.t.Name() + "-" + name, + } + container, err := testcontainers.GenericContainer(h.ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + h.t.Fatalf("failed to start container %s: %v", name, err) + } + buffer := &LogBuffer{} + h.mu.Lock() + h.LogBuffers[name] = buffer + h.Nodes[name] = container + h.mu.Unlock() + container.FollowOutput(&LogConsumer{Name: name, Buffer: buffer}) + container.StartLogProducer(h.ctx) + return container +} +func (h *Harness) WaitForLog(nodeName string, pattern string) { + h.mu.Lock() + buffer, ok := h.LogBuffers[nodeName] + h.mu.Unlock() + if !ok { + h.t.Fatalf("log buffer for node %s not found", nodeName) + } + // Poll the buffer + timeout := time.After(WaitTimeout) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-timeout: + h.t.Fatalf("timed out waiting for log pattern %q in node %s", pattern, nodeName) + case <-ticker.C: + if strings.Contains(buffer.String(), pattern) { + return + } + case <-h.ctx.Done(): + h.t.Fatal("context canceled") + } + } +} +func (h *Harness) WaitForMatch(nodeName string, pattern string) { + h.mu.Lock() + buffer, ok := h.LogBuffers[nodeName] + h.mu.Unlock() + if !ok { + h.t.Fatalf("log buffer for node %s not found", nodeName) + } + + // Compile the regex once before the loop + re, err := regexp.Compile(pattern) + if err != nil { + h.t.Fatalf("invalid regex pattern %q: %v", pattern, err) + } + + // Poll the buffer + timeout := time.After(WaitTimeout) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-timeout: + h.t.Fatalf("timed out waiting for regex match %q in node %s", pattern, nodeName) + case <-ticker.C: + // Check against the compiled regex + if re.MatchString(buffer.String()) { + return + } + case <-h.ctx.Done(): + h.t.Fatal("context canceled") + } + } +} +func (h *Harness) Cleanup() { + h.mu.Lock() + defer h.mu.Unlock() + for name, c := range h.Nodes { + if err := c.Terminate(h.ctx); err != nil { + h.t.Logf("failed to terminate container %s: %v", name, err) + } + } + if err := h.Network.Remove(context.Background()); err != nil { + h.t.Logf("failed to remove network: %v", err) + } +} +func (h *Harness) Exec(nodeName string, cmd []string) (string, string, error) { + h.mu.Lock() + container, ok := h.Nodes[nodeName] + h.mu.Unlock() + + if !ok { + return "", "", fmt.Errorf("node %s not found", nodeName) + } + + code, r, err := container.Exec(h.ctx, cmd) + if err != nil { + return "", "", err + } + + stdoutBuf := new(bytes.Buffer) + stderrBuf := new(bytes.Buffer) + + // Demultiplex the stream using stdcopy + _, err = stdcopy.StdCopy(stdoutBuf, stderrBuf, r) + if err != nil { + return "", "", fmt.Errorf("failed to copy output: %w", err) + } + + stdout := StripAnsi(stdoutBuf.String()) + stderr := StripAnsi(stderrBuf.String()) + + if code != 0 { + return stdout, stderr, fmt.Errorf("command exited with code %d: %s\nStderr: %s", code, stdout, stderr) + } + + return stdout, stderr, nil +} + +type BackgroundExec struct { + Stdout string + Stderr string + Err error + done chan struct{} +} + +func (e *BackgroundExec) Wait() (string, string, error) { + select { + case <-e.done: + break + case <-time.After(WaitTimeout): + return "", "", fmt.Errorf("timed out waiting for command to finish") + } + return e.Stdout, e.Stderr, e.Err +} + +func (h *Harness) ExecBackground(nodeName string, cmd []string) *BackgroundExec { + bg := &BackgroundExec{ + done: make(chan struct{}), + } + go func() { + defer close(bg.done) + bg.Stdout, bg.Stderr, bg.Err = h.Exec(nodeName, cmd) + }() + return bg +} + +// GetIP returns the IP address of the node in the test network +func (h *Harness) GetIP(nodeName string) (string, error) { + h.mu.Lock() + container, ok := h.Nodes[nodeName] + h.mu.Unlock() + if !ok { + return "", fmt.Errorf("node %s not found", nodeName) + } + return container.ContainerIP(h.ctx) +} +func (h *Harness) PrintLogs(nodeName string) { + h.mu.Lock() + container, ok := h.Nodes[nodeName] + h.mu.Unlock() + if !ok { + h.t.Logf("node %s not found for logging", nodeName) + return + } + r, err := container.Logs(h.ctx) + if err != nil { + h.t.Logf("failed to get logs for %s: %v", nodeName, err) + return + } + buf := new(bytes.Buffer) + io.Copy(buf, r) + h.t.Logf("Logs for %s:\n%s", nodeName, buf.String()) +} diff --git a/e2e/healthcheck_test.go b/e2e/healthcheck_test.go new file mode 100644 index 0000000..9513e6c --- /dev/null +++ b/e2e/healthcheck_test.go @@ -0,0 +1,131 @@ +//go:build e2e + +package e2e + +import ( + "fmt" + "net/netip" + "strings" + "testing" + "time" + + "github.com/encodeous/nylon/state" + "github.com/stretchr/testify/assert" +) + +func TestHealthcheckPing(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + t.Parallel() + + // Use a specific subnet for this test to avoid conflicts + h := NewHarness(t) + + // Generate keys + node1Key := state.GenerateKey() + node2Key := state.GenerateKey() + node3Key := state.GenerateKey() + + // IPs in the docker network + node1IP := GetIP(h.Subnet, 10) + node2IP := GetIP(h.Subnet, 11) + node3IP := GetIP(h.Subnet, 12) + + // Internal Nylon IPs + node1NylonIP := "10.0.0.1" + node2NylonIP := "10.0.0.2" + node3NylonIP := "10.0.0.3" + + // Create config directory for this test run + configDir := h.SetupTestDir() + + // 1. Create Central Config + central := state.CentralCfg{ + Routers: []state.RouterCfg{ + SimpleRouter("node1", node1Key.Pubkey(), node1NylonIP, ""), + SimpleRouter("node2", node2Key.Pubkey(), node2NylonIP, node2IP), + SimpleRouter("node3", node3Key.Pubkey(), node3NylonIP, ""), + }, + Graph: []string{ + "node1, node2", + "node2, node3", + }, + Timestamp: time.Now().UnixNano(), + } + + // make node 1 and node 2 both advertise 10.0.0.4/32 + // 1 would be default + n1Metric := uint32(10) + central.Routers[0].Prefixes = []state.PrefixHealthWrapper{ + { + &state.PingPrefixHealth{ + Prefix: netip.MustParsePrefix("10.0.1.4/32"), + Addr: netip.MustParseAddr("10.0.1.4"), + Metric: &n1Metric, + }, + }, + } + // 2 would be fallback + n2Metric := uint32(1000) + central.Routers[1].Prefixes = []state.PrefixHealthWrapper{ + { + &state.PingPrefixHealth{ + Prefix: netip.MustParsePrefix("10.0.1.4/32"), + Addr: netip.MustParseAddr("10.0.1.4"), + Metric: &n2Metric, + }, + }, + } + + centralPath := h.WriteConfig(configDir, "central.yaml", central) + + // 2. Create Node Configs + node1Cfg := SimpleLocal("node1", node1Key) + node2Cfg := SimpleLocal("node2", node2Key) + node3Cfg := SimpleLocal("node3", node3Key) + + // add a dummy loopback interface on each node + node1Cfg.PreUp = append(node1Cfg.PreUp, "ip addr add 10.0.1.4/32 dev lo") + node1Cfg.PreUp = append(node1Cfg.PreUp, "ip route add 10.0.1.4/32 dev lo") + node2Cfg.PreUp = append(node2Cfg.PreUp, "ip addr add 10.0.1.4/32 dev lo") + node2Cfg.PreUp = append(node2Cfg.PreUp, "ip route add 10.0.1.4/32 dev lo") + + node1Path := h.WriteConfig(configDir, "node1.yaml", node1Cfg) + node2Path := h.WriteConfig(configDir, "node2.yaml", node2Cfg) + node3Path := h.WriteConfig(configDir, "node3.yaml", node3Cfg) + + // 4. Start Containers in Parallel + h.StartNodes( + NodeSpec{Name: "node1", IP: node1IP, CentralConfigPath: centralPath, NodeConfigPath: node1Path}, + NodeSpec{Name: "node2", IP: node2IP, CentralConfigPath: centralPath, NodeConfigPath: node2Path}, + NodeSpec{Name: "node3", IP: node3IP, CentralConfigPath: centralPath, NodeConfigPath: node3Path}, + ) + + // 5. Wait for convergence + t.Log("Waiting for convergence...") + h.WaitForMatch("node3", ".+old.router=node2.+old.prefix=10.0.1.4\\/32.+new.router=node1.+new.prefix=10.0.1.4\\/32.+") + h.WaitForLog("node1", "prefix=10.0.0.3/32") + + // ping from 3 to 10.0.0.4 + stdout, stderr, err := h.Exec("node3", []string{"ping", "-c", "3", "10.0.1.4"}) + if err != nil { + t.Fatalf("Ping failed: %v\nStdout: %s\nStderr: %s", err, stdout, stderr) + } + t.Logf("Ping output:\n%s", stdout) + + msg := "hello from node 3" + // listen on node 1 + bg := h.ExecBackground("node1", []string{"nc", "-l", "8888"}) + // send on node 3 + stdout, stderr, err = h.Exec("node3", []string{"bash", "-c", fmt.Sprintf("echo '%s' | nc -N 10.0.1.4 8888", msg)}) + if err != nil { + t.Fatalf("Failed: %v\nStdout: %s\nStderr: %s", err, stdout, stderr) + } + //time.Sleep(time.Hour) + stdout, stderr, err = bg.Wait() + if err != nil { + t.Fatalf("Failed to listen: %v\nStdout: %s\nStderr: %s", err, stdout, stderr) + } + assert.Equal(t, msg, strings.TrimSpace(stdout)) +} diff --git a/e2e/main_test.go b/e2e/main_test.go new file mode 100644 index 0000000..f71935f --- /dev/null +++ b/e2e/main_test.go @@ -0,0 +1,72 @@ +//go:build e2e + +package e2e + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/docker/docker/api/types/build" + "github.com/testcontainers/testcontainers-go" +) + +func TestMain(m *testing.M) { + if err := buildImage(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to build image: %v\n", err) + os.Exit(1) + } + os.Exit(m.Run()) +} + +func buildImage() error { + ctx := context.Background() + // Find root directory + wd, err := os.Getwd() + if err != nil { + return err + } + // Traversing up to find go.mod + rootDir := wd + for { + if _, err := os.Stat(filepath.Join(rootDir, "go.mod")); err == nil { + break + } + parent := filepath.Dir(rootDir) + if parent == rootDir { + return fmt.Errorf("could not find project root") + } + rootDir = parent + } + + fmt.Println("Pre-building nylon-debug:latest image...") + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Context: rootDir, + Dockerfile: "Dockerfile", + KeepImage: true, + Repo: "nylon-debug", + Tag: "latest", + BuildOptionsModifier: func(buildOptions *build.ImageBuildOptions) { + buildOptions.Target = "debug" + }, + }, + } + + // Creating the container triggers the build + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: false, + }) + if err != nil { + return fmt.Errorf("failed to build image: %v", err) + } + + // We don't need this container, just the image. + if err := c.Terminate(ctx); err != nil { + fmt.Printf("Warning: failed to terminate builder container: %v\n", err) + } + return nil +} diff --git a/e2e/network_allocator.go b/e2e/network_allocator.go new file mode 100644 index 0000000..46195c5 --- /dev/null +++ b/e2e/network_allocator.go @@ -0,0 +1,58 @@ +package e2e + +import ( + "fmt" + "sync" +) + +type NetworkAllocator struct { + mu sync.Mutex + counter int +} + +var GlobalNetworkAllocator = &NetworkAllocator{} + +// Allocate returns a unique subnet and gateway for a test run. +// It assumes a base network of 172.20.0.0/16 and hands out /24 subnets. +// e.g. 0 -> 172.20.0.0/24 (Gateway 172.20.0.1) +// +// 1 -> 172.20.1.0/24 (Gateway 172.20.1.1) +func (s *NetworkAllocator) Allocate() (string, string) { + s.mu.Lock() + defer s.mu.Unlock() + + idx := s.counter + s.counter++ + + subnet := fmt.Sprintf("172.20.%d.0/24", idx) + gateway := fmt.Sprintf("172.20.%d.1", idx) + + return subnet, gateway +} + +// GetIP returns an IP address within the allocated subnet for a given host suffix. +// subnet is expected to be in "172.20.x.0/24" format. +// suffix is the last octet (e.g. 10). +func GetIP(subnet string, suffix int) string { + // Simple parsing, assuming the format generated by Allocate + // subnet: 172.20.x.0/24 + base := subnet[:len(subnet)-4] // remove /24 + // base: 172.20.x.0 + // we want 172.20.x. + + // find the last dot + lastDot := -1 + for i := len(base) - 1; i >= 0; i-- { + if base[i] == '.' { + lastDot = i + break + } + } + + if lastDot == -1 { + panic(fmt.Sprintf("invalid subnet format: %s", subnet)) + } + + prefix := base[:lastDot+1] // 172.20.x. + return fmt.Sprintf("%s%d", prefix, suffix) +} diff --git a/e2e/utils.go b/e2e/utils.go new file mode 100644 index 0000000..7109c66 --- /dev/null +++ b/e2e/utils.go @@ -0,0 +1,67 @@ +//go:build e2e + +package e2e + +import ( + "fmt" + "net/netip" + "os" + "path/filepath" + + "github.com/encodeous/nylon/state" + "github.com/goccy/go-yaml" +) + +// SetupTestDir creates a directory for the current test run +func (h *Harness) SetupTestDir() string { + dir := filepath.Join(h.RootDir, "e2e", "runs", h.t.Name()) + // Clean up previous run + os.RemoveAll(dir) + if err := os.MkdirAll(dir, 0755); err != nil { + h.t.Fatal(err) + } + return dir +} + +// WriteConfig marshals the config to YAML and writes it to the specified directory with the given filename +func (h *Harness) WriteConfig(dir, filename string, cfg any) string { + path := filepath.Join(dir, filename) + data, err := yaml.Marshal(cfg) + if err != nil { + h.t.Fatal(err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + h.t.Fatal(err) + } + return path +} + +// SimpleRouter creates a basic RouterCfg with the given parameters +func SimpleRouter(id string, pubKey state.NyPublicKey, nylonIP string, endpointIP string) state.RouterCfg { + cfg := state.RouterCfg{ + NodeCfg: state.NodeCfg{ + Id: state.NodeId(id), + PubKey: pubKey, + Addresses: []netip.Addr{ + netip.MustParseAddr(nylonIP), + }, + }, + } + if endpointIP != "" { + cfg.Endpoints = []netip.AddrPort{ + netip.MustParseAddrPort(fmt.Sprintf("%s:57175", endpointIP)), + } + } + return cfg +} + +// SimpleLocal creates a basic LocalCfg with the given parameters and defaults +func SimpleLocal(id string, key state.NyPrivateKey) state.LocalCfg { + return state.LocalCfg{ + Id: state.NodeId(id), + Key: key, + Port: 57175, + NoNetConfigure: false, + InterfaceName: "nylon0", + } +} diff --git a/go.mod b/go.mod index 13a4a3f..5b0b23d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.4 require ( github.com/cilium/cilium v1.18.4 github.com/digineo/go-ping v1.2.0 + github.com/docker/docker v28.5.1+incompatible github.com/encodeous/metric v0.0.0-20251111175231-f339c2f7c4bd github.com/encodeous/tint v1.2.0 github.com/gaissmai/bart v0.25.0 @@ -15,34 +16,80 @@ require ( github.com/samber/slog-multi v1.5.0 github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 go.step.sm/crypto v0.70.0 go.uber.org/goleak v1.3.0 golang.org/x/crypto v0.45.0 golang.org/x/net v0.47.0 - golang.org/x/sys v0.38.0 + golang.org/x/sys v0.39.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 google.golang.org/protobuf v1.36.10 gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c ) require ( + dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/samber/lo v1.51.0 // indirect github.com/samber/slog-common v0.19.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ec57bb2..529e16f 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,121 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/cilium v1.18.4 h1:/HrbeMmk46UDkJs4uJemIqxB7wuZ+QRMNVscNYua5C8= github.com/cilium/cilium v1.18.4/go.mod h1:PPAuhDhMHOLaAiraQKDxEHUTmE68RrDlZj3988R+Lco= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 h1:OT/LKmj81wMymnWXaKaKBR9n1vPlu+GC0VVKaZP6kzs= github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0/go.mod h1:DmqdumeAKGQNU5E8MN0ruT5ZGx8l/WbAsMbXCXcSEts= github.com/digineo/go-ping v1.2.0 h1:/9vEsoCRtQvol5vRMA2pE8guuhUVDSJB2ok2nKuJJbA= github.com/digineo/go-ping v1.2.0/go.mod h1:cXJTVTs7mthQ41c/nWykPYuhlDQwCN0ba5LyOnyYv8Y= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/encodeous/metric v0.0.0-20251111175231-f339c2f7c4bd h1:B32Ob80QTv5MomcVt709TsiWyD0QrpUYtnwW1jQFNlE= github.com/encodeous/metric v0.0.0-20251111175231-f339c2f7c4bd/go.mod h1:DiXCPJtfZYioejF9zv9wfs3TXqWWglKGQ20DsBNVWVw= github.com/encodeous/tint v1.2.0 h1:1Y+32Iu+C8MXBoNjsM4YDf6iAkcks7csAI9f7b4fr8k= github.com/encodeous/tint v1.2.0/go.mod h1:pyfyH+fKmtmIuWVWeMcSJjJKVJSb9sVlIfVsW3jkP+Q= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gaissmai/bart v0.25.0 h1:eqiokVPqM3F94vJ0bTHXHtH91S8zkKL+bKh+BsGOsJM= github.com/gaissmai/bart v0.25.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kmahyyg/go-network-compo v0.2.10 h1:H5rZ59xxmaZsIucz5FGV56ww+nzR3k1RFaKEN2bYRog= github.com/kmahyyg/go-network-compo v0.2.10/go.mod h1:LN1qGQuWqk6v27GXWWiXGIaYmV8TUPZrPGgm0LFwmbU= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -48,17 +125,53 @@ github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89 github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= github.com/samber/slog-multi v1.5.0 h1:UDRJdsdb0R5vFQFy3l26rpX3rL3FEPJTJ2yKVjoiT1I= github.com/samber/slog-multi v1.5.0/go.mod h1:im2Zi3mH/ivSY5XDj6LFcKToRIWPw1OcjSVSdXt+2d0= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.step.sm/crypto v0.70.0 h1:Q9Ft7N637mucyZcHZd1+0VVQJVwDCKqcb9CYcYi7cds= go.step.sm/crypto v0.70.0/go.mod h1:pzfUhS5/ue7ev64PLlEgXvhx1opwbhFCjkvlhsxVds0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -71,22 +184,41 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..89dbe7b --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +go = "1.25.4" diff --git a/smoke/fixtures/testcentral1.yaml b/smoke/fixtures/testcentral1.yaml deleted file mode 100755 index 7fe4d13..0000000 --- a/smoke/fixtures/testcentral1.yaml +++ /dev/null @@ -1,18 +0,0 @@ -dist: - key: YjlOIfTaJVGZ+j638l/j4KKHl26MPwyfmjIoKiIWhHs= - repos: - - file:~/.nylon/central.nybundle - - https://127.0.0.1:8000/example/central.nybundle -routers: - - id: sample_node1 - pubkey: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - addresses: [10.0.0.1] - endpoints: - - 8.8.8.8:57175 -clients: - - id: external-client - pubkey: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - addresses: [10.2.0.1] -graph: - - Group1 = sample_node1, sample_node1 -timestamp: 1740879895918834978 diff --git a/smoke/fixtures/testnode1.yaml b/smoke/fixtures/testnode1.yaml deleted file mode 100644 index ab6597f..0000000 --- a/smoke/fixtures/testnode1.yaml +++ /dev/null @@ -1,5 +0,0 @@ -key: IG5bLY5ar8+IXeqgI4pVUYqqQFbyCVhF2qUA4f/54Uo= -id: sample_node1 -port: 57175 -no_net_configure: false - diff --git a/smoke/smoke-test.sh b/smoke/smoke-test.sh deleted file mode 100755 index 1249f7a..0000000 --- a/smoke/smoke-test.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Detect if terminal supports colors -if [ -t 1 ] && command -v tput &> /dev/null && tput colors &> /dev/null && [ $(tput colors) -ge 8 ]; then - USE_COLOR=true -else - USE_COLOR=false -fi - -print_status() { - if [ "$USE_COLOR" = true ]; then - echo -e "${GREEN}✓${NC} $1" - else - echo "✓ $1" - fi -} - -print_error() { - if [ "$USE_COLOR" = true ]; then - echo -e "${RED}✗${NC} $1" - else - echo "✗ $1" - fi -} - -print_info() { - if [ "$USE_COLOR" = true ]; then - echo -e "${YELLOW}ℹ${NC} $1" - else - echo "ℹ $1" - fi -} - -# Change to script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -# Check if nylon binary exists -NYLON_BINARY="../nylon" -if [ ! -f "$NYLON_BINARY" ]; then - print_error "Nylon binary not found at $NYLON_BINARY" - print_info "Please build the binary first: go build -o nylon" - exit 1 -fi - -# Make nylon binary executable -chmod +x "$NYLON_BINARY" - -print_info "Running smoke tests for Nylon..." -echo "" - -# Test 1: Check that nylon executes and shows help -print_info "Test 1: Checking if nylon executes and shows help text..." - -CONTAINER_NAME="nylon-smoke-test-1-$$" -docker run --rm --name "$CONTAINER_NAME" \ - -v "$(cd .. && pwd)/nylon:/nylon:ro" \ - busybox:1.37-glibc \ - /nylon > /tmp/nylon-output-1.txt 2>&1 || true - -if grep -q "Nylon is a mesh networking system." /tmp/nylon-output-1.txt; then - print_status "Test 1 PASSED: Nylon executes and shows help text" -else - print_error "Test 1 FAILED: Expected help text not found" - cat /tmp/nylon-output-1.txt - rm -f /tmp/nylon-output-1.txt - exit 1 -fi -rm -f /tmp/nylon-output-1.txt -echo "" - -# Test 2: Check that nylon runs with config files -print_info "Test 2: Checking if nylon runs with config files..." - -CONTAINER_NAME="nylon-smoke-test-2-$$" -CONTAINER_ID=$(docker run -d --name "$CONTAINER_NAME" \ - --cap-add=NET_ADMIN \ - -v "$(cd .. && pwd)/nylon:/nylon:ro" \ - -v "$SCRIPT_DIR/fixtures/testcentral1.yaml:/central.yaml:ro" \ - -v "$SCRIPT_DIR/fixtures/testnode1.yaml:/node.yaml:ro" \ - busybox:1.37-glibc \ - sh -c "mkdir -p /dev/net && mknod /dev/net/tun c 10 200 && /nylon -c /central.yaml -n /node.yaml run") - -# Wait for the expected log message (with timeout) -TIMEOUT=10 -ELAPSED=0 -SUCCESS=false - -while [ $ELAPSED -lt $TIMEOUT ]; do - if docker logs "$CONTAINER_ID" 2>&1 | grep -q "Nylon has been initialized. To gracefully exit, send SIGINT or Ctrl+C."; then - SUCCESS=true - break - fi - sleep 1 - ELAPSED=$((ELAPSED + 1)) -done - -# Cleanup -docker stop "$CONTAINER_ID" > /dev/null 2>&1 || true -docker rm "$CONTAINER_ID" > /dev/null 2>&1 || true - -if [ "$SUCCESS" = true ]; then - print_status "Test 2 PASSED: Nylon runs successfully with config files" -else - print_error "Test 2 FAILED: Expected initialization message not found within ${TIMEOUT}s" - docker logs "$CONTAINER_ID" 2>&1 || true - exit 1 -fi -echo "" - -print_status "All smoke tests passed!" -exit 0 - diff --git a/state/config_test.go b/state/config_test.go index 87cedfb..bdc4e3f 100644 --- a/state/config_test.go +++ b/state/config_test.go @@ -1,9 +1,11 @@ package state import ( - "github.com/stretchr/testify/assert" + "net/netip" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestParseGraph_SimpleGraph(t *testing.T) { @@ -148,3 +150,32 @@ func TestParseGraph_InvalidGraph(t *testing.T) { failGraph(t, `,,,,,,,,,,,,,,,,`) failGraph(t, `a=a`) } + +func TestSubtractPrefixDirect(t *testing.T) { + includes := []netip.Prefix{ + netip.MustParsePrefix("10.0.0.1/32"), + netip.MustParsePrefix("10.0.0.2/32"), + netip.MustParsePrefix("10.0.0.3/32"), + } + excludes := []netip.Prefix{ + netip.MustParsePrefix("10.0.0.3/32"), + } + result := SubtractPrefix(includes, excludes) + assert.ElementsMatch(t, result, []netip.Prefix{ + netip.MustParsePrefix("10.0.0.1/32"), + netip.MustParsePrefix("10.0.0.2/32"), + }) +} + +func TestSubtractPrefixLargerRange(t *testing.T) { + includes := []netip.Prefix{ + netip.MustParsePrefix("10.0.0.1/32"), + netip.MustParsePrefix("10.0.0.2/32"), + netip.MustParsePrefix("10.0.0.3/32"), + } + excludes := []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/24"), + } + result := SubtractPrefix(includes, excludes) + assert.ElementsMatch(t, result, []netip.Prefix{}) +} diff --git a/state/constants.go b/state/constants.go index 3c741d4..42fce72 100644 --- a/state/constants.go +++ b/state/constants.go @@ -37,4 +37,8 @@ var ( // central updates CentralUpdateDelay = time.Second * 10 + + // healthcheck defaults + HealthCheckDelay = time.Second * 15 + HealthCheckMaxFailures = 3 ) diff --git a/state/debug.go b/state/debug.go index c7ff52f..0971e30 100644 --- a/state/debug.go +++ b/state/debug.go @@ -5,3 +5,4 @@ var DBG_log_wireguard = false var DBG_log_repo_updates = false var DBG_debug = false var DBG_trace = false +var DBG_trace_tc = false diff --git a/state/prefix_health.go b/state/prefix_health.go index 6276310..27db535 100644 --- a/state/prefix_health.go +++ b/state/prefix_health.go @@ -40,12 +40,12 @@ func (s *StaticPrefixHealth) Start(log *slog.Logger) { } type PingPrefixHealth struct { - Prefix netip.Prefix `yaml:"prefix"` - Addr netip.Addr `yaml:"addr"` // the address to ping - MaxFailures int `yaml:"max_failures,omitempty"` // number of failures before returning infinite metric - Delay time.Duration `yaml:"delay,omitempty"` // delay between pings - BindIf string `yaml:"bind_if,omitempty"` // local interface to bind to - Metric *uint32 `yaml:"metric,omitempty"` // metric override + Prefix netip.Prefix `yaml:"prefix"` + Addr netip.Addr `yaml:"addr"` // the address to ping + MaxFailures *int `yaml:"max_failures,omitempty"` // number of failures before returning infinite metric + Delay *time.Duration `yaml:"delay,omitempty"` // delay between pings + BindIf string `yaml:"bind_if,omitempty"` // local interface to bind to + Metric *uint32 `yaml:"metric,omitempty"` // metric override lastMetric uint32 running atomic.Bool } @@ -88,10 +88,16 @@ func (p *PingPrefixHealth) GetPrefix() netip.Prefix { } func (p *PingPrefixHealth) Start(log *slog.Logger) { p.running.Swap(true) + if p.Delay == nil { + p.Delay = &HealthCheckDelay + } + if p.MaxFailures == nil { + p.MaxFailures = &HealthCheckMaxFailures + } go func() { - ticker := time.NewTicker(p.Delay) + ticker := time.NewTicker(*p.Delay) for p.running.Load() { - time.Sleep(p.Delay) + time.Sleep(*p.Delay) p.lastMetric = INF bind4 := "" bind6 := "" @@ -122,7 +128,7 @@ func (p *PingPrefixHealth) Start(log *slog.Logger) { <-ticker.C // ICMP ping addr := &net.IPAddr{IP: net.IP(p.Addr.AsSlice())} - rtt, err := pinger.PingAttempts(addr, time.Duration(int64(p.Delay)/int64(p.MaxFailures)), p.MaxFailures) + rtt, err := pinger.PingAttempts(addr, time.Duration(int64(*p.Delay)/int64(*p.MaxFailures)), *p.MaxFailures) if err != nil { // failed p.lastMetric = INF @@ -139,10 +145,10 @@ func (p *PingPrefixHealth) Start(log *slog.Logger) { } type HTTPPrefixHealth struct { - Prefix netip.Prefix `yaml:"prefix"` - URL string `yaml:"url"` // the URL to check - Delay time.Duration `yaml:"delay,omitempty"` // delay between probes - Metric *uint32 `yaml:"metric,omitempty"` // metric override + Prefix netip.Prefix `yaml:"prefix"` + URL string `yaml:"url"` // the URL to check + Delay *time.Duration `yaml:"delay,omitempty"` // delay between probes + Metric *uint32 `yaml:"metric,omitempty"` // metric override lastMetric uint32 running atomic.Bool } @@ -163,8 +169,11 @@ func (h *HTTPPrefixHealth) GetPrefix() netip.Prefix { func (h *HTTPPrefixHealth) Start(log *slog.Logger) { h.lastMetric = INF h.running.Swap(true) + if h.Delay == nil { + h.Delay = &HealthCheckDelay + } go func() { - ticker := time.NewTicker(h.Delay) + ticker := time.NewTicker(*h.Delay) defer ticker.Stop() for h.running.Load() { // TODO: add a way to interrupt this sleep, if ticker has a high delay <-ticker.C diff --git a/state/prefix_health_test.go b/state/prefix_health_test.go index fd2deb3..3954386 100644 --- a/state/prefix_health_test.go +++ b/state/prefix_health_test.go @@ -10,6 +10,9 @@ import ( ) func TestPrefixHealthSerialization(t *testing.T) { + threeFails := 3 + tenSecond := 10 * time.Second + fiveSecond := 5 * time.Second tests := []struct { name string wrapper PrefixHealthWrapper @@ -34,8 +37,8 @@ metric: 100 PrefixHealth: &PingPrefixHealth{ Prefix: netip.MustParsePrefix("192.168.1.0/24"), Addr: netip.MustParseAddr("8.8.8.8"), - MaxFailures: 3, - Delay: 10 * time.Second, + MaxFailures: &threeFails, + Delay: &tenSecond, }, }, yamlStr: `type: ping @@ -51,7 +54,7 @@ delay: 10s PrefixHealth: &HTTPPrefixHealth{ Prefix: netip.MustParsePrefix("172.16.0.0/16"), URL: "http://example.com/health", - Delay: 5 * time.Second, + Delay: &fiveSecond, }, }, yamlStr: `type: http diff --git a/state/routing.go b/state/routing.go index 01c0c7f..eb18a90 100644 --- a/state/routing.go +++ b/state/routing.go @@ -2,6 +2,7 @@ package state import ( "fmt" + "log/slog" "net/netip" "slices" "strings" @@ -16,6 +17,13 @@ type Source struct { netip.Prefix } +func (s Source) LogValue() slog.Value { + return slog.GroupValue( + slog.String("router", string(s.NodeId)), + slog.String("prefix", s.Prefix.String()), + ) +} + func (s Source) String() string { return fmt.Sprintf("(router: %s, prefix: %s)", s.NodeId, s.Prefix) } @@ -60,7 +68,7 @@ func (s *RouterState) StringRoutes() string { type Neighbour struct { Id NodeId - Routes map[Source]NeighRoute + Routes map[netip.Prefix]NeighRoute Eps []Endpoint } @@ -82,6 +90,15 @@ func (r PubRoute) String() string { return fmt.Sprintf("(router: %s, prefix: %s, seqno: %d, metric: %d)", r.NodeId, r.Prefix, r.Seqno, r.Metric) } +func (r PubRoute) LogValue() slog.Value { + return slog.GroupValue( + slog.String("router", string(r.NodeId)), + slog.String("prefix", r.Prefix.String()), + slog.Uint64("seqno", uint64(r.Seqno)), + slog.Uint64("metric", uint64(r.Metric)), + ) +} + type NeighRoute struct { PubRoute ExpireAt time.Time // when the route expires @@ -98,6 +115,16 @@ func (r SelRoute) String() string { return fmt.Sprintf("(nh: %s, router: %s, prefix: %s, seqno: %d, metric: %d)", r.Nh, r.NodeId, r.Prefix, r.Seqno, r.Metric) } +func (r SelRoute) LogValue() slog.Value { + return slog.GroupValue( + slog.Any("nh", r.Nh), // Use Any if Nh is an object/interface + slog.String("router", string(r.NodeId)), + slog.String("prefix", r.Prefix.String()), + slog.Uint64("seqno", uint64(r.Seqno)), + slog.Uint64("metric", uint64(r.Metric)), + ) +} + func (s *RouterState) GetNeighbour(node NodeId) *Neighbour { nIdx := slices.IndexFunc(s.Neighbours, func(neighbour *Neighbour) bool { return neighbour.Id == node diff --git a/state/validation.go b/state/validation.go index 1f36a59..5032a31 100644 --- a/state/validation.go +++ b/state/validation.go @@ -171,10 +171,10 @@ func CentralConfigValidator(cfg *CentralCfg) error { if !v.Addr.IsValid() { return fmt.Errorf("invalid ping address %s for prefix %s", v.Addr, p.GetPrefix()) } - if v.Delay <= 0 { + if v.Delay != nil && *v.Delay <= 0 { return fmt.Errorf("ping delay must be greater than 0 for prefix %s", p.GetPrefix()) } - if v.MaxFailures <= 0 { + if v.MaxFailures != nil && *v.MaxFailures <= 0 { return fmt.Errorf("ping max_failures must be greater than 0 for prefix %s", p.GetPrefix()) } case *HTTPPrefixHealth: @@ -182,7 +182,7 @@ func CentralConfigValidator(cfg *CentralCfg) error { if err != nil { return fmt.Errorf("invalid HTTP URL %s for prefix %s: %v", v.URL, p.GetPrefix(), err) } - if v.Delay <= 0 { + if v.Delay != nil && *v.Delay <= 0 { return fmt.Errorf("HTTP delay must be greater than 0 for prefix %s", p.GetPrefix()) } default: diff --git a/unit-test-coverage.sh b/unit-test-coverage.sh deleted file mode 100755 index d1fd683..0000000 --- a/unit-test-coverage.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e - -echo "Running tests with coverage..." -go test -v -coverprofile=coverage.out -tags=router_test ./... -echo "Generating HTML coverage report..." -go tool cover -html=coverage.out -o coverage.html -echo "Coverage report generated: coverage.html"