Lint project

This commit is contained in:
JY Tan
2025-12-18 17:02:09 -08:00
parent 55230dd774
commit 14a737a36b
9 changed files with 66 additions and 47 deletions

View File

@@ -44,7 +44,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full project structure and compon
- Keep edits focused and covered by tests where possible. - Keep edits focused and covered by tests where possible.
- Update [ARCHITECTURE.md](ARCHITECTURE.md) when adding features or changing behavior. - Update [ARCHITECTURE.md](ARCHITECTURE.md) when adding features or changing behavior.
- Prefer small, reviewable PRs with a clear rationale. - Prefer small, reviewable PRs with a clear rationale.
- Run `make fmt` and `make lint` before committing. - Run `make fmt` and `make lint` before committing. This project uses `golangci-lint` v1.64.8.
## Testing ## Testing

View File

@@ -77,11 +77,12 @@ Configuration file format (~/.fence.json):
func runCommand(cmd *cobra.Command, args []string) error { func runCommand(cmd *cobra.Command, args []string) error {
var command string var command string
if cmdString != "" { switch {
case cmdString != "":
command = cmdString command = cmdString
} else if len(args) > 0 { case len(args) > 0:
command = strings.Join(args, " ") command = strings.Join(args, " ")
} else { default:
return fmt.Errorf("no command specified. Use -c <command> or provide command arguments") return fmt.Errorf("no command specified. Use -c <command> or provide command arguments")
} }
@@ -148,7 +149,7 @@ func runCommand(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "[fence] Sandboxed command: %s\n", sandboxedCommand) fmt.Fprintf(os.Stderr, "[fence] Sandboxed command: %s\n", sandboxedCommand)
} }
execCmd := exec.Command("sh", "-c", sandboxedCommand) execCmd := exec.Command("sh", "-c", sandboxedCommand) //nolint:gosec // sandboxedCommand is constructed from user input - intentional
execCmd.Stdin = os.Stdin execCmd.Stdin = os.Stdin
execCmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
@@ -159,7 +160,7 @@ func runCommand(cmd *cobra.Command, args []string) error {
go func() { go func() {
sig := <-sigChan sig := <-sigChan
if execCmd.Process != nil { if execCmd.Process != nil {
execCmd.Process.Signal(sig) _ = execCmd.Process.Signal(sig)
} }
// Give child time to exit, then cleanup will happen via defer // Give child time to exit, then cleanup will happen via defer
}() }()

View File

@@ -63,7 +63,7 @@ func DefaultConfigPath() string {
// Load loads configuration from a file path. // Load loads configuration from a file path.
func Load(path string) (*Config, error) { func Load(path string) (*Config, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path) //nolint:gosec // user-provided config path - intentional
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, nil return nil, nil

View File

@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -50,7 +51,8 @@ func (p *HTTPProxy) Start() (int, error) {
p.listener = listener p.listener = listener
p.server = &http.Server{ p.server = &http.Server{
Handler: http.HandlerFunc(p.handleRequest), Handler: http.HandlerFunc(p.handleRequest),
ReadHeaderTimeout: 10 * time.Second,
} }
p.mu.Lock() p.mu.Lock()
@@ -109,7 +111,9 @@ func (p *HTTPProxy) handleConnect(w http.ResponseWriter, r *http.Request) {
port := 443 port := 443
if portStr != "" { if portStr != "" {
fmt.Sscanf(portStr, "%d", &port) if p, err := strconv.Atoi(portStr); err == nil {
port = p
}
} }
// Check if allowed // Check if allowed
@@ -128,7 +132,7 @@ func (p *HTTPProxy) handleConnect(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Gateway", http.StatusBadGateway) http.Error(w, "Bad Gateway", http.StatusBadGateway)
return return
} }
defer targetConn.Close() defer func() { _ = targetConn.Close() }()
// Hijack the connection // Hijack the connection
hijacker, ok := w.(http.Hijacker) hijacker, ok := w.(http.Hijacker)
@@ -142,9 +146,11 @@ func (p *HTTPProxy) handleConnect(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Failed to hijack connection", http.StatusInternalServerError) http.Error(w, "Failed to hijack connection", http.StatusInternalServerError)
return return
} }
defer clientConn.Close() defer func() { _ = clientConn.Close() }()
clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")) if _, err := clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")); err != nil {
return
}
// Pipe data bidirectionally // Pipe data bidirectionally
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -152,12 +158,12 @@ func (p *HTTPProxy) handleConnect(w http.ResponseWriter, r *http.Request) {
go func() { go func() {
defer wg.Done() defer wg.Done()
io.Copy(targetConn, clientConn) _, _ = io.Copy(targetConn, clientConn)
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
io.Copy(clientConn, targetConn) _, _ = io.Copy(clientConn, targetConn)
}() }()
wg.Wait() wg.Wait()
@@ -175,7 +181,9 @@ func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
host := targetURL.Hostname() host := targetURL.Hostname()
port := 80 port := 80
if targetURL.Port() != "" { if targetURL.Port() != "" {
fmt.Sscanf(targetURL.Port(), "%d", &port) if p, err := strconv.Atoi(targetURL.Port()); err == nil {
port = p
}
} else if targetURL.Scheme == "https" { } else if targetURL.Scheme == "https" {
port = 443 port = 443
} }
@@ -216,7 +224,7 @@ func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Gateway", http.StatusBadGateway) http.Error(w, "Bad Gateway", http.StatusBadGateway)
return return
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
// Copy response headers // Copy response headers
for key, values := range resp.Header { for key, values := range resp.Header {
@@ -226,7 +234,7 @@ func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(resp.StatusCode) w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body) _, _ = io.Copy(w, resp.Body)
p.logRequest(r.Method, r.RequestURI, host, resp.StatusCode, "ALLOWED", time.Since(start)) p.logRequest(r.Method, r.RequestURI, host, resp.StatusCode, "ALLOWED", time.Since(start))
} }

View File

@@ -38,7 +38,9 @@ func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge
} }
id := make([]byte, 8) id := make([]byte, 8)
rand.Read(id) if _, err := rand.Read(id); err != nil {
return nil, fmt.Errorf("failed to generate socket ID: %w", err)
}
socketID := hex.EncodeToString(id) socketID := hex.EncodeToString(id)
tmpDir := os.TempDir() tmpDir := os.TempDir()
@@ -56,7 +58,7 @@ func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", httpSocketPath), fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", httpSocketPath),
fmt.Sprintf("TCP:localhost:%d", httpProxyPort), fmt.Sprintf("TCP:localhost:%d", httpProxyPort),
} }
bridge.httpProcess = exec.Command("socat", httpArgs...) bridge.httpProcess = exec.Command("socat", httpArgs...) //nolint:gosec // args constructed from trusted input
if debug { if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Starting HTTP bridge: socat %s\n", strings.Join(httpArgs, " ")) fmt.Fprintf(os.Stderr, "[fence:linux] Starting HTTP bridge: socat %s\n", strings.Join(httpArgs, " "))
} }
@@ -69,7 +71,7 @@ func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", socksSocketPath), fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", socksSocketPath),
fmt.Sprintf("TCP:localhost:%d", socksProxyPort), fmt.Sprintf("TCP:localhost:%d", socksProxyPort),
} }
bridge.socksProcess = exec.Command("socat", socksArgs...) bridge.socksProcess = exec.Command("socat", socksArgs...) //nolint:gosec // args constructed from trusted input
if debug { if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Starting SOCKS bridge: socat %s\n", strings.Join(socksArgs, " ")) fmt.Fprintf(os.Stderr, "[fence:linux] Starting SOCKS bridge: socat %s\n", strings.Join(socksArgs, " "))
} }
@@ -98,17 +100,17 @@ func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge
// Cleanup stops the bridge processes and removes socket files. // Cleanup stops the bridge processes and removes socket files.
func (b *LinuxBridge) Cleanup() { func (b *LinuxBridge) Cleanup() {
if b.httpProcess != nil && b.httpProcess.Process != nil { if b.httpProcess != nil && b.httpProcess.Process != nil {
b.httpProcess.Process.Kill() _ = b.httpProcess.Process.Kill()
b.httpProcess.Wait() _ = b.httpProcess.Wait()
} }
if b.socksProcess != nil && b.socksProcess.Process != nil { if b.socksProcess != nil && b.socksProcess.Process != nil {
b.socksProcess.Process.Kill() _ = b.socksProcess.Process.Kill()
b.socksProcess.Wait() _ = b.socksProcess.Wait()
} }
// Clean up socket files // Clean up socket files
os.Remove(b.HTTPSocketPath) _ = os.Remove(b.HTTPSocketPath)
os.Remove(b.SOCKSSocketPath) _ = os.Remove(b.SOCKSSocketPath)
if b.debug { if b.debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Bridges cleaned up\n") fmt.Fprintf(os.Stderr, "[fence:linux] Bridges cleaned up\n")
@@ -127,7 +129,9 @@ func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
} }
id := make([]byte, 8) id := make([]byte, 8)
rand.Read(id) if _, err := rand.Read(id); err != nil {
return nil, fmt.Errorf("failed to generate socket ID: %w", err)
}
socketID := hex.EncodeToString(id) socketID := hex.EncodeToString(id)
tmpDir := os.TempDir() tmpDir := os.TempDir()
@@ -147,7 +151,7 @@ func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
fmt.Sprintf("TCP-LISTEN:%d,fork,reuseaddr", port), fmt.Sprintf("TCP-LISTEN:%d,fork,reuseaddr", port),
fmt.Sprintf("UNIX-CONNECT:%s,retry=50,interval=0.1", socketPath), fmt.Sprintf("UNIX-CONNECT:%s,retry=50,interval=0.1", socketPath),
} }
proc := exec.Command("socat", args...) proc := exec.Command("socat", args...) //nolint:gosec // args constructed from trusted input
if debug { if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Starting reverse bridge for port %d: socat %s\n", port, strings.Join(args, " ")) fmt.Fprintf(os.Stderr, "[fence:linux] Starting reverse bridge for port %d: socat %s\n", port, strings.Join(args, " "))
} }
@@ -169,14 +173,14 @@ func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
func (b *ReverseBridge) Cleanup() { func (b *ReverseBridge) Cleanup() {
for _, proc := range b.processes { for _, proc := range b.processes {
if proc != nil && proc.Process != nil { if proc != nil && proc.Process != nil {
proc.Process.Kill() _ = proc.Process.Kill()
proc.Wait() _ = proc.Wait()
} }
} }
// Clean up socket files // Clean up socket files
for _, socketPath := range b.SocketPaths { for _, socketPath := range b.SocketPaths {
os.Remove(socketPath) _ = os.Remove(socketPath)
} }
if b.debug { if b.debug {

View File

@@ -18,7 +18,9 @@ var sessionSuffix = generateSessionSuffix()
func generateSessionSuffix() string { func generateSessionSuffix() string {
bytes := make([]byte, 8) bytes := make([]byte, 8)
rand.Read(bytes) if _, err := rand.Read(bytes); err != nil {
panic("failed to generate session suffix: " + err.Error())
}
return "_" + hex.EncodeToString(bytes)[:9] + "_SBX" return "_" + hex.EncodeToString(bytes)[:9] + "_SBX"
} }
@@ -175,7 +177,10 @@ func generateWriteRules(allowPaths, denyPaths []string, allowGitConfig bool, log
// Combine user-specified and mandatory deny patterns // Combine user-specified and mandatory deny patterns
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
allDenyPaths := append(denyPaths, GetMandatoryDenyPatterns(cwd, allowGitConfig)...) mandatoryDeny := GetMandatoryDenyPatterns(cwd, allowGitConfig)
allDenyPaths := make([]string, 0, len(denyPaths)+len(mandatoryDeny))
allDenyPaths = append(allDenyPaths, denyPaths...)
allDenyPaths = append(allDenyPaths, mandatoryDeny...)
for _, pathPattern := range allDenyPaths { for _, pathPattern := range allDenyPaths {
normalized := NormalizePath(pathPattern) normalized := NormalizePath(pathPattern)

View File

@@ -60,7 +60,7 @@ func (m *Manager) Initialize() error {
m.socksProxy = proxy.NewSOCKSProxy(filter, m.debug, m.monitor) m.socksProxy = proxy.NewSOCKSProxy(filter, m.debug, m.monitor)
socksPort, err := m.socksProxy.Start() socksPort, err := m.socksProxy.Start()
if err != nil { if err != nil {
m.httpProxy.Stop() _ = m.httpProxy.Stop()
return fmt.Errorf("failed to start SOCKS proxy: %w", err) return fmt.Errorf("failed to start SOCKS proxy: %w", err)
} }
m.socksPort = socksPort m.socksPort = socksPort
@@ -69,8 +69,8 @@ func (m *Manager) Initialize() error {
if platform.Detect() == platform.Linux { if platform.Detect() == platform.Linux {
bridge, err := NewLinuxBridge(m.httpPort, m.socksPort, m.debug) bridge, err := NewLinuxBridge(m.httpPort, m.socksPort, m.debug)
if err != nil { if err != nil {
m.httpProxy.Stop() _ = m.httpProxy.Stop()
m.socksProxy.Stop() _ = m.socksProxy.Stop()
return fmt.Errorf("failed to initialize Linux bridge: %w", err) return fmt.Errorf("failed to initialize Linux bridge: %w", err)
} }
m.linuxBridge = bridge m.linuxBridge = bridge
@@ -80,8 +80,8 @@ func (m *Manager) Initialize() error {
reverseBridge, err := NewReverseBridge(m.exposedPorts, m.debug) reverseBridge, err := NewReverseBridge(m.exposedPorts, m.debug)
if err != nil { if err != nil {
m.linuxBridge.Cleanup() m.linuxBridge.Cleanup()
m.httpProxy.Stop() _ = m.httpProxy.Stop()
m.socksProxy.Stop() _ = m.socksProxy.Stop()
return fmt.Errorf("failed to initialize reverse bridge: %w", err) return fmt.Errorf("failed to initialize reverse bridge: %w", err)
} }
m.reverseBridge = reverseBridge m.reverseBridge = reverseBridge
@@ -121,10 +121,10 @@ func (m *Manager) Cleanup() {
m.linuxBridge.Cleanup() m.linuxBridge.Cleanup()
} }
if m.httpProxy != nil { if m.httpProxy != nil {
m.httpProxy.Stop() _ = m.httpProxy.Stop()
} }
if m.socksProxy != nil { if m.socksProxy != nil {
m.socksProxy.Stop() _ = m.socksProxy.Stop()
} }
m.logDebug("Sandbox manager cleaned up") m.logDebug("Sandbox manager cleaned up")
} }

View File

@@ -94,8 +94,8 @@ func (m *LogMonitor) Stop() {
} }
if m.cmd != nil && m.cmd.Process != nil { if m.cmd != nil && m.cmd.Process != nil {
m.cmd.Process.Kill() _ = m.cmd.Process.Kill()
m.cmd.Wait() _ = m.cmd.Wait()
} }
m.running = false m.running = false

View File

@@ -26,14 +26,15 @@ func NormalizePath(pathPattern string) string {
normalized := pathPattern normalized := pathPattern
// Expand ~ to home directory // Expand ~ and relative paths
if pathPattern == "~" { switch {
case pathPattern == "~":
normalized = home normalized = home
} else if strings.HasPrefix(pathPattern, "~/") { case strings.HasPrefix(pathPattern, "~/"):
normalized = filepath.Join(home, pathPattern[2:]) normalized = filepath.Join(home, pathPattern[2:])
} else if strings.HasPrefix(pathPattern, "./") || strings.HasPrefix(pathPattern, "../") { case strings.HasPrefix(pathPattern, "./"), strings.HasPrefix(pathPattern, "../"):
normalized, _ = filepath.Abs(filepath.Join(cwd, pathPattern)) normalized, _ = filepath.Abs(filepath.Join(cwd, pathPattern))
} else if !filepath.IsAbs(pathPattern) && !ContainsGlobChars(pathPattern) { case !filepath.IsAbs(pathPattern) && !ContainsGlobChars(pathPattern):
normalized, _ = filepath.Abs(filepath.Join(cwd, pathPattern)) normalized, _ = filepath.Abs(filepath.Join(cwd, pathPattern))
} }