Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"log/slog"
"net"
"os"
"sync"

"github.com/TeoSlayer/pilotprotocol/pkg/protocol"
Expand Down Expand Up @@ -42,6 +43,15 @@ type Gateway struct {
done chan struct{}
}

// loopbackPrivilegeCheck verifies the process can manage loopback aliases.
// Overridden in tests to bypass the OS euid gate.
var loopbackPrivilegeCheck = func() error {
if os.Geteuid() != 0 {
return fmt.Errorf("loopback alias management requires root (or CAP_NET_ADMIN on Linux); effective UID %d — restart gateway with sudo or grant ambient capabilities", os.Geteuid())
}
return nil
}

// New creates a new Gateway bound to the given Dialer. The Dialer is
// typically a *driver.Driver constructed by cmd/gateway.
func New(cfg Config, d Dialer) (*Gateway, error) {
Expand Down Expand Up @@ -156,7 +166,16 @@ func (gw *Gateway) Unmap(localIP string) error {
}

func (gw *Gateway) startProxy(localIP net.IP, pilotAddr protocol.Addr) {
gw.addLoopbackAlias(localIP)
if err := loopbackPrivilegeCheck(); err != nil {
slog.Error("gateway startProxy: privilege check failed — loopback alias not created",
"err", err)
return
}
if err := gw.addLoopbackAlias(localIP); err != nil {
slog.Error("gateway startProxy: loopback alias setup failed — proxy not started",
"ip", localIP, "pilot_addr", pilotAddr, "err", err)
return
}

gw.mu.Lock()
gw.aliases = append(gw.aliases, localIP)
Expand Down
6 changes: 4 additions & 2 deletions loopback_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
package gateway

import (
"fmt"
"log/slog"
"net"
"os/exec"
)

func (gw *Gateway) addLoopbackAlias(ip net.IP) {
func (gw *Gateway) addLoopbackAlias(ip net.IP) error {
if err := exec.Command("ifconfig", "lo0", "alias", ip.String()).Run(); err != nil {
slog.Error("addLoopbackAlias failed", "ip", ip, "os", "darwin", "err", err)
return fmt.Errorf("ifconfig lo0 alias %s: %w", ip, err)
}
return nil
}

func (gw *Gateway) removeLoopbackAlias(ip net.IP) {
Expand Down
6 changes: 4 additions & 2 deletions loopback_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
package gateway

import (
"fmt"
"log/slog"
"net"
"os/exec"
)

func (gw *Gateway) addLoopbackAlias(ip net.IP) {
func (gw *Gateway) addLoopbackAlias(ip net.IP) error {
if err := exec.Command("ip", "addr", "add", ip.String()+"/32", "dev", "lo").Run(); err != nil {
slog.Error("addLoopbackAlias failed", "ip", ip, "os", "linux", "err", err)
return fmt.Errorf("ip addr add %s/32 dev lo: %w", ip, err)
}
return nil
}

func (gw *Gateway) removeLoopbackAlias(ip net.IP) {
Expand Down
5 changes: 3 additions & 2 deletions loopback_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
package gateway

import (
"fmt"
"log/slog"
"net"
"runtime"
)

func (gw *Gateway) addLoopbackAlias(ip net.IP) {
slog.Error("addLoopbackAlias: unsupported OS", "ip", ip, "os", runtime.GOOS)
func (gw *Gateway) addLoopbackAlias(ip net.IP) error {
return fmt.Errorf("loopback alias unsupported on %s", runtime.GOOS)
}

func (gw *Gateway) removeLoopbackAlias(ip net.IP) {
Expand Down
19 changes: 17 additions & 2 deletions zz_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,12 @@ func TestMap_StartsProxyWithStubs(t *testing.T) {
}
withExecStubs(t, true)

// Bypass the privilege check so the test can exercise the real path
// through addLoopbackAlias without requiring root.
old := loopbackPrivilegeCheck
loopbackPrivilegeCheck = func() error { return nil }
defer func() { loopbackPrivilegeCheck = old }()

// Use ports=[] so listenPort never actually attempts a bind — we only
// need to exercise Map → startProxy → addLoopbackAlias. (The bind path
// is already covered by TestListenPort_AcceptsAndBridges.)
Expand Down Expand Up @@ -326,6 +332,10 @@ func TestMap_ExplicitIPWithStubs(t *testing.T) {
}
withExecStubs(t, true)

old := loopbackPrivilegeCheck
loopbackPrivilegeCheck = func() error { return nil }
defer func() { loopbackPrivilegeCheck = old }()

d := &pipeDialerSync{}
gw, err := New(Config{Subnet: "10.66.0.0/16", Ports: []uint16{}}, d)
if err != nil {
Expand Down Expand Up @@ -401,7 +411,10 @@ func TestAddRemoveLoopbackAlias_StubExec(t *testing.T) {

// Both must complete without panic. Errors from the stub (rc=0) are
// expected to be nil; logging is the only side effect we observe.
gw.addLoopbackAlias(ip)
// The function now returns an error; with stubs it should succeed.
if err := gw.addLoopbackAlias(ip); err != nil {
t.Fatalf("addLoopbackAlias with successful stub: %v", err)
}
gw.removeLoopbackAlias(ip)
}

Expand All @@ -420,7 +433,9 @@ func TestAddRemoveLoopbackAlias_StubExecFailure(t *testing.T) {
t.Fatalf("New: %v", err)
}
ip := net.ParseIP("10.4.0.200")
gw.addLoopbackAlias(ip) // logs error
if err := gw.addLoopbackAlias(ip); err != nil {
t.Logf("addLoopbackAlias (rc=1 stub) returned error: %v", err)
}
gw.removeLoopbackAlias(ip) // logs error
}

Expand Down
Loading