mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Issue #33: Move portmapper and portallocator into libnetwork
- As they provide network translation functionalities, they should be part of libnetwork - In driver/bridge/setup_ip_tables.go remove depenency on docker/daemon/networkdriver Signed-off-by: Alessandro Boch <aboch@docker.com>
This commit is contained in:
parent
724948d6ed
commit
5d7b430801
7 changed files with 923 additions and 3 deletions
182
libnetwork/portmapper/mapper.go
Normal file
182
libnetwork/portmapper/mapper.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package portmapper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/pkg/iptables"
|
||||
"github.com/docker/libnetwork/portallocator"
|
||||
)
|
||||
|
||||
type mapping struct {
|
||||
proto string
|
||||
userlandProxy userlandProxy
|
||||
host net.Addr
|
||||
container net.Addr
|
||||
}
|
||||
|
||||
var (
|
||||
chain *iptables.Chain
|
||||
lock sync.Mutex
|
||||
|
||||
// udp:ip:port
|
||||
currentMappings = make(map[string]*mapping)
|
||||
|
||||
newProxy = newProxyCommand
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnknownBackendAddressType refers to an unknown container or unsupported address type
|
||||
ErrUnknownBackendAddressType = errors.New("unknown container address type not supported")
|
||||
// ErrPortMappedForIP refers to a port already mapped to an ip address
|
||||
ErrPortMappedForIP = errors.New("port is already mapped to ip")
|
||||
// ErrPortNotMapped refers to an unampped port
|
||||
ErrPortNotMapped = errors.New("port is not mapped")
|
||||
)
|
||||
|
||||
// SetIptablesChain sets the specified chain into portmapper
|
||||
func SetIptablesChain(c *iptables.Chain) {
|
||||
chain = c
|
||||
}
|
||||
|
||||
// Map maps the specified container transport address to the host's network address and transport port
|
||||
func Map(container net.Addr, hostIP net.IP, hostPort int) (host net.Addr, err error) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
var (
|
||||
m *mapping
|
||||
proto string
|
||||
allocatedHostPort int
|
||||
proxy userlandProxy
|
||||
)
|
||||
|
||||
switch container.(type) {
|
||||
case *net.TCPAddr:
|
||||
proto = "tcp"
|
||||
if allocatedHostPort, err = portallocator.RequestPort(hostIP, proto, hostPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m = &mapping{
|
||||
proto: proto,
|
||||
host: &net.TCPAddr{IP: hostIP, Port: allocatedHostPort},
|
||||
container: container,
|
||||
}
|
||||
|
||||
proxy = newProxy(proto, hostIP, allocatedHostPort, container.(*net.TCPAddr).IP, container.(*net.TCPAddr).Port)
|
||||
case *net.UDPAddr:
|
||||
proto = "udp"
|
||||
if allocatedHostPort, err = portallocator.RequestPort(hostIP, proto, hostPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m = &mapping{
|
||||
proto: proto,
|
||||
host: &net.UDPAddr{IP: hostIP, Port: allocatedHostPort},
|
||||
container: container,
|
||||
}
|
||||
|
||||
proxy = newProxy(proto, hostIP, allocatedHostPort, container.(*net.UDPAddr).IP, container.(*net.UDPAddr).Port)
|
||||
default:
|
||||
return nil, ErrUnknownBackendAddressType
|
||||
}
|
||||
|
||||
// release the allocated port on any further error during return.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
portallocator.ReleasePort(hostIP, proto, allocatedHostPort)
|
||||
}
|
||||
}()
|
||||
|
||||
key := getKey(m.host)
|
||||
if _, exists := currentMappings[key]; exists {
|
||||
return nil, ErrPortMappedForIP
|
||||
}
|
||||
|
||||
containerIP, containerPort := getIPAndPort(m.container)
|
||||
if err := forward(iptables.Append, m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cleanup := func() error {
|
||||
// need to undo the iptables rules before we return
|
||||
proxy.Stop()
|
||||
forward(iptables.Delete, m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort)
|
||||
if err := portallocator.ReleasePort(hostIP, m.proto, allocatedHostPort); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := proxy.Start(); err != nil {
|
||||
if err := cleanup(); err != nil {
|
||||
return nil, fmt.Errorf("Error during port allocation cleanup: %v", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
m.userlandProxy = proxy
|
||||
currentMappings[key] = m
|
||||
return m.host, nil
|
||||
}
|
||||
|
||||
// Unmap removes stored mapping for the specified host transport address
|
||||
func Unmap(host net.Addr) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
key := getKey(host)
|
||||
data, exists := currentMappings[key]
|
||||
if !exists {
|
||||
return ErrPortNotMapped
|
||||
}
|
||||
|
||||
data.userlandProxy.Stop()
|
||||
|
||||
delete(currentMappings, key)
|
||||
|
||||
containerIP, containerPort := getIPAndPort(data.container)
|
||||
hostIP, hostPort := getIPAndPort(data.host)
|
||||
if err := forward(iptables.Delete, data.proto, hostIP, hostPort, containerIP.String(), containerPort); err != nil {
|
||||
log.Errorf("Error on iptables delete: %s", err)
|
||||
}
|
||||
|
||||
switch a := host.(type) {
|
||||
case *net.TCPAddr:
|
||||
return portallocator.ReleasePort(a.IP, "tcp", a.Port)
|
||||
case *net.UDPAddr:
|
||||
return portallocator.ReleasePort(a.IP, "udp", a.Port)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getKey(a net.Addr) string {
|
||||
switch t := a.(type) {
|
||||
case *net.TCPAddr:
|
||||
return fmt.Sprintf("%s:%d/%s", t.IP.String(), t.Port, "tcp")
|
||||
case *net.UDPAddr:
|
||||
return fmt.Sprintf("%s:%d/%s", t.IP.String(), t.Port, "udp")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getIPAndPort(a net.Addr) (net.IP, int) {
|
||||
switch t := a.(type) {
|
||||
case *net.TCPAddr:
|
||||
return t.IP, t.Port
|
||||
case *net.UDPAddr:
|
||||
return t.IP, t.Port
|
||||
}
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
func forward(action iptables.Action, proto string, sourceIP net.IP, sourcePort int, containerIP string, containerPort int) error {
|
||||
if chain == nil {
|
||||
return nil
|
||||
}
|
||||
return chain.Forward(action, sourceIP, sourcePort, proto, containerIP, containerPort)
|
||||
}
|
||||
152
libnetwork/portmapper/mapper_test.go
Normal file
152
libnetwork/portmapper/mapper_test.go
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package portmapper
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/iptables"
|
||||
"github.com/docker/libnetwork/portallocator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// override this func to mock out the proxy server
|
||||
newProxy = newMockProxyCommand
|
||||
}
|
||||
|
||||
func reset() {
|
||||
chain = nil
|
||||
currentMappings = make(map[string]*mapping)
|
||||
}
|
||||
|
||||
func TestSetIptablesChain(t *testing.T) {
|
||||
defer reset()
|
||||
|
||||
c := &iptables.Chain{
|
||||
Name: "TEST",
|
||||
Bridge: "192.168.1.1",
|
||||
}
|
||||
|
||||
if chain != nil {
|
||||
t.Fatal("chain should be nil at init")
|
||||
}
|
||||
|
||||
SetIptablesChain(c)
|
||||
if chain == nil {
|
||||
t.Fatal("chain should not be nil after set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapPorts(t *testing.T) {
|
||||
dstIP1 := net.ParseIP("192.168.0.1")
|
||||
dstIP2 := net.ParseIP("192.168.0.2")
|
||||
dstAddr1 := &net.TCPAddr{IP: dstIP1, Port: 80}
|
||||
dstAddr2 := &net.TCPAddr{IP: dstIP2, Port: 80}
|
||||
|
||||
srcAddr1 := &net.TCPAddr{Port: 1080, IP: net.ParseIP("172.16.0.1")}
|
||||
srcAddr2 := &net.TCPAddr{Port: 1080, IP: net.ParseIP("172.16.0.2")}
|
||||
|
||||
addrEqual := func(addr1, addr2 net.Addr) bool {
|
||||
return (addr1.Network() == addr2.Network()) && (addr1.String() == addr2.String())
|
||||
}
|
||||
|
||||
if host, err := Map(srcAddr1, dstIP1, 80); err != nil {
|
||||
t.Fatalf("Failed to allocate port: %s", err)
|
||||
} else if !addrEqual(dstAddr1, host) {
|
||||
t.Fatalf("Incorrect mapping result: expected %s:%s, got %s:%s",
|
||||
dstAddr1.String(), dstAddr1.Network(), host.String(), host.Network())
|
||||
}
|
||||
|
||||
if _, err := Map(srcAddr1, dstIP1, 80); err == nil {
|
||||
t.Fatalf("Port is in use - mapping should have failed")
|
||||
}
|
||||
|
||||
if _, err := Map(srcAddr2, dstIP1, 80); err == nil {
|
||||
t.Fatalf("Port is in use - mapping should have failed")
|
||||
}
|
||||
|
||||
if _, err := Map(srcAddr2, dstIP2, 80); err != nil {
|
||||
t.Fatalf("Failed to allocate port: %s", err)
|
||||
}
|
||||
|
||||
if Unmap(dstAddr1) != nil {
|
||||
t.Fatalf("Failed to release port")
|
||||
}
|
||||
|
||||
if Unmap(dstAddr2) != nil {
|
||||
t.Fatalf("Failed to release port")
|
||||
}
|
||||
|
||||
if Unmap(dstAddr2) == nil {
|
||||
t.Fatalf("Port already released, but no error reported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUDPKey(t *testing.T) {
|
||||
addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.5"), Port: 53}
|
||||
|
||||
key := getKey(addr)
|
||||
|
||||
if expected := "192.168.1.5:53/udp"; key != expected {
|
||||
t.Fatalf("expected key %s got %s", expected, key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTCPKey(t *testing.T) {
|
||||
addr := &net.TCPAddr{IP: net.ParseIP("192.168.1.5"), Port: 80}
|
||||
|
||||
key := getKey(addr)
|
||||
|
||||
if expected := "192.168.1.5:80/tcp"; key != expected {
|
||||
t.Fatalf("expected key %s got %s", expected, key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUDPIPAndPort(t *testing.T) {
|
||||
addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.5"), Port: 53}
|
||||
|
||||
ip, port := getIPAndPort(addr)
|
||||
if expected := "192.168.1.5"; ip.String() != expected {
|
||||
t.Fatalf("expected ip %s got %s", expected, ip)
|
||||
}
|
||||
|
||||
if ep := 53; port != ep {
|
||||
t.Fatalf("expected port %d got %d", ep, port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapAllPortsSingleInterface(t *testing.T) {
|
||||
dstIP1 := net.ParseIP("0.0.0.0")
|
||||
srcAddr1 := &net.TCPAddr{Port: 1080, IP: net.ParseIP("172.16.0.1")}
|
||||
|
||||
hosts := []net.Addr{}
|
||||
var host net.Addr
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
for _, val := range hosts {
|
||||
Unmap(val)
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := portallocator.BeginPortRange; i < portallocator.EndPortRange; i++ {
|
||||
if host, err = Map(srcAddr1, dstIP1, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
if _, err := Map(srcAddr1, dstIP1, portallocator.BeginPortRange); err == nil {
|
||||
t.Fatalf("Port %d should be bound but is not", portallocator.BeginPortRange)
|
||||
}
|
||||
|
||||
for _, val := range hosts {
|
||||
if err := Unmap(val); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
hosts = []net.Addr{}
|
||||
}
|
||||
}
|
||||
18
libnetwork/portmapper/mock_proxy.go
Normal file
18
libnetwork/portmapper/mock_proxy.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package portmapper
|
||||
|
||||
import "net"
|
||||
|
||||
func newMockProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.IP, containerPort int) userlandProxy {
|
||||
return &mockProxyCommand{}
|
||||
}
|
||||
|
||||
type mockProxyCommand struct {
|
||||
}
|
||||
|
||||
func (p *mockProxyCommand) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *mockProxyCommand) Stop() error {
|
||||
return nil
|
||||
}
|
||||
161
libnetwork/portmapper/proxy.go
Normal file
161
libnetwork/portmapper/proxy.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package portmapper
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/pkg/proxy"
|
||||
"github.com/docker/docker/pkg/reexec"
|
||||
)
|
||||
|
||||
const userlandProxyCommandName = "docker-proxy"
|
||||
|
||||
func init() {
|
||||
reexec.Register(userlandProxyCommandName, execProxy)
|
||||
}
|
||||
|
||||
type userlandProxy interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// proxyCommand wraps an exec.Cmd to run the userland TCP and UDP
|
||||
// proxies as separate processes.
|
||||
type proxyCommand struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// execProxy is the reexec function that is registered to start the userland proxies
|
||||
func execProxy() {
|
||||
f := os.NewFile(3, "signal-parent")
|
||||
host, container := parseHostContainerAddrs()
|
||||
|
||||
p, err := proxy.NewProxy(host, container)
|
||||
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()
|
||||
}
|
||||
|
||||
// parseHostContainerAddrs parses the flags passed on reexec to create the TCP or UDP
|
||||
// net.Addrs to map the host and container ports
|
||||
func parseHostContainerAddrs() (host net.Addr, container net.Addr) {
|
||||
var (
|
||||
proto = flag.String("proto", "tcp", "proxy protocol")
|
||||
hostIP = flag.String("host-ip", "", "host ip")
|
||||
hostPort = flag.Int("host-port", -1, "host port")
|
||||
containerIP = flag.String("container-ip", "", "container ip")
|
||||
containerPort = flag.Int("container-port", -1, "container port")
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
switch *proto {
|
||||
case "tcp":
|
||||
host = &net.TCPAddr{IP: net.ParseIP(*hostIP), Port: *hostPort}
|
||||
container = &net.TCPAddr{IP: net.ParseIP(*containerIP), Port: *containerPort}
|
||||
case "udp":
|
||||
host = &net.UDPAddr{IP: net.ParseIP(*hostIP), Port: *hostPort}
|
||||
container = &net.UDPAddr{IP: net.ParseIP(*containerIP), Port: *containerPort}
|
||||
default:
|
||||
log.Fatalf("unsupported protocol %s", *proto)
|
||||
}
|
||||
|
||||
return host, container
|
||||
}
|
||||
|
||||
func handleStopSignals(p proxy.Proxy) {
|
||||
s := make(chan os.Signal, 10)
|
||||
signal.Notify(s, os.Interrupt, syscall.SIGTERM, syscall.SIGSTOP)
|
||||
|
||||
for _ = range s {
|
||||
p.Close()
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func newProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.IP, containerPort int) userlandProxy {
|
||||
args := []string{
|
||||
userlandProxyCommandName,
|
||||
"-proto", proto,
|
||||
"-host-ip", hostIP.String(),
|
||||
"-host-port", strconv.Itoa(hostPort),
|
||||
"-container-ip", containerIP.String(),
|
||||
"-container-port", strconv.Itoa(containerPort),
|
||||
}
|
||||
|
||||
return &proxyCommand{
|
||||
cmd: &exec.Cmd{
|
||||
Path: reexec.Self(),
|
||||
Args: args,
|
||||
SysProcAttr: &syscall.SysProcAttr{
|
||||
Pdeathsig: syscall.SIGTERM, // send a sigterm to the proxy if the daemon process dies
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyCommand) Start() error {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("proxy unable to open os.Pipe %s", err)
|
||||
}
|
||||
defer r.Close()
|
||||
p.cmd.ExtraFiles = []*os.File{w}
|
||||
if err := p.cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Close()
|
||||
|
||||
errchan := make(chan error, 1)
|
||||
go func() {
|
||||
buf := make([]byte, 2)
|
||||
r.Read(buf)
|
||||
|
||||
if string(buf) != "0\n" {
|
||||
errStr, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
errchan <- fmt.Errorf("Error reading exit status from userland proxy: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
errchan <- fmt.Errorf("Error starting userland proxy: %s", errStr)
|
||||
return
|
||||
}
|
||||
errchan <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errchan:
|
||||
return err
|
||||
case <-time.After(16 * time.Second):
|
||||
return fmt.Errorf("Timed out proxy starting the userland proxy")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyCommand) Stop() error {
|
||||
if p.cmd.Process != nil {
|
||||
if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.cmd.Wait()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue