mirror of
https://github.com/moby/moby.git
synced 2026-07-16 04:21:19 +00:00
Before commit4f09af6, when allocating host ports for a new port mapping, iptables rules were set up then docker-proxy was started. If the host port was already in-use, docker-proxy exited with an error, and the iptables rules were removed. That could potentially interfere with a non-docker service that was already using the host port for something unrelated. Commit4f09af6swapped that problem for a different one... in order to check that a port was available before creating iptables rules, it attempted to start docker-proxy first. If it failed, it could then try a different host port, without interfering with any other service. The problem with that is docker-proxy would start listening before the iptables rules were in place, so it could accept connections then become unusable because new NAT rules diverted packets directly to the container. This would leave the client with a broken connection, causing at-least a delay while it figured that out and reconnected. This change creates and binds the socket in the daemon, before creating iptables rules. If the bind fails, it may try a different port. When or if the bind succeeds, iptables rules are created, then the daemon calls listen on the socket. If docker-proxy is needed, the socket is handed over to it at that point. In rootless mode, the ports have to be bound to an address in the rootless network namespace (where dockerd is running). DNAT rules now use the same address. If docker-proxy is not needed ("--userland-proxy=false"), the daemon still listens on TCP sockets as the old dummyProxy would have done. This makes the socket show up in "netstat" output. The dummyProxy is no longer needed on Linux. Its job was to bind the host ports if docker-proxy was disabled, but that's now already handled by binding the sockets early. This change doesn't affect SCTP, because it's not currently possible for docker-proxy to convert the file descriptor into an SCTPListener. So, docker-proxy is still started early, and the window for lost connections remains. If the user has an old docker-proxy in their path and it's given a listener docker with '-use-listen-fd', it'll fail because of the unknown option. In this case, the daemon's error message suggests checking $PATH. Signed-off-by: Rob Murray <rob.murray@docker.com>
130 lines
3.3 KiB
Go
130 lines
3.3 KiB
Go
package portmapper
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/docker/docker/libnetwork/types"
|
|
)
|
|
|
|
// StartProxy starts the proxy process at proxyPath.
|
|
// If listenSock is not nil, it must be a bound socket that can be passed to
|
|
// the proxy process for it to listen on.
|
|
func StartProxy(pb types.PortBinding,
|
|
proxyPath string,
|
|
listenSock *os.File,
|
|
) (stop func() error, retErr error) {
|
|
if proxyPath == "" {
|
|
return nil, fmt.Errorf("no path provided for userland-proxy binary")
|
|
}
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("proxy unable to open os.Pipe %s", err)
|
|
}
|
|
defer func() {
|
|
if w != nil {
|
|
w.Close()
|
|
}
|
|
r.Close()
|
|
}()
|
|
|
|
cmd := &exec.Cmd{
|
|
Path: proxyPath,
|
|
Args: []string{
|
|
proxyPath,
|
|
"-proto", pb.Proto.String(),
|
|
"-host-ip", pb.HostIP.String(),
|
|
"-host-port", strconv.FormatUint(uint64(pb.HostPort), 10),
|
|
"-container-ip", pb.IP.String(),
|
|
"-container-port", strconv.FormatUint(uint64(pb.Port), 10),
|
|
},
|
|
ExtraFiles: []*os.File{w},
|
|
SysProcAttr: &syscall.SysProcAttr{
|
|
Pdeathsig: syscall.SIGTERM, // send a sigterm to the proxy if the creating thread in the daemon process dies (https://go.dev/issue/27505)
|
|
},
|
|
}
|
|
if listenSock != nil {
|
|
cmd.Args = append(cmd.Args, "-use-listen-fd")
|
|
cmd.ExtraFiles = append(cmd.ExtraFiles, listenSock)
|
|
}
|
|
|
|
wait := make(chan error, 1)
|
|
|
|
// As p.cmd.SysProcAttr.Pdeathsig is set, the signal will be sent to the
|
|
// process when the OS thread on which p.cmd.Start() was executed dies.
|
|
// If the thread is allowed to be released back into the goroutine
|
|
// thread pool, the thread could get terminated at any time if a
|
|
// goroutine gets scheduled onto it which calls runtime.LockOSThread()
|
|
// and exits without a matching number of runtime.UnlockOSThread()
|
|
// calls. Ensure that the thread from which Start() is called stays
|
|
// alive until the proxy or the daemon process exits to prevent the
|
|
// proxy from getting terminated early. See https://go.dev/issue/27505
|
|
// for more details.
|
|
started := make(chan error)
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
err := cmd.Start()
|
|
started <- err
|
|
if err != nil {
|
|
return
|
|
}
|
|
wait <- cmd.Wait()
|
|
}()
|
|
if err := <-started; err != nil {
|
|
return nil, err
|
|
}
|
|
w.Close()
|
|
w = nil
|
|
|
|
errchan := make(chan error, 1)
|
|
go func() {
|
|
buf := make([]byte, 2)
|
|
r.Read(buf)
|
|
|
|
if string(buf) != "0\n" {
|
|
errStr, err := io.ReadAll(r)
|
|
if err != nil {
|
|
errchan <- fmt.Errorf("error reading exit status from userland proxy: %v", err)
|
|
return
|
|
}
|
|
// If the user has an old docker-proxy in their PATH, and we passed "-use-listen-fd"
|
|
// on the command line, it exits with no response on the pipe.
|
|
if listenSock != nil && buf[0] == 0 && len(errStr) == 0 {
|
|
errchan <- errors.New("failed to start docker-proxy, check that the current version is in your $PATH")
|
|
return
|
|
}
|
|
errchan <- fmt.Errorf("error starting userland proxy: %s", errStr)
|
|
return
|
|
}
|
|
errchan <- nil
|
|
}()
|
|
|
|
select {
|
|
case err := <-errchan:
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case <-time.After(16 * time.Second):
|
|
return nil, fmt.Errorf("timed out starting the userland proxy")
|
|
}
|
|
|
|
stopFn := func() error {
|
|
if cmd.Process == nil {
|
|
return nil
|
|
}
|
|
if err := cmd.Process.Signal(os.Interrupt); err != nil {
|
|
return err
|
|
}
|
|
return <-wait
|
|
}
|
|
return stopFn, nil
|
|
}
|