| 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<error message>" 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) | |
| } | |
| } |