diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-06-04 23:09:46 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-06-04 23:09:46 -0400 |
| commit | 78059b43e3d00a0f2b75677461692745cce34a63 (patch) | |
| tree | 4a3359f978485141fe610f227d483e23552a702e | |
| parent | 04dca5dada8c2d971ff3b54eeedc5ab6e53a29ac (diff) | |
refactor: remove dependency on ip CLI tool and abstract network logic
Eliminate the external dependency on the `ip` (iproute2) command-line tool by centralizing network configuration and diagnostics within a new `internal/network` package using the `netlink` library.
Changes:
- Introduced `internal/network` package to handle network interface listing and configuration.
- Replaced `exec.Command("ip", "link")` in `internal/namespace.VerifyIsolation` with `network.ListInterfaces()`.
- Improved `VerifyIsolation` to explicitly ensure only the loopback interface is present in a fresh network namespace.
- Moved interface and routing configuration logic from `internal/wireguard` to `internal/network`.
- Removed unnecessary `os/exec` imports from network-related files.
This change increases the tool's portability by removing the requirement for `iproute2` to be installed in the target environment.
| -rw-r--r-- | internal/namespace/namespace.go | 32 | ||||
| -rw-r--r-- | internal/network/network.go | 82 | ||||
| -rw-r--r-- | internal/wireguard/wireguard.go | 53 |
3 files changed, 105 insertions, 62 deletions
diff --git a/internal/namespace/namespace.go b/internal/namespace/namespace.go index a50f70a..45eba73 100644 --- a/internal/namespace/namespace.go +++ b/internal/namespace/namespace.go @@ -26,9 +26,9 @@ import ( "fmt" "net" "os" - "os/exec" "syscall" + "git.theodohertyfamily.com/wg-wrap/internal/network" "golang.org/x/sys/unix" ) @@ -50,21 +50,29 @@ func VerifyIsolation() (bool, string) { // 2. Check Network Isolation // We expect a fresh network namespace to have only the loopback interface. - // We use a simple shell call to 'ip link' to avoid importing heavy net libraries - // if we just want a quick diagnostic. - cmd := exec.Command("ip", "link") - out, err := cmd.CombinedOutput() + interfaces, err := network.ListInterfaces() if err != nil { - return false, fmt.Sprintf("failed to execute ip link: %v", err) + return false, fmt.Sprintf("failed to list interfaces: %v", err) } // In a fresh netns, we typically only see 'lo'. - // We check if any common host interfaces (eth, wlan, br, enp) appear. - output := string(out) - // This is a simple heuristic; for a real test we'd be more precise. - // We are looking for evidence of host interfaces. - if len(output) == 0 { - return false, "ip link returned no output" + // If we see more than just loopback, or loopback is missing, it might not be isolated. + if len(interfaces) == 0 { + return false, "no network interfaces found" + } + + hasLo := false + for _, iface := range interfaces { + if iface.Name == "lo" { + hasLo = true + } else { + // If we find any other interface (eth0, wlan0, etc.), we aren't isolated. + return false, fmt.Sprintf("detected non-isolated interface: %s", iface.Name) + } + } + + if !hasLo { + return false, "loopback interface missing" } // 3. Check Filesystem Transparency diff --git a/internal/network/network.go b/internal/network/network.go new file mode 100644 index 0000000..6afcf5e --- /dev/null +++ b/internal/network/network.go @@ -0,0 +1,82 @@ +//go:build linux + +package network + +import ( + "fmt" + "net" + "strings" + + "github.com/vishvananda/netlink" +) + +// InterfaceInfo contains basic information about a network interface. +type InterfaceInfo struct { + Name string + Index int +} + +// ListInterfaces returns a list of all network interfaces present in the current namespace. +func ListInterfaces() ([]InterfaceInfo, error) { + links, err := netlink.LinkList() + if err != nil { + return nil, fmt.Errorf("failed to list interfaces: %w", err) + } + + var interfaces []InterfaceInfo + for _, link := range links { + interfaces = append(interfaces, InterfaceInfo{ + Name: link.Attrs().Name, + Index: link.Attrs().Index, + }) + } + return interfaces, nil +} + +// ConfigureInterface sets the MTU, brings the interface up, assigns an IP address, +// and configures the default route. +func ConfigureInterface(name, address string, mtu int) error { + link, err := netlink.LinkByName(name) + if err != nil { + return fmt.Errorf("failed to find link %s: %w", name, err) + } + + if err := netlink.LinkSetMTU(link, mtu); err != nil { + return fmt.Errorf("failed to set MTU %d on link %s: %w", mtu, name, err) + } + + if err := netlink.LinkSetUp(link); err != nil { + return fmt.Errorf("failed to bring up link %s: %w", name, err) + } + + addr, err := netlink.ParseAddr(address) + if err != nil { + return fmt.Errorf("invalid IP address %s: %w", address, err) + } + if err := netlink.AddrAdd(link, addr); err != nil { + if !strings.Contains(err.Error(), "file exists") { + return fmt.Errorf("failed to add address %s to link %s: %w", address, name, err) + } + } + + var dst *net.IPNet + if addr.IP.To4() != nil { + _, dst, _ = net.ParseCIDR("0.0.0.0/0") + } else { + _, dst, _ = net.ParseCIDR("::/0") + } + + route := &netlink.Route{ + Scope: netlink.SCOPE_UNIVERSE, + LinkIndex: link.Attrs().Index, + Dst: dst, + } + + if err := netlink.RouteAdd(route); err != nil { + if err := netlink.RouteReplace(route); err != nil { + return fmt.Errorf("failed to configure default route via %s: %w", name, err) + } + } + + return nil +} diff --git a/internal/wireguard/wireguard.go b/internal/wireguard/wireguard.go index 5db588e..8ffe794 100644 --- a/internal/wireguard/wireguard.go +++ b/internal/wireguard/wireguard.go @@ -33,9 +33,9 @@ import ( "strings" "git.theodohertyfamily.com/wg-wrap/internal/namespace" + "git.theodohertyfamily.com/wg-wrap/internal/network" "git.theodohertyfamily.com/wg-wrap/internal/paths" "git.theodohertyfamily.com/wg-wrap/pkg/wgconf" - "github.com/vishvananda/netlink" "golang.org/x/sys/unix" "golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/device" @@ -117,8 +117,8 @@ func StartTunnel(pm *paths.PathManager, profile string, cfg *wgconf.Config, dnsS return nil, fmt.Errorf("failed to bring up WireGuard device: %w", err) } - // 4. Configure network interface using netlink - if err := configureInterface(tunName, cfg.Address, mtu); err != nil { + // 4. Configure network interface + if err := network.ConfigureInterface(tunName, cfg.Address, mtu); err != nil { return nil, fmt.Errorf("failed to configure network interface %s: %w", tunName, err) } @@ -201,53 +201,6 @@ func buildUAPIConfig(cfg *wgconf.Config) (string, error) { return sb.String(), nil } -// configureInterface uses netlink to set address, MTU, and default routing table. -func configureInterface(name, address string, mtu int) error { - link, err := netlink.LinkByName(name) - if err != nil { - return fmt.Errorf("failed to find link %s: %w", name, err) - } - - if err := netlink.LinkSetMTU(link, mtu); err != nil { - return fmt.Errorf("failed to set MTU %d on link %s: %w", mtu, name, err) - } - - if err := netlink.LinkSetUp(link); err != nil { - return fmt.Errorf("failed to bring up link %s: %w", name, err) - } - - addr, err := netlink.ParseAddr(address) - if err != nil { - return fmt.Errorf("invalid IP address %s: %w", address, err) - } - if err := netlink.AddrAdd(link, addr); err != nil { - if !strings.Contains(err.Error(), "file exists") { - return fmt.Errorf("failed to add address %s to link %s: %w", address, name, err) - } - } - - var dst *net.IPNet - if addr.IP.To4() != nil { - _, dst, _ = net.ParseCIDR("0.0.0.0/0") - } else { - _, dst, _ = net.ParseCIDR("::/0") - } - - route := &netlink.Route{ - Scope: netlink.SCOPE_UNIVERSE, - LinkIndex: link.Attrs().Index, - Dst: dst, - } - - if err := netlink.RouteAdd(route); err != nil { - if err := netlink.RouteReplace(route); err != nil { - return fmt.Errorf("failed to configure default route via %s: %w", name, err) - } - } - - return nil -} - // GetTunnelLocalIP extracts the local IP address (without CIDR) from the config. func GetTunnelLocalIP(cfg *wgconf.Config) (string, error) { if cfg.Address == "" { |
