summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-06-04 23:09:46 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-06-04 23:09:46 -0400
commit78059b43e3d00a0f2b75677461692745cce34a63 (patch)
tree4a3359f978485141fe610f227d483e23552a702e
parent04dca5dada8c2d971ff3b54eeedc5ab6e53a29ac (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.go32
-rw-r--r--internal/network/network.go82
-rw-r--r--internal/wireguard/wireguard.go53
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 == "" {