diff --git a/.dockerignore b/.dockerignore index 87553b2..b660be0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,3 @@ +/bin/ /plugin/ +/multiarch/ diff --git a/README.md b/README.md index 05730fd..4d46d62 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ $ # With IPv6 enabled # Although `docker network create` has a `--ipv6` flag, it doesn't work with the null IPAM driver -$ docker network create -d ghcr.io/devplayer0/docker-net-dhcp:release-linux-amd64 --ipam-driver null -o bridge=test -o ipv6=true my-dhcp-net +$ docker network create -d ghcr.io/devplayer0/docker-net-dhcp:release-linux-amd64 --ipam-driver null -o bridge=my-bridge -o ipv6=true my-dhcp-net $ ``` @@ -148,9 +148,11 @@ services: - dhcp networks: dhcp: - driver: ghcr.io/devplayer0/docker-net-dhcp:golang + driver: ghcr.io/devplayer0/docker-net-dhcp:release-linux-amd64 driver_opts: bridge: my-bridge + ipv6: 'true' + ignore_conflicts: 'false' ipam: driver: 'null' ``` @@ -164,11 +166,10 @@ Note: - Add `--hostname my-host` to have the DHCP transmit this name as the host for the container. This is useful if your DHCP server is configured to update DNS records from DHCP leases. - If the `docker run` command times out waiting for a lease, you can try increasing the initial timeout value by - passing `-o lease_timeout=60s` (e.g. to increase to 60 seconds) - -## Docker Compose - -Sample Docker Compose file: + passing `-o lease_timeout=60s` when creating the network (e.g. to increase to 60 seconds) + - By default, a bridge can only be used for a single DHCP network. There is additionally a check to see if a bridge is + is used by any other Docker networks. To disable this check (it's also possible this check might mistakenly detect a + conflict), pass `-o ignore_conflicts=true` when creating the network. ## Debugging diff --git a/go.mod b/go.mod index b057fac..f498443 100644 --- a/go.mod +++ b/go.mod @@ -10,14 +10,14 @@ require ( github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 // indirect github.com/mitchellh/mapstructure v1.4.1 - github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 // indirect github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f - golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect - golang.org/x/sys v0.0.0-20210603125802-9665404d3644 - golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect + golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 + golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect google.golang.org/grpc v1.38.0 // indirect ) diff --git a/go.sum b/go.sum index baf5817..9a94e7b 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= @@ -431,6 +433,8 @@ github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGq github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -697,6 +701,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -777,6 +783,8 @@ golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b h1:qh4f65QIVFjq9eBURLEYWqaEX golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -793,6 +801,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs= +golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/plugin/network.go b/pkg/plugin/network.go index 0d7dac2..d1aec40 100644 --- a/pkg/plugin/network.go +++ b/pkg/plugin/network.go @@ -1,6 +1,7 @@ package plugin import ( + "bytes" "context" "fmt" "net" @@ -41,53 +42,65 @@ func (p *Plugin) CreateNetwork(r CreateNetworkRequest) error { } } - links, err := netlink.LinkList() + link, err := netlink.LinkByName(opts.Bridge) if err != nil { - return fmt.Errorf("failed to retrieve list of network interfaces: %w", err) + return fmt.Errorf("failed to lookup interface %v: %w", opts.Bridge, err) } - - nets, err := p.docker.NetworkList(context.Background(), dTypes.NetworkListOptions{}) - if err != nil { - return fmt.Errorf("failed to retrieve list of networks from Docker: %w", err) + if link.Type() != "bridge" { + return util.ErrNotBridge } - found := false - for _, l := range links { - attrs := l.Attrs() - if l.Type() != "bridge" || attrs.Name != opts.Bridge { - continue + if !opts.IgnoreConflicts { + v4Addrs, err := netlink.AddrList(link, unix.AF_INET) + if err != nil { + return fmt.Errorf("failed to retrieve IPv4 addresses for %v: %w", opts.Bridge, err) } - - v4Addrs, err := netlink.AddrList(l, unix.AF_INET) + v6Addrs, err := netlink.AddrList(link, unix.AF_INET6) if err != nil { - return fmt.Errorf("failed to retrieve IPv4 addresses for %v: %w", attrs.Name, err) + return fmt.Errorf("failed to retrieve IPv6 addresses for %v: %w", opts.Bridge, err) } - v6Addrs, err := netlink.AddrList(l, unix.AF_INET6) + bridgeAddrs := append(v4Addrs, v6Addrs...) + + nets, err := p.docker.NetworkList(context.Background(), dTypes.NetworkListOptions{}) if err != nil { - return fmt.Errorf("failed to retrieve IPv6 addresses for %v: %w", attrs.Name, err) + return fmt.Errorf("failed to retrieve list of networks from Docker: %w", err) } - addrs := append(v4Addrs, v6Addrs...) // Make sure the addresses on this bridge aren't used by another network for _, n := range nets { + if IsDHCPPlugin(n.Driver) { + otherOpts, err := decodeOpts(n.Options) + if err != nil { + log. + WithField("network", n.Name). + WithError(err). + Warn("Failed to parse other DHCP network's options") + } else if otherOpts.Bridge == opts.Bridge { + return util.ErrBridgeUsed + } + } + if n.IPAM.Driver == "null" { + // Null driver networks will have 0.0.0.0/0 which covers any address range! + continue + } + for _, c := range n.IPAM.Config { - _, cidr, err := net.ParseCIDR(c.Subnet) + _, dockerCIDR, err := net.ParseCIDR(c.Subnet) if err != nil { return fmt.Errorf("failed to parse subnet %v on Docker network %v: %w", c.Subnet, n.ID, err) } + if bytes.Equal(dockerCIDR.Mask, net.CIDRMask(0, 32)) || bytes.Equal(dockerCIDR.Mask, net.CIDRMask(0, 128)) { + // Last check to make sure the network isn't 0.0.0.0/0 or ::/0 (which would always pass the check below) + continue + } - for _, linkAddr := range addrs { - if linkAddr.IPNet.Contains(cidr.IP) || cidr.Contains(linkAddr.IP) { + for _, bridgeAddr := range bridgeAddrs { + if bridgeAddr.IPNet.Contains(dockerCIDR.IP) || dockerCIDR.Contains(bridgeAddr.IP) { return util.ErrBridgeUsed } } } } - found = true - break - } - if !found { - return util.ErrBridgeNotFound } log.WithFields(log.Fields{ diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 3e17c8c..3d964c9 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "net/http" + "regexp" "time" docker "github.com/docker/docker/client" @@ -19,11 +20,19 @@ const DriverName string = "net-dhcp" const defaultLeaseTimeout = 10 * time.Second +var driverRegexp = regexp.MustCompile(`^ghcr\.io/devplayer0/docker-net-dhcp:.+$`) + +// IsDHCPPlugin checks if a Docker network driver is an instance of this plugin +func IsDHCPPlugin(driver string) bool { + return driverRegexp.MatchString(driver) +} + // DHCPNetworkOptions contains options for the DHCP network driver type DHCPNetworkOptions struct { - Bridge string - IPv6 bool - LeaseTimeout time.Duration `mapstructure:"lease_timeout"` + Bridge string + IPv6 bool + LeaseTimeout time.Duration `mapstructure:"lease_timeout"` + IgnoreConflicts bool `mapstructure:"ignore_conflicts"` } func decodeOpts(input interface{}) (DHCPNetworkOptions, error) { diff --git a/pkg/util/errors.go b/pkg/util/errors.go index 43df8c3..e6069a5 100644 --- a/pkg/util/errors.go +++ b/pkg/util/errors.go @@ -10,8 +10,8 @@ var ( ErrIPAM = errors.New("only the null IPAM driver is supported") // ErrBridgeRequired indicates a network bridge was not provided for network creation ErrBridgeRequired = errors.New("bridge required") - // ErrBridgeNotFound indicates that a bridge could not be found - ErrBridgeNotFound = errors.New("bridge not found") + // ErrNotBridge indicates that the provided network interface is not a bridge + ErrNotBridge = errors.New("network interface is not a bridge") // ErrBridgeUsed indicates that a bridge is already in use ErrBridgeUsed = errors.New("bridge already in use by Docker") // ErrMACAddress indicates an invalid MAC address @@ -30,7 +30,7 @@ var ( func ErrToStatus(err error) int { switch { - case errors.Is(err, ErrIPAM), errors.Is(err, ErrBridgeRequired), errors.Is(err, ErrBridgeNotFound), + case errors.Is(err, ErrIPAM), errors.Is(err, ErrBridgeRequired), errors.Is(err, ErrNotBridge), errors.Is(err, ErrBridgeUsed), errors.Is(err, ErrMACAddress): return http.StatusBadRequest default: