File created successfully at: /Users/misha/Documents/GolandProjects/netbird/netbird/combined/perftest/main.go

package main

import (
	"context"
	"encoding/binary"
	"flag"
	"fmt"
	"math"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/netbirdio/netbird/client/embed"
	"github.com/netbirdio/netbird/client/internal/peer"
)

const (
	echoPort       = 9000
	connectTimeout = 120 * time.Second
	startTimeout   = 60 * time.Second
	stopTimeout    = 30 * time.Second
)

type peerInfo struct {
	client   *embed.Client
	tunnelIP string
	name     string
}

type pairStats struct {
	from     string
	to       string
	sent     int64
	received int64
	lost     int64
	rtts     []time.Duration
}

func (s *pairStats) summary() (avgRTT, minRTT, maxRTT time.Duration, lossPercent float64) {
	if len(s.rtts) == 0 {
		return 0, 0, 0, 100
	}
	minRTT = s.rtts[0]
	maxRTT = s.rtts[0]
	var total time.Duration
	for _, rtt := range s.rtts {
		total += rtt
		if rtt < minRTT {
			minRTT = rtt
		}
		if rtt > maxRTT {
			maxRTT = rtt
		}
	}
	avgRTT = total / time.Duration(len(s.rtts))
	if s.sent > 0 {
		lossPercent = float64(s.lost) / float64(s.sent) * 100
	}
	return
}

func main() {
	managementURL := flag.String("management-url", "", "Management server URL (required)")
	setupKey := flag.String("setup-key", "", "Reusable setup key (required)")
	numPeers := flag.Int("peers", 5, "Number of peers to spawn")
	forceRelay := flag.Bool("force-relay", false, "Force relay connections (NB_FORCE_RELAY=true)")
	duration := flag.Duration("duration", 30*time.Second, "Traffic test duration")
	packetSize := flag.Int("packet-size", 512, "UDP packet size in bytes")
	logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")
	flag.Parse()

	if *managementURL == "" || *setupKey == "" {
		fmt.Fprintln(os.Stderr, "Error: --management-url and --setup-key are required")
		flag.Usage()
		os.Exit(1)
	}

	if *numPeers < 2 {
		fmt.Fprintln(os.Stderr, "Error: --peers must be at least 2")
		os.Exit(1)
	}

	// Minimum packet size: 8 bytes for timestamp + 8 bytes for sequence number
	if *packetSize < 16 {
		fmt.Fprintln(os.Stderr, "Error: --packet-size must be at least 16")
		os.Exit(1)
	}

	if *forceRelay {
		os.Setenv("NB_FORCE_RELAY", "true")
	}
	os.Setenv("NB_USE_NETSTACK_MODE", "true")

	fmt.Println("=== NetBird Performance Test ===")
	fmt.Printf("Management URL: %s\n", *managementURL)
	fmt.Printf("Peers:          %d\n", *numPeers)
	fmt.Printf("Force relay:    %v\n", *forceRelay)
	fmt.Printf("Duration:       %s\n", *duration)
	fmt.Printf("Packet size:    %d bytes\n", *packetSize)
	fmt.Println()

	// Phase 1: Create peers
	fmt.Println("--- Phase 1: Creating peers ---")
	peers := make([]peerInfo, *numPeers)
	for i := 0; i < *numPeers; i++ {
		name := fmt.Sprintf("perf-peer-%d", i)
		port := 0
		c, err := embed.New(embed.Options{
			DeviceName:    name,
			SetupKey:      *setupKey,
			ManagementURL: *managementURL,
			WireguardPort: &port,
			LogLevel:      *logLevel,
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error creating peer %s: %v\n", name, err)
			os.Exit(1)
		}
		peers[i] = peerInfo{client: c, name: name}
		fmt.Printf("  Created %s\n", name)
	}

	// Phase 2: Start peers in parallel
	fmt.Println("\n--- Phase 2: Starting peers ---")
	startTime := time.Now()
	var wg sync.WaitGroup
	startErrors := make([]error, *numPeers)

	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), startTimeout)
			defer cancel()
			t := time.Now()
			if err := peers[idx].client.Start(ctx); err != nil {
				startErrors[idx] = err
				return
			}
			fmt.Printf("  %s started in %s\n", peers[idx].name, time.Since(t).Round(time.Millisecond))
		}(i)
	}
	wg.Wait()

	// Check for start errors
	var failed bool
	for i, err := range startErrors {
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error starting %s: %v\n", peers[i].name, err)
			failed = true
		}
	}
	if failed {
		cleanup(peers)
		os.Exit(1)
	}
	fmt.Printf("  All peers started in %s\n", time.Since(startTime).Round(time.Millisecond))

	// Get tunnel IPs
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error getting status for %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		ip := status.LocalPeerState.IP
		// Strip CIDR suffix if present (e.g. "100.64.0.1/16" -> "100.64.0.1")
		if idx := strings.Index(ip, "/"); idx != -1 {
			ip = ip[:idx]
		}
		peers[i].tunnelIP = ip
		fmt.Printf("  %s -> %s\n", peers[i].name, peers[i].tunnelIP)
	}

	// Phase 3: Wait for connections
	fmt.Println("\n--- Phase 3: Waiting for peer connections ---")
	connStart := time.Now()
	expectedPeers := *numPeers - 1
	deadline := time.After(connectTimeout)
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	allConnected := false
waitLoop:
	for {
		select {
		case <-deadline:
			fmt.Fprintf(os.Stderr, "  Timeout waiting for connections after %s\n", connectTimeout)
			printConnectionStatus(peers)
			cleanup(peers)
			os.Exit(1)
		case <-ticker.C:
			allConnected = true
			for i := range peers {
				connected := countConnectedPeers(peers[i].client)
				if connected < expectedPeers {
					allConnected = false
					break
				}
			}
			if allConnected {
				break waitLoop
			}
		}
	}

	fmt.Printf("  All peers connected in %s\n", time.Since(connStart).Round(time.Millisecond))
	printConnectionStatus(peers)

	// Phase 4: Traffic test
	fmt.Printf("\n--- Phase 4: Traffic test (%s) ---\n", *duration)

	// Start echo listeners on all peers
	listeners := make([]net.PacketConn, *numPeers)
	for i := range peers {
		conn, err := peers[i].client.ListenUDP(fmt.Sprintf(":%d", echoPort))
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error creating listener on %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		listeners[i] = conn
		go echoServer(conn, *packetSize)
		fmt.Printf("  Echo listener started on %s:%d\n", peers[i].tunnelIP, echoPort)
	}

	// Run traffic between all pairs (i < j)
	var statsMu sync.Mutex
	var allStats []pairStats

	var trafficWg sync.WaitGroup
	for i := 0; i < *numPeers; i++ {
		for j := i + 1; j < *numPeers; j++ {
			trafficWg.Add(1)
			go func(from, to int) {
				defer trafficWg.Done()
				stats := runTraffic(peers[from].client, peers[from].name, peers[to].tunnelIP, peers[to].name, *duration, *packetSize)
				statsMu.Lock()
				allStats = append(allStats, stats)
				statsMu.Unlock()
			}(i, j)
		}
	}
	trafficWg.Wait()

	// Close listeners
	for _, l := range listeners {
		if l != nil {
			l.Close()
		}
	}

	// Phase 5: Report
	fmt.Println("\n--- Phase 5: Results ---")
	printReport(allStats, *packetSize)

	// Cleanup
	fmt.Println("\n--- Cleanup ---")
	cleanup(peers)
	fmt.Println("Done.")
}

func countConnectedPeers(c *embed.Client) int {
	status, err := c.Status()
	if err != nil {
		return 0
	}
	count := 0
	for _, p := range status.Peers {
		if p.ConnStatus == peer.StatusConnected {
			count++
		}
	}
	return count
}

func printConnectionStatus(peers []peerInfo) {
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Printf("  %s: error getting status: %v\n", peers[i].name, err)
			continue
		}
		connected := 0
		relayed := 0
		for _, p := range status.Peers {
			if p.ConnStatus == peer.StatusConnected {
				connected++
				if p.Relayed {
					relayed++
				}
			}
		}
		connType := "direct"
		if relayed > 0 {
			connType = fmt.Sprintf("%d direct, %d relayed", connected-relayed, relayed)
		}
		fmt.Printf("  %s: %d/%d connected (%s)\n", peers[i].name, connected, len(status.Peers), connType)
	}
}

func echoServer(conn net.PacketConn, maxSize int) {
	buf := make([]byte, maxSize+100)
	for {
		n, addr, err := conn.ReadFrom(buf)
		if err != nil {
			return
		}
		_, _ = conn.WriteTo(buf[:n], addr)
	}
}

func runTraffic(client *embed.Client, fromName, toIP, toName string, duration time.Duration, packetSize int) pairStats {
	stats := pairStats{
		from: fromName,
		to:   toName,
	}

	ctx, cancel := context.WithTimeout(context.Background(), duration+10*time.Second)
	defer cancel()

	conn, err := client.Dial(ctx, "udp", fmt.Sprintf("%s:%d", toIP, echoPort))
	if err != nil {
		fmt.Fprintf(os.Stderr, "  Error dialing %s -> %s: %v\n", fromName, toName, err)
		return stats
	}
	defer conn.Close()

	deadline := time.Now().Add(duration)
	buf := make([]byte, packetSize)
	recvBuf := make([]byte, packetSize+100)
	var seq uint64

	for time.Now().Before(deadline) {
		seq++
		// Encode timestamp and sequence number
		binary.BigEndian.PutUint64(buf[0:8], uint64(time.Now().UnixNano()))
		binary.BigEndian.PutUint64(buf[8:16], seq)

		stats.sent++
		conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
		_, err := conn.Write(buf)
		if err != nil {
			stats.lost++
			continue
		}

		conn.SetReadDeadline(time.Now().Add(5 * time.Second))
		n, err := conn.Read(recvBuf)
		if err != nil {
			stats.lost++
			continue
		}

		if n >= 8 {
			sentNano := binary.BigEndian.Uint64(recvBuf[0:8])
			rtt := time.Since(time.Unix(0, int64(sentNano)))
			stats.received++
			stats.rtts = append(stats.rtts, rtt)
		} else {
			stats.received++
		}

		// Small sleep to avoid flooding
		time.Sleep(10 * time.Millisecond)
	}

	return stats
}

func printReport(allStats []pairStats, packetSize int) {
	if len(allStats) == 0 {
		fmt.Println("  No traffic data collected.")
		return
	}

	fmt.Printf("  %-30s %8s %8s %8s %8s %10s %10s %10s\n",
		"Pair", "Sent", "Recv", "Lost", "Loss%", "Avg RTT", "Min RTT", "Max RTT")
	fmt.Println("  " + strings.Repeat("-", 108))

	var totalSent, totalRecv, totalLost int64
	var totalRTTs []time.Duration

	for _, s := range allStats {
		avg, min, max, loss := s.summary()
		pair := fmt.Sprintf("%s -> %s", s.from, s.to)
		fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
			pair, s.sent, s.received, s.lost, loss,
			avg.Round(time.Microsecond), min.Round(time.Microsecond), max.Round(time.Microsecond))
		totalSent += s.sent
		totalRecv += s.received
		totalLost += s.lost
		totalRTTs = append(totalRTTs, s.rtts...)
	}

	fmt.Println("  " + strings.Repeat("-", 108))

	// Overall summary
	var overallLoss float64
	if totalSent > 0 {
		overallLoss = float64(totalLost) / float64(totalSent) * 100
	}

	var avgRTT, minRTT, maxRTT time.Duration
	if len(totalRTTs) > 0 {
		minRTT = totalRTTs[0]
		maxRTT = totalRTTs[0]
		var total time.Duration
		for _, rtt := range totalRTTs {
			total += rtt
			if rtt < minRTT {
				minRTT = rtt
			}
			if rtt > maxRTT {
				maxRTT = rtt
			}
		}
		avgRTT = total / time.Duration(len(totalRTTs))
	}

	fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
		"TOTAL", totalSent, totalRecv, totalLost, overallLoss,
		avgRTT.Round(time.Microsecond), minRTT.Round(time.Microsecond), maxRTT.Round(time.Microsecond))

	// Throughput
	if len(totalRTTs) > 0 {
		fmt.Println()
		totalBytes := totalRecv * int64(packetSize)
		// Calculate stddev
		var sumSq float64
		avgNs := float64(avgRTT.Nanoseconds())
		for _, rtt := range totalRTTs {
			diff := float64(rtt.Nanoseconds()) - avgNs
			sumSq += diff * diff
		}
		stddev := time.Duration(math.Sqrt(sumSq / float64(len(totalRTTs))))

		fmt.Printf("  Total data transferred: %.2f KB\n", float64(totalBytes)/1024)
		fmt.Printf("  RTT stddev:             %s\n", stddev.Round(time.Microsecond))
		fmt.Printf("  Pairs tested:           %d\n", len(allStats))
	}
}

func cleanup(peers []peerInfo) {
	ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
	defer cancel()

	var wg sync.WaitGroup
	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			if err := peers[idx].client.Stop(ctx); err != nil {
				fmt.Fprintf(os.Stderr, "  Error stopping %s: %v\n", peers[idx].name, err)
			} else {
				fmt.Printf("  Stopped %s\n", peers[idx].name)
			}
		}(i)
	}
	wg.Wait()
}


Now let's build it to verify compilation.


Bash

go build -o /dev/null ./combined/perftest/ 2>&1

Build perftest to verify compilation


Error: Exit code 1
package github.com/netbirdio/netbird/combined/perftest
	combined/perftest/main.go:16:2: use of internal package github.com/netbirdio/netbird/client/internal/peer not allowed


The `client/internal/peer` package is internal, so we can't import it from `combined/perftest`. However, the `embed` package re-exports `PeerStatusConnected` and `PeerConnStatus`. Let me use those instead.

Looking at embed.go:
```go
type PeerConnStatus = peer.ConnStatus

const (
    PeerStatusConnected = peer.StatusConnected
)
```

So I can use `embed.PeerStatusConnected` and `embed.PeerConnStatus` instead. But the `State` type is from `peer.State`... I need to check if `FullStatus` is also re-exported by embed.

Actually, I realize that in Go you can receive values of internal types from exported functions even if you can't import the package directly—the caller just can't name the type explicitly. So `Status()` returning `peer.FullStatus` is fine; I can work with the returned value and access its fields like `ConnStatus` without needing to import `peer`. you just can't import the internal package directly. But you CAN use the value. The fields are accessible.

Wait, actually that's not entirely correct. Let me think more carefully...

The `embed` package is at `github.com/netbirdio/netbird/client/embed`. The `perftest` is at `github.com/netbirdio/netbird/combined/perftest`. The `internal` package is at `github.com/netbirdio/netbird/client/internal/peer`.

Go's internal package rule: a package can only be imported by code rooted at the parent of the `internal` directory. So `client/internal/peer` can only be imported by packages rooted at `client/`. The `combined/perftest` package is not rooted at `client/`, so it can't import `client/internal/peer`.

However, I can receive values of type `peer.FullStatus` from `embed.Status()` and access the struct fields directly, even though I can't name the type. Since `embed` re-exports the constants like `PeerStatusConnected` and `PeerConnStatus`, I should use those instead of trying to reference the internal `peer` package directly.


The `client/internal/peer` package is internal and can't be imported from `combined/`. The `embed` package re-exports the constants we need. Let me fix this.


Edit

	"github.com/netbirdio/netbird/client/embed"
	"github.com/netbirdio/netbird/client/internal/peer"
)


The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/perftest/main.go has been updated successfully.

package main

import (
	"context"
	"encoding/binary"
	"flag"
	"fmt"
	"math"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/netbirdio/netbird/client/embed"
	"github.com/netbirdio/netbird/client/internal/peer"
)

const (
	echoPort       = 9000
	connectTimeout = 120 * time.Second
	startTimeout   = 60 * time.Second
	stopTimeout    = 30 * time.Second
)

type peerInfo struct {
	client   *embed.Client
	tunnelIP string
	name     string
}

type pairStats struct {
	from     string
	to       string
	sent     int64
	received int64
	lost     int64
	rtts     []time.Duration
}

func (s *pairStats) summary() (avgRTT, minRTT, maxRTT time.Duration, lossPercent float64) {
	if len(s.rtts) == 0 {
		return 0, 0, 0, 100
	}
	minRTT = s.rtts[0]
	maxRTT = s.rtts[0]
	var total time.Duration
	for _, rtt := range s.rtts {
		total += rtt
		if rtt < minRTT {
			minRTT = rtt
		}
		if rtt > maxRTT {
			maxRTT = rtt
		}
	}
	avgRTT = total / time.Duration(len(s.rtts))
	if s.sent > 0 {
		lossPercent = float64(s.lost) / float64(s.sent) * 100
	}
	return
}

func main() {
	managementURL := flag.String("management-url", "", "Management server URL (required)")
	setupKey := flag.String("setup-key", "", "Reusable setup key (required)")
	numPeers := flag.Int("peers", 5, "Number of peers to spawn")
	forceRelay := flag.Bool("force-relay", false, "Force relay connections (NB_FORCE_RELAY=true)")
	duration := flag.Duration("duration", 30*time.Second, "Traffic test duration")
	packetSize := flag.Int("packet-size", 512, "UDP packet size in bytes")
	logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")
	flag.Parse()

	if *managementURL == "" || *setupKey == "" {
		fmt.Fprintln(os.Stderr, "Error: --management-url and --setup-key are required")
		flag.Usage()
		os.Exit(1)
	}

	if *numPeers < 2 {
		fmt.Fprintln(os.Stderr, "Error: --peers must be at least 2")
		os.Exit(1)
	}

	// Minimum packet size: 8 bytes for timestamp + 8 bytes for sequence number
	if *packetSize < 16 {
		fmt.Fprintln(os.Stderr, "Error: --packet-size must be at least 16")
		os.Exit(1)
	}

	if *forceRelay {
		os.Setenv("NB_FORCE_RELAY", "true")
	}
	os.Setenv("NB_USE_NETSTACK_MODE", "true")

	fmt.Println("=== NetBird Performance Test ===")
	fmt.Printf("Management URL: %s\n", *managementURL)
	fmt.Printf("Peers:          %d\n", *numPeers)
	fmt.Printf("Force relay:    %v\n", *forceRelay)
	fmt.Printf("Duration:       %s\n", *duration)
	fmt.Printf("Packet size:    %d bytes\n", *packetSize)
	fmt.Println()

	// Phase 1: Create peers
	fmt.Println("--- Phase 1: Creating peers ---")
	peers := make([]peerInfo, *numPeers)
	for i := 0; i < *numPeers; i++ {
		name := fmt.Sprintf("perf-peer-%d", i)
		port := 0
		c, err := embed.New(embed.Options{
			DeviceName:    name,
			SetupKey:      *setupKey,
			ManagementURL: *managementURL,
			WireguardPort: &port,
			LogLevel:      *logLevel,
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error creating peer %s: %v\n", name, err)
			os.Exit(1)
		}
		peers[i] = peerInfo{client: c, name: name}
		fmt.Printf("  Created %s\n", name)
	}

	// Phase 2: Start peers in parallel
	fmt.Println("\n--- Phase 2: Starting peers ---")
	startTime := time.Now()
	var wg sync.WaitGroup
	startErrors := make([]error, *numPeers)

	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), startTimeout)
			defer cancel()
			t := time.Now()
			if err := peers[idx].client.Start(ctx); err != nil {
				startErrors[idx] = err
				return
			}
			fmt.Printf("  %s started in %s\n", peers[idx].name, time.Since(t).Round(time.Millisecond))
		}(i)
	}
	wg.Wait()

	// Check for start errors
	var failed bool
	for i, err := range startErrors {
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error starting %s: %v\n", peers[i].name, err)
			failed = true
		}
	}
	if failed {
		cleanup(peers)
		os.Exit(1)
	}
	fmt.Printf("  All peers started in %s\n", time.Since(startTime).Round(time.Millisecond))

	// Get tunnel IPs
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error getting status for %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		ip := status.LocalPeerState.IP
		// Strip CIDR suffix if present (e.g. "100.64.0.1/16" -> "100.64.0.1")
		if idx := strings.Index(ip, "/"); idx != -1 {
			ip = ip[:idx]
		}
		peers[i].tunnelIP = ip
		fmt.Printf("  %s -> %s\n", peers[i].name, peers[i].tunnelIP)
	}

	// Phase 3: Wait for connections
	fmt.Println("\n--- Phase 3: Waiting for peer connections ---")
	connStart := time.Now()
	expectedPeers := *numPeers - 1
	deadline := time.After(connectTimeout)
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	allConnected := false
waitLoop:
	for {
		select {
		case <-deadline:
			fmt.Fprintf(os.Stderr, "  Timeout waiting for connections after %s\n", connectTimeout)
			printConnectionStatus(peers)
			cleanup(peers)
			os.Exit(1)
		case <-ticker.C:
			allConnected = true
			for i := range peers {
				connected := countConnectedPeers(peers[i].client)
				if connected < expectedPeers {
					allConnected = false
					break
				}
			}
			if allConnected {
				break waitLoop
			}
		}
	}

	fmt.Printf("  All peers connected in %s\n", time.Since(connStart).Round(time.Millisecond))
	printConnectionStatus(peers)

	// Phase 4: Traffic test
	fmt.Printf("\n--- Phase 4: Traffic test (%s) ---\n", *duration)

	// Start echo listeners on all peers
	listeners := make([]net.PacketConn, *numPeers)
	for i := range peers {
		conn, err := peers[i].client.ListenUDP(fmt.Sprintf(":%d", echoPort))
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error creating listener on %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		listeners[i] = conn
		go echoServer(conn, *packetSize)
		fmt.Printf("  Echo listener started on %s:%d\n", peers[i].tunnelIP, echoPort)
	}

	// Run traffic between all pairs (i < j)
	var statsMu sync.Mutex
	var allStats []pairStats

	var trafficWg sync.WaitGroup
	for i := 0; i < *numPeers; i++ {
		for j := i + 1; j < *numPeers; j++ {
			trafficWg.Add(1)
			go func(from, to int) {
				defer trafficWg.Done()
				stats := runTraffic(peers[from].client, peers[from].name, peers[to].tunnelIP, peers[to].name, *duration, *packetSize)
				statsMu.Lock()
				allStats = append(allStats, stats)
				statsMu.Unlock()
			}(i, j)
		}
	}
	trafficWg.Wait()

	// Close listeners
	for _, l := range listeners {
		if l != nil {
			l.Close()
		}
	}

	// Phase 5: Report
	fmt.Println("\n--- Phase 5: Results ---")
	printReport(allStats, *packetSize)

	// Cleanup
	fmt.Println("\n--- Cleanup ---")
	cleanup(peers)
	fmt.Println("Done.")
}

func countConnectedPeers(c *embed.Client) int {
	status, err := c.Status()
	if err != nil {
		return 0
	}
	count := 0
	for _, p := range status.Peers {
		if p.ConnStatus == peer.StatusConnected {
			count++
		}
	}
	return count
}

func printConnectionStatus(peers []peerInfo) {
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Printf("  %s: error getting status: %v\n", peers[i].name, err)
			continue
		}
		connected := 0
		relayed := 0
		for _, p := range status.Peers {
			if p.ConnStatus == peer.StatusConnected {
				connected++
				if p.Relayed {
					relayed++
				}
			}
		}
		connType := "direct"
		if relayed > 0 {
			connType = fmt.Sprintf("%d direct, %d relayed", connected-relayed, relayed)
		}
		fmt.Printf("  %s: %d/%d connected (%s)\n", peers[i].name, connected, len(status.Peers), connType)
	}
}

func echoServer(conn net.PacketConn, maxSize int) {
	buf := make([]byte, maxSize+100)
	for {
		n, addr, err := conn.ReadFrom(buf)
		if err != nil {
			return
		}
		_, _ = conn.WriteTo(buf[:n], addr)
	}
}

func runTraffic(client *embed.Client, fromName, toIP, toName string, duration time.Duration, packetSize int) pairStats {
	stats := pairStats{
		from: fromName,
		to:   toName,
	}

	ctx, cancel := context.WithTimeout(context.Background(), duration+10*time.Second)
	defer cancel()

	conn, err := client.Dial(ctx, "udp", fmt.Sprintf("%s:%d", toIP, echoPort))
	if err != nil {
		fmt.Fprintf(os.Stderr, "  Error dialing %s -> %s: %v\n", fromName, toName, err)
		return stats
	}
	defer conn.Close()

	deadline := time.Now().Add(duration)
	buf := make([]byte, packetSize)
	recvBuf := make([]byte, packetSize+100)
	var seq uint64

	for time.Now().Before(deadline) {
		seq++
		// Encode timestamp and sequence number
		binary.BigEndian.PutUint64(buf[0:8], uint64(time.Now().UnixNano()))
		binary.BigEndian.PutUint64(buf[8:16], seq)

		stats.sent++
		conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
		_, err := conn.Write(buf)
		if err != nil {
			stats.lost++
			continue
		}

		conn.SetReadDeadline(time.Now().Add(5 * time.Second))
		n, err := conn.Read(recvBuf)
		if err != nil {
			stats.lost++
			continue
		}

		if n >= 8 {
			sentNano := binary.BigEndian.Uint64(recvBuf[0:8])
			rtt := time.Since(time.Unix(0, int64(sentNano)))
			stats.received++
			stats.rtts = append(stats.rtts, rtt)
		} else {
			stats.received++
		}

		// Small sleep to avoid flooding
		time.Sleep(10 * time.Millisecond)
	}

	return stats
}

func printReport(allStats []pairStats, packetSize int) {
	if len(allStats) == 0 {
		fmt.Println("  No traffic data collected.")
		return
	}

	fmt.Printf("  %-30s %8s %8s %8s %8s %10s %10s %10s\n",
		"Pair", "Sent", "Recv", "Lost", "Loss%", "Avg RTT", "Min RTT", "Max RTT")
	fmt.Println("  " + strings.Repeat("-", 108))

	var totalSent, totalRecv, totalLost int64
	var totalRTTs []time.Duration

	for _, s := range allStats {
		avg, min, max, loss := s.summary()
		pair := fmt.Sprintf("%s -> %s", s.from, s.to)
		fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
			pair, s.sent, s.received, s.lost, loss,
			avg.Round(time.Microsecond), min.Round(time.Microsecond), max.Round(time.Microsecond))
		totalSent += s.sent
		totalRecv += s.received
		totalLost += s.lost
		totalRTTs = append(totalRTTs, s.rtts...)
	}

	fmt.Println("  " + strings.Repeat("-", 108))

	// Overall summary
	var overallLoss float64
	if totalSent > 0 {
		overallLoss = float64(totalLost) / float64(totalSent) * 100
	}

	var avgRTT, minRTT, maxRTT time.Duration
	if len(totalRTTs) > 0 {
		minRTT = totalRTTs[0]
		maxRTT = totalRTTs[0]
		var total time.Duration
		for _, rtt := range totalRTTs {
			total += rtt
			if rtt < minRTT {
				minRTT = rtt
			}
			if rtt > maxRTT {
				maxRTT = rtt
			}
		}
		avgRTT = total / time.Duration(len(totalRTTs))
	}

	fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
		"TOTAL", totalSent, totalRecv, totalLost, overallLoss,
		avgRTT.Round(time.Microsecond), minRTT.Round(time.Microsecond), maxRTT.Round(time.Microsecond))

	// Throughput
	if len(totalRTTs) > 0 {
		fmt.Println()
		totalBytes := totalRecv * int64(packetSize)
		// Calculate stddev
		var sumSq float64
		avgNs := float64(avgRTT.Nanoseconds())
		for _, rtt := range totalRTTs {
			diff := float64(rtt.Nanoseconds()) - avgNs
			sumSq += diff * diff
		}
		stddev := time.Duration(math.Sqrt(sumSq / float64(len(totalRTTs))))

		fmt.Printf("  Total data transferred: %.2f KB\n", float64(totalBytes)/1024)
		fmt.Printf("  RTT stddev:             %s\n", stddev.Round(time.Microsecond))
		fmt.Printf("  Pairs tested:           %d\n", len(allStats))
	}
}

func cleanup(peers []peerInfo) {
	ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
	defer cancel()

	var wg sync.WaitGroup
	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			if err := peers[idx].client.Stop(ctx); err != nil {
				fmt.Fprintf(os.Stderr, "  Error stopping %s: %v\n", peers[idx].name, err)
			} else {
				fmt.Printf("  Stopped %s\n", peers[idx].name)
			}
		}(i)
	}
	wg.Wait()
}


if p.ConnStatus == peer.StatusConnected {

if p.ConnStatus == embed.PeerStatusConnected {


The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/perftest/main.go has been updated. All occurrences of 'if p.ConnStatus == peer.StatusConnected {' were successfully replaced with 'if p.ConnStatus == embed.PeerStatusConnected {'.

package main

import (
	"context"
	"encoding/binary"
	"flag"
	"fmt"
	"math"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/netbirdio/netbird/client/embed"
)

const (
	echoPort       = 9000
	connectTimeout = 120 * time.Second
	startTimeout   = 60 * time.Second
	stopTimeout    = 30 * time.Second
)

type peerInfo struct {
	client   *embed.Client
	tunnelIP string
	name     string
}

type pairStats struct {
	from     string
	to       string
	sent     int64
	received int64
	lost     int64
	rtts     []time.Duration
}

func (s *pairStats) summary() (avgRTT, minRTT, maxRTT time.Duration, lossPercent float64) {
	if len(s.rtts) == 0 {
		return 0, 0, 0, 100
	}
	minRTT = s.rtts[0]
	maxRTT = s.rtts[0]
	var total time.Duration
	for _, rtt := range s.rtts {
		total += rtt
		if rtt < minRTT {
			minRTT = rtt
		}
		if rtt > maxRTT {
			maxRTT = rtt
		}
	}
	avgRTT = total / time.Duration(len(s.rtts))
	if s.sent > 0 {
		lossPercent = float64(s.lost) / float64(s.sent) * 100
	}
	return
}

func main() {
	managementURL := flag.String("management-url", "", "Management server URL (required)")
	setupKey := flag.String("setup-key", "", "Reusable setup key (required)")
	numPeers := flag.Int("peers", 5, "Number of peers to spawn")
	forceRelay := flag.Bool("force-relay", false, "Force relay connections (NB_FORCE_RELAY=true)")
	duration := flag.Duration("duration", 30*time.Second, "Traffic test duration")
	packetSize := flag.Int("packet-size", 512, "UDP packet size in bytes")
	logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")
	flag.Parse()

	if *managementURL == "" || *setupKey == "" {
		fmt.Fprintln(os.Stderr, "Error: --management-url and --setup-key are required")
		flag.Usage()
		os.Exit(1)
	}

	if *numPeers < 2 {
		fmt.Fprintln(os.Stderr, "Error: --peers must be at least 2")
		os.Exit(1)
	}

	// Minimum packet size: 8 bytes for timestamp + 8 bytes for sequence number
	if *packetSize < 16 {
		fmt.Fprintln(os.Stderr, "Error: --packet-size must be at least 16")
		os.Exit(1)
	}

	if *forceRelay {
		os.Setenv("NB_FORCE_RELAY", "true")
	}
	os.Setenv("NB_USE_NETSTACK_MODE", "true")

	fmt.Println("=== NetBird Performance Test ===")
	fmt.Printf("Management URL: %s\n", *managementURL)
	fmt.Printf("Peers:          %d\n", *numPeers)
	fmt.Printf("Force relay:    %v\n", *forceRelay)
	fmt.Printf("Duration:       %s\n", *duration)
	fmt.Printf("Packet size:    %d bytes\n", *packetSize)
	fmt.Println()

	// Phase 1: Create peers
	fmt.Println("--- Phase 1: Creating peers ---")
	peers := make([]peerInfo, *numPeers)
	for i := 0; i < *numPeers; i++ {
		name := fmt.Sprintf("perf-peer-%d", i)
		port := 0
		c, err := embed.New(embed.Options{
			DeviceName:    name,
			SetupKey:      *setupKey,
			ManagementURL: *managementURL,
			WireguardPort: &port,
			LogLevel:      *logLevel,
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error creating peer %s: %v\n", name, err)
			os.Exit(1)
		}
		peers[i] = peerInfo{client: c, name: name}
		fmt.Printf("  Created %s\n", name)
	}

	// Phase 2: Start peers in parallel
	fmt.Println("\n--- Phase 2: Starting peers ---")
	startTime := time.Now()
	var wg sync.WaitGroup
	startErrors := make([]error, *numPeers)

	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), startTimeout)
			defer cancel()
			t := time.Now()
			if err := peers[idx].client.Start(ctx); err != nil {
				startErrors[idx] = err
				return
			}
			fmt.Printf("  %s started in %s\n", peers[idx].name, time.Since(t).Round(time.Millisecond))
		}(i)
	}
	wg.Wait()

	// Check for start errors
	var failed bool
	for i, err := range startErrors {
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error starting %s: %v\n", peers[i].name, err)
			failed = true
		}
	}
	if failed {
		cleanup(peers)
		os.Exit(1)
	}
	fmt.Printf("  All peers started in %s\n", time.Since(startTime).Round(time.Millisecond))

	// Get tunnel IPs
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error getting status for %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		ip := status.LocalPeerState.IP
		// Strip CIDR suffix if present (e.g. "100.64.0.1/16" -> "100.64.0.1")
		if idx := strings.Index(ip, "/"); idx != -1 {
			ip = ip[:idx]
		}
		peers[i].tunnelIP = ip
		fmt.Printf("  %s -> %s\n", peers[i].name, peers[i].tunnelIP)
	}

	// Phase 3: Wait for connections
	fmt.Println("\n--- Phase 3: Waiting for peer connections ---")
	connStart := time.Now()
	expectedPeers := *numPeers - 1
	deadline := time.After(connectTimeout)
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	allConnected := false
waitLoop:
	for {
		select {
		case <-deadline:
			fmt.Fprintf(os.Stderr, "  Timeout waiting for connections after %s\n", connectTimeout)
			printConnectionStatus(peers)
			cleanup(peers)
			os.Exit(1)
		case <-ticker.C:
			allConnected = true
			for i := range peers {
				connected := countConnectedPeers(peers[i].client)
				if connected < expectedPeers {
					allConnected = false
					break
				}
			}
			if allConnected {
				break waitLoop
			}
		}
	}

	fmt.Printf("  All peers connected in %s\n", time.Since(connStart).Round(time.Millisecond))
	printConnectionStatus(peers)

	// Phase 4: Traffic test
	fmt.Printf("\n--- Phase 4: Traffic test (%s) ---\n", *duration)

	// Start echo listeners on all peers
	listeners := make([]net.PacketConn, *numPeers)
	for i := range peers {
		conn, err := peers[i].client.ListenUDP(fmt.Sprintf(":%d", echoPort))
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error creating listener on %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		listeners[i] = conn
		go echoServer(conn, *packetSize)
		fmt.Printf("  Echo listener started on %s:%d\n", peers[i].tunnelIP, echoPort)
	}

	// Run traffic between all pairs (i < j)
	var statsMu sync.Mutex
	var allStats []pairStats

	var trafficWg sync.WaitGroup
	for i := 0; i < *numPeers; i++ {
		for j := i + 1; j < *numPeers; j++ {
			trafficWg.Add(1)
			go func(from, to int) {
				defer trafficWg.Done()
				stats := runTraffic(peers[from].client, peers[from].name, peers[to].tunnelIP, peers[to].name, *duration, *packetSize)
				statsMu.Lock()
				allStats = append(allStats, stats)
				statsMu.Unlock()
			}(i, j)
		}
	}
	trafficWg.Wait()

	// Close listeners
	for _, l := range listeners {
		if l != nil {
			l.Close()
		}
	}

	// Phase 5: Report
	fmt.Println("\n--- Phase 5: Results ---")
	printReport(allStats, *packetSize)

	// Cleanup
	fmt.Println("\n--- Cleanup ---")
	cleanup(peers)
	fmt.Println("Done.")
}

func countConnectedPeers(c *embed.Client) int {
	status, err := c.Status()
	if err != nil {
		return 0
	}
	count := 0
	for _, p := range status.Peers {
		if p.ConnStatus == peer.StatusConnected {
			count++
		}
	}
	return count
}

func printConnectionStatus(peers []peerInfo) {
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Printf("  %s: error getting status: %v\n", peers[i].name, err)
			continue
		}
		connected := 0
		relayed := 0
		for _, p := range status.Peers {
			if p.ConnStatus == peer.StatusConnected {
				connected++
				if p.Relayed {
					relayed++
				}
			}
		}
		connType := "direct"
		if relayed > 0 {
			connType = fmt.Sprintf("%d direct, %d relayed", connected-relayed, relayed)
		}
		fmt.Printf("  %s: %d/%d connected (%s)\n", peers[i].name, connected, len(status.Peers), connType)
	}
}

func echoServer(conn net.PacketConn, maxSize int) {
	buf := make([]byte, maxSize+100)
	for {
		n, addr, err := conn.ReadFrom(buf)
		if err != nil {
			return
		}
		_, _ = conn.WriteTo(buf[:n], addr)
	}
}

func runTraffic(client *embed.Client, fromName, toIP, toName string, duration time.Duration, packetSize int) pairStats {
	stats := pairStats{
		from: fromName,
		to:   toName,
	}

	ctx, cancel := context.WithTimeout(context.Background(), duration+10*time.Second)
	defer cancel()

	conn, err := client.Dial(ctx, "udp", fmt.Sprintf("%s:%d", toIP, echoPort))
	if err != nil {
		fmt.Fprintf(os.Stderr, "  Error dialing %s -> %s: %v\n", fromName, toName, err)
		return stats
	}
	defer conn.Close()

	deadline := time.Now().Add(duration)
	buf := make([]byte, packetSize)
	recvBuf := make([]byte, packetSize+100)
	var seq uint64

	for time.Now().Before(deadline) {
		seq++
		// Encode timestamp and sequence number
		binary.BigEndian.PutUint64(buf[0:8], uint64(time.Now().UnixNano()))
		binary.BigEndian.PutUint64(buf[8:16], seq)

		stats.sent++
		conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
		_, err := conn.Write(buf)
		if err != nil {
			stats.lost++
			continue
		}

		conn.SetReadDeadline(time.Now().Add(5 * time.Second))
		n, err := conn.Read(recvBuf)
		if err != nil {
			stats.lost++
			continue
		}

		if n >= 8 {
			sentNano := binary.BigEndian.Uint64(recvBuf[0:8])
			rtt := time.Since(time.Unix(0, int64(sentNano)))
			stats.received++
			stats.rtts = append(stats.rtts, rtt)
		} else {
			stats.received++
		}

		// Small sleep to avoid flooding
		time.Sleep(10 * time.Millisecond)
	}

	return stats
}

func printReport(allStats []pairStats, packetSize int) {
	if len(allStats) == 0 {
		fmt.Println("  No traffic data collected.")
		return
	}

	fmt.Printf("  %-30s %8s %8s %8s %8s %10s %10s %10s\n",
		"Pair", "Sent", "Recv", "Lost", "Loss%", "Avg RTT", "Min RTT", "Max RTT")
	fmt.Println("  " + strings.Repeat("-", 108))

	var totalSent, totalRecv, totalLost int64
	var totalRTTs []time.Duration

	for _, s := range allStats {
		avg, min, max, loss := s.summary()
		pair := fmt.Sprintf("%s -> %s", s.from, s.to)
		fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
			pair, s.sent, s.received, s.lost, loss,
			avg.Round(time.Microsecond), min.Round(time.Microsecond), max.Round(time.Microsecond))
		totalSent += s.sent
		totalRecv += s.received
		totalLost += s.lost
		totalRTTs = append(totalRTTs, s.rtts...)
	}

	fmt.Println("  " + strings.Repeat("-", 108))

	// Overall summary
	var overallLoss float64
	if totalSent > 0 {
		overallLoss = float64(totalLost) / float64(totalSent) * 100
	}

	var avgRTT, minRTT, maxRTT time.Duration
	if len(totalRTTs) > 0 {
		minRTT = totalRTTs[0]
		maxRTT = totalRTTs[0]
		var total time.Duration
		for _, rtt := range totalRTTs {
			total += rtt
			if rtt < minRTT {
				minRTT = rtt
			}
			if rtt > maxRTT {
				maxRTT = rtt
			}
		}
		avgRTT = total / time.Duration(len(totalRTTs))
	}

	fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
		"TOTAL", totalSent, totalRecv, totalLost, overallLoss,
		avgRTT.Round(time.Microsecond), minRTT.Round(time.Microsecond), maxRTT.Round(time.Microsecond))

	// Throughput
	if len(totalRTTs) > 0 {
		fmt.Println()
		totalBytes := totalRecv * int64(packetSize)
		// Calculate stddev
		var sumSq float64
		avgNs := float64(avgRTT.Nanoseconds())
		for _, rtt := range totalRTTs {
			diff := float64(rtt.Nanoseconds()) - avgNs
			sumSq += diff * diff
		}
		stddev := time.Duration(math.Sqrt(sumSq / float64(len(totalRTTs))))

		fmt.Printf("  Total data transferred: %.2f KB\n", float64(totalBytes)/1024)
		fmt.Printf("  RTT stddev:             %s\n", stddev.Round(time.Microsecond))
		fmt.Printf("  Pairs tested:           %d\n", len(allStats))
	}
}

func cleanup(peers []peerInfo) {
	ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
	defer cancel()

	var wg sync.WaitGroup
	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			if err := peers[idx].client.Stop(ctx); err != nil {
				fmt.Fprintf(os.Stderr, "  Error stopping %s: %v\n", peers[idx].name, err)
			} else {
				fmt.Printf("  Stopped %s\n", peers[idx].name)
			}
		}(i)
	}
	wg.Wait()
}

-    if p.ConnStatus == peer.StatusConnected {

+    if p.ConnStatus == embed.PeerStatusConnected {


Bash

go build -o /dev/null ./combined/perftest/ 2>&1

Build perftest after fixing internal import


Builds successfully. Let me also quickly verify the final file looks correct.


     1→package main
     2→
     3→import (
     4→	"context"
     5→	"encoding/binary"
     6→	"flag"
     7→	"fmt"
     8→	"math"
     9→	"net"
    10→	"os"
    11→	"strings"
    12→	"sync"
    13→	"time"
    14→
    15→	"github.com/netbirdio/netbird/client/embed"
    16→)
    17→
    18→const (
    19→	echoPort       = 9000
    20→	connectTimeout = 120 * time.Second
    21→	startTimeout   = 60 * time.Second
    22→	stopTimeout    = 30 * time.Second
    23→)
    24→
    25→type peerInfo struct {
    26→	client   *embed.Client
    27→	tunnelIP string
    28→	name     string
    29→}
    30→
    31→type pairStats struct {
    32→	from     string
    33→	to       string
    34→	sent     int64
    35→	received int64
    36→	lost     int64
    37→	rtts     []time.Duration
    38→}
    39→
    40→func (s *pairStats) summary() (avgRTT, minRTT, maxRTT time.Duration, lossPercent float64) {
    41→	if len(s.rtts) == 0 {
    42→		return 0, 0, 0, 100
    43→	}
    44→	minRTT = s.rtts[0]
    45→	maxRTT = s.rtts[0]
    46→	var total time.Duration
    47→	for _, rtt := range s.rtts {
    48→		total += rtt
    49→		if rtt < minRTT {
    50→			minRTT = rtt
    51→		}
    52→		if rtt > maxRTT {
    53→			maxRTT = rtt
    54→		}
    55→	}
    56→	avgRTT = total / time.Duration(len(s.rtts))
    57→	if s.sent > 0 {
    58→		lossPercent = float64(s.lost) / float64(s.sent) * 100
    59→	}
    60→	return
    61→}
    62→
    63→func main() {
    64→	managementURL := flag.String("management-url", "", "Management server URL (required)")
    65→	setupKey := flag.String("setup-key", "", "Reusable setup key (required)")
    66→	numPeers := flag.Int("peers", 5, "Number of peers to spawn")
    67→	forceRelay := flag.Bool("force-relay", false, "Force relay connections (NB_FORCE_RELAY=true)")
    68→	duration := flag.Duration("duration", 30*time.Second, "Traffic test duration")
    69→	packetSize := flag.Int("packet-size", 512, "UDP packet size in bytes")
    70→	logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")
    71→	flag.Parse()
    72→
    73→	if *managementURL == "" || *setupKey == "" {
    74→		fmt.Fprintln(os.Stderr, "Error: --management-url and --setup-key are required")
    75→		flag.Usage()
    76→		os.Exit(1)
    77→	}
    78→
    79→	if *numPeers < 2 {
    80→		fmt.Fprintln(os.Stderr, "Error: --peers must be at least 2")
    81→		os.Exit(1)
    82→	}
    83→
    84→	// Minimum packet size: 8 bytes for timestamp + 8 bytes for sequence number
    85→	if *packetSize < 16 {
    86→		fmt.Fprintln(os.Stderr, "Error: --packet-size must be at least 16")
    87→		os.Exit(1)
    88→	}
    89→
    90→	if *forceRelay {
    91→		os.Setenv("NB_FORCE_RELAY", "true")
    92→	}
    93→	os.Setenv("NB_USE_NETSTACK_MODE", "true")
    94→
    95→	fmt.Println("=== NetBird Performance Test ===")
    96→	fmt.Printf("Management URL: %s\n", *managementURL)
    97→	fmt.Printf("Peers:          %d\n", *numPeers)
    98→	fmt.Printf("Force relay:    %v\n", *forceRelay)
    99→	fmt.Printf("Duration:       %s\n", *duration)
   100→	fmt.Printf("Packet size:    %d bytes\n", *packetSize)
   101→	fmt.Println()
   102→
   103→	// Phase 1: Create peers
   104→	fmt.Println("--- Phase 1: Creating peers ---")
   105→	peers := make([]peerInfo, *numPeers)
   106→	for i := 0; i < *numPeers; i++ {
   107→		name := fmt.Sprintf("perf-peer-%d", i)
   108→		port := 0
   109→		c, err := embed.New(embed.Options{
   110→			DeviceName:    name,
   111→			SetupKey:      *setupKey,
   112→			ManagementURL: *managementURL,
   113→			WireguardPort: &port,
   114→			LogLevel:      *logLevel,
   115→		})
   116→		if err != nil {
   117→			fmt.Fprintf(os.Stderr, "Error creating peer %s: %v\n", name, err)
   118→			os.Exit(1)
   119→		}
   120→		peers[i] = peerInfo{client: c, name: name}
   121→		fmt.Printf("  Created %s\n", name)
   122→	}
   123→
   124→	// Phase 2: Start peers in parallel
   125→	fmt.Println("\n--- Phase 2: Starting peers ---")
   126→	startTime := time.Now()
   127→	var wg sync.WaitGroup
   128→	startErrors := make([]error, *numPeers)
   129→
   130→	for i := range peers {
   131→		wg.Add(1)
   132→		go func(idx int) {
   133→			defer wg.Done()
   134→			ctx, cancel := context.WithTimeout(context.Background(), startTimeout)
   135→			defer cancel()
   136→			t := time.Now()
   137→			if err := peers[idx].client.Start(ctx); err != nil {
   138→				startErrors[idx] = err
   139→				return
   140→			}
   141→			fmt.Printf("  %s started in %s\n", peers[idx].name, time.Since(t).Round(time.Millisecond))
   142→		}(i)
   143→	}
   144→	wg.Wait()
   145→
   146→	// Check for start errors
   147→	var failed bool
   148→	for i, err := range startErrors {
   149→		if err != nil {
   150→			fmt.Fprintf(os.Stderr, "  Error starting %s: %v\n", peers[i].name, err)
   151→			failed = true
   152→		}
   153→	}
   154→	if failed {
   155→		cleanup(peers)
   156→		os.Exit(1)
   157→	}
   158→	fmt.Printf("  All peers started in %s\n", time.Since(startTime).Round(time.Millisecond))
   159→
   160→	// Get tunnel IPs
   161→	for i := range peers {
   162→		status, err := peers[i].client.Status()
   163→		if err != nil {
   164→			fmt.Fprintf(os.Stderr, "  Error getting status for %s: %v\n", peers[i].name, err)
   165→			cleanup(peers)
   166→			os.Exit(1)
   167→		}
   168→		ip := status.LocalPeerState.IP
   169→		// Strip CIDR suffix if present (e.g. "100.64.0.1/16" -> "100.64.0.1")
   170→		if idx := strings.Index(ip, "/"); idx != -1 {
   171→			ip = ip[:idx]
   172→		}
   173→		peers[i].tunnelIP = ip
   174→		fmt.Printf("  %s -> %s\n", peers[i].name, peers[i].tunnelIP)
   175→	}
   176→
   177→	// Phase 3: Wait for connections
   178→	fmt.Println("\n--- Phase 3: Waiting for peer connections ---")
   179→	connStart := time.Now()
   180→	expectedPeers := *numPeers - 1
   181→	deadline := time.After(connectTimeout)
   182→	ticker := time.NewTicker(2 * time.Second)
   183→	defer ticker.Stop()
   184→
   185→	allConnected := false
   186→waitLoop:
   187→	for {
   188→		select {
   189→		case <-deadline:
   190→			fmt.Fprintf(os.Stderr, "  Timeout waiting for connections after %s\n", connectTimeout)
   191→			printConnectionStatus(peers)
   192→			cleanup(peers)
   193→			os.Exit(1)
   194→		case <-ticker.C:
   195→			allConnected = true
   196→			for i := range peers {
   197→				connected := countConnectedPeers(peers[i].client)
   198→				if connected < expectedPeers {
   199→					allConnected = false
   200→					break
   201→				}
   202→			}
   203→			if allConnected {
   204→				break waitLoop
   205→			}
   206→		}
   207→	}
   208→
   209→	fmt.Printf("  All peers connected in %s\n", time.Since(connStart).Round(time.Millisecond))
   210→	printConnectionStatus(peers)
   211→
   212→	// Phase 4: Traffic test
   213→	fmt.Printf("\n--- Phase 4: Traffic test (%s) ---\n", *duration)
   214→
   215→	// Start echo listeners on all peers
   216→	listeners := make([]net.PacketConn, *numPeers)
   217→	for i := range peers {
   218→		conn, err := peers[i].client.ListenUDP(fmt.Sprintf(":%d", echoPort))
   219→		if err != nil {
   220→			fmt.Fprintf(os.Stderr, "  Error creating listener on %s: %v\n", peers[i].name, err)
   221→			cleanup(peers)
   222→			os.Exit(1)
   223→		}
   224→		listeners[i] = conn
   225→		go echoServer(conn, *packetSize)
   226→		fmt.Printf("  Echo listener started on %s:%d\n", peers[i].tunnelIP, echoPort)
   227→	}
   228→
   229→	// Run traffic between all pairs (i < j)
   230→	var statsMu sync.Mutex
   231→	var allStats []pairStats
   232→
   233→	var trafficWg sync.WaitGroup
   234→	for i := 0; i < *numPeers; i++ {
   235→		for j := i + 1; j < *numPeers; j++ {
   236→			trafficWg.Add(1)
   237→			go func(from, to int) {
   238→				defer trafficWg.Done()
   239→				stats := runTraffic(peers[from].client, peers[from].name, peers[to].tunnelIP, peers[to].name, *duration, *packetSize)
   240→				statsMu.Lock()
   241→				allStats = append(allStats, stats)
   242→				statsMu.Unlock()
   243→			}(i, j)
   244→		}
   245→	}
   246→	trafficWg.Wait()
   247→
   248→	// Close listeners
   249→	for _, l := range listeners {
   250→		if l != nil {
   251→			l.Close()
   252→		}
   253→	}
   254→
   255→	// Phase 5: Report
   256→	fmt.Println("\n--- Phase 5: Results ---")
   257→	printReport(allStats, *packetSize)
   258→
   259→	// Cleanup
   260→	fmt.Println("\n--- Cleanup ---")
   261→	cleanup(peers)
   262→	fmt.Println("Done.")
   263→}
   264→
   265→func countConnectedPeers(c *embed.Client) int {
   266→	status, err := c.Status()
   267→	if err != nil {
   268→		return 0
   269→	}
   270→	count := 0
   271→	for _, p := range status.Peers {
   272→		if p.ConnStatus == embed.PeerStatusConnected {
   273→			count++
   274→		}
   275→	}
   276→	return count
   277→}
   278→
   279→func printConnectionStatus(peers []peerInfo) {
   280→	for i := range peers {
   281→		status, err := peers[i].client.Status()
   282→		if err != nil {
   283→			fmt.Printf("  %s: error getting status: %v\n", peers[i].name, err)
   284→			continue
   285→		}
   286→		connected := 0
   287→		relayed := 0
   288→		for _, p := range status.Peers {
   289→			if p.ConnStatus == embed.PeerStatusConnected {
   290→				connected++
   291→				if p.Relayed {
   292→					relayed++
   293→				}
   294→			}
   295→		}
   296→		connType := "direct"
   297→		if relayed > 0 {
   298→			connType = fmt.Sprintf("%d direct, %d relayed", connected-relayed, relayed)
   299→		}
   300→		fmt.Printf("  %s: %d/%d connected (%s)\n", peers[i].name, connected, len(status.Peers), connType)
   301→	}
   302→}
   303→
   304→func echoServer(conn net.PacketConn, maxSize int) {
   305→	buf := make([]byte, maxSize+100)
   306→	for {
   307→		n, addr, err := conn.ReadFrom(buf)
   308→		if err != nil {
   309→			return
   310→		}
   311→		_, _ = conn.WriteTo(buf[:n], addr)
   312→	}
   313→}
   314→
   315→func runTraffic(client *embed.Client, fromName, toIP, toName string, duration time.Duration, packetSize int) pairStats {
   316→	stats := pairStats{
   317→		from: fromName,
   318→		to:   toName,
   319→	}
   320→
   321→	ctx, cancel := context.WithTimeout(context.Background(), duration+10*time.Second)
   322→	defer cancel()
   323→
   324→	conn, err := client.Dial(ctx, "udp", fmt.Sprintf("%s:%d", toIP, echoPort))
   325→	if err != nil {
   326→		fmt.Fprintf(os.Stderr, "  Error dialing %s -> %s: %v\n", fromName, toName, err)
   327→		return stats
   328→	}
   329→	defer conn.Close()
   330→
   331→	deadline := time.Now().Add(duration)
   332→	buf := make([]byte, packetSize)
   333→	recvBuf := make([]byte, packetSize+100)
   334→	var seq uint64
   335→
   336→	for time.Now().Before(deadline) {
   337→		seq++
   338→		// Encode timestamp and sequence number
   339→		binary.BigEndian.PutUint64(buf[0:8], uint64(time.Now().UnixNano()))
   340→		binary.BigEndian.PutUint64(buf[8:16], seq)
   341→
   342→		stats.sent++
   343→		conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
   344→		_, err := conn.Write(buf)
   345→		if err != nil {
   346→			stats.lost++
   347→			continue
   348→		}
   349→
   350→		conn.SetReadDeadline(time.Now().Add(5 * time.Second))
   351→		n, err := conn.Read(recvBuf)
   352→		if err != nil {
   353→			stats.lost++
   354→			continue
   355→		}
   356→
   357→		if n >= 8 {
   358→			sentNano := binary.BigEndian.Uint64(recvBuf[0:8])
   359→			rtt := time.Since(time.Unix(0, int64(sentNano)))
   360→			stats.received++
   361→			stats.rtts = append(stats.rtts, rtt)
   362→		} else {
   363→			stats.received++
   364→		}
   365→
   366→		// Small sleep to avoid flooding
   367→		time.Sleep(10 * time.Millisecond)
   368→	}
   369→
   370→	return stats
   371→}
   372→
   373→func printReport(allStats []pairStats, packetSize int) {
   374→	if len(allStats) == 0 {
   375→		fmt.Println("  No traffic data collected.")
   376→		return
   377→	}
   378→
   379→	fmt.Printf("  %-30s %8s %8s %8s %8s %10s %10s %10s\n",
   380→		"Pair", "Sent", "Recv", "Lost", "Loss%", "Avg RTT", "Min RTT", "Max RTT")
   381→	fmt.Println("  " + strings.Repeat("-", 108))
   382→
   383→	var totalSent, totalRecv, totalLost int64
   384→	var totalRTTs []time.Duration
   385→
   386→	for _, s := range allStats {
   387→		avg, min, max, loss := s.summary()
   388→		pair := fmt.Sprintf("%s -> %s", s.from, s.to)
   389→		fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
   390→			pair, s.sent, s.received, s.lost, loss,
   391→			avg.Round(time.Microsecond), min.Round(time.Microsecond), max.Round(time.Microsecond))
   392→		totalSent += s.sent
   393→		totalRecv += s.received
   394→		totalLost += s.lost
   395→		totalRTTs = append(totalRTTs, s.rtts...)
   396→	}
   397→
   398→	fmt.Println("  " + strings.Repeat("-", 108))
   399→
   400→	// Overall summary
   401→	var overallLoss float64
   402→	if totalSent > 0 {
   403→		overallLoss = float64(totalLost) / float64(totalSent) * 100
   404→	}
   405→
   406→	var avgRTT, minRTT, maxRTT time.Duration
   407→	if len(totalRTTs) > 0 {
   408→		minRTT = totalRTTs[0]
   409→		maxRTT = totalRTTs[0]
   410→		var total time.Duration
   411→		for _, rtt := range totalRTTs {
   412→			total += rtt
   413→			if rtt < minRTT {
   414→				minRTT = rtt
   415→			}
   416→			if rtt > maxRTT {
   417→				maxRTT = rtt
   418→			}
   419→		}
   420→		avgRTT = total / time.Duration(len(totalRTTs))
   421→	}
   422→
   423→	fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
   424→		"TOTAL", totalSent, totalRecv, totalLost, overallLoss,
   425→		avgRTT.Round(time.Microsecond), minRTT.Round(time.Microsecond), maxRTT.Round(time.Microsecond))
   426→
   427→	// Throughput
   428→	if len(totalRTTs) > 0 {
   429→		fmt.Println()
   430→		totalBytes := totalRecv * int64(packetSize)
   431→		// Calculate stddev
   432→		var sumSq float64
   433→		avgNs := float64(avgRTT.Nanoseconds())
   434→		for _, rtt := range totalRTTs {
   435→			diff := float64(rtt.Nanoseconds()) - avgNs
   436→			sumSq += diff * diff
   437→		}
   438→		stddev := time.Duration(math.Sqrt(sumSq / float64(len(totalRTTs))))
   439→
   440→		fmt.Printf("  Total data transferred: %.2f KB\n", float64(totalBytes)/1024)
   441→		fmt.Printf("  RTT stddev:             %s\n", stddev.Round(time.Microsecond))
   442→		fmt.Printf("  Pairs tested:           %d\n", len(allStats))
   443→	}
   444→}
   445→
   446→func cleanup(peers []peerInfo) {
   447→	ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
   448→	defer cancel()
   449→
   450→	var wg sync.WaitGroup
   451→	for i := range peers {
   452→		wg.Add(1)
   453→		go func(idx int) {
   454→			defer wg.Done()
   455→			if err := peers[idx].client.Stop(ctx); err != nil {
   456→				fmt.Fprintf(os.Stderr, "  Error stopping %s: %v\n", peers[idx].name, err)
   457→			} else {
   458→				fmt.Printf("  Stopped %s\n", peers[idx].name)
   459→			}
   460→		}(i)
   461→	}
   462→	wg.Wait()
   463→}
   464→

package main

import (
	"context"
	"encoding/binary"
	"flag"
	"fmt"
	"math"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/netbirdio/netbird/client/embed"
)

const (
	echoPort       = 9000
	connectTimeout = 120 * time.Second
	startTimeout   = 60 * time.Second
	stopTimeout    = 30 * time.Second
)

type peerInfo struct {
	client   *embed.Client
	tunnelIP string
	name     string
}

type pairStats struct {
	from     string
	to       string
	sent     int64
	received int64
	lost     int64
	rtts     []time.Duration
}

func (s *pairStats) summary() (avgRTT, minRTT, maxRTT time.Duration, lossPercent float64) {
	if len(s.rtts) == 0 {
		return 0, 0, 0, 100
	}
	minRTT = s.rtts[0]
	maxRTT = s.rtts[0]
	var total time.Duration
	for _, rtt := range s.rtts {
		total += rtt
		if rtt < minRTT {
			minRTT = rtt
		}
		if rtt > maxRTT {
			maxRTT = rtt
		}
	}
	avgRTT = total / time.Duration(len(s.rtts))
	if s.sent > 0 {
		lossPercent = float64(s.lost) / float64(s.sent) * 100
	}
	return
}

func main() {
	managementURL := flag.String("management-url", "", "Management server URL (required)")
	setupKey := flag.String("setup-key", "", "Reusable setup key (required)")
	numPeers := flag.Int("peers", 5, "Number of peers to spawn")
	forceRelay := flag.Bool("force-relay", false, "Force relay connections (NB_FORCE_RELAY=true)")
	duration := flag.Duration("duration", 30*time.Second, "Traffic test duration")
	packetSize := flag.Int("packet-size", 512, "UDP packet size in bytes")
	logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")
	flag.Parse()

	if *managementURL == "" || *setupKey == "" {
		fmt.Fprintln(os.Stderr, "Error: --management-url and --setup-key are required")
		flag.Usage()
		os.Exit(1)
	}

	if *numPeers < 2 {
		fmt.Fprintln(os.Stderr, "Error: --peers must be at least 2")
		os.Exit(1)
	}

	// Minimum packet size: 8 bytes for timestamp + 8 bytes for sequence number
	if *packetSize < 16 {
		fmt.Fprintln(os.Stderr, "Error: --packet-size must be at least 16")
		os.Exit(1)
	}

	if *forceRelay {
		os.Setenv("NB_FORCE_RELAY", "true")
	}
	os.Setenv("NB_USE_NETSTACK_MODE", "true")

	fmt.Println("=== NetBird Performance Test ===")
	fmt.Printf("Management URL: %s\n", *managementURL)
	fmt.Printf("Peers:          %d\n", *numPeers)
	fmt.Printf("Force relay:    %v\n", *forceRelay)
	fmt.Printf("Duration:       %s\n", *duration)
	fmt.Printf("Packet size:    %d bytes\n", *packetSize)
	fmt.Println()

	// Phase 1: Create peers
	fmt.Println("--- Phase 1: Creating peers ---")
	peers := make([]peerInfo, *numPeers)
	for i := 0; i < *numPeers; i++ {
		name := fmt.Sprintf("perf-peer-%d", i)
		port := 0
		c, err := embed.New(embed.Options{
			DeviceName:    name,
			SetupKey:      *setupKey,
			ManagementURL: *managementURL,
			WireguardPort: &port,
			LogLevel:      *logLevel,
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error creating peer %s: %v\n", name, err)
			os.Exit(1)
		}
		peers[i] = peerInfo{client: c, name: name}
		fmt.Printf("  Created %s\n", name)
	}

	// Phase 2: Start peers in parallel
	fmt.Println("\n--- Phase 2: Starting peers ---")
	startTime := time.Now()
	var wg sync.WaitGroup
	startErrors := make([]error, *numPeers)

	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), startTimeout)
			defer cancel()
			t := time.Now()
			if err := peers[idx].client.Start(ctx); err != nil {
				startErrors[idx] = err
				return
			}
			fmt.Printf("  %s started in %s\n", peers[idx].name, time.Since(t).Round(time.Millisecond))
		}(i)
	}
	wg.Wait()

	// Check for start errors
	var failed bool
	for i, err := range startErrors {
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error starting %s: %v\n", peers[i].name, err)
			failed = true
		}
	}
	if failed {
		cleanup(peers)
		os.Exit(1)
	}
	fmt.Printf("  All peers started in %s\n", time.Since(startTime).Round(time.Millisecond))

	// Get tunnel IPs
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error getting status for %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		ip := status.LocalPeerState.IP
		// Strip CIDR suffix if present (e.g. "100.64.0.1/16" -> "100.64.0.1")
		if idx := strings.Index(ip, "/"); idx != -1 {
			ip = ip[:idx]
		}
		peers[i].tunnelIP = ip
		fmt.Printf("  %s -> %s\n", peers[i].name, peers[i].tunnelIP)
	}

	// Phase 3: Wait for connections
	fmt.Println("\n--- Phase 3: Waiting for peer connections ---")
	connStart := time.Now()
	expectedPeers := *numPeers - 1
	deadline := time.After(connectTimeout)
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	allConnected := false
waitLoop:
	for {
		select {
		case <-deadline:
			fmt.Fprintf(os.Stderr, "  Timeout waiting for connections after %s\n", connectTimeout)
			printConnectionStatus(peers)
			cleanup(peers)
			os.Exit(1)
		case <-ticker.C:
			allConnected = true
			for i := range peers {
				connected := countConnectedPeers(peers[i].client)
				if connected < expectedPeers {
					allConnected = false
					break
				}
			}
			if allConnected {
				break waitLoop
			}
		}
	}

	fmt.Printf("  All peers connected in %s\n", time.Since(connStart).Round(time.Millisecond))
	printConnectionStatus(peers)

	// Phase 4: Traffic test
	fmt.Printf("\n--- Phase 4: Traffic test (%s) ---\n", *duration)

	// Start echo listeners on all peers
	listeners := make([]net.PacketConn, *numPeers)
	for i := range peers {
		conn, err := peers[i].client.ListenUDP(fmt.Sprintf(":%d", echoPort))
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error creating listener on %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		listeners[i] = conn
		go echoServer(conn, *packetSize)
		fmt.Printf("  Echo listener started on %s:%d\n", peers[i].tunnelIP, echoPort)
	}

	// Run traffic between all pairs (i < j)
	var statsMu sync.Mutex
	var allStats []pairStats

	var trafficWg sync.WaitGroup
	for i := 0; i < *numPeers; i++ {
		for j := i + 1; j < *numPeers; j++ {
			trafficWg.Add(1)
			go func(from, to int) {
				defer trafficWg.Done()
				stats := runTraffic(peers[from].client, peers[from].name, peers[to].tunnelIP, peers[to].name, *duration, *packetSize)
				statsMu.Lock()
				allStats = append(allStats, stats)
				statsMu.Unlock()
			}(i, j)
		}
	}
	trafficWg.Wait()

	// Close listeners
	for _, l := range listeners {
		if l != nil {
			l.Close()
		}
	}

	// Phase 5: Report
	fmt.Println("\n--- Phase 5: Results ---")
	printReport(allStats, *packetSize)

	// Cleanup
	fmt.Println("\n--- Cleanup ---")
	cleanup(peers)
	fmt.Println("Done.")
}

func countConnectedPeers(c *embed.Client) int {
	status, err := c.Status()
	if err != nil {
		return 0
	}
	count := 0
	for _, p := range status.Peers {
		if p.ConnStatus == embed.PeerStatusConnected {
			count++
		}
	}
	return count
}

func printConnectionStatus(peers []peerInfo) {
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Printf("  %s: error getting status: %v\n", peers[i].name, err)
			continue
		}
		connected := 0
		relayed := 0
		for _, p := range status.Peers {
			if p.ConnStatus == embed.PeerStatusConnected {
				connected++
				if p.Relayed {
					relayed++
				}
			}
		}
		connType := "direct"
		if relayed > 0 {
			connType = fmt.Sprintf("%d direct, %d relayed", connected-relayed, relayed)
		}
		fmt.Printf("  %s: %d/%d connected (%s)\n", peers[i].name, connected, len(status.Peers), connType)
	}
}

func echoServer(conn net.PacketConn, maxSize int) {
	buf := make([]byte, maxSize+100)
	for {
		n, addr, err := conn.ReadFrom(buf)
		if err != nil {
			return
		}
		_, _ = conn.WriteTo(buf[:n], addr)
	}
}

func runTraffic(client *embed.Client, fromName, toIP, toName string, duration time.Duration, packetSize int) pairStats {
	stats := pairStats{
		from: fromName,
		to:   toName,
	}

	ctx, cancel := context.WithTimeout(context.Background(), duration+10*time.Second)
	defer cancel()

	conn, err := client.Dial(ctx, "udp", fmt.Sprintf("%s:%d", toIP, echoPort))
	if err != nil {
		fmt.Fprintf(os.Stderr, "  Error dialing %s -> %s: %v\n", fromName, toName, err)
		return stats
	}
	defer conn.Close()

	deadline := time.Now().Add(duration)
	buf := make([]byte, packetSize)
	recvBuf := make([]byte, packetSize+100)
	var seq uint64

	for time.Now().Before(deadline) {
		seq++
		// Encode timestamp and sequence number
		binary.BigEndian.PutUint64(buf[0:8], uint64(time.Now().UnixNano()))
		binary.BigEndian.PutUint64(buf[8:16], seq)

		stats.sent++
		conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
		_, err := conn.Write(buf)
		if err != nil {
			stats.lost++
			continue
		}

		conn.SetReadDeadline(time.Now().Add(5 * time.Second))
		n, err := conn.Read(recvBuf)
		if err != nil {
			stats.lost++
			continue
		}

		if n >= 8 {
			sentNano := binary.BigEndian.Uint64(recvBuf[0:8])
			rtt := time.Since(time.Unix(0, int64(sentNano)))
			stats.received++
			stats.rtts = append(stats.rtts, rtt)
		} else {
			stats.received++
		}

		// Small sleep to avoid flooding
		time.Sleep(10 * time.Millisecond)
	}

	return stats
}

func printReport(allStats []pairStats, packetSize int) {
	if len(allStats) == 0 {
		fmt.Println("  No traffic data collected.")
		return
	}

	fmt.Printf("  %-30s %8s %8s %8s %8s %10s %10s %10s\n",
		"Pair", "Sent", "Recv", "Lost", "Loss%", "Avg RTT", "Min RTT", "Max RTT")
	fmt.Println("  " + strings.Repeat("-", 108))

	var totalSent, totalRecv, totalLost int64
	var totalRTTs []time.Duration

	for _, s := range allStats {
		avg, min, max, loss := s.summary()
		pair := fmt.Sprintf("%s -> %s", s.from, s.to)
		fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
			pair, s.sent, s.received, s.lost, loss,
			avg.Round(time.Microsecond), min.Round(time.Microsecond), max.Round(time.Microsecond))
		totalSent += s.sent
		totalRecv += s.received
		totalLost += s.lost
		totalRTTs = append(totalRTTs, s.rtts...)
	}

	fmt.Println("  " + strings.Repeat("-", 108))

	// Overall summary
	var overallLoss float64
	if totalSent > 0 {
		overallLoss = float64(totalLost) / float64(totalSent) * 100
	}

	var avgRTT, minRTT, maxRTT time.Duration
	if len(totalRTTs) > 0 {
		minRTT = totalRTTs[0]
		maxRTT = totalRTTs[0]
		var total time.Duration
		for _, rtt := range totalRTTs {
			total += rtt
			if rtt < minRTT {
				minRTT = rtt
			}
			if rtt > maxRTT {
				maxRTT = rtt
			}
		}
		avgRTT = total / time.Duration(len(totalRTTs))
	}

	fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
		"TOTAL", totalSent, totalRecv, totalLost, overallLoss,
		avgRTT.Round(time.Microsecond), minRTT.Round(time.Microsecond), maxRTT.Round(time.Microsecond))

	// Throughput
	if len(totalRTTs) > 0 {
		fmt.Println()
		totalBytes := totalRecv * int64(packetSize)
		// Calculate stddev
		var sumSq float64
		avgNs := float64(avgRTT.Nanoseconds())
		for _, rtt := range totalRTTs {
			diff := float64(rtt.Nanoseconds()) - avgNs
			sumSq += diff * diff
		}
		stddev := time.Duration(math.Sqrt(sumSq / float64(len(totalRTTs))))

		fmt.Printf("  Total data transferred: %.2f KB\n", float64(totalBytes)/1024)
		fmt.Printf("  RTT stddev:             %s\n", stddev.Round(time.Microsecond))
		fmt.Printf("  Pairs tested:           %d\n", len(allStats))
	}
}

func cleanup(peers []peerInfo) {
	ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
	defer cancel()

	var wg sync.WaitGroup
	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			if err := peers[idx].client.Stop(ctx); err != nil {
				fmt.Fprintf(os.Stderr, "  Error stopping %s: %v\n", peers[idx].name, err)
			} else {
				fmt.Printf("  Stopped %s\n", peers[idx].name)
			}
		}(i)
	}
	wg.Wait()
}


The tool is implemented and compiles successfully. Here's what was created:

**`combined/perftest/main.go`** - a single-file Go program (~460 lines) that:

1. **CLI flags**: `--management-url`, `--setup-key`, `--peers`, `--force-relay`, `--duration`, `--packet-size`, `--log-level`
2. **Phase 1** - Creates N `embed.Client` instances with unique names and auto-assigned WireGuard ports
3. **Phase 2** - Starts all clients in parallel, prints per-peer startup times
4. **Phase 3** - Polls `Status()` on each client until all peers see N-1 connected peers (120s timeout)
5. **Phase 4** - Each peer runs a UDP echo listener; for each pair (i < j), sends timestamped UDP packets and measures RTT
6. **Phase 5** - Prints per-pair and aggregate stats: sent/recv/lost, loss%, avg/min/max RTT, stddev, total data transferred

One notable fix from the plan: used `embed.PeerStatusConnected` instead of directly importing `client/internal/peer` (which is blocked by Go's internal package visibility rules from `combined/`).

To use:
```bash
cd combined/perftest && go build -o perftest .
./perftest --management-url https://localhost:443 --setup-key <key> --peers 3
```


disable logs of the client, I wanna see just logs of the test


The user wants to suppress the netbird client logs (which use logrus) so only the perftest's own fmt output is visible. The simplest approach: set the log level to "panic" (effectively silent) regardless of the `--log-level` flag, or better, redirect the client's log output to `io.Discard` via the `LogOutput` option in `embed.Options`.

Looking at the embed.Options struct:
```go
// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
LogOutput io.Writer
```

So I can set `LogOutput: io.Discard` on each client to suppress all client logs. This is cleaner than messing with log levels since the user might still want `--log-level` for debugging purposes later.

I'm thinking the cleanest solution is to always discard client logs by default with `LogOutput: io.Discard`, and if the user needs client debug output in the future, that's an easy change to make. I should probably remove the `--log-level` flag since it was only controlling the client's verbosity anyway.


The `embed.Options` has a `LogOutput` field. Setting it to `io.Discard` will suppress all client/logrus output.


Edit

	"encoding/binary"
	"flag"
	"fmt"
	"math"

	"encoding/binary"
	"flag"
	"fmt"
	"io"
	"math"


	"encoding/binary"
	"flag"
	"fmt"
	"math"

	"encoding/binary"
	"flag"
	"fmt"
	"io"
	"math"

package main

import (
	"context"
	"encoding/binary"
	"flag"
	"fmt"
	"math"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/netbirdio/netbird/client/embed"
)

const (
	echoPort       = 9000
	connectTimeout = 120 * time.Second
	startTimeout   = 60 * time.Second
	stopTimeout    = 30 * time.Second
)

type peerInfo struct {
	client   *embed.Client
	tunnelIP string
	name     string
}

type pairStats struct {
	from     string
	to       string
	sent     int64
	received int64
	lost     int64
	rtts     []time.Duration
}

func (s *pairStats) summary() (avgRTT, minRTT, maxRTT time.Duration, lossPercent float64) {
	if len(s.rtts) == 0 {
		return 0, 0, 0, 100
	}
	minRTT = s.rtts[0]
	maxRTT = s.rtts[0]
	var total time.Duration
	for _, rtt := range s.rtts {
		total += rtt
		if rtt < minRTT {
			minRTT = rtt
		}
		if rtt > maxRTT {
			maxRTT = rtt
		}
	}
	avgRTT = total / time.Duration(len(s.rtts))
	if s.sent > 0 {
		lossPercent = float64(s.lost) / float64(s.sent) * 100
	}
	return
}

func main() {
	managementURL := flag.String("management-url", "", "Management server URL (required)")
	setupKey := flag.String("setup-key", "", "Reusable setup key (required)")
	numPeers := flag.Int("peers", 5, "Number of peers to spawn")
	forceRelay := flag.Bool("force-relay", false, "Force relay connections (NB_FORCE_RELAY=true)")
	duration := flag.Duration("duration", 30*time.Second, "Traffic test duration")
	packetSize := flag.Int("packet-size", 512, "UDP packet size in bytes")
	logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")
	flag.Parse()

	if *managementURL == "" || *setupKey == "" {
		fmt.Fprintln(os.Stderr, "Error: --management-url and --setup-key are required")
		flag.Usage()
		os.Exit(1)
	}

	if *numPeers < 2 {
		fmt.Fprintln(os.Stderr, "Error: --peers must be at least 2")
		os.Exit(1)
	}

	// Minimum packet size: 8 bytes for timestamp + 8 bytes for sequence number
	if *packetSize < 16 {
		fmt.Fprintln(os.Stderr, "Error: --packet-size must be at least 16")
		os.Exit(1)
	}

	if *forceRelay {
		os.Setenv("NB_FORCE_RELAY", "true")
	}
	os.Setenv("NB_USE_NETSTACK_MODE", "true")

	fmt.Println("=== NetBird Performance Test ===")
	fmt.Printf("Management URL: %s\n", *managementURL)
	fmt.Printf("Peers:          %d\n", *numPeers)
	fmt.Printf("Force relay:    %v\n", *forceRelay)
	fmt.Printf("Duration:       %s\n", *duration)
	fmt.Printf("Packet size:    %d bytes\n", *packetSize)
	fmt.Println()

	// Phase 1: Create peers
	fmt.Println("--- Phase 1: Creating peers ---")
	peers := make([]peerInfo, *numPeers)
	for i := 0; i < *numPeers; i++ {
		name := fmt.Sprintf("perf-peer-%d", i)
		port := 0
		c, err := embed.New(embed.Options{
			DeviceName:    name,
			SetupKey:      *setupKey,
			ManagementURL: *managementURL,
			WireguardPort: &port,
			LogLevel:      *logLevel,
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error creating peer %s: %v\n", name, err)
			os.Exit(1)
		}
		peers[i] = peerInfo{client: c, name: name}
		fmt.Printf("  Created %s\n", name)
	}

	// Phase 2: Start peers in parallel
	fmt.Println("\n--- Phase 2: Starting peers ---")
	startTime := time.Now()
	var wg sync.WaitGroup
	startErrors := make([]error, *numPeers)

	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), startTimeout)
			defer cancel()
			t := time.Now()
			if err := peers[idx].client.Start(ctx); err != nil {
				startErrors[idx] = err
				return
			}
			fmt.Printf("  %s started in %s\n", peers[idx].name, time.Since(t).Round(time.Millisecond))
		}(i)
	}
	wg.Wait()

	// Check for start errors
	var failed bool
	for i, err := range startErrors {
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error starting %s: %v\n", peers[i].name, err)
			failed = true
		}
	}
	if failed {
		cleanup(peers)
		os.Exit(1)
	}
	fmt.Printf("  All peers started in %s\n", time.Since(startTime).Round(time.Millisecond))

	// Get tunnel IPs
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error getting status for %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		ip := status.LocalPeerState.IP
		// Strip CIDR suffix if present (e.g. "100.64.0.1/16" -> "100.64.0.1")
		if idx := strings.Index(ip, "/"); idx != -1 {
			ip = ip[:idx]
		}
		peers[i].tunnelIP = ip
		fmt.Printf("  %s -> %s\n", peers[i].name, peers[i].tunnelIP)
	}

	// Phase 3: Wait for connections
	fmt.Println("\n--- Phase 3: Waiting for peer connections ---")
	connStart := time.Now()
	expectedPeers := *numPeers - 1
	deadline := time.After(connectTimeout)
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	allConnected := false
waitLoop:
	for {
		select {
		case <-deadline:
			fmt.Fprintf(os.Stderr, "  Timeout waiting for connections after %s\n", connectTimeout)
			printConnectionStatus(peers)
			cleanup(peers)
			os.Exit(1)
		case <-ticker.C:
			allConnected = true
			for i := range peers {
				connected := countConnectedPeers(peers[i].client)
				if connected < expectedPeers {
					allConnected = false
					break
				}
			}
			if allConnected {
				break waitLoop
			}
		}
	}

	fmt.Printf("  All peers connected in %s\n", time.Since(connStart).Round(time.Millisecond))
	printConnectionStatus(peers)

	// Phase 4: Traffic test
	fmt.Printf("\n--- Phase 4: Traffic test (%s) ---\n", *duration)

	// Start echo listeners on all peers
	listeners := make([]net.PacketConn, *numPeers)
	for i := range peers {
		conn, err := peers[i].client.ListenUDP(fmt.Sprintf(":%d", echoPort))
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error creating listener on %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		listeners[i] = conn
		go echoServer(conn, *packetSize)
		fmt.Printf("  Echo listener started on %s:%d\n", peers[i].tunnelIP, echoPort)
	}

	// Run traffic between all pairs (i < j)
	var statsMu sync.Mutex
	var allStats []pairStats

	var trafficWg sync.WaitGroup
	for i := 0; i < *numPeers; i++ {
		for j := i + 1; j < *numPeers; j++ {
			trafficWg.Add(1)
			go func(from, to int) {
				defer trafficWg.Done()
				stats := runTraffic(peers[from].client, peers[from].name, peers[to].tunnelIP, peers[to].name, *duration, *packetSize)
				statsMu.Lock()
				allStats = append(allStats, stats)
				statsMu.Unlock()
			}(i, j)
		}
	}
	trafficWg.Wait()

	// Close listeners
	for _, l := range listeners {
		if l != nil {
			l.Close()
		}
	}

	// Phase 5: Report
	fmt.Println("\n--- Phase 5: Results ---")
	printReport(allStats, *packetSize)

	// Cleanup
	fmt.Println("\n--- Cleanup ---")
	cleanup(peers)
	fmt.Println("Done.")
}

func countConnectedPeers(c *embed.Client) int {
	status, err := c.Status()
	if err != nil {
		return 0
	}
	count := 0
	for _, p := range status.Peers {
		if p.ConnStatus == embed.PeerStatusConnected {
			count++
		}
	}
	return count
}

func printConnectionStatus(peers []peerInfo) {
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Printf("  %s: error getting status: %v\n", peers[i].name, err)
			continue
		}
		connected := 0
		relayed := 0
		for _, p := range status.Peers {
			if p.ConnStatus == embed.PeerStatusConnected {
				connected++
				if p.Relayed {
					relayed++
				}
			}
		}
		connType := "direct"
		if relayed > 0 {
			connType = fmt.Sprintf("%d direct, %d relayed", connected-relayed, relayed)
		}
		fmt.Printf("  %s: %d/%d connected (%s)\n", peers[i].name, connected, len(status.Peers), connType)
	}
}

func echoServer(conn net.PacketConn, maxSize int) {
	buf := make([]byte, maxSize+100)
	for {
		n, addr, err := conn.ReadFrom(buf)
		if err != nil {
			return
		}
		_, _ = conn.WriteTo(buf[:n], addr)
	}
}

func runTraffic(client *embed.Client, fromName, toIP, toName string, duration time.Duration, packetSize int) pairStats {
	stats := pairStats{
		from: fromName,
		to:   toName,
	}

	ctx, cancel := context.WithTimeout(context.Background(), duration+10*time.Second)
	defer cancel()

	conn, err := client.Dial(ctx, "udp", fmt.Sprintf("%s:%d", toIP, echoPort))
	if err != nil {
		fmt.Fprintf(os.Stderr, "  Error dialing %s -> %s: %v\n", fromName, toName, err)
		return stats
	}
	defer conn.Close()

	deadline := time.Now().Add(duration)
	buf := make([]byte, packetSize)
	recvBuf := make([]byte, packetSize+100)
	var seq uint64

	for time.Now().Before(deadline) {
		seq++
		// Encode timestamp and sequence number
		binary.BigEndian.PutUint64(buf[0:8], uint64(time.Now().UnixNano()))
		binary.BigEndian.PutUint64(buf[8:16], seq)

		stats.sent++
		conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
		_, err := conn.Write(buf)
		if err != nil {
			stats.lost++
			continue
		}

		conn.SetReadDeadline(time.Now().Add(5 * time.Second))
		n, err := conn.Read(recvBuf)
		if err != nil {
			stats.lost++
			continue
		}

		if n >= 8 {
			sentNano := binary.BigEndian.Uint64(recvBuf[0:8])
			rtt := time.Since(time.Unix(0, int64(sentNano)))
			stats.received++
			stats.rtts = append(stats.rtts, rtt)
		} else {
			stats.received++
		}

		// Small sleep to avoid flooding
		time.Sleep(10 * time.Millisecond)
	}

	return stats
}

func printReport(allStats []pairStats, packetSize int) {
	if len(allStats) == 0 {
		fmt.Println("  No traffic data collected.")
		return
	}

	fmt.Printf("  %-30s %8s %8s %8s %8s %10s %10s %10s\n",
		"Pair", "Sent", "Recv", "Lost", "Loss%", "Avg RTT", "Min RTT", "Max RTT")
	fmt.Println("  " + strings.Repeat("-", 108))

	var totalSent, totalRecv, totalLost int64
	var totalRTTs []time.Duration

	for _, s := range allStats {
		avg, min, max, loss := s.summary()
		pair := fmt.Sprintf("%s -> %s", s.from, s.to)
		fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
			pair, s.sent, s.received, s.lost, loss,
			avg.Round(time.Microsecond), min.Round(time.Microsecond), max.Round(time.Microsecond))
		totalSent += s.sent
		totalRecv += s.received
		totalLost += s.lost
		totalRTTs = append(totalRTTs, s.rtts...)
	}

	fmt.Println("  " + strings.Repeat("-", 108))

	// Overall summary
	var overallLoss float64
	if totalSent > 0 {
		overallLoss = float64(totalLost) / float64(totalSent) * 100
	}

	var avgRTT, minRTT, maxRTT time.Duration
	if len(totalRTTs) > 0 {
		minRTT = totalRTTs[0]
		maxRTT = totalRTTs[0]
		var total time.Duration
		for _, rtt := range totalRTTs {
			total += rtt
			if rtt < minRTT {
				minRTT = rtt
			}
			if rtt > maxRTT {
				maxRTT = rtt
			}
		}
		avgRTT = total / time.Duration(len(totalRTTs))
	}

	fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
		"TOTAL", totalSent, totalRecv, totalLost, overallLoss,
		avgRTT.Round(time.Microsecond), minRTT.Round(time.Microsecond), maxRTT.Round(time.Microsecond))

	// Throughput
	if len(totalRTTs) > 0 {
		fmt.Println()
		totalBytes := totalRecv * int64(packetSize)
		// Calculate stddev
		var sumSq float64
		avgNs := float64(avgRTT.Nanoseconds())
		for _, rtt := range totalRTTs {
			diff := float64(rtt.Nanoseconds()) - avgNs
			sumSq += diff * diff
		}
		stddev := time.Duration(math.Sqrt(sumSq / float64(len(totalRTTs))))

		fmt.Printf("  Total data transferred: %.2f KB\n", float64(totalBytes)/1024)
		fmt.Printf("  RTT stddev:             %s\n", stddev.Round(time.Microsecond))
		fmt.Printf("  Pairs tested:           %d\n", len(allStats))
	}
}

func cleanup(peers []peerInfo) {
	ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
	defer cancel()

	var wg sync.WaitGroup
	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			if err := peers[idx].client.Stop(ctx); err != nil {
				fmt.Fprintf(os.Stderr, "  Error stopping %s: %v\n", peers[idx].name, err)
			} else {
				fmt.Printf("  Stopped %s\n", peers[idx].name)
			}
		}(i)
	}
	wg.Wait()
}

"encoding/binary"
"flag"
"fmt"
+  "io"
"math"
"net"
"os"


	logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")
	flag.Parse()

	logLevel := flag.String("log-level", "panic", "Client log level (trace, debug, info, warn, error, panic)")
	flag.Parse()


	logLevel := flag.String("log-level", "panic", "Client log level (trace, debug, info, warn, error, panic)")
	flag.Parse()

package main

import (
	"context"
	"encoding/binary"
	"flag"
	"fmt"
	"io"
	"math"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/netbirdio/netbird/client/embed"
)

const (
	echoPort       = 9000
	connectTimeout = 120 * time.Second
	startTimeout   = 60 * time.Second
	stopTimeout    = 30 * time.Second
)

type peerInfo struct {
	client   *embed.Client
	tunnelIP string
	name     string
}

type pairStats struct {
	from     string
	to       string
	sent     int64
	received int64
	lost     int64
	rtts     []time.Duration
}

func (s *pairStats) summary() (avgRTT, minRTT, maxRTT time.Duration, lossPercent float64) {
	if len(s.rtts) == 0 {
		return 0, 0, 0, 100
	}
	minRTT = s.rtts[0]
	maxRTT = s.rtts[0]
	var total time.Duration
	for _, rtt := range s.rtts {
		total += rtt
		if rtt < minRTT {
			minRTT = rtt
		}
		if rtt > maxRTT {
			maxRTT = rtt
		}
	}
	avgRTT = total / time.Duration(len(s.rtts))
	if s.sent > 0 {
		lossPercent = float64(s.lost) / float64(s.sent) * 100
	}
	return
}

func main() {
	managementURL := flag.String("management-url", "", "Management server URL (required)")
	setupKey := flag.String("setup-key", "", "Reusable setup key (required)")
	numPeers := flag.Int("peers", 5, "Number of peers to spawn")
	forceRelay := flag.Bool("force-relay", false, "Force relay connections (NB_FORCE_RELAY=true)")
	duration := flag.Duration("duration", 30*time.Second, "Traffic test duration")
	packetSize := flag.Int("packet-size", 512, "UDP packet size in bytes")
	logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")
	flag.Parse()

	if *managementURL == "" || *setupKey == "" {
		fmt.Fprintln(os.Stderr, "Error: --management-url and --setup-key are required")
		flag.Usage()
		os.Exit(1)
	}

	if *numPeers < 2 {
		fmt.Fprintln(os.Stderr, "Error: --peers must be at least 2")
		os.Exit(1)
	}

	// Minimum packet size: 8 bytes for timestamp + 8 bytes for sequence number
	if *packetSize < 16 {
		fmt.Fprintln(os.Stderr, "Error: --packet-size must be at least 16")
		os.Exit(1)
	}

	if *forceRelay {
		os.Setenv("NB_FORCE_RELAY", "true")
	}
	os.Setenv("NB_USE_NETSTACK_MODE", "true")

	fmt.Println("=== NetBird Performance Test ===")
	fmt.Printf("Management URL: %s\n", *managementURL)
	fmt.Printf("Peers:          %d\n", *numPeers)
	fmt.Printf("Force relay:    %v\n", *forceRelay)
	fmt.Printf("Duration:       %s\n", *duration)
	fmt.Printf("Packet size:    %d bytes\n", *packetSize)
	fmt.Println()

	// Phase 1: Create peers
	fmt.Println("--- Phase 1: Creating peers ---")
	peers := make([]peerInfo, *numPeers)
	for i := 0; i < *numPeers; i++ {
		name := fmt.Sprintf("perf-peer-%d", i)
		port := 0
		c, err := embed.New(embed.Options{
			DeviceName:    name,
			SetupKey:      *setupKey,
			ManagementURL: *managementURL,
			WireguardPort: &port,
			LogLevel:      *logLevel,
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error creating peer %s: %v\n", name, err)
			os.Exit(1)
		}
		peers[i] = peerInfo{client: c, name: name}
		fmt.Printf("  Created %s\n", name)
	}

	// Phase 2: Start peers in parallel
	fmt.Println("\n--- Phase 2: Starting peers ---")
	startTime := time.Now()
	var wg sync.WaitGroup
	startErrors := make([]error, *numPeers)

	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), startTimeout)
			defer cancel()
			t := time.Now()
			if err := peers[idx].client.Start(ctx); err != nil {
				startErrors[idx] = err
				return
			}
			fmt.Printf("  %s started in %s\n", peers[idx].name, time.Since(t).Round(time.Millisecond))
		}(i)
	}
	wg.Wait()

	// Check for start errors
	var failed bool
	for i, err := range startErrors {
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error starting %s: %v\n", peers[i].name, err)
			failed = true
		}
	}
	if failed {
		cleanup(peers)
		os.Exit(1)
	}
	fmt.Printf("  All peers started in %s\n", time.Since(startTime).Round(time.Millisecond))

	// Get tunnel IPs
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error getting status for %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		ip := status.LocalPeerState.IP
		// Strip CIDR suffix if present (e.g. "100.64.0.1/16" -> "100.64.0.1")
		if idx := strings.Index(ip, "/"); idx != -1 {
			ip = ip[:idx]
		}
		peers[i].tunnelIP = ip
		fmt.Printf("  %s -> %s\n", peers[i].name, peers[i].tunnelIP)
	}

	// Phase 3: Wait for connections
	fmt.Println("\n--- Phase 3: Waiting for peer connections ---")
	connStart := time.Now()
	expectedPeers := *numPeers - 1
	deadline := time.After(connectTimeout)
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	allConnected := false
waitLoop:
	for {
		select {
		case <-deadline:
			fmt.Fprintf(os.Stderr, "  Timeout waiting for connections after %s\n", connectTimeout)
			printConnectionStatus(peers)
			cleanup(peers)
			os.Exit(1)
		case <-ticker.C:
			allConnected = true
			for i := range peers {
				connected := countConnectedPeers(peers[i].client)
				if connected < expectedPeers {
					allConnected = false
					break
				}
			}
			if allConnected {
				break waitLoop
			}
		}
	}

	fmt.Printf("  All peers connected in %s\n", time.Since(connStart).Round(time.Millisecond))
	printConnectionStatus(peers)

	// Phase 4: Traffic test
	fmt.Printf("\n--- Phase 4: Traffic test (%s) ---\n", *duration)

	// Start echo listeners on all peers
	listeners := make([]net.PacketConn, *numPeers)
	for i := range peers {
		conn, err := peers[i].client.ListenUDP(fmt.Sprintf(":%d", echoPort))
		if err != nil {
			fmt.Fprintf(os.Stderr, "  Error creating listener on %s: %v\n", peers[i].name, err)
			cleanup(peers)
			os.Exit(1)
		}
		listeners[i] = conn
		go echoServer(conn, *packetSize)
		fmt.Printf("  Echo listener started on %s:%d\n", peers[i].tunnelIP, echoPort)
	}

	// Run traffic between all pairs (i < j)
	var statsMu sync.Mutex
	var allStats []pairStats

	var trafficWg sync.WaitGroup
	for i := 0; i < *numPeers; i++ {
		for j := i + 1; j < *numPeers; j++ {
			trafficWg.Add(1)
			go func(from, to int) {
				defer trafficWg.Done()
				stats := runTraffic(peers[from].client, peers[from].name, peers[to].tunnelIP, peers[to].name, *duration, *packetSize)
				statsMu.Lock()
				allStats = append(allStats, stats)
				statsMu.Unlock()
			}(i, j)
		}
	}
	trafficWg.Wait()

	// Close listeners
	for _, l := range listeners {
		if l != nil {
			l.Close()
		}
	}

	// Phase 5: Report
	fmt.Println("\n--- Phase 5: Results ---")
	printReport(allStats, *packetSize)

	// Cleanup
	fmt.Println("\n--- Cleanup ---")
	cleanup(peers)
	fmt.Println("Done.")
}

func countConnectedPeers(c *embed.Client) int {
	status, err := c.Status()
	if err != nil {
		return 0
	}
	count := 0
	for _, p := range status.Peers {
		if p.ConnStatus == embed.PeerStatusConnected {
			count++
		}
	}
	return count
}

func printConnectionStatus(peers []peerInfo) {
	for i := range peers {
		status, err := peers[i].client.Status()
		if err != nil {
			fmt.Printf("  %s: error getting status: %v\n", peers[i].name, err)
			continue
		}
		connected := 0
		relayed := 0
		for _, p := range status.Peers {
			if p.ConnStatus == embed.PeerStatusConnected {
				connected++
				if p.Relayed {
					relayed++
				}
			}
		}
		connType := "direct"
		if relayed > 0 {
			connType = fmt.Sprintf("%d direct, %d relayed", connected-relayed, relayed)
		}
		fmt.Printf("  %s: %d/%d connected (%s)\n", peers[i].name, connected, len(status.Peers), connType)
	}
}

func echoServer(conn net.PacketConn, maxSize int) {
	buf := make([]byte, maxSize+100)
	for {
		n, addr, err := conn.ReadFrom(buf)
		if err != nil {
			return
		}
		_, _ = conn.WriteTo(buf[:n], addr)
	}
}

func runTraffic(client *embed.Client, fromName, toIP, toName string, duration time.Duration, packetSize int) pairStats {
	stats := pairStats{
		from: fromName,
		to:   toName,
	}

	ctx, cancel := context.WithTimeout(context.Background(), duration+10*time.Second)
	defer cancel()

	conn, err := client.Dial(ctx, "udp", fmt.Sprintf("%s:%d", toIP, echoPort))
	if err != nil {
		fmt.Fprintf(os.Stderr, "  Error dialing %s -> %s: %v\n", fromName, toName, err)
		return stats
	}
	defer conn.Close()

	deadline := time.Now().Add(duration)
	buf := make([]byte, packetSize)
	recvBuf := make([]byte, packetSize+100)
	var seq uint64

	for time.Now().Before(deadline) {
		seq++
		// Encode timestamp and sequence number
		binary.BigEndian.PutUint64(buf[0:8], uint64(time.Now().UnixNano()))
		binary.BigEndian.PutUint64(buf[8:16], seq)

		stats.sent++
		conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
		_, err := conn.Write(buf)
		if err != nil {
			stats.lost++
			continue
		}

		conn.SetReadDeadline(time.Now().Add(5 * time.Second))
		n, err := conn.Read(recvBuf)
		if err != nil {
			stats.lost++
			continue
		}

		if n >= 8 {
			sentNano := binary.BigEndian.Uint64(recvBuf[0:8])
			rtt := time.Since(time.Unix(0, int64(sentNano)))
			stats.received++
			stats.rtts = append(stats.rtts, rtt)
		} else {
			stats.received++
		}

		// Small sleep to avoid flooding
		time.Sleep(10 * time.Millisecond)
	}

	return stats
}

func printReport(allStats []pairStats, packetSize int) {
	if len(allStats) == 0 {
		fmt.Println("  No traffic data collected.")
		return
	}

	fmt.Printf("  %-30s %8s %8s %8s %8s %10s %10s %10s\n",
		"Pair", "Sent", "Recv", "Lost", "Loss%", "Avg RTT", "Min RTT", "Max RTT")
	fmt.Println("  " + strings.Repeat("-", 108))

	var totalSent, totalRecv, totalLost int64
	var totalRTTs []time.Duration

	for _, s := range allStats {
		avg, min, max, loss := s.summary()
		pair := fmt.Sprintf("%s -> %s", s.from, s.to)
		fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
			pair, s.sent, s.received, s.lost, loss,
			avg.Round(time.Microsecond), min.Round(time.Microsecond), max.Round(time.Microsecond))
		totalSent += s.sent
		totalRecv += s.received
		totalLost += s.lost
		totalRTTs = append(totalRTTs, s.rtts...)
	}

	fmt.Println("  " + strings.Repeat("-", 108))

	// Overall summary
	var overallLoss float64
	if totalSent > 0 {
		overallLoss = float64(totalLost) / float64(totalSent) * 100
	}

	var avgRTT, minRTT, maxRTT time.Duration
	if len(totalRTTs) > 0 {
		minRTT = totalRTTs[0]
		maxRTT = totalRTTs[0]
		var total time.Duration
		for _, rtt := range totalRTTs {
			total += rtt
			if rtt < minRTT {
				minRTT = rtt
			}
			if rtt > maxRTT {
				maxRTT = rtt
			}
		}
		avgRTT = total / time.Duration(len(totalRTTs))
	}

	fmt.Printf("  %-30s %8d %8d %8d %7.1f%% %10s %10s %10s\n",
		"TOTAL", totalSent, totalRecv, totalLost, overallLoss,
		avgRTT.Round(time.Microsecond), minRTT.Round(time.Microsecond), maxRTT.Round(time.Microsecond))

	// Throughput
	if len(totalRTTs) > 0 {
		fmt.Println()
		totalBytes := totalRecv * int64(packetSize)
		// Calculate stddev
		var sumSq float64
		avgNs := float64(avgRTT.Nanoseconds())
		for _, rtt := range totalRTTs {
			diff := float64(rtt.Nanoseconds()) - avgNs
			sumSq += diff * diff
		}
		stddev := time.Duration(math.Sqrt(sumSq / float64(len(totalRTTs))))

		fmt.Printf("  Total data transferred: %.2f KB\n", float64(totalBytes)/1024)
		fmt.Printf("  RTT stddev:             %s\n", stddev.Round(time.Microsecond))
		fmt.Printf("  Pairs tested:           %d\n", len(allStats))
	}
}

func cleanup(peers []peerInfo) {
	ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
	defer cancel()

	var wg sync.WaitGroup
	for i := range peers {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			if err := peers[idx].client.Stop(ctx); err != nil {
				fmt.Fprintf(os.Stderr, "  Error stopping %s: %v\n", peers[idx].name, err)
			} else {
				fmt.Printf("  Stopped %s\n", peers[idx].name)
			}
		}(i)
	}
	wg.Wait()
}

-  logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error)")

+  logLevel := flag.String("log-level", "panic", "Client log level (trace, debug, info, warn, error, panic)")


		c, err := embed.New(embed.Options{
			DeviceName:    name,
			SetupKey:      *setupKey,
			ManagementURL: *managementURL,
			WireguardPort: &port,
			LogLevel:      *logLevel,
		})

		c, err := embed.New(embed.Options{
			DeviceName:    name,
			SetupKey:      *setupKey,
			ManagementURL: *managementURL,
			WireguardPort: &port,
			LogLevel:      *logLevel,
			LogOutput:     io.Discard,
		})