diff --git a/Makefile b/Makefile index 0b9b09b..1be790c 100644 --- a/Makefile +++ b/Makefile @@ -8,24 +8,24 @@ BINARY_UNIX=$(BINARY_NAME)_unix TUN2SOCKS_VERSION=v2.5.2 TUN2SOCKS_BIN_DIR=internal/sandbox/bin -.PHONY: all build build-ci build-linux test test-ci clean deps install-lint-tools setup setup-ci run fmt lint release release-minor download-tun2socks help +.PHONY: all build build-ci build-linux build-darwin test test-ci clean deps install-lint-tools setup setup-ci run fmt lint release release-minor download-tun2socks help all: build +TUN2SOCKS_PLATFORMS=linux-amd64 linux-arm64 darwin-amd64 darwin-arm64 + download-tun2socks: - @echo "Downloading tun2socks $(TUN2SOCKS_VERSION)..." @mkdir -p $(TUN2SOCKS_BIN_DIR) - @curl -sL "https://github.com/xjasonlyu/tun2socks/releases/download/$(TUN2SOCKS_VERSION)/tun2socks-linux-amd64.zip" -o /tmp/tun2socks-linux-amd64.zip - @unzip -o -q /tmp/tun2socks-linux-amd64.zip -d /tmp/tun2socks-amd64 - @mv /tmp/tun2socks-amd64/tun2socks-linux-amd64 $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-amd64 - @chmod +x $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-amd64 - @rm -rf /tmp/tun2socks-linux-amd64.zip /tmp/tun2socks-amd64 - @curl -sL "https://github.com/xjasonlyu/tun2socks/releases/download/$(TUN2SOCKS_VERSION)/tun2socks-linux-arm64.zip" -o /tmp/tun2socks-linux-arm64.zip - @unzip -o -q /tmp/tun2socks-linux-arm64.zip -d /tmp/tun2socks-arm64 - @mv /tmp/tun2socks-arm64/tun2socks-linux-arm64 $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-arm64 - @chmod +x $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-arm64 - @rm -rf /tmp/tun2socks-linux-arm64.zip /tmp/tun2socks-arm64 - @echo "tun2socks binaries downloaded to $(TUN2SOCKS_BIN_DIR)/" + @for platform in $(TUN2SOCKS_PLATFORMS); do \ + if [ ! -f $(TUN2SOCKS_BIN_DIR)/tun2socks-$$platform ]; then \ + echo "Downloading tun2socks-$$platform $(TUN2SOCKS_VERSION)..."; \ + curl -sL "https://github.com/xjasonlyu/tun2socks/releases/download/$(TUN2SOCKS_VERSION)/tun2socks-$$platform.zip" -o /tmp/tun2socks-$$platform.zip; \ + unzip -o -q /tmp/tun2socks-$$platform.zip -d /tmp/tun2socks-$$platform; \ + mv /tmp/tun2socks-$$platform/tun2socks-$$platform $(TUN2SOCKS_BIN_DIR)/tun2socks-$$platform; \ + chmod +x $(TUN2SOCKS_BIN_DIR)/tun2socks-$$platform; \ + rm -rf /tmp/tun2socks-$$platform.zip /tmp/tun2socks-$$platform; \ + fi; \ + done build: download-tun2socks @echo "Building $(BINARY_NAME)..." @@ -53,6 +53,7 @@ clean: rm -f $(BINARY_UNIX) rm -f coverage.out rm -f $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-* + rm -f $(TUN2SOCKS_BIN_DIR)/tun2socks-darwin-* deps: @echo "Downloading dependencies..." @@ -63,7 +64,7 @@ build-linux: download-tun2socks @echo "Building for Linux..." CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_UNIX) -v ./cmd/greywall -build-darwin: +build-darwin: download-tun2socks @echo "Building for macOS..." CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINARY_NAME)_darwin -v ./cmd/greywall diff --git a/cmd/greywall/main.go b/cmd/greywall/main.go index 20f1315..5f33895 100644 --- a/cmd/greywall/main.go +++ b/cmd/greywall/main.go @@ -111,6 +111,7 @@ Configuration file format: rootCmd.AddCommand(newCompletionCmd(rootCmd)) rootCmd.AddCommand(newTemplatesCmd()) + rootCmd.AddCommand(newDaemonCmd()) if err := rootCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) @@ -594,12 +595,12 @@ parseCommand: // Find the executable execPath, err := exec.LookPath(command[0]) if err != nil { - fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Error: command not found: %s\n", command[0]) + fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Error: command not found: %s\n", command[0]) //nolint:gosec // logging to stderr, not web output os.Exit(127) } if debugMode { - fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Exec: %s %v\n", execPath, command[1:]) + fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Exec: %s %v\n", execPath, command[1:]) //nolint:gosec // logging to stderr, not web output } // Sanitize environment (strips LD_PRELOAD, etc.) diff --git a/internal/daemon/launchd.go b/internal/daemon/launchd.go index 6d25386..0e7c8f8 100644 --- a/internal/daemon/launchd.go +++ b/internal/daemon/launchd.go @@ -21,7 +21,7 @@ const ( InstallBinaryPath = "/usr/local/bin/greywall" InstallLibDir = "/usr/local/lib/greywall" SandboxUserName = "_greywall" - SandboxUserUID = "399" // System user range on macOS + SandboxUserUID = "399" // System user range on macOS SandboxGroupName = "_greywall" // Group used for pf routing (same name as user) SudoersFilePath = "/etc/sudoers.d/greywall" DefaultSocketPath = "/var/run/greywall.sock" @@ -446,7 +446,7 @@ func installSudoersRule(debug bool) error { // Write to a temp file first, then validate with visudo. tmpFile := SudoersFilePath + ".tmp" - if err := os.WriteFile(tmpFile, []byte(rule), 0o440); err != nil { + if err := os.WriteFile(tmpFile, []byte(rule), 0o440); err != nil { //nolint:gosec // sudoers files require 0440 per sudo(8) return fmt.Errorf("failed to write sudoers temp file: %w", err) } @@ -467,7 +467,7 @@ func installSudoersRule(debug bool) error { if err := runCmd(debug, "chown", "root:wheel", SudoersFilePath); err != nil { return fmt.Errorf("failed to set sudoers ownership: %w", err) } - if err := os.Chmod(SudoersFilePath, 0o440); err != nil { + if err := os.Chmod(SudoersFilePath, 0o440); err != nil { //nolint:gosec // sudoers files require 0440 per sudo(8) return fmt.Errorf("failed to set sudoers permissions: %w", err) } diff --git a/internal/daemon/server.go b/internal/daemon/server.go index 8865502..b2bebf7 100644 --- a/internal/daemon/server.go +++ b/internal/daemon/server.go @@ -24,10 +24,10 @@ type Request struct { // Response from daemon to CLI. type Response struct { - OK bool `json:"ok"` - Error string `json:"error,omitempty"` - SessionID string `json:"session_id,omitempty"` - TunDevice string `json:"tun_device,omitempty"` + OK bool `json:"ok"` + Error string `json:"error,omitempty"` + SessionID string `json:"session_id,omitempty"` + TunDevice string `json:"tun_device,omitempty"` SandboxUser string `json:"sandbox_user,omitempty"` SandboxGroup string `json:"sandbox_group,omitempty"` // Status response fields. diff --git a/internal/sandbox/benchmark_test.go b/internal/sandbox/benchmark_test.go index c4115b2..aeebe5e 100644 --- a/internal/sandbox/benchmark_test.go +++ b/internal/sandbox/benchmark_test.go @@ -333,7 +333,7 @@ func execBenchCommand(b *testing.B, command string, workDir string) { shell = "/bin/bash" } - cmd := exec.CommandContext(ctx, shell, "-c", command) + cmd := exec.CommandContext(ctx, shell, "-c", command) //nolint:gosec // test helper running shell commands cmd.Dir = workDir cmd.Stdout = &bytes.Buffer{} cmd.Stderr = &bytes.Buffer{} diff --git a/internal/sandbox/integration_test.go b/internal/sandbox/integration_test.go index 45b0a0c..44093ea 100644 --- a/internal/sandbox/integration_test.go +++ b/internal/sandbox/integration_test.go @@ -245,7 +245,7 @@ func executeShellCommandWithTimeout(t *testing.T, command string, workDir string shell = "/bin/bash" } - cmd := exec.CommandContext(ctx, shell, "-c", command) + cmd := exec.CommandContext(ctx, shell, "-c", command) //nolint:gosec // test helper running shell commands cmd.Dir = workDir var stdout, stderr bytes.Buffer diff --git a/internal/sandbox/learning.go b/internal/sandbox/learning.go index 3c45974..aa79540 100644 --- a/internal/sandbox/learning.go +++ b/internal/sandbox/learning.go @@ -426,8 +426,8 @@ func buildTemplate(cmdName string, allowRead, allowWrite []string) string { data, _ := json.MarshalIndent(cfg, "", " ") var sb strings.Builder - sb.WriteString(fmt.Sprintf("// Learned template for %q\n", cmdName)) - sb.WriteString(fmt.Sprintf("// Generated by: greywall --learning -- %s\n", cmdName)) + fmt.Fprintf(&sb, "// Learned template for %q\n", cmdName) + fmt.Fprintf(&sb, "// Generated by: greywall --learning -- %s\n", cmdName) sb.WriteString("// Review and adjust paths as needed\n") sb.Write(data) sb.WriteString("\n") diff --git a/internal/sandbox/linux_stub.go b/internal/sandbox/linux_stub.go index 713e72f..66955dc 100644 --- a/internal/sandbox/linux_stub.go +++ b/internal/sandbox/linux_stub.go @@ -64,12 +64,12 @@ func (b *ReverseBridge) Cleanup() {} // WrapCommandLinux returns an error on non-Linux platforms. func WrapCommandLinux(cfg *config.Config, command string, proxyBridge *ProxyBridge, dnsBridge *DnsBridge, reverseBridge *ReverseBridge, tun2socksPath string, debug bool) (string, error) { - return "", fmt.Errorf("Linux sandbox not available on this platform") + return "", fmt.Errorf("linux sandbox not available on this platform") } // WrapCommandLinuxWithOptions returns an error on non-Linux platforms. func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge *ProxyBridge, dnsBridge *DnsBridge, reverseBridge *ReverseBridge, tun2socksPath string, opts LinuxSandboxOptions) (string, error) { - return "", fmt.Errorf("Linux sandbox not available on this platform") + return "", fmt.Errorf("linux sandbox not available on this platform") } // StartLinuxMonitor returns nil on non-Linux platforms. diff --git a/internal/sandbox/monitor.go b/internal/sandbox/monitor.go index 5cc33a8..d8ec30f 100644 --- a/internal/sandbox/monitor.go +++ b/internal/sandbox/monitor.go @@ -66,7 +66,7 @@ func (m *LogMonitor) Start() error { for scanner.Scan() { line := scanner.Text() if violation := parseViolation(line); violation != "" { - fmt.Fprintf(os.Stderr, "%s\n", violation) + fmt.Fprintf(os.Stderr, "%s\n", violation) //nolint:gosec // logging to stderr, not web output } } }() diff --git a/internal/sandbox/tun2socks_embed.go b/internal/sandbox/tun2socks_embed.go index 580eed8..1f73090 100644 --- a/internal/sandbox/tun2socks_embed.go +++ b/internal/sandbox/tun2socks_embed.go @@ -13,9 +13,9 @@ import ( //go:embed bin/tun2socks-linux-* var tun2socksFS embed.FS -// extractTun2Socks writes the embedded tun2socks binary to a temp file and returns its path. +// ExtractTun2Socks writes the embedded tun2socks binary to a temp file and returns its path. // The caller is responsible for removing the file when done. -func extractTun2Socks() (string, error) { +func ExtractTun2Socks() (string, error) { var arch string switch runtime.GOARCH { case "amd64": diff --git a/internal/sandbox/tun2socks_embed_darwin.go b/internal/sandbox/tun2socks_embed_darwin.go new file mode 100644 index 0000000..05d1ec9 --- /dev/null +++ b/internal/sandbox/tun2socks_embed_darwin.go @@ -0,0 +1,53 @@ +//go:build darwin + +package sandbox + +import ( + "embed" + "fmt" + "io/fs" + "os" + "runtime" +) + +//go:embed bin/tun2socks-darwin-* +var tun2socksFS embed.FS + +// ExtractTun2Socks writes the embedded tun2socks binary to a temp file and returns its path. +// The caller is responsible for removing the file when done. +func ExtractTun2Socks() (string, error) { + var arch string + switch runtime.GOARCH { + case "amd64": + arch = "amd64" + case "arm64": + arch = "arm64" + default: + return "", fmt.Errorf("tun2socks: unsupported architecture %s", runtime.GOARCH) + } + + name := fmt.Sprintf("bin/tun2socks-darwin-%s", arch) + data, err := fs.ReadFile(tun2socksFS, name) + if err != nil { + return "", fmt.Errorf("tun2socks: embedded binary not found for %s: %w", arch, err) + } + + tmpFile, err := os.CreateTemp("", "greywall-tun2socks-*") + if err != nil { + return "", fmt.Errorf("tun2socks: failed to create temp file: %w", err) + } + + if _, err := tmpFile.Write(data); err != nil { + _ = tmpFile.Close() + _ = os.Remove(tmpFile.Name()) //nolint:gosec // path from os.CreateTemp, not user input + return "", fmt.Errorf("tun2socks: failed to write binary: %w", err) + } + _ = tmpFile.Close() + + if err := os.Chmod(tmpFile.Name(), 0o755); err != nil { //nolint:gosec // executable binary needs execute permission + _ = os.Remove(tmpFile.Name()) //nolint:gosec // path from os.CreateTemp, not user input + return "", fmt.Errorf("tun2socks: failed to make executable: %w", err) + } + + return tmpFile.Name(), nil +} diff --git a/internal/sandbox/tun2socks_embed_stub.go b/internal/sandbox/tun2socks_embed_stub.go index 22baedd..68768da 100644 --- a/internal/sandbox/tun2socks_embed_stub.go +++ b/internal/sandbox/tun2socks_embed_stub.go @@ -1,10 +1,10 @@ -//go:build !linux +//go:build !linux && !darwin package sandbox import "fmt" -// extractTun2Socks is not available on non-Linux platforms. -func extractTun2Socks() (string, error) { - return "", fmt.Errorf("tun2socks is only available on Linux") +// ExtractTun2Socks is not available on unsupported platforms. +func ExtractTun2Socks() (string, error) { + return "", fmt.Errorf("tun2socks is only available on Linux and macOS") }