mirror of
https://github.com/netbirdio/netbird.git
synced 2025-03-04 09:51:16 +01:00
* compile client under freebsd (#1620) Compile netbird client under freebsd and now support netstack and userspace modes. Refactoring linux specific code to share same code with FreeBSD, move to *_unix.go files. Not implemented yet: Kernel mode not supported DNS probably does not work yet Routing also probably does not work yet SSH support did not tested yet Lack of test environment for freebsd (dedicated VM for github runners under FreeBSD required) Lack of tests for freebsd specific code info reporting need to review and also implement, for example OS reported as GENERIC instead of FreeBSD (lack of FreeBSD icon in management interface) Lack of proper client setup under FreeBSD Lack of FreeBSD port/package * Add DNS routes (#1943) Given domains are resolved periodically and resolved IPs are replaced with the new ones. Unless the flag keep_route is set to true, then only new ones are added. This option is helpful if there are long-running connections that might still point to old IP addresses from changed DNS records. * Add process posture check (#1693) Introduces a process posture check to validate the existence and active status of specific binaries on peer systems. The check ensures that files are present at specified paths, and that corresponding processes are running. This check supports Linux, Windows, and macOS systems. Co-authored-by: Evgenii <mail@skillcoder.com> Co-authored-by: Pascal Fischer <pascal@netbird.io> Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com> Co-authored-by: Bethuel Mmbaga <bethuelmbaga12@gmail.com>
235 lines
6.7 KiB
Go
235 lines
6.7 KiB
Go
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || netbsd || dragonfly
|
|
|
|
package systemops
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gopacket/gopacket"
|
|
"github.com/gopacket/gopacket/layers"
|
|
"github.com/gopacket/gopacket/pcap"
|
|
"github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
nbnet "github.com/netbirdio/netbird/util/net"
|
|
)
|
|
|
|
type PacketExpectation struct {
|
|
SrcIP net.IP
|
|
DstIP net.IP
|
|
SrcPort int
|
|
DstPort int
|
|
UDP bool
|
|
TCP bool
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
destination string
|
|
expectedInterface string
|
|
dialer dialer
|
|
expectedPacket PacketExpectation
|
|
}
|
|
|
|
var testCases = []testCase{
|
|
{
|
|
name: "To external host without custom dialer via vpn",
|
|
destination: "192.0.2.1:53",
|
|
expectedInterface: expectedVPNint,
|
|
dialer: &net.Dialer{},
|
|
expectedPacket: createPacketExpectation("100.64.0.1", 12345, "192.0.2.1", 53),
|
|
},
|
|
{
|
|
name: "To external host with custom dialer via physical interface",
|
|
destination: "192.0.2.1:53",
|
|
expectedInterface: expectedExternalInt,
|
|
dialer: nbnet.NewDialer(),
|
|
expectedPacket: createPacketExpectation("192.168.0.1", 12345, "192.0.2.1", 53),
|
|
},
|
|
|
|
{
|
|
name: "To duplicate internal route with custom dialer via physical interface",
|
|
destination: "10.0.0.2:53",
|
|
expectedInterface: expectedInternalInt,
|
|
dialer: nbnet.NewDialer(),
|
|
expectedPacket: createPacketExpectation("192.168.1.1", 12345, "10.0.0.2", 53),
|
|
},
|
|
{
|
|
name: "To duplicate internal route without custom dialer via physical interface", // local route takes precedence
|
|
destination: "10.0.0.2:53",
|
|
expectedInterface: expectedInternalInt,
|
|
dialer: &net.Dialer{},
|
|
expectedPacket: createPacketExpectation("192.168.1.1", 12345, "10.0.0.2", 53),
|
|
},
|
|
|
|
{
|
|
name: "To unique vpn route with custom dialer via physical interface",
|
|
destination: "172.16.0.2:53",
|
|
expectedInterface: expectedExternalInt,
|
|
dialer: nbnet.NewDialer(),
|
|
expectedPacket: createPacketExpectation("192.168.0.1", 12345, "172.16.0.2", 53),
|
|
},
|
|
{
|
|
name: "To unique vpn route without custom dialer via vpn",
|
|
destination: "172.16.0.2:53",
|
|
expectedInterface: expectedVPNint,
|
|
dialer: &net.Dialer{},
|
|
expectedPacket: createPacketExpectation("100.64.0.1", 12345, "172.16.0.2", 53),
|
|
},
|
|
}
|
|
|
|
func TestRouting(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
setupTestEnv(t)
|
|
|
|
filter := createBPFFilter(tc.destination)
|
|
handle := startPacketCapture(t, tc.expectedInterface, filter)
|
|
|
|
sendTestPacket(t, tc.destination, tc.expectedPacket.SrcPort, tc.dialer)
|
|
|
|
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
|
|
packet, err := packetSource.NextPacket()
|
|
require.NoError(t, err)
|
|
|
|
verifyPacket(t, packet, tc.expectedPacket)
|
|
})
|
|
}
|
|
}
|
|
|
|
func createPacketExpectation(srcIP string, srcPort int, dstIP string, dstPort int) PacketExpectation {
|
|
return PacketExpectation{
|
|
SrcIP: net.ParseIP(srcIP),
|
|
DstIP: net.ParseIP(dstIP),
|
|
SrcPort: srcPort,
|
|
DstPort: dstPort,
|
|
UDP: true,
|
|
}
|
|
}
|
|
|
|
func startPacketCapture(t *testing.T, intf, filter string) *pcap.Handle {
|
|
t.Helper()
|
|
|
|
inactive, err := pcap.NewInactiveHandle(intf)
|
|
require.NoError(t, err, "Failed to create inactive pcap handle")
|
|
defer inactive.CleanUp()
|
|
|
|
err = inactive.SetSnapLen(1600)
|
|
require.NoError(t, err, "Failed to set snap length on inactive handle")
|
|
|
|
err = inactive.SetTimeout(time.Second * 10)
|
|
require.NoError(t, err, "Failed to set timeout on inactive handle")
|
|
|
|
err = inactive.SetImmediateMode(true)
|
|
require.NoError(t, err, "Failed to set immediate mode on inactive handle")
|
|
|
|
handle, err := inactive.Activate()
|
|
require.NoError(t, err, "Failed to activate pcap handle")
|
|
t.Cleanup(handle.Close)
|
|
|
|
err = handle.SetBPFFilter(filter)
|
|
require.NoError(t, err, "Failed to set BPF filter")
|
|
|
|
return handle
|
|
}
|
|
|
|
func sendTestPacket(t *testing.T, destination string, sourcePort int, dialer dialer) {
|
|
t.Helper()
|
|
|
|
if dialer == nil {
|
|
dialer = &net.Dialer{}
|
|
}
|
|
|
|
if sourcePort != 0 {
|
|
localUDPAddr := &net.UDPAddr{
|
|
IP: net.IPv4zero,
|
|
Port: sourcePort,
|
|
}
|
|
switch dialer := dialer.(type) {
|
|
case *nbnet.Dialer:
|
|
dialer.LocalAddr = localUDPAddr
|
|
case *net.Dialer:
|
|
dialer.LocalAddr = localUDPAddr
|
|
default:
|
|
t.Fatal("Unsupported dialer type")
|
|
}
|
|
}
|
|
|
|
msg := new(dns.Msg)
|
|
msg.Id = dns.Id()
|
|
msg.RecursionDesired = true
|
|
msg.Question = []dns.Question{
|
|
{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
|
}
|
|
|
|
conn, err := dialer.Dial("udp", destination)
|
|
require.NoError(t, err, "Failed to dial UDP")
|
|
defer conn.Close()
|
|
|
|
data, err := msg.Pack()
|
|
require.NoError(t, err, "Failed to pack DNS message")
|
|
|
|
_, err = conn.Write(data)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "required key not available") {
|
|
t.Logf("Ignoring WireGuard key error: %v", err)
|
|
return
|
|
}
|
|
t.Fatalf("Failed to send DNS query: %v", err)
|
|
}
|
|
}
|
|
|
|
func createBPFFilter(destination string) string {
|
|
host, port, err := net.SplitHostPort(destination)
|
|
if err != nil {
|
|
return fmt.Sprintf("udp and dst host %s and dst port %s", host, port)
|
|
}
|
|
return "udp"
|
|
}
|
|
|
|
func verifyPacket(t *testing.T, packet gopacket.Packet, exp PacketExpectation) {
|
|
t.Helper()
|
|
|
|
ipLayer := packet.Layer(layers.LayerTypeIPv4)
|
|
require.NotNil(t, ipLayer, "Expected IPv4 layer not found in packet")
|
|
|
|
ip, ok := ipLayer.(*layers.IPv4)
|
|
require.True(t, ok, "Failed to cast to IPv4 layer")
|
|
|
|
// Convert both source and destination IP addresses to 16-byte representation
|
|
expectedSrcIP := exp.SrcIP.To16()
|
|
actualSrcIP := ip.SrcIP.To16()
|
|
assert.Equal(t, expectedSrcIP, actualSrcIP, "Source IP mismatch")
|
|
|
|
expectedDstIP := exp.DstIP.To16()
|
|
actualDstIP := ip.DstIP.To16()
|
|
assert.Equal(t, expectedDstIP, actualDstIP, "Destination IP mismatch")
|
|
|
|
if exp.UDP {
|
|
udpLayer := packet.Layer(layers.LayerTypeUDP)
|
|
require.NotNil(t, udpLayer, "Expected UDP layer not found in packet")
|
|
|
|
udp, ok := udpLayer.(*layers.UDP)
|
|
require.True(t, ok, "Failed to cast to UDP layer")
|
|
|
|
assert.Equal(t, layers.UDPPort(exp.SrcPort), udp.SrcPort, "UDP source port mismatch")
|
|
assert.Equal(t, layers.UDPPort(exp.DstPort), udp.DstPort, "UDP destination port mismatch")
|
|
}
|
|
|
|
if exp.TCP {
|
|
tcpLayer := packet.Layer(layers.LayerTypeTCP)
|
|
require.NotNil(t, tcpLayer, "Expected TCP layer not found in packet")
|
|
|
|
tcp, ok := tcpLayer.(*layers.TCP)
|
|
require.True(t, ok, "Failed to cast to TCP layer")
|
|
|
|
assert.Equal(t, layers.TCPPort(exp.SrcPort), tcp.SrcPort, "TCP source port mismatch")
|
|
assert.Equal(t, layers.TCPPort(exp.DstPort), tcp.DstPort, "TCP destination port mismatch")
|
|
}
|
|
}
|