diff --git a/cli/cli.go b/cli/cli.go index 0554f6d..88ee226 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -6,11 +6,15 @@ import ( "log/slog" "os" "os/signal" + "os/user" + "path/filepath" + "strconv" "strings" "syscall" "github.com/coder/jail" "github.com/coder/jail/audit" + "github.com/coder/jail/namespace" "github.com/coder/jail/rules" "github.com/coder/jail/tls" "github.com/coder/serpent" @@ -64,35 +68,12 @@ Examples: } } -// setupLogging creates a slog logger with the specified level -func setupLogging(logLevel string) *slog.Logger { - var level slog.Level - switch strings.ToLower(logLevel) { - case "error": - level = slog.LevelError - case "warn": - level = slog.LevelWarn - case "info": - level = slog.LevelInfo - case "debug": - level = slog.LevelDebug - default: - level = slog.LevelWarn // Default to warn if invalid level - } - - // Create a standard slog logger with the appropriate level - handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ - Level: level, - }) - - return slog.New(handler) -} - // Run executes the jail command with the given configuration and arguments func Run(ctx context.Context, config Config, args []string) error { ctx, cancel := context.WithCancel(ctx) defer cancel() logger := setupLogging(config.LogLevel) + userInfo := getUserInfo() // Get command arguments if len(args) == 0 { @@ -118,7 +99,10 @@ func Run(ctx context.Context, config Config, args []string) error { auditor := audit.NewLoggingAuditor(logger) // Create certificate manager - certManager, err := tls.NewCertificateManager(logger) + certManager, err := tls.NewCertificateManager(tls.Config{ + Logger: logger, + ConfigDir: userInfo.ConfigDir, + }) if err != nil { logger.Error("Failed to create certificate manager", "error", err) return fmt.Errorf("failed to create certificate manager: %v", err) @@ -173,3 +157,101 @@ func Run(ctx context.Context, config Config, args []string) error { return nil } + +func getUserInfo() namespace.UserInfo { + // get the user info of the original user even if we are running under sudo + sudoUser := os.Getenv("SUDO_USER") + + // If running under sudo, get original user information + if sudoUser != "" { + user, err := user.Lookup(sudoUser) + if err != nil { + // Fallback to current user if lookup fails + return getCurrentUserInfo() + } + + // Parse SUDO_UID and SUDO_GID + uid := 0 + gid := 0 + + if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" { + if parsedUID, err := strconv.Atoi(sudoUID); err == nil { + uid = parsedUID + } + } + + if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" { + if parsedGID, err := strconv.Atoi(sudoGID); err == nil { + gid = parsedGID + } + } + + configDir := getConfigDir(user.HomeDir) + + return namespace.UserInfo{ + Username: sudoUser, + Uid: uid, + Gid: gid, + HomeDir: user.HomeDir, + ConfigDir: configDir, + } + } + + // Not running under sudo, use current user + return getCurrentUserInfo() +} + +// setupLogging creates a slog logger with the specified level +func setupLogging(logLevel string) *slog.Logger { + var level slog.Level + switch strings.ToLower(logLevel) { + case "error": + level = slog.LevelError + case "warn": + level = slog.LevelWarn + case "info": + level = slog.LevelInfo + case "debug": + level = slog.LevelDebug + default: + level = slog.LevelWarn // Default to warn if invalid level + } + + // Create a standard slog logger with the appropriate level + handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: level, + }) + + return slog.New(handler) +} + +// getCurrentUserInfo gets information for the current user +func getCurrentUserInfo() namespace.UserInfo { + currentUser, err := user.Current() + if err != nil { + // Fallback with empty values if we can't get user info + return namespace.UserInfo{} + } + + uid, _ := strconv.Atoi(currentUser.Uid) + gid, _ := strconv.Atoi(currentUser.Gid) + + configDir := getConfigDir(currentUser.HomeDir) + + return namespace.UserInfo{ + Username: currentUser.Username, + Uid: uid, + Gid: gid, + HomeDir: currentUser.HomeDir, + ConfigDir: configDir, + } +} + +// getConfigDir determines the config directory based on XDG_CONFIG_HOME or fallback +func getConfigDir(homeDir string) string { + // Use XDG_CONFIG_HOME if set, otherwise fallback to ~/.config/coder_jail + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { + return filepath.Join(xdgConfigHome, "coder_jail") + } + return filepath.Join(homeDir, ".config", "coder_jail") +} diff --git a/jail b/jail new file mode 100755 index 0000000..458e8ab Binary files /dev/null and b/jail differ diff --git a/namespace/linux.go b/namespace/linux.go index accc168..173d536 100644 --- a/namespace/linux.go +++ b/namespace/linux.go @@ -7,8 +7,6 @@ import ( "log/slog" "os" "os/exec" - "os/user" - "strconv" "strings" "syscall" "time" @@ -23,6 +21,10 @@ type Linux struct { procAttr *syscall.SysProcAttr httpProxyPort int httpsProxyPort int + user string + homeDir string + uid int + gid int } // NewLinux creates a new Linux network jail instance @@ -84,42 +86,17 @@ func (l *Linux) Start() error { } } - // When running under sudo, restore essential user environment variables - sudoUser := os.Getenv("SUDO_USER") - if sudoUser != "" { - user, err := user.Lookup(sudoUser) - if err == nil { - // Set HOME to original user's home directory - l.preparedEnv["HOME"] = user.HomeDir - // Set USER to original username - l.preparedEnv["USER"] = sudoUser - // Set LOGNAME to original username (some tools check this instead of USER) - l.preparedEnv["LOGNAME"] = sudoUser - l.logger.Debug("Restored user environment", "home", user.HomeDir, "user", sudoUser) - } - } + // Set HOME to original user's home directory + l.preparedEnv["HOME"] = l.homeDir + // Set USER to original username + l.preparedEnv["USER"] = l.user + // Set LOGNAME to original username (some tools check this instead of USER) + l.preparedEnv["LOGNAME"] = l.user - // Prepare process credentials once during setup - l.logger.Debug("Preparing process credentials") - var gid, uid int - sudoUID := os.Getenv("SUDO_UID") - if sudoUID != "" { - uid, err = strconv.Atoi(sudoUID) - if err != nil { - l.logger.Warn("Invalid SUDO_UID, subprocess will run as root", "sudo_uid", sudoUID, "error", err) - } - } - sudoGID := os.Getenv("SUDO_GID") - if sudoGID != "" { - gid, err = strconv.Atoi(sudoGID) - if err != nil { - l.logger.Warn("Invalid SUDO_GID, subprocess will run as root", "sudo_gid", sudoGID, "error", err) - } - } l.procAttr = &syscall.SysProcAttr{ Credential: &syscall.Credential{ - Uid: uint32(uid), - Gid: uint32(gid), + Uid: uint32(l.uid), + Gid: uint32(l.gid), }, } @@ -303,4 +280,4 @@ func (l *Linux) removeNamespace() error { return fmt.Errorf("failed to remove namespace: %v", err) } return nil -} +} \ No newline at end of file diff --git a/namespace/macos.go b/namespace/macos.go index a9db968..30d9fd8 100644 --- a/namespace/macos.go +++ b/namespace/macos.go @@ -7,7 +7,6 @@ import ( "log/slog" "os" "os/exec" - "os/user" "strconv" "strings" "syscall" @@ -20,7 +19,7 @@ const ( // MacOSNetJail implements network jail using macOS PF (Packet Filter) and group-based isolation type MacOSNetJail struct { - groupID int + restrictedGid int pfRulesPath string mainRulesPath string logger *slog.Logger @@ -28,6 +27,7 @@ type MacOSNetJail struct { procAttr *syscall.SysProcAttr httpProxyPort int httpsProxyPort int + userInfo UserInfo } // NewMacOS creates a new macOS network jail instance @@ -49,6 +49,7 @@ func NewMacOS(config Config) (*MacOSNetJail, error) { preparedEnv: preparedEnv, httpProxyPort: config.HttpProxyPort, httpsProxyPort: config.HttpsProxyPort, + userInfo: config.UserInfo, }, nil } @@ -83,47 +84,23 @@ func (m *MacOSNetJail) Start() error { } } - // When running under sudo, restore essential user environment variables - sudoUser := os.Getenv("SUDO_USER") - if sudoUser != "" { - user, err := user.Lookup(sudoUser) - if err == nil { - // Set HOME to original user's home directory - m.preparedEnv["HOME"] = user.HomeDir - // Set USER to original username - m.preparedEnv["USER"] = sudoUser - // Set LOGNAME to original username (some tools check this instead of USER) - m.preparedEnv["LOGNAME"] = sudoUser - m.logger.Debug("Restored user environment", "home", user.HomeDir, "user", sudoUser) - } - } + // Set HOME to original user's home directory + m.preparedEnv["HOME"] = m.userInfo.HomeDir + // Set USER to original username + m.preparedEnv["USER"] = m.userInfo.Username + // Set LOGNAME to original username (some tools check this instead of USER) + m.preparedEnv["LOGNAME"] = m.userInfo.Username // Prepare process credentials once during setup m.logger.Debug("Preparing process credentials") + // Use original user ID but KEEP the jail group for network isolation procAttr := &syscall.SysProcAttr{ Credential: &syscall.Credential{ - Gid: uint32(m.groupID), + Uid: uint32(m.userInfo.Uid), + Gid: uint32(m.restrictedGid), }, } - // Drop privileges to original user if running under sudo - sudoUID := os.Getenv("SUDO_UID") - if sudoUID != "" { - uid, err := strconv.Atoi(sudoUID) - if err != nil { - m.logger.Warn("Invalid SUDO_UID, subprocess will run as root", "sudo_uid", sudoUID, "error", err) - } else { - // Use original user ID but KEEP the jail group for network isolation - procAttr = &syscall.SysProcAttr{ - Credential: &syscall.Credential{ - Uid: uint32(uid), - Gid: uint32(m.groupID), // Keep jail group, not original user's group - }, - } - m.logger.Debug("Dropping privileges to original user with jail group", "uid", uid, "jail_gid", m.groupID) - } - } - // Store prepared process attributes for use in Command method m.procAttr = procAttr @@ -136,7 +113,7 @@ func (m *MacOSNetJail) Command(command []string) *exec.Cmd { m.logger.Debug("Command called", "command", command) // Create command directly (no sg wrapper needed) - m.logger.Debug("Creating command with group membership", "groupID", m.groupID) + m.logger.Debug("Creating command with group membership", "groupID", m.restrictedGid) cmd := exec.Command(command[0], command[1:]...) m.logger.Debug("Full command args", "args", command) @@ -190,7 +167,7 @@ func (m *MacOSNetJail) ensureGroup() error { if err != nil { return fmt.Errorf("failed to parse GID: %v", err) } - m.groupID = gid + m.restrictedGid = gid return nil } } @@ -219,7 +196,7 @@ func (m *MacOSNetJail) ensureGroup() error { if err != nil { return fmt.Errorf("failed to parse GID: %v", err) } - m.groupID = gid + m.restrictedGid = gid return nil } } @@ -276,15 +253,15 @@ pass out on %s route-to (lo0 127.0.0.1) inet proto tcp from any to any group %d # Allow all loopback traffic pass on lo0 all `, - m.groupID, + m.restrictedGid, iface, m.httpsProxyPort, // Use HTTPS proxy port for all TCP traffic - m.groupID, + m.restrictedGid, iface, - m.groupID, + m.restrictedGid, ) - m.logger.Debug("Comprehensive TCP jailing enabled for macOS", "group_id", m.groupID, "proxy_port", m.httpsProxyPort) + m.logger.Debug("Comprehensive TCP jailing enabled for macOS", "group_id", m.restrictedGid, "proxy_port", m.httpsProxyPort) return rules, nil } diff --git a/namespace/name.go b/namespace/name.go new file mode 100644 index 0000000..235ab30 --- /dev/null +++ b/namespace/name.go @@ -0,0 +1,14 @@ +package namespace + +import ( + "fmt" + "time" +) + +const ( + prefix = "coder_jail" +) + +func newNamespaceName() string { + return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano()%10000000) +} diff --git a/namespace/namespace.go b/namespace/namespace.go index 6436782..5d9a33a 100644 --- a/namespace/namespace.go +++ b/namespace/namespace.go @@ -1,14 +1,8 @@ package namespace import ( - "fmt" "log/slog" "os/exec" - "time" -) - -const ( - namespacePrefix = "coder_jail" ) type Commander interface { @@ -17,14 +11,18 @@ type Commander interface { Close() error } -// JailConfig holds configuration for network jail type Config struct { Logger *slog.Logger HttpProxyPort int HttpsProxyPort int Env map[string]string + UserInfo UserInfo } -func newNamespaceName() string { - return fmt.Sprintf("%s_%d", namespacePrefix, time.Now().UnixNano()%10000000) +type UserInfo struct { + Username string + Uid int + Gid int + HomeDir string + ConfigDir string } diff --git a/tls/tls.go b/tls/tls.go index 75f34aa..e36e6de 100644 --- a/tls/tls.go +++ b/tls/tls.go @@ -12,17 +12,23 @@ import ( "math/big" "net" "os" - "os/user" "path/filepath" - "strconv" "sync" "time" + + "github.com/coder/jail/namespace" ) type Manager interface { SetupTLSAndWriteCACert() (*tls.Config, string, string, error) } +type Config struct { + Logger *slog.Logger + ConfigDir string + UserInfo namespace.UserInfo +} + // CertificateManager manages TLS certificates for the proxy type CertificateManager struct { caKey *rsa.PrivateKey @@ -31,23 +37,20 @@ type CertificateManager struct { mutex sync.RWMutex logger *slog.Logger configDir string + userInfo namespace.UserInfo } // NewCertificateManager creates a new certificate manager -func NewCertificateManager(logger *slog.Logger) (*CertificateManager, error) { - configDir, err := getConfigDir() - if err != nil { - return nil, fmt.Errorf("failed to determine config directory: %v", err) - } - +func NewCertificateManager(config Config) (*CertificateManager, error) { cm := &CertificateManager{ certCache: make(map[string]*tls.Certificate), - logger: logger, - configDir: configDir, + logger: config.Logger, + configDir: config.ConfigDir, + userInfo: config.UserInfo, } // Load or generate CA certificate - err = cm.loadOrGenerateCA() + err := cm.loadOrGenerateCA() if err != nil { return nil, fmt.Errorf("failed to load or generate CA: %v", err) } @@ -58,12 +61,6 @@ func NewCertificateManager(logger *slog.Logger) (*CertificateManager, error) { // SetupTLSAndWriteCACert sets up TLS config and writes CA certificate to file // Returns the TLS config, CA cert path, and config directory func (cm *CertificateManager) SetupTLSAndWriteCACert() (*tls.Config, string, string, error) { - // Get config directory - configDir, err := getConfigDir() - if err != nil { - return nil, "", "", fmt.Errorf("failed to get config directory: %v", err) - } - // Get TLS config tlsConfig := cm.getTLSConfig() @@ -74,13 +71,13 @@ func (cm *CertificateManager) SetupTLSAndWriteCACert() (*tls.Config, string, str } // Write CA certificate to file - caCertPath := filepath.Join(configDir, "ca-cert.pem") + caCertPath := filepath.Join(cm.configDir, "ca-cert.pem") err = os.WriteFile(caCertPath, caCertPEM, 0644) if err != nil { return nil, "", "", fmt.Errorf("failed to write CA certificate file: %v", err) } - return tlsConfig, caCertPath, configDir, nil + return tlsConfig, caCertPath, cm.configDir, nil } // loadOrGenerateCA loads existing CA or generates a new one @@ -182,21 +179,10 @@ func (cm *CertificateManager) generateCA(keyPath, certPath string) error { return fmt.Errorf("failed to create config directory at %s: %v", cm.configDir, err) } - // When running under sudo, ensure the directory is owned by the original user - if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { - if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" { - if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" { - uid, err1 := strconv.Atoi(sudoUID) - gid, err2 := strconv.Atoi(sudoGID) - if err1 == nil && err2 == nil { - // Change ownership of the config directory to the original user - err := os.Chown(cm.configDir, uid, gid) - if err != nil { - cm.logger.Warn("Failed to change config directory ownership", "error", err) - } - } - } - } + // ensure the directory is owned by the original user + err = os.Chown(cm.configDir, cm.userInfo.Uid, cm.userInfo.Gid) + if err != nil { + cm.logger.Warn("Failed to change config directory ownership", "error", err) } // Generate private key @@ -349,42 +335,4 @@ func (cm *CertificateManager) generateServerCertificate(hostname string) (*tls.C cm.logger.Debug("Generated certificate", "hostname", hostname) return tlsCert, nil -} - -// getConfigDir returns the configuration directory path -func getConfigDir() (string, error) { - // When running under sudo, use the original user's home directory - // so the subprocess can access the CA certificate files - var homeDir string - if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { - // Get original user's home directory - if user, err := user.Lookup(sudoUser); err == nil { - homeDir = user.HomeDir - } else { - // Fallback to current user if lookup fails - var err2 error - homeDir, err2 = os.UserHomeDir() - if err2 != nil { - return "", fmt.Errorf("failed to get user home directory: %v", err2) - } - } - } else { - // Normal case - use current user's home - var err error - homeDir, err = os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get user home directory: %v", err) - } - } - - // Use platform-specific config directory - var configDir string - switch { - case os.Getenv("XDG_CONFIG_HOME") != "": - configDir = filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "coder_jail") - default: - configDir = filepath.Join(homeDir, ".config", "coder_jail") - } - - return configDir, nil -} +} \ No newline at end of file