veyron/lib/netstate,netconfig: move netconfig out of the runtime and create netstate.
netstate is a set of routines for determining current networking state
and making policy based selections from the set of available addresses.
Change-Id: Iba2e20f046ae6018bd14f4fcb66f3d1c17af1518
diff --git a/lib/netconfig/example_test.go b/lib/netconfig/example_test.go
new file mode 100644
index 0000000..56b0cbd
--- /dev/null
+++ b/lib/netconfig/example_test.go
@@ -0,0 +1,20 @@
+package netconfig_test
+
+import (
+ "fmt"
+ "log"
+
+ "veyron/lib/netconfig"
+)
+
+func ExampleNetConfigWatcher() {
+ w, err := netconfig.NewNetConfigWatcher()
+ if err != nil {
+ log.Fatalf("oops: %s", err)
+ }
+ fmt.Println("Do something to your network. You should see one or more dings.")
+ for {
+ <-w.Channel()
+ fmt.Println("ding")
+ }
+}
diff --git a/lib/netconfig/ipaux.go b/lib/netconfig/ipaux.go
new file mode 100644
index 0000000..ffade53
--- /dev/null
+++ b/lib/netconfig/ipaux.go
@@ -0,0 +1,47 @@
+// +build plan9 windows
+
+package netconfig
+
+// Code to signal a network change every 2 minutes. We use
+// this for systems where we don't yet have a good way to
+// watch for network changes.
+
+import (
+ "time"
+)
+
+type timerNetConfigWatcher struct {
+ c chan struct{} // channel to signal confg changes
+ stop chan struct{} // channel to tell the watcher to stop
+}
+
+// Stop any waiter
+func (w *timerNetConfigWatcher) Stop() {
+ w.stop <- struct{}{}
+}
+
+func (w *timerNetConfigWatcher) Channel() chan struct{} {
+ return w.c
+}
+
+func (w *timerNetConfigWatcher) watcher() {
+ for {
+ select {
+ case <-w.stop:
+ close(w.c)
+ return
+ case <-time.NewTimer(2 * time.Minute).C:
+ select {
+ case w.c <- struct{}{}:
+ default:
+ }
+ }
+ }
+}
+
+func NewNetConfigWatcher() (NetConfigWatcher, error) {
+ w.c = make(chan struct{})
+ w.stop = make(chan struct{}, 1)
+ go w.watcher()
+ return w, nil
+}
diff --git a/lib/netconfig/ipaux_bsd.go b/lib/netconfig/ipaux_bsd.go
new file mode 100644
index 0000000..1002471
--- /dev/null
+++ b/lib/netconfig/ipaux_bsd.go
@@ -0,0 +1,120 @@
+// +build darwin dragonfly freebsd netbsd openbsd
+
+package netconfig
+
+// We connect to the Route socket and parse messages to
+// look for network configuration changes. This is generic
+// to all BSD based systems (including MacOS). The net
+// library already has code to parse the messages so all
+// we need to do is look for message types.
+
+import (
+ "sync"
+ "syscall"
+ "time"
+ "veyron2/vlog"
+)
+
+/*
+#include <sys/socket.h>
+*/
+import "C"
+
+type bsdNetConfigWatcher struct {
+ sync.Mutex
+ t *time.Timer
+ c chan struct{}
+ s int
+ stopped bool
+}
+
+func (w *bsdNetConfigWatcher) Stop() {
+ w.Lock()
+ defer w.Unlock()
+ if w.stopped {
+ return
+ }
+ w.stopped = true
+ syscall.Close(w.s)
+}
+
+func (w *bsdNetConfigWatcher) Channel() chan struct{} {
+ return w.c
+}
+
+// NewNetConfigWatcher returns a watcher that sends a message
+// on the Channel() whenever the config changes.
+func NewNetConfigWatcher() (NetConfigWatcher, error) {
+ s, err := syscall.Socket(C.PF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
+ if err != nil {
+ vlog.Infof("socket failed: %s", err)
+ return nil, err
+ }
+ w := &bsdNetConfigWatcher{c: make(chan struct{}, 1), s: s}
+ go w.watcher()
+ return w, nil
+}
+
+func (w *bsdNetConfigWatcher) ding() {
+ w.Lock()
+ defer w.Unlock()
+ w.t = nil
+ if w.stopped {
+ return
+ }
+ // Don't let us hang in the lock. The default is safe because the requirement
+ // is that the client get a message after the last config change. Since this is
+ // a queued chan, we really don't have to stuff anything in it if there's already
+ // something there.
+ select {
+ case w.c <- struct{}{}:
+ default:
+ }
+}
+
+func (w *bsdNetConfigWatcher) watcher() {
+ defer w.Stop()
+
+ // Loop waiting for messages.
+ for {
+ b := make([]byte, 4096)
+ nr, err := syscall.Read(w.s, b)
+ if err != nil {
+ return
+ }
+ msgs, err := syscall.ParseRoutingMessage(b[:nr])
+ if err != nil {
+ vlog.Infof("Couldn't parse: %s", err)
+ continue
+ }
+
+ // Decode the addresses.
+ for _, m := range msgs {
+ switch m.(type) {
+ case *syscall.InterfaceMessage:
+ case *syscall.InterfaceAddrMessage:
+ default:
+ continue
+ }
+ // Changing networks usually spans many seconds and involves
+ // multiple network config changes. We add histeresis by
+ // setting an alarm when the first change is detected and
+ // not informing the client till the alarm goes off.
+ // NOTE(p): I chose 3 seconds because that covers all the
+ // events involved in moving from one wifi network to another.
+ w.Lock()
+ if w.t == nil {
+ w.t = time.AfterFunc(3*time.Second, w.ding)
+ }
+ w.Unlock()
+ }
+ }
+
+ w.Stop()
+ w.Lock()
+ close(w.c)
+ if w.t != nil {
+ w.t.Stop()
+ }
+ w.Unlock()
+}
diff --git a/lib/netconfig/ipaux_linux.go b/lib/netconfig/ipaux_linux.go
new file mode 100644
index 0000000..9e90003
--- /dev/null
+++ b/lib/netconfig/ipaux_linux.go
@@ -0,0 +1,371 @@
+package netconfig
+
+// We connect to the Netlink Route socket and parse messages to
+// look for network configuration changes. This is very Linux
+// specific, hence the file name.
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "sync"
+ "syscall"
+ "time"
+ "unsafe"
+ "veyron2/vlog"
+)
+
+/*
+#include <linux/rtnetlink.h>
+*/
+import "C"
+
+// All rtnetlink attributes start with this header.
+type rtAttrHdr C.struct_rtattr
+
+const rtAttrHdrLen = C.sizeof_struct_rtattr
+
+// The address change messages (RTM_NEWADDR, RTM_DELADDR, RTM_GETADDR).
+type ifaddrMsgHdr C.struct_ifaddrmsg
+
+const ifaddrMsgHdrLen = C.sizeof_struct_ifaddrmsg
+
+type rtAttribute fmt.Stringer
+
+type rtAddressMessage struct {
+ name string
+ hdr ifaddrMsgHdr
+ attributes []rtAttribute
+}
+
+// Attribute types (see rtnetlink(7))
+type ifaAddress net.IP
+type ifaLocal net.IP
+type ifaBroadcast net.IP
+type ifaAnycast net.IP
+type ifaMulticast net.IP
+type ifaLabel string
+type ifaCacheInfo C.struct_ifa_cacheinfo
+
+const ifaCacheInfoLen = C.sizeof_struct_ifa_cacheinfo
+
+// String routines to make debugging easier.
+func (a ifaAddress) String() string { return "Address=" + net.IP(a).String() }
+func (a ifaLocal) String() string { return "Local=" + net.IP(a).String() }
+func (a ifaBroadcast) String() string { return "Braodcast=" + net.IP(a).String() }
+func (a ifaAnycast) String() string { return "Anycast=" + net.IP(a).String() }
+func (a ifaMulticast) String() string { return "Anycast=" + net.IP(a).String() }
+func (a ifaLabel) String() string { return "Label=" + string(a) }
+func (a ifaCacheInfo) String() string {
+ return fmt.Sprintf("CacheInfo[preferred %d valid %d cstamp %d tstamp %d]", a.ifa_prefered, a.ifa_valid, a.cstamp, a.tstamp)
+}
+func (m *rtAddressMessage) String() string {
+ return fmt.Sprintf("%s: index %d %v", m.name, m.hdr.ifa_index, m.attributes)
+}
+
+// Address looks for the address attribute in an rtAddressMessage. If it isn't there, just assume the null address.
+func (m *rtAddressMessage) Address() net.IP {
+ for _, a := range m.attributes {
+ switch a.(type) {
+ case ifaAddress:
+ return net.IP(a.(ifaAddress))
+ }
+ }
+ return net.IPv4zero
+}
+
+func parsertAddressAttribute(b []byte) (rtAttribute, []byte, error) {
+ if len(b) == 0 {
+ return nil, nil, nil
+ }
+ if len(b) < rtAttrHdrLen {
+ return nil, nil, errors.New("attribute too short")
+ }
+ ahdr := (*rtAttrHdr)(unsafe.Pointer(&b[0]))
+ rounded := ((ahdr.rta_len + 3) / 4) * 4
+ if len(b) < int(rounded) {
+ return nil, nil, errors.New("attribute too short")
+ }
+ remaining := b[rounded:]
+ b = b[rtAttrHdrLen:ahdr.rta_len]
+ switch ahdr.rta_type {
+ case C.IFA_ADDRESS:
+ return rtAttribute(ifaAddress(net.IP(b))), remaining, nil
+ case C.IFA_LOCAL:
+ return rtAttribute(ifaLocal(b)), remaining, nil
+ case C.IFA_LABEL:
+ return rtAttribute(ifaLabel(b)), remaining, nil
+ case C.IFA_BROADCAST:
+ return rtAttribute(ifaBroadcast(b)), remaining, nil
+ case C.IFA_ANYCAST:
+ return rtAttribute(ifaAnycast(b)), remaining, nil
+ case C.IFA_CACHEINFO:
+ if len(b) < ifaCacheInfoLen {
+ return nil, nil, errors.New("attribute too short")
+ }
+ return rtAttribute(ifaCacheInfo(*(*C.struct_ifa_cacheinfo)(unsafe.Pointer(&b[0])))), remaining, nil
+ case C.IFA_MULTICAST:
+ return rtAttribute(ifaMulticast(b)), remaining, nil
+ }
+ return nil, remaining, errors.New("unknown attribute")
+}
+
+func parsertAddressMessage(nlm syscall.NetlinkMessage) (*rtAddressMessage, error) {
+ var name string
+ switch nlm.Header.Type {
+ case C.RTM_NEWADDR:
+ name = "RTM_NEWADDR"
+ case C.RTM_DELADDR:
+ name = "RTM_DELADDR"
+ case C.RTM_GETADDR:
+ name = "RTM_GETADDR"
+ default:
+ return nil, fmt.Errorf("unknown message type")
+ }
+ if len(nlm.Data) < ifaddrMsgHdrLen {
+ return nil, errors.New("bad length")
+ }
+ m := &rtAddressMessage{name: name, hdr: *(*ifaddrMsgHdr)(unsafe.Pointer(&nlm.Data[0]))}
+ b := nlm.Data[ifaddrMsgHdrLen:]
+ for {
+ var a rtAttribute
+ var err error
+ a, b, err = parsertAddressAttribute(b)
+ if b == nil {
+ break
+ }
+ if err == nil {
+ m.attributes = append(m.attributes, a)
+ }
+ }
+ return m, nil
+}
+
+// The link change messages (RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK).
+type ifInfoMsgHdr C.struct_ifinfomsg
+
+const ifInfoMsgHdrLen = C.sizeof_struct_ifinfomsg
+
+type rtLinkMessage struct {
+ name string
+ hdr ifInfoMsgHdr
+ attributes []rtAttribute
+}
+
+// Attribute types (see rtnetlink(7))
+type iflaAddress []byte
+type iflaBroadcast []byte
+type iflaIFName string
+type iflaMTU uint32
+type iflaLink int
+type iflaQDisc string
+type iflaOperstate int
+type iflaStats C.struct_rtnl_link_stats
+
+const iflaStatsLen = C.sizeof_struct_rtnl_link_stats
+
+// String routines to make debugging easier.
+func (a iflaAddress) String() string { return fmt.Sprintf("HWAddress=%v", []byte(a)) }
+func (a iflaBroadcast) String() string { return fmt.Sprintf("HWBroadcast=%v", []byte(a)) }
+func (a iflaIFName) String() string { return "Name=" + string(a) }
+func (a iflaMTU) String() string { return fmt.Sprintf("MTU=%d", uint32(a)) }
+func (a iflaLink) String() string { return fmt.Sprintf("Type=%d", int(a)) }
+func (a iflaQDisc) String() string { return "Qdisc=" + string(a) }
+func (a iflaStats) String() string {
+ return fmt.Sprintf("Stats[rx %d tx %d ...]", a.rx_packets, a.tx_packets)
+}
+func (a iflaOperstate) String() string { return fmt.Sprintf("Operstate=%d", int(a)) }
+func (m *rtLinkMessage) String() string {
+ return fmt.Sprintf("%s: index %d %v", m.name, m.hdr.ifi_index, m.attributes)
+}
+
+func parseRTLinkAttribute(b []byte) (rtAttribute, []byte, error) {
+ if len(b) == 0 {
+ return nil, nil, nil
+ }
+ if len(b) < rtAttrHdrLen {
+ return nil, nil, errors.New("attribute too short")
+ }
+ ahdr := (*rtAttrHdr)(unsafe.Pointer(&b[0]))
+ rounded := ((ahdr.rta_len + 3) / 4) * 4
+ if len(b) < int(rounded) {
+ return nil, nil, errors.New("attribute too short")
+ }
+ remaining := b[rounded:]
+ b = b[rtAttrHdrLen:ahdr.rta_len]
+ switch ahdr.rta_type {
+ case C.IFLA_ADDRESS:
+ return rtAttribute(iflaAddress(b)), remaining, nil
+ case C.IFLA_BROADCAST:
+ return rtAttribute(iflaBroadcast(b)), remaining, nil
+ case C.IFLA_IFNAME:
+ return rtAttribute(iflaIFName(string(b))), remaining, nil
+ case C.IFLA_MTU:
+ return rtAttribute(iflaMTU(*(*C.uint)(unsafe.Pointer(&b[0])))), remaining, nil
+ case C.IFLA_LINK:
+ return rtAttribute(iflaMTU(*(*C.int)(unsafe.Pointer(&b[0])))), remaining, nil
+ case C.IFLA_QDISC:
+ return rtAttribute(iflaQDisc(string(b))), remaining, nil
+ case C.IFLA_STATS:
+ if len(b) < iflaStatsLen {
+ return nil, remaining, errors.New("attribute too short")
+ }
+ return rtAttribute(iflaStats(*(*C.struct_rtnl_link_stats)(unsafe.Pointer(&b[0])))), remaining, nil
+ case C.IFLA_OPERSTATE:
+ return rtAttribute(iflaOperstate(*(*C.int)(unsafe.Pointer(&b[0])))), remaining, nil
+ }
+ return nil, remaining, errors.New("unknown attribute")
+}
+
+func parsertLinkMessage(nlm syscall.NetlinkMessage) (*rtLinkMessage, error) {
+ var name string
+ switch nlm.Header.Type {
+ case C.RTM_NEWLINK:
+ name = "RTM_NEWLINK"
+ case C.RTM_DELLINK:
+ name = "RTM_DELLINK"
+ case C.RTM_GETLINK:
+ name = "RTM_GETLINK"
+ default:
+ return nil, fmt.Errorf("unknown message type")
+ }
+ if len(nlm.Data) < ifInfoMsgHdrLen {
+ return nil, errors.New("bad length")
+ }
+ m := &rtLinkMessage{name: name, hdr: *(*ifInfoMsgHdr)(unsafe.Pointer(&nlm.Data[0]))}
+ b := nlm.Data[ifInfoMsgHdrLen:]
+ for {
+ var a rtAttribute
+ var err error
+ a, b, err = parseRTLinkAttribute(b)
+ if b == nil {
+ break
+ }
+ if err == nil {
+ m.attributes = append(m.attributes, a)
+ }
+ }
+ return m, nil
+}
+
+type rtnetlinkWatcher struct {
+ sync.Mutex
+ t *time.Timer
+ c chan struct{}
+ s int
+ stopped bool
+}
+
+func (w *rtnetlinkWatcher) Stop() {
+ w.Lock()
+ defer w.Unlock()
+ if w.stopped {
+ return
+ }
+ w.stopped = true
+ syscall.Close(w.s)
+}
+
+func (w *rtnetlinkWatcher) Channel() chan struct{} {
+ return w.c
+}
+
+const (
+ GROUPS = C.RTMGRP_LINK | C.RTMGRP_IPV4_IFADDR | C.RTMGRP_IPV6_IFADDR | C.RTMGRP_NOTIFY
+)
+
+// NewNetConfigWatcher returns a watcher that wakes up anyone
+// calling the Wait routine whenever the configuration changes.
+func NewNetConfigWatcher() (NetConfigWatcher, error) {
+ s, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_ROUTE)
+ if err != nil {
+ vlog.Infof("netconfig socket failed: %s", err)
+ return nil, err
+ }
+
+ lsa := &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK, Groups: GROUPS}
+ if err := syscall.Bind(s, lsa); err != nil {
+ vlog.Infof("netconfig bind failed: %s", err)
+ return nil, err
+ }
+
+ w := &rtnetlinkWatcher{c: make(chan struct{}, 1), s: s}
+ go w.watcher()
+ return w, nil
+}
+
+func (w *rtnetlinkWatcher) ding() {
+ w.Lock()
+ defer w.Unlock()
+ w.t = nil
+ if w.stopped {
+ return
+ }
+ // Don't let us hang in the lock. The default is safe because the requirement
+ // is that the client get a message after the last config change. Since this is
+ // a queued chan, we really don't have to stuff anything in it if there's already
+ // something there.
+ select {
+ case w.c <- struct{}{}:
+ default:
+ }
+}
+
+func (w *rtnetlinkWatcher) watcher() {
+ var newAddrs []net.IP
+ for {
+ rb := make([]byte, 4096)
+ nr, _, err := syscall.Recvfrom(w.s, rb, 0)
+ if err != nil {
+ break
+ }
+ rb = rb[:nr]
+ msgs, err := syscall.ParseNetlinkMessage(rb)
+ if err != nil {
+ vlog.Infof("ParseNetlinkMessage failed: %s", err)
+ continue
+ }
+ L:
+ for _, m := range msgs {
+ if am, err := parsertAddressMessage(m); err == nil {
+ // NOTE(p): We get continuous NEWADDR messages about some
+ // IPv6 addresses in Google corp. Just ignore duplicate back
+ // to back NEWADDRs about the same addresses.
+ if am.name == "RTM_NEWADDR" {
+ addr := am.Address()
+ for _, a := range newAddrs {
+ if addr.Equal(a) {
+ break L
+ }
+ }
+ newAddrs = append(newAddrs, addr)
+ } else {
+ newAddrs = nil
+ }
+ } else if _, err := parsertLinkMessage(m); err == nil {
+ newAddrs = nil
+ } else {
+ continue
+ }
+ // Changing networks usually spans many seconds and involves
+ // multiple network config changes. We add histeresis by
+ // setting an alarm when the first change is detected and
+ // not informing the client till the alarm goes off.
+ // NOTE(p): I chose 3 seconds because that covers all the
+ // events involved in moving from one wifi network to another.
+ w.Lock()
+ if w.t == nil {
+ w.t = time.AfterFunc(3*time.Second, w.ding)
+ }
+ w.Unlock()
+ }
+ }
+
+ w.Stop()
+ w.Lock()
+ close(w.c)
+ if w.t != nil {
+ w.t.Stop()
+ }
+ w.Unlock()
+}
diff --git a/lib/netconfig/model.go b/lib/netconfig/model.go
new file mode 100644
index 0000000..c65afb6
--- /dev/null
+++ b/lib/netconfig/model.go
@@ -0,0 +1,17 @@
+// package netconfig implements a network configuration watcher.
+// NOTE(p): This is also where we should put any code that changes
+// network configuration.
+
+package netconfig
+
+// NetConfigWatcher sends on channel whenever an interface or interface address
+// is added or deleted.
+type NetConfigWatcher interface {
+ // Stop watching.
+ Stop()
+
+ // A channel that returns an item whenever the network addresses or
+ // interfaces have changed. It is up to the caller to reread the
+ // network configuration in such cases.
+ Channel() chan struct{}
+}
diff --git a/lib/netstate/isgloballyroutable.go b/lib/netstate/isgloballyroutable.go
new file mode 100644
index 0000000..ee6bae5
--- /dev/null
+++ b/lib/netstate/isgloballyroutable.go
@@ -0,0 +1,29 @@
+package netstate
+
+import (
+ "net"
+)
+
+var privateCIDRs = []net.IPNet{
+ net.IPNet{IP: net.IPv4(10, 0, 0, 0), Mask: net.IPv4Mask(0xff, 0, 0, 0)},
+ net.IPNet{IP: net.IPv4(172, 16, 0, 0), Mask: net.IPv4Mask(0xff, 0xf0, 0, 0)},
+ net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(0xff, 0xff, 0, 0)},
+}
+
+// IsGloballyRoutable returns true if the argument is a globally routable IP address.
+func IsGloballyRoutableIP(ip net.IP) bool {
+ if !ip.IsGlobalUnicast() {
+ return false
+ }
+ if ip4 := ip.To4(); ip4 != nil {
+ for _, cidr := range privateCIDRs {
+ if cidr.Contains(ip4) {
+ return false
+ }
+ }
+ if ip4.Equal(net.IPv4bcast) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/lib/netstate/isgloballyroutable_test.go b/lib/netstate/isgloballyroutable_test.go
new file mode 100644
index 0000000..dd0885a
--- /dev/null
+++ b/lib/netstate/isgloballyroutable_test.go
@@ -0,0 +1,31 @@
+package netstate
+
+import (
+ "net"
+ "testing"
+)
+
+func TestIsGloballyRoutable(t *testing.T) {
+ tests := []struct {
+ ip string
+ want bool
+ }{
+ {"192.168.1.1", false},
+ {"192.169.0.3", true},
+ {"10.1.1.1", false},
+ {"172.17.100.255", false},
+ {"172.32.0.1", true},
+ {"255.255.255.255", false},
+ {"127.0.0.1", false},
+ {"224.0.0.1", false},
+ {"FF02::FB", false},
+ {"fe80::be30:5bff:fed3:843f", false},
+ {"2620:0:1000:8400:be30:5bff:fed3:843f", true},
+ }
+ for _, test := range tests {
+ ip := net.ParseIP(test.ip)
+ if got := IsGloballyRoutableIP(ip); got != test.want {
+ t.Fatalf("%s: want %v got %v", test.ip, test.want, got)
+ }
+ }
+}
diff --git a/lib/netstate/netstate.go b/lib/netstate/netstate.go
new file mode 100644
index 0000000..e35e595
--- /dev/null
+++ b/lib/netstate/netstate.go
@@ -0,0 +1,270 @@
+// Package netstate provides routines to obtain the available set of
+// of network addresess, for determining changes to those addresses and for
+// selecting from amongst them according to some set of policies that are
+// implemented by applying simple predicates (functions with names of the form
+// Is<condition> to filter or find the first matching address from a list
+// of addresses. The intent is to make it easy to create policies that do
+// things like 'find the first IPv4 unicast address that is globally routable,
+// failing that use a private IPv4 address, and failing that, an IPv6 address'.
+//
+// A typical usage would be:
+//
+// state, _ := netstate.GetAccessibleIPs()
+// first := netstate.First(netstate.IsPublicIPv4)
+// // first will contain the first public IPv4 address or be nil.
+//
+// The example policy described above would be implemented using a
+// series of calls to First with appropriate predicates.
+//
+// Although most commercial networking hardware supports IPv6, some consumer
+// devices and more importantly many ISPs do not, so routines are provided
+// to allow developers to easily distinguish between the two and to use
+// whichever is appropriate for their product/situation.
+//
+// The term 'accessible' is used to refer to any non-loopback IP address.
+// The term 'public' is used to refer to any globally routable IP address.
+//
+// All IPv6 addresses are intended to be 'public', but any starting with
+// fc00::/7 are (RFC4193) are reserved for private use, but the go
+// net libraries do not appear to recognise this. Similarly fe80::/10
+// (RFC 4291) are reserved for 'site-local' usage, but again this is not
+// implemented in the go libraries. Any developer who needs to distinguish
+// these cases will need to write their own routines to test for them.
+//
+// When using the go net package it is important to remember that IPv6
+// addresses subsume IPv4 and hence in many cases the same internal
+// representation is used for both, thus testing for the length of the IP
+// address is unreliable. The reliable test is to use the net.To4() which
+// will return a non-nil result if can be used as an IPv4 one. Any address
+// can be used as an IPv6 and hence the only reliable way to test for an IPv6
+// address that is not an IPv4 one also is for the To4 call to return nil for
+// it.
+package netstate
+
+import (
+ "fmt"
+ "net"
+ "strings"
+)
+
+type AddrList []net.Addr
+
+func (al AddrList) String() string {
+ r := ""
+ for _, v := range al {
+ r += fmt.Sprintf("(%s) ", v)
+ }
+ return strings.TrimRight(r, " ")
+}
+
+// GetAll gets all of the available addresses on the device, including
+// loopback addresses.
+func GetAll() (AddrList, error) {
+ interfaces, err := net.Interfaces()
+ if err != nil {
+ return nil, err
+ }
+ var all AddrList
+ for _, ifc := range interfaces {
+ addrs, err := ifc.Addrs()
+ if err != nil {
+ continue
+ }
+ all = append(all, addrs...)
+ }
+ return all, nil
+}
+
+// GetAccessibleIPs returns all of the IP addresses on the device that are
+// accessible to other devices - i.e. excluding loopback etc.
+func GetAccessibleIPs() (AddrList, error) {
+ all, err := GetAll()
+ if err != nil {
+ return nil, err
+ }
+ return all.Filter(IsAccessibleIP), nil
+}
+
+type Predicate func(a net.Addr) bool
+
+// Filter returns all of the addresses for which the predicate
+// function is true.
+func (al AddrList) Filter(predicate Predicate) AddrList {
+ r := AddrList{}
+ for _, a := range al {
+ if predicate(a) {
+ r = append(r, a)
+ }
+ }
+ return r
+}
+
+// Filter returns the first address for which the predicate function is true.
+func (al AddrList) First(predicate Predicate) net.Addr {
+ for _, a := range al {
+ if predicate(a) {
+ return a
+ }
+ }
+ return nil
+}
+
+func IsIPNetwork(n string) bool {
+ switch n {
+ case "ip+net", "tcp", "tcp4", "tcp6", "udp":
+ return true
+ default:
+ return false
+
+ }
+}
+
+// AsIP returns its argument as a net.IP if that's possible.
+func AsIP(a net.Addr) net.IP {
+ ipn, ok := a.(*net.IPNet)
+ if ok {
+ return ipn.IP
+ }
+ ipa, ok := a.(*net.IPAddr)
+ if ok {
+ return ipa.IP
+ }
+ switch a.Network() {
+ default:
+ return nil
+ case "ip+net", "tcp", "tcp4", "tcp6", "udp":
+ }
+ return net.ParseIP(a.String())
+}
+
+// IsUnspecified returns true if its argument is an unspecified IP address
+func IsUnspecifiedIP(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil {
+ return ip.IsUnspecified()
+ }
+ return false
+}
+
+// IsLoopback returns true if its argument is a loopback IP address
+func IsLoopbackIP(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil && !ip.IsUnspecified() {
+ return ip.IsLoopback()
+ }
+ return false
+}
+
+// IsAccessible returns true if its argument is an accessible (non-loopback)
+// IP address.
+func IsAccessibleIP(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil && !ip.IsUnspecified() {
+ return !ip.IsLoopback()
+ }
+ return false
+}
+
+type ma struct {
+ n, a string
+}
+
+func (a *ma) Network() string {
+ return a.n
+}
+
+func (a *ma) String() string {
+ return a.a
+}
+
+// AsAddr returns its argument as a net.Addr.
+func AsAddr(network string, a net.IP) net.Addr {
+ return &ma{n: network, a: a.String()}
+}
+
+// IsUnicastIP returns true if its argument is a unicast IP address.
+func IsUnicastIP(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil && !ip.IsUnspecified() {
+ // ipv4 or v6
+ return !(ip.IsMulticast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast())
+ }
+ return false
+}
+
+// IsUnicastIPv4 returns true if its argument is a unicast IP4 address
+func IsUnicastIPv4(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil && ip.To4() != nil && !ip.IsMulticast() {
+ return !ip.IsUnspecified() && !ip.IsMulticast()
+ }
+ return false
+}
+
+// IsPublicUnicastIPv4 returns true if its argument is a globally routable,
+// public IPv4 unicast address.
+func IsPublicUnicastIPv4(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil && !ip.IsUnspecified() {
+ if t := ip.To4(); t != nil && IsGloballyRoutableIP(t) {
+ return !ip.IsMulticast()
+ }
+ }
+ return false
+}
+
+// IsUnicastIPv6 returns true if its argument is a unicast IPv6 address
+func IsUnicastIPv6(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil && ip.To4() == nil {
+ return !ip.IsUnspecified() && !(ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast())
+ }
+ return false
+}
+
+// IsUnicastIPv6 returns true if its argument is a globally routable IP6 address
+func IsPublicUnicastIPv6(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil && ip.To4() == nil {
+ if t := ip.To16(); t != nil && IsGloballyRoutableIP(t) {
+ return true
+ }
+ }
+ return false
+}
+
+// IsPublicUnicastIP returns true if its argument is a global routable IPv4 or 6
+// address.
+func IsPublicUnicastIP(a net.Addr) bool {
+ if ip := AsIP(a); ip != nil {
+ if t := ip.To4(); t != nil && IsGloballyRoutableIP(t) {
+ return true
+ }
+ if t := ip.To16(); t != nil && IsGloballyRoutableIP(t) {
+ return true
+ }
+ }
+ return false
+}
+
+func diffAB(a, b AddrList) AddrList {
+ diff := AddrList{}
+ for _, av := range a {
+ found := false
+ for _, bv := range b {
+ if av.Network() == bv.Network() &&
+ av.String() == bv.String() {
+ found = true
+ break
+ }
+ }
+ if !found {
+ diff = append(diff, av)
+ }
+ }
+ return diff
+}
+
+// FindAdded returns the set of interfaces and/or addresses that are
+// present in b, but not in a - i.e. have been added.
+func FindAdded(a, b AddrList) AddrList {
+ return diffAB(b, a)
+}
+
+// FindRemoved returns the set of interfaces and/or addresses that
+// are present in a, but not in b - i.e. have been removed.
+func FindRemoved(a, b AddrList) AddrList {
+ return diffAB(a, b)
+}
diff --git a/lib/netstate/netstate_test.go b/lib/netstate/netstate_test.go
new file mode 100644
index 0000000..262d5e5
--- /dev/null
+++ b/lib/netstate/netstate_test.go
@@ -0,0 +1,252 @@
+package netstate_test
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "reflect"
+ "testing"
+
+ "veyron/lib/netstate"
+)
+
+func TestGet(t *testing.T) {
+ all, err := netstate.GetAll()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ accessible, err := netstate.GetAccessibleIPs()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if len(all) == 0 || len(accessible) == 0 {
+ t.Errorf("expected non zero lengths, not %d and %d", len(all), len(accessible))
+ }
+ if len(accessible) > len(all) {
+ t.Errorf("should never be more accessible addresses than 'all' addresses")
+ }
+ loopback := netstate.FindAdded(accessible, all)
+ if got, want := loopback.Filter(netstate.IsLoopbackIP), loopback; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func mkAddr(n, a string) net.Addr {
+ ip := net.ParseIP(a)
+ fmt.Fprintf(os.Stderr, "%s -> %d\n", a, len(ip))
+ return netstate.AsAddr(n, ip)
+}
+
+func TestAsIP(t *testing.T) {
+ lh := net.ParseIP("127.0.0.1")
+ if got, want := netstate.AsIP(&net.IPAddr{IP: lh}), "127.0.0.1"; got == nil || got.String() != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ if got, want := netstate.AsIP(&net.IPNet{IP: lh}), "127.0.0.1"; got == nil || got.String() != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ if got, want := netstate.AsIP(netstate.AsAddr("tcp", lh)), "127.0.0.1"; got == nil || got.String() != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+type hw struct{}
+
+func (*hw) Network() string { return "mac" }
+func (*hw) String() string { return "01:23:45:67:89:ab:cd:ef" }
+
+func TestPredicates(t *testing.T) {
+ if got, want := netstate.IsUnicastIP(&hw{}), false; got != want {
+ t.Errorf("got %t, want %t", got, want)
+
+ }
+
+ cases := []struct {
+ f func(a net.Addr) bool
+ a string
+ r bool
+ }{
+ {netstate.IsUnspecifiedIP, "0.0.0.0", true},
+ {netstate.IsUnspecifiedIP, "::", true},
+ {netstate.IsUnspecifiedIP, "127.0.0.1", false},
+ {netstate.IsUnspecifiedIP, "::1", false},
+
+ {netstate.IsLoopbackIP, "0.0.0.0", false},
+ {netstate.IsLoopbackIP, "::", false},
+ {netstate.IsLoopbackIP, "127.0.0.1", true},
+ {netstate.IsLoopbackIP, "::1", true},
+
+ {netstate.IsAccessibleIP, "0.0.0.0", false},
+ {netstate.IsAccessibleIP, "::", false},
+ {netstate.IsAccessibleIP, "127.0.0.1", false},
+ {netstate.IsAccessibleIP, "::1", false},
+ {netstate.IsAccessibleIP, "224.0.0.2", true},
+ {netstate.IsAccessibleIP, "fc00:1234::", true},
+ {netstate.IsAccessibleIP, "192.168.1.1", true},
+ {netstate.IsAccessibleIP, "2001:4860:0:2001::68", true},
+
+ {netstate.IsUnicastIP, "0.0.0.0", false},
+ {netstate.IsUnicastIP, "::", false},
+ {netstate.IsUnicastIP, "127.0.0.1", true},
+ {netstate.IsUnicastIP, "::1", true},
+ {netstate.IsUnicastIP, "192.168.1.2", true},
+ {netstate.IsUnicastIP, "74.125.239.36", true},
+ {netstate.IsUnicastIP, "224.0.0.2", false},
+ {netstate.IsUnicastIP, "fc00:1235::", true},
+ {netstate.IsUnicastIP, "ff01::01", false},
+ {netstate.IsUnicastIP, "2001:4860:0:2001::69", true},
+
+ {netstate.IsUnicastIPv4, "0.0.0.0", false},
+ {netstate.IsUnicastIPv4, "::", false},
+ {netstate.IsUnicastIPv4, "127.0.0.1", true},
+ {netstate.IsUnicastIPv4, "::1", false},
+ {netstate.IsUnicastIPv4, "192.168.1.3", true},
+ {netstate.IsUnicastIPv6, "74.125.239.37", false},
+ {netstate.IsUnicastIPv4, "224.0.0.2", false},
+ {netstate.IsUnicastIPv4, "fc00:1236::", false},
+ {netstate.IsUnicastIPv4, "ff01::02", false},
+ {netstate.IsUnicastIPv4, "2001:4860:0:2001::6a", false},
+
+ {netstate.IsUnicastIPv6, "0.0.0.0", false},
+ {netstate.IsUnicastIPv6, "::", false},
+ {netstate.IsUnicastIPv6, "127.0.0.1", false},
+ {netstate.IsUnicastIPv6, "::1", true},
+ {netstate.IsUnicastIPv6, "192.168.1.4", false},
+ {netstate.IsUnicastIPv6, "74.125.239.38", false},
+ {netstate.IsUnicastIPv6, "224.0.0.2", false},
+ {netstate.IsUnicastIPv6, "fc00:1237::", true},
+ {netstate.IsUnicastIPv6, "ff01::03", false},
+ {netstate.IsUnicastIPv6, "2607:f8b0:4003:c00::6b", true},
+
+ {netstate.IsPublicUnicastIP, "0.0.0.0", false},
+ {netstate.IsPublicUnicastIP, "::", false},
+ {netstate.IsPublicUnicastIP, "127.0.0.1", false},
+ {netstate.IsPublicUnicastIP, "::1", false},
+ {netstate.IsPublicUnicastIP, "192.168.1.2", false},
+ {netstate.IsPublicUnicastIP, "74.125.239.39", true},
+ {netstate.IsPublicUnicastIP, "224.0.0.2", false},
+ // Arguably this is buggy, the fc00:/7 prefix is supposed to be
+ // non-routable.
+ {netstate.IsPublicUnicastIP, "fc00:1238::", true},
+ {netstate.IsPublicUnicastIP, "ff01::01", false},
+ {netstate.IsPublicUnicastIP, "2001:4860:0:2001::69", true},
+
+ {netstate.IsPublicUnicastIPv4, "0.0.0.0", false},
+ {netstate.IsPublicUnicastIPv4, "::", false},
+ {netstate.IsPublicUnicastIPv4, "127.0.0.1", false},
+ {netstate.IsPublicUnicastIPv4, "::1", false},
+ {netstate.IsPublicUnicastIPv4, "192.168.1.3", false},
+ {netstate.IsPublicUnicastIPv4, "74.125.239.40", true},
+ {netstate.IsPublicUnicastIPv4, "224.0.0.2", false},
+ {netstate.IsPublicUnicastIPv4, "fc00:1239::", false},
+ {netstate.IsPublicUnicastIPv4, "ff01::02", false},
+ {netstate.IsPublicUnicastIPv4, "2001:4860:0:2001::6a", false},
+
+ {netstate.IsPublicUnicastIPv6, "0.0.0.0", false},
+ {netstate.IsPublicUnicastIPv6, "::", false},
+ {netstate.IsPublicUnicastIPv6, "127.0.0.1", false},
+ {netstate.IsPublicUnicastIPv6, "::1", false},
+ {netstate.IsPublicUnicastIPv6, "192.168.1.4", false},
+ {netstate.IsPublicUnicastIPv6, "74.125.239.41", false},
+ {netstate.IsPublicUnicastIPv6, "224.0.0.2", false},
+ // Arguably this is buggy, the fc00:/7 prefix is supposed to be
+ // non-routable.
+ {netstate.IsPublicUnicastIPv6, "fc00:123a::", true},
+ {netstate.IsPublicUnicastIPv6, "ff01::03", false},
+ {netstate.IsPublicUnicastIPv6, "2607:f8b0:4003:c00::6b", true},
+ }
+ for i, c := range cases {
+ net := "tcp"
+ if got, want := c.f(mkAddr(net, c.a)), c.r; got != want {
+ t.Errorf("#%d: %s %s: got %t, want %t", i+1, net, c.a, got, want)
+ }
+ }
+}
+
+var (
+ a = mkAddr("tcp4", "1.2.3.4")
+ b = mkAddr("tcp4", "1.2.3.5")
+ c = mkAddr("tcp4", "1.2.3.6")
+ d = mkAddr("tcp4", "1.2.3.7")
+ a6 = mkAddr("tcp6", "2001:4860:0:2001::68")
+ b6 = mkAddr("tcp6", "2001:4860:0:2001::69")
+ c6 = mkAddr("tcp6", "2001:4860:0:2001::70")
+ d6 = mkAddr("tcp6", "2001:4860:0:2001::71")
+)
+
+func TestRemoved(t *testing.T) {
+ al := netstate.AddrList{a, b, c, a6, b6, c6}
+ bl := netstate.AddrList{}
+
+ // no changes.
+ got, want := netstate.FindRemoved(al, al), netstate.AddrList{}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+
+ // missing everything
+ if got, want := netstate.FindRemoved(al, bl), al; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // missing nothing
+ if got, want := netstate.FindRemoved(bl, al), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // remove some addresses
+ bl = netstate.AddrList{a, b, a6, b6}
+ if got, want := netstate.FindRemoved(al, bl), (netstate.AddrList{c, c6}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // add some addresses
+ bl = netstate.AddrList{a, b, c, a6, b6, c6, d6}
+ if got, want := netstate.FindRemoved(al, bl), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // change some addresses
+ bl = netstate.AddrList{a, b, d, a6, d6, c6}
+ if got, want := netstate.FindRemoved(al, bl), (netstate.AddrList{c, b6}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+}
+
+func TestAdded(t *testing.T) {
+ al := netstate.AddrList{a, b, c, a6, b6, c6}
+ bl := netstate.AddrList{}
+
+ // no changes.
+ if got, want := netstate.FindAdded(al, al), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // add nothing
+ if got, want := netstate.FindAdded(al, bl), bl; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // add everything
+ if got, want := netstate.FindAdded(bl, al), al; !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // add some addresses
+ bl = netstate.AddrList{a, b, c, d, a6, b6, c6, d6}
+ if got, want := netstate.FindAdded(al, bl), (netstate.AddrList{d, d6}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // remove some addresses
+ bl = netstate.AddrList{a, b, c, b6}
+ if got, want := netstate.FindAdded(al, bl), (netstate.AddrList{}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+
+ // change some addresses
+ bl = netstate.AddrList{a, d, c, a6, d6, c6}
+ if got, want := netstate.FindAdded(al, bl), (netstate.AddrList{d, d6}); !reflect.DeepEqual(got, want) {
+ t.Errorf("got %s, want %s", got, want)
+ }
+}