Compare commits

..

4 Commits

Author SHA1 Message Date
Viktor Liu
a06cfc54cc Reword the P2P to relay switch comment 2026-08-17 20:35:46 +02:00
Viktor Liu
f420366f54 Fix the remaining turn wording in the packet forward error 2026-08-17 20:34:40 +02:00
Viktor Liu
5d807c0491 Drop the duplicate proxy setup error log and fix the udp proxy doc comment 2026-08-17 13:31:39 +02:00
Viktor Liu
76abf27d3a Rename TURN-specific wg proxy naming to relayed connections 2026-08-17 13:19:13 +02:00
20 changed files with 82 additions and 246 deletions

View File

@@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"os/user"
"strings"
"time"
@@ -113,7 +114,7 @@ func debugConfigDump(cmd *cobra.Command, _ []string) error {
if err != nil {
return fmt.Errorf("get active profile: %v", err)
}
currUser, err := profilemanager.InvokingUser()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"os/user"
"strings"
log "github.com/sirupsen/logrus"
@@ -52,7 +53,7 @@ var loginCmd = &cobra.Command{
// nolint
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
}
username, err := profilemanager.InvokingUser()
username, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
}

View File

@@ -3,11 +3,11 @@ package cmd
import (
"context"
"fmt"
"os/user"
"time"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto"
)
@@ -37,7 +37,7 @@ var logoutCmd = &cobra.Command{
if profileName != "" {
req.ProfileName = &profileName
currUser, err := profilemanager.InvokingUser()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"os/user"
"strings"
"text/tabwriter"
"time"
@@ -96,7 +97,7 @@ func listProfilesFunc(cmd *cobra.Command, _ []string) error {
}
defer conn.Close()
currUser, err := profilemanager.InvokingUser()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}
@@ -137,7 +138,7 @@ func addProfileFunc(cmd *cobra.Command, args []string) error {
return err
}
currUser, err := profilemanager.InvokingUser()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}
@@ -178,7 +179,7 @@ func renameProfileFunc(cmd *cobra.Command, args []string) error {
}
defer conn.Close()
currUser, err := profilemanager.InvokingUser()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}
@@ -232,7 +233,7 @@ func removeProfileFunc(cmd *cobra.Command, args []string) error {
}
defer conn.Close()
currUser, err := profilemanager.InvokingUser()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}
@@ -260,7 +261,7 @@ func selectProfileFunc(cmd *cobra.Command, args []string) error {
profileManager := profilemanager.NewProfileManager()
handle := args[0]
currUser, err := profilemanager.InvokingUser()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/netip"
"os/user"
"runtime"
"strings"
"time"
@@ -121,7 +122,7 @@ func upFunc(cmd *cobra.Command, args []string) error {
pm := profilemanager.NewProfileManager()
username, err := profilemanager.InvokingUser()
username, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
}
@@ -294,21 +295,6 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
client := proto.NewDaemonServiceClient(conn)
// Plain root has no invoking user to resolve profiles for, so the local
// state falls back to root's own — the default profile. Acting on that
// while the daemon runs another user's profile would silently switch the
// daemon away from it (and a later browser login would register the
// default profile's peer under whichever account the IdP returns). Refuse
// the ambiguity instead of guessing.
if profilemanager.IsPlainRoot() && profileName == "" {
if active, err := client.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{}); err == nil &&
active.GetId() != "" && active.GetId() != activeProf.ID.String() {
return fmt.Errorf(
"running as root: the daemon's active profile is %q (user %q), but this invocation resolves to %q; pass --profile to choose one explicitly, or run via sudo from your own user",
active.GetProfileName(), active.GetUsername(), activeProf.ID)
}
}
status, err := client.Status(ctx, &proto.StatusRequest{
WaitForReady: func() *bool { b := true; return &b }(),
})
@@ -328,7 +314,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
}
}
username, err := profilemanager.InvokingUser()
username, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
}

View File

@@ -53,15 +53,15 @@ func NewProxyBind(bind Bind, mtu uint16) *ProxyBind {
return p
}
// AddTurnConn adds a new connection to the bind.
// AddRelayedConn adds a new connection to the bind.
// endpoint is the NetBird address of the remote peer. The SetEndpoint return with the address what will be used in the
// WireGuard configuration.
//
// Parameters:
// - ctx: Context is used for proxyToLocal to avoid unnecessary error messages
// - nbAddr: The NetBird UDP address of the remote peer, it required to generate fake address
// - remoteConn: The established TURN connection to the remote peer
func (p *ProxyBind) AddTurnConn(ctx context.Context, nbAddr *net.UDPAddr, remoteConn net.Conn) error {
// - remoteConn: The established relayed connection to the remote peer
func (p *ProxyBind) AddRelayedConn(ctx context.Context, nbAddr *net.UDPAddr, remoteConn net.Conn) error {
fakeNetIP, err := fakeAddress(nbAddr)
if err != nil {
return err

View File

@@ -30,9 +30,9 @@ type WGEBPFProxy struct {
proxyPort int
mtu uint16
ebpfManager ebpfMgr.Manager
turnConnStore map[uint16]net.Conn
turnConnMutex sync.Mutex
ebpfManager ebpfMgr.Manager
relayedConnStore map[uint16]net.Conn
relayedConnMutex sync.Mutex
lastUsedPort uint16
rawConnIPv4 net.PacketConn
@@ -50,7 +50,7 @@ func NewWGEBPFProxy(wgPort int, mtu uint16) *WGEBPFProxy {
localWGListenPort: wgPort,
mtu: mtu,
ebpfManager: ebpf.GetEbpfManagerInstance(),
turnConnStore: make(map[uint16]net.Conn),
relayedConnStore: make(map[uint16]net.Conn),
}
return wgProxy
}
@@ -110,14 +110,14 @@ func (p *WGEBPFProxy) Listen() error {
return nil
}
// AddTurnConn add new turn connection for the proxy
func (p *WGEBPFProxy) AddTurnConn(turnConn net.Conn) (*net.UDPAddr, error) {
wgEndpointPort, err := p.storeTurnConn(turnConn)
// AddRelayedConn add new relayed connection for the proxy
func (p *WGEBPFProxy) AddRelayedConn(relayedConn net.Conn) (*net.UDPAddr, error) {
wgEndpointPort, err := p.storeRelayedConn(relayedConn)
if err != nil {
return nil, err
}
log.Infof("turn conn added to wg proxy store: %s, endpoint port: :%d", turnConn.RemoteAddr(), wgEndpointPort)
log.Infof("relayed conn added to wg proxy store: %s, endpoint port: :%d", relayedConn.RemoteAddr(), wgEndpointPort)
wgEndpoint := &net.UDPAddr{
IP: net.ParseIP(loopbackAddr),
@@ -186,48 +186,48 @@ func (p *WGEBPFProxy) readAndForwardPacket(buf []byte) error {
return fmt.Errorf("failed to read UDP packet from WG: %w", err)
}
p.turnConnMutex.Lock()
conn, ok := p.turnConnStore[uint16(addr.Port)]
p.turnConnMutex.Unlock()
p.relayedConnMutex.Lock()
conn, ok := p.relayedConnStore[uint16(addr.Port)]
p.relayedConnMutex.Unlock()
if !ok {
if p.ctx.Err() == nil {
log.Debugf("turn conn not found by port because conn already has been closed: %d", addr.Port)
log.Debugf("relayed conn not found by port because conn already has been closed: %d", addr.Port)
}
return nil
}
if _, err := conn.Write(buf[:n]); err != nil {
return fmt.Errorf("failed to forward local WG packet (%d) to remote turn conn: %w", addr.Port, err)
return fmt.Errorf("forward local WG packet (%d) to remote relayed conn: %w", addr.Port, err)
}
return nil
}
func (p *WGEBPFProxy) storeTurnConn(turnConn net.Conn) (uint16, error) {
p.turnConnMutex.Lock()
defer p.turnConnMutex.Unlock()
func (p *WGEBPFProxy) storeRelayedConn(relayedConn net.Conn) (uint16, error) {
p.relayedConnMutex.Lock()
defer p.relayedConnMutex.Unlock()
np, err := p.nextFreePort()
if err != nil {
return np, err
}
p.turnConnStore[np] = turnConn
p.relayedConnStore[np] = relayedConn
return np, nil
}
func (p *WGEBPFProxy) removeTurnConn(turnConnID uint16) {
p.turnConnMutex.Lock()
defer p.turnConnMutex.Unlock()
func (p *WGEBPFProxy) removeRelayedConn(relayedConnID uint16) {
p.relayedConnMutex.Lock()
defer p.relayedConnMutex.Unlock()
_, ok := p.turnConnStore[turnConnID]
_, ok := p.relayedConnStore[relayedConnID]
if ok {
log.Debugf("remove turn conn from store by port: %d", turnConnID)
log.Debugf("remove relayed conn from store by port: %d", relayedConnID)
}
delete(p.turnConnStore, turnConnID)
delete(p.relayedConnStore, relayedConnID)
}
func (p *WGEBPFProxy) nextFreePort() (uint16, error) {
if len(p.turnConnStore) == 65535 {
return 0, fmt.Errorf("reached maximum turn connection numbers")
if len(p.relayedConnStore) == 65535 {
return 0, fmt.Errorf("reached maximum relayed connection numbers")
}
generatePort:
if p.lastUsedPort == 65535 {
@@ -236,7 +236,7 @@ generatePort:
p.lastUsedPort++
}
if _, ok := p.turnConnStore[p.lastUsedPort]; ok {
if _, ok := p.relayedConnStore[p.lastUsedPort]; ok {
goto generatePort
}
return p.lastUsedPort, nil

View File

@@ -9,32 +9,32 @@ import (
func TestWGEBPFProxy_connStore(t *testing.T) {
wgProxy := NewWGEBPFProxy(1, 1280)
p, _ := wgProxy.storeTurnConn(nil)
p, _ := wgProxy.storeRelayedConn(nil)
if p != 1 {
t.Errorf("invalid initial port: %d", wgProxy.lastUsedPort)
}
numOfConns := 10
for i := 0; i < numOfConns; i++ {
p, _ = wgProxy.storeTurnConn(nil)
p, _ = wgProxy.storeRelayedConn(nil)
}
if p != uint16(numOfConns)+1 {
t.Errorf("invalid last used port: %d, expected: %d", p, numOfConns+1)
}
if len(wgProxy.turnConnStore) != numOfConns+1 {
t.Errorf("invalid store size: %d, expected: %d", len(wgProxy.turnConnStore), numOfConns+1)
if len(wgProxy.relayedConnStore) != numOfConns+1 {
t.Errorf("invalid store size: %d, expected: %d", len(wgProxy.relayedConnStore), numOfConns+1)
}
}
func TestWGEBPFProxy_portCalculation_overflow(t *testing.T) {
wgProxy := NewWGEBPFProxy(1, 1280)
_, _ = wgProxy.storeTurnConn(nil)
_, _ = wgProxy.storeRelayedConn(nil)
wgProxy.lastUsedPort = 65535
p, _ := wgProxy.storeTurnConn(nil)
p, _ := wgProxy.storeRelayedConn(nil)
if len(wgProxy.turnConnStore) != 2 {
t.Errorf("invalid store size: %d, expected: %d", len(wgProxy.turnConnStore), 2)
if len(wgProxy.relayedConnStore) != 2 {
t.Errorf("invalid store size: %d, expected: %d", len(wgProxy.relayedConnStore), 2)
}
if p != 2 {
@@ -46,11 +46,11 @@ func TestWGEBPFProxy_portCalculation_maxConn(t *testing.T) {
wgProxy := NewWGEBPFProxy(1, 1280)
for i := 0; i < 65535; i++ {
_, _ = wgProxy.storeTurnConn(nil)
_, _ = wgProxy.storeRelayedConn(nil)
}
_, err := wgProxy.storeTurnConn(nil)
_, err := wgProxy.storeRelayedConn(nil)
if err == nil {
t.Errorf("invalid turn conn store calculation")
t.Errorf("invalid relayed conn store calculation")
}
}

View File

@@ -121,10 +121,10 @@ func NewProxyWrapper(proxy *WGEBPFProxy) *ProxyWrapper {
}
}
func (p *ProxyWrapper) AddTurnConn(ctx context.Context, _ *net.UDPAddr, remoteConn net.Conn) error {
addr, err := p.wgeBPFProxy.AddTurnConn(remoteConn)
func (p *ProxyWrapper) AddRelayedConn(ctx context.Context, _ *net.UDPAddr, remoteConn net.Conn) error {
addr, err := p.wgeBPFProxy.AddRelayedConn(remoteConn)
if err != nil {
return fmt.Errorf("add turn conn: %w", err)
return fmt.Errorf("add relayed conn: %w", err)
}
headers, err := NewPacketHeaders(p.wgeBPFProxy.localWGListenPort, addr)
@@ -252,7 +252,7 @@ func (p *ProxyWrapper) CloseConn() error {
}
func (p *ProxyWrapper) proxyToLocal(ctx context.Context) {
defer p.wgeBPFProxy.removeTurnConn(uint16(p.wgRelayedEndpointAddr.Port))
defer p.wgeBPFProxy.removeRelayedConn(uint16(p.wgRelayedEndpointAddr.Port))
buf := make([]byte, p.wgeBPFProxy.mtu+bufsize.WGBufferOverhead)
for {
@@ -273,7 +273,7 @@ func (p *ProxyWrapper) proxyToLocal(ctx context.Context) {
if ctx.Err() != nil {
return
}
log.Errorf("failed to write out turn pkg to local conn: %v", err)
log.Errorf("failed to write out relayed pkg to local conn: %v", err)
}
}
}
@@ -286,7 +286,7 @@ func (p *ProxyWrapper) readFromRemote(ctx context.Context, buf []byte) (int, err
}
p.closeListener.Notify()
if !errors.Is(err, io.EOF) {
log.Errorf("failed to read from turn conn (endpoint: :%d): %s", p.wgRelayedEndpointAddr.Port, err)
log.Errorf("failed to read from relayed conn (endpoint: :%d): %s", p.wgRelayedEndpointAddr.Port, err)
}
return 0, err
}

View File

@@ -7,7 +7,7 @@ import (
// Proxy is a transfer layer between the relayed connection and the WireGuard
type Proxy interface {
AddTurnConn(ctx context.Context, endpoint *net.UDPAddr, remoteConn net.Conn) error
AddRelayedConn(ctx context.Context, endpoint *net.UDPAddr, remoteConn net.Conn) error
EndpointAddr() *net.UDPAddr // EndpointAddr returns the address of the WireGuard peer endpoint
Work() // Work start or resume the proxy
Pause() // Pause to forward the packages from remote connection to WireGuard. The opposite way still works.

View File

@@ -95,7 +95,7 @@ func TestProxyCloseByRemoteConn(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
addr, _ := net.ResolveUDPAddr("udp", "100.108.135.221:51892")
relayedConn := newMockConn()
err := tt.proxy.AddTurnConn(ctx, addr, relayedConn)
err := tt.proxy.AddRelayedConn(ctx, addr, relayedConn)
if err != nil {
t.Errorf("error: %v", err)
}
@@ -157,7 +157,7 @@ func redirectTraffic(t *testing.T, proxy Proxy, wgPort int, endPointAddr *net.UD
_ = relayedServer.Close()
}()
if err := proxy.AddTurnConn(context.Background(), endPointAddr, relayedConn); err != nil {
if err := proxy.AddRelayedConn(context.Background(), endPointAddr, relayedConn); err != nil {
t.Errorf("error: %v", err)
}
defer func() {

View File

@@ -119,9 +119,9 @@ func testRedirectAs(t *testing.T, proxy Proxy, wgPort int, nbAddr, p2pEndpoint *
}
defer relayConn.Close()
// Add TURN connection to proxy
if err := proxy.AddTurnConn(ctx, nbAddr, relayConn); err != nil {
t.Fatalf("failed to add TURN connection: %v", err)
// Add relayed connection to proxy
if err := proxy.AddRelayedConn(ctx, nbAddr, relayConn); err != nil {
t.Fatalf("failed to add relayed connection: %v", err)
}
defer func() {
if err := proxy.CloseConn(); err != nil {
@@ -304,8 +304,8 @@ func TestRedirectAs_Multiple_Switches(t *testing.T) {
Port: 38746,
}
if err := proxy.AddTurnConn(ctx, nbAddr, relayConn); err != nil {
t.Fatalf("failed to add TURN connection: %v", err)
if err := proxy.AddRelayedConn(ctx, nbAddr, relayConn); err != nil {
t.Fatalf("failed to add relayed connection: %v", err)
}
defer func() {
if err := proxy.CloseConn(); err != nil {

View File

@@ -51,12 +51,12 @@ func NewWGUDPProxy(wgPort int, mtu uint16) *WGUDPProxy {
return p
}
// AddTurnConn
// AddRelayedConn dials the local WireGuard port and stores the relayed connection.
// The provided Context must be non-nil. If the context expires before
// the connection is complete, an error is returned. Once successfully
// connected, any expiration of the context will not affect the
// connection.
func (p *WGUDPProxy) AddTurnConn(ctx context.Context, _ *net.UDPAddr, remoteConn net.Conn) error {
func (p *WGUDPProxy) AddRelayedConn(ctx context.Context, _ *net.UDPAddr, remoteConn net.Conn) error {
dialer := net.Dialer{}
localConn, err := dialer.DialContext(ctx, "udp", fmt.Sprintf(":%d", p.localWGListenPort))
if err != nil {

View File

@@ -440,7 +440,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
conn.dumpState.NewLocalProxy()
wgProxy, err = conn.newProxy(iceConnInfo.RemoteConn)
if err != nil {
conn.Log.Errorf("failed to add turn net.Conn to local proxy: %v", err)
conn.Log.Errorf("failed to add relayed net.Conn to local proxy: %v", err)
return
}
ep = wgProxy.EndpointAddr()
@@ -878,9 +878,8 @@ func (conn *Conn) newProxy(remoteConn net.Conn) (wgproxy.Proxy, error) {
}
wgProxy := conn.config.WgConfig.WgInterface.GetProxy()
if err := wgProxy.AddTurnConn(conn.ctx, udpAddr, remoteConn); err != nil {
conn.Log.Errorf("failed to add turn net.Conn to local proxy: %v", err)
return nil, err
if err := wgProxy.AddRelayedConn(conn.ctx, udpAddr, remoteConn); err != nil {
return nil, fmt.Errorf("add relayed conn to proxy: %w", err)
}
return wgProxy, nil
}

View File

@@ -255,8 +255,8 @@ func (w *WorkerICE) connect(ctx context.Context, agent *icemaker.ThreadSafeAgent
return
}
w.log.Debugf("turn agent dial")
remoteConn, err := w.turnAgentDial(ctx, agent, remoteOfferAnswer)
w.log.Debugf("agent dial")
remoteConn, err := w.agentDial(ctx, agent, remoteOfferAnswer)
if err != nil {
w.log.Debugf("failed to dial the remote peer: %s", err)
w.closeAgent(agent, w.agentDialerCancel)
@@ -517,8 +517,8 @@ func (w *WorkerICE) onConnectionStateChange(agent *icemaker.ThreadSafeAgent, dia
w.logSuccessfulPaths(agent)
return
case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected, ice.ConnectionStateClosed:
// ice.ConnectionStateClosed happens when we recreate the agent. For the P2P to TURN switch important to
// notify the conn.onICEStateDisconnected changes to update the current used priority
// ice.ConnectionStateClosed happens when we recreate the agent. The P2P to relay switch requires
// notifying conn.onICEStateDisconnected so it can update the currently used priority.
sessionChanged := w.closeAgent(agent, dialerCancel)
@@ -532,7 +532,7 @@ func (w *WorkerICE) onConnectionStateChange(agent *icemaker.ThreadSafeAgent, dia
}
}
func (w *WorkerICE) turnAgentDial(ctx context.Context, agent *icemaker.ThreadSafeAgent, remoteOfferAnswer *OfferAnswer) (*ice.Conn, error) {
func (w *WorkerICE) agentDial(ctx context.Context, agent *icemaker.ThreadSafeAgent, remoteOfferAnswer *OfferAnswer) (*ice.Conn, error) {
if isController(w.config) {
return agent.Dial(ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
} else {

View File

@@ -217,12 +217,6 @@ func getConfigDir() (string, error) {
}
configDir := filepath.Join(base, "netbird")
// Under sudo this is the invoking user's directory and strictly read-only:
// anything root creates in it would be root-owned and break the user's own
// runs. Reads of a missing directory fall through to defaults.
if _, sudo := sudoInvokingUser(); sudo {
return configDir, nil
}
if err := os.MkdirAll(configDir, 0o755); err != nil {
return "", err
}
@@ -230,9 +224,6 @@ func getConfigDir() (string, error) {
}
func baseConfigDir() (string, error) {
if u, ok := sudoInvokingUser(); ok {
return userBaseConfigDir(u)
}
if runtime.GOOS == "darwin" {
if u, err := user.Current(); err == nil && u.HomeDir != "" {
return filepath.Join(u.HomeDir, "Library", "Application Support"), nil

View File

@@ -1,69 +0,0 @@
package profilemanager
import (
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
log "github.com/sirupsen/logrus"
)
// InvokingUser returns the user a CLI invocation acts for. Under sudo that is
// the user who ran sudo, not root: privileged flags force commands through
// sudo, and resolving profiles as root would silently switch the daemon to
// root's (default) profile instead of the invoking user's. Privilege decisions
// are not made here — those stay on the kernel credentials of the daemon
// connection, which SUDO_USER (a plain environment variable) can never
// influence; a forged value only selects a profile root could select anyway.
func InvokingUser() (*user.User, error) {
if u, ok := sudoInvokingUser(); ok {
return u, nil
}
return user.Current()
}
// IsPlainRoot reports that the process runs as root with no usable sudo
// context: there is no invoking user to act for, so per-user resolution falls
// back to root's own (empty) state. Callers use it to refuse ambiguous
// operations instead of silently acting on the wrong profile.
func IsPlainRoot() bool {
if os.Geteuid() != 0 {
return false
}
_, ok := sudoInvokingUser()
return !ok
}
// sudoInvokingUser resolves SUDO_USER when the process runs as root under
// sudo. Returns false whenever the sudo context is absent or unusable, in
// which case callers fall back to the process user.
func sudoInvokingUser() (*user.User, bool) {
if os.Geteuid() != 0 {
return nil, false
}
name := os.Getenv("SUDO_USER")
if name == "" || name == "root" {
return nil, false
}
u, err := user.Lookup(name)
if err != nil {
log.Warnf("failed to look up sudo invoking user %q, acting as root: %v", name, err)
return nil, false
}
return u, true
}
// userBaseConfigDir mirrors os.UserConfigDir for a user other than the process
// owner. Environment overrides (XDG_CONFIG_HOME) cannot be honoured here: under
// sudo the environment is root's, not the invoking user's.
func userBaseConfigDir(u *user.User) (string, error) {
if u.HomeDir == "" {
return "", fmt.Errorf("user %s has no home directory", u.Username)
}
if runtime.GOOS == "darwin" {
return filepath.Join(u.HomeDir, "Library", "Application Support"), nil
}
return filepath.Join(u.HomeDir, ".config"), nil
}

View File

@@ -1,57 +0,0 @@
package profilemanager
import (
"os"
"os/user"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInvokingUserFallsBackToProcessUser(t *testing.T) {
t.Setenv("SUDO_USER", "")
got, err := InvokingUser()
require.NoError(t, err)
current, err := user.Current()
require.NoError(t, err)
assert.Equal(t, current.Username, got.Username)
}
func TestSudoInvokingUserInactiveWithoutSudoContext(t *testing.T) {
t.Setenv("SUDO_USER", "")
_, ok := sudoInvokingUser()
assert.False(t, ok)
}
func TestSudoInvokingUserIgnoresRoot(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("needs root to enter the sudo branch")
}
t.Setenv("SUDO_USER", "root")
_, ok := sudoInvokingUser()
assert.False(t, ok, "sudo from a root shell must not redirect anything")
}
func TestUserBaseConfigDir(t *testing.T) {
u := &user.User{Username: "misha", HomeDir: filepath.Join("/home", "misha")}
dir, err := userBaseConfigDir(u)
require.NoError(t, err)
if runtime.GOOS == "darwin" {
assert.Equal(t, filepath.Join(u.HomeDir, "Library", "Application Support"), dir)
} else {
assert.Equal(t, filepath.Join(u.HomeDir, ".config"), dir)
}
_, err = userBaseConfigDir(&user.User{Username: "nohome"})
require.Error(t, err)
}
func TestIsPlainRoot(t *testing.T) {
t.Setenv("SUDO_USER", "")
assert.Equal(t, os.Geteuid() == 0, IsPlainRoot())
}

View File

@@ -3,6 +3,7 @@ package profilemanager
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"sync"
@@ -53,7 +54,7 @@ func (p *Profile) FilePath() (string, error) {
return "", fmt.Errorf("invalid profile ID: %q", id)
}
username, err := InvokingUser()
username, err := user.Current()
if err != nil {
return "", fmt.Errorf("failed to get current user: %w", err)
}
@@ -129,7 +130,7 @@ func (pm *ProfileManager) getActiveProfileState() ID {
if err != nil {
if !os.IsNotExist(err) {
log.Warnf("failed to read active profile state: %v", err)
} else if _, sudo := sudoInvokingUser(); !sudo {
} else {
if err := pm.setActiveProfileState(defaultProfileName); err != nil {
log.Warnf("failed to set default profile state: %v", err)
}
@@ -147,13 +148,6 @@ func (pm *ProfileManager) getActiveProfileState() ID {
}
func (pm *ProfileManager) setActiveProfileState(id ID) error {
// The invoking user's state is read-only under sudo — a root-owned file in
// the user's directory would break their own runs. The daemon still records
// the switch on its side; only the user-local bookkeeping is skipped.
if u, sudo := sudoInvokingUser(); sudo {
log.Infof("running under sudo: not persisting active profile %q for user %s", id, u.Username)
return nil
}
configDir, err := getConfigDir()
if err != nil {

View File

@@ -7,8 +7,6 @@ import (
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/util"
)
@@ -65,15 +63,6 @@ func (pm *ProfileManager) SetProfileState(id ID, state *ProfileState) error {
return fmt.Errorf("invalid profile ID: %q", id)
}
// The invoking user's state is read-only under sudo. The file only carries
// the account email for the login hint and display, so skipping the write
// costs at most one extra account prompt later — a root-owned file in the
// user's directory would cost every later update instead.
if u, sudo := sudoInvokingUser(); sudo {
log.Debugf("running under sudo: not persisting profile state for user %s", u.Username)
return nil
}
stateFile := filepath.Join(configDir, id.String()+".state.json")
if err := util.WriteJsonWithRestrictedPermission(context.Background(), stateFile, state); err != nil {
return fmt.Errorf("write profile state: %w", err)