Compare commits
3 Commits
3dd772d35a
...
a470f86ee4
| Author | SHA1 | Date | |
|---|---|---|---|
| a470f86ee4 | |||
| 7e85083c38 | |||
| 267c82f4bd |
@@ -55,9 +55,9 @@ func main() {
|
|||||||
Long: `greywall is a command-line tool that runs commands in a sandboxed environment
|
Long: `greywall is a command-line tool that runs commands in a sandboxed environment
|
||||||
with network and filesystem restrictions.
|
with network and filesystem restrictions.
|
||||||
|
|
||||||
By default, all network access is blocked. Use --proxy to route traffic through
|
By default, traffic is routed through the GreyHaven SOCKS5 proxy at localhost:42051
|
||||||
an external SOCKS5 proxy, or configure a proxy URL in your settings file at
|
with DNS via localhost:42053. Use --proxy and --dns to override, or configure in
|
||||||
~/.config/greywall/greywall.json (or ~/Library/Application Support/greywall/greywall.json on macOS).
|
your settings file at ~/.config/greywall/greywall.json (or ~/Library/Application Support/greywall/greywall.json on macOS).
|
||||||
|
|
||||||
On Linux, greywall uses tun2socks for truly transparent proxying: all TCP/UDP traffic
|
On Linux, greywall uses tun2socks for truly transparent proxying: all TCP/UDP traffic
|
||||||
from any binary is captured at the kernel level via a TUN device and forwarded
|
from any binary is captured at the kernel level via a TUN device and forwarded
|
||||||
@@ -98,8 +98,8 @@ Configuration file format:
|
|||||||
rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging")
|
rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging")
|
||||||
rootCmd.Flags().BoolVarP(&monitor, "monitor", "m", false, "Monitor and log sandbox violations")
|
rootCmd.Flags().BoolVarP(&monitor, "monitor", "m", false, "Monitor and log sandbox violations")
|
||||||
rootCmd.Flags().StringVarP(&settingsPath, "settings", "s", "", "Path to settings file (default: OS config directory)")
|
rootCmd.Flags().StringVarP(&settingsPath, "settings", "s", "", "Path to settings file (default: OS config directory)")
|
||||||
rootCmd.Flags().StringVar(&proxyURL, "proxy", "", "External SOCKS5 proxy URL (e.g., socks5://localhost:1080)")
|
rootCmd.Flags().StringVar(&proxyURL, "proxy", "", "External SOCKS5 proxy URL (default: socks5://localhost:42052)")
|
||||||
rootCmd.Flags().StringVar(&dnsAddr, "dns", "", "DNS server address on host (e.g., localhost:3153)")
|
rootCmd.Flags().StringVar(&dnsAddr, "dns", "", "DNS server address on host (default: localhost:42053)")
|
||||||
rootCmd.Flags().StringVarP(&cmdString, "c", "c", "", "Run command string directly (like sh -c)")
|
rootCmd.Flags().StringVarP(&cmdString, "c", "c", "", "Run command string directly (like sh -c)")
|
||||||
rootCmd.Flags().StringArrayVarP(&exposePorts, "port", "p", nil, "Expose port for inbound connections (can be used multiple times)")
|
rootCmd.Flags().StringArrayVarP(&exposePorts, "port", "p", nil, "Expose port for inbound connections (can be used multiple times)")
|
||||||
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information")
|
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information")
|
||||||
@@ -229,14 +229,35 @@ func runCommand(cmd *cobra.Command, args []string) error {
|
|||||||
cfg.Network.DnsAddr = dnsAddr
|
cfg.Network.DnsAddr = dnsAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-inject command name as SOCKS5 proxy username when no credentials are set.
|
// GreyHaven defaults: when no proxy or DNS is configured (neither via CLI
|
||||||
// This lets the proxy identify which sandboxed command originated the traffic.
|
// nor config file), use the standard GreyHaven infrastructure ports.
|
||||||
if cfg.Network.ProxyURL != "" && cmdName != "" {
|
if cfg.Network.ProxyURL == "" {
|
||||||
if u, err := url.Parse(cfg.Network.ProxyURL); err == nil && u.User == nil {
|
cfg.Network.ProxyURL = "socks5://localhost:42052"
|
||||||
u.User = url.User(cmdName)
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[greywall] Defaulting proxy to socks5://localhost:42052\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.Network.DnsAddr == "" {
|
||||||
|
cfg.Network.DnsAddr = "localhost:42053"
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[greywall] Defaulting DNS to localhost:42053\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-inject proxy credentials so the proxy can identify the sandboxed command.
|
||||||
|
// - If a command name is available, use it as the username with "proxy" as password.
|
||||||
|
// - If no command name, default to "proxy:proxy" (required by gost for auth).
|
||||||
|
// This always overrides any existing credentials in the URL.
|
||||||
|
if cfg.Network.ProxyURL != "" {
|
||||||
|
if u, err := url.Parse(cfg.Network.ProxyURL); err == nil {
|
||||||
|
proxyUser := "proxy"
|
||||||
|
if cmdName != "" {
|
||||||
|
proxyUser = cmdName
|
||||||
|
}
|
||||||
|
u.User = url.UserPassword(proxyUser, "proxy")
|
||||||
cfg.Network.ProxyURL = u.String()
|
cfg.Network.ProxyURL = u.String()
|
||||||
if debug {
|
if debug {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall] Auto-set proxy username to %q\n", cmdName)
|
fmt.Fprintf(os.Stderr, "[greywall] Auto-set proxy credentials to %q:proxy\n", proxyUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,25 +355,32 @@ func runCommand(cmd *cobra.Command, args []string) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for command to finish
|
// Wait for command to finish
|
||||||
|
commandFailed := false
|
||||||
if err := execCmd.Wait(); err != nil {
|
if err := execCmd.Wait(); err != nil {
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
// Set exit code but don't os.Exit() here - let deferred cleanup run
|
// Set exit code but don't os.Exit() here - let deferred cleanup run
|
||||||
exitCode = exitErr.ExitCode()
|
exitCode = exitErr.ExitCode()
|
||||||
// Continue to template generation even if command exited non-zero
|
commandFailed = true
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("command failed: %w", err)
|
return fmt.Errorf("command failed: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate learned template after command completes
|
// Generate learned template after command completes successfully.
|
||||||
|
// Skip template generation if the command failed — the strace trace
|
||||||
|
// is likely incomplete and would produce an unreliable template.
|
||||||
if learning && manager.IsLearning() {
|
if learning && manager.IsLearning() {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall] Analyzing filesystem access patterns...\n")
|
if commandFailed {
|
||||||
templatePath, genErr := manager.GenerateLearnedTemplate(cmdName)
|
fmt.Fprintf(os.Stderr, "[greywall] Skipping template generation: command exited with code %d\n", exitCode)
|
||||||
if genErr != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "[greywall] Warning: failed to generate template: %v\n", genErr)
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall] Template saved to: %s\n", templatePath)
|
fmt.Fprintf(os.Stderr, "[greywall] Analyzing filesystem access patterns...\n")
|
||||||
fmt.Fprintf(os.Stderr, "[greywall] Next run will auto-load this template.\n")
|
templatePath, genErr := manager.GenerateLearnedTemplate(cmdName)
|
||||||
|
if genErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[greywall] Warning: failed to generate template: %v\n", genErr)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "[greywall] Template saved to: %s\n", templatePath)
|
||||||
|
fmt.Fprintf(os.Stderr, "[greywall] Next run will auto-load this template.\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -422,9 +422,15 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
|
|||||||
// Build bwrap args with filesystem restrictions
|
// Build bwrap args with filesystem restrictions
|
||||||
bwrapArgs := []string{
|
bwrapArgs := []string{
|
||||||
"bwrap",
|
"bwrap",
|
||||||
"--new-session",
|
|
||||||
"--die-with-parent",
|
|
||||||
}
|
}
|
||||||
|
// --new-session calls setsid() which detaches from the controlling terminal.
|
||||||
|
// Skip it in learning mode so interactive programs (TUIs, prompts) can
|
||||||
|
// read from /dev/tty. Learning mode already relaxes security constraints
|
||||||
|
// (no seccomp, no landlock), so skipping new-session is acceptable.
|
||||||
|
if !opts.Learning {
|
||||||
|
bwrapArgs = append(bwrapArgs, "--new-session")
|
||||||
|
}
|
||||||
|
bwrapArgs = append(bwrapArgs, "--die-with-parent")
|
||||||
|
|
||||||
// Always use --unshare-net when available (network namespace isolation)
|
// Always use --unshare-net when available (network namespace isolation)
|
||||||
// Inside the namespace, tun2socks will provide transparent proxy access
|
// Inside the namespace, tun2socks will provide transparent proxy access
|
||||||
@@ -886,43 +892,21 @@ sleep 0.3
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
// In learning mode, wrap the command with strace to trace syscalls.
|
// In learning mode, wrap the command with strace to trace syscalls.
|
||||||
// strace -f follows forked children, which means it hangs if the app spawns
|
// Run strace in the foreground so the traced command retains terminal
|
||||||
// long-lived child processes (LSP servers, file watchers, etc.).
|
// access (stdin, /dev/tty) for interactive programs like TUIs.
|
||||||
// To handle this, we run strace in the background and spawn a monitor that
|
// If the app spawns long-lived child processes, strace -f may hang
|
||||||
// detects when the main command (strace's direct child) exits by polling
|
// after the main command exits; the user can Ctrl+C to stop it.
|
||||||
// /proc/STRACE_PID/task/STRACE_PID/children, then kills strace.
|
// A SIGCHLD trap kills strace once its direct child exits, handling
|
||||||
|
// the common case of background daemons (LSP servers, watchers).
|
||||||
if opts.Learning && opts.StraceLogPath != "" {
|
if opts.Learning && opts.StraceLogPath != "" {
|
||||||
innerScript.WriteString(fmt.Sprintf(`# Learning mode: trace filesystem access
|
innerScript.WriteString(fmt.Sprintf(`# Learning mode: trace filesystem access (foreground for terminal access)
|
||||||
strace -f -qq -I2 -e trace=openat,open,creat,mkdir,mkdirat,unlinkat,renameat,renameat2,symlinkat,linkat -o %s -- %s &
|
strace -f -qq -I2 -e trace=openat,open,creat,mkdir,mkdirat,unlinkat,renameat,renameat2,symlinkat,linkat -o %s -- %s
|
||||||
GREYWALL_STRACE_PID=$!
|
GREYWALL_STRACE_EXIT=$?
|
||||||
|
|
||||||
# Monitor: detect when the main command exits, then kill strace.
|
|
||||||
# strace's direct child is the command. When it exits, the children file
|
|
||||||
# becomes empty (grandchildren are reparented to init in the PID namespace).
|
|
||||||
(
|
|
||||||
sleep 1
|
|
||||||
while kill -0 $GREYWALL_STRACE_PID 2>/dev/null; do
|
|
||||||
CHILDREN=$(cat /proc/$GREYWALL_STRACE_PID/task/$GREYWALL_STRACE_PID/children 2>/dev/null)
|
|
||||||
if [ -z "$CHILDREN" ]; then
|
|
||||||
sleep 0.5
|
|
||||||
kill $GREYWALL_STRACE_PID 2>/dev/null
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
GREYWALL_MONITOR_PID=$!
|
|
||||||
|
|
||||||
trap 'kill -INT $GREYWALL_STRACE_PID 2>/dev/null' INT
|
|
||||||
trap 'kill -TERM $GREYWALL_STRACE_PID 2>/dev/null' TERM
|
|
||||||
wait $GREYWALL_STRACE_PID 2>/dev/null
|
|
||||||
kill $GREYWALL_MONITOR_PID 2>/dev/null
|
|
||||||
wait $GREYWALL_MONITOR_PID 2>/dev/null
|
|
||||||
# Kill any orphaned child processes (LSP servers, file watchers, etc.)
|
# Kill any orphaned child processes (LSP servers, file watchers, etc.)
|
||||||
# that were spawned by the traced command and reparented to PID 1.
|
# that were spawned by the traced command and reparented to PID 1.
|
||||||
# Without this, greywall hangs until they exit (they hold pipe FDs open).
|
|
||||||
kill -TERM -1 2>/dev/null
|
kill -TERM -1 2>/dev/null
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
|
exit $GREYWALL_STRACE_EXIT
|
||||||
`,
|
`,
|
||||||
ShellQuoteSingle(opts.StraceLogPath), command,
|
ShellQuoteSingle(opts.StraceLogPath), command,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ func (m *Manager) Initialize() error {
|
|||||||
|
|
||||||
m.initialized = true
|
m.initialized = true
|
||||||
if m.config.Network.ProxyURL != "" {
|
if m.config.Network.ProxyURL != "" {
|
||||||
m.logDebug("Sandbox manager initialized (proxy: %s)", m.config.Network.ProxyURL)
|
dnsInfo := "none"
|
||||||
|
if m.config.Network.DnsAddr != "" {
|
||||||
|
dnsInfo = m.config.Network.DnsAddr
|
||||||
|
}
|
||||||
|
m.logDebug("Sandbox manager initialized (proxy: %s, dns: %s)", m.config.Network.ProxyURL, dnsInfo)
|
||||||
} else {
|
} else {
|
||||||
m.logDebug("Sandbox manager initialized (no proxy, network blocked)")
|
m.logDebug("Sandbox manager initialized (no proxy, network blocked)")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user