@@ -26,6 +26,7 @@ import (
2626 "os"
2727 "os/exec"
2828 "path/filepath"
29+ "strconv"
2930 "strings"
3031 "time"
3132
@@ -420,11 +421,94 @@ func getIP6AddressOpts(opts *handlerOpts) ([]cni.NamespaceOpts, error) {
420421 return nil , nil
421422}
422423
424+ func reserveSocket (protocol , hostAddr string ) (* os.File , error ) {
425+ type filer interface {
426+ File () (* os.File , error )
427+ }
428+ var f filer
429+ switch {
430+ case strings .HasPrefix (protocol , "tcp" ):
431+ l , err := net .Listen (protocol , hostAddr )
432+ if err != nil {
433+ return nil , err
434+ }
435+ defer l .Close ()
436+ var ok bool
437+ f , ok = l .(filer )
438+ if ! ok {
439+ return nil , fmt .Errorf ("cannot get file descriptor from the listener of type %T" , l )
440+ }
441+ case strings .HasPrefix (protocol , "udp" ):
442+ l , err := net .ListenPacket (protocol , hostAddr )
443+ if err != nil {
444+ return nil , err
445+ }
446+ defer l .Close ()
447+ var ok bool
448+ f , ok = l .(filer )
449+ if ! ok {
450+ return nil , fmt .Errorf ("cannot get file descriptor from the listener of type %T" , l )
451+ }
452+ default :
453+ return nil , fmt .Errorf ("unsupported protocol %q" , protocol )
454+ }
455+ return f .File ()
456+ }
457+
458+ // portReserverPidFilePath returns /run/nerdctl/<namespace>/<id>/port-reserver.pid
459+ func portReserverPidFilePath (opts * handlerOpts ) string {
460+ return filepath .Join ("/run/nerdctl/" , opts .state .Annotations [labels .Namespace ], opts .state .ID , "port-reserver.pid" )
461+ }
462+
423463func applyNetworkSettings (opts * handlerOpts ) (err error ) {
424464 portMapOpts , err := getPortMapOpts (opts )
425465 if err != nil {
426466 return err
427467 }
468+ if ! rootlessutil .IsRootlessChild () && len (opts .ports ) > 0 {
469+ // When running in rootful mode, reserve the ports on the host
470+ // so that the ports appears on /proc/net/tcp.
471+ //
472+ // This also prevents other processes from binding to the same ports.
473+ //
474+ // Note that in rootless mode this is not necessary because
475+ // RootlessKit's port driver already reserves the ports.
476+ //
477+ // See https://github.com/lima-vm/lima/issues/4085
478+ //
479+ // Similar patterns are used in Docker and Podman.
480+ // - https://github.com/moby/moby/pull/48132
481+ // - https://github.com/containers/podman/pull/23446
482+ reserverCmd := exec .Command ("sleep" , "infinity" )
483+ for _ , p := range opts .ports {
484+ protocol := p .Protocol
485+ if ! strings .HasSuffix (protocol , "4" ) && ! strings .HasSuffix (protocol , "6" ) {
486+ // e.g. "tcp" -> "tcp4"
487+ protocol += "4"
488+ }
489+ hostAddr := net .JoinHostPort (p .HostIP , strconv .Itoa (int (p .HostPort )))
490+ f , err := reserveSocket (protocol , hostAddr )
491+ if err != nil {
492+ log .L .WithError (err ).Warnf ("cannot reserve the port %s/%s" , hostAddr , protocol )
493+ continue
494+ }
495+ reserverCmd .ExtraFiles = append (reserverCmd .ExtraFiles , f )
496+ }
497+ if err := reserverCmd .Start (); err != nil {
498+ return fmt .Errorf ("cannot start the port reserver process: %w" , err )
499+ }
500+ reserverCmdPid := reserverCmd .Process .Pid
501+ log .L .Debugf ("started the port reserver process (pid=%d)" , reserverCmdPid )
502+ defer func () {
503+ if err != nil {
504+ log .L .Debugf ("killing the port reserver process (pid=%d)" , reserverCmdPid )
505+ _ = reserverCmd .Process .Kill ()
506+ }
507+ }()
508+ if err := writePidFile (portReserverPidFilePath (opts ), reserverCmdPid ); err != nil {
509+ return fmt .Errorf ("cannot write the pid file of the port reserver process: %w" , err )
510+ }
511+ }
428512 nsPath , err := getNetNSPath (opts .state )
429513 if err != nil {
430514 return err
@@ -659,6 +743,11 @@ func onPostStop(opts *handlerOpts) error {
659743 if err := namst .Release (name , opts .state .ID ); err != nil && ! errors .Is (err , store .ErrNotFound ) {
660744 return fmt .Errorf ("failed to release container name %s: %w" , name , err )
661745 }
746+ // Kill port-reserver process if any
747+ portReserverPidFile := portReserverPidFilePath (opts )
748+ if err = killProcessByPidFile (portReserverPidFile ); err != nil {
749+ log .L .WithError (err ).Errorf ("failed to kill the port-reserver process" )
750+ }
662751 return nil
663752}
664753
@@ -706,7 +795,11 @@ func writePidFile(path string, pid int) error {
706795 if err != nil {
707796 return err
708797 }
709- tempPath := filepath .Join (filepath .Dir (path ), fmt .Sprintf (".%s" , filepath .Base (path )))
798+ dir := filepath .Dir (path )
799+ if err := os .MkdirAll (dir , 0755 ); err != nil {
800+ return err
801+ }
802+ tempPath := filepath .Join (dir , fmt .Sprintf (".%s" , filepath .Base (path )))
710803 f , err := os .OpenFile (tempPath , os .O_RDWR | os .O_CREATE | os .O_EXCL | os .O_SYNC , 0666 )
711804 if err != nil {
712805 return err
@@ -718,3 +811,25 @@ func writePidFile(path string, pid int) error {
718811 }
719812 return os .Rename (tempPath , path )
720813}
814+
815+ func killProcessByPidFile (pidFile string ) error {
816+ pidData , err := os .ReadFile (pidFile )
817+ if err != nil {
818+ if errors .Is (err , os .ErrNotExist ) {
819+ err = nil
820+ }
821+ return err
822+ }
823+ pid , err := strconv .Atoi (strings .TrimSpace (string (pidData )))
824+ if err != nil {
825+ return fmt .Errorf ("failed to parse pid %q from %q: %w" , string (pidData ), pidFile , err )
826+ }
827+ proc , err := os .FindProcess (pid )
828+ if err != nil {
829+ return fmt .Errorf ("failed to find process %d: %w" , pid , err )
830+ }
831+ if err := proc .Kill (); err != nil {
832+ return fmt .Errorf ("failed to kill process %d: %w" , pid , err )
833+ }
834+ return nil
835+ }
0 commit comments