File: main_linux.go

package info (click to toggle)
docker.io 28.5.2%2Bdfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 69,048 kB
  • sloc: sh: 5,867; makefile: 863; ansic: 184; python: 162; asm: 159
file content (198 lines) | stat: -rw-r--r-- 6,562 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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)
	}
}