aboutsummaryrefslogtreecommitdiff
path: root/goapps/web/web.go
blob: d184e974e03c97e95f10e3d6f407233e53c87c00 (plain)
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
// Progam web provides an example of a webserver using capabilities to
// bind to a privileged port, and then drop all capabilities before
// handling the first web request.
//
// This program cannot work reliably as a pure Go application without
// the equivalent of the Go runtime patch that adds a POSIX semantics
// wrapper around the system calls that change per-thread security
// state. A patch for the pure Go compiler/runtime to add this support
// is available here [2019-12-14]:
//
//    https://go-review.googlesource.com/c/go/+/210639/
//
// Until that patch, or something like it, is absorbed into the Go
// runtime the only way to get capabilities to work reliably on the Go
// runtime is to use something like libpsx via CGo to do capability
// setting syscalls in C with POSIX semantics. As of this build of the
// Go "kernel.org/pub/linux/libs/security/libcap/cap" package,
// courtesy of the "kernel.org/pub/linux/libs/security/libcap/psx"
// package, this is how things work.
//
// To set this up, compile and empower this binary as follows (read
// over the detail in the psx package description if this doesn't
// 'just' work):
//
//   go build web.go
//   sudo setcap cap_setpcap,cap_net_bind_service=p web
//   ./web --port=80
//
// Make requests using wget and observe the log of web:
//
//   wget -o/dev/null -O/dev/stdout localhost:80
package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"net/http"
	"runtime"
	"syscall"

	"kernel.org/pub/linux/libs/security/libcap/cap"
)

var (
	port     = flag.Int("port", 0, "port to listen on")
	skipPriv = flag.Bool("skip", false, "skip raising the effective capability - will fail for low ports")
)

// ensureNotEUID aborts the program if it is running setuid something,
// or being invoked by root.  That is, the preparer isn't setting up
// the program correctly.
func ensureNotEUID() {
	euid := syscall.Geteuid()
	uid := syscall.Getuid()
	egid := syscall.Getegid()
	gid := syscall.Getgid()
	if uid != euid || gid != egid {
		log.Fatalf("go runtime is setuid uids:(%d vs %d), gids(%d vs %d)", uid, euid, gid, egid)
	}
	if uid == 0 {
		log.Fatalf("go runtime is running as root - cheating")
	}
}

// listen creates a listener by raising effective privilege only to
// bind to address and then lowering that effective privilege.
func listen(network, address string) (net.Listener, error) {
	if *skipPriv {
		return net.Listen(network, address)
	}

	orig := cap.GetProc()
	defer orig.SetProc() // restore original caps on exit.

	c, err := orig.Dup()
	if err != nil {
		return nil, fmt.Errorf("failed to dup caps: %v", err)
	}

	if on, _ := c.GetFlag(cap.Permitted, cap.NET_BIND_SERVICE); !on {
		return nil, fmt.Errorf("insufficient privilege to bind to low ports - want %q, have %q", cap.NET_BIND_SERVICE, c)
	}

	if err := c.SetFlag(cap.Effective, true, cap.NET_BIND_SERVICE); err != nil {
		return nil, fmt.Errorf("unable to set capability: %v", err)
	}

	if err := c.SetProc(); err != nil {
		return nil, fmt.Errorf("unable to raise capabilities %q: %v", c, err)
	}
	return net.Listen(network, address)
}

// Handler is used to abstract the ServeHTTP function.
type Handler struct{}

// ServeHTTP says hello from a single Go hardware thread and reveals
// its capabilities.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	runtime.LockOSThread()
	// Get some numbers consistent to the current execution, so
	// the returned web page demonstrates that the code execution
	// is bouncing around on different kernel thread ids.
	p := syscall.Getpid()
	t := syscall.Gettid()
	c := cap.GetProc()
	runtime.UnlockOSThread()

	log.Printf("Saying hello from proc: %d->%d, caps=%q", p, t, c)
	fmt.Fprintf(w, "Hello from proc: %d->%d, caps=%q\n", p, t, c)
}

func main() {
	flag.Parse()

	if *port == 0 {
		log.Fatal("please supply --port value")
	}

	ensureNotEUID()

	ls, err := listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("aborting: %v", err)
	}
	defer ls.Close()

	if !*skipPriv {
		if err := cap.ModeNoPriv.Set(); err != nil {
			log.Fatalf("unable to drop all privilege: %v", err)
		}
	}

	if err := http.Serve(ls, &Handler{}); err != nil {
		log.Fatalf("server failed: %v", err)
	}
}