veyron/profiles: first pass at implementing profiles.

Please see comments in veyron/profiles/doc.go

Change-Id: I514c0dfe7d03221314d45686aadd5054e39324dc
diff --git a/products/lib/gce/gce.go b/products/lib/gce/gce.go
deleted file mode 100644
index 2d1cd7b..0000000
--- a/products/lib/gce/gce.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Package gce provides a way to test whether the current process is running on
-// Google Compute Engine, and to extract settings from this environment.
-package gce
-
-import (
-	"io/ioutil"
-	"net"
-	"net/http"
-	"sync"
-	"time"
-)
-
-// This URL returns the external IP address assigned to the local GCE instance.
-// If a HTTP GET request fails for any reason, this is not a GCE instance. If
-// the result of the GET request doesn't contain a "Metadata-Flavor: Google"
-// header, it is also not a GCE instance. The body of the document contains the
-// external IP address, if present. Otherwise, the body is empty.
-const url = "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip"
-
-// How long to wait for the HTTP request to return.
-const timeout = time.Second
-
-var (
-	once       sync.Once
-	isGCE      bool
-	externalIP net.IP
-)
-
-// RunningOnGoogleComputeEngine returns true if the current process is running
-// on a Google Compute Engine instance.
-func RunningOnGoogleComputeEngine() bool {
-	once.Do(func() {
-		isGCE, externalIP = googleComputeEngineTest(url)
-	})
-	return isGCE
-}
-
-// GoogleComputeEngineExternalIPAddress returns the external IP address of this
-// Google Compute Engine instance, or nil if there is none. Must be called after
-// RunningOnGoogleComputeEngine.
-func GoogleComputeEngineExternalIPAddress() net.IP {
-	return externalIP
-}
-
-func googleComputeEngineTest(url string) (bool, net.IP) {
-	client := &http.Client{Timeout: timeout}
-	req, err := http.NewRequest("GET", url, nil)
-	if err != nil {
-		return false, nil
-	}
-	req.Header.Add("Metadata-Flavor", "Google")
-	resp, err := client.Do(req)
-	if err != nil {
-		return false, nil
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != 200 {
-		return false, nil
-	}
-	if flavor := resp.Header["Metadata-Flavor"]; len(flavor) != 1 || flavor[0] != "Google" {
-		return false, nil
-	}
-	body, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return true, nil
-	}
-	return true, net.ParseIP(string(body))
-}
diff --git a/profiles/doc.go b/profiles/doc.go
new file mode 100644
index 0000000..bfd4bfc
--- /dev/null
+++ b/profiles/doc.go
@@ -0,0 +1,67 @@
+// Package Profiles, and its children, provide implementations of the
+// veyron2.Profile interface. These implementations should import all of the
+// packages that they require to implement Profile-specific functionality.
+//
+// The taxonomy used to organise Profiles may be arbitrary and the directory
+// structure used here is just one convention for how to do so. This directory
+// structure reflects the generality of a Profile at any given depth in the
+// hierarchy, with higher levels of the directory structure being more
+// generic and lower levels more specific.
+//
+// Profiles register themselves by calling veyron2/rt.RegisterProfile in their
+// init function and are hence are chosen by importing them into an
+// applications main package. More specific packages may use functionality
+// exposed by more general packages and rely on go's module dependency
+// algorithm to execute the init function from the more specific package
+// after the less specific one and hence override the earlier Profile
+// registration.
+//
+// This top level directory contains a 'generic' Profile and utility routines
+// used by other Profiles. It does not follow the convention of registering
+// itself via its Init function, since the expected use is that it will
+// used automatically as a default by the Runtime. Instead it provides a New
+// function. This avoids the need for every main package to import
+// "veyron/profiles", instead, only more specific Profiles must be so imported.
+//
+// The 'net' Profile adds operating system support for varied network
+// configurations and in particular dhcp. It should be used by any application
+// that may 'roam' or any may be behind a 1-1 NAT.
+//
+// The 'net/bluetooth' Profile adds operating system support for bluetooth
+// networking.
+// TODO(cnicolaou,ashankar): add this
+package profiles
+
+import (
+	"veyron2"
+	"veyron2/config"
+)
+
+type generic struct{}
+
+// New returns a new instance of a very generic Profile. It can be used
+// as a default by Runtime implementations, in unit tests etc.
+func New() veyron2.Profile {
+	return &generic{}
+}
+
+func (g *generic) Name() string {
+	return "generic"
+}
+
+func (g *generic) Runtime() string {
+	return ""
+}
+
+func (g *generic) Platform() *veyron2.Platform {
+	p, _ := Platform()
+	return p
+}
+
+func (g *generic) Init(rt veyron2.Runtime, _ *config.Publisher) {
+	rt.Logger().VI(1).Infof("%s", g)
+}
+
+func (g *generic) String() string {
+	return "generic profile on " + g.Platform().String()
+}
diff --git a/profiles/gce/init.go b/profiles/gce/init.go
new file mode 100644
index 0000000..58a148d
--- /dev/null
+++ b/profiles/gce/init.go
@@ -0,0 +1,53 @@
+// +build linux
+
+// Package gce provides a Profile for Google Compute Engine and should be
+// used by binaries that only ever expect to be run on GCE.
+package gce
+
+import (
+	"veyron/profiles"
+
+	"veyron2"
+	"veyron2/config"
+	"veyron2/rt"
+
+	"veyron/profiles/internal/gce"
+)
+
+func init() {
+	rt.RegisterProfile(&profile{})
+}
+
+type profile struct{}
+
+func (p *profile) Name() string {
+	return "GCE"
+}
+
+func (p *profile) Runtime() string {
+	return ""
+}
+
+func (p *profile) Platform() *veyron2.Platform {
+	platform, _ := profiles.Platform()
+	return platform
+}
+
+func (p *profile) String() string {
+	return "net " + p.Platform().String()
+}
+
+func (p *profile) Init(rt veyron2.Runtime, publisher *config.Publisher) {
+	if !gce.RunningOnGCE() {
+		panic("GCE profile used on a non-GCE system")
+	}
+	go addressPublisher(rt, publisher)
+}
+
+func addressPublisher(rt veyron2.Runtime, p *config.Publisher) {
+	ch := make(chan config.Setting)
+	p.CreateStream("net", "network configuration", ch)
+	for {
+		// TODO(cnicolaou): publish local and external address..
+	}
+}
diff --git a/profiles/internal/gce/gce_linux.go b/profiles/internal/gce/gce_linux.go
new file mode 100644
index 0000000..bc89a54
--- /dev/null
+++ b/profiles/internal/gce/gce_linux.go
@@ -0,0 +1,76 @@
+// +build linux
+
+// Package gce functions to test whether the current process is running on
+// Google Compute Engine, and to extract settings from this environment.
+// Any server that knows it will only ever run on GCE can import this Profile,
+// but in most cases, other Profiles will use the utility routines provided
+// by it to test to ensure that they are running on GCE and to obtain
+// metadata directly from its service APIs.
+package gce
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"sync"
+	"time"
+)
+
+// This URL returns the external IP address assigned to the local GCE instance.
+// If a HTTP GET request fails for any reason, this is not a GCE instance. If
+// the result of the GET request doesn't contain a "Metadata-Flavor: Google"
+// header, it is also not a GCE instance. The body of the document contains the
+// external IP address, if present. Otherwise, the body is empty.
+// See https://developers.google.com/compute/docs/metadata for details.
+const url = "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip"
+
+// How long to wait for the HTTP request to return.
+const timeout = time.Second
+
+var (
+	once       sync.Once
+	isGCEErr   error
+	externalIP net.IP
+)
+
+// RunningOnGCE returns true if the current process is running
+// on a Google Compute Engine instance.
+func RunningOnGCE() bool {
+	once.Do(func() {
+		externalIP, isGCEErr = gceTest(url)
+	})
+	return isGCEErr == nil
+}
+
+// ExternalIPAddress returns the external IP address of this
+// Google Compute Engine instance, or nil if there is none. Must be
+// called after RunningOnGCE.
+func ExternalIPAddress() (net.IP, error) {
+	return externalIP, isGCEErr
+}
+
+func gceTest(url string) (net.IP, error) {
+	client := &http.Client{Timeout: timeout}
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Add("Metadata-Flavor", "Google")
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf("http error: %d", resp.StatusCode)
+	}
+	if flavor := resp.Header["Metadata-Flavor"]; len(flavor) != 1 || flavor[0] != "Google" {
+		return nil, fmt.Errorf("unexpected http header: %q", flavor)
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	return net.ParseIP(string(body)), nil
+}
diff --git a/profiles/internal/gce/gce_other.go b/profiles/internal/gce/gce_other.go
new file mode 100644
index 0000000..5c8fe4a
--- /dev/null
+++ b/profiles/internal/gce/gce_other.go
@@ -0,0 +1,6 @@
+// +build !linux
+
+package gce
+
+// This package is a no-op on non-linux systems, but we still need at least
+// one file in the package to keep go happy!
diff --git a/products/lib/gce/gce_test.go b/profiles/internal/gce/gce_test.go
similarity index 66%
rename from products/lib/gce/gce_test.go
rename to profiles/internal/gce/gce_test.go
index ba2591b..585a842 100644
--- a/products/lib/gce/gce_test.go
+++ b/profiles/internal/gce/gce_test.go
@@ -1,3 +1,5 @@
+// +build linux
+
 package gce
 
 import (
@@ -47,16 +49,16 @@
 	defer stop()
 	baseURL := "http://" + addr.String()
 
-	if isGCE, ip := googleComputeEngineTest(baseURL + "/404"); isGCE != false || ip != nil {
-		t.Errorf("Unexpected result. Got %v:%v, want false:nil", isGCE, ip)
+	if ip, err := gceTest(baseURL + "/404"); err == nil || ip != nil {
+		t.Errorf("expected error, but not got nil")
 	}
-	if isGCE, ip := googleComputeEngineTest(baseURL + "/200_not_gce"); isGCE != false || ip != nil {
-		t.Errorf("Unexpected result. Got %v:%v, want false:nil", isGCE, ip)
+	if ip, err := gceTest(baseURL + "/200_not_gce"); err == nil || ip != nil {
+		t.Errorf("expected error, but not got nil")
 	}
-	if isGCE, ip := googleComputeEngineTest(baseURL + "/gce_no_ip"); isGCE != true || ip != nil {
-		t.Errorf("Unexpected result. Got %v:%v, want true:nil", isGCE, ip)
+	if ip, err := gceTest(baseURL + "/gce_no_ip"); err != nil || ip != nil {
+		t.Errorf("Unexpected result. Got (%v, %v), want nil:nil", ip, err)
 	}
-	if isGCE, ip := googleComputeEngineTest(baseURL + "/gce_with_ip"); isGCE != true || ip.String() != "1.2.3.4" {
-		t.Errorf("Unexpected result. Got %v:%v, want true:1.2.3.4", isGCE, ip)
+	if ip, err := gceTest(baseURL + "/gce_with_ip"); err != nil || ip.String() != "1.2.3.4" {
+		t.Errorf("Unexpected result. Got (%v, %v), want nil:1.2.3.4", ip, err)
 	}
 }
diff --git a/profiles/net/gce_linux.go b/profiles/net/gce_linux.go
new file mode 100644
index 0000000..785d38e
--- /dev/null
+++ b/profiles/net/gce_linux.go
@@ -0,0 +1,40 @@
+// +build linux
+
+package net
+
+import (
+	"net"
+
+	"veyron2"
+
+	"veyron/profiles/internal/gce"
+)
+
+func handleGCE(rt veyron2.Runtime, publisher *config.Publisher) bool {
+	log := rt.Logger()
+	if gce.RunningOnGCE() {
+		var pub net.IP
+		pub = publish_addr.IP
+		if pub == nil {
+			// Determine the IP address from GCE's metadata
+			if ip, err := gce.ExternalIPAddress(); err != nil {
+				log.Infof("failed to query GCE metadata: %s", err)
+			} else {
+				// 1:1 NAT case, our network config will not change.
+				pub = ip
+			}
+		}
+		if pub == nil {
+			log.Infof("failed to determine public IP address to publish with")
+		}
+
+		ch := make(chan config.Setting)
+		defer close(ch)
+		if _, err := publisher.CreateStream(StreamName, "network configuration", ch); err != nil {
+			return nil, nil, err
+		}
+		publishInitialSettings(ch, listenProtocol, listenSpec, prevAddr)
+		return true
+	}
+	return false
+}
diff --git a/profiles/net/gce_other.go b/profiles/net/gce_other.go
new file mode 100644
index 0000000..1abfc5c
--- /dev/null
+++ b/profiles/net/gce_other.go
@@ -0,0 +1,12 @@
+// +build !linux
+
+package net
+
+import (
+	"veyron2"
+	"veyron2/config"
+)
+
+func handleGCE(veyron2.Runtime, *config.Publisher) bool {
+	return false
+}
diff --git a/profiles/net/init.go b/profiles/net/init.go
new file mode 100644
index 0000000..bad1750
--- /dev/null
+++ b/profiles/net/init.go
@@ -0,0 +1,162 @@
+// +build linux darwin
+
+// Package net provides a network-aware Profile that provides appropriate
+// options and configuration for a variety of network configurations, including
+// being behind 1-1 NATs, using dhcp and auto-configuration for being on
+// Google Compute Engine.
+//
+// The config.Publisher mechanism is used for communicating networking
+// settings to other components of the application. The Settings stream
+// is called "net" (net.StreamName) and the Settings sent over it are
+// those defined by the ipc package.
+// TODO(cnicolaou): define the settings in the ipc package, not here.
+package net
+
+import (
+	"flag"
+	"net"
+
+	"veyron2"
+	"veyron2/config"
+	"veyron2/rt"
+
+	"veyron/profiles"
+
+	// TODO(cnicolaou): move this to profiles/internal
+	"veyron/runtimes/google/lib/netconfig"
+)
+
+const (
+	StreamName = "net"
+
+	// TODO(cnicolaou): these will eventually be defined in the veyron2/ipc
+	// package.
+	ProtocolSetting          = "Protocol"
+	ListenSpecSetting        = "ListenSpec"
+	AddPublishAddressSetting = "AddPublishAddr"
+	RmPublishAddressSetting  = "RmPublishAddr"
+)
+
+var (
+	listen_protocol string
+	listen_addr     config.IPFlag
+)
+
+func init() {
+	flag.StringVar(&listen_protocol, "veyron.protocol", "tcp4", "protocol to listen with")
+	flag.Var(&listen_addr, "veyron.address", "address to listen on")
+	rt.RegisterProfile(&profile{})
+}
+
+type profile struct{}
+
+func (p *profile) Platform() *veyron2.Platform {
+	platform, _ := profiles.Platform()
+	return platform
+}
+
+func (p *profile) Name() string {
+	return "net"
+}
+
+func (p *profile) Runtime() string {
+	return ""
+}
+
+func (p *profile) String() string {
+	return "net " + p.Platform().String()
+}
+
+func (p *profile) Init(rt veyron2.Runtime, publisher *config.Publisher) {
+	log := rt.Logger()
+
+	// TODO(cnicolaou): figure out the correct heuristics for using IPv6.
+	_, _, first := firstUsableIPv4()
+	if first == nil {
+		log.Infof("failed to find any usable IP addresses at startup")
+	}
+	public := publicIP(first)
+
+	// We now know that there is an IP address to listen on, and whether
+	// it's public or private.
+
+	// Our address is private, so we test for running on GCE
+	// and for its 1:1 NAT configuration. handleGCE returns true
+	// if we are indeed running on GCE.
+	if !public && handleGCE(rt, publisher) {
+		return
+	}
+
+	// Create stream in Init function to avoid a race between any
+	// goroutines started here and consumers started after Init returns.
+	ch := make(chan config.Setting)
+	stop, err := publisher.CreateStream(StreamName, "network configuration", ch)
+	if err != nil {
+		log.Errorf("failed to create publisher: %s", err)
+		return
+	}
+	go monitorAndPublishNetworkSettings(rt, stop, ch, listen_protocol, listen_addr.IP.String())
+}
+
+func publishInitialSettings(ch chan<- config.Setting, protocol, listenSpec string, addr net.IP) {
+	for _, setting := range []config.Setting{
+		config.NewString(ProtocolSetting, "protocol to listen with", protocol),
+		config.NewString(ListenSpecSetting, "address spec to listen on", listenSpec),
+		config.NewIP(AddPublishAddressSetting, "address to publish", addr),
+	} {
+		ch <- setting
+	}
+}
+
+// monitorNetworkSettings will publish initial Settings and then
+// monitor network configuration changes and publish subsequent
+// Settings to reflect any changes detected. It will never publish an
+// RmPublishAddressSetting without first sending an AddPublishAddressSetting.
+func monitorAndPublishNetworkSettings(rt veyron2.Runtime, stop <-chan struct{},
+	ch chan<- config.Setting,
+	listenProtocol string, listenSpec string) {
+	defer close(ch)
+
+	log := rt.Logger()
+	prev4, _, prevAddr := firstUsableIPv4()
+	// prevAddr may be nil if we are currently offline.
+
+	publishInitialSettings(ch, listenProtocol, listenSpec, prevAddr)
+
+	// Start the dhcp watcher.
+	watcher, err := netconfig.NewNetConfigWatcher()
+	if err != nil {
+		log.VI(1).Infof("Failed to get new config watcher: %s", err)
+		<-stop
+		return
+	}
+
+	for {
+		select {
+		case <-watcher.Channel():
+			cur4, _, _ := ipState()
+			added := findAdded(prev4, cur4)
+			ifc, newAddr := added.first()
+			log.VI(1).Infof("new address found: %s:%s", ifc, newAddr)
+			removed := findRemoved(prev4, cur4)
+			if prevAddr == nil || (removed.has(prevAddr) && newAddr != nil) {
+				log.VI(1).Infof("address change from %s to %s:%s",
+					prevAddr, ifc, newAddr)
+				ch <- config.NewIP(AddPublishAddressSetting, "new dhcp address to publish", newAddr)
+				ch <- config.NewIP(RmPublishAddressSetting, "remove address", prevAddr)
+				prevAddr = newAddr
+			}
+		case <-stop:
+			return
+		}
+	}
+}
+
+func firstUsableIPv4() (ipAndIf, string, net.IP) {
+	v4, _, _ := publicIPState()
+	if v4.empty() {
+		v4, _, _ = ipState()
+	}
+	ifc, first := v4.first()
+	return v4, ifc, first
+}
diff --git a/profiles/net/main.go b/profiles/net/main.go
new file mode 100644
index 0000000..cf99fc1
--- /dev/null
+++ b/profiles/net/main.go
@@ -0,0 +1,30 @@
+// +build ignore
+
+package main
+
+import (
+	"fmt"
+
+	"veyron2/config"
+	"veyron2/rt"
+
+	"veyron/profiles/net"
+)
+
+func main() {
+	r := rt.Init()
+	defer r.Cleanup()
+
+	ch := make(chan config.Setting, 10)
+	p := r.Publisher()
+	settings, err := p.ForkStream(net.StreamName, ch)
+	if err != nil {
+		r.Logger().Infof("failed to fork stream: %s", err)
+	}
+	for _, setting := range settings.Latest {
+		fmt.Println("Setting: ", setting)
+	}
+	for setting := range ch {
+		fmt.Println("Setting: ", setting)
+	}
+}
diff --git a/profiles/net/net_test.go b/profiles/net/net_test.go
new file mode 100644
index 0000000..ef862dc
--- /dev/null
+++ b/profiles/net/net_test.go
@@ -0,0 +1,129 @@
+package net
+
+import (
+	"fmt"
+	"net"
+	"reflect"
+	"testing"
+)
+
+// TODO(cnicolaou): more unit tests for:
+// Monitor/Publish network settings
+// PublicIPState
+// Empty
+// Has
+// First
+// etc.
+
+func exampleIPState() {
+	ip4, ip6, err := ipState()
+	if err != nil {
+		panic("failed to find any network interfaces!")
+	}
+	for ifc, addrs := range ip4 {
+		for _, addr := range addrs {
+			fmt.Printf("ipv4: %s: %s", ifc, addr)
+		}
+	}
+	for ifc, addrs := range ip6 {
+		for _, addr := range addrs {
+			fmt.Printf("ipv6: %s: %s", ifc, addr)
+		}
+	}
+}
+
+var (
+	a  = net.ParseIP("1.2.3.4")
+	b  = net.ParseIP("1.2.3.5")
+	c  = net.ParseIP("1.2.3.6")
+	d  = net.ParseIP("1.2.3.7")
+	a6 = net.ParseIP("2001:4860:0:2001::68")
+	b6 = net.ParseIP("2001:4860:0:2001::69")
+	c6 = net.ParseIP("2001:4860:0:2001::70")
+	d6 = net.ParseIP("2001:4860:0:2001::71")
+)
+
+func TestRemoved(t *testing.T) {
+	aif := make(ipAndIf)
+	bif := make(ipAndIf)
+	aif["eth0"] = []net.IP{a, b, c, a6, b6, c6}
+	aif["eth1"] = []net.IP{a, b, c, a6, b6, c6}
+
+	// no changes.
+	got, want := findRemoved(aif, aif), ipAndIf{}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+
+	// missing interfaces
+	got, want = findRemoved(aif, bif), ipAndIf{"eth0": nil, "eth1": nil}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+
+	// missing some addresses on a single interface
+	bif["eth0"] = []net.IP{a, b, a6, b6}
+	got, want = findRemoved(aif, bif), ipAndIf{
+		"eth0": []net.IP{c, c6},
+		"eth1": nil,
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+
+	// change an address on a single interface
+	bif["eth0"] = []net.IP{a, b, c, a6, b6, c6}
+	bif["eth1"] = []net.IP{a, b, c, a6, b6, c6}
+	bif["eth1"][2] = d
+	got, want = findRemoved(aif, bif), ipAndIf{
+		"eth1": []net.IP{c},
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+
+	// additions
+	bif["eth0"] = []net.IP{a, b, c, a6, b6, c6, d6}
+	bif["eth1"] = []net.IP{a, b, c, a6, b6, c6, d6}
+	got, want = findRemoved(aif, bif), ipAndIf{}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+}
+
+func TestAdded(t *testing.T) {
+	aif := make(ipAndIf)
+	bif := make(ipAndIf)
+	aif["eth0"] = []net.IP{a, b, c, a6, b6, c6}
+
+	// no changes.
+	got, want := findAdded(aif, aif), ipAndIf{}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+
+	// added interface
+	bif["eth0"] = []net.IP{a, b, c, a6, b6, c6}
+	bif["eth1"] = []net.IP{a, b, c, a6, b6, c6}
+	got, want = findAdded(aif, bif), ipAndIf{
+		"eth1": []net.IP{a, b, c, a6, b6, c6}}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+
+	// added some address on a single interface
+	bif["eth0"] = []net.IP{a, b, c, d, a6, b6, c6, d6}
+	delete(bif, "eth1")
+	got, want = findAdded(aif, bif), ipAndIf{
+		"eth0": []net.IP{d, d6}}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+
+	// removals
+	bif["eth0"] = []net.IP{a, b, c, b6}
+	got, want = findAdded(aif, bif), ipAndIf{}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %s, want %s", got, want)
+	}
+}
diff --git a/profiles/net/util.go b/profiles/net/util.go
new file mode 100644
index 0000000..cee7e63
--- /dev/null
+++ b/profiles/net/util.go
@@ -0,0 +1,154 @@
+package net
+
+import (
+	"fmt"
+	"net"
+
+	// TODO(cnicolaou): move this out of the runtimes and into here.
+	"veyron/runtimes/google/lib/netconfig"
+)
+
+// ipAndIf is a map of interface name to the set of IP addresses available on
+// that interface.
+type ipAndIf map[string][]net.IP
+
+func (ifcs ipAndIf) String() string {
+	r := ""
+	for k, v := range ifcs {
+		r += fmt.Sprintf("(%v: %v)", k, v)
+	}
+	return r
+}
+
+// empty returns true if there are no addresses on any interfaces.
+func (ifcs ipAndIf) empty() bool {
+	for _, addrs := range ifcs {
+		if len(addrs) > 0 {
+			return false
+		}
+	}
+	return true
+}
+
+func (ifcs ipAndIf) first() (string, net.IP) {
+	for ifc, addrs := range ifcs {
+		if len(addrs) > 0 {
+			return ifc, addrs[0]
+		}
+	}
+	return "", nil
+}
+
+func (ifcs ipAndIf) has(ip net.IP) bool {
+	for _, addrs := range ifcs {
+		for _, a := range addrs {
+			if a.Equal(ip) {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// ipState returns the set of IPv4 and IPv6 addresses available to the running
+// process.
+func ipState() (ip4, ip6 ipAndIf, err error) {
+	interfaces, err := net.Interfaces()
+	if err != nil {
+		return nil, nil, err
+	}
+	ip4 = make(ipAndIf)
+	ip6 = make(ipAndIf)
+	for _, ifc := range interfaces {
+		addrs, err := ifc.Addrs()
+		if err != nil {
+			continue
+		}
+		for _, addr := range addrs {
+			if _, ok := addr.(*net.IPAddr); ok {
+				continue
+			}
+			ip := net.ParseIP(addr.String())
+			if ip == nil || ip.IsLoopback() {
+				continue
+			}
+			if t := ip.To4(); t != nil {
+				ip4[ifc.Name] = append(ip4[ifc.Name], ip)
+			}
+			if t := ip.To16(); t != nil {
+				ip6[ifc.Name] = append(ip6[ifc.Name], ip)
+			}
+		}
+	}
+	return ip4, ip6, nil
+}
+
+func filterPublic(ips ipAndIf) ipAndIf {
+	public := make(ipAndIf)
+	for ifc, addrs := range ips {
+		for _, a := range addrs {
+			if publicIP(a) {
+				public[ifc] = append(public[ifc], a)
+			}
+		}
+	}
+	return public
+}
+
+// publicIPState returns the set of IPv4 and IPv6 addresses available
+// to the running process that are publicly/globally routable.
+func publicIPState() (ipAndIf, ipAndIf, error) {
+	v4, v6, err := ipState()
+	if err != nil {
+		return nil, nil, err
+	}
+	return filterPublic(v4), filterPublic(v6), nil
+}
+
+// publicIP returns true if the supplied IP address is publicly/gobally
+// routable.
+func publicIP(ip net.IP) bool {
+	if ip == nil {
+		return false
+	}
+	return netconfig.IsGloballyRoutable(ip)
+}
+
+func diffAB(a, b ipAndIf, added bool) ipAndIf {
+	diff := make(ipAndIf)
+	for ak, av := range a {
+		if b[ak] == nil {
+			if added {
+				diff[ak] = av
+			} else {
+				diff[ak] = nil
+			}
+			continue
+		}
+		for _, v := range av {
+			found := false
+			for _, bv := range b[ak] {
+				if v.Equal(bv) {
+					found = true
+					break
+				}
+			}
+			if !found {
+				diff[ak] = append(diff[ak], v)
+			}
+		}
+	}
+	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 ipAndIf) ipAndIf {
+	return diffAB(b, a, true)
+}
+
+// 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 ipAndIf) ipAndIf {
+	return diffAB(a, b, false)
+}
diff --git a/runtimes/google/rt/platform_darwin.go b/profiles/platform_darwin.go
similarity index 86%
rename from runtimes/google/rt/platform_darwin.go
rename to profiles/platform_darwin.go
index 3e47627..55e39a3 100644
--- a/runtimes/google/rt/platform_darwin.go
+++ b/profiles/platform_darwin.go
@@ -1,9 +1,6 @@
 // +build darwin
 
-package rt
-
-// TODO(cnicolaou): these routines should be moved out to a library so
-// that they're usable by all profile implementations.
+package profiles
 
 // #include <sys/utsname.h>
 // #include <errno.h>
diff --git a/runtimes/google/rt/platform_linux.go b/profiles/platform_linux.go
similarity index 97%
rename from runtimes/google/rt/platform_linux.go
rename to profiles/platform_linux.go
index 0d2a322..b38e12c 100644
--- a/runtimes/google/rt/platform_linux.go
+++ b/profiles/platform_linux.go
@@ -1,6 +1,6 @@
 // +build linux
 
-package rt
+package profiles
 
 import (
 	"fmt"
diff --git a/profiles/profiles_test.go b/profiles/profiles_test.go
new file mode 100644
index 0000000..5a67c69
--- /dev/null
+++ b/profiles/profiles_test.go
@@ -0,0 +1,20 @@
+package profiles_test
+
+import (
+	"os"
+	"testing"
+
+	"veyron/profiles"
+)
+
+func TestGeneric(t *testing.T) {
+	p := profiles.New()
+
+	if got, want := p.Name(), "generic"; got != want {
+		t.Errorf("got %q, want %q", got, want)
+	}
+	hostname, _ := os.Hostname()
+	if got, want := p.Platform().Node, hostname; got != want {
+		t.Errorf("got %q, want %q", got, want)
+	}
+}
diff --git a/runtimes/google/rt/profile.go b/runtimes/google/rt/profile.go
deleted file mode 100644
index 33accf8..0000000
--- a/runtimes/google/rt/profile.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package rt
-
-import (
-	"veyron2"
-	"veyron2/config"
-)
-
-type generic struct{}
-
-func (g *generic) Name() string {
-	return "generic"
-}
-
-func (g *generic) Runtime() string {
-	return veyron2.GoogleRuntimeName
-}
-
-func (g *generic) Platform() *veyron2.Platform {
-	p, _ := Platform()
-	return p
-}
-
-func (g *generic) Init(rt veyron2.Runtime, _ *config.Publisher) {
-	rt.Logger().VI(1).Infof("%s\n", g.String())
-}
-
-func (g *generic) String() string {
-	return "default generic on " + g.Platform().String()
-}
diff --git a/runtimes/google/rt/rt.go b/runtimes/google/rt/rt.go
index 01b8e85..717e05c 100644
--- a/runtimes/google/rt/rt.go
+++ b/runtimes/google/rt/rt.go
@@ -15,6 +15,7 @@
 	"veyron2/security"
 	"veyron2/vlog"
 
+	"veyron/profiles"
 	"veyron/runtimes/google/naming/namespace"
 	"veyron/services/mgmt/lib/exec"
 )
@@ -66,7 +67,7 @@
 	rt.initSignalHandling()
 
 	if rt.profile == nil {
-		rt.profile = &generic{}
+		rt.profile = profiles.New()
 	}
 	vlog.VI(1).Infof("Using profile %q", rt.profile.Name())
 
diff --git a/runtimes/google/rt/uts_str_linux_arm.go b/runtimes/google/rt/uts_str_linux_arm.go
deleted file mode 100644
index a3cee4e..0000000
--- a/runtimes/google/rt/uts_str_linux_arm.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// +build linux,arm
-
-package rt
-
-// str converts the input byte slice to a string, ignoring everything following
-// a null character (including the null character).
-func utsStr(c []uint8) string {
-	ret := make([]byte, 0, len(c))
-	for _, v := range c {
-		if v == 0 {
-			break
-		}
-		ret = append(ret, byte(v))
-	}
-	return string(ret)
-}
diff --git a/runtimes/google/rt/uts_str_linux_nonarm.go b/runtimes/google/rt/uts_str_linux_nonarm.go
deleted file mode 100644
index 02b8f81..0000000
--- a/runtimes/google/rt/uts_str_linux_nonarm.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// +build linux,!arm
-
-package rt
-
-// str converts the input byte slice to a string, ignoring everything following
-// a null character (including the null character).
-func utsStr(c []int8) string {
-	ret := make([]byte, 0, len(c))
-	for _, v := range c {
-		if v == 0 {
-			break
-		}
-		ret = append(ret, byte(v))
-	}
-	return string(ret)
-}