forked from ghostunnel/ghostunnel
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlandlock_linux.go
More file actions
380 lines (346 loc) · 13.1 KB
/
landlock_linux.go
File metadata and controls
380 lines (346 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
//go:build linux
/*-
* Copyright 2024, Ghostunnel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"errors"
"fmt"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/landlock-lsm/go-landlock/landlock"
llsys "github.com/landlock-lsm/go-landlock/landlock/syscall"
)
type portRuleFunc = func(port uint16) landlock.NetRule
var defaultReadWritePaths = []string{
"/dev", // /dev/log syslog socket, /dev/urandom, /dev/null
"/run", // sd_notify socket, journald socket, runtime state
"/var/run", // legacy alias of /run; some syslog daemons listen here
"/proc", // Go runtime: /proc/self/* for GC and scheduler
"/tmp", // temporary files (e.g. spooled writes, Go runtime)
}
var defaultReadOnlyPaths = []string{
"/sys", // Go runtime: cgroup info for GOMAXPROCS, CPU topology
"/etc", // DNS config, hosts, nsswitch, localtime, distro CA bundles
"/usr/share/zoneinfo", // tzdata, referenced via /etc/localtime symlink
"/usr/share/ca-certificates", // Debian/Ubuntu CA cert source files
"/usr/local/share/ca-certificates", // locally-installed CA certs (Debian/Ubuntu)
"/var/lib/ca-certificates", // openSUSE/SLES CA bundle location
}
// setupLandlock processes flags given to the process and generates an
// appropriate landlock rule configuration to limit our privileges.
func setupLandlock() error {
fsRules := []landlock.Rule{}
netRules := []landlock.Rule{}
// Extra RW paths registered by init() hooks (e.g. GOCOVERDIR for
// coverage-instrumented builds).
for _, path := range extraRWPaths {
fsRules = append(fsRules, landlock.RWDirs(path))
}
// DNS over TCP as a fallback path for Go's resolver when a UDP response
// is truncated. UDP DNS is not gated by landlock net rules.
netRules = append(netRules, landlock.ConnectTCP(53))
// Default RW FS rules. Some paths we need always accessible for syslog and
// for creating runtime/temporary files. Note that syslog can be in multiple
// places not just /dev/log, e.g. /var/run is an option.
for _, path := range defaultReadWritePaths {
fsRules = append(fsRules, landlock.RWDirs(path).IgnoreIfMissing())
}
// Default RO FS rules. Some paths we need always accessible for name
// resolution or time zones. For this purpose we keep /etc accessible. Note
// that we could have chosen to limit ourselves to specific files (e.g.
// /etc/nsswitch.conf, /etc/gai.conf), but it's difficult to enumerate the
// exact set of files required in every conceivable situation.
for _, path := range defaultReadOnlyPaths {
fsRules = append(fsRules, landlock.RODirs(path).IgnoreIfMissing())
}
// When ACME is enabled, certmagic persists certificates and keys to
// $XDG_DATA_HOME/certmagic (defaulting to $HOME/.local/share/certmagic).
// Grant RW on the parent dir so certmagic can create and populate the
// certmagic/ subdirectory on first use. The path is per-user so it's not
// safe to add unconditionally. Outbound access to the ACME CA URL (for
// directory/order/finalize and cert downloads) is granted via the
// ConnectTCP loop below using the configured --auto-acme-ca /
// --auto-acme-testca URLs, falling back to Let's Encrypt on tcp/443 when
// neither is set. TLS-ALPN-01 challenge traffic is inbound on the
// listener and is already covered by its BindTCP rule.
if serverAutoACMEFQDN != nil && *serverAutoACMEFQDN != "" {
fsRules = append(fsRules, landlock.RWDirs(filepath.Dir(certmagicDataDir())).IgnoreIfMissing())
if (serverAutoACMEProdCA == nil || *serverAutoACMEProdCA == "") &&
(serverAutoACMETestCA == nil || *serverAutoACMETestCA == "") {
netRules = append(netRules, landlock.ConnectTCP(443))
}
}
// SSL_CERT_FILE and SSL_CERT_DIR override Go's compiled-in CA bundle search
// paths (see crypto/x509/root_unix.go), so if an operator has set these the
// default RO paths above won't cover what Go actually reads.
if f := os.Getenv("SSL_CERT_FILE"); f != "" {
fsRules = append(fsRules, rulesFromFile(f)...)
}
if d := os.Getenv("SSL_CERT_DIR"); d != "" {
for _, dir := range strings.Split(d, ":") {
if dir == "" {
continue
}
fsRules = append(fsRules, rulesFromCertDir(dir)...)
}
}
// Process string flags containing addresses or URLs.
for _, addr := range []*string{
serverListenAddress,
clientListenAddress,
statusAddress,
} {
if addr == nil || len(*addr) == 0 {
continue
}
rule, err := ruleFromStringAddress(*addr, landlock.BindTCP)
if err != nil {
return fmt.Errorf("error processing argument '%s' for landlock rule: %w", *addr, err)
}
if rule != nil {
netRules = append(netRules, rule)
}
}
for _, addr := range []*string{
serverForwardAddress,
serverStatusTargetAddress,
clientForwardAddress,
useWorkloadAPIAddr,
metricsURL,
serverAutoACMEProdCA,
serverAutoACMETestCA,
} {
if addr == nil || len(*addr) == 0 {
continue
}
rule, err := ruleFromStringAddress(*addr, landlock.ConnectTCP)
if err != nil {
return fmt.Errorf("error processing argument '%s' for landlock rule: %w", *addr, err)
}
if rule != nil {
netRules = append(netRules, rule)
}
}
// Process string flags containing file paths.
for _, path := range []*string{
serverAllowPolicy,
clientAllowPolicy,
keystorePath,
certPath,
keyPath,
caBundlePath,
} {
if path == nil || len(*path) == 0 {
continue
}
fsRules = append(fsRules, rulesFromFile(*path)...)
}
// Process net.TCPAddr flags.
for _, addr := range []**net.TCPAddr{metricsGraphite} {
if addr == nil || *addr == nil {
continue
}
rule, err := ruleFromTCPAddress(*addr, landlock.ConnectTCP)
if err != nil {
return fmt.Errorf("error processing argument '%s' for landlock rule: %w", *addr, err)
}
if rule != nil {
netRules = append(netRules, rule)
}
}
// Outbound HTTP from Go's net/http transport (certmagic ACME calls)
// honors the HTTP_PROXY / HTTPS_PROXY env vars at request time. If
// they're set, allow outbound connect to the proxy.
for _, u := range proxyURLsFromEnv() {
rule, err := ruleFromURL(u, landlock.ConnectTCP)
if err != nil {
return fmt.Errorf("error processing proxy env URL '%s' for landlock rule: %w", u, err)
}
if rule != nil {
netRules = append(netRules, rule)
}
}
// Process url.URL flags.
for _, url := range []**url.URL{clientProxy} {
if url == nil || *url == nil {
continue
}
rule, err := ruleFromURL(*url, landlock.ConnectTCP)
if err != nil {
return fmt.Errorf("error processing argument '%s' for landlock rule: %w", *url, err)
}
if rule != nil {
netRules = append(netRules, rule)
}
}
// Log if kernel doesn't support net rules so we don't just silently downgrade
abiVersion, err := llsys.LandlockGetABIVersion()
if err != nil || abiVersion < 4 {
logger.Printf("note: kernel does not support landlock net rules, sandboxing will be limited")
}
// Enable best-effort mode: If the kernel doesn't support ABI v8, then go-landlock
// will enforce as much as possible given the ABI version that *is* available. Note
// that normally no error will be returned in best-effort mode, but we capture and
// return it here anyway to be defensive.
config := landlock.V8.BestEffort()
err = config.RestrictPaths(fsRules...)
if err != nil {
return err
}
return config.RestrictNet(netRules...)
}
// ruleFromStringAddress turns a flag-supplied address string into a landlock
// rule. Handles unix sockets (RW on the socket path), systemd/launchd socket
// activation (no rule needed; the FD is inherited), HTTP/HTTPS URLs, and
// host:port forms. ruleFromPort selects bind vs connect semantics.
func ruleFromStringAddress(addr string, ruleFromPort portRuleFunc) (landlock.Rule, error) {
if strings.HasPrefix(addr, "unix:") {
return landlock.RWFiles(addr[5:]), nil
}
if strings.HasPrefix(addr, "systemd:") || strings.HasPrefix(addr, "launchd:") {
// Socket activation - no rule needed
return nil, nil
}
if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") {
u, err := url.Parse(addr)
if err != nil {
return nil, err
}
return ruleFromURL(u, ruleFromPort)
}
parts := strings.Split(addr, ":")
if len(parts) < 2 {
return nil, errors.New("unable to extract port number from address")
}
port, err := strconv.ParseUint(parts[len(parts)-1], 10, 16)
if err != nil {
return nil, errors.New("unable to extract port number from address")
}
if port == 0 {
return nil, errors.New("unable to extract port number from address")
}
return ruleFromPort(uint16(port)), nil
}
// ruleFromTCPAddress turns a *net.TCPAddr (from already-parsed flags like
// --metrics-graphite) into a port-based landlock rule. ruleFromPort selects
// bind vs connect semantics.
func ruleFromTCPAddress(addr *net.TCPAddr, ruleFromPort portRuleFunc) (landlock.Rule, error) {
if addr.Port == 0 {
return nil, errors.New("unable to extract port number from address")
}
return ruleFromPort(uint16(addr.Port)), nil
}
// ruleFromURL turns a *url.URL into a port-based landlock rule, defaulting
// the port by scheme when none is explicit: 80 for http, 443 for https,
// 1080 for socks5/socks5h. ruleFromPort selects bind vs connect semantics.
func ruleFromURL(u *url.URL, ruleFromPort portRuleFunc) (landlock.Rule, error) {
port := u.Port()
if len(port) == 0 {
switch u.Scheme {
case "http":
return ruleFromPort(uint16(80)), nil
case "https":
return ruleFromPort(uint16(443)), nil
case "socks5", "socks5h":
return ruleFromPort(uint16(1080)), nil
}
}
numericPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return nil, errors.New("unable to extract port number from address")
}
if numericPort == 0 {
return nil, errors.New("unable to extract port number from address")
}
return ruleFromPort(uint16(numericPort)), nil
}
// proxyURLsFromEnv returns deduplicated proxy URLs that Go's net/http would
// honor via HTTP_PROXY / HTTPS_PROXY (and the lowercase variants) at request
// time. NO_PROXY is a bypass list, not a destination, so it needs no rule.
// Mirrors the fallback in golang.org/x/net/http/httpproxy: if the value isn't
// a URL with a recognized scheme, retry with "http://" prepended.
func proxyURLsFromEnv() []*url.URL {
var urls []*url.URL
seen := make(map[string]bool)
for _, name := range []string{"HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"} {
v := os.Getenv(name)
if v == "" || seen[v] {
continue
}
seen[v] = true
u, err := url.Parse(v)
if err != nil || u.Host == "" ||
(u.Scheme != "http" && u.Scheme != "https" &&
u.Scheme != "socks5" && u.Scheme != "socks5h") {
u, err = url.Parse("http://" + v)
if err != nil || u.Host == "" {
continue
}
}
urls = append(urls, u)
}
return urls
}
// rulesFromFile returns RO rules covering the parent directory of path, and
// (if path is a symlink) the parent directory of the symlink target. We grant
// the parent rather than the file itself so the file can be replaced
// atomically (write-new-then-rename) and reloaded without breaking the rule.
func rulesFromFile(path string) []landlock.Rule {
rules := []landlock.Rule{landlock.RODirs(filepath.Dir(path)).IgnoreIfMissing()}
if target, err := filepath.EvalSymlinks(path); err == nil && target != path {
rules = append(rules, landlock.RODirs(filepath.Dir(target)).IgnoreIfMissing())
}
return rules
}
// rulesFromCertDir returns an RO rule for dir, plus an RO rule for the parent
// directory of every readable entry whose symlinks resolve. Distros commonly
// populate cert directories (e.g. /etc/ssl/certs) with symlinks pointing into
// a vendor-specific bundle location, and Go's crypto/x509 follows those
// symlinks when scanning SSL_CERT_DIR — so we need rules covering the
// eventual targets too. Plain files and subdirs produce redundant rules,
// which landlock dedupes at the kernel level.
func rulesFromCertDir(dir string) []landlock.Rule {
rules := []landlock.Rule{landlock.RODirs(dir).IgnoreIfMissing()}
entries, err := os.ReadDir(dir)
if err != nil {
return rules
}
for _, entry := range entries {
target, err := filepath.EvalSymlinks(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
rules = append(rules, landlock.RODirs(filepath.Dir(target)).IgnoreIfMissing())
}
return rules
}
// certmagicDataDir mirrors the path resolution logic in certmagic's
// FileStorage so the landlock rule covers exactly what the library writes to.
// See vendor/github.com/caddyserver/certmagic/filestorage.go (dataDir).
func certmagicDataDir() string {
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
return filepath.Join(xdg, "certmagic")
}
home, err := os.UserHomeDir()
if err != nil || home == "" {
home = "."
}
return filepath.Join(home, ".local", "share", "certmagic")
}