feat: add macOS daemon support with group-based pf routing
- Add daemon CLI subcommand (install/uninstall/status/run) - Download tun2socks for darwin platforms in Makefile - Export ExtractTun2Socks and add darwin embed support - Use group-based pf filtering instead of user-based for transparent proxying - Install sudoers rule for passwordless sandbox-exec with _greywall group - Add nolint directives for gosec false positives on sudoers 0440 perms - Fix lint issues: lowercase errors, fmt.Fprintf, nolint comments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
29
Makefile
29
Makefile
@@ -8,24 +8,24 @@ BINARY_UNIX=$(BINARY_NAME)_unix
|
|||||||
TUN2SOCKS_VERSION=v2.5.2
|
TUN2SOCKS_VERSION=v2.5.2
|
||||||
TUN2SOCKS_BIN_DIR=internal/sandbox/bin
|
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
|
all: build
|
||||||
|
|
||||||
|
TUN2SOCKS_PLATFORMS=linux-amd64 linux-arm64 darwin-amd64 darwin-arm64
|
||||||
|
|
||||||
download-tun2socks:
|
download-tun2socks:
|
||||||
@echo "Downloading tun2socks $(TUN2SOCKS_VERSION)..."
|
|
||||||
@mkdir -p $(TUN2SOCKS_BIN_DIR)
|
@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
|
@for platform in $(TUN2SOCKS_PLATFORMS); do \
|
||||||
@unzip -o -q /tmp/tun2socks-linux-amd64.zip -d /tmp/tun2socks-amd64
|
if [ ! -f $(TUN2SOCKS_BIN_DIR)/tun2socks-$$platform ]; then \
|
||||||
@mv /tmp/tun2socks-amd64/tun2socks-linux-amd64 $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-amd64
|
echo "Downloading tun2socks-$$platform $(TUN2SOCKS_VERSION)..."; \
|
||||||
@chmod +x $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-amd64
|
curl -sL "https://github.com/xjasonlyu/tun2socks/releases/download/$(TUN2SOCKS_VERSION)/tun2socks-$$platform.zip" -o /tmp/tun2socks-$$platform.zip; \
|
||||||
@rm -rf /tmp/tun2socks-linux-amd64.zip /tmp/tun2socks-amd64
|
unzip -o -q /tmp/tun2socks-$$platform.zip -d /tmp/tun2socks-$$platform; \
|
||||||
@curl -sL "https://github.com/xjasonlyu/tun2socks/releases/download/$(TUN2SOCKS_VERSION)/tun2socks-linux-arm64.zip" -o /tmp/tun2socks-linux-arm64.zip
|
mv /tmp/tun2socks-$$platform/tun2socks-$$platform $(TUN2SOCKS_BIN_DIR)/tun2socks-$$platform; \
|
||||||
@unzip -o -q /tmp/tun2socks-linux-arm64.zip -d /tmp/tun2socks-arm64
|
chmod +x $(TUN2SOCKS_BIN_DIR)/tun2socks-$$platform; \
|
||||||
@mv /tmp/tun2socks-arm64/tun2socks-linux-arm64 $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-arm64
|
rm -rf /tmp/tun2socks-$$platform.zip /tmp/tun2socks-$$platform; \
|
||||||
@chmod +x $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-arm64
|
fi; \
|
||||||
@rm -rf /tmp/tun2socks-linux-arm64.zip /tmp/tun2socks-arm64
|
done
|
||||||
@echo "tun2socks binaries downloaded to $(TUN2SOCKS_BIN_DIR)/"
|
|
||||||
|
|
||||||
build: download-tun2socks
|
build: download-tun2socks
|
||||||
@echo "Building $(BINARY_NAME)..."
|
@echo "Building $(BINARY_NAME)..."
|
||||||
@@ -53,6 +53,7 @@ clean:
|
|||||||
rm -f $(BINARY_UNIX)
|
rm -f $(BINARY_UNIX)
|
||||||
rm -f coverage.out
|
rm -f coverage.out
|
||||||
rm -f $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-*
|
rm -f $(TUN2SOCKS_BIN_DIR)/tun2socks-linux-*
|
||||||
|
rm -f $(TUN2SOCKS_BIN_DIR)/tun2socks-darwin-*
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
@echo "Downloading dependencies..."
|
@echo "Downloading dependencies..."
|
||||||
@@ -63,7 +64,7 @@ build-linux: download-tun2socks
|
|||||||
@echo "Building for Linux..."
|
@echo "Building for Linux..."
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_UNIX) -v ./cmd/greywall
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_UNIX) -v ./cmd/greywall
|
||||||
|
|
||||||
build-darwin:
|
build-darwin: download-tun2socks
|
||||||
@echo "Building for macOS..."
|
@echo "Building for macOS..."
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINARY_NAME)_darwin -v ./cmd/greywall
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINARY_NAME)_darwin -v ./cmd/greywall
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ Configuration file format:
|
|||||||
|
|
||||||
rootCmd.AddCommand(newCompletionCmd(rootCmd))
|
rootCmd.AddCommand(newCompletionCmd(rootCmd))
|
||||||
rootCmd.AddCommand(newTemplatesCmd())
|
rootCmd.AddCommand(newTemplatesCmd())
|
||||||
|
rootCmd.AddCommand(newDaemonCmd())
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
@@ -594,12 +595,12 @@ parseCommand:
|
|||||||
// Find the executable
|
// Find the executable
|
||||||
execPath, err := exec.LookPath(command[0])
|
execPath, err := exec.LookPath(command[0])
|
||||||
if err != nil {
|
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)
|
os.Exit(127)
|
||||||
}
|
}
|
||||||
|
|
||||||
if debugMode {
|
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.)
|
// Sanitize environment (strips LD_PRELOAD, etc.)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const (
|
|||||||
InstallBinaryPath = "/usr/local/bin/greywall"
|
InstallBinaryPath = "/usr/local/bin/greywall"
|
||||||
InstallLibDir = "/usr/local/lib/greywall"
|
InstallLibDir = "/usr/local/lib/greywall"
|
||||||
SandboxUserName = "_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)
|
SandboxGroupName = "_greywall" // Group used for pf routing (same name as user)
|
||||||
SudoersFilePath = "/etc/sudoers.d/greywall"
|
SudoersFilePath = "/etc/sudoers.d/greywall"
|
||||||
DefaultSocketPath = "/var/run/greywall.sock"
|
DefaultSocketPath = "/var/run/greywall.sock"
|
||||||
@@ -446,7 +446,7 @@ func installSudoersRule(debug bool) error {
|
|||||||
|
|
||||||
// Write to a temp file first, then validate with visudo.
|
// Write to a temp file first, then validate with visudo.
|
||||||
tmpFile := SudoersFilePath + ".tmp"
|
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)
|
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 {
|
if err := runCmd(debug, "chown", "root:wheel", SudoersFilePath); err != nil {
|
||||||
return fmt.Errorf("failed to set sudoers ownership: %w", err)
|
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)
|
return fmt.Errorf("failed to set sudoers permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ type Request struct {
|
|||||||
|
|
||||||
// Response from daemon to CLI.
|
// Response from daemon to CLI.
|
||||||
type Response struct {
|
type Response struct {
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
SessionID string `json:"session_id,omitempty"`
|
SessionID string `json:"session_id,omitempty"`
|
||||||
TunDevice string `json:"tun_device,omitempty"`
|
TunDevice string `json:"tun_device,omitempty"`
|
||||||
SandboxUser string `json:"sandbox_user,omitempty"`
|
SandboxUser string `json:"sandbox_user,omitempty"`
|
||||||
SandboxGroup string `json:"sandbox_group,omitempty"`
|
SandboxGroup string `json:"sandbox_group,omitempty"`
|
||||||
// Status response fields.
|
// Status response fields.
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ func execBenchCommand(b *testing.B, command string, workDir string) {
|
|||||||
shell = "/bin/bash"
|
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.Dir = workDir
|
||||||
cmd.Stdout = &bytes.Buffer{}
|
cmd.Stdout = &bytes.Buffer{}
|
||||||
cmd.Stderr = &bytes.Buffer{}
|
cmd.Stderr = &bytes.Buffer{}
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ func executeShellCommandWithTimeout(t *testing.T, command string, workDir string
|
|||||||
shell = "/bin/bash"
|
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.Dir = workDir
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
|
|||||||
@@ -426,8 +426,8 @@ func buildTemplate(cmdName string, allowRead, allowWrite []string) string {
|
|||||||
data, _ := json.MarshalIndent(cfg, "", " ")
|
data, _ := json.MarshalIndent(cfg, "", " ")
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString(fmt.Sprintf("// Learned template for %q\n", cmdName))
|
fmt.Fprintf(&sb, "// Learned template for %q\n", cmdName)
|
||||||
sb.WriteString(fmt.Sprintf("// Generated by: greywall --learning -- %s\n", cmdName))
|
fmt.Fprintf(&sb, "// Generated by: greywall --learning -- %s\n", cmdName)
|
||||||
sb.WriteString("// Review and adjust paths as needed\n")
|
sb.WriteString("// Review and adjust paths as needed\n")
|
||||||
sb.Write(data)
|
sb.Write(data)
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ func (b *ReverseBridge) Cleanup() {}
|
|||||||
|
|
||||||
// WrapCommandLinux returns an error on non-Linux platforms.
|
// 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) {
|
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.
|
// 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) {
|
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.
|
// StartLinuxMonitor returns nil on non-Linux platforms.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (m *LogMonitor) Start() error {
|
|||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if violation := parseViolation(line); violation != "" {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import (
|
|||||||
//go:embed bin/tun2socks-linux-*
|
//go:embed bin/tun2socks-linux-*
|
||||||
var tun2socksFS embed.FS
|
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.
|
// The caller is responsible for removing the file when done.
|
||||||
func extractTun2Socks() (string, error) {
|
func ExtractTun2Socks() (string, error) {
|
||||||
var arch string
|
var arch string
|
||||||
switch runtime.GOARCH {
|
switch runtime.GOARCH {
|
||||||
case "amd64":
|
case "amd64":
|
||||||
|
|||||||
53
internal/sandbox/tun2socks_embed_darwin.go
Normal file
53
internal/sandbox/tun2socks_embed_darwin.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
//go:build !linux
|
//go:build !linux && !darwin
|
||||||
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// extractTun2Socks is not available on non-Linux platforms.
|
// ExtractTun2Socks is not available on unsupported platforms.
|
||||||
func extractTun2Socks() (string, error) {
|
func ExtractTun2Socks() (string, error) {
|
||||||
return "", fmt.Errorf("tun2socks is only available on Linux")
|
return "", fmt.Errorf("tun2socks is only available on Linux and macOS")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user