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
This commit is contained in:
2026-02-26 09:46:33 -06:00
parent cfe29d2c0b
commit cb474b2d99
12 changed files with 91 additions and 36 deletions

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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{}

View File

@@ -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

View File

@@ -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")

View File

@@ -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.

View File

@@ -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
}
}
}()

View File

@@ -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":

View 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
}

View File

@@ -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")
}