From a1defdc5aadaa743d9972a353efe0048c43135f6 Mon Sep 17 00:00:00 2001 From: mwnx Date: Sat, 2 Jun 2018 11:55:48 +0200 Subject: [PATCH] Fully implement ip6zone support - Add `FromIPAndZone` function (complementing the `FromIP` function). - Handle zones when parsing `net.Addr`s. - In `DialArgs`, return an error if an `ip6zone` is prefixed to an `ip4` or to another `ip6zone`. Note: I was not able to add a listen test (in TestListenAddrs) for `/zone/.../ip6/...` since there is no link-local address which is guaranteed to exist on all systems. --- convert.go | 44 ++++++++++++++++++++++++------- convert_test.go | 59 ++++++++++++++++++++++++++++-------------- ip.go | 69 ++++++++++++++++++++++++++++++++----------------- net_test.go | 32 +++++++++++++++++++++++ 4 files changed, 153 insertions(+), 51 deletions(-) diff --git a/convert.go b/convert.go index e978046..06fb414 100644 --- a/convert.go +++ b/convert.go @@ -65,24 +65,39 @@ func parseBasicNetMaddr(maddr ma.Multiaddr) (net.Addr, error) { return nil, fmt.Errorf("network not supported: %s", network) } -// FromIP converts a net.IP type to a Multiaddr. -func FromIP(ip net.IP) (ma.Multiaddr, error) { - var proto string +func FromIPAndZone(ip net.IP, zone string) (ma.Multiaddr, error) { switch { case ip.To4() != nil: - proto = "ip4" + return ma.NewComponent("ip4", ip.String()) case ip.To16() != nil: - proto = "ip6" + ip6, err := ma.NewComponent("ip6", ip.String()) + if err != nil { + return nil, err + } + if zone == "" { + return ip6, nil + } else { + zone, err := ma.NewComponent("ip6zone", zone) + if err != nil { + return nil, err + } + return zone.Encapsulate(ip6), nil + } default: return nil, errIncorrectNetAddr } - return ma.NewComponent(proto, ip.String()) +} + +// FromIP converts a net.IP type to a Multiaddr. +func FromIP(ip net.IP) (ma.Multiaddr, error) { + return FromIPAndZone(ip, "") } // DialArgs is a convenience function returning arguments for use in net.Dial func DialArgs(m ma.Multiaddr) (string, string, error) { var ( zone, network, ip, port string + err error ) ma.ForEach(m, func(c ma.Component) bool { @@ -90,6 +105,10 @@ func DialArgs(m ma.Multiaddr) (string, string, error) { case "": switch c.Protocol().Code { case ma.P_IP6ZONE: + if zone != "" { + err = fmt.Errorf("%s has multiple zones", m) + return false + } zone = c.Value() return true case ma.P_IP6: @@ -97,6 +116,10 @@ func DialArgs(m ma.Multiaddr) (string, string, error) { ip = c.Value() return true case ma.P_IP4: + if zone != "" { + err = fmt.Errorf("%s has ip4 with zone", m) + return false + } network = "ip4" ip = c.Value() return true @@ -125,6 +148,9 @@ func DialArgs(m ma.Multiaddr) (string, string, error) { // Done. return false }) + if err != nil { + return "", "", err + } switch network { case "ip6": if zone != "" { @@ -152,7 +178,7 @@ func parseTCPNetAddr(a net.Addr) (ma.Multiaddr, error) { } // Get IP Addr - ipm, err := FromIP(ac.IP) + ipm, err := FromIPAndZone(ac.IP, ac.Zone) if err != nil { return nil, errIncorrectNetAddr } @@ -174,7 +200,7 @@ func parseUDPNetAddr(a net.Addr) (ma.Multiaddr, error) { } // Get IP Addr - ipm, err := FromIP(ac.IP) + ipm, err := FromIPAndZone(ac.IP, ac.Zone) if err != nil { return nil, errIncorrectNetAddr } @@ -194,7 +220,7 @@ func parseIPNetAddr(a net.Addr) (ma.Multiaddr, error) { if !ok { return nil, errIncorrectNetAddr } - return FromIP(ac.IP) + return FromIPAndZone(ac.IP, ac.Zone) } func parseIPPlusNetAddr(a net.Addr) (ma.Multiaddr, error) { diff --git a/convert_test.go b/convert_test.go index 0d06741..105f6b7 100644 --- a/convert_test.go +++ b/convert_test.go @@ -90,18 +90,25 @@ func TestFromUDP(t *testing.T) { func TestThinWaist(t *testing.T) { addrs := map[string]bool{ - "/ip4/127.0.0.1/udp/1234": true, - "/ip4/127.0.0.1/tcp/1234": true, - "/ip4/127.0.0.1/udp/1234/tcp/1234": true, - "/ip4/127.0.0.1/tcp/12345/ip4/1.2.3.4": true, - "/ip6/::1/tcp/80": true, - "/ip6/::1/udp/80": true, - "/ip6/::1": true, - "/tcp/1234/ip4/1.2.3.4": false, - "/tcp/1234": false, - "/tcp/1234/udp/1234": false, - "/ip4/1.2.3.4/ip4/2.3.4.5": true, - "/ip6/::1/ip4/2.3.4.5": true, + "/ip4/127.0.0.1/udp/1234": true, + "/ip4/127.0.0.1/tcp/1234": true, + "/ip4/127.0.0.1/udp/1234/tcp/1234": true, + "/ip4/127.0.0.1/tcp/12345/ip4/1.2.3.4": true, + "/ip6/::1/tcp/80": true, + "/ip6/::1/udp/80": true, + "/ip6/::1": true, + "/ip6zone/hello/ip6/fe80::1/tcp/80": true, + "/ip6zone/hello/ip6/fe80::1": true, + "/tcp/1234/ip4/1.2.3.4": false, + "/tcp/1234": false, + "/tcp/1234/udp/1234": false, + "/ip4/1.2.3.4/ip4/2.3.4.5": true, + "/ip6/fe80::1/ip4/2.3.4.5": true, + "/ip6zone/hello/ip6/fe80::1/ip4/2.3.4.5": true, + + // Invalid ip6zone usage: + "/ip6zone/hello": false, + "/ip6zone/hello/ip4/1.1.1.1": false, } for a, res := range addrs { @@ -120,7 +127,7 @@ func TestDialArgs(t *testing.T) { test := func(e_maddr, e_nw, e_host string) { m, err := ma.NewMultiaddr(e_maddr) if err != nil { - t.Fatal("failed to construct", "/ip4/127.0.0.1/udp/1234", e_maddr) + t.Fatal("failed to construct", e_maddr) } nw, host, err := DialArgs(m) @@ -137,14 +144,28 @@ func TestDialArgs(t *testing.T) { } } + test_error := func(e_maddr string) { + m, err := ma.NewMultiaddr(e_maddr) + if err != nil { + t.Fatal("failed to construct", e_maddr) + } + + _, _, err = DialArgs(m) + if err == nil { + t.Fatal("expected DialArgs to fail on", e_maddr) + } + } + test("/ip4/127.0.0.1/udp/1234", "udp4", "127.0.0.1:1234") test("/ip4/127.0.0.1/tcp/4321", "tcp4", "127.0.0.1:4321") test("/ip6/::1/udp/1234", "udp6", "[::1]:1234") test("/ip6/::1/tcp/4321", "tcp6", "[::1]:4321") - test("/ip6/::1", "ip6", "::1") // Just an IP - test("/ip4/1.2.3.4", "ip4", "1.2.3.4") // Just an IP - test("/ip6zone/foo/ip6/::1/tcp/4321", "tcp6", "[::1%foo]:4321") // zone - test("/ip6zone/foo/ip6/::1", "ip6", "::1%foo") // no TCP - test("/ip6zone/foo/ip6/::1/ip6zone/bar", "ip6", "::1%foo") // IP over IP - test("/ip6zone/foo/ip4/127.0.0.1/ip6zone/bar", "ip4", "127.0.0.1") // Skip zones in IP + test("/ip6/::1", "ip6", "::1") // Just an IP + test("/ip4/1.2.3.4", "ip4", "1.2.3.4") // Just an IP + test("/ip6zone/foo/ip6/::1/tcp/4321", "tcp6", "[::1%foo]:4321") // zone + test("/ip6zone/foo/ip6/::1/udp/4321", "udp6", "[::1%foo]:4321") // zone + test("/ip6zone/foo/ip6/::1", "ip6", "::1%foo") // no TCP + test_error("/ip6zone/foo/ip4/127.0.0.1") // IP4 doesn't take zone + test("/ip6zone/foo/ip6/::1/ip6zone/bar", "ip6", "::1%foo") // IP over IP + test_error("/ip6zone/foo/ip6zone/bar/ip6/::1") // Only one zone per IP6 } diff --git a/ip.go b/ip.go index ef39c71..296d3c1 100644 --- a/ip.go +++ b/ip.go @@ -27,6 +27,10 @@ var ( // IsThinWaist returns whether a Multiaddr starts with "Thin Waist" Protocols. // This means: /{IP4, IP6}[/{TCP, UDP}] func IsThinWaist(m ma.Multiaddr) bool { + m = zoneless(m) + if m == nil { + return false + } p := m.Protocols() // nothing? not even a waist. @@ -52,9 +56,14 @@ func IsThinWaist(m ma.Multiaddr) bool { } // IsIPLoopback returns whether a Multiaddr is a "Loopback" IP address -// This means either /ip4/127.*.*.*, /ip6/::1, or /ip6/::ffff:127.*.*.*.* +// This means either /ip4/127.*.*.*, /ip6/::1, or /ip6/::ffff:127.*.*.*.*, +// or /ip6zone//ip6/ func IsIPLoopback(m ma.Multiaddr) bool { + m = zoneless(m) c, rest := ma.SplitFirst(m) + if c == nil { + return false + } if rest != nil { // Not *just* an IPv4 addr return false @@ -66,33 +75,47 @@ func IsIPLoopback(m ma.Multiaddr) bool { return false } -// IsIP6LinkLocal returns if a an IPv6 link-local multiaddress (with zero or -// more leading zones). These addresses are non routable. +// IsIP6LinkLocal returns whether a Multiaddr starts with an IPv6 link-local +// multiaddress (with zero or one leading zone). These addresses are non +// routable. func IsIP6LinkLocal(m ma.Multiaddr) bool { - matched := false - ma.ForEach(m, func(c ma.Component) bool { - // Too much. - if matched { - matched = false - return false - } - - switch c.Protocol().Code { - case ma.P_IP6ZONE: - return true - case ma.P_IP6: - ip := net.IP(c.RawValue()) - matched = ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() - return true - default: - return false - } - }) - return matched + m = zoneless(m) + c, _ := ma.SplitFirst(m) + if c == nil || c.Protocol().Code != ma.P_IP6 { + return false + } + ip := net.IP(c.RawValue()) + return ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() } // IsIPUnspecified returns whether a Multiaddr is am Unspecified IP address // This means either /ip4/0.0.0.0 or /ip6/:: func IsIPUnspecified(m ma.Multiaddr) bool { + m = zoneless(m) + if m == nil { + return false + } return IP4Unspecified.Equal(m) || IP6Unspecified.Equal(m) } + +// If m matches [zone,ip6,...], return [ip6,...] +// else if m matches [], [zone], or [zone,...], return nil +// else return m +func zoneless(m ma.Multiaddr) ma.Multiaddr { + head, tail := ma.SplitFirst(m) + if head == nil { + return nil + } + if head.Protocol().Code == ma.P_IP6ZONE { + if tail == nil { + return nil + } + tailhead, _ := ma.SplitFirst(tail) + if tailhead.Protocol().Code != ma.P_IP6 { + return nil + } + return tail + } else { + return m + } +} diff --git a/net_test.go b/net_test.go index cccdabc..1c70997 100644 --- a/net_test.go +++ b/net_test.go @@ -175,12 +175,20 @@ func TestListenAddrs(t *testing.T) { test("/ip4/0.0.0.0/tcp/4324", "", true) test("/ip4/0.0.0.0/udp/4325", "", false) test("/ip4/0.0.0.0/udp/4326/udt", "", false) + test("/ip6/::1/tcp/4324", "", true) test("/ip6/::1/udp/4325", "", false) test("/ip6/::1/udp/4326/udt", "", false) test("/ip6/::/tcp/4324", "", true) test("/ip6/::/udp/4325", "", false) test("/ip6/::/udp/4326/udt", "", false) + + /* "An implementation should also support the concept of a "default" + * zone for each scope. And, when supported, the index value zero + * at each scope SHOULD be reserved to mean "use the default zone"." + * -- rfc4007. So, this _should_ work everywhere(?). */ + test("/ip6zone/0/ip6/::1/tcp/4324", "/ip6/::1/tcp/4324", true) + test("/ip6zone/0/ip6/::1/udp/4324", "", false) } func TestListenAndDial(t *testing.T) { @@ -345,6 +353,22 @@ func TestIPLoopback(t *testing.T) { if IsIPLoopback(newMultiaddr(t, "/ip6/::fffa:127.99.3.2")) { t.Error("IsIPLoopback false positive (/ip6/::fffa:127.99.3.2)") } + + if !IsIPLoopback(newMultiaddr(t, "/ip6zone/0/ip6/::1")) { + t.Error("IsIPLoopback failed (/ip6zone/0/ip6/::1)") + } + + if !IsIPLoopback(newMultiaddr(t, "/ip6zone/xxx/ip6/::1")) { + t.Error("IsIPLoopback failed (/ip6zone/xxx/ip6/::1)") + } + + if IsIPLoopback(newMultiaddr(t, "/ip6zone/0/ip6/::1/tcp/3333")) { + t.Error("IsIPLoopback failed (/ip6zone/0/ip6/::1/tcp/3333)") + } + + if IsIPLoopback(newMultiaddr(t, "/ip6zone/0/ip6/1::1")) { + t.Errorf("IsIPLoopback false positive (/ip6zone/0/ip6/1::1)") + } } func TestIPUnspecified(t *testing.T) { @@ -363,6 +387,10 @@ func TestIPUnspecified(t *testing.T) { if !IsIPUnspecified(IP6Unspecified) { t.Error("IsIPUnspecified failed (IP6Unspecified)") } + + if !IsIPUnspecified(newMultiaddr(t, "/ip6zone/xxx/ip6/::")) { + t.Error("IsIPUnspecified failed (/ip6zone/xxx/ip6/::)") + } } func TestIP6LinkLocal(t *testing.T) { @@ -373,6 +401,10 @@ func TestIP6LinkLocal(t *testing.T) { t.Errorf("IsIP6LinkLocal failed (%s != %v)", m, isLinkLocal) } } + + if !IsIP6LinkLocal(newMultiaddr(t, "/ip6zone/hello/ip6/fe80::9999")) { + t.Error("IsIP6LinkLocal failed (/ip6/fe80::9999)") + } } func TestConvertNetAddr(t *testing.T) {