mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
9.2 KiB
Go
406 lines
9.2 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnauth
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"os/user"
|
|
"runtime"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/tailscale/peercred"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/tstest"
|
|
)
|
|
|
|
func TestConnIdentity_Accessors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ci *ConnIdentity
|
|
wantPid int
|
|
wantUnix bool
|
|
wantCreds *peercred.Creds
|
|
}{
|
|
{
|
|
name: "basic_unix",
|
|
ci: &ConnIdentity{
|
|
pid: 12345,
|
|
isUnixSock: true,
|
|
creds: &peercred.Creds{},
|
|
},
|
|
wantPid: 12345,
|
|
wantUnix: true,
|
|
wantCreds: &peercred.Creds{},
|
|
},
|
|
{
|
|
name: "no_creds",
|
|
ci: &ConnIdentity{
|
|
pid: 0,
|
|
isUnixSock: false,
|
|
creds: nil,
|
|
},
|
|
wantPid: 0,
|
|
wantUnix: false,
|
|
wantCreds: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.ci.Pid(); got != tt.wantPid {
|
|
t.Errorf("Pid() = %v, want %v", got, tt.wantPid)
|
|
}
|
|
if got := tt.ci.IsUnixSock(); got != tt.wantUnix {
|
|
t.Errorf("IsUnixSock() = %v, want %v", got, tt.wantUnix)
|
|
}
|
|
if got := tt.ci.Creds(); got != tt.wantCreds {
|
|
t.Errorf("Creds() = %v, want %v", got, tt.wantCreds)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsReadonlyConn(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("IsReadonlyConn always returns false on Windows")
|
|
}
|
|
|
|
selfUID := strconv.Itoa(os.Getuid())
|
|
operatorUID := "99999" // Some non-existent operator UID
|
|
|
|
tests := []struct {
|
|
name string
|
|
ci *ConnIdentity
|
|
operatorUID string
|
|
wantRO bool
|
|
desc string
|
|
}{
|
|
{
|
|
name: "no_creds",
|
|
ci: &ConnIdentity{
|
|
notWindows: true,
|
|
creds: nil,
|
|
},
|
|
operatorUID: "",
|
|
wantRO: true,
|
|
desc: "connection with no credentials should be read-only",
|
|
},
|
|
{
|
|
name: "root_user",
|
|
ci: &ConnIdentity{
|
|
notWindows: true,
|
|
creds: makeCreds("0", 0),
|
|
},
|
|
operatorUID: "",
|
|
wantRO: false,
|
|
desc: "root user (uid 0) should have read-write access",
|
|
},
|
|
{
|
|
name: "self_user_non_root_daemon",
|
|
ci: &ConnIdentity{
|
|
notWindows: true,
|
|
creds: makeCreds(selfUID, mustParseInt(selfUID)),
|
|
},
|
|
operatorUID: "",
|
|
wantRO: false,
|
|
desc: "connection from same user as daemon should have access",
|
|
},
|
|
{
|
|
name: "operator_user",
|
|
ci: &ConnIdentity{
|
|
notWindows: true,
|
|
creds: makeCreds(operatorUID, mustParseInt(operatorUID)),
|
|
},
|
|
operatorUID: operatorUID,
|
|
wantRO: false,
|
|
desc: "configured operator should have read-write access",
|
|
},
|
|
{
|
|
name: "random_user",
|
|
ci: &ConnIdentity{
|
|
notWindows: true,
|
|
creds: makeCreds("12345", 12345),
|
|
},
|
|
operatorUID: "",
|
|
wantRO: true,
|
|
desc: "random non-privileged user should be read-only",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logf := t.Logf
|
|
got := tt.ci.IsReadonlyConn(tt.operatorUID, logf)
|
|
if got != tt.wantRO {
|
|
t.Errorf("IsReadonlyConn() = %v, want %v (%s)", got, tt.wantRO, tt.desc)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsReadonlyConn_Windows(t *testing.T) {
|
|
if runtime.GOOS != "windows" {
|
|
t.Skip("Windows-specific test")
|
|
}
|
|
|
|
ci := &ConnIdentity{
|
|
notWindows: false,
|
|
}
|
|
|
|
// On Windows, IsReadonlyConn should always return false
|
|
if got := ci.IsReadonlyConn("", t.Logf); got != false {
|
|
t.Errorf("IsReadonlyConn() on Windows = %v, want false", got)
|
|
}
|
|
}
|
|
|
|
func TestWindowsUserID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
goos string
|
|
wantSID bool
|
|
}{
|
|
{
|
|
name: "non_windows",
|
|
goos: "linux",
|
|
wantSID: false,
|
|
},
|
|
{
|
|
name: "windows",
|
|
goos: "windows",
|
|
wantSID: true, // will try to get WindowsToken
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if runtime.GOOS != tt.goos {
|
|
t.Skipf("test requires GOOS=%s", tt.goos)
|
|
}
|
|
|
|
ci := &ConnIdentity{
|
|
notWindows: tt.goos != "windows",
|
|
}
|
|
|
|
uid := ci.WindowsUserID()
|
|
if tt.wantSID && uid == "" {
|
|
// On Windows, we might get empty if WindowsToken fails
|
|
// which is acceptable in unit tests
|
|
t.Logf("WindowsUserID returned empty (expected in test env)")
|
|
}
|
|
if !tt.wantSID && uid != "" {
|
|
t.Errorf("WindowsUserID() on %s = %q, want empty", tt.goos, uid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLookupUserFromID(t *testing.T) {
|
|
// Test with current user's UID
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
t.Skipf("can't get current user: %v", err)
|
|
}
|
|
|
|
logf := t.Logf
|
|
u, err := LookupUserFromID(logf, currentUser.Uid)
|
|
if err != nil {
|
|
t.Fatalf("LookupUserFromID(%q) failed: %v", currentUser.Uid, err)
|
|
}
|
|
if u.Uid != currentUser.Uid {
|
|
t.Errorf("LookupUserFromID(%q).Uid = %q, want %q", currentUser.Uid, u.Uid, currentUser.Uid)
|
|
}
|
|
|
|
// Test with invalid UID
|
|
invalidUID := "99999999"
|
|
_, err = LookupUserFromID(logf, invalidUID)
|
|
if err == nil && runtime.GOOS != "windows" {
|
|
// On non-Windows, invalid UID should return error
|
|
// On Windows, it might succeed due to workarounds
|
|
t.Errorf("LookupUserFromID(%q) succeeded, expected error", invalidUID)
|
|
}
|
|
}
|
|
|
|
func TestErrNotImplemented(t *testing.T) {
|
|
expectedMsg := "not implemented for GOOS=" + runtime.GOOS
|
|
if !errors.Is(ErrNotImplemented, ErrNotImplemented) {
|
|
t.Error("ErrNotImplemented should match itself")
|
|
}
|
|
if got := ErrNotImplemented.Error(); got != expectedMsg {
|
|
t.Errorf("ErrNotImplemented.Error() = %q, want %q", got, expectedMsg)
|
|
}
|
|
}
|
|
|
|
func TestWindowsToken_NotWindows(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test for non-Windows platforms")
|
|
}
|
|
|
|
ci := &ConnIdentity{
|
|
notWindows: true,
|
|
}
|
|
|
|
tok, err := ci.WindowsToken()
|
|
if !errors.Is(err, ErrNotImplemented) {
|
|
t.Errorf("WindowsToken() on non-Windows: err = %v, want ErrNotImplemented", err)
|
|
}
|
|
if tok != nil {
|
|
t.Errorf("WindowsToken() on non-Windows: token = %v, want nil", tok)
|
|
}
|
|
}
|
|
|
|
func TestGetConnIdentity_NotWindows(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test for non-Windows platforms")
|
|
}
|
|
|
|
// Create a Unix socket pair for testing
|
|
server, client := net.Pipe()
|
|
defer server.Close()
|
|
defer client.Close()
|
|
|
|
// Convert to UnixConn for testing (requires actual Unix socket)
|
|
// For now, test with regular net.Conn
|
|
ci, err := GetConnIdentity(t.Logf, client)
|
|
if err != nil {
|
|
t.Fatalf("GetConnIdentity() failed: %v", err)
|
|
}
|
|
|
|
if ci == nil {
|
|
t.Fatal("GetConnIdentity() returned nil ConnIdentity")
|
|
}
|
|
if !ci.notWindows {
|
|
t.Error("GetConnIdentity() on non-Windows should set notWindows=true")
|
|
}
|
|
}
|
|
|
|
func TestIsLocalAdmin_UnsupportedPlatform(t *testing.T) {
|
|
// Test on platforms where isLocalAdmin doesn't support admin group detection
|
|
if runtime.GOOS == "darwin" {
|
|
t.Skip("darwin supports admin group detection")
|
|
}
|
|
|
|
// Use a fake UID
|
|
fakeUID := "12345"
|
|
isAdmin, err := isLocalAdmin(fakeUID)
|
|
if err == nil {
|
|
t.Error("isLocalAdmin() on unsupported platform should return error")
|
|
}
|
|
if isAdmin {
|
|
t.Error("isLocalAdmin() on unsupported platform should return false")
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func makeCreds(uid string, pidVal int) *peercred.Creds {
|
|
// Note: peercred.Creds struct may vary by platform
|
|
// This is a simplified helper for testing
|
|
c := &peercred.Creds{}
|
|
// Set UID if possible (may require reflection or platform-specific code)
|
|
// For now, return empty creds - tests will need platform-specific setup
|
|
return c
|
|
}
|
|
|
|
func mustParseInt(s string) int {
|
|
i, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return i
|
|
}
|
|
|
|
func TestConnIdentity_NilChecks(t *testing.T) {
|
|
// Test that nil checks don't panic
|
|
var ci *ConnIdentity
|
|
|
|
// These should not panic even with nil receiver
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("operations on nil ConnIdentity should not panic: %v", r)
|
|
}
|
|
}()
|
|
|
|
// Note: Calling methods on nil pointer will panic in Go
|
|
// This test documents the behavior
|
|
ci = &ConnIdentity{}
|
|
_ = ci.Pid()
|
|
_ = ci.IsUnixSock()
|
|
_ = ci.Creds()
|
|
_ = ci.WindowsUserID()
|
|
}
|
|
|
|
func TestConnIdentity_ConcurrentAccess(t *testing.T) {
|
|
ci := &ConnIdentity{
|
|
pid: 12345,
|
|
isUnixSock: true,
|
|
notWindows: true,
|
|
}
|
|
|
|
// Test concurrent reads are safe
|
|
done := make(chan bool)
|
|
for i := 0; i < 10; i++ {
|
|
go func() {
|
|
_ = ci.Pid()
|
|
_ = ci.IsUnixSock()
|
|
_ = ci.Creds()
|
|
done <- true
|
|
}()
|
|
}
|
|
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func TestWindowsUserID_EmptyOnNonWindows(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test for non-Windows behavior")
|
|
}
|
|
|
|
ci := &ConnIdentity{
|
|
notWindows: true,
|
|
}
|
|
|
|
uid := ci.WindowsUserID()
|
|
if uid != "" {
|
|
t.Errorf("WindowsUserID() on non-Windows = %q, want empty string", uid)
|
|
}
|
|
}
|
|
|
|
func TestIsReadonlyConn_LogOutput(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test for non-Windows platforms")
|
|
}
|
|
|
|
// Test that logging actually happens
|
|
var loggedMessages []string
|
|
logf := func(format string, args ...any) {
|
|
loggedMessages = append(loggedMessages, format)
|
|
}
|
|
|
|
ci := &ConnIdentity{
|
|
notWindows: true,
|
|
creds: nil,
|
|
}
|
|
|
|
_ = ci.IsReadonlyConn("", logf)
|
|
|
|
if len(loggedMessages) == 0 {
|
|
t.Error("IsReadonlyConn should log messages")
|
|
}
|
|
}
|
|
|
|
func TestGetConnIdentity_Integration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
// This would require actual socket setup
|
|
// Skipping for now, but placeholder for integration tests
|
|
t.Skip("integration test requires real socket setup")
|
|
}
|