diff --git a/.github/workflows/golang-test-freebsd.yml b/.github/workflows/golang-test-freebsd.yml new file mode 100644 index 000000000..15fc6a729 --- /dev/null +++ b/.github/workflows/golang-test-freebsd.yml @@ -0,0 +1,39 @@ + +name: Test Code FreeBSD + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Test in FreeBSD + id: test + uses: vmactions/freebsd-vm@v1 + with: + usesh: true + prepare: | + pkg install -y curl + pkg install -y git + + run: | + set -x + curl -o go.tar.gz https://go.dev/dl/go1.21.11.freebsd-amd64.tar.gz -L + tar zxf go.tar.gz + mv go /usr/local/go + ln -s /usr/local/go/bin/go /usr/local/bin/go + go mod tidy + go test -timeout 5m -p 1 ./iface/... + go test -timeout 5m -p 1 ./client/... + cd client + go build . + cd .. \ No newline at end of file diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 28edd5d5a..9f95fbc27 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -58,9 +58,9 @@ var ( ) func TestEngine_SSH(t *testing.T) { - - if runtime.GOOS == "windows" { - t.Skip("skipping TestEngine_SSH on Windows") + // todo resolve test execution on freebsd + if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" { + t.Skip("skipping TestEngine_SSH") } key, err := wgtypes.GeneratePrivateKey() diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index 5d32032a5..1b226da29 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -436,7 +436,7 @@ func TestManagerUpdateRoutes(t *testing.T) { require.NoError(t, err, "should update routes") expectedWatchers := testCase.clientNetworkWatchersExpected - if (runtime.GOOS == "linux" || runtime.GOOS == "windows" || runtime.GOOS == "darwin") && testCase.clientNetworkWatchersExpectedAllowed != 0 { + if testCase.clientNetworkWatchersExpectedAllowed != 0 { expectedWatchers = testCase.clientNetworkWatchersExpectedAllowed } require.Len(t, routeManager.clientNetworks, expectedWatchers, "client networks size should match") diff --git a/client/internal/routemanager/systemops/systemops_bsd_test.go b/client/internal/routemanager/systemops/systemops_bsd_test.go index 2240b053c..ce9a9082a 100644 --- a/client/internal/routemanager/systemops/systemops_bsd_test.go +++ b/client/internal/routemanager/systemops/systemops_bsd_test.go @@ -3,12 +3,73 @@ package systemops import ( + "fmt" + "net" + "net/netip" + "os/exec" + "regexp" + "sync" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/net/route" ) +var expectedVPNint = "utun100" +var expectedExternalInt = "lo0" +var expectedInternalInt = "lo0" + +func init() { + testCases = append(testCases, []testCase{ + { + name: "To more specific route without custom dialer via vpn", + destination: "10.10.0.2:53", + expectedInterface: expectedVPNint, + dialer: &net.Dialer{}, + expectedPacket: createPacketExpectation("100.64.0.1", 12345, "10.10.0.2", 53), + }, + }...) +} + +func TestConcurrentRoutes(t *testing.T) { + baseIP := netip.MustParseAddr("192.0.2.0") + intf := &net.Interface{Name: "lo0"} + + r := NewSysOps(nil) + + var wg sync.WaitGroup + for i := 0; i < 1024; i++ { + wg.Add(1) + go func(ip netip.Addr) { + defer wg.Done() + prefix := netip.PrefixFrom(ip, 32) + if err := r.addToRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { + t.Errorf("Failed to add route for %s: %v", prefix, err) + } + }(baseIP) + baseIP = baseIP.Next() + } + + wg.Wait() + + baseIP = netip.MustParseAddr("192.0.2.0") + + for i := 0; i < 1024; i++ { + wg.Add(1) + go func(ip netip.Addr) { + defer wg.Done() + prefix := netip.PrefixFrom(ip, 32) + if err := r.removeFromRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { + t.Errorf("Failed to remove route for %s: %v", prefix, err) + } + }(baseIP) + baseIP = baseIP.Next() + } + + wg.Wait() +} + func TestBits(t *testing.T) { tests := []struct { name string @@ -55,3 +116,73 @@ func TestBits(t *testing.T) { }) } } + +func createAndSetupDummyInterface(t *testing.T, intf string, ipAddressCIDR string) string { + t.Helper() + + err := exec.Command("ifconfig", intf, "alias", ipAddressCIDR).Run() + require.NoError(t, err, "Failed to create loopback alias") + + t.Cleanup(func() { + err := exec.Command("ifconfig", intf, ipAddressCIDR, "-alias").Run() + assert.NoError(t, err, "Failed to remove loopback alias") + }) + + return "lo0" +} + +func addDummyRoute(t *testing.T, dstCIDR string, gw net.IP, _ string) { + t.Helper() + + var originalNexthop net.IP + if dstCIDR == "0.0.0.0/0" { + var err error + originalNexthop, err = fetchOriginalGateway() + if err != nil { + t.Logf("Failed to fetch original gateway: %v", err) + } + + if output, err := exec.Command("route", "delete", "-net", dstCIDR).CombinedOutput(); err != nil { + t.Logf("Failed to delete route: %v, output: %s", err, output) + } + } + + t.Cleanup(func() { + if originalNexthop != nil { + err := exec.Command("route", "add", "-net", dstCIDR, originalNexthop.String()).Run() + assert.NoError(t, err, "Failed to restore original route") + } + }) + + err := exec.Command("route", "add", "-net", dstCIDR, gw.String()).Run() + require.NoError(t, err, "Failed to add route") + + t.Cleanup(func() { + err := exec.Command("route", "delete", "-net", dstCIDR).Run() + assert.NoError(t, err, "Failed to remove route") + }) +} + +func fetchOriginalGateway() (net.IP, error) { + output, err := exec.Command("route", "-n", "get", "default").CombinedOutput() + if err != nil { + return nil, err + } + + matches := regexp.MustCompile(`gateway: (\S+)`).FindStringSubmatch(string(output)) + if len(matches) == 0 { + return nil, fmt.Errorf("gateway not found") + } + + return net.ParseIP(matches[1]), nil +} + +func setupDummyInterfacesAndRoutes(t *testing.T) { + t.Helper() + + defaultDummy := createAndSetupDummyInterface(t, expectedExternalInt, "192.168.0.1/24") + addDummyRoute(t, "0.0.0.0/0", net.IPv4(192, 168, 0, 1), defaultDummy) + + otherDummy := createAndSetupDummyInterface(t, expectedInternalInt, "192.168.1.1/24") + addDummyRoute(t, "10.0.0.0/8", net.IPv4(192, 168, 1, 1), otherDummy) +} diff --git a/client/internal/routemanager/systemops/systemops_darwin_test.go b/client/internal/routemanager/systemops/systemops_darwin_test.go deleted file mode 100644 index e42bd343d..000000000 --- a/client/internal/routemanager/systemops/systemops_darwin_test.go +++ /dev/null @@ -1,140 +0,0 @@ -//go:build !ios - -package systemops - -import ( - "fmt" - "net" - "net/netip" - "os/exec" - "regexp" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var expectedVPNint = "utun100" -var expectedExternalInt = "lo0" -var expectedInternalInt = "lo0" - -func init() { - testCases = append(testCases, []testCase{ - { - name: "To more specific route without custom dialer via vpn", - destination: "10.10.0.2:53", - expectedInterface: expectedVPNint, - dialer: &net.Dialer{}, - expectedPacket: createPacketExpectation("100.64.0.1", 12345, "10.10.0.2", 53), - }, - }...) -} - -func TestConcurrentRoutes(t *testing.T) { - baseIP := netip.MustParseAddr("192.0.2.0") - intf := &net.Interface{Name: "lo0"} - - r := NewSysOps(nil) - - var wg sync.WaitGroup - for i := 0; i < 1024; i++ { - wg.Add(1) - go func(ip netip.Addr) { - defer wg.Done() - prefix := netip.PrefixFrom(ip, 32) - if err := r.addToRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { - t.Errorf("Failed to add route for %s: %v", prefix, err) - } - }(baseIP) - baseIP = baseIP.Next() - } - - wg.Wait() - - baseIP = netip.MustParseAddr("192.0.2.0") - - for i := 0; i < 1024; i++ { - wg.Add(1) - go func(ip netip.Addr) { - defer wg.Done() - prefix := netip.PrefixFrom(ip, 32) - if err := r.removeFromRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { - t.Errorf("Failed to remove route for %s: %v", prefix, err) - } - }(baseIP) - baseIP = baseIP.Next() - } - - wg.Wait() -} - -func createAndSetupDummyInterface(t *testing.T, intf string, ipAddressCIDR string) string { - t.Helper() - - err := exec.Command("ifconfig", intf, "alias", ipAddressCIDR).Run() - require.NoError(t, err, "Failed to create loopback alias") - - t.Cleanup(func() { - err := exec.Command("ifconfig", intf, ipAddressCIDR, "-alias").Run() - assert.NoError(t, err, "Failed to remove loopback alias") - }) - - return "lo0" -} - -func addDummyRoute(t *testing.T, dstCIDR string, gw net.IP, _ string) { - t.Helper() - - var originalNexthop net.IP - if dstCIDR == "0.0.0.0/0" { - var err error - originalNexthop, err = fetchOriginalGateway() - if err != nil { - t.Logf("Failed to fetch original gateway: %v", err) - } - - if output, err := exec.Command("route", "delete", "-net", dstCIDR).CombinedOutput(); err != nil { - t.Logf("Failed to delete route: %v, output: %s", err, output) - } - } - - t.Cleanup(func() { - if originalNexthop != nil { - err := exec.Command("route", "add", "-net", dstCIDR, originalNexthop.String()).Run() - assert.NoError(t, err, "Failed to restore original route") - } - }) - - err := exec.Command("route", "add", "-net", dstCIDR, gw.String()).Run() - require.NoError(t, err, "Failed to add route") - - t.Cleanup(func() { - err := exec.Command("route", "delete", "-net", dstCIDR).Run() - assert.NoError(t, err, "Failed to remove route") - }) -} - -func fetchOriginalGateway() (net.IP, error) { - output, err := exec.Command("route", "-n", "get", "default").CombinedOutput() - if err != nil { - return nil, err - } - - matches := regexp.MustCompile(`gateway: (\S+)`).FindStringSubmatch(string(output)) - if len(matches) == 0 { - return nil, fmt.Errorf("gateway not found") - } - - return net.ParseIP(matches[1]), nil -} - -func setupDummyInterfacesAndRoutes(t *testing.T) { - t.Helper() - - defaultDummy := createAndSetupDummyInterface(t, expectedExternalInt, "192.168.0.1/24") - addDummyRoute(t, "0.0.0.0/0", net.IPv4(192, 168, 0, 1), defaultDummy) - - otherDummy := createAndSetupDummyInterface(t, expectedInternalInt, "192.168.1.1/24") - addDummyRoute(t, "10.0.0.0/8", net.IPv4(192, 168, 1, 1), otherDummy) -} diff --git a/client/internal/routemanager/systemops/systemops_generic_test.go b/client/internal/routemanager/systemops/systemops_generic_test.go index f7cb17477..c79b2ac64 100644 --- a/client/internal/routemanager/systemops/systemops_generic_test.go +++ b/client/internal/routemanager/systemops/systemops_generic_test.go @@ -49,6 +49,10 @@ func TestAddRemoveRoutes(t *testing.T) { } for n, testCase := range testCases { + // todo resolve test execution on freebsd + if runtime.GOOS == "freebsd" { + t.Skip("skipping ", testCase.name, " on freebsd") + } t.Run(testCase.name, func(t *testing.T) { t.Setenv("NB_DISABLE_ROUTE_CACHE", "true") @@ -107,6 +111,9 @@ func TestAddRemoveRoutes(t *testing.T) { } func TestGetNextHop(t *testing.T) { + if runtime.GOOS == "freebsd" { + t.Skip("skipping on freebsd") + } nexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) if err != nil { t.Fatal("shouldn't return error when fetching the gateway: ", err) @@ -300,19 +307,22 @@ func TestExistsInRouteTable(t *testing.T) { var addressPrefixes []netip.Prefix for _, address := range addresses { p := netip.MustParsePrefix(address.String()) - if p.Addr().Is6() { - continue - } - // Windows sometimes has hidden interface link local addrs that don't turn up on any interface - if runtime.GOOS == "windows" && p.Addr().IsLinkLocalUnicast() { - continue - } - // Linux loopback 127/8 is in the local table, not in the main table and always takes precedence - if runtime.GOOS == "linux" && p.Addr().IsLoopback() { - continue - } - addressPrefixes = append(addressPrefixes, p.Masked()) + switch { + case p.Addr().Is6(): + continue + // Windows sometimes has hidden interface link local addrs that don't turn up on any interface + case runtime.GOOS == "windows" && p.Addr().IsLinkLocalUnicast(): + continue + // Linux loopback 127/8 is in the local table, not in the main table and always takes precedence + case runtime.GOOS == "linux" && p.Addr().IsLoopback(): + continue + // FreeBSD loopback 127/8 is not added to the routing table + case runtime.GOOS == "freebsd" && p.Addr().IsLoopback(): + continue + default: + addressPrefixes = append(addressPrefixes, p.Masked()) + } } for _, prefix := range addressPrefixes { diff --git a/client/internal/routemanager/systemops/systemops_unix_test.go b/client/internal/routemanager/systemops/systemops_unix_test.go index fc964aa62..a6000d963 100644 --- a/client/internal/routemanager/systemops/systemops_unix_test.go +++ b/client/internal/routemanager/systemops/systemops_unix_test.go @@ -5,6 +5,7 @@ package systemops import ( "fmt" "net" + "runtime" "strings" "testing" "time" @@ -85,6 +86,10 @@ var testCases = []testCase{ func TestRouting(t *testing.T) { for _, tc := range testCases { + // todo resolve test execution on freebsd + if runtime.GOOS == "freebsd" { + t.Skip("skipping ", tc.name, " on freebsd") + } t.Run(tc.name, func(t *testing.T) { setupTestEnv(t) diff --git a/client/ui/route.go b/client/ui/route.go index cb4399254..70f72cf9b 100644 --- a/client/ui/route.go +++ b/client/ui/route.go @@ -1,4 +1,4 @@ -//go:build !(linux && 386) +//go:build !(linux && 386) && !freebsd package main