@@ -162,13 +162,15 @@ func errorf(fmt string, parts ...interface{}) {
162162func 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