From e070f372942fb3f6db3f037936c28a61726dd789 Mon Sep 17 00:00:00 2001 From: Evgenii Date: Thu, 30 May 2024 12:39:43 +0200 Subject: [PATCH] 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 --- client/internal/dns/consts_freebsd.go | 6 + client/internal/dns/consts_linux.go | 8 + .../dns/{dbus_linux.go => dbus_unix.go} | 2 +- ...le_parser_linux.go => file_parser_unix.go} | 2 +- ...linux_test.go => file_parser_unix_test.go} | 2 +- ...le_repair_linux.go => file_repair_unix.go} | 2 +- ...linux_test.go => file_repair_unix_test.go} | 2 +- .../dns/{file_linux.go => file_unix.go} | 2 +- .../{file_linux_test.go => file_unix_test.go} | 2 +- .../dns/{host_linux.go => host_unix.go} | 16 +- ...nager_linux.go => network_manager_unix.go} | 2 +- ...resolvconf_linux.go => resolvconf_unix.go} | 2 +- .../dns/{server_linux.go => server_unix.go} | 2 +- client/internal/dns/systemd_freebsd.go | 20 ++ client/internal/dns/systemd_linux.go | 22 ++ ...down_linux.go => unclean_shutdown_unix.go} | 7 +- client/internal/engine.go | 4 +- .../internal/routemanager/routeflags_bsd.go | 18 ++ .../routemanager/routeflags_freebsd.go | 19 ++ client/internal/routemanager/systemops.go | 4 + client/internal/routemanager/systemops_bsd.go | 3 +- ...{systemops_darwin.go => systemops_unix.go} | 2 +- client/ssh/window_freebsd.go | 10 + client/system/info_freebsd.go | 12 +- client/ui/client_ui.go | 2 +- iface/freebsd/errors.go | 8 + iface/freebsd/iface.go | 108 ++++++++ iface/freebsd/iface_internal_test.go | 76 ++++++ iface/freebsd/link.go | 239 ++++++++++++++++++ iface/iface_create.go | 1 - iface/iface_darwin.go | 1 - iface/iface_ios.go | 1 - iface/{iface_linux.go => iface_unix.go} | 6 +- iface/module.go | 3 +- iface/module_freebsd.go | 18 ++ iface/name.go | 3 +- iface/name_darwin.go | 1 - ...tun_kernel_linux.go => tun_kernel_unix.go} | 86 ++----- iface/tun_link_freebsd.go | 80 ++++++ iface/tun_link_linux.go | 102 +++++++- iface/{tun_usp_linux.go => tun_usp_unix.go} | 39 +-- ...kernel.go => wg_configurer_kernel_unix.go} | 2 +- util/membership_unix.go | 2 +- version/url_freebsd.go | 6 + 44 files changed, 810 insertions(+), 145 deletions(-) create mode 100644 client/internal/dns/consts_freebsd.go create mode 100644 client/internal/dns/consts_linux.go rename client/internal/dns/{dbus_linux.go => dbus_unix.go} (96%) rename client/internal/dns/{file_parser_linux.go => file_parser_unix.go} (99%) rename client/internal/dns/{file_parser_linux_test.go => file_parser_unix_test.go} (99%) rename client/internal/dns/{file_repair_linux.go => file_repair_unix.go} (98%) rename client/internal/dns/{file_repair_linux_test.go => file_repair_unix_test.go} (98%) rename client/internal/dns/{file_linux.go => file_unix.go} (99%) rename client/internal/dns/{file_linux_test.go => file_unix_test.go} (98%) rename client/internal/dns/{host_linux.go => host_unix.go} (85%) rename client/internal/dns/{network_manager_linux.go => network_manager_unix.go} (99%) rename client/internal/dns/{resolvconf_linux.go => resolvconf_unix.go} (98%) rename client/internal/dns/{server_linux.go => server_unix.go} (76%) create mode 100644 client/internal/dns/systemd_freebsd.go rename client/internal/dns/{unclean_shutdown_linux.go => unclean_shutdown_unix.go} (94%) create mode 100644 client/internal/routemanager/routeflags_bsd.go create mode 100644 client/internal/routemanager/routeflags_freebsd.go rename client/internal/routemanager/{systemops_darwin.go => systemops_unix.go} (96%) create mode 100644 client/ssh/window_freebsd.go create mode 100644 iface/freebsd/errors.go create mode 100644 iface/freebsd/iface.go create mode 100644 iface/freebsd/iface_internal_test.go create mode 100644 iface/freebsd/link.go rename iface/{iface_linux.go => iface_unix.go} (89%) create mode 100644 iface/module_freebsd.go rename iface/{tun_kernel_linux.go => tun_kernel_unix.go} (64%) create mode 100644 iface/tun_link_freebsd.go rename iface/{tun_usp_linux.go => tun_usp_unix.go} (78%) rename iface/{wg_configurer_kernel.go => wg_configurer_kernel_unix.go} (99%) create mode 100644 version/url_freebsd.go diff --git a/client/internal/dns/consts_freebsd.go b/client/internal/dns/consts_freebsd.go new file mode 100644 index 00000000000..958eca8e55b --- /dev/null +++ b/client/internal/dns/consts_freebsd.go @@ -0,0 +1,6 @@ +package dns + +const ( + fileUncleanShutdownResolvConfLocation = "/var/db/netbird/resolv.conf" + fileUncleanShutdownManagerTypeLocation = "/var/db/netbird/manager" +) diff --git a/client/internal/dns/consts_linux.go b/client/internal/dns/consts_linux.go new file mode 100644 index 00000000000..32456a50fee --- /dev/null +++ b/client/internal/dns/consts_linux.go @@ -0,0 +1,8 @@ +//go:build !android + +package dns + +const ( + fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf" + fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager" +) diff --git a/client/internal/dns/dbus_linux.go b/client/internal/dns/dbus_unix.go similarity index 96% rename from client/internal/dns/dbus_linux.go rename to client/internal/dns/dbus_unix.go index b2604e9faea..ba1c07fae3e 100644 --- a/client/internal/dns/dbus_linux.go +++ b/client/internal/dns/dbus_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_parser_linux.go b/client/internal/dns/file_parser_unix.go similarity index 99% rename from client/internal/dns/file_parser_linux.go rename to client/internal/dns/file_parser_unix.go index 02f6d03a587..130c8821454 100644 --- a/client/internal/dns/file_parser_linux.go +++ b/client/internal/dns/file_parser_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_parser_linux_test.go b/client/internal/dns/file_parser_unix_test.go similarity index 99% rename from client/internal/dns/file_parser_linux_test.go rename to client/internal/dns/file_parser_unix_test.go index 4263d4063e4..1d6e64683ce 100644 --- a/client/internal/dns/file_parser_linux_test.go +++ b/client/internal/dns/file_parser_unix_test.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_repair_linux.go b/client/internal/dns/file_repair_unix.go similarity index 98% rename from client/internal/dns/file_repair_linux.go rename to client/internal/dns/file_repair_unix.go index cbdda5e9e17..ae2c33b8684 100644 --- a/client/internal/dns/file_repair_linux.go +++ b/client/internal/dns/file_repair_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_repair_linux_test.go b/client/internal/dns/file_repair_unix_test.go similarity index 98% rename from client/internal/dns/file_repair_linux_test.go rename to client/internal/dns/file_repair_unix_test.go index 4e27f46ba41..4dba79e996d 100644 --- a/client/internal/dns/file_repair_linux_test.go +++ b/client/internal/dns/file_repair_unix_test.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_linux.go b/client/internal/dns/file_unix.go similarity index 99% rename from client/internal/dns/file_linux.go rename to client/internal/dns/file_unix.go index b9d6d699da9..624e089cb48 100644 --- a/client/internal/dns/file_linux.go +++ b/client/internal/dns/file_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_linux_test.go b/client/internal/dns/file_unix_test.go similarity index 98% rename from client/internal/dns/file_linux_test.go rename to client/internal/dns/file_unix_test.go index 902791b3600..46726536ece 100644 --- a/client/internal/dns/file_linux_test.go +++ b/client/internal/dns/file_unix_test.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/host_linux.go b/client/internal/dns/host_unix.go similarity index 85% rename from client/internal/dns/host_linux.go rename to client/internal/dns/host_unix.go index cb246bcfee2..72b8f6c6e6b 100644 --- a/client/internal/dns/host_linux.go +++ b/client/internal/dns/host_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns @@ -108,7 +108,7 @@ func getOSDNSManagerType() (osManagerType, error) { if strings.Contains(text, "NetworkManager") && isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() { return networkManager, nil } - if strings.Contains(text, "systemd-resolved") && isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) { + if strings.Contains(text, "systemd-resolved") && isSystemdResolvedRunning() { if checkStub() { return systemdManager, nil } else { @@ -116,16 +116,10 @@ func getOSDNSManagerType() (osManagerType, error) { } } if strings.Contains(text, "resolvconf") { - if isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) { - var value string - err = getSystemdDbusProperty(systemdDbusResolvConfModeProperty, &value) - if err == nil { - if value == systemdDbusResolvConfModeForeign { - return systemdManager, nil - } - } - log.Errorf("got an error while checking systemd resolv conf mode, error: %s", err) + if isSystemdResolveConfMode() { + return systemdManager, nil } + return resolvConfManager, nil } } diff --git a/client/internal/dns/network_manager_linux.go b/client/internal/dns/network_manager_unix.go similarity index 99% rename from client/internal/dns/network_manager_linux.go rename to client/internal/dns/network_manager_unix.go index dfd4cf4d3e1..184047a643d 100644 --- a/client/internal/dns/network_manager_linux.go +++ b/client/internal/dns/network_manager_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/resolvconf_linux.go b/client/internal/dns/resolvconf_unix.go similarity index 98% rename from client/internal/dns/resolvconf_linux.go rename to client/internal/dns/resolvconf_unix.go index 72db5faf135..0c17626c7a9 100644 --- a/client/internal/dns/resolvconf_linux.go +++ b/client/internal/dns/resolvconf_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/server_linux.go b/client/internal/dns/server_unix.go similarity index 76% rename from client/internal/dns/server_linux.go rename to client/internal/dns/server_unix.go index aeb24b51144..45542562585 100644 --- a/client/internal/dns/server_linux.go +++ b/client/internal/dns/server_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/systemd_freebsd.go b/client/internal/dns/systemd_freebsd.go new file mode 100644 index 00000000000..0de805337d9 --- /dev/null +++ b/client/internal/dns/systemd_freebsd.go @@ -0,0 +1,20 @@ +package dns + +import ( + "errors" + "fmt" +) + +var errNotImplemented = errors.New("not implemented") + +func newSystemdDbusConfigurator(wgInterface string) (hostManager, error) { + return nil, fmt.Errorf("systemd dns management: %w on freebsd", errNotImplemented) +} + +func isSystemdResolvedRunning() bool { + return false +} + +func isSystemdResolveConfMode() bool { + return false +} diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index 27a93fbe114..e2fa5b71ae3 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -242,3 +242,25 @@ func getSystemdDbusProperty(property string, store any) error { return v.Store(store) } + +func isSystemdResolvedRunning() bool { + return isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) +} + +func isSystemdResolveConfMode() bool { + if !isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) { + return false + } + + var value string + if err := getSystemdDbusProperty(systemdDbusResolvConfModeProperty, &value); err != nil { + log.Errorf("got an error while checking systemd resolv conf mode, error: %s", err) + return false + } + + if value == systemdDbusResolvConfModeForeign { + return true + } + + return false +} diff --git a/client/internal/dns/unclean_shutdown_linux.go b/client/internal/dns/unclean_shutdown_unix.go similarity index 94% rename from client/internal/dns/unclean_shutdown_linux.go rename to client/internal/dns/unclean_shutdown_unix.go index afd58772080..8a32090c34d 100644 --- a/client/internal/dns/unclean_shutdown_linux.go +++ b/client/internal/dns/unclean_shutdown_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns @@ -14,11 +14,6 @@ import ( log "github.com/sirupsen/logrus" ) -const ( - fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf" - fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager" -) - func CheckUncleanShutdown(wgIface string) error { if _, err := os.Stat(fileUncleanShutdownResolvConfLocation); err != nil { if errors.Is(err, fs.ErrNotExist) { diff --git a/client/internal/engine.go b/client/internal/engine.go index 9c96dc50def..669fdaaff29 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -546,8 +546,8 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error { } else { if sshConf.GetSshEnabled() { - if runtime.GOOS == "windows" { - log.Warnf("running SSH server on Windows is not supported") + if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" { + log.Warnf("running SSH server on %s is not supported", runtime.GOOS) return nil } // start SSH server if it wasn't running diff --git a/client/internal/routemanager/routeflags_bsd.go b/client/internal/routemanager/routeflags_bsd.go new file mode 100644 index 00000000000..b39079e61b9 --- /dev/null +++ b/client/internal/routemanager/routeflags_bsd.go @@ -0,0 +1,18 @@ +//go:build darwin || dragonfly || netbsd || openbsd + +package routemanager + +import "syscall" + +// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags. +func filterRoutesByFlags(routeMessageFlags int) bool { + if routeMessageFlags&syscall.RTF_UP == 0 { + return true + } + + if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE|syscall.RTF_WASCLONED) != 0 { + return true + } + + return false +} diff --git a/client/internal/routemanager/routeflags_freebsd.go b/client/internal/routemanager/routeflags_freebsd.go new file mode 100644 index 00000000000..259253a765f --- /dev/null +++ b/client/internal/routemanager/routeflags_freebsd.go @@ -0,0 +1,19 @@ +//go:build: freebsd +package routemanager + +import "syscall" + +// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags. +func filterRoutesByFlags(routeMessageFlags int) bool { + if routeMessageFlags&syscall.RTF_UP == 0 { + return true + } + + // NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0 (https://www.freebsd.org/releases/8.0R/relnotes-detailed/) + // a concept of cloned route (a route generated by an entry with RTF_CLONING flag) is deprecated. + if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE) != 0 { + return true + } + + return false +} diff --git a/client/internal/routemanager/systemops.go b/client/internal/routemanager/systemops.go index bc506411c4c..f349402d15c 100644 --- a/client/internal/routemanager/systemops.go +++ b/client/internal/routemanager/systemops.go @@ -82,6 +82,10 @@ func GetNextHop(ip netip.Addr) (netip.Addr, *net.Interface, error) { log.Debugf("Route for %s: interface %v nexthop %v, preferred source %v", ip, intf, gateway, preferredSrc) if gateway == nil { + if runtime.GOOS == "freebsd" { + return netip.Addr{}, intf, nil + } + if preferredSrc == nil { return netip.Addr{}, nil, ErrRouteNotFound } diff --git a/client/internal/routemanager/systemops_bsd.go b/client/internal/routemanager/systemops_bsd.go index a3548a1f182..45dbe525636 100644 --- a/client/internal/routemanager/systemops_bsd.go +++ b/client/internal/routemanager/systemops_bsd.go @@ -43,8 +43,7 @@ func getRoutesFromTable() ([]netip.Prefix, error) { return nil, fmt.Errorf("unexpected RIB message type: %d", m.Type) } - if m.Flags&syscall.RTF_UP == 0 || - m.Flags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE|syscall.RTF_WASCLONED) != 0 { + if filterRoutesByFlags(m.Flags) { continue } diff --git a/client/internal/routemanager/systemops_darwin.go b/client/internal/routemanager/systemops_unix.go similarity index 96% rename from client/internal/routemanager/systemops_darwin.go rename to client/internal/routemanager/systemops_unix.go index ee4196a0ca5..e54924d65f9 100644 --- a/client/internal/routemanager/systemops_darwin.go +++ b/client/internal/routemanager/systemops_unix.go @@ -1,4 +1,4 @@ -//go:build darwin && !ios +//go:build (darwin && !ios) || dragonfly || freebsd || netbsd || openbsd package routemanager diff --git a/client/ssh/window_freebsd.go b/client/ssh/window_freebsd.go new file mode 100644 index 00000000000..ef4848341c6 --- /dev/null +++ b/client/ssh/window_freebsd.go @@ -0,0 +1,10 @@ +//go:build freebsd + +package ssh + +import ( + "os" +) + +func setWinSize(file *os.File, width, height int) { +} diff --git a/client/system/info_freebsd.go b/client/system/info_freebsd.go index b44fdee7c4a..148fbcb6b3e 100644 --- a/client/system/info_freebsd.go +++ b/client/system/info_freebsd.go @@ -1,15 +1,18 @@ +//go:build freebsd + package system import ( "bytes" "context" - "fmt" "os" "os/exec" "runtime" "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/system/detect_cloud" "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" @@ -22,8 +25,8 @@ func GetInfo(ctx context.Context) *Info { out = _getInfo() time.Sleep(500 * time.Millisecond) } - osStr := strings.Replace(out, "\n", "", -1) - osStr = strings.Replace(osStr, "\r\n", "", -1) + osStr := strings.ReplaceAll(out, "\n", "") + osStr = strings.ReplaceAll(osStr, "\r\n", "") osInfo := strings.Split(osStr, " ") env := Environment{ @@ -50,7 +53,8 @@ func _getInfo() string { cmd.Stderr = &stderr err := cmd.Run() if err != nil { - fmt.Println("getInfo:", err) + log.Warnf("getInfo: %s", err) } + return out.String() } diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 7b1e0320a89..75f1586018b 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -1,4 +1,4 @@ -//go:build !(linux && 386) +//go:build !(linux && 386) && !freebsd package main diff --git a/iface/freebsd/errors.go b/iface/freebsd/errors.go new file mode 100644 index 00000000000..e2c6a2aa990 --- /dev/null +++ b/iface/freebsd/errors.go @@ -0,0 +1,8 @@ +package freebsd + +import "errors" + +var ( + ErrDoesNotExist = errors.New("does not exist") + ErrNameDoesNotMatch = errors.New("name does not match") +) diff --git a/iface/freebsd/iface.go b/iface/freebsd/iface.go new file mode 100644 index 00000000000..d32fa6436f3 --- /dev/null +++ b/iface/freebsd/iface.go @@ -0,0 +1,108 @@ +package freebsd + +import ( + "bufio" + "fmt" + "strconv" + "strings" +) + +type iface struct { + Name string + MTU int + Group string + IPAddrs []string +} + +func parseError(output []byte) error { + // TODO: implement without allocations + lines := string(output) + + if strings.Contains(lines, "does not exist") { + return ErrDoesNotExist + } + + return nil +} + +func parseIfconfigOutput(output []byte) (*iface, error) { + // TODO: implement without allocations + lines := string(output) + + scanner := bufio.NewScanner(strings.NewReader(lines)) + + var name, mtu, group string + var ips []string + + for scanner.Scan() { + line := scanner.Text() + + // If line contains ": flags", it's a line with interface information + if strings.Contains(line, ": flags") { + parts := strings.Fields(line) + if len(parts) < 4 { + return nil, fmt.Errorf("failed to parse line: %s", line) + } + name = strings.TrimSuffix(parts[0], ":") + if strings.Contains(line, "mtu") { + mtuIndex := 0 + for i, part := range parts { + if part == "mtu" { + mtuIndex = i + break + } + } + mtu = parts[mtuIndex+1] + } + } + + // If line contains "groups:", it's a line with interface group + if strings.Contains(line, "groups:") { + parts := strings.Fields(line) + if len(parts) < 2 { + return nil, fmt.Errorf("failed to parse line: %s", line) + } + group = parts[1] + } + + // If line contains "inet ", it's a line with IP address + if strings.Contains(line, "inet ") { + parts := strings.Fields(line) + if len(parts) < 2 { + return nil, fmt.Errorf("failed to parse line: %s", line) + } + ips = append(ips, parts[1]) + } + } + + if name == "" { + return nil, fmt.Errorf("interface name not found in ifconfig output") + } + + mtuInt, err := strconv.Atoi(mtu) + if err != nil { + return nil, fmt.Errorf("failed to parse MTU: %w", err) + } + + return &iface{ + Name: name, + MTU: mtuInt, + Group: group, + IPAddrs: ips, + }, nil +} + +func parseIFName(output []byte) (string, error) { + // TODO: implement without allocations + lines := strings.Split(string(output), "\n") + if len(lines) == 0 || lines[0] == "" { + return "", fmt.Errorf("no output returned") + } + + fields := strings.Fields(lines[0]) + if len(fields) > 1 { + return "", fmt.Errorf("invalid output") + } + + return fields[0], nil +} diff --git a/iface/freebsd/iface_internal_test.go b/iface/freebsd/iface_internal_test.go new file mode 100644 index 00000000000..f933ae63439 --- /dev/null +++ b/iface/freebsd/iface_internal_test.go @@ -0,0 +1,76 @@ +package freebsd + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseIfconfigOutput(t *testing.T) { + testOutput := `wg1: flags=8080 metric 0 mtu 1420 + options=80000 + groups: wg + nd6 options=109` + + expected := &iface{ + Name: "wg1", + MTU: 1420, + Group: "wg", + } + + result, err := parseIfconfigOutput(([]byte)(testOutput)) + if err != nil { + t.Errorf("Error parsing ifconfig output: %v", err) + return + } + + assert.Equal(t, expected.Name, result.Name, "Name should match") + assert.Equal(t, expected.MTU, result.MTU, "MTU should match") + assert.Equal(t, expected.Group, result.Group, "Group should match") +} + +func TestParseIFName(t *testing.T) { + tests := []struct { + name string + output string + expected string + expectedErr error + }{ + { + name: "ValidOutput", + output: "eth0\n", + expected: "eth0", + }, + { + name: "ValidOutputOneLine", + output: "eth0", + expected: "eth0", + }, + { + name: "EmptyOutput", + output: "", + expectedErr: fmt.Errorf("no output returned"), + }, + { + name: "InvalidOutput", + output: "This is an invalid output\n", + expectedErr: fmt.Errorf("invalid output"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := parseIFName(([]byte)(test.output)) + + assert.Equal(t, test.expected, result, "Interface names should match") + + if test.expectedErr != nil { + assert.NotNil(t, err, "Error should not be nil") + assert.EqualError(t, err, test.expectedErr.Error(), "Error messages should match") + } else { + assert.Nil(t, err, "Error should be nil") + } + }) + } +} diff --git a/iface/freebsd/link.go b/iface/freebsd/link.go new file mode 100644 index 00000000000..b7924f04b04 --- /dev/null +++ b/iface/freebsd/link.go @@ -0,0 +1,239 @@ +package freebsd + +import ( + "bytes" + "errors" + "fmt" + "os/exec" + "strconv" + + log "github.com/sirupsen/logrus" +) + +const wgIFGroup = "wg" + +// Link represents a network interface. +type Link struct { + name string +} + +func NewLink(name string) *Link { + return &Link{ + name: name, + } +} + +// LinkByName retrieves a network interface by its name. +func LinkByName(name string) (*Link, error) { + out, err := exec.Command("ifconfig", name).CombinedOutput() + if err != nil { + if pErr := parseError(out); pErr != nil { + return nil, pErr + } + + log.Debugf("ifconfig out: %s", out) + + return nil, fmt.Errorf("command run: %w", err) + } + + i, err := parseIfconfigOutput(out) + if err != nil { + return nil, fmt.Errorf("parse ifconfig output: %w", err) + } + + if i.Name != name { + return nil, ErrNameDoesNotMatch + } + + return &Link{name: i.Name}, nil +} + +// Recreate - create new interface, remove current before create if it exists +func (l *Link) Recreate() error { + ok, err := l.isExist() + if err != nil { + return fmt.Errorf("is exist: %w", err) + } + + if ok { + if err := l.del(l.name); err != nil { + return fmt.Errorf("del: %w", err) + } + } + + return l.Add() +} + +// Add creates a new network interface. +func (l *Link) Add() error { + parsedName, err := l.create(wgIFGroup) + if err != nil { + return fmt.Errorf("create link: %w", err) + } + + if parsedName == l.name { + return nil + } + + parsedName, err = l.rename(parsedName, l.name) + if err != nil { + errDel := l.del(parsedName) + if errDel != nil { + return fmt.Errorf("del on rename link: %w: %w", err, errDel) + } + + return fmt.Errorf("rename link: %w", err) + } + + return nil +} + +// Del removes an existing network interface. +func (l *Link) Del() error { + return l.del(l.name) +} + +// SetMTU sets the MTU of the network interface. +func (l *Link) SetMTU(mtu int) error { + return l.setMTU(mtu) +} + +// AssignAddr assigns an IP address and netmask to the network interface. +func (l *Link) AssignAddr(ip, netmask string) error { + return l.setAddr(ip, netmask) +} + +func (l *Link) Up() error { + return l.up(l.name) +} + +func (l *Link) Down() error { + return l.down(l.name) +} + +func (l *Link) isExist() (bool, error) { + _, err := LinkByName(l.name) + if errors.Is(err, ErrDoesNotExist) { + return false, nil + } + + if err != nil { + return false, fmt.Errorf("link by name: %w", err) + } + + return true, nil +} + +func (l *Link) create(groupName string) (string, error) { + cmd := exec.Command("ifconfig", groupName, "create") + + output, err := cmd.CombinedOutput() + if err != nil { + log.Debugf("ifconfig out: %s", output) + + return "", fmt.Errorf("create %s interface: %w", groupName, err) + } + + interfaceName, err := parseIFName(output) + if err != nil { + return "", fmt.Errorf("parse interface name: %w", err) + } + + return interfaceName, nil +} + +func (l *Link) rename(oldName, newName string) (string, error) { + cmd := exec.Command("ifconfig", oldName, "name", newName) + + output, err := cmd.CombinedOutput() + if err != nil { + log.Debugf("ifconfig out: %s", output) + + return "", fmt.Errorf("change name %q -> %q: %w", oldName, newName, err) + } + + interfaceName, err := parseIFName(output) + if err != nil { + return "", fmt.Errorf("parse new name: %w", err) + } + + return interfaceName, nil +} + +func (l *Link) del(name string) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", name, "destroy") + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("destroy %s interface: %w", name, err) + } + + return nil +} + +func (l *Link) setMTU(mtu int) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", l.name, "mtu", strconv.Itoa(mtu)) + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("set interface mtu: %w", err) + } + + return nil +} + +func (l *Link) setAddr(ip, netmask string) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", l.name, "inet", ip, "netmask", netmask) + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("set interface addr: %w", err) + } + + return nil +} + +func (l *Link) up(name string) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", name, "up") + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("up %s interface: %w", name, err) + } + + return nil +} + +func (l *Link) down(name string) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", name, "down") + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("down %s interface: %w", name, err) + } + + return nil +} diff --git a/iface/iface_create.go b/iface/iface_create.go index 86c3f320fa3..cfc555f2ea8 100644 --- a/iface/iface_create.go +++ b/iface/iface_create.go @@ -1,5 +1,4 @@ //go:build !android -// +build !android package iface diff --git a/iface/iface_darwin.go b/iface/iface_darwin.go index 4d62c6af69c..d68f562cd2c 100644 --- a/iface/iface_darwin.go +++ b/iface/iface_darwin.go @@ -1,5 +1,4 @@ //go:build !ios -// +build !ios package iface diff --git a/iface/iface_ios.go b/iface/iface_ios.go index b22e1a6a418..39032e6bdb5 100644 --- a/iface/iface_ios.go +++ b/iface/iface_ios.go @@ -1,5 +1,4 @@ //go:build ios -// +build ios package iface diff --git a/iface/iface_linux.go b/iface/iface_unix.go similarity index 89% rename from iface/iface_linux.go rename to iface/iface_unix.go index 62ae0f0deaf..b378abef3c9 100644 --- a/iface/iface_linux.go +++ b/iface/iface_unix.go @@ -1,10 +1,10 @@ -//go:build !android -// +build !android +//go:build (linux && !android) || freebsd package iface import ( "fmt" + "runtime" "github.com/pion/transport/v3" @@ -43,5 +43,5 @@ func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string, // CreateOnAndroid this function make sense on mobile only func (w *WGIface) CreateOnAndroid([]string, string, []string) error { - return fmt.Errorf("this function has not implemented on this platform") + return fmt.Errorf("CreateOnAndroid function has not implemented on %s platform", runtime.GOOS) } diff --git a/iface/module.go b/iface/module.go index 7f337201d17..ca70cf3c7de 100644 --- a/iface/module.go +++ b/iface/module.go @@ -1,5 +1,4 @@ -//go:build !linux || android -// +build !linux android +//go:build (!linux && !freebsd) || android package iface diff --git a/iface/module_freebsd.go b/iface/module_freebsd.go new file mode 100644 index 00000000000..00ad882c29a --- /dev/null +++ b/iface/module_freebsd.go @@ -0,0 +1,18 @@ +package iface + +// WireGuardModuleIsLoaded check if kernel support wireguard +func WireGuardModuleIsLoaded() bool { + // Despite the fact FreeBSD natively support Wireguard (https://github.com/WireGuard/wireguard-freebsd) + // we are currently do not use it, since it is required to add wireguard kernel support to + // - https://github.com/netbirdio/netbird/tree/main/sharedsock + // - https://github.com/mdlayher/socket + // TODO: implement kernel space + return false +} + +// tunModuleIsLoaded check if tun module exist, if is not attempt to load it +func tunModuleIsLoaded() bool { + // Assume tun supported by freebsd kernel by default + // TODO: implement check for module loaded in kernel or build-it + return true +} diff --git a/iface/name.go b/iface/name.go index 05d0299d3ff..706cb65ad3c 100644 --- a/iface/name.go +++ b/iface/name.go @@ -1,5 +1,4 @@ -//go:build linux || windows -// +build linux windows +//go:build linux || windows || freebsd package iface diff --git a/iface/name_darwin.go b/iface/name_darwin.go index c80f790f5e7..a4016ce153b 100644 --- a/iface/name_darwin.go +++ b/iface/name_darwin.go @@ -1,5 +1,4 @@ //go:build darwin -// +build darwin package iface diff --git a/iface/tun_kernel_linux.go b/iface/tun_kernel_unix.go similarity index 64% rename from iface/tun_kernel_linux.go rename to iface/tun_kernel_unix.go index 12adcdf7378..db47b68cf30 100644 --- a/iface/tun_kernel_linux.go +++ b/iface/tun_kernel_unix.go @@ -1,4 +1,4 @@ -//go:build linux && !android +//go:build (linux && !android) || freebsd package iface @@ -6,11 +6,9 @@ import ( "context" "fmt" "net" - "os" "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" - "github.com/vishvananda/netlink" "github.com/netbirdio/netbird/iface/bind" "github.com/netbirdio/netbird/sharedsock" @@ -32,6 +30,8 @@ type tunKernelDevice struct { } func newTunDevice(name string, address WGAddress, wgPort int, key string, mtu int, transportNet transport.Net) wgTunDevice { + checkUser() + ctx, cancel := context.WithCancel(context.Background()) return &tunKernelDevice{ ctx: ctx, @@ -48,53 +48,29 @@ func newTunDevice(name string, address WGAddress, wgPort int, key string, mtu in func (t *tunKernelDevice) Create() (wgConfigurer, error) { link := newWGLink(t.name) - // check if interface exists - l, err := netlink.LinkByName(t.name) - if err != nil { - switch err.(type) { - case netlink.LinkNotFoundError: - break - default: - return nil, err - } - } - - // remove if interface exists - if l != nil { - err = netlink.LinkDel(link) - if err != nil { - return nil, err - } - } - - log.Debugf("adding device: %s", t.name) - err = netlink.LinkAdd(link) - if os.IsExist(err) { - log.Infof("interface %s already exists. Will reuse.", t.name) - } else if err != nil { - return nil, err + if err := link.recreate(); err != nil { + return nil, fmt.Errorf("recreate: %w", err) } t.link = link - err = t.assignAddr() - if err != nil { - return nil, err + if err := t.assignAddr(); err != nil { + return nil, fmt.Errorf("assign addr: %w", err) } - // todo do a discovery + // TODO: do a MTU discovery log.Debugf("setting MTU: %d interface: %s", t.mtu, t.name) - err = netlink.LinkSetMTU(link, t.mtu) - if err != nil { - log.Errorf("error setting MTU on interface: %s", t.name) - return nil, err + + if err := link.setMTU(t.mtu); err != nil { + return nil, fmt.Errorf("set mtu: %w", err) } configurer := newWGConfigurer(t.name) - err = configurer.configureInterface(t.key, t.wgPort) - if err != nil { + + if err := configurer.configureInterface(t.key, t.wgPort); err != nil { return nil, err } + return configurer, nil } @@ -108,9 +84,10 @@ func (t *tunKernelDevice) Up() (*bind.UniversalUDPMuxDefault, error) { } log.Debugf("bringing up interface: %s", t.name) - err := netlink.LinkSetUp(t.link) - if err != nil { + + if err := t.link.up(); err != nil { log.Errorf("error bringing up interface: %s", t.name) + return nil, err } @@ -178,32 +155,5 @@ func (t *tunKernelDevice) Wrapper() *DeviceWrapper { // assignAddr Adds IP address to the tunnel interface func (t *tunKernelDevice) assignAddr() error { - link := newWGLink(t.name) - - //delete existing addresses - list, err := netlink.AddrList(link, 0) - if err != nil { - return err - } - if len(list) > 0 { - for _, a := range list { - addr := a - err = netlink.AddrDel(link, &addr) - if err != nil { - return err - } - } - } - - log.Debugf("adding address %s to interface: %s", t.address.String(), t.name) - addr, _ := netlink.ParseAddr(t.address.String()) - err = netlink.AddrAdd(link, addr) - if os.IsExist(err) { - log.Infof("interface %s already has the address: %s", t.name, t.address.String()) - } else if err != nil { - return err - } - // On linux, the link must be brought up - err = netlink.LinkSetUp(link) - return err + return t.link.assignAddr(t.address) } diff --git a/iface/tun_link_freebsd.go b/iface/tun_link_freebsd.go new file mode 100644 index 00000000000..be7921fdb5e --- /dev/null +++ b/iface/tun_link_freebsd.go @@ -0,0 +1,80 @@ +package iface + +import ( + "fmt" + + "github.com/netbirdio/netbird/iface/freebsd" + log "github.com/sirupsen/logrus" +) + +type wgLink struct { + name string + link *freebsd.Link +} + +func newWGLink(name string) *wgLink { + link := freebsd.NewLink(name) + + return &wgLink{ + name: name, + link: link, + } +} + +// Type returns the interface type +func (l *wgLink) Type() string { + return "wireguard" +} + +// Close deletes the link interface +func (l *wgLink) Close() error { + return l.link.Del() +} + +func (l *wgLink) recreate() error { + if err := l.link.Recreate(); err != nil { + return fmt.Errorf("recreate: %w", err) + } + + return nil +} + +func (l *wgLink) setMTU(mtu int) error { + if err := l.link.SetMTU(mtu); err != nil { + return fmt.Errorf("set mtu: %w", err) + } + + return nil +} + +func (l *wgLink) up() error { + if err := l.link.Up(); err != nil { + return fmt.Errorf("up: %w", err) + } + + return nil +} + +func (l *wgLink) assignAddr(address WGAddress) error { + link, err := freebsd.LinkByName(l.name) + if err != nil { + return fmt.Errorf("link by name: %w", err) + } + + ip := address.IP.String() + mask := "0x" + address.Network.Mask.String() + + log.Infof("assign addr %s mask %s to %s interface", ip, mask, l.name) + + err = link.AssignAddr(ip, mask) + if err != nil { + return fmt.Errorf("assign addr: %w", err) + } + + err = link.Up() + if err != nil { + return fmt.Errorf("up: %w", err) + } + + return nil +} diff --git a/iface/tun_link_linux.go b/iface/tun_link_linux.go index ab28b7e3875..3ce644e8452 100644 --- a/iface/tun_link_linux.go +++ b/iface/tun_link_linux.go @@ -2,7 +2,13 @@ package iface -import "github.com/vishvananda/netlink" +import ( + "fmt" + "os" + + log "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" +) type wgLink struct { attrs *netlink.LinkAttrs @@ -31,3 +37,97 @@ func (l *wgLink) Type() string { func (l *wgLink) Close() error { return netlink.LinkDel(l) } + +func (l *wgLink) recreate() error { + name := l.attrs.Name + + // check if interface exists + link, err := netlink.LinkByName(name) + if err != nil { + switch err.(type) { + case netlink.LinkNotFoundError: + break + default: + return fmt.Errorf("link by name: %w", err) + } + } + + // remove if interface exists + if link != nil { + err = netlink.LinkDel(l) + if err != nil { + return err + } + } + + log.Debugf("adding device: %s", name) + err = netlink.LinkAdd(l) + if os.IsExist(err) { + log.Infof("interface %s already exists. Will reuse.", name) + } else if err != nil { + return fmt.Errorf("link add: %w", err) + } + + return nil +} + +func (l *wgLink) setMTU(mtu int) error { + if err := netlink.LinkSetMTU(l, mtu); err != nil { + log.Errorf("error setting MTU on interface: %s", l.attrs.Name) + + return fmt.Errorf("link set mtu: %w", err) + } + + return nil +} + +func (l *wgLink) up() error { + if err := netlink.LinkSetUp(l); err != nil { + log.Errorf("error bringing up interface: %s", l.attrs.Name) + return fmt.Errorf("link setup: %w", err) + } + + return nil +} + +func (l *wgLink) assignAddr(address WGAddress) error { + //delete existing addresses + list, err := netlink.AddrList(l, 0) + if err != nil { + return fmt.Errorf("list addr: %w", err) + } + + if len(list) > 0 { + for _, a := range list { + addr := a + err = netlink.AddrDel(l, &addr) + if err != nil { + return fmt.Errorf("del addr: %w", err) + } + } + } + + name := l.attrs.Name + addrStr := address.String() + + log.Debugf("adding address %s to interface: %s", addrStr, name) + + addr, err := netlink.ParseAddr(addrStr) + if err != nil { + return fmt.Errorf("parse addr: %w", err) + } + + err = netlink.AddrAdd(l, addr) + if os.IsExist(err) { + log.Infof("interface %s already has the address: %s", name, addrStr) + } else if err != nil { + return fmt.Errorf("add addr: %w", err) + } + + // On linux, the link must be brought up + if err := netlink.LinkSetUp(l); err != nil { + return fmt.Errorf("link setup: %w", err) + } + + return nil +} diff --git a/iface/tun_usp_linux.go b/iface/tun_usp_unix.go similarity index 78% rename from iface/tun_usp_linux.go rename to iface/tun_usp_unix.go index 9f02102286e..2e4be5280d1 100644 --- a/iface/tun_usp_linux.go +++ b/iface/tun_usp_unix.go @@ -1,14 +1,14 @@ -//go:build linux && !android +//go:build (linux && !android) || freebsd package iface import ( "fmt" "os" + "runtime" "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" - "github.com/vishvananda/netlink" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" @@ -31,6 +31,9 @@ type tunUSPDevice struct { func newTunUSPDevice(name string, address WGAddress, port int, key string, mtu int, transportNet transport.Net) wgTunDevice { log.Infof("using userspace bind mode") + + checkUser() + return &tunUSPDevice{ name: name, address: address, @@ -129,30 +132,14 @@ func (t *tunUSPDevice) Wrapper() *DeviceWrapper { func (t *tunUSPDevice) assignAddr() error { link := newWGLink(t.name) - //delete existing addresses - list, err := netlink.AddrList(link, 0) - if err != nil { - return err - } - if len(list) > 0 { - for _, a := range list { - addr := a - err = netlink.AddrDel(link, &addr) - if err != nil { - return err - } - } - } + return link.assignAddr(t.address) +} - log.Debugf("adding address %s to interface: %s", t.address.String(), t.name) - addr, _ := netlink.ParseAddr(t.address.String()) - err = netlink.AddrAdd(link, addr) - if os.IsExist(err) { - log.Infof("interface %s already has the address: %s", t.name, t.address.String()) - } else if err != nil { - return err +func checkUser() { + if runtime.GOOS == "freebsd" { + euid := os.Geteuid() + if euid != 0 { + log.Warn("newTunUSPDevice: on netbird must run as root to be able to assign address to the tun interface with ifconfig") + } } - // On linux, the link must be brought up - err = netlink.LinkSetUp(link) - return err } diff --git a/iface/wg_configurer_kernel.go b/iface/wg_configurer_kernel_unix.go similarity index 99% rename from iface/wg_configurer_kernel.go rename to iface/wg_configurer_kernel_unix.go index 67bfb716d0f..f2d6001cadc 100644 --- a/iface/wg_configurer_kernel.go +++ b/iface/wg_configurer_kernel_unix.go @@ -1,4 +1,4 @@ -//go:build linux && !android +//go:build (linux && !android) || freebsd package iface diff --git a/util/membership_unix.go b/util/membership_unix.go index 82237461c75..a9e55af84f5 100644 --- a/util/membership_unix.go +++ b/util/membership_unix.go @@ -1,4 +1,4 @@ -//go:build linux || darwin +//go:build linux || darwin || freebsd package util diff --git a/version/url_freebsd.go b/version/url_freebsd.go new file mode 100644 index 00000000000..c8193e30c31 --- /dev/null +++ b/version/url_freebsd.go @@ -0,0 +1,6 @@ +package version + +// DownloadUrl return with the proper download link +func DownloadUrl() string { + return downloadURL +}