Browse Source

Merge pull request #6101 from LK4D4/ip_range_#4986

Implement allocating IPs from CIDR within bridge network
unclejack 10 years ago
parent
commit
9fb34ae571

+ 2 - 0
daemon/config.go

@@ -30,6 +30,7 @@ type Config struct {
 	DefaultIp                   net.IP
 	BridgeIface                 string
 	BridgeIP                    string
+	FixedCIDR                   string
 	InterContainerCommunication bool
 	GraphDriver                 string
 	GraphOptions                []string
@@ -53,6 +54,7 @@ func (config *Config) InstallFlags() {
 	flag.BoolVar(&config.EnableIpMasq, []string{"-ip-masq"}, true, "Enable IP masquerading for bridge's IP range")
 	flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b")
 	flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking")
+	flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)")
 	flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Enable inter-container communication")
 	flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver")
 	flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver")

+ 1 - 0
daemon/daemon.go

@@ -821,6 +821,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
 		job.SetenvBool("EnableIpMasq", config.EnableIpMasq)
 		job.Setenv("BridgeIface", config.BridgeIface)
 		job.Setenv("BridgeIP", config.BridgeIP)
+		job.Setenv("FixedCIDR", config.FixedCIDR)
 		job.Setenv("DefaultBindingIP", config.DefaultIp.String())
 
 		if err := job.Run(); err != nil {

+ 15 - 4
daemon/networkdriver/bridge/driver.go

@@ -84,6 +84,7 @@ func InitDriver(job *engine.Job) engine.Status {
 		ipMasq         = job.GetenvBool("EnableIpMasq")
 		ipForward      = job.GetenvBool("EnableIpForward")
 		bridgeIP       = job.Getenv("BridgeIP")
+		fixedCIDR      = job.Getenv("FixedCIDR")
 	)
 
 	if defaultIP := job.Getenv("DefaultBindingIP"); defaultIP != "" {
@@ -155,6 +156,16 @@ func InitDriver(job *engine.Job) engine.Status {
 	}
 
 	bridgeNetwork = network
+	if fixedCIDR != "" {
+		_, subnet, err := net.ParseCIDR(fixedCIDR)
+		if err != nil {
+			return job.Error(err)
+		}
+		log.Debugf("Subnet: %v", subnet)
+		if err := ipallocator.RegisterSubnet(bridgeNetwork, subnet); err != nil {
+			return job.Error(err)
+		}
+	}
 
 	// https://github.com/docker/docker/issues/2768
 	job.Eng.Hack_SetGlobalVar("httpapi.bridgeIP", bridgeNetwork.IP)
@@ -318,14 +329,14 @@ func createBridgeIface(name string) error {
 // Allocate a network interface
 func Allocate(job *engine.Job) engine.Status {
 	var (
-		ip          *net.IP
+		ip          net.IP
 		err         error
 		id          = job.Args[0]
 		requestedIP = net.ParseIP(job.Getenv("RequestedIP"))
 	)
 
 	if requestedIP != nil {
-		ip, err = ipallocator.RequestIP(bridgeNetwork, &requestedIP)
+		ip, err = ipallocator.RequestIP(bridgeNetwork, requestedIP)
 	} else {
 		ip, err = ipallocator.RequestIP(bridgeNetwork, nil)
 	}
@@ -343,7 +354,7 @@ func Allocate(job *engine.Job) engine.Status {
 	out.SetInt("IPPrefixLen", size)
 
 	currentInterfaces.Set(id, &networkInterface{
-		IP: *ip,
+		IP: ip,
 	})
 
 	out.WriteTo(job.Stdout)
@@ -368,7 +379,7 @@ func Release(job *engine.Job) engine.Status {
 		}
 	}
 
-	if err := ipallocator.ReleaseIP(bridgeNetwork, &containerInterface.IP); err != nil {
+	if err := ipallocator.ReleaseIP(bridgeNetwork, containerInterface.IP); err != nil {
 		log.Infof("Unable to release ip %s", err)
 	}
 	return engine.StatusOK

+ 61 - 49
daemon/networkdriver/ipallocator/allocator.go

@@ -3,26 +3,39 @@ package ipallocator
 import (
 	"encoding/binary"
 	"errors"
-	"github.com/docker/docker/daemon/networkdriver"
 	"net"
 	"sync"
+
+	"github.com/docker/docker/daemon/networkdriver"
 )
 
 // allocatedMap is thread-unsafe set of allocated IP
 type allocatedMap struct {
-	p    map[int32]struct{}
-	last int32
+	p     map[uint32]struct{}
+	last  uint32
+	begin uint32
+	end   uint32
 }
 
-func newAllocatedMap() *allocatedMap {
-	return &allocatedMap{p: make(map[int32]struct{})}
+func newAllocatedMap(network *net.IPNet) *allocatedMap {
+	firstIP, lastIP := networkdriver.NetworkRange(network)
+	begin := ipToInt(firstIP) + 2
+	end := ipToInt(lastIP) - 1
+	return &allocatedMap{
+		p:     make(map[uint32]struct{}),
+		begin: begin,
+		end:   end,
+		last:  begin - 1, // so first allocated will be begin
+	}
 }
 
 type networkSet map[string]*allocatedMap
 
 var (
-	ErrNoAvailableIPs     = errors.New("no available ip addresses on network")
-	ErrIPAlreadyAllocated = errors.New("ip already allocated")
+	ErrNoAvailableIPs           = errors.New("no available ip addresses on network")
+	ErrIPAlreadyAllocated       = errors.New("ip already allocated")
+	ErrNetworkAlreadyRegistered = errors.New("network already registered")
+	ErrBadSubnet                = errors.New("network not contains specified subnet")
 )
 
 var (
@@ -30,47 +43,63 @@ var (
 	allocatedIPs = networkSet{}
 )
 
+// RegisterSubnet registers network in global allocator with bounds
+// defined by subnet. If you want to use network range you must call
+// this method before first RequestIP, otherwise full network range will be used
+func RegisterSubnet(network *net.IPNet, subnet *net.IPNet) error {
+	lock.Lock()
+	defer lock.Unlock()
+	key := network.String()
+	if _, ok := allocatedIPs[key]; ok {
+		return ErrNetworkAlreadyRegistered
+	}
+	n := newAllocatedMap(network)
+	beginIP, endIP := networkdriver.NetworkRange(subnet)
+	begin, end := ipToInt(beginIP)+1, ipToInt(endIP)-1
+	if !(begin >= n.begin && end <= n.end && begin < end) {
+		return ErrBadSubnet
+	}
+	n.begin = begin
+	n.end = end
+	n.last = begin - 1
+	allocatedIPs[key] = n
+	return nil
+}
+
 // RequestIP requests an available ip from the given network.  It
 // will return the next available ip if the ip provided is nil.  If the
 // ip provided is not nil it will validate that the provided ip is available
 // for use or return an error
-func RequestIP(network *net.IPNet, ip *net.IP) (*net.IP, error) {
+func RequestIP(network *net.IPNet, ip net.IP) (net.IP, error) {
 	lock.Lock()
 	defer lock.Unlock()
 	key := network.String()
 	allocated, ok := allocatedIPs[key]
 	if !ok {
-		allocated = newAllocatedMap()
+		allocated = newAllocatedMap(network)
 		allocatedIPs[key] = allocated
 	}
 
 	if ip == nil {
-		return allocated.getNextIP(network)
+		return allocated.getNextIP()
 	}
-	return allocated.checkIP(network, ip)
+	return allocated.checkIP(ip)
 }
 
 // ReleaseIP adds the provided ip back into the pool of
 // available ips to be returned for use.
-func ReleaseIP(network *net.IPNet, ip *net.IP) error {
+func ReleaseIP(network *net.IPNet, ip net.IP) error {
 	lock.Lock()
 	defer lock.Unlock()
 	if allocated, exists := allocatedIPs[network.String()]; exists {
-		pos := getPosition(network, ip)
+		pos := ipToInt(ip)
 		delete(allocated.p, pos)
 	}
 	return nil
 }
 
-// convert the ip into the position in the subnet.  Only
-// position are saved in the set
-func getPosition(network *net.IPNet, ip *net.IP) int32 {
-	first, _ := networkdriver.NetworkRange(network)
-	return ipToInt(ip) - ipToInt(&first)
-}
-
-func (allocated *allocatedMap) checkIP(network *net.IPNet, ip *net.IP) (*net.IP, error) {
-	pos := getPosition(network, ip)
+func (allocated *allocatedMap) checkIP(ip net.IP) (net.IP, error) {
+	pos := ipToInt(ip)
 	if _, ok := allocated.p[pos]; ok {
 		return nil, ErrIPAlreadyAllocated
 	}
@@ -81,47 +110,30 @@ func (allocated *allocatedMap) checkIP(network *net.IPNet, ip *net.IP) (*net.IP,
 
 // return an available ip if one is currently available.  If not,
 // return the next available ip for the nextwork
-func (allocated *allocatedMap) getNextIP(network *net.IPNet) (*net.IP, error) {
-	var (
-		ownIP    = ipToInt(&network.IP)
-		first, _ = networkdriver.NetworkRange(network)
-		base     = ipToInt(&first)
-		size     = int(networkdriver.NetworkSize(network.Mask))
-		max      = int32(size - 2) // size -1 for the broadcast network, -1 for the gateway network
-		pos      = allocated.last
-	)
-
-	var (
-		firstNetIP = network.IP.To4().Mask(network.Mask)
-		firstAsInt = ipToInt(&firstNetIP) + 1
-	)
-
-	for i := int32(0); i < max; i++ {
-		pos = pos%max + 1
-		next := int32(base + pos)
-
-		if next == ownIP || next == firstAsInt {
-			continue
+func (allocated *allocatedMap) getNextIP() (net.IP, error) {
+	for pos := allocated.last + 1; pos != allocated.last; pos++ {
+		if pos > allocated.end {
+			pos = allocated.begin
 		}
 		if _, ok := allocated.p[pos]; ok {
 			continue
 		}
 		allocated.p[pos] = struct{}{}
 		allocated.last = pos
-		return intToIP(next), nil
+		return intToIP(pos), nil
 	}
 	return nil, ErrNoAvailableIPs
 }
 
 // Converts a 4 bytes IP into a 32 bit integer
-func ipToInt(ip *net.IP) int32 {
-	return int32(binary.BigEndian.Uint32(ip.To4()))
+func ipToInt(ip net.IP) uint32 {
+	return binary.BigEndian.Uint32(ip.To4())
 }
 
 // Converts 32 bit integer into a 4 bytes IP address
-func intToIP(n int32) *net.IP {
+func intToIP(n uint32) net.IP {
 	b := make([]byte, 4)
-	binary.BigEndian.PutUint32(b, uint32(n))
+	binary.BigEndian.PutUint32(b, n)
 	ip := net.IP(b)
-	return &ip
+	return ip
 }

+ 100 - 20
daemon/networkdriver/ipallocator/allocator_test.go

@@ -17,7 +17,7 @@ func TestRequestNewIps(t *testing.T) {
 		Mask: []byte{255, 255, 255, 0},
 	}
 
-	var ip *net.IP
+	var ip net.IP
 	var err error
 	for i := 2; i < 10; i++ {
 		ip, err = RequestIP(network, nil)
@@ -106,19 +106,19 @@ func TestRequesetSpecificIp(t *testing.T) {
 
 	ip := net.ParseIP("192.168.1.5")
 
-	if _, err := RequestIP(network, &ip); err != nil {
+	if _, err := RequestIP(network, ip); err != nil {
 		t.Fatal(err)
 	}
 }
 
 func TestConversion(t *testing.T) {
 	ip := net.ParseIP("127.0.0.1")
-	i := ipToInt(&ip)
+	i := ipToInt(ip)
 	if i == 0 {
 		t.Fatal("converted to zero")
 	}
 	conv := intToIP(i)
-	if !ip.Equal(*conv) {
+	if !ip.Equal(conv) {
 		t.Error(conv.String())
 	}
 }
@@ -146,7 +146,7 @@ func TestIPAllocator(t *testing.T) {
 			t.Fatal(err)
 		}
 
-		assertIPEquals(t, &expectedIPs[i], ip)
+		assertIPEquals(t, expectedIPs[i], ip)
 	}
 	// Before loop begin
 	// 2(f) - 3(f) - 4(f) - 5(f) - 6(f)
@@ -179,19 +179,19 @@ func TestIPAllocator(t *testing.T) {
 	}
 
 	// Release some IPs in non-sequential order
-	if err := ReleaseIP(network, &expectedIPs[3]); err != nil {
+	if err := ReleaseIP(network, expectedIPs[3]); err != nil {
 		t.Fatal(err)
 	}
 	// 2(u) - 3(u) - 4(u) - 5(f) - 6(u)
 	//                       ↑
 
-	if err := ReleaseIP(network, &expectedIPs[2]); err != nil {
+	if err := ReleaseIP(network, expectedIPs[2]); err != nil {
 		t.Fatal(err)
 	}
 	// 2(u) - 3(u) - 4(f) - 5(f) - 6(u)
 	//                       ↑
 
-	if err := ReleaseIP(network, &expectedIPs[4]); err != nil {
+	if err := ReleaseIP(network, expectedIPs[4]); err != nil {
 		t.Fatal(err)
 	}
 	// 2(u) - 3(u) - 4(f) - 5(f) - 6(f)
@@ -199,7 +199,7 @@ func TestIPAllocator(t *testing.T) {
 
 	// Make sure that IPs are reused in sequential order, starting
 	// with the first released IP
-	newIPs := make([]*net.IP, 3)
+	newIPs := make([]net.IP, 3)
 	for i := 0; i < 3; i++ {
 		ip, err := RequestIP(network, nil)
 		if err != nil {
@@ -208,9 +208,9 @@ func TestIPAllocator(t *testing.T) {
 
 		newIPs[i] = ip
 	}
-	assertIPEquals(t, &expectedIPs[2], newIPs[0])
-	assertIPEquals(t, &expectedIPs[3], newIPs[1])
-	assertIPEquals(t, &expectedIPs[4], newIPs[2])
+	assertIPEquals(t, expectedIPs[2], newIPs[0])
+	assertIPEquals(t, expectedIPs[3], newIPs[1])
+	assertIPEquals(t, expectedIPs[4], newIPs[2])
 
 	_, err = RequestIP(network, nil)
 	if err == nil {
@@ -226,7 +226,7 @@ func TestAllocateFirstIP(t *testing.T) {
 	}
 
 	firstIP := network.IP.To4().Mask(network.Mask)
-	first := ipToInt(&firstIP) + 1
+	first := ipToInt(firstIP) + 1
 
 	ip, err := RequestIP(network, nil)
 	if err != nil {
@@ -247,7 +247,7 @@ func TestAllocateAllIps(t *testing.T) {
 	}
 
 	var (
-		current, first *net.IP
+		current, first net.IP
 		err            error
 		isFirst        = true
 	)
@@ -313,14 +313,94 @@ func TestAllocateDifferentSubnets(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	assertIPEquals(t, &expectedIPs[0], ip11)
-	assertIPEquals(t, &expectedIPs[1], ip12)
-	assertIPEquals(t, &expectedIPs[2], ip21)
-	assertIPEquals(t, &expectedIPs[3], ip22)
+	assertIPEquals(t, expectedIPs[0], ip11)
+	assertIPEquals(t, expectedIPs[1], ip12)
+	assertIPEquals(t, expectedIPs[2], ip21)
+	assertIPEquals(t, expectedIPs[3], ip22)
+}
+func TestRegisterBadTwice(t *testing.T) {
+	defer reset()
+	network := &net.IPNet{
+		IP:   []byte{192, 168, 1, 1},
+		Mask: []byte{255, 255, 255, 0},
+	}
+	subnet := &net.IPNet{
+		IP:   []byte{192, 168, 1, 8},
+		Mask: []byte{255, 255, 255, 248},
+	}
+
+	if err := RegisterSubnet(network, subnet); err != nil {
+		t.Fatal(err)
+	}
+	subnet = &net.IPNet{
+		IP:   []byte{192, 168, 1, 16},
+		Mask: []byte{255, 255, 255, 248},
+	}
+	if err := RegisterSubnet(network, subnet); err != ErrNetworkAlreadyRegistered {
+		t.Fatalf("Expecteded ErrNetworkAlreadyRegistered error, got %v", err)
+	}
+}
+
+func TestRegisterBadRange(t *testing.T) {
+	defer reset()
+	network := &net.IPNet{
+		IP:   []byte{192, 168, 1, 1},
+		Mask: []byte{255, 255, 255, 0},
+	}
+	subnet := &net.IPNet{
+		IP:   []byte{192, 168, 1, 1},
+		Mask: []byte{255, 255, 0, 0},
+	}
+	if err := RegisterSubnet(network, subnet); err != ErrBadSubnet {
+		t.Fatalf("Expected ErrBadSubnet error, got %v", err)
+	}
+}
+
+func TestAllocateFromRange(t *testing.T) {
+	defer reset()
+	network := &net.IPNet{
+		IP:   []byte{192, 168, 0, 1},
+		Mask: []byte{255, 255, 255, 0},
+	}
+	// 192.168.1.9 - 192.168.1.14
+	subnet := &net.IPNet{
+		IP:   []byte{192, 168, 0, 8},
+		Mask: []byte{255, 255, 255, 248},
+	}
+	if err := RegisterSubnet(network, subnet); err != nil {
+		t.Fatal(err)
+	}
+	expectedIPs := []net.IP{
+		0: net.IPv4(192, 168, 0, 9),
+		1: net.IPv4(192, 168, 0, 10),
+		2: net.IPv4(192, 168, 0, 11),
+		3: net.IPv4(192, 168, 0, 12),
+		4: net.IPv4(192, 168, 0, 13),
+		5: net.IPv4(192, 168, 0, 14),
+	}
+	for _, ip := range expectedIPs {
+		rip, err := RequestIP(network, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		assertIPEquals(t, ip, rip)
+	}
+
+	if _, err := RequestIP(network, nil); err != ErrNoAvailableIPs {
+		t.Fatalf("Expected ErrNoAvailableIPs error, got %v", err)
+	}
+	for _, ip := range expectedIPs {
+		ReleaseIP(network, ip)
+		rip, err := RequestIP(network, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		assertIPEquals(t, ip, rip)
+	}
 }
 
-func assertIPEquals(t *testing.T, ip1, ip2 *net.IP) {
-	if !ip1.Equal(*ip2) {
+func assertIPEquals(t *testing.T, ip1, ip2 net.IP) {
+	if !ip1.Equal(ip2) {
 		t.Fatalf("Expected IP %s, got %s", ip1, ip2)
 	}
 }

+ 4 - 0
docs/man/docker.1.md

@@ -49,6 +49,10 @@ unix://[/path/to/socket] to use.
 **-g**=""
   Path to use as the root of the Docker runtime. Default is `/var/lib/docker`.
 
+
+**--fixed-cidr**=""
+  IPv4 subnet for fixed IPs (ex: 10.20.0.0/16); this subnet must be nested in the bridge subnet (which is defined by \-b or \-\-bip)
+
 **--icc**=*true*|*false*
   Enable inter\-container communication. Default is true.
 

+ 17 - 6
docs/sources/articles/networking.md

@@ -54,6 +54,9 @@ server when it starts up, and cannot be changed once it is running:
  *  `--bip=CIDR` — see
     [Customizing docker0](#docker0)
 
+ *  `--fixed-cidr` — see
+    [Customizing docker0](#docker0)
+
  *  `-H SOCKET...` or `--host=SOCKET...` —
     This might sound like it would affect container networking,
     but it actually faces in the other direction:
@@ -365,17 +368,25 @@ By default, the Docker server creates and configures the host system's
 can pass packets back and forth between other physical or virtual
 network interfaces so that they behave as a single Ethernet network.
 
-Docker configures `docker0` with an IP address and netmask so the host
-machine can both receive and send packets to containers connected to the
-bridge, and gives it an MTU — the *maximum transmission unit* or largest
-packet length that the interface will allow — of either 1,500 bytes or
-else a more specific value copied from the Docker host's interface that
-supports its default route.  Both are configurable at server startup:
+Docker configures `docker0` with an IP address, netmask and IP
+allocation range. The host machine can both receive and send packets to
+containers connected to the bridge, and gives it an MTU — the *maximum
+transmission unit* or largest packet length that the interface will
+allow — of either 1,500 bytes or else a more specific value copied from
+the Docker host's interface that supports its default route.  These
+options are configurable at server startup:
 
  *  `--bip=CIDR` — supply a specific IP address and netmask for the
     `docker0` bridge, using standard CIDR notation like
     `192.168.1.5/24`.
 
+ *  `--fixed-cidr=CIDR` — restrict the IP range from the `docker0` subnet,
+    using the standard CIDR notation like `172.167.1.0/28`. This range must
+    be and IPv4 range for fixed IPs (ex: 10.20.0.0/16) and must be a subset
+    of the bridge IP range (`docker0` or set using `--bridge`). For example
+    with `--fixed-cidr=192.168.1.0/25`, IPs for your containers will be chosen
+    from the first half of `192.168.1.0/24` subnet.
+
  *  `--mtu=BYTES` — override the maximum packet length on `docker0`.
 
 On Ubuntu you would add these to the `DOCKER_OPTS` setting in

+ 2 - 0
docs/sources/reference/commandline/cli.md

@@ -54,6 +54,8 @@ expect an integer, and they can only be specified once.
       -b, --bridge=""                            Attach containers to a pre-existing network bridge
                                                    use 'none' to disable container networking
       --bip=""                                   Use this CIDR notation address for the network bridge's IP, not compatible with -b
+      --fixed-cidr=""                            IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)
+                                                   this subnet must be nested in the bridge subnet (which is defined by -b or --bip)
       -D, --debug=false                          Enable debug mode
       -d, --daemon=false                         Enable daemon mode
       --dns=[]                                   Force Docker to use specific DNS servers