Skip to content

Commit 9f8f5a6

Browse files
authored
Merge pull request #32 from monasticacademy/match-original-uid
Run subcommands in same user and group that httptap was original run in
2 parents bb6fcd3 + f616aeb commit 9f8f5a6

File tree

2 files changed

+137
-64
lines changed

2 files changed

+137
-64
lines changed

Makefile

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ test-echo:
3939
# Output:
4040
# hello
4141

42+
# Test that the user and group doesn't change inside httptap
43+
44+
test-uid:
45+
id -u > expected
46+
httptap -- bash -c "id -u > actual"
47+
diff actual expected
48+
49+
test-gid:
50+
id -g > expected
51+
httptap -- bash -c "id -g > actual"
52+
diff actual expected
53+
54+
test-root:
55+
httptap --user root -- id -u
56+
57+
# Output:
58+
# 0
59+
4260
test-curl:
4361
httptap -- bash -c "curl -s https://example.com > out"
4462

@@ -161,7 +179,7 @@ test-python:
161179

162180
test-java:
163181
javac testing/java/Example.java
164-
httptap -- java -cp testing/java Example 2>1 | grep -v JAVA_OPTIONS
182+
httptap -- java -cp testing/java Example 2>&1 | grep -v JAVA_OPTIONS
165183

166184
# Output:
167185
# ---> GET https://example.com/
@@ -224,16 +242,14 @@ test-har:
224242
# ---> GET https://www.monasticacademy.org/
225243
# <--- 200 https://www.monasticacademy.org/ (31955 bytes)
226244

227-
# These tests are currently broken
228-
229-
manual-test-nonroot-user:
230-
./testing/httptap_test --user $(USER) -- bash -norc
231-
232-
# these tests require things that I do not want to install into github actions
245+
# These tests require things that I do not want to install into github actions
233246

234247
manual-test-gcloud:
235248
./testing/httptap_test gcloud compute instances list
236249

250+
manual-test-wine-battle-net:
251+
go run . -- wine ~/Downloads/Battle.net-Setup.exe
252+
237253
# Test running inside sudo
238254

239255
test-sudo:

httptap.go

Lines changed: 114 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,15 @@ func errorf(fmt string, parts ...interface{}) {
162162
func Main() error {
163163
ctx := context.Background()
164164
var args struct {
165-
Verbose bool `arg:"-v,--verbose,env:HTTPTAP_VERBOSE"`
166-
NoNewUserNamespace bool `arg:"--no-new-user-namespace,env:HTTPTAP_NO_NEW_USER_NAMESPACE" help:"do not create a new user namespace (must be run as root)"`
167-
Stderr bool `arg:"env:HTTPTAP_LOG_TO_STDERR" help:"log to standard error (default is standard out)"`
168-
Tun string `default:"httptap" help:"name of the TUN device that will be created"`
169-
Subnet string `default:"10.1.1.100/24" help:"IP address of the network interface that the subprocess will see"`
170-
Gateway string `default:"10.1.1.1" help:"IP address of the gateway that intercepts and proxies network packets"`
171-
WebUI string `arg:"env:HTTPTAP_WEB_UI" help:"address and port to serve API on"`
165+
Verbose bool `arg:"-v,--verbose,env:HTTPTAP_VERBOSE"`
166+
NoNewUserNamespace bool `arg:"--no-new-user-namespace,env:HTTPTAP_NO_NEW_USER_NAMESPACE" help:"do not create a new user namespace (must be run as root)"`
167+
Stderr bool `arg:"env:HTTPTAP_LOG_TO_STDERR" help:"log to standard error (default is standard out)"`
168+
Tun string `default:"httptap" help:"name of the TUN device that will be created"`
169+
Subnet string `default:"10.1.1.100/24" help:"IP address of the network interface that the subprocess will see"`
170+
Gateway string `default:"10.1.1.1" help:"IP address of the gateway that intercepts and proxies network packets"`
171+
WebUI string `arg:"env:HTTPTAP_WEB_UI" help:"address and port to serve API on"`
172+
UID int
173+
GID int
172174
User string `help:"run command as this user (username or id)"`
173175
NoOverlay bool `arg:"--no-overlay,env:HTTPTAP_NO_OVERLAY" help:"do not mount any overlay filesystems"`
174176
Stack string `arg:"env:HTTPTAP_STACK" default:"gvisor" help:"which tcp implementation to use: 'gvisor' or 'homegrown'"`
@@ -194,8 +196,29 @@ func Main() error {
194196
isVerbose = args.Verbose
195197

196198
// first we re-exec ourselves in a new user namespace
197-
if os.Args[0] != "/proc/self/exe" && !args.NoNewUserNamespace {
198-
verbosef("re-execing in a new user namespace...")
199+
if !strings.HasPrefix(os.Args[0], "httptap.stage.") && !args.NoNewUserNamespace {
200+
verbosef("at first stage, launching second stage in a new user namespace...")
201+
202+
// Decide which user and group we should later switch to. We must do this before creating the user
203+
// namespace because then we will not know which user we were originally launched by.
204+
uid := os.Geteuid()
205+
gid := os.Getegid()
206+
if args.User != "" {
207+
u, err := user.Lookup(args.User)
208+
if err != nil {
209+
return fmt.Errorf("error looking up user %q: %w", args.User, err)
210+
}
211+
212+
uid, err = strconv.Atoi(u.Uid)
213+
if err != nil {
214+
return fmt.Errorf("error parsing user id %q as a number: %w", u.Uid, err)
215+
}
216+
217+
gid, err = strconv.Atoi(u.Gid)
218+
if err != nil {
219+
return fmt.Errorf("error parsing group id %q as a number: %w", u.Gid, err)
220+
}
221+
}
199222

200223
// Here we move to a new user namespace, which is an unpriveleged operation, and which
201224
// allows us to do everything else without being root.
@@ -210,7 +233,11 @@ func Main() error {
210233
// is the same approach taken by docker's reexec package.
211234

212235
cmd := exec.Command("/proc/self/exe")
213-
cmd.Args = append([]string{"/proc/self/exe"}, os.Args[1:]...)
236+
cmd.Args = append([]string{
237+
"httptap.stage.2",
238+
"--uid", strconv.Itoa(uid),
239+
"--gid", strconv.Itoa(gid)},
240+
os.Args[1:]...)
214241
cmd.Stdin = os.Stdin
215242
cmd.Stdout = os.Stdout
216243
cmd.Stderr = os.Stderr
@@ -240,7 +267,48 @@ func Main() error {
240267
return nil
241268
}
242269

243-
verbosef("running assuming we are in a user namespace...")
270+
if os.Args[0] == "httptap.stage.3" {
271+
verbose("at third stage...")
272+
273+
// there are three (!) user/group IDs for a process: the real, effective, and saved
274+
// they have the purpose of allowing the process to go "back" to them
275+
// here we set just the effective, which, when you are root, sets all three
276+
277+
if args.GID != 0 {
278+
verbosef("switching to gid %d", args.GID)
279+
err := unix.Setgid(args.GID)
280+
if err != nil {
281+
return fmt.Errorf("error switching to group %v: %w", args.GID, err)
282+
}
283+
}
284+
285+
if args.UID != 0 {
286+
verbosef("switching to uid %d", args.UID)
287+
err := unix.Setuid(args.UID)
288+
if err != nil {
289+
return fmt.Errorf("error switching to user %v: %w", args.UID, err)
290+
}
291+
}
292+
293+
verbosef("third stage now in uid %d, gid %d, launching final subprocess...", unix.Getuid(), unix.Getgid())
294+
295+
// launch the command that the user originally requested
296+
cmd := exec.Command(args.Command[0])
297+
cmd.Args = args.Command
298+
cmd.Stdin = os.Stdin
299+
cmd.Stdout = os.Stdout
300+
cmd.Stderr = os.Stderr
301+
err := cmd.Run()
302+
if exiterr, ok := err.(*exec.ExitError); ok {
303+
os.Exit(exiterr.ExitCode())
304+
}
305+
if err != nil {
306+
return fmt.Errorf("error launching final subprocess from third stage: %w", err)
307+
}
308+
return nil
309+
}
310+
311+
verbosef("at second stage, creating certificate authority...")
244312

245313
// generate a root certificate authority
246314
ca, err := certin.NewCert(nil, certin.Request{CN: "root CA", IsCA: true})
@@ -441,40 +509,6 @@ func Main() error {
441509
}
442510
}
443511

444-
// switch user and group if requested
445-
if args.User != "" {
446-
u, err := user.Lookup(args.User)
447-
if err != nil {
448-
return fmt.Errorf("error looking up user %q: %w", args.User, err)
449-
}
450-
451-
uid, err := strconv.Atoi(u.Uid)
452-
if err != nil {
453-
return fmt.Errorf("error parsing user id %q as a number: %w", u.Uid, err)
454-
}
455-
456-
gid, err := strconv.Atoi(u.Gid)
457-
if err != nil {
458-
return fmt.Errorf("error parsing group id %q as a number: %w", u.Gid, err)
459-
}
460-
461-
// there are three (!) user/group IDs for a process: the real, effective, and saved
462-
// they have the purpose of allowing the process to go "back" to them
463-
// here we set just the effective, which, when you are root, sets all three
464-
465-
err = unix.Setgid(gid)
466-
if err != nil {
467-
return fmt.Errorf("error switching to group %q (gid %v): %w", args.User, gid, err)
468-
}
469-
470-
err = unix.Setuid(uid)
471-
if err != nil {
472-
return fmt.Errorf("error switching to user %q (uid %v): %w", args.User, uid, err)
473-
}
474-
475-
verbosef("now in uid %d, gid %d", unix.Getuid(), unix.Getgid())
476-
}
477-
478512
// start printing to standard output if requested
479513
httpcalls, _ := listenHTTP()
480514
go func() {
@@ -597,18 +631,6 @@ func Main() error {
597631

598632
verbose("running subcommand now ================")
599633

600-
// launch a subprocess -- we are already in the network namespace so nothing special here
601-
cmd := exec.Command(args.Command[0])
602-
cmd.Args = args.Command
603-
cmd.Stdin = os.Stdin
604-
cmd.Stdout = os.Stdout
605-
cmd.Stderr = os.Stderr
606-
cmd.Env = env
607-
err = cmd.Start()
608-
if err != nil {
609-
return fmt.Errorf("error starting subprocess: %w", err)
610-
}
611-
612634
// create a goroutine to facilitate sending packets to the process
613635
toSubprocess := make(chan []byte, 1000)
614636
go copyToDevice(ctx, tun, toSubprocess)
@@ -846,6 +868,41 @@ func Main() error {
846868
return fmt.Errorf("invalid stack %q; valid choices are 'gvisor' or 'homegrown'", args.Stack)
847869
}
848870

871+
verbosef("launching third stage targetting uid %d, gid %d...", args.UID, args.GID)
872+
873+
// launch the third stage in a second user namespace, this time with mappings reversed
874+
cmd := exec.Command("/proc/self/exe")
875+
cmd.Args = append([]string{
876+
"httptap.stage.3",
877+
"--uid", strconv.Itoa(args.UID),
878+
"--gid", strconv.Itoa(args.GID), "--"},
879+
args.Command...)
880+
cmd.Stdin = os.Stdin
881+
cmd.Stdout = os.Stdout
882+
cmd.Stderr = os.Stderr
883+
cmd.Env = env
884+
885+
if !args.NoNewUserNamespace {
886+
cmd.SysProcAttr = &syscall.SysProcAttr{
887+
Cloneflags: syscall.CLONE_NEWUSER,
888+
UidMappings: []syscall.SysProcIDMap{{
889+
ContainerID: args.UID,
890+
HostID: 0,
891+
Size: 1,
892+
}},
893+
GidMappings: []syscall.SysProcIDMap{{
894+
ContainerID: args.GID,
895+
HostID: 0,
896+
Size: 1,
897+
}},
898+
}
899+
}
900+
901+
err = cmd.Start()
902+
if err != nil {
903+
return fmt.Errorf("error starting third stage subprocess: %w", err)
904+
}
905+
849906
// wait for the subprocess to complete
850907
err = cmd.Wait()
851908
if err != nil {

0 commit comments

Comments
 (0)