mirror of
https://github.com/moby/moby.git
synced 2026-06-30 19:58:03 +00:00
When a UDP server is running on a multihomed server, as is the case with pretty much _all_ Docker hosts (eg. eth0 + docker0), the kernel has to choose which source address is used when replying to a UDP client. But that process is based on heuristics and is fallible. If the address picked doesn't match the original destination address used by the client, it'll drop the datagram and return an ICMP Port Unreachable. To prevent that, we need to: - `setsockopt(IP_PKTINFO)` on proxy's sockets. - Extract the original destination address from an ancillary message every time a new 'UDP connection' is 'established' (ie. every time we insert a new entry into the UDP conntrack table). - And finally, pass a control message containing the desired source address to the kernel, every time we send a response back to the client. Also, update the inline comment on read errors in `(*UDPProxy).Run()`. This comment was misleadingly referencing ECONNREFUSED - Linux's UDP implementation never returns this error (see [1]). Instead, state why `net.ErrClosed` is perfectly fine and doesn't need to be logged (although, docker-proxy currently logs to nowhere). [1]: https://github.com/search?q=repo%3Atorvalds%2Flinux+ECONNREFUSED+path%3A%2F%5Enet%5C%2F%28ipv4%7Cipv6%29%5C%2F%28udp%7Ctcp%29%2F&type=code Signed-off-by: Albin Kerouanton <albinker@gmail.com>
199 lines
6.4 KiB
Go
199 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/docker/docker/dockerversion"
|
|
"github.com/ishidawataru/sctp"
|
|
"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 {
|
|
// There's no way to construct an SCTPListener from a file descriptor at the moment.
|
|
// If a socket has been passed in, it's probably from a newer daemon using a version
|
|
// of the sctp module that does allow it.
|
|
return nil, errors.New("cannot use supplied SCTP socket, check the latest docker-proxy is in your $PATH")
|
|
}
|
|
hostAddr := &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: config.HostIP}}, Port: config.HostPort}
|
|
container := &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: config.ContainerIP}}, Port: config.ContainerPort}
|
|
listener, err = sctp.ListenSCTP("sctp"+string(ipv), hostAddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to listen on %s: %w", hostAddr, err)
|
|
}
|
|
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)
|
|
}
|
|
}
|