github.com/rish1988/moby@v25.0.2+incompatible/libnetwork/portmapper/proxy_linux.go (about)

     1  package portmapper
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net"
     7  	"os"
     8  	"os/exec"
     9  	"runtime"
    10  	"strconv"
    11  	"syscall"
    12  	"time"
    13  )
    14  
    15  func newProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.IP, containerPort int, proxyPath string) (userlandProxy, error) {
    16  	if proxyPath == "" {
    17  		return nil, fmt.Errorf("no path provided for userland-proxy binary")
    18  	}
    19  
    20  	return &proxyCommand{
    21  		cmd: &exec.Cmd{
    22  			Path: proxyPath,
    23  			Args: []string{
    24  				proxyPath,
    25  				"-proto", proto,
    26  				"-host-ip", hostIP.String(),
    27  				"-host-port", strconv.Itoa(hostPort),
    28  				"-container-ip", containerIP.String(),
    29  				"-container-port", strconv.Itoa(containerPort),
    30  			},
    31  			SysProcAttr: &syscall.SysProcAttr{
    32  				Pdeathsig: syscall.SIGTERM, // send a sigterm to the proxy if the creating thread in the daemon process dies (https://go.dev/issue/27505)
    33  			},
    34  		},
    35  		wait: make(chan error, 1),
    36  	}, nil
    37  }
    38  
    39  // proxyCommand wraps an exec.Cmd to run the userland TCP and UDP
    40  // proxies as separate processes.
    41  type proxyCommand struct {
    42  	cmd  *exec.Cmd
    43  	wait chan error
    44  }
    45  
    46  func (p *proxyCommand) Start() error {
    47  	r, w, err := os.Pipe()
    48  	if err != nil {
    49  		return fmt.Errorf("proxy unable to open os.Pipe %s", err)
    50  	}
    51  	defer r.Close()
    52  	p.cmd.ExtraFiles = []*os.File{w}
    53  
    54  	// As p.cmd.SysProcAttr.Pdeathsig is set, the signal will be sent to the
    55  	// process when the OS thread on which p.cmd.Start() was executed dies.
    56  	// If the thread is allowed to be released back into the goroutine
    57  	// thread pool, the thread could get terminated at any time if a
    58  	// goroutine gets scheduled onto it which calls runtime.LockOSThread()
    59  	// and exits without a matching number of runtime.UnlockOSThread()
    60  	// calls. Ensure that the thread from which Start() is called stays
    61  	// alive until the proxy or the daemon process exits to prevent the
    62  	// proxy from getting terminated early. See https://go.dev/issue/27505
    63  	// for more details.
    64  	started := make(chan error)
    65  	go func() {
    66  		runtime.LockOSThread()
    67  		defer runtime.UnlockOSThread()
    68  		err := p.cmd.Start()
    69  		started <- err
    70  		if err != nil {
    71  			return
    72  		}
    73  		p.wait <- p.cmd.Wait()
    74  	}()
    75  	if err := <-started; err != nil {
    76  		return err
    77  	}
    78  	w.Close()
    79  
    80  	errchan := make(chan error, 1)
    81  	go func() {
    82  		buf := make([]byte, 2)
    83  		r.Read(buf)
    84  
    85  		if string(buf) != "0\n" {
    86  			errStr, err := io.ReadAll(r)
    87  			if err != nil {
    88  				errchan <- fmt.Errorf("Error reading exit status from userland proxy: %v", err)
    89  				return
    90  			}
    91  
    92  			errchan <- fmt.Errorf("Error starting userland proxy: %s", errStr)
    93  			return
    94  		}
    95  		errchan <- nil
    96  	}()
    97  
    98  	select {
    99  	case err := <-errchan:
   100  		return err
   101  	case <-time.After(16 * time.Second):
   102  		return fmt.Errorf("Timed out proxy starting the userland proxy")
   103  	}
   104  }
   105  
   106  func (p *proxyCommand) Stop() error {
   107  	if p.cmd.Process != nil {
   108  		if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
   109  			return err
   110  		}
   111  		return <-p.wait
   112  	}
   113  	return nil
   114  }