diff --git a/bridge/resolvconf.go b/bridge/resolvconf.go new file mode 100644 index 0000000..8861184 --- /dev/null +++ b/bridge/resolvconf.go @@ -0,0 +1,67 @@ +package bridge + +import ( + "bytes" + "io/ioutil" + "regexp" +) + +const ( + ipv4NumBlock = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)` + ipv4Address = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock + + // This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also + // will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants + // -- e.g. other link-local types -- either won't work in containers or are unnecessary. + // For readability and sufficiency for Docker purposes this seemed more reasonable than a + // 1000+ character regexp with exact and complete IPv6 validation + ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})` +) + +var nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`) + +func readResolvConf() ([]byte, error) { + resolv, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + return nil, err + } + return resolv, nil +} + +// getLines parses input into lines and strips away comments. +func getLines(input []byte, commentMarker []byte) [][]byte { + lines := bytes.Split(input, []byte("\n")) + var output [][]byte + for _, currentLine := range lines { + var commentIndex = bytes.Index(currentLine, commentMarker) + if commentIndex == -1 { + output = append(output, currentLine) + } else { + output = append(output, currentLine[:commentIndex]) + } + } + return output +} + +// GetNameserversAsCIDR returns nameservers (if any) listed in +// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32") +// This function's output is intended for net.ParseCIDR +func getNameserversAsCIDR(resolvConf []byte) []string { + nameservers := []string{} + for _, nameserver := range getNameservers(resolvConf) { + nameservers = append(nameservers, nameserver+"/32") + } + return nameservers +} + +// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf +func getNameservers(resolvConf []byte) []string { + nameservers := []string{} + for _, line := range getLines(resolvConf, []byte("#")) { + var ns = nsRegexp.FindSubmatch(line) + if len(ns) > 0 { + nameservers = append(nameservers, string(ns[1])) + } + } + return nameservers +} diff --git a/bridge/setup_device.go b/bridge/setup_device.go index d2e285b..e4938b8 100644 --- a/bridge/setup_device.go +++ b/bridge/setup_device.go @@ -39,7 +39,17 @@ func SetupDevice(i *Interface) error { // SetupDeviceUp ups the given bridge interface. func SetupDeviceUp(i *Interface) error { - return netlink.LinkSetUp(i.Link) + err := netlink.LinkSetUp(i.Link) + if err != nil { + return err + } + + // Attempt to update the bridge interface to refresh the flags status, + // ignoring any failure to do so. + if lnk, err := netlink.LinkByName(i.Config.BridgeName); err == nil { + i.Link = lnk + } + return nil } func generateRandomMAC() net.HardwareAddr { diff --git a/bridge/setup_device_test.go b/bridge/setup_device_test.go new file mode 100644 index 0000000..f9a0c3d --- /dev/null +++ b/bridge/setup_device_test.go @@ -0,0 +1,77 @@ +package bridge + +import ( + "bytes" + "net" + "strings" + "testing" + + "github.com/docker/libnetwork" + "github.com/vishvananda/netlink" +) + +func TestSetupNewBridge(t *testing.T) { + defer libnetwork.SetupTestNetNS(t)() + + br := &Interface{ + Config: &Configuration{ + BridgeName: DefaultBridgeName, + }, + } + if err := SetupDevice(br); err != nil { + t.Fatalf("Bridge creation failed: %v", err) + } + if br.Link == nil { + t.Fatal("Interface link is nil (expected valid link)") + } + if _, err := netlink.LinkByName(DefaultBridgeName); err != nil { + t.Fatalf("Failed to retrieve bridge device: %v", err) + } + if br.Link.Attrs().Flags&net.FlagUp == net.FlagUp { + t.Fatalf("Interface should be created down") + } +} + +func TestSetupNewNonDefaultBridge(t *testing.T) { + defer libnetwork.SetupTestNetNS(t)() + + br := &Interface{ + Config: &Configuration{ + BridgeName: "test0", + }, + } + if err := SetupDevice(br); err == nil || !strings.Contains(err.Error(), "non default name") { + t.Fatalf("Expected bridge creation failure with \"non default name\", got: %v", err) + } +} + +func TestSetupDeviceUp(t *testing.T) { + defer libnetwork.SetupTestNetNS(t)() + + br := &Interface{ + Config: &Configuration{ + BridgeName: DefaultBridgeName, + }, + } + if err := SetupDevice(br); err != nil { + t.Fatalf("Bridge creation failed: %v", err) + } + if err := SetupDeviceUp(br); err != nil { + t.Fatalf("Failed to up bridge device: %v", err) + } + + lnk, _ := netlink.LinkByName(DefaultBridgeName) + if lnk.Attrs().Flags&net.FlagUp != net.FlagUp { + t.Fatalf("Interface should be up") + } +} + +func TestGenerateRandomMAC(t *testing.T) { + defer libnetwork.SetupTestNetNS(t)() + + mac1 := generateRandomMAC() + mac2 := generateRandomMAC() + if bytes.Compare(mac1, mac2) == 0 { + t.Fatalf("Generated twice the same MAC address %v", mac1) + } +} diff --git a/bridge/setup_ipv4.go b/bridge/setup_ipv4.go index 8068eea..cfbc6b0 100644 --- a/bridge/setup_ipv4.go +++ b/bridge/setup_ipv4.go @@ -5,7 +5,6 @@ import ( "net" log "github.com/Sirupsen/logrus" - "github.com/docker/docker/pkg/networkfs/resolvconf" "github.com/vishvananda/netlink" ) @@ -61,8 +60,8 @@ func electBridgeIPv4(config *Configuration) (*net.IPNet, error) { // can't read /etc/resolv.conf. So instead we skip the append if resolvConf // is nil. It either doesn't exist, or we can't read it for some reason. nameservers := []string{} - if resolvConf, _ := resolvconf.Get(); resolvConf != nil { - nameservers = append(nameservers, resolvconf.GetNameserversAsCIDR(resolvConf)...) + if resolvConf, _ := readResolvConf(); resolvConf != nil { + nameservers = append(nameservers, getNameserversAsCIDR(resolvConf)...) } // Try to automatically elect appropriate brige IPv4 settings. diff --git a/bridge/setup_ipv4_test.go b/bridge/setup_ipv4_test.go new file mode 100644 index 0000000..f7200c4 --- /dev/null +++ b/bridge/setup_ipv4_test.go @@ -0,0 +1,79 @@ +package bridge + +import ( + "net" + "testing" + + "github.com/docker/libnetwork" + "github.com/vishvananda/netlink" +) + +func setupTestInterface(t *testing.T) *Interface { + br := &Interface{ + Config: &Configuration{ + BridgeName: DefaultBridgeName, + }, + } + if err := SetupDevice(br); err != nil { + t.Fatalf("Bridge creation failed: %v", err) + } + return br +} + +func TestSetupBridgeIPv4Fixed(t *testing.T) { + defer libnetwork.SetupTestNetNS(t)() + + ip, netw, err := net.ParseCIDR("192.168.1.1/24") + if err != nil { + t.Fatalf("Failed to parse bridge IPv4: %v", err) + } + + br := setupTestInterface(t) + br.Config.AddressIPv4 = &net.IPNet{IP: ip, Mask: netw.Mask} + if err := SetupBridgeIPv4(br); err != nil { + t.Fatalf("Failed to setup bridge IPv4: %v", err) + } + + addrsv4, err := netlink.AddrList(br.Link, netlink.FAMILY_V4) + if err != nil { + t.Fatalf("Failed to list device IPv4 addresses: %v", err) + } + + var found bool + for _, addr := range addrsv4 { + if br.Config.AddressIPv4.String() == addr.IPNet.String() { + found = true + break + } + } + + if !found { + t.Fatalf("Bridge device does not have requested IPv4 address %v", br.Config.AddressIPv4) + } +} + +func TestSetupBridgeIPv4Auto(t *testing.T) { + defer libnetwork.SetupTestNetNS(t)() + + br := setupTestInterface(t) + if err := SetupBridgeIPv4(br); err != nil { + t.Fatalf("Failed to setup bridge IPv4: %v", err) + } + + addrsv4, err := netlink.AddrList(br.Link, netlink.FAMILY_V4) + if err != nil { + t.Fatalf("Failed to list device IPv4 addresses: %v", err) + } + + var found bool + for _, addr := range addrsv4 { + if bridgeNetworks[0].String() == addr.IPNet.String() { + found = true + break + } + } + + if !found { + t.Fatalf("Bridge device does not have the automatic IPv4 address %v", bridgeNetworks[0].String()) + } +} diff --git a/namespace.go b/namespace.go index c09c350..4cc53e5 100644 --- a/namespace.go +++ b/namespace.go @@ -1,5 +1,7 @@ package libnetwork +import "syscall" + type networkNamespace struct { path string interfaces []*Interface @@ -29,3 +31,9 @@ func (n *networkNamespace) Interfaces() []*Interface { func (n *networkNamespace) Path() string { return n.path } + +func (n *networkNamespace) Destroy() error { + // Assuming no running process is executing in this network namespace, + // unmounting is sufficient to destroy it. + return syscall.Unmount(n.path, syscall.MNT_DETACH) +} diff --git a/pkg/options/options_test.go b/pkg/options/options_test.go index 36c5060..f1ab7ea 100644 --- a/pkg/options/options_test.go +++ b/pkg/options/options_test.go @@ -2,6 +2,7 @@ package options import ( "reflect" + "strings" "testing" ) @@ -23,6 +24,39 @@ func TestGenerate(t *testing.T) { t.Fatal(err) } + cast, ok := result.(Model) + if !ok { + t.Fatalf("result has unexpected type %s", reflect.TypeOf(result)) + } + if expected := 1; cast.Int != expected { + t.Fatalf("wrong value for field Int: expected %v, got %v", expected, cast.Int) + } + if expected := 'b'; cast.Rune != expected { + t.Fatalf("wrong value for field Rune: expected %v, got %v", expected, cast.Rune) + } + if expected := 2.0; cast.Float64 != expected { + t.Fatalf("wrong value for field Int: expected %v, got %v", expected, cast.Float64) + } +} + +func TestGeneratePtr(t *testing.T) { + gen := NewGeneric() + gen["Int"] = 1 + gen["Rune"] = 'b' + gen["Float64"] = 2.0 + + type Model struct { + Int int + Rune rune + Float64 float64 + } + + result, err := GenerateFromModel(gen, &Model{}) + + if err != nil { + t.Fatal(err) + } + cast, ok := result.(*Model) if !ok { t.Fatalf("result has unexpected type %s", reflect.TypeOf(result)) @@ -44,6 +78,8 @@ func TestGenerateMissingField(t *testing.T) { if _, ok := err.(NoSuchFieldError); !ok { t.Fatalf("expected NoSuchFieldError, got %#v", err) + } else if expected := "no field"; !strings.Contains(err.Error(), expected) { + t.Fatalf("expected %q in error message, got %s", expected, err.Error()) } } @@ -53,5 +89,7 @@ func TestFieldCannotBeSet(t *testing.T) { if _, ok := err.(CannotSetFieldError); !ok { t.Fatalf("expected CannotSetFieldError, got %#v", err) + } else if expected := "cannot set field"; !strings.Contains(err.Error(), expected) { + t.Fatalf("expected %q in error message, got %s", expected, err.Error()) } } diff --git a/reexec_netns_create.go b/reexec_netns_create.go index 867061e..96c63bc 100644 --- a/reexec_netns_create.go +++ b/reexec_netns_create.go @@ -1,11 +1,11 @@ package libnetwork import ( - "log" "os" "runtime" "syscall" + log "github.com/Sirupsen/logrus" "github.com/vishvananda/netlink" ) diff --git a/testing.go b/testing.go new file mode 100644 index 0000000..3c8960a --- /dev/null +++ b/testing.go @@ -0,0 +1,33 @@ +package libnetwork + +import ( + "runtime" + "syscall" + "testing" +) + +// SetupTestNetNS joins a new network namespace, and returns its associated +// teardown function. +// +// Example usage: +// +// defer SetupTestNetNS(t)() +// +func SetupTestNetNS(t *testing.T) func() { + runtime.LockOSThread() + if err := syscall.Unshare(syscall.CLONE_NEWNET); err != nil { + t.Fatalf("Failed to enter netns: %v", err) + } + + fd, err := syscall.Open("/proc/self/ns/net", syscall.O_RDONLY, 0) + if err != nil { + t.Fatal("Failed to open netns file") + } + + return func() { + if err := syscall.Close(fd); err != nil { + t.Logf("Warning: netns closing failed (%v)", err) + } + runtime.UnlockOSThread() + } +}