1// Progam web provides an example of a webserver using capabilities to
2// bind to a privileged port, and then drop all capabilities before
3// handling the first web request.
4//
5// This program cannot work reliably as a pure Go application without
6// the equivalent of the Go runtime patch that adds a POSIX semantics
7// wrapper around the system calls that change per-thread security
8// state. A patch for the pure Go compiler/runtime to add this support
9// is available here [2019-12-14]:
10//
11//    https://go-review.googlesource.com/c/go/+/210639/
12//
13// Until that patch, or something like it, is absorbed into the Go
14// runtime the only way to get capabilities to work reliably on the Go
15// runtime is to use something like libpsx via CGo to do capability
16// setting syscalls in C with POSIX semantics. As of this build of the
17// Go "kernel.org/pub/linux/libs/security/libcap/cap" package,
18// courtesy of the "kernel.org/pub/linux/libs/security/libcap/psx"
19// package, this is how things work.
20//
21// To set this up, compile and empower this binary as follows (read
22// over the detail in the psx package description if this doesn't
23// 'just' work):
24//
25//   go build web.go
26//   sudo setcap cap_setpcap,cap_net_bind_service=p web
27//   ./web --port=80
28//
29// Make requests using wget and observe the log of web:
30//
31//   wget -o/dev/null -O/dev/stdout localhost:80
32package main
33
34import (
35	"flag"
36	"fmt"
37	"log"
38	"net"
39	"net/http"
40	"runtime"
41	"syscall"
42
43	"kernel.org/pub/linux/libs/security/libcap/cap"
44)
45
46var (
47	port     = flag.Int("port", 0, "port to listen on")
48	skipPriv = flag.Bool("skip", false, "skip raising the effective capability - will fail for low ports")
49)
50
51// ensureNotEUID aborts the program if it is running setuid something,
52// or being invoked by root.  That is, the preparer isn't setting up
53// the program correctly.
54func ensureNotEUID() {
55	euid := syscall.Geteuid()
56	uid := syscall.Getuid()
57	egid := syscall.Getegid()
58	gid := syscall.Getgid()
59	if uid != euid || gid != egid {
60		log.Fatalf("go runtime is setuid uids:(%d vs %d), gids(%d vs %d)", uid, euid, gid, egid)
61	}
62	if uid == 0 {
63		log.Fatalf("go runtime is running as root - cheating")
64	}
65}
66
67// listen creates a listener by raising effective privilege only to
68// bind to address and then lowering that effective privilege.
69func listen(network, address string) (net.Listener, error) {
70	if *skipPriv {
71		return net.Listen(network, address)
72	}
73
74	orig := cap.GetProc()
75	defer orig.SetProc() // restore original caps on exit.
76
77	c, err := orig.Dup()
78	if err != nil {
79		return nil, fmt.Errorf("failed to dup caps: %v", err)
80	}
81
82	if on, _ := c.GetFlag(cap.Permitted, cap.NET_BIND_SERVICE); !on {
83		return nil, fmt.Errorf("insufficient privilege to bind to low ports - want %q, have %q", cap.NET_BIND_SERVICE, c)
84	}
85
86	if err := c.SetFlag(cap.Effective, true, cap.NET_BIND_SERVICE); err != nil {
87		return nil, fmt.Errorf("unable to set capability: %v", err)
88	}
89
90	if err := c.SetProc(); err != nil {
91		return nil, fmt.Errorf("unable to raise capabilities %q: %v", c, err)
92	}
93	return net.Listen(network, address)
94}
95
96// Handler is used to abstract the ServeHTTP function.
97type Handler struct{}
98
99// ServeHTTP says hello from a single Go hardware thread and reveals
100// its capabilities.
101func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
102	runtime.LockOSThread()
103	// Get some numbers consistent to the current execution, so
104	// the returned web page demonstrates that the code execution
105	// is bouncing around on different kernel thread ids.
106	p := syscall.Getpid()
107	t := syscall.Gettid()
108	c := cap.GetProc()
109	runtime.UnlockOSThread()
110
111	log.Printf("Saying hello from proc: %d->%d, caps=%q", p, t, c)
112	fmt.Fprintf(w, "Hello from proc: %d->%d, caps=%q\n", p, t, c)
113}
114
115func main() {
116	flag.Parse()
117
118	if *port == 0 {
119		log.Fatal("please supply --port value")
120	}
121
122	ensureNotEUID()
123
124	ls, err := listen("tcp", fmt.Sprintf(":%d", *port))
125	if err != nil {
126		log.Fatalf("aborting: %v", err)
127	}
128	defer ls.Close()
129
130	if !*skipPriv {
131		if err := cap.ModeNoPriv.Set(); err != nil {
132			log.Fatalf("unable to drop all privilege: %v", err)
133		}
134	}
135
136	if err := http.Serve(ls, &Handler{}); err != nil {
137		log.Fatalf("server failed: %v", err)
138	}
139}
140