diff --git a/vpn/ipc/conn_nonwindows.go b/vpn/ipc/conn_nonwindows.go index 76266fd8..0a4358e8 100644 --- a/vpn/ipc/conn_nonwindows.go +++ b/vpn/ipc/conn_nonwindows.go @@ -48,7 +48,7 @@ func listen() (net.Listener, error) { Listener: listener, path: path, } - // ensure listener is closed + // Close the listener when the socket wrapper is garbage-collected. runtime.AddCleanup(socket, func(ll *net.UnixListener) { ll.Close() }, listener) @@ -84,10 +84,48 @@ func getConnPeer(conn net.Conn) (p usr, err error) { } uidStr := strconv.FormatUint(uint64(uid), 10) + peer, err := getPeerUser(uid, uidStr) + if err != nil { + return p, err + } + return peer, nil +} + +func linuxUserInControlGroup(u *user.User) (bool, error) { + controlGroupGID, err := controlGroupGID() + if err != nil { + return false, err + } + gids, err := u.GroupIds() + if err != nil { + return false, fmt.Errorf("lookup groups for %s: %w", u.Username, err) + } + for _, gid := range gids { + if gid == controlGroupGID { + return true, nil + } + } + return false, nil +} + +func getPeerUser(uid uint32, uidStr string) (usr, error) { u, err := user.LookupId(uidStr) if err != nil { - return p, fmt.Errorf("lookup user id %v: %w", uid, err) + return usr{}, fmt.Errorf("lookup user id %v: %w", uid, err) + } + + if runtime.GOOS == "linux" { + inControlGroup, err := linuxUserInControlGroup(u) + if err != nil { + return usr{}, err + } + return usr{ + uid: uidStr, + uname: u.Username, + inControlGroup: inControlGroup, + }, nil } + return usr{ uid: uidStr, uname: u.Username, diff --git a/vpn/ipc/control_group_mobile.go b/vpn/ipc/control_group_mobile.go new file mode 100644 index 00000000..fb5d1a3b --- /dev/null +++ b/vpn/ipc/control_group_mobile.go @@ -0,0 +1,13 @@ +//go:build android || ios + +package ipc + +import "fmt" + +func controlGroupGID() (string, error) { + return "", fmt.Errorf("control group lookup is unsupported on %s", controlGroup) +} + +func controlGroupGIDInt() (int, error) { + return 0, fmt.Errorf("control group gid conversion is unsupported on %s", controlGroup) +} diff --git a/vpn/ipc/control_group_nonwindows.go b/vpn/ipc/control_group_nonwindows.go new file mode 100644 index 00000000..d3086175 --- /dev/null +++ b/vpn/ipc/control_group_nonwindows.go @@ -0,0 +1,42 @@ +//go:build !android && !ios && !windows + +package ipc + +import ( + "fmt" + "os/user" + "strconv" +) + +func controlGroupInfo() (*user.Group, error) { + group, err := user.LookupGroup(controlGroup) + if err != nil { + return nil, fmt.Errorf( + "lookup %s group: %w. Create the %s group on this system and add the users that should access IPC, or configure a different control group if supported", + controlGroup, + err, + controlGroup, + ) + } + return group, nil +} + +func controlGroupGID() (string, error) { + group, err := controlGroupInfo() + if err != nil { + return "", err + } + return group.Gid, nil +} + +func controlGroupGIDInt() (int, error) { + gid, err := controlGroupGID() + if err != nil { + return 0, err + } + parsed, err := strconv.Atoi(gid) + if err != nil { + return 0, fmt.Errorf("convert %s gid %s: %w", controlGroup, gid, err) + } + return parsed, nil +} diff --git a/vpn/ipc/middlewares.go b/vpn/ipc/middlewares.go index 56716242..90741aed 100644 --- a/vpn/ipc/middlewares.go +++ b/vpn/ipc/middlewares.go @@ -61,5 +61,5 @@ func authPeer(next http.Handler) http.Handler { } func peerCanAccess(peer usr) bool { - return peer.isAdmin + return peer.isAdmin || peer.inControlGroup || peer.uid == "0" } diff --git a/vpn/ipc/socket.go b/vpn/ipc/socket.go index e04ae70a..c7816c80 100644 --- a/vpn/ipc/socket.go +++ b/vpn/ipc/socket.go @@ -10,6 +10,8 @@ import ( "strconv" ) +const controlGroup = "lantern" + // use a var so it can be overridden in tests var _socketPath = "/var/run/lantern/lanternd.sock" @@ -25,8 +27,24 @@ func socketPath() string { func setPermissions() error { path := socketPath() if runtime.GOOS == "linux" { - // we'll check if user is sudoer to restrict access - return os.Chmod(socketPath(), 0666) + if _testing || os.Geteuid() != 0 { + if err := os.Chmod(path, 0600); err != nil { + return fmt.Errorf("chmod %s: %w", path, err) + } + return nil + } + + gid, err := controlGroupGIDInt() + if err != nil { + return err + } + if err := os.Chown(path, 0, gid); err != nil { + return fmt.Errorf("chown %s: %w", path, err) + } + if err := os.Chmod(path, 0660); err != nil { + return fmt.Errorf("chmod %s: %w", path, err) + } + return nil } // chown admin group and let the OS restrict access diff --git a/vpn/ipc/usr.go b/vpn/ipc/usr.go index a40f4a64..cf4a325d 100644 --- a/vpn/ipc/usr.go +++ b/vpn/ipc/usr.go @@ -23,9 +23,10 @@ func init() { type usrKey struct{} type usr struct { - uid string - uname string - isAdmin bool + uid string + uname string + isAdmin bool + inControlGroup bool } func contextWithUsr(ctx context.Context, u usr) context.Context {