@@ -3,11 +3,14 @@ package main
33import (
44 "context"
55 "fmt"
6+ "github.com/landlock-lsm/go-landlock/landlock"
67 "log"
8+ "net"
79 "net/http"
810 "os"
911 "os/exec"
1012 "os/signal"
13+ "strconv"
1114 "syscall"
1215
1316 "github.com/akamensky/argparse"
@@ -21,22 +24,22 @@ const daemonProcess = "daemon-process"
2124
2225var version = "1.0.8-dev"
2326
24- // attempts to pledge and panic if it fails
25- // this does nothing on non-OpenBSD systems
26- func pledgeOrPanic (promises string ) {
27- err := protect .Pledge (promises )
27+ func panicIfError (err error ) {
2828 if err != nil {
2929 log .Fatal (err )
3030 }
3131}
3232
33+ // attempts to pledge and panic if it fails
34+ // this does nothing on non-OpenBSD systems
35+ func pledgeOrPanic (promises string ) {
36+ panicIfError (protect .Pledge (promises ))
37+ }
38+
3339// attempts to unveil and panic if it fails
3440// this does nothing on non-OpenBSD systems
3541func unveilOrPanic (path string , flags string ) {
36- err := protect .Unveil (path , flags )
37- if err != nil {
38- log .Fatal (err )
39- }
42+ panicIfError (protect .Unveil (path , flags ))
4043}
4144
4245// get the executable path via syscalls or infer it from argv
@@ -48,6 +51,91 @@ func executablePath() string {
4851 return programPath
4952}
5053
54+ func lock (stage string ) {
55+ switch stage {
56+ case "boot" :
57+ exePath := executablePath ()
58+ // OpenBSD
59+ unveilOrPanic ("/" , "r" )
60+ unveilOrPanic (exePath , "x" )
61+ // only allow standard stdio operation, file reading, networking, and exec
62+ // also remove unveil permission to lock unveil
63+ pledgeOrPanic ("stdio rpath inet dns proc exec" )
64+ // Linux
65+ panicIfError (landlock .V1 .BestEffort ().RestrictPaths (
66+ landlock .RODirs ("/" ),
67+ ))
68+ case "boot-daemon" :
69+ case "read-config" :
70+ // OpenBSD
71+ pledgeOrPanic ("stdio rpath inet dns" )
72+ case "ready" :
73+ // no file access is allowed from now on, only networking
74+ // OpenBSD
75+ pledgeOrPanic ("stdio inet dns" )
76+ // Linux
77+ net .DefaultResolver .PreferGo = true // needed to lock down dependencies
78+ panicIfError (landlock .V1 .BestEffort ().RestrictPaths (
79+ landlock .ROFiles ("/etc/resolv.conf" ),
80+ landlock .ROFiles ("/dev/fd" ),
81+ landlock .ROFiles ("/dev/zero" ),
82+ landlock .ROFiles ("/dev/urandom" ),
83+ landlock .ROFiles ("/etc/localtime" ),
84+ landlock .ROFiles ("/proc/self/stat" ),
85+ landlock .ROFiles ("/proc/self/status" ),
86+ landlock .ROFiles ("/usr/share/locale" ),
87+ landlock .ROFiles ("/proc/self/cmdline" ),
88+ landlock .ROFiles ("/usr/share/zoneinfo" ),
89+ landlock .ROFiles ("/proc/sys/kernel/version" ),
90+ landlock .ROFiles ("/proc/sys/kernel/ngroups_max" ),
91+ landlock .ROFiles ("/proc/sys/kernel/cap_last_cap" ),
92+ landlock .ROFiles ("/proc/sys/vm/overcommit_memory" ),
93+ landlock .RWFiles ("/dev/log" ),
94+ landlock .RWFiles ("/dev/null" ),
95+ landlock .RWFiles ("/dev/full" ),
96+ landlock .RWFiles ("/proc/self/fd" ),
97+ ))
98+ default :
99+ panic ("invalid stage" )
100+ }
101+ }
102+
103+ func extractPort (addr string ) uint16 {
104+ _ , portStr , err := net .SplitHostPort (addr )
105+ if err != nil {
106+ panic (fmt .Errorf ("failed to extract port from %s: %w" , addr , err ))
107+ }
108+
109+ port , err := strconv .Atoi (portStr )
110+ if err != nil {
111+ panic (fmt .Errorf ("failed to extract port from %s: %w" , addr , err ))
112+ }
113+
114+ return uint16 (port )
115+ }
116+
117+ func lockNetwork (sections []wireproxy.RoutineSpawner , infoAddr * string ) {
118+ var rules []landlock.Rule
119+ if infoAddr != nil && * infoAddr != "" {
120+ rules = append (rules , landlock .BindTCP (extractPort (* infoAddr )))
121+ }
122+
123+ for _ , section := range sections {
124+ switch section := section .(type ) {
125+ case * wireproxy.TCPServerTunnelConfig :
126+ rules = append (rules , landlock .ConnectTCP (extractPort (section .Target )))
127+ case * wireproxy.HTTPConfig :
128+ rules = append (rules , landlock .BindTCP (extractPort (section .BindAddress )))
129+ case * wireproxy.TCPClientTunnelConfig :
130+ rules = append (rules , landlock .ConnectTCP (uint16 (section .BindAddress .Port )))
131+ case * wireproxy.Socks5Config :
132+ rules = append (rules , landlock .BindTCP (extractPort (section .BindAddress )))
133+ }
134+ }
135+
136+ panicIfError (landlock .V4 .BestEffort ().RestrictNet (rules ... ))
137+ }
138+
51139func main () {
52140 s := make (chan os.Signal , 1 )
53141 signal .Notify (s , syscall .SIGINT , syscall .SIGQUIT )
@@ -59,18 +147,12 @@ func main() {
59147 }()
60148
61149 exePath := executablePath ()
62- unveilOrPanic ("/" , "r" )
63- unveilOrPanic (exePath , "x" )
64-
65- // only allow standard stdio operation, file reading, networking, and exec
66- // also remove unveil permission to lock unveil
67- pledgeOrPanic ("stdio rpath inet dns proc exec" )
150+ lock ("boot" )
68151
69152 isDaemonProcess := len (os .Args ) > 1 && os .Args [1 ] == daemonProcess
70153 args := os .Args
71154 if isDaemonProcess {
72- // remove proc and exec if they are not needed
73- pledgeOrPanic ("stdio rpath inet dns" )
155+ lock ("boot-daemon" )
74156 args = []string {args [0 ]}
75157 args = append (args , os .Args [2 :]... )
76158 }
@@ -100,8 +182,7 @@ func main() {
100182 }
101183
102184 if ! * daemon {
103- // remove proc and exec if they are not needed
104- pledgeOrPanic ("stdio rpath inet dns" )
185+ lock ("read-config" )
105186 }
106187
107188 conf , err := wireproxy .ParseConfig (* config )
@@ -114,6 +195,8 @@ func main() {
114195 return
115196 }
116197
198+ lockNetwork (conf .Routines , info )
199+
117200 if isDaemonProcess {
118201 os .Stdout , _ = os .Open (os .DevNull )
119202 os .Stderr , _ = os .Open (os .DevNull )
@@ -139,8 +222,7 @@ func main() {
139222 logLevel = device .LogLevelSilent
140223 }
141224
142- // no file access is allowed from now on, only networking
143- pledgeOrPanic ("stdio inet dns" )
225+ lock ("ready" )
144226
145227 tun , err := wireproxy .StartWireguard (conf .Device , logLevel )
146228 if err != nil {
0 commit comments