package main import ( "flag" "fmt" "net" "os" "os/signal" "syscall" "github.com/ishidawataru/sctp" "github.com/moby/moby/v2/dockerversion" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) // The caller is expected to pass-in open file descriptors ... const ( // Pipe for reporting status, as a string. "0\n" if the proxy // started normally. "1\n" otherwise. parentPipeFd uintptr = 3 + iota // If -use-listen-fd=true, a listening socket ready to accept TCP // connections or receive UDP. (Without that option on the command // line, the listener needs to be opened by docker-proxy, for // compatibility with older docker daemons. In this case fd 4 // may belong to the Go runtime.) listenSockFd ) func main() { // Mark any files we expect to inherit as close-on-exec // so that they are not unexpectedly inherited by any child processes // if we ever need docker-proxy to exec something. // This is safe to do even if the fd belongs to the Go runtime // as it would be a no-op: // the Go runtime marks all file descriptors it opens as close-on-exec. // See the godoc for syscall.ForkLock for more information. syscall.CloseOnExec(int(parentPipeFd)) syscall.CloseOnExec(int(listenSockFd)) config := parseFlags() p, err := newProxy(config) if config.ListenSock != nil { config.ListenSock.Close() } _ = syscall.SetNonblock(int(parentPipeFd), true) f := os.NewFile(parentPipeFd, "signal-parent") if err != nil { fmt.Fprintf(f, "1\n%s", err) f.Close() os.Exit(1) } go handleStopSignals(p) fmt.Fprint(f, "0\n") f.Close() // Run will block until the proxy stops p.Run() } func newProxy(config ProxyConfig) (p Proxy, err error) { ipv := ip4 if config.HostIP.To4() == nil { ipv = ip6 } switch config.Proto { case "tcp": var listener *net.TCPListener if config.ListenSock == nil { // Fall back to HostIP:HostPort if no socket on fd 4, for compatibility with older daemons. hostAddr := &net.TCPAddr{IP: config.HostIP, Port: config.HostPort} listener, err = net.ListenTCP("tcp"+string(ipv), hostAddr) if err != nil { return nil, fmt.Errorf("failed to listen on %s: %w", hostAddr, err) } } else { l, err := net.FileListener(config.ListenSock) if err != nil { return nil, err } var ok bool listener, ok = l.(*net.TCPListener) if !ok { return nil, fmt.Errorf("unexpected socket type for listener fd: %s", l.Addr().Network()) } } container := &net.TCPAddr{IP: config.ContainerIP, Port: config.ContainerPort} p, err = NewTCPProxy(listener, container) case "udp": var listener *net.UDPConn if config.ListenSock == nil { // Fall back to HostIP:HostPort if no socket on fd 4, for compatibility with older daemons. hostAddr := &net.UDPAddr{IP: config.HostIP, Port: config.HostPort} listener, err = net.ListenUDP("udp"+string(ipv), hostAddr) if err != nil { return nil, fmt.Errorf("failed to listen on %s: %w", hostAddr, err) } // We need to setsockopt(IP_PKTINFO) on the listener to get the destination address as an ancillary // message. The daddr will be used as the source address when sending back replies coming from the // container to the client. If we don't do this, the kernel will have to pick a source address for us, and // it might not pick what the client expects. That would result in ICMP Port Unreachable. if ipv == ip4 { pc := ipv4.NewPacketConn(listener) if err := pc.SetControlMessage(ipv4.FlagDst, true); err != nil { return nil, fmt.Errorf("failed to setsockopt(IP_PKTINFO): %w", err) } } else { pc := ipv6.NewPacketConn(listener) if err := pc.SetControlMessage(ipv6.FlagDst, true); err != nil { return nil, fmt.Errorf("failed to setsockopt(IPV6_RECVPKTINFO): %w", err) } } } else { l, err := net.FilePacketConn(config.ListenSock) if err != nil { return nil, err } var ok bool listener, ok = l.(*net.UDPConn) if !ok { return nil, fmt.Errorf("unexpected socket type for listener fd: %s", l.LocalAddr().Network()) } } container := &net.UDPAddr{IP: config.ContainerIP, Port: config.ContainerPort} p, err = NewUDPProxy(listener, container, ipv) case "sctp": var listener *sctp.SCTPListener if config.ListenSock == nil { hostAddr := &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: config.HostIP}}, Port: config.HostPort} listener, err = sctp.ListenSCTP("sctp"+string(ipv), hostAddr) if err != nil { return nil, fmt.Errorf("failed to listen on %s: %w", hostAddr, err) } } else { if listener, err = sctp.FileListener(config.ListenSock); err != nil { return nil, err } } container := &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: config.ContainerIP}}, Port: config.ContainerPort} p, err = NewSCTPProxy(listener, container) default: return nil, fmt.Errorf("unsupported protocol %s", config.Proto) } return p, err } type ProxyConfig struct { Proto string HostIP, ContainerIP net.IP HostPort, ContainerPort int ListenSock *os.File } // parseFlags parses the flags passed on reexec to create the TCP/UDP/SCTP // net.Addrs to map the host and container ports. func parseFlags() ProxyConfig { var ( config ProxyConfig useListenFd bool printVer bool ) flag.StringVar(&config.Proto, "proto", "tcp", "proxy protocol") flag.TextVar(&config.HostIP, "host-ip", net.IPv4zero, "host ip") flag.IntVar(&config.HostPort, "host-port", -1, "host port") flag.TextVar(&config.ContainerIP, "container-ip", net.IPv4zero, "container ip") flag.IntVar(&config.ContainerPort, "container-port", -1, "container port") flag.BoolVar(&useListenFd, "use-listen-fd", false, "use a supplied listen fd") flag.BoolVar(&printVer, "v", false, "print version information and quit") flag.BoolVar(&printVer, "version", false, "print version information and quit") flag.Parse() if printVer { fmt.Printf("docker-proxy (commit %s) version %s\n", dockerversion.GitCommit, dockerversion.Version) os.Exit(0) } if useListenFd { _ = syscall.SetNonblock(int(listenSockFd), true) config.ListenSock = os.NewFile(listenSockFd, "listen-sock") } return config } func handleStopSignals(p Proxy) { s := make(chan os.Signal, 10) signal.Notify(s, os.Interrupt, syscall.SIGTERM) for range s { p.Close() os.Exit(0) } }