Repository: scionproto/scion
Pull request #3427 (2019-11-25 14:59:47)

Simplify topology.TopoAddr, remove topology.TopoBRAddr
Topology file format stays the same.

Also:
- add documentation for how topology file contents are interpreted
by Go libraries;
- add strict checks for features that are not supported;
- explicitly disable IPv4+IPv6 dual stacked behavior for now; it was
never used nor tested anyway, and supporting it would have needed
signifcant changes to all existing applications;
- remove bind logic for SCION sockets because it was never used;
- use net.UDPAddr instead of TopoBRAddr;

Contributes https://github.com/scionproto/scion/issues/3346.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/3427)
<!-- Reviewable:end -->



Patch:
diff --git a/go/beacon_srv/internal/beaconing/originator.go b/go/beacon_srv/internal/beaconing/originator.go
index e76871294..fb8fdab65 100644
--- a/go/beacon_srv/internal/beaconing/originator.go
+++ b/go/beacon_srv/internal/beaconing/originator.go
@@ -164,8 +164,6 @@ func (o *beaconOriginator) originateBeacon(ctx context.Context) error {
 		return serrors.New("Interface does not exist")
 	}
 	topoInfo := intf.TopoInfo()
-	ov := topoInfo.InternalAddrs.PublicOverlayUDP(topoInfo.InternalAddrs.Overlay)
-
 	bseg, err := o.createBeacon()
 	if err != nil {
 		metrics.Originator.Beacons(labels.WithResult(metrics.ErrCreate)).Inc()
@@ -178,7 +176,7 @@ func (o *beaconOriginator) originateBeacon(ctx context.Context) error {
 		topoInfo.ISD_AS,
 		o.ifID,
 		o.cfg.Signer,
-		ov,
+		topoInfo.InternalAddrs,
 	)
 	if err != nil {
 		metrics.Originator.Beacons(labels.WithResult(metrics.ErrSend)).Inc()
diff --git a/go/beacon_srv/internal/beaconing/propagator.go b/go/beacon_srv/internal/beaconing/propagator.go
index 88748d864..ef0c73888 100644
--- a/go/beacon_srv/internal/beaconing/propagator.go
+++ b/go/beacon_srv/internal/beaconing/propagator.go
@@ -277,15 +277,13 @@ func (p *beaconPropagator) extendAndSend(ctx context.Context, bseg beacon.Beacon
 			return
 		}
 		topoInfo := intf.TopoInfo()
-		ov := topoInfo.InternalAddrs.PublicOverlayUDP(topoInfo.InternalAddrs.Overlay)
-
 		err := p.beaconSender.Send(
 			ctx,
 			&seg.Beacon{Segment: bseg.Segment},
 			topoInfo.ISD_AS,
 			egIfid,
 			p.cfg.Signer,
-			ov,
+			topoInfo.InternalAddrs,
 		)
 		if err != nil {
 			p.logger.Error("[beaconing.Propagator] Unable to send packet",
diff --git a/go/beacon_srv/internal/keepalive/sender.go b/go/beacon_srv/internal/keepalive/sender.go
index 38f9bd23b..0d4ee1bfb 100644
--- a/go/beacon_srv/internal/keepalive/sender.go
+++ b/go/beacon_srv/internal/keepalive/sender.go
@@ -71,8 +71,7 @@ func (s *Sender) Run(ctx context.Context) {
 			InfoTime: time.Now(),
 			Pld:      pld,
 		}
-		ov := intf.InternalAddrs.PublicOverlayUDP(intf.InternalAddrs.Overlay)
-		if err := s.Send(msg, ov); err != nil {
+		if err := s.Send(msg, intf.InternalAddrs); err != nil {
 			logger.Error("[keepalive.Sender] Unable to send packet", "err", err)
 			metrics.Keepalive.Transmits(l).Inc()
 			continue
diff --git a/go/beacon_srv/main.go b/go/beacon_srv/main.go
index 17a6b9a84..945fee44e 100644
--- a/go/beacon_srv/main.go
+++ b/go/beacon_srv/main.go
@@ -133,7 +133,6 @@ func realMain() int {
 	nc := infraenv.NetworkConfig{
 		IA:                    topo.IA(),
 		Public:                topo.SPublicAddress(addr.SvcBS, cfg.General.ID),
-		Bind:                  topo.SBindAddress(addr.SvcBS, cfg.General.ID),
 		SVC:                   addr.SvcBS,
 		ReconnectToDispatcher: cfg.General.ReconnectToDispatcher,
 		QUIC: infraenv.QUIC{
diff --git a/go/border/BUILD.bazel b/go/border/BUILD.bazel
index f75556ef9..b401b7f85 100644
--- a/go/border/BUILD.bazel
+++ b/go/border/BUILD.bazel
@@ -72,7 +72,6 @@ go_test(
         "//go/lib/prom:go_default_library",
         "//go/lib/ringbuf:go_default_library",
         "//go/lib/topology:go_default_library",
-        "//go/lib/topology/overlay:go_default_library",
         "//go/lib/xtest:go_default_library",
         "@com_github_golang_mock//gomock:go_default_library",
         "@com_github_smartystreets_goconvey//convey:go_default_library",
diff --git a/go/border/rctrl/ctrl.go b/go/border/rctrl/ctrl.go
index 574c17e88..84ea47a85 100644
--- a/go/border/rctrl/ctrl.go
+++ b/go/border/rctrl/ctrl.go
@@ -57,12 +57,8 @@ func Control(sRevInfoQ chan rpkt.RawSRevCallbackArgs, dispatcherReconnect bool)
 		},
 	)
 	ctrlAddr := ctx.Conf.BR.CtrlAddrs
-	pub := &snet.Addr{IA: ia, Host: ctrlAddr.IPv4.PublicAddr()}
-	bind := &snet.Addr{IA: ia, Host: ctrlAddr.IPv4.BindAddr()}
-	if bind.Host == nil {
-		bind = nil
-	}
-	snetConn, err = scionNetwork.ListenSCIONWithBindSVC("udp4", pub, bind, addr.SvcNone, 0)
+	pub := &snet.Addr{IA: ia, Host: ctrlAddr.SCIONAddress}
+	snetConn, err = scionNetwork.ListenSCIONWithBindSVC("udp4", pub, nil, addr.SvcNone, 0)
 	if err != nil {
 		fatal.Fatal(common.NewBasicError("Listening on address", err, "addr", ctrlAddr))
 	}
diff --git a/go/border/rpkt/create.go b/go/border/rpkt/create.go
index d34e0896a..15f6ac865 100644
--- a/go/border/rpkt/create.go
+++ b/go/border/rpkt/create.go
@@ -179,7 +179,7 @@ func (rp *RtrPkt) CreateReplyScnPkt() (*spkt.ScnPkt, error) {
 	}
 	sp.SrcIA = rp.Ctx.Conf.IA
 	// Use the local address as the source host
-	pub := rp.Ctx.Conf.BR.InternalAddrs.PublicOverlayUDP(rp.Ctx.Conf.Topo.Overlay())
+	pub := rp.Ctx.Conf.BR.InternalAddrs
 	sp.SrcHost = addr.HostFromIP(pub.IP)
 	return sp, nil
 }
diff --git a/go/border/rpkt/route.go b/go/border/rpkt/route.go
index fe5c1ae2d..bb5b6e8bc 100644
--- a/go/border/rpkt/route.go
+++ b/go/border/rpkt/route.go
@@ -144,8 +144,7 @@ func (rp *RtrPkt) forwardFromExternal() (HookResult, error) {
 		return rp.reprocess()
 	}
 	nextBR := rp.Ctx.Conf.Topo.IFInfoMap()[*rp.ifNext]
-	dst := nextBR.InternalAddrs.PublicOverlayUDP(rp.Ctx.Conf.Topo.Overlay())
-	rp.Egress = append(rp.Egress, EgressPair{S: rp.Ctx.LocSockOut, Dst: dst})
+	rp.Egress = append(rp.Egress, EgressPair{S: rp.Ctx.LocSockOut, Dst: nextBR.InternalAddrs})
 	return HookContinue, nil
 }
 
diff --git a/go/border/setup-posix.go b/go/border/setup-posix.go
index 04b653e64..f5e341a7c 100644
--- a/go/border/setup-posix.go
+++ b/go/border/setup-posix.go
@@ -17,7 +17,6 @@ package main
 
 import (
 	"fmt"
-	"net"
 
 	"github.com/scionproto/scion/go/border/brconf"
 	"github.com/scionproto/scion/go/border/rcmn"
@@ -46,7 +45,7 @@ func (p posixLoc) Setup(r *Router, ctx *rctx.Ctx, oldCtx *rctx.Ctx) error {
 	// Check if the existing socket can be reused. On startup, oldCtx is nil.
 	if oldCtx != nil {
 		// The socket can be reused if the local address does not change.
-		if ctx.Conf.BR.InternalAddrs.Equal(oldCtx.Conf.BR.InternalAddrs) {
+		if ctx.Conf.BR.InternalAddrs.String() == oldCtx.Conf.BR.InternalAddrs.String() {
 			log.Trace("No change detected for local socket.")
 			// Nothing changed. Copy I/O functions from old context.
 			ctx.LocSockIn = oldCtx.LocSockIn
@@ -64,7 +63,8 @@ func (p posixLoc) Setup(r *Router, ctx *rctx.Ctx, oldCtx *rctx.Ctx) error {
 func (p posixLoc) Rollback(r *Router, ctx *rctx.Ctx, oldCtx *rctx.Ctx) error {
 
 	// Do nothing if socket is reused.
-	if oldCtx != nil && ctx.Conf.BR.InternalAddrs.Equal(oldCtx.Conf.BR.InternalAddrs) {
+	if oldCtx != nil &&
+		ctx.Conf.BR.InternalAddrs.String() == oldCtx.Conf.BR.InternalAddrs.String() {
 		return nil
 	}
 	// Remove new socket if it exists. It might not be set if the setup failed.
@@ -83,11 +83,7 @@ func (p posixLoc) Rollback(r *Router, ctx *rctx.Ctx, oldCtx *rctx.Ctx) error {
 
 func (p posixLoc) addSock(r *Router, ctx *rctx.Ctx) error {
 	// Get Bind address if set, Public otherwise
-	bindOv := ctx.Conf.BR.InternalAddrs.BindOrPublicOverlay(ctx.Conf.Topo.Overlay())
-	var bind *net.UDPAddr
-	if bindOv != nil {
-		bind = bindOv.ToUDPAddr()
-	}
+	bind := ctx.Conf.BR.InternalAddrs
 	log.Debug("Setting up new local socket.", "bind", bind)
 	// Listen on the socket.
 	over, err := conn.New(bind, nil, nil)
@@ -116,8 +112,7 @@ func (p posixExt) Setup(r *Router, ctx *rctx.Ctx, intf *topology.IFInfo, oldCtx
 	if oldIntf, ok := oldCtx.Conf.BR.IFs[intf.Id]; ok {
 		// Reuse socket if the interface has not changed.
 		if !interfaceChanged(intf, oldIntf) {
-			log.Trace("No change detected for external socket.", "conn",
-				intf.Local.BindOrPublicOverlay(ctx.Conf.Topo.Overlay()))
+			log.Trace("No change detected for external socket.", "conn", intf.Local)
 			ctx.ExtSockIn[intf.Id] = oldCtx.ExtSockIn[intf.Id]
 			ctx.ExtSockOut[intf.Id] = oldCtx.ExtSockOut[intf.Id]
 			return nil
@@ -158,17 +153,7 @@ func (p posixExt) addIntf(r *Router, ctx *rctx.Ctx, intf *topology.IFInfo) error
 
 	// Connect to remote address.
 	log.Debug("Setting up new external socket.", "intf", intf)
-	bindOv := intf.Local.BindOrPublicOverlay(intf.Local.Overlay)
-	var bind *net.UDPAddr
-	if bindOv != nil {
-		bind = bindOv.ToUDPAddr()
-	}
-	remoteOv := intf.Remote
-	var remote *net.UDPAddr
-	if remoteOv != nil {
-		remote = remoteOv.ToUDPAddr()
-	}
-	c, err := conn.New(bind, remote, nil)
+	c, err := conn.New(intf.Local, intf.Remote, nil)
 	if err != nil {
 		return common.NewBasicError("Unable to listen on external socket", err)
 	}
@@ -197,7 +182,7 @@ func (p posixExt) Teardown(r *Router, ctx *rctx.Ctx, intf *topology.IFInfo, oldC
 // interfaceChanged returns true if a new input goroutine is needed for the
 // corresponding interface.
 func interfaceChanged(newIntf *topology.IFInfo, oldIntf *topology.IFInfo) bool {
-	return (newIntf.Id != oldIntf.Id ||
-		!newIntf.Local.Equal(oldIntf.Local) ||
-		!newIntf.Remote.Equal(oldIntf.Remote))
+	return newIntf.Id != oldIntf.Id ||
+		newIntf.Local.String() != oldIntf.Local.String() ||
+		newIntf.Remote.String() != oldIntf.Remote.String()
 }
diff --git a/go/border/setup.go b/go/border/setup.go
index 20e2e19aa..0eda34ddb 100644
--- a/go/border/setup.go
+++ b/go/border/setup.go
@@ -305,8 +305,7 @@ func (r *Router) startDiscovery() error {
 // discoveryClient returns a client with the source address set to the internal address.
 func (r *Router) discoveryClient() (*http.Client, error) {
 	internalAddr := rctx.Get().Conf.BR.InternalAddrs
-	tcpAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0",
-		internalAddr.PublicOverlayUDP(internalAddr.Overlay).IP))
+	tcpAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", internalAddr.IP))
 	if err != nil {
 		return nil, err
 	}
@@ -384,18 +383,18 @@ func validateCtx(ctx, oldCtx *rctx.Ctx, sockConf brconf.SockConf) error {
 		}
 
 		// Validate interface does not take over local address.
-		if intf.Local.Equal(oldCtx.Conf.BR.InternalAddrs) {
+		if intf.Local.String() == oldCtx.Conf.BR.InternalAddrs.String() {
 			return common.NewBasicError("Address must not switch from local", nil,
 				"intf", intf, "locAddr", oldCtx.Conf.BR.InternalAddrs)
 		}
 		for _, oldIntf := range oldCtx.Conf.BR.IFs {
 			// Validate interface does not take over the address of old interface.
-			if intf.Local.Equal(oldIntf.Local) && intf.Id != oldIntf.Id {
+			if intf.Local.String() == oldIntf.Local.String() && intf.Id != oldIntf.Id {
 				return common.NewBasicError("Address must not switch interface", nil,
 					"intf", intf, "oldIntf", oldIntf)
 			}
 			// Validate local sock does not take over the address of old interface.
-			if ctx.Conf.BR.InternalAddrs.Equal(oldIntf.Local) {
+			if ctx.Conf.BR.InternalAddrs.String() == oldIntf.Local.String() {
 				return common.NewBasicError("Address must not switch to local", nil,
 					"oldIntf", intf, "locAddr", oldCtx.Conf.BR.InternalAddrs)
 			}
diff --git a/go/cert_srv/setup.go b/go/cert_srv/setup.go
index 32397e523..aea8cc50e 100644
--- a/go/cert_srv/setup.go
+++ b/go/cert_srv/setup.go
@@ -165,7 +165,6 @@ func setMessenger(cfg *config.Config, router snet.Router) error {
 	nc := infraenv.NetworkConfig{
 		IA:                    topo.IA(),
 		Public:                topo.SPublicAddress(addr.SvcCS, cfg.General.ID),
-		Bind:                  topo.SBindAddress(addr.SvcCS, cfg.General.ID),
 		SVC:                   addr.SvcCS,
 		ReconnectToDispatcher: cfg.General.ReconnectToDispatcher,
 		QUIC: infraenv.QUIC{
diff --git a/go/lib/discovery/discoverypool/pool.go b/go/lib/discovery/discoverypool/pool.go
index e231e1292..7ce67d82f 100644
--- a/go/lib/discovery/discoverypool/pool.go
+++ b/go/lib/discovery/discoverypool/pool.go
@@ -54,10 +54,9 @@ func (p *Pool) Update(svcInfo topology.IDAddrMap) error {
 	defer p.mu.Unlock()
 	// Add missing DS servers.
 	for k, v := range svcInfo {
-		x := v.PublicAddr(v.Overlay)
 		y := &net.UDPAddr{
-			IP:   x.L3.IP(),
-			Port: int(x.L4),
+			IP:   v.SCIONAddress.L3.IP(),
+			Port: int(v.SCIONAddress.L4),
 		}
 		if info, ok := p.m[k]; !ok {
 			p.m[k] = discoveryinfo.New(k, y)
diff --git a/go/lib/healthpool/svcinstance/pool.go b/go/lib/healthpool/svcinstance/pool.go
index 37b27dbce..ac05b73f2 100644
--- a/go/lib/healthpool/svcinstance/pool.go
+++ b/go/lib/healthpool/svcinstance/pool.go
@@ -99,11 +99,11 @@ func createMap(svcInfo topology.IDAddrMap, oldInfos infoMap) infoMap {
 	for k, svc := range svcInfo {
 		if oldInfo, ok := oldInfos[k]; ok {
 			infos[k] = oldInfo
-			oldInfo.update(svc.PublicAddr(svc.Overlay))
+			oldInfo.update(svc.SCIONAddress)
 		} else {
 			infos[k] = &info{
 				Info: healthpool.NewInfo(),
-				addr: svc.PublicAddr(svc.Overlay),
+				addr: svc.SCIONAddress,
 				name: k,
 			}
 		}
diff --git a/go/lib/infra/modules/itopo/topology.go b/go/lib/infra/modules/itopo/topology.go
index 8faf5f982..111c40756 100644
--- a/go/lib/infra/modules/itopo/topology.go
+++ b/go/lib/infra/modules/itopo/topology.go
@@ -51,17 +51,6 @@ type Topology interface {
 	// Exists returns true if the service and name are present in the topology file.
 	Exists(svc addr.HostSVC, name string) bool
 
-	// BindAddress gets the bind address of a server with the requested type and name, and nil
-	// if no such server exists.
-	//
-	// FIXME(scrye): See whether this or its snet variant below can be removed.
-	BindAddress(svc addr.HostSVC, name string) *addr.AppAddr
-	// BindAddress gets the bind address of a server with the requested type and name, and nil
-	// if no such server exists.
-	//
-	// FIXME(scrye): See whether this or its app variant above can be removed.
-	SBindAddress(svc addr.HostSVC, name string) *snet.Addr
-
 	// SBRAddress returns the internal public address of the BR with the specified name.
 	SBRAddress(name string) *snet.Addr
 
@@ -191,17 +180,7 @@ func (t *topologyS) OverlayNextHop(ifid common.IFIDType) (*net.UDPAddr, bool) {
 	if !ok {
 		return nil, false
 	}
-	if ifInfo.InternalAddrs.IPv4 != nil {
-		if v4Addr := ifInfo.InternalAddrs.IPv4.PublicOverlay; v4Addr != nil {
-			return &net.UDPAddr{IP: copyIP(v4Addr.L3().IP()), Port: int(v4Addr.L4())}, true
-		}
-	}
-	if ifInfo.InternalAddrs.IPv6 != nil {
-		if v6Addr := ifInfo.InternalAddrs.IPv6.PublicOverlay; v6Addr != nil {
-			return &net.UDPAddr{IP: copyIP(v6Addr.L3().IP()), Port: int(v6Addr.L4())}, true
-		}
-	}
-	return nil, false
+	return copyUDP(ifInfo.InternalAddrs), true
 }
 
 func (t *topologyS) OverlayNextHop2(ifid common.IFIDType) (*net.UDPAddr, bool) {
@@ -209,7 +188,7 @@ func (t *topologyS) OverlayNextHop2(ifid common.IFIDType) (*net.UDPAddr, bool) {
 	if !ok {
 		return nil, false
 	}
-	return ifInfo.InternalAddrs.PublicOverlayUDP(t.Topology.Overlay), true
+	return copyUDP(ifInfo.InternalAddrs), true
 }
 
 func (t *topologyS) MakeHostInfos(st proto.ServiceType) []net.UDPAddr {
@@ -221,13 +200,13 @@ func (t *topologyS) MakeHostInfos(st proto.ServiceType) []net.UDPAddr {
 		return hostInfos
 	}
 	for _, a := range addresses {
-		if v4Addr := a.PublicAddr(overlay.IPv4); v4Addr != nil {
+		if v4Addr := a.SCIONAddress; v4Addr != nil {
 			hostInfos = append(hostInfos, net.UDPAddr{
 				IP:   copyIP(v4Addr.L3.IP()),
 				Port: int(v4Addr.L4),
 			})
 		}
-		if v6Addr := a.PublicAddr(overlay.IPv6); v6Addr != nil {
+		if v6Addr := a.SCIONAddress; v6Addr != nil {
 			hostInfos = append(hostInfos, net.UDPAddr{
 				IP:   copyIP(v6Addr.L3.IP()),
 				Port: int(v6Addr.L4),
@@ -262,7 +241,7 @@ func (t *topologyS) PublicAddress(svc addr.HostSVC, name string) *addr.AppAddr {
 	if topoAddr == nil {
 		return nil
 	}
-	publicAddr := topoAddr.PublicAddr(topoAddr.Overlay)
+	publicAddr := topoAddr.SCIONAddress
 	if publicAddr == nil {
 		return nil
 	}
@@ -273,29 +252,6 @@ func (t *topologyS) Exists(svc addr.HostSVC, name string) bool {
 	return t.PublicAddress(svc, name) != nil
 }
 
-func (t *topologyS) SBindAddress(svc addr.HostSVC, name string) *snet.Addr {
-	address := t.BindAddress(svc, name)
-	if address == nil {
-		return nil
-	}
-	return &snet.Addr{
-		IA:   t.IA(),
-		Host: address.Copy(),
-	}
-}
-
-func (t *topologyS) BindAddress(svc addr.HostSVC, name string) *addr.AppAddr {
-	topoAddr := t.topoAddress(svc, name)
-	if topoAddr == nil {
-		return nil
-	}
-	bindAddr := topoAddr.BindAddr(topoAddr.Overlay)
-	if bindAddr == nil {
-		return nil
-	}
-	return bindAddr.Copy()
-}
-
 func (t *topologyS) topoAddress(svc addr.HostSVC, name string) *topology.TopoAddr {
 	var addresses topology.IDAddrMap
 	switch svc.Base() {
@@ -326,11 +282,8 @@ func (t *topologyS) OverlayAnycast(svc addr.HostSVC) (*net.UDPAddr, error) {
 		return nil, common.NewBasicError("No instances found for SVC address",
 			scmp.NewError(scmp.C_Routing, scmp.T_R_UnreachHost, nil, nil), "svc", svc)
 	}
-	ov := topoAddr.OverlayAddrUDP(t.Topology.Overlay)
-	if ov == nil {
-		return nil, serrors.New("overlay address not found", "svc", svc)
-	}
-	return ov, nil
+	// FIXME(scrye): This should return net.Addr
+	return copyUDP(topoAddr.UnderlayAddr()), nil
 }
 
 func (t *topologyS) OverlayMulticast(svc addr.HostSVC) ([]*net.UDPAddr, error) {
@@ -354,7 +307,7 @@ func (t *topologyS) OverlayMulticast(svc addr.HostSVC) ([]*net.UDPAddr, error) {
 	// multiple times by the remote dispatcher.
 	uniqueOverlayAddrs := make(map[string]*net.UDPAddr)
 	for _, topoAddr := range topoAddrs {
-		overlayAddr := topoAddr.OverlayAddrUDP(t.Topology.Overlay)
+		overlayAddr := topoAddr.UnderlayAddr()
 		if overlayAddr == nil {
 			continue
 		}
@@ -377,7 +330,7 @@ func (t *topologyS) OverlayByName(svc addr.HostSVC, name string) (*net.UDPAddr,
 	if err != nil {
 		return nil, serrors.WrapStr("SVC not supported", err, "svc", svc)
 	}
-	overlayAddr := topoAddr.OverlayAddrUDP(t.Topology.Overlay)
+	overlayAddr := topoAddr.UnderlayAddr()
 	if overlayAddr == nil {
 		return nil, serrors.New("overlay address not found", "svc", svc)
 	}
@@ -417,8 +370,8 @@ func (t *topologyS) SBRAddress(name string) *snet.Addr {
 	}
 	return &snet.Addr{
 		IA:      t.IA(),
-		Host:    br.CtrlAddrs.PublicAddr(br.CtrlAddrs.Overlay),
-		NextHop: br.CtrlAddrs.OverlayAddrUDP(br.CtrlAddrs.Overlay),
+		Host:    br.CtrlAddrs.SCIONAddress,
+		NextHop: br.CtrlAddrs.UnderlayAddr(),
 	}
 }
 
@@ -448,3 +401,13 @@ func (t *topologyS) Raw() *topology.Topo {
 func copyIP(ip net.IP) net.IP {
 	return append(ip[:0:0], ip...)
 }
+
+func copyUDP(a *net.UDPAddr) *net.UDPAddr {
+	if a == nil {
+		return nil
+	}
+	return &net.UDPAddr{
+		IP:   copyIP(a.IP),
+		Port: a.Port,
+	}
+}
diff --git a/go/lib/infra/modules/itopo/validate.go b/go/lib/infra/modules/itopo/validate.go
index 45c622c57..fd1d21076 100644
--- a/go/lib/infra/modules/itopo/validate.go
+++ b/go/lib/infra/modules/itopo/validate.go
@@ -15,6 +15,8 @@
 package itopo
 
 import (
+	"reflect"
+
 	"github.com/google/go-cmp/cmp"
 
 	"github.com/scionproto/scion/go/lib/common"
@@ -156,7 +158,10 @@ func (v *svcValidator) Immutable(topo, oldTopo *topology.Topo) error {
 	// We already checked that the service is in the map.
 	nAddr, _ := topo.GetTopoAddr(v.id, v.svc)
 	oAddr, _ := oldTopo.GetTopoAddr(v.id, v.svc)
-	if !nAddr.Equal(oAddr) {
+	// FIXME(scrye): The equality check below is protocol specific. Use reflect.DeepEqual for now,
+	// but it's better to define what "entry must not change" actually means w.r.t. all possible
+	// internal addresses.
+	if !reflect.DeepEqual(nAddr, oAddr) {
 		return common.NewBasicError("Local service entry must not change", nil,
 			"id", v.id, "svc", v.svc, "expected", oAddr, "actual", nAddr)
 	}
@@ -190,11 +195,11 @@ func (v *brValidator) Immutable(topo, oldTopo *topology.Topo) error {
 	if err := v.generalValidator.Immutable(topo, oldTopo); err != nil {
 		return err
 	}
-	if !topo.BR[v.id].InternalAddrs.Equal(oldTopo.BR[v.id].InternalAddrs) {
+	if topo.BR[v.id].InternalAddrs.String() != oldTopo.BR[v.id].InternalAddrs.String() {
 		return common.NewBasicError("InternalAddrs is immutable", nil, "expected",
 			oldTopo.BR[v.id].InternalAddrs, "actual", topo.BR[v.id].InternalAddrs)
 	}
-	if !topo.BR[v.id].CtrlAddrs.Equal(oldTopo.BR[v.id].CtrlAddrs) {
+	if !reflect.DeepEqual(topo.BR[v.id].CtrlAddrs, oldTopo.BR[v.id].CtrlAddrs) {
 		return common.NewBasicError("CtrlAddrs is immutable", nil, "expected",
 			oldTopo.BR[v.id].CtrlAddrs, "actual", topo.BR[v.id].CtrlAddrs)
 	}
diff --git a/go/lib/infra/modules/trust/BUILD.bazel b/go/lib/infra/modules/trust/BUILD.bazel
index bd842800f..c8e2a31c2 100644
--- a/go/lib/infra/modules/trust/BUILD.bazel
+++ b/go/lib/infra/modules/trust/BUILD.bazel
@@ -64,7 +64,6 @@ go_test(
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/topology:go_default_library",
-        "//go/lib/topology/topotestutil:go_default_library",
         "//go/lib/xtest:go_default_library",
         "//go/lib/xtest/p2p:go_default_library",
         "//go/proto:go_default_library",
diff --git a/go/lib/integration/integration.go b/go/lib/integration/integration.go
index a32214a07..6885a1536 100644
--- a/go/lib/integration/integration.go
+++ b/go/lib/integration/integration.go
@@ -194,7 +194,7 @@ var DispAddr HostAddr = func(ia addr.IA) snet.Addr {
 		os.Exit(1)
 	}
 	bs := topo.BS["bs"+ia.FileFmt(false)+"-1"]
-	return snet.Addr{Host: &addr.AppAddr{L3: bs.IPv4.PublicAddr().L3}, IA: ia}
+	return snet.Addr{Host: bs.SCIONAddress, IA: ia}
 }
 
 // interface kept similar to go 1.10
diff --git a/go/lib/topology/BUILD.bazel b/go/lib/topology/BUILD.bazel
index cf8dff373..24b3b0f63 100644
--- a/go/lib/topology/BUILD.bazel
+++ b/go/lib/topology/BUILD.bazel
@@ -4,7 +4,6 @@ go_library(
     name = "go_default_library",
     srcs = [
         "addr.go",
-        "braddr.go",
         "doc.go",
         "raw.go",
         "strip.go",
diff --git a/go/lib/topology/addr.go b/go/lib/topology/addr.go
index fd4d411be..e5abae918 100644
--- a/go/lib/topology/addr.go
+++ b/go/lib/topology/addr.go
@@ -17,260 +17,66 @@ package topology
 import (
 	"fmt"
 	"net"
-	"strings"
 
 	"github.com/scionproto/scion/go/lib/addr"
-	"github.com/scionproto/scion/go/lib/common"
+	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/topology/overlay"
 )
 
-const (
-	ErrUnsupportedOverlay   common.ErrMsg = "Unsupported overlay"
-	ErrUnsupportedAddrType  common.ErrMsg = "Unsupported address type"
-	ErrInvalidPub           common.ErrMsg = "Invalid public address"
-	ErrInvalidBind          common.ErrMsg = "Invalid bind address"
-	ErrAtLeastOnePub        common.ErrMsg = "Overlay requires at least one public address"
-	ErrOverlayPort          common.ErrMsg = "Overlay port set for non-UDP overlay"
-	ErrBindAddrEqPubAddr    common.ErrMsg = "Bind address equal to Public address"
-	ErrMismatchOverlayAddr  common.ErrMsg = "Mismatch overlay type and address"
-	ErrMismatchPubAddrType  common.ErrMsg = "Mismatch public address and type "
-	ErrMismatchBindAddrType common.ErrMsg = "Mismatch bind address and type"
+var (
+	ErrAtLeastOnePub       = serrors.New("underlay requires at least one public address")
+	ErrUnsupportedUnderlay = serrors.New("unsupported underlay")
+	ErrUnsupportedAddrType = serrors.New("unsupported address type")
+	ErrInvalidPub          = serrors.New("invalid public address")
+	ErrInvalidBind         = serrors.New("invalid bind address")
+	ErrBindNotSupported    = serrors.New(
+		"bind addresses are not supported for this address type")
+	ErrCustomUnderlayPortNotSupported = serrors.New("custom underlay port not supported")
+	ErrUnderlayPort                   = serrors.New("underlay port set for non-UDP underlay")
+	ErrBindAddrEqPubAddr              = serrors.New("bind address equal to Public address")
+	ErrMismatchUnderlayAddr           = serrors.New("mismatch underlay type and address")
+	ErrMismatchPubAddrType            = serrors.New("mismatch public address and type")
+	ErrMismatchBindAddrType           = serrors.New("mismatch bind address and type")
+	ErrUnderlayAddressNotFound        = serrors.New("underlay address not found")
+	ErrExpectedIPv4FoundIPv6          = serrors.New("expected IPv4 address, but found IPv6 address")
+	ErrExpectedIPv6FoundIPv4          = serrors.New("expected IPv6 address, but found IPv4 address")
 )
 
 // TopoAddr wraps the possible addresses of a SCION service and describes
-// the overlay to be used for contacting said service.
+// the underlay to be used for contacting said service.
 type TopoAddr struct {
-	IPv4    *pubBindAddr
-	IPv6    *pubBindAddr
-	Overlay overlay.Type
+	SCIONAddress    *addr.AppAddr
+	UnderlayAddress net.Addr
 }
 
-// TestTopoAddr creates a new TopoAddr. This is only for testing and should
-// never be used by apps.
-func TestTopoAddr(v4AppAddr, v6AppAddr *addr.AppAddr,
-	v4OverlayAddr, v6OverlayAddr *overlay.OverlayAddr) TopoAddr {
-
-	return TopoAddr{
-		IPv4: &pubBindAddr{
-			pub:     v4AppAddr,
-			overlay: v4OverlayAddr,
-		},
-		IPv6: &pubBindAddr{
-			pub:     v6AppAddr,
-			overlay: v6OverlayAddr,
-		},
-		Overlay: overlay.IPv46,
-	}
-}
-
-// Create TopoAddr from RawAddrMap, depending on supplied Overlay type
-func topoAddrFromRAM(s RawAddrMap, ot overlay.Type) (*TopoAddr, error) {
-	if err := overlayCheck(ot); err != nil {
-		return nil, err
-	}
-	t := &TopoAddr{Overlay: ot}
-	if err := t.fromRaw(s); err != nil {
-		return nil, common.NewBasicError("Failed to parse raw topo address", err, "addr", s)
-	}
-	return t, nil
-}
-
-func (t *TopoAddr) fromRaw(s RawAddrMap) error {
-	for k, rpbo := range s {
-		var hostType addr.HostAddrType
-		pbo := &pubBindAddr{}
-		switch k {
-		case "IPv4":
-			if !t.Overlay.IsIPv4() {
-				return common.NewBasicError(ErrMismatchOverlayAddr, nil, "Overlay", t.Overlay)
-			}
-			t.IPv4 = pbo
-			hostType = addr.HostTypeIPv4
-		case "IPv6":
-			if !t.Overlay.IsIPv6() {
-				return common.NewBasicError(ErrMismatchOverlayAddr, nil, "Overlay", t.Overlay)
-			}
-			t.IPv6 = pbo
-			hostType = addr.HostTypeIPv6
-		default:
-			return common.NewBasicError(ErrUnsupportedAddrType, nil, "Type", k)
-		}
-		if err := pbo.fromRaw(rpbo, t.Overlay.IsUDP()); err != nil {
-			return err
-		}
-		// Check parsed addresses match the expected address type
-		if pbo.pub.L3.Type() != hostType {
-			return common.NewBasicError(ErrMismatchPubAddrType, nil,
-				"AddrType", hostType, "Addr", pbo.pub)
-		}
-		if pbo.bind != nil {
-			if pbo.bind.L3.Type() != hostType {
-				return common.NewBasicError(ErrMismatchBindAddrType, nil,
-					"AddrType", hostType, "Addr", pbo.bind)
-			}
-			// Check pub and bind are not the same address
-			if pbo.pub.Equal(pbo.bind) {
-				return common.NewBasicError(ErrBindAddrEqPubAddr, nil,
-					"bindAddr", pbo.bind, "pubAddr", pbo.pub)
-			}
-		}
-	}
-	if t.IPv4 == nil && t.IPv6 == nil {
-		// Both are empty.
-		return common.NewBasicError(ErrAtLeastOnePub, nil)
-	}
-	return nil
-}
-
-func (t *TopoAddr) PublicAddr(ot overlay.Type) *addr.AppAddr {
-	return t.getAddr(overlay.Type(ot)).PublicAddr()
-}
-
-func (t *TopoAddr) BindAddr(ot overlay.Type) *addr.AppAddr {
-	return t.getAddr(overlay.Type(ot)).BindAddr()
-}
-
-func (t *TopoAddr) OverlayAddrUDP(ot overlay.Type) *net.UDPAddr {
-	ovAddr := t.getAddr(overlay.Type(ot)).OverlayAddr()
-	if ovAddr == nil {
-		return nil
-	}
-	return ovAddr.ToUDPAddr()
-}
-
-func (t *TopoAddr) BindOrPublic(ot overlay.Type) *addr.AppAddr {
-	return t.getAddr(ot).BindOrPublic()
-}
-
-func (t *TopoAddr) getAddr(ot overlay.Type) *pubBindAddr {
-	if t.IPv6 != nil && ot.IsIPv6() {
-		return t.IPv6
-	}
-	if t.IPv4 != nil && ot.IsIPv4() {
-		return t.IPv4
-	}
-	return nil
-}
-
-func (t *TopoAddr) Equal(o *TopoAddr) bool {
-	if t == nil || o == nil {
-		return t == o
-	}
-	if t.Overlay != o.Overlay {
-		return false
-	}
-	if !t.IPv4.Equal(o.IPv4) {
-		return false
-	}
-	if !t.IPv6.Equal(o.IPv6) {
-		return false
-	}
-	return true
+func (t *TopoAddr) UnderlayAddr() *net.UDPAddr {
+	return t.UnderlayAddress.(*net.UDPAddr)
 }
 
 func (t *TopoAddr) String() string {
-	var s []string
-	s = append(s, "TopoAddr{")
-	if t.IPv4 != nil {
-		s = append(s, fmt.Sprintf("IPv4:{%s},", t.IPv4))
-	}
-	if t.IPv6 != nil {
-		s = append(s, fmt.Sprintf("IPv6:{%s},", t.IPv6))
-	}
-	s = append(s, fmt.Sprintf("Overlay: %s}", t.Overlay))
-	return strings.Join(s, "")
-}
-
-type pubBindAddr struct {
-	pub     *addr.AppAddr
-	bind    *addr.AppAddr
-	overlay *overlay.OverlayAddr
+	return fmt.Sprintf("TopoAddr{SCION: %v, Underlay: %v}", t.SCIONAddress, t.UnderlayAddress)
 }
 
-func (pbo *pubBindAddr) fromRaw(rpbo *RawPubBindOverlay, udpOverlay bool) error {
-	l3 := addr.HostFromIPStr(rpbo.Public.Addr)
-	if l3 == nil {
-		return common.NewBasicError(ErrInvalidPub, nil, "ip", rpbo.Public.Addr)
-	}
-	pbo.pub = &addr.AppAddr{
-		L3: l3,
-		L4: uint16(rpbo.Public.L4Port),
-	}
-	port := uint16(rpbo.Public.OverlayPort)
-	if port == 0 && udpOverlay {
-		port = overlay.EndhostPort
-	}
-	pbo.overlay = overlay.NewOverlayAddr(pbo.pub.L3.IP(), port)
-	if rpbo.Bind != nil {
-		l3 := addr.HostFromIPStr(rpbo.Bind.Addr)
-		if l3 == nil {
-			return common.NewBasicError(ErrInvalidBind, nil, "ip", rpbo.Bind.Addr)
-		}
-		pbo.bind = &addr.AppAddr{
-			L3: l3,
-			L4: uint16(rpbo.Bind.L4Port),
-		}
-	}
-	return nil
-}
-
-func (t *pubBindAddr) PublicAddr() *addr.AppAddr {
-	if t == nil {
+func overlayCheck(ot overlay.Type) error {
+	switch ot {
+	case overlay.IPv4, overlay.IPv6, overlay.IPv46, overlay.UDPIPv4,
+		overlay.UDPIPv6, overlay.UDPIPv46:
 		return nil
+	default:
+		return serrors.WithCtx(ErrUnsupportedUnderlay, "type", ot)
 	}
-	return t.pub
 }
 
-func (t *pubBindAddr) BindAddr() *addr.AppAddr {
-	if t == nil {
+func toUDPAddr(a net.Addr) *net.UDPAddr {
+	if a == nil {
 		return nil
 	}
-	return t.bind
-}
-
-func (t *pubBindAddr) OverlayAddr() *overlay.OverlayAddr {
-	if t == nil {
+	udpAddr, ok := a.(*net.UDPAddr)
+	if !ok {
 		return nil
 	}
-	return t.overlay
-}
-
-func (t *pubBindAddr) BindOrPublic() *addr.AppAddr {
-	if t.bind == nil {
-		return t.pub
-	}
-	return t.bind
-}
-
-func (t1 *pubBindAddr) Equal(t2 *pubBindAddr) bool {
-	if (t1 == nil) && (t2 == nil) {
-		return true
-	}
-	if (t1 == nil) != (t2 == nil) {
-		return false
-	}
-	if !t1.pub.Equal(t2.pub) {
-		return false
-	}
-	if !t1.bind.Equal(t2.bind) {
-		return false
-	}
-	if !t1.overlay.Equal(t2.overlay) {
-		return false
-	}
-	return true
-}
-
-func (a *pubBindAddr) String() string {
-	return fmt.Sprintf("public: %v bind: %v overlay: %v", a.pub, a.bind, a.overlay)
-}
-
-func overlayCheck(ot overlay.Type) error {
-	switch ot {
-	case overlay.IPv4, overlay.IPv6, overlay.IPv46, overlay.UDPIPv4,
-		overlay.UDPIPv6, overlay.UDPIPv46:
-	default:
-		return common.NewBasicError(ErrUnsupportedOverlay, nil, "type", ot)
+	return &net.UDPAddr{
+		IP:   append(udpAddr.IP[:0:0], udpAddr.IP...),
+		Port: udpAddr.Port,
 	}
-	return nil
 }
diff --git a/go/lib/topology/braddr.go b/go/lib/topology/braddr.go
deleted file mode 100644
index 1479ade20..000000000
--- a/go/lib/topology/braddr.go
+++ /dev/null
@@ -1,227 +0,0 @@
-// Copyright 2018 ETH Zurich
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package topology
-
-import (
-	"fmt"
-	"net"
-	"strings"
-
-	"github.com/scionproto/scion/go/lib/addr"
-	"github.com/scionproto/scion/go/lib/common"
-	"github.com/scionproto/scion/go/lib/topology/overlay"
-)
-
-type TopoBRAddr struct {
-	IPv4    *OverBindAddr
-	IPv6    *OverBindAddr
-	Overlay overlay.Type
-}
-
-// Create TopoAddr from RawAddrMap, depending on supplied Overlay type
-func topoBRAddrFromRBRAM(s RawBRAddrMap, ot overlay.Type) (*TopoBRAddr, error) {
-	if err := overlayCheck(ot); err != nil {
-		return nil, err
-	}
-	t := &TopoBRAddr{Overlay: ot}
-	if err := t.fromRaw(s); err != nil {
-		return nil, common.NewBasicError("Failed to parse raw topo address", err, "addr", s)
-	}
-	return t, nil
-}
-
-func (t *TopoBRAddr) fromRaw(s RawBRAddrMap) error {
-	for k, rob := range s {
-		var hostType addr.HostAddrType
-		ob := &OverBindAddr{}
-		switch k {
-		case "IPv4":
-			if !t.Overlay.IsIPv4() {
-				return common.NewBasicError(ErrMismatchOverlayAddr, nil, "Overlay", t.Overlay)
-			}
-			t.IPv4 = ob
-			hostType = addr.HostTypeIPv4
-		case "IPv6":
-			if !t.Overlay.IsIPv6() {
-				return common.NewBasicError(ErrMismatchOverlayAddr, nil, "Overlay", t.Overlay)
-			}
-			t.IPv6 = ob
-			hostType = addr.HostTypeIPv6
-		default:
-			return common.NewBasicError(ErrUnsupportedAddrType, nil, "Type", k)
-		}
-		if err := ob.fromRaw(rob, t.Overlay.IsUDP()); err != nil {
-			return err
-		}
-		// Check parsed addresses match the expected address type
-		if ob.PublicOverlay.L3().Type() != hostType {
-			return common.NewBasicError(ErrMismatchPubAddrType, nil,
-				"AddrType", hostType, "Addr", ob.PublicOverlay)
-		}
-		if ob.BindOverlay != nil {
-			if ob.BindOverlay.L3().Type() != hostType {
-				return common.NewBasicError(ErrMismatchBindAddrType, nil,
-					"AddrType", hostType, "Addr", ob.BindOverlay)
-			}
-			// Check PublicOverlay and BindOverlay are not the same address
-			if ob.PublicOverlay.Equal(ob.BindOverlay) {
-				return common.NewBasicError(ErrBindAddrEqPubAddr, nil,
-					"BindOverlayAddr", ob.BindOverlay, "PublicOverlayAddr", ob.PublicOverlay)
-			}
-		}
-	}
-	if t.IPv4 == nil && t.IPv6 == nil {
-		// Both are empty.
-		return common.NewBasicError(ErrAtLeastOnePub, nil)
-	}
-	return nil
-}
-
-func (t *TopoBRAddr) PublicOverlay(ot overlay.Type) *overlay.OverlayAddr {
-	if oba := t.getAddr(ot); oba != nil {
-		return oba.PublicOverlay
-	}
-	return nil
-}
-
-func (t *TopoBRAddr) PublicOverlayUDP(ot overlay.Type) *net.UDPAddr {
-	ov := t.PublicOverlay(ot)
-	if ov == nil {
-		return nil
-	}
-	// XXX(scrye): Return only a shallow copy here because some tests use the
-	// returned value to edit the topology in place.
-	return ov.ShallowUDPAddr()
-}
-
-func (t *TopoBRAddr) BindOverlay(ot overlay.Type) *overlay.OverlayAddr {
-	if oba := t.getAddr(ot); oba != nil {
-		return oba.BindOverlay
-	}
-	return nil
-}
-
-func (t *TopoBRAddr) BindOverlayUDP(ot overlay.Type) *net.UDPAddr {
-	ov := t.BindOverlay(ot)
-	if ov == nil {
-		return nil
-	}
-	return ov.ToUDPAddr()
-}
-
-func (t *TopoBRAddr) BindOrPublicOverlay(ot overlay.Type) *overlay.OverlayAddr {
-	if oba := t.getAddr(ot); oba != nil {
-		return oba.BindOrPublicOverlay()
-	}
-	return nil
-}
-
-func (t *TopoBRAddr) BindOrPublicOverlayUDP(ot overlay.Type) *net.UDPAddr {
-	ov := t.BindOrPublicOverlay(ot)
-	if ov == nil {
-		return nil
-	}
-	return ov.ToUDPAddr()
-}
-
-func (t *TopoBRAddr) getAddr(ot overlay.Type) *OverBindAddr {
-	if t == nil {
-		return nil
-	}
-	if t.IPv6 != nil && ot.IsIPv6() {
-		return t.IPv6
-	}
-	if t.IPv4 != nil && ot.IsIPv4() {
-		return t.IPv4
-	}
-	return nil
-}
-
-func (t *TopoBRAddr) Equal(o *TopoBRAddr) bool {
-	if t == nil || o == nil {
-		return t == o
-	}
-	if t.Overlay != o.Overlay {
-		return false
-	}
-	if !t.IPv4.Equal(o.IPv4) {
-		return false
-	}
-	if !t.IPv6.Equal(o.IPv6) {
-		return false
-	}
-	return true
-}
-
-func (t *TopoBRAddr) String() string {
-	var s []string
-	s = append(s, "TopoBRAddr{")
-	if t.IPv4 != nil {
-		s = append(s, fmt.Sprintf("IPv4:{%s},", t.IPv4))
-	}
-	if t.IPv6 != nil {
-		s = append(s, fmt.Sprintf("IPv6:{%s},", t.IPv6))
-	}
-	s = append(s, fmt.Sprintf("Overlay: %s}", t.Overlay))
-	return strings.Join(s, "")
-}
-
-type OverBindAddr struct {
-	PublicOverlay *overlay.OverlayAddr
-	BindOverlay   *overlay.OverlayAddr
-}
-
-func (ob *OverBindAddr) fromRaw(rob *RawOverlayBind, udpOverlay bool) error {
-	l3 := addr.HostFromIPStr(rob.PublicOverlay.Addr)
-	if l3 == nil {
-		return common.NewBasicError(ErrInvalidPub, nil, "ip", rob.PublicOverlay.Addr)
-	}
-	ob.PublicOverlay = overlay.NewOverlayAddr(l3.IP(), uint16(rob.PublicOverlay.OverlayPort))
-	if rob.BindOverlay != nil {
-		l3 := addr.HostFromIPStr(rob.BindOverlay.Addr)
-		if l3 == nil {
-			return common.NewBasicError(ErrInvalidBind, nil, "ip", rob.BindOverlay.Addr)
-		}
-		ob.BindOverlay = overlay.NewOverlayAddr(l3.IP(), uint16(rob.PublicOverlay.OverlayPort))
-	}
-	return nil
-}
-
-func (t *OverBindAddr) BindOrPublicOverlay() *overlay.OverlayAddr {
-	if t.BindOverlay != nil {
-		return t.BindOverlay
-	}
-	return t.PublicOverlay
-}
-
-func (t1 *OverBindAddr) Equal(t2 *OverBindAddr) bool {
-	if (t1 == nil) && (t2 == nil) {
-		return true
-	}
-	if (t1 == nil) != (t2 == nil) {
-		return false
-	}
-	if !t1.PublicOverlay.Equal(t2.PublicOverlay) {
-		return false
-	}
-	if !t1.BindOverlay.Equal(t2.BindOverlay) {
-		return false
-	}
-	return true
-}
-
-func (a *OverBindAddr) String() string {
-	return fmt.Sprintf("PublicOverlay: %v BindOverlay: %v", a.PublicOverlay, a.BindOverlay)
-}
diff --git a/go/lib/topology/doc.go b/go/lib/topology/doc.go
index 344568b3b..9c0475834 100644
--- a/go/lib/topology/doc.go
+++ b/go/lib/topology/doc.go
@@ -13,10 +13,128 @@
 // limitations under the License.
 
 /*
-Package topology wraps two versions of the topology. The first is RawTopo
-(in raw.go), which closely matches the JSON format. It is mainly used for
-loading the topology from disk. The second data structure is Topo. It is used
-by Go code directly and thus has a different structure and stricter types.
+Package topology wraps two versions of the topology. The first is RawTopo (in raw.go), which closely
+matches the JSON format. It is mainly used for loading the topology from disk. The second data
+structure is Topo. It is used by Go code directly and thus has a different structure and stricter
+types.
+
+The full JSON format for a SCION address looks like the following:
+
+  "Addrs":{
+    "IPv4": {
+      "Public": {
+        "Addr": "192.168.1.1",
+        "L4Port": 31000,
+        "OverlayPort": 30041,
+      },
+      "Bind": {
+        "Addr": "127.0.0.1",
+        "L4Port": 31000,
+        "OverlayPort": 30041,
+      }
+    },
+    "IPv6": {
+      "Public": {
+        "Addr": "2001:db8:f00:b43::1",
+        "L4Port": 31000,
+        "OverlayPort": 30041,
+      },
+      "Bind": {
+        "Addr": "2001:db8:f00:b43::1",
+        "L4Port": 31000,
+        "OverlayPort": 30041,
+      }
+    }
+  }
+
+Go applications parse the above in the following manner:
+  - Properties not listed in the above are ignored;
+  - If a "Bind" property is found, parsing will return an error; bind addresses for SCION sockets
+    are currently not supported.
+  - If an "OverlayPort" property is found, parsing will return an error; custom underlay ports for
+    SCION sockets are currently not supported. NOTE: the JSON file uses the old "Overlay" term for
+    the AS-level UDP fabric that forwards traffic. The new term for this (which is also used in the
+    Go code base) is "Underlay".
+  - If both property "IPv4" and "IPv6" are present, the address is assumed to be IPv6 and only
+    that address is used (in other words, the IPv4 property contents are discarded); dual stacked
+    addresses are currently not supported.
+
+The full JSON format for a BR data-plane AS-external underlay socket looks like the following:
+
+  {
+    "Overlay": "UDP/IPv4",
+    "ISD_AS": "1-ff00:0:1",
+    "Bandwidth": 1000,
+    "PublicOverlay": {
+      "Addr": "192.168.0.1",
+      "OverlayPort": 50000,
+    },
+    "BindOverlay": {
+      "Addr": "127.0.0.1",
+    },
+    "RemoteOverlay": {
+      "Addr": "192.168.0.2",
+      "OverlayPort": 50000,
+    },
+    "LinkTo": "CORE",
+    "MTU": 1472,
+  }
+
+To construct a BR data-plane AS-external underlay socket address out of the above, the following
+rules are used:
+  - Properties "ISD_AS", "Bandwidth", "LinkTo", and "MTU" are ignored. All unknown properties not
+    included in the above example are silently ignored.
+  - Properties "PublicOverlay" and "RemoteOverlay" must exist. An error is returned if one of them
+    is missing.
+  - Property "BindOverlay" is optional. If it is not present, then the address checks below do not
+    apply to property "Addr" under "BindOverlay". If it is present, the address is subject to all
+    format constraints below.
+  - If property "Overlay" is "UDP/IPv4", all "Addr" properties are checked to be IPv4. If one cannot
+    be parsed as an IPv4 address, an error is returned. If the address is the empty string, an error
+    is returned.
+  - If property "Overlay" is "UDP/IPv6", all "Addr" properties are checked to be IPv6. If one cannot
+    be parsed as an IPv6 address, an error is returned. If the address is the empty string, an error
+    is returned.
+  - If a port property is missing, it is assumed to be 0. The application is free to interpret this
+    however it sees fit.
+
+
+The full JSON format for a BR data-plane AS-internal underlay socket address looks like the
+following:
+  {
+    "IPv4": {
+      "PublicOverlay": {
+        "Addr": "192.168.0.1",
+        "OverlayPort": 31000,
+      },
+      BindOverlay": {
+        "Addr": "127.0.0.1"
+      }
+    },
+    "IPv6": {
+      "PublicOverlay": {
+        "Addr": "2001:db8:f00:b43::1",
+        "OverlayPort": 31000
+      },
+      "BindOverlay": {
+        "Addr": "::1"
+      }
+    }
+  }
+
+To construct a BR data-plane AS-internal underlay socket address out of the above, the following
+rules are used:
+  - Properties not listed in the above are ignored.
+  - If both property "IPv4" and "IPv6" are present, the address is assumed to be IPv6 and only that
+    address is used (in other words, the IPv4 property contents are discarded); dual stacked
+    addresses are currently not supported.
+  - For the chosen address, the underlay address is taken from the "BindOverlay" property; if this
+    is the case, the "PublicOverlay" address is never parsed. The address must match the address
+    type in the top-level property (IPv4 or IPv6), otherwise an error is returned. If the
+    "BindOverlay" property does not exist, the underlay address is taken from the "PublicOverlay"
+    property. The address must match the address type in the top-level property, otherwise an error
+    is returned. No matter which address is used, the port is always taken from the "PublicOverlay"
+    property". An unset port is interpreted as 0.
 */
 package topology
 
diff --git a/go/lib/topology/raw.go b/go/lib/topology/raw.go
index 16ca3f221..15b49ebfb 100644
--- a/go/lib/topology/raw.go
+++ b/go/lib/topology/raw.go
@@ -19,10 +19,12 @@ import (
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
+	"net"
 	"strings"
 
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
+	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/topology/overlay"
 )
 
@@ -66,8 +68,59 @@ func (ras RawSrvInfo) String() string {
 
 type RawAddrMap map[string]*RawPubBindOverlay
 
-func (ram RawAddrMap) ToTopoAddr(ot overlay.Type) (t *TopoAddr, err error) {
-	return topoAddrFromRAM(ram, ot)
+func (ram RawAddrMap) ToTopoAddr() (*TopoAddr, error) {
+	addressInfo, mustBeIPv6 := ram.extractAddressInfo()
+	mustBeIPv4 := !mustBeIPv6
+	if addressInfo == nil {
+		return nil, serrors.WithCtx(ErrAtLeastOnePub, "address", ram)
+	}
+	if addressInfo.Bind != nil {
+		return nil, serrors.WithCtx(ErrBindNotSupported, "address", addressInfo.Bind)
+	}
+	if addressInfo.Public.OverlayPort != 0 {
+		return nil, serrors.WithCtx(ErrCustomUnderlayPortNotSupported,
+			"port", addressInfo.Public.OverlayPort)
+	}
+
+	ip := net.ParseIP(addressInfo.Public.RawAddrPort.Addr)
+	if ip == nil {
+		return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.Public.RawAddrPort.Addr)
+	}
+
+	if mustBeIPv6 && ip.To4() != nil {
+		return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.Public.RawAddrPort.Addr)
+	}
+	if mustBeIPv4 {
+		// Convert to 4-byte format to simplify testing
+		ip = ip.To4()
+		if ip == nil {
+			return nil, serrors.WithCtx(ErrInvalidPub,
+				"address", addressInfo.Public.RawAddrPort.Addr)
+		}
+	}
+
+	return &TopoAddr{
+		SCIONAddress: &addr.AppAddr{
+			L3: addr.HostFromIP(ip),
+			L4: uint16(addressInfo.Public.RawAddrPort.L4Port),
+		},
+		UnderlayAddress: &net.UDPAddr{
+			IP:   append(ip[:0:0], ip...),
+			Port: EndhostPort,
+		},
+	}, nil
+}
+
+func (ram RawAddrMap) extractAddressInfo() (info *RawPubBindOverlay, is6 bool) {
+	a, ok := ram["IPv6"]
+	if ok {
+		return a, true
+	}
+	a, ok = ram["IPv4"]
+	if ok {
+		return a, false
+	}
+	return nil, false
 }
 
 func (ram RawAddrMap) String() string {
@@ -131,8 +184,48 @@ func (b RawBRInfo) String() string {
 
 type RawBRAddrMap map[string]*RawOverlayBind
 
-func (roa RawBRAddrMap) ToTopoBRAddr(ot overlay.Type) (t *TopoBRAddr, err error) {
-	return topoBRAddrFromRBRAM(roa, ot)
+func (m RawBRAddrMap) ToUDPAddr() (*net.UDPAddr, error) {
+	addressInfo, mustBeIPv6 := m.extractAddressInfo()
+	mustBeIPv4 := !mustBeIPv6
+	if addressInfo == nil {
+		return nil, serrors.WithCtx(ErrAtLeastOnePub, "address", m)
+	}
+	if addressInfo.BindOverlay != nil {
+		return nil, serrors.WithCtx(ErrBindNotSupported, "address", addressInfo.BindOverlay)
+	}
+
+	ip := net.ParseIP(addressInfo.PublicOverlay.Addr)
+	if ip == nil {
+		return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.PublicOverlay.Addr)
+	}
+
+	if mustBeIPv6 && ip.To4() != nil {
+		return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.PublicOverlay.Addr)
+	}
+	if mustBeIPv4 {
+		// Convert to 4-byte format to simplify testing
+		ip = ip.To4()
+		if ip == nil {
+			return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.PublicOverlay.Addr)
+		}
+	}
+
+	return &net.UDPAddr{
+		IP:   append(ip[:0:0], ip...),
+		Port: addressInfo.PublicOverlay.OverlayPort,
+	}, nil
+}
+
+func (m RawBRAddrMap) extractAddressInfo() (info *RawOverlayBind, is6 bool) {
+	a, ok := m["IPv6"]
+	if ok {
+		return a, true
+	}
+	a, ok = m["IPv4"]
+	if ok {
+		return a, false
+	}
+	return nil, false
 }
 
 func (roa RawBRAddrMap) String() string {
@@ -168,27 +261,53 @@ type RawBRIntf struct {
 	MTU           int
 }
 
-// Convert a RawBRIntf struct (filled from JSON) to a TopoBRAddr (used by Go code)
-func (b RawBRIntf) localTopoBRAddr(o overlay.Type) (*TopoBRAddr, error) {
-	rbram := make(RawBRAddrMap)
-	rbram[o.ToIP().String()] = &RawOverlayBind{
-		PublicOverlay: *b.PublicOverlay,
-		BindOverlay:   b.BindOverlay,
-	}
-	return topoBRAddrFromRBRAM(rbram, o)
-}
-
 // make an OverlayAddr object from a BR interface Remote entry
-func (b RawBRIntf) remoteBRAddr(o overlay.Type) (*overlay.OverlayAddr, error) {
+func (b RawBRIntf) remoteBRAddr(o overlay.Type) (*net.UDPAddr, error) {
 	l3 := addr.HostFromIPStr(b.RemoteOverlay.Addr)
 	if l3 == nil {
 		return nil, common.NewBasicError("Could not parse remote IP from string", nil,
 			"ip", b.RemoteOverlay.Addr)
 	}
 	if !o.IsUDP() && (b.RemoteOverlay.OverlayPort != 0) {
-		return nil, common.NewBasicError(ErrOverlayPort, nil, "addr", b.RemoteOverlay)
+		return nil, serrors.WithCtx(ErrUnderlayPort, "addr", b.RemoteOverlay)
+	}
+	return &net.UDPAddr{IP: l3.IP(), Port: b.RemoteOverlay.OverlayPort}, nil
+}
+
+func (i *RawBRIntf) TopoBRAddr() (*net.UDPAddr, error) {
+	if i.Overlay != "UDP/IPv4" && i.Overlay != "UDP/IPv6" {
+		return nil, serrors.WithCtx(ErrUnsupportedUnderlay, "underlay", i.Overlay)
+	}
+
+	mustBeIPv4 := i.Overlay == "UDP/IPv4"
+	mustBeIPv6 := i.Overlay == "UDP/IPv6"
+
+	var udpAddr net.UDPAddr
+	if i.BindOverlay != nil {
+		udpAddr.IP = net.ParseIP(i.BindOverlay.Addr)
+	} else {
+		if i.PublicOverlay == nil {
+			return nil, serrors.WithCtx(ErrUnderlayAddressNotFound, "underlay", i.Overlay)
+		}
+		udpAddr.IP = net.ParseIP(i.PublicOverlay.Addr)
+	}
+	if i.PublicOverlay != nil {
+		udpAddr.Port = i.PublicOverlay.OverlayPort
+	}
+
+	if udpAddr.IP == nil {
+		return nil, serrors.WithCtx(ErrInvalidPub, "address", i.PublicOverlay.Addr)
+	}
+	if mustBeIPv4 {
+		udpAddr.IP = udpAddr.IP.To4()
+		if udpAddr.IP == nil {
+			return nil, serrors.WithCtx(ErrExpectedIPv4FoundIPv6, "address", i.PublicOverlay.Addr)
+		}
+	}
+	if mustBeIPv6 && udpAddr.IP.To4() != nil {
+		return nil, serrors.WithCtx(ErrExpectedIPv6FoundIPv4, "address", i.PublicOverlay.Addr)
 	}
-	return overlay.NewOverlayAddr(l3.IP(), uint16(b.RemoteOverlay.OverlayPort)), nil
+	return &udpAddr, nil
 }
 
 type RawAddrOverlay struct {
diff --git a/go/lib/topology/topology.go b/go/lib/topology/topology.go
index 6d768929c..79e3e2291 100644
--- a/go/lib/topology/topology.go
+++ b/go/lib/topology/topology.go
@@ -18,6 +18,7 @@ package topology
 import (
 	"fmt"
 	"math/rand"
+	"net"
 	"sort"
 	"time"
 
@@ -74,14 +75,17 @@ type IfInfoMap map[common.IFIDType]IFInfo
 // there is again a sorted slice of names of the servers that provide the service.
 // Additionally, there is a map from those names to TopoAddr structs.
 type Topo struct {
-	Timestamp      time.Time
-	TimestampHuman string // This can vary wildly in format and is only for informational purposes.
+	Timestamp time.Time
+	// TimestampHuman is a human readable format of the Timestamp field. This can vary wildly in
+	// format and is only for informational purposes.
+	TimestampHuman string
 	TTL            time.Duration
 	ISD_AS         addr.IA
-	Overlay        overlay.Type
-	MTU            int
 	Core           bool
 
+	Overlay overlay.Type
+	MTU     int
+
 	BR      map[string]BRInfo
 	BRNames []string
 	// This maps Interface IDs to internal addresses. Clients use this to
@@ -95,10 +99,6 @@ type Topo struct {
 	CSNames  ServiceNames
 	PS       IDAddrMap
 	PSNames  ServiceNames
-	SB       IDAddrMap
-	SBNames  ServiceNames
-	RS       IDAddrMap
-	RSNames  ServiceNames
 	DS       IDAddrMap
 	DSNames  ServiceNames
 	SIG      IDAddrMap
@@ -112,8 +112,6 @@ func NewTopo() *Topo {
 		BS:        make(IDAddrMap),
 		CS:        make(IDAddrMap),
 		PS:        make(IDAddrMap),
-		SB:        make(IDAddrMap),
-		RS:        make(IDAddrMap),
 		SIG:       make(IDAddrMap),
 		DS:        make(IDAddrMap),
 		IFInfoMap: make(IfInfoMap),
@@ -133,7 +131,6 @@ func TopoFromRaw(raw *RawTopo) (*Topo, error) {
 	if err := t.populateServices(raw); err != nil {
 		return nil, err
 	}
-
 	return t, nil
 }
 
@@ -166,13 +163,13 @@ func (t *Topo) populateBR(raw *RawTopo) error {
 		if rawBr.InternalAddrs == nil {
 			return common.NewBasicError("Missing Internal Address", nil, "br", name)
 		}
-		ctrlAddr, err := topoAddrFromRAM(rawBr.CtrlAddr, t.Overlay)
+		ctrlAddr, err := rawBr.CtrlAddr.ToTopoAddr()
 		if err != nil {
-			return err
+			return serrors.WrapStr("unable to extract SCION control-plane address", err)
 		}
-		intAddr, err := topoBRAddrFromRBRAM(rawBr.InternalAddrs, t.Overlay)
+		intAddr, err := rawBr.InternalAddrs.ToUDPAddr()
 		if err != nil {
-			return err
+			return serrors.WrapStr("unable to extract underlay internal data-plane address", err)
 		}
 		brInfo := BRInfo{
 			Name:          name,
@@ -212,13 +209,15 @@ func (t *Topo) populateBR(raw *RawTopo) error {
 				continue
 			}
 			if ifinfo.Overlay, err = overlay.TypeFromString(rawIntf.Overlay); err != nil {
-				return err
+				return serrors.WrapStr("unable to extract underlay type", err)
 			}
-			if ifinfo.Local, err = rawIntf.localTopoBRAddr(ifinfo.Overlay); err != nil {
-				return err
+			if ifinfo.Local, err = rawIntf.TopoBRAddr(); err != nil {
+				return serrors.WrapStr("unable to extract "+
+					"underlay external data-plane local address", err)
 			}
 			if ifinfo.Remote, err = rawIntf.remoteBRAddr(ifinfo.Overlay); err != nil {
-				return err
+				return serrors.WrapStr("unable to extract "+
+					"underlay external data-plane remote address", err)
 			}
 			brInfo.IFs[ifid] = &ifinfo
 			t.IFInfoMap[ifid] = ifinfo
@@ -248,14 +247,6 @@ func (t *Topo) populateServices(raw *RawTopo) error {
 	if err != nil {
 		return err
 	}
-	t.SBNames, err = svcMapFromRaw(raw.SibraService, common.SB, t.SB, t.Overlay)
-	if err != nil {
-		return err
-	}
-	t.RSNames, err = svcMapFromRaw(raw.RainsService, common.RS, t.RS, t.Overlay)
-	if err != nil {
-		return err
-	}
 	t.SIGNames, err = svcMapFromRaw(raw.SIG, common.SIG, t.SIG, t.Overlay)
 	if err != nil {
 		return err
@@ -326,8 +317,6 @@ func (t *Topo) getSvcInfo(svc proto.ServiceType) (*svcInfo, error) {
 		return &svcInfo{overlay: t.Overlay, names: t.PSNames, idTopoAddrMap: t.PS}, nil
 	case proto.ServiceType_cs:
 		return &svcInfo{overlay: t.Overlay, names: t.CSNames, idTopoAddrMap: t.CS}, nil
-	case proto.ServiceType_sb:
-		return &svcInfo{overlay: t.Overlay, names: t.SBNames, idTopoAddrMap: t.SB}, nil
 	case proto.ServiceType_sig:
 		return &svcInfo{overlay: t.Overlay, names: t.SIGNames, idTopoAddrMap: t.SIG}, nil
 	case proto.ServiceType_ds:
@@ -359,7 +348,7 @@ func svcMapFromRaw(ras map[string]*RawSrvInfo, stype string, smap IDAddrMap,
 
 	var snames []string
 	for name, svc := range ras {
-		svcTopoAddr, err := svc.Addrs.ToTopoAddr(ot)
+		svcTopoAddr, err := svc.Addrs.ToTopoAddr()
 		if err != nil {
 			return nil, common.NewBasicError(
 				"Could not convert RawAddrMap to TopoAddr", err,
@@ -381,7 +370,7 @@ type BRInfo struct {
 	// CtrlAddrs are the local control-plane addresses.
 	CtrlAddrs *TopoAddr
 	// InternalAddrs are the local data-plane addresses.
-	InternalAddrs *TopoBRAddr
+	InternalAddrs *net.UDPAddr
 	// IFIDs is a sorted list of the interface IDs.
 	IFIDs []common.IFIDType
 	// IFs is a map of interface IDs.
@@ -397,9 +386,9 @@ type IFInfo struct {
 	BRName        string
 	CtrlAddrs     *TopoAddr
 	Overlay       overlay.Type
-	InternalAddrs *TopoBRAddr
-	Local         *TopoBRAddr
-	Remote        *overlay.OverlayAddr
+	InternalAddrs *net.UDPAddr
+	Local         *net.UDPAddr
+	Remote        *net.UDPAddr
 	RemoteIFID    common.IFIDType
 	Bandwidth     int
 	ISD_AS        addr.IA
@@ -431,3 +420,13 @@ func (i IFInfo) String() string {
 		"Remote:%+v Bw:%d IA:%s Type:%s MTU:%d", i.BRName, i.InternalAddrs, i.CtrlAddrs, i.Overlay,
 		i.Local, i.Remote, i.Bandwidth, i.ISD_AS, i.LinkType, i.MTU)
 }
+
+func copyUDPAddr(a *net.UDPAddr) *net.UDPAddr {
+	if a == nil {
+		return nil
+	}
+	return &net.UDPAddr{
+		IP:   append(a.IP[:0:0], a.IP...),
+		Port: a.Port,
+	}
+}
diff --git a/go/path_srv/main.go b/go/path_srv/main.go
index d8437da6d..6c92d7e2b 100644
--- a/go/path_srv/main.go
+++ b/go/path_srv/main.go
@@ -125,7 +125,6 @@ func realMain() int {
 	nc := infraenv.NetworkConfig{
 		IA:                    topo.IA(),
 		Public:                topo.SPublicAddress(addr.SvcPS, cfg.General.ID),
-		Bind:                  topo.SBindAddress(addr.SvcPS, cfg.General.ID),
 		SVC:                   addr.SvcPS,
 		ReconnectToDispatcher: cfg.General.ReconnectToDispatcher,
 		QUIC: infraenv.QUIC{


Test patch:
diff --git a/acceptance/topo_br_reload_if_ip_acceptance/test b/acceptance/topo_br_reload_if_ip_acceptance/test
index ae3000349..6c4d94599 100755
--- a/acceptance/topo_br_reload_if_ip_acceptance/test
+++ b/acceptance/topo_br_reload_if_ip_acceptance/test
@@ -93,7 +93,7 @@ change_remote_ip() {
     jq ".BorderRouters[].Interfaces[].RemoteOverlay.Addr = $3" $1 | sponge $1
     ./tools/dc scion kill -s HUP scion_br"$2"-1
     sleep 2
-    check_logs "Remote:[$(unqoute $3)]" $2
+    check_logs "Remote:$(unqoute $3)" $2
 }
 
 check_change_initial_ip() {
diff --git a/acceptance/topo_br_reload_if_port_acceptance/test b/acceptance/topo_br_reload_if_port_acceptance/test
index f039414f4..0a46df595 100755
--- a/acceptance/topo_br_reload_if_port_acceptance/test
+++ b/acceptance/topo_br_reload_if_port_acceptance/test
@@ -51,7 +51,7 @@ check_change_remote_port() {
     jq '.BorderRouters[].Interfaces[].RemoteOverlay.OverlayPort = 42424' $DST_TOPO | sponge $DST_TOPO
     ./tools/dc scion kill -s HUP scion_br"$DST_IA_FILE"-1
     sleep 5
-    check_logs "Remote:[$addr_src]:42424" $DST_IA_FILE
+    check_logs "Remote:$addr_src:42424" $DST_IA_FILE
     check_connectivity "End check_change_remote_port"
 }
 
diff --git a/go/beacon_srv/internal/beaconing/originator_test.go b/go/beacon_srv/internal/beaconing/originator_test.go
index 8b596f48a..fd3eca11f 100644
--- a/go/beacon_srv/internal/beaconing/originator_test.go
+++ b/go/beacon_srv/internal/beaconing/originator_test.go
@@ -187,8 +187,7 @@ func checkMsg(t *testing.T, msg msg, pub common.RawBytes, infos topology.IfInfoM
 		xtest.FailOnErr(t, err)
 		SoMsg("Egress", hopF.ConsEgress, ShouldEqual, bHopF.ConsEgress)
 		brAddr := infos[hopF.ConsEgress].InternalAddrs
-		SoMsg("ov", msg.ov, ShouldResemble,
-			brAddr.PublicOverlayUDP(brAddr.Overlay))
+		SoMsg("ov", msg.ov, ShouldResemble, brAddr)
 	})
 }
 
diff --git a/go/beacon_srv/internal/beaconing/registrar_test.go b/go/beacon_srv/internal/beaconing/registrar_test.go
index bbe7eea30..19852c9aa 100644
--- a/go/beacon_srv/internal/beaconing/registrar_test.go
+++ b/go/beacon_srv/internal/beaconing/registrar_test.go
@@ -176,8 +176,7 @@ func TestRegistrarRun(t *testing.T) {
 					SoMsg("HopField", []uint8(hopF.Pack()), ShouldResemble,
 						pseg.ASEntries[pseg.MaxAEIdx()].HopEntries[0].RawHopField)
 					a := topoProvider.Get().IFInfoMap()[hopF.ConsIngress].InternalAddrs
-					SoMsg("Next", s.Addr.NextHop, ShouldResemble,
-						a.PublicOverlayUDP(a.Overlay))
+					SoMsg("Next", s.Addr.NextHop, ShouldResemble, a)
 				})
 			}
 			// The second run should not do anything, since the period has not passed.
diff --git a/go/border/setup_test.go b/go/border/setup_test.go
index 07121d1a7..d7c836a14 100644
--- a/go/border/setup_test.go
+++ b/go/border/setup_test.go
@@ -28,7 +28,6 @@ import (
 	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/ringbuf"
-	"github.com/scionproto/scion/go/lib/topology/overlay"
 	"github.com/scionproto/scion/go/lib/xtest"
 )
 
@@ -52,8 +51,7 @@ func TestSetupNet(t *testing.T) {
 		ctx := rctx.New(loadConfig(t))
 		// Modify local socket address. A new socket should be opened when
 		// setting up the context.
-		addr := ctx.Conf.BR.InternalAddrs
-		addr.PublicOverlayUDP(addr.Overlay).IP[3] = 255
+		ctx.Conf.BR.InternalAddrs.IP[3] = 255
 		SoMsg("In", oldCtx.LocSockIn, ShouldNotBeNil)
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
@@ -69,7 +67,7 @@ func TestSetupNet(t *testing.T) {
 		r, oldCtx := setupTestRouter(t)
 		copyCtx := copyContext(oldCtx)
 		ctx := rctx.New(loadConfig(t))
-		ctx.Conf.BR.IFs[12].Local.PublicOverlayUDP(overlay.IPv4).IP[3] = 255
+		ctx.Conf.BR.IFs[12].Local.IP[3] = 255
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
 		// Check that unaffected sockets have not changed.
@@ -90,7 +88,7 @@ func TestSetupNet(t *testing.T) {
 		r, oldCtx := setupTestRouter(t)
 		copyCtx := copyContext(oldCtx)
 		ctx := rctx.New(loadConfig(t))
-		ctx.Conf.BR.IFs[12].Remote.L3().IP()[3] = 255
+		ctx.Conf.BR.IFs[12].Remote.IP[3] = 255
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
 		// Check that unaffected sockets have not changed.
@@ -132,8 +130,7 @@ func TestRollbackNet(t *testing.T) {
 		r, oldCtx := setupTestRouter(t)
 		copyCtx := copyContext(oldCtx)
 		ctx := rctx.New(loadConfig(t))
-		addr := ctx.Conf.BR.InternalAddrs
-		addr.PublicOverlayUDP(addr.Overlay).IP[3] = 255
+		ctx.Conf.BR.InternalAddrs.IP[3] = 255
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
 		// Rollback the changes.
@@ -151,7 +148,7 @@ func TestRollbackNet(t *testing.T) {
 		r, oldCtx := setupTestRouter(t)
 		copyCtx := copyContext(oldCtx)
 		ctx := rctx.New(loadConfig(t))
-		ctx.Conf.BR.IFs[12].Local.PublicOverlayUDP(overlay.IPv4).IP[3] = 255
+		ctx.Conf.BR.IFs[12].Local.IP[3] = 255
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
 		// Rollback the changes.
@@ -190,8 +187,7 @@ func TestTeardownNet(t *testing.T) {
 	Convey("Tearing down config with changed local address  should be a noop", t, func() {
 		r, oldCtx := setupTestRouter(t)
 		ctx := rctx.New(loadConfig(t))
-		addr := ctx.Conf.BR.InternalAddrs
-		addr.PublicOverlayUDP(addr.Overlay).IP[3] = 255
+		ctx.Conf.BR.InternalAddrs.IP[3] = 255
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
 		// Start sockets on the new context.
@@ -209,7 +205,7 @@ func TestTeardownNet(t *testing.T) {
 	Convey("Tearing down config with changed interface should be a noop", t, func() {
 		r, oldCtx := setupTestRouter(t)
 		ctx := rctx.New(loadConfig(t))
-		ctx.Conf.BR.IFs[12].Local.PublicOverlayUDP(overlay.IPv4).IP[3] = 255
+		ctx.Conf.BR.IFs[12].Local.IP[3] = 255
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
 		// Start sockets on the new context.
diff --git a/go/lib/discovery/discoverypool/pool_test.go b/go/lib/discovery/discoverypool/pool_test.go
index b9eaaee51..ee3078854 100644
--- a/go/lib/discovery/discoverypool/pool_test.go
+++ b/go/lib/discovery/discoverypool/pool_test.go
@@ -81,7 +81,7 @@ func TestPoolUpdate(t *testing.T) {
 		pool := mustLoadPool(t)
 		svcInfo := mustLoadSvcInfo(t)
 		Convey("And a topology containing an updated discovery service entry", func() {
-			svcInfo[ds[0].key].IPv4.PublicAddr().L3 = addr.HostFromIP(
+			svcInfo[ds[0].key].SCIONAddress.L3 = addr.HostFromIP(
 				net.IPv4(127, 0, 0, 21))
 			pool.Update(svcInfo)
 			Convey("The pool should contain the updated info", func() {
@@ -89,7 +89,7 @@ func TestPoolUpdate(t *testing.T) {
 					key: ds[0].key,
 					addr: &net.UDPAddr{
 						IP:   net.IPv4(127, 0, 0, 21),
-						Port: int(svcInfo[ds[0].key].IPv4.PublicAddr().L4),
+						Port: int(svcInfo[ds[0].key].SCIONAddress.L4),
 					},
 				})
 			})
diff --git a/go/lib/healthpool/svcinstance/pool_test.go b/go/lib/healthpool/svcinstance/pool_test.go
index 49bf476b8..a4ffabae5 100644
--- a/go/lib/healthpool/svcinstance/pool_test.go
+++ b/go/lib/healthpool/svcinstance/pool_test.go
@@ -74,7 +74,7 @@ func TestPoolUpdate(t *testing.T) {
 		pool := mustLoadPool(t)
 		svcInfo := mustLoadSvcInfo(t)
 		Convey("And an instance map containing an updated discovery service entry", func() {
-			svcInfo[ds1].IPv4.PublicAddr().L3 = dsInfos[ds1updated].L3
+			svcInfo[ds1].SCIONAddress.L3 = dsInfos[ds1updated].L3
 			pool.infos[ds1].Fail()
 			err := pool.Update(svcInfo)
 			SoMsg("err", err, ShouldBeNil)
diff --git a/go/lib/infra/modules/itopo/itopo_test.go b/go/lib/infra/modules/itopo/itopo_test.go
index e8c7d9a64..8e2abf778 100644
--- a/go/lib/infra/modules/itopo/itopo_test.go
+++ b/go/lib/infra/modules/itopo/itopo_test.go
@@ -15,6 +15,7 @@
 package itopo
 
 import (
+	"net"
 	"testing"
 	"time"
 
@@ -22,7 +23,6 @@ import (
 	. "github.com/smartystreets/goconvey/convey"
 
 	"github.com/scionproto/scion/go/lib/topology"
-	"github.com/scionproto/scion/go/lib/topology/overlay"
 	"github.com/scionproto/scion/go/lib/xtest"
 	"github.com/scionproto/scion/go/lib/xtest/mock_xtest"
 	"github.com/scionproto/scion/go/proto"
@@ -87,7 +87,7 @@ func TestStateSetStatic(t *testing.T) {
 			topo.IFInfoMap[1] = ifinfo
 			// modify other cs
 			cs := topo.CS["cs1-ff00:0:311-2"]
-			cs.Overlay = overlay.IPv6
+			cs.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.CS["cs1-ff00:0:311-2"] = cs
 			clbks.update.EXPECT().Call().Do(wg.Done)
 			newTopo, updated, err := s.setStatic(topo, true)
@@ -99,7 +99,7 @@ func TestStateSetStatic(t *testing.T) {
 		})
 		Convey("Modifying the element's entry should not be allowed", func() {
 			cs := topo.CS[id]
-			cs.Overlay = overlay.IPv6
+			cs.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.CS[id] = cs
 			_, updated, err := s.setStatic(topo, true)
 			SoMsg("err", err, ShouldNotBeNil)
@@ -126,7 +126,7 @@ func TestStateSetStatic(t *testing.T) {
 			topo.IFInfoMap[2] = ifinfo
 			// modify other cs
 			cs := topo.CS["cs1-ff00:0:311-2"]
-			cs.Overlay = overlay.IPv6
+			cs.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.CS["cs1-ff00:0:311-2"] = cs
 			Convey("If semi-mutation is allowed", func() {
 				clbks.update.EXPECT().Call().Do(wg.Done)
@@ -148,9 +148,7 @@ func TestStateSetStatic(t *testing.T) {
 			})
 		})
 		Convey("Modifying the internal address is not allowed", func() {
-			brInfo := topo.BR[id]
-			brInfo.InternalAddrs.Overlay = overlay.IPv6
-			topo.BR[id] = brInfo
+			topo.BR[id].InternalAddrs.Port = 42
 			Convey("If semi-mutation is allowed", func() {
 				_, updated, err := s.setStatic(topo, true)
 				SoMsg("err", err, ShouldNotBeNil)
@@ -230,7 +228,7 @@ func TestStateSetDynamic(t *testing.T) {
 			topo.IFInfoMap[1] = ifinfo
 			// modify other cs
 			cs := topo.CS["cs1-ff00:0:311-2"]
-			cs.Overlay = overlay.IPv6
+			cs.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.CS["cs1-ff00:0:311-2"] = cs
 			newTopo, updated, err := s.setDynamic(topo)
 			SoMsg("err", err, ShouldBeNil)
@@ -239,7 +237,7 @@ func TestStateSetDynamic(t *testing.T) {
 		})
 		Convey("Modifying the element's entry should not be allowed", func() {
 			cs := topo.CS[id]
-			cs.Overlay = overlay.IPv6
+			cs.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.CS[id] = cs
 			_, updated, err := s.setDynamic(topo)
 			SoMsg("err", err, ShouldNotBeNil)
@@ -264,7 +262,7 @@ func TestStateSetDynamic(t *testing.T) {
 			topo.IFInfoMap[2] = ifinfo
 			// modify other cs
 			cs := topo.CS["cs1-ff00:0:311-2"]
-			cs.Overlay = overlay.IPv6
+			cs.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.CS["cs1-ff00:0:311-2"] = cs
 			newTopo, updated, err := s.setDynamic(topo)
 			SoMsg("err", err, ShouldBeNil)
@@ -272,14 +270,12 @@ func TestStateSetDynamic(t *testing.T) {
 			SoMsg("topo", newTopo, ShouldEqual, topo)
 		})
 		Convey("Modifying the internal address is not allowed", func() {
-			brInfo := topo.BR[id]
-			brInfo.InternalAddrs.Overlay = overlay.IPv6
-			topo.BR[id] = brInfo
+			topo.BR[id].InternalAddrs.Port = 42
 			_, updated, err := s.setDynamic(topo)
 			SoMsg("err", err, ShouldNotBeNil)
 			SoMsg("updated", updated, ShouldBeFalse)
 		})
-		Convey("Modifying an interfaces is not allowed", func() {
+		Convey("Modifying an interface is not allowed", func() {
 			ifinfo := topo.IFInfoMap[1]
 			ifinfo.MTU = 42
 			topo.IFInfoMap[1] = ifinfo
diff --git a/go/lib/infra/modules/itopo/testdata/topo.json b/go/lib/infra/modules/itopo/testdata/topo.json
index fe9cffaa0..2e717a986 100644
--- a/go/lib/infra/modules/itopo/testdata/topo.json
+++ b/go/lib/infra/modules/itopo/testdata/topo.json
@@ -9,19 +9,45 @@
     "BorderRouters": {
         "br1-ff00:0:311-1": {
             "InternalAddrs": {
-                "IPv4": {"PublicOverlay": {"Addr": "10.1.0.1"}},
-                "IPv6": {"PublicOverlay": {"Addr": "2001:db8:a0b:12f0::1"}}
+                "IPv4": {
+                    "PublicOverlay": {
+                        "Addr": "10.1.0.1"
+                    }
+                },
+                "IPv6": {
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::1"
+                    }
+                }
             },
             "CtrlAddr": {
-                "IPv4": {"Public": {"Addr": "10.1.0.1", "L4Port": 30098}},
-                "IPv6": {"Public": {"Addr": "2001:db8:a0b:12f0::1", "L4Port": 30098}}
+                "IPv4": {
+                    "Public": {
+                        "Addr": "10.1.0.1",
+                        "L4Port": 30098
+                    }
+                },
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:a0b:12f0::1",
+                        "L4Port": 30098
+                    }
+                }
             },
             "Interfaces": {
                 "1": {
                     "Overlay": "UDP/IPv4",
-                    "BindOverlay": {"Addr": "10.0.0.1"},
-                    "PublicOverlay": {"Addr": "192.0.2.1", "OverlayPort": 44997},
-                    "RemoteOverlay": {"Addr": "192.0.2.2", "OverlayPort": 44998},
+                    "BindOverlay": {
+                        "Addr": "10.0.0.1"
+                    },
+                    "PublicOverlay": {
+                        "Addr": "192.0.2.1",
+                        "OverlayPort": 44997
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "192.0.2.2",
+                        "OverlayPort": 44998
+                    },
                     "Bandwidth": 1000,
                     "ISD_AS": "1-ff00:0:312",
                     "LinkTo": "PARENT",
@@ -31,17 +57,34 @@
         },
         "br1-ff00:0:311-2": {
             "InternalAddrs": {
-                "IPv4": {"PublicOverlay": {"Addr": "10.1.0.2"}}
+                "IPv4": {
+                    "PublicOverlay": {
+                        "Addr": "10.1.0.2"
+                    }
+                }
             },
             "CtrlAddr": {
-                "IPv4": {"Public": {"Addr": "10.1.0.2", "L4Port": 30098}}
+                "IPv4": {
+                    "Public": {
+                        "Addr": "10.1.0.2",
+                        "L4Port": 30098
+                    }
+                }
             },
             "Interfaces": {
                 "2": {
                     "Overlay": "UDP/IPv4",
-                    "BindOverlay": {"Addr": "10.0.0.2"},
-                    "PublicOverlay": {"Addr": "192.0.2.11", "OverlayPort": 44997},
-                    "RemoteOverlay": {"Addr": "192.0.2.12", "OverlayPort": 44998},
+                    "BindOverlay": {
+                        "Addr": "10.0.0.2"
+                    },
+                    "PublicOverlay": {
+                        "Addr": "192.0.2.11",
+                        "OverlayPort": 44997
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "192.0.2.12",
+                        "OverlayPort": 44998
+                    },
                     "Bandwidth": 1000,
                     "ISD_AS": "1-ff00:0:312",
                     "LinkTo": "PARENT",
@@ -51,11 +94,25 @@
         }
     },
     "CertificateService": {
-        "cs1-ff00:0:311-1": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.66", "L4Port": 30081},
-                     "Bind": {"Addr": "127.0.0.67", "L4Port": 30081}}}
+        "cs1-ff00:0:311-1": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.66",
+                        "L4Port": 30081
+                    }
+                }
+            }
         },
-        "cs1-ff00:0:311-2": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.67", "L4Port": 30073}}}}
+        "cs1-ff00:0:311-2": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.67",
+                        "L4Port": 30073
+                    }
+                }
+            }
+        }
     }
 }
diff --git a/go/lib/infra/modules/itopo/validate_test.go b/go/lib/infra/modules/itopo/validate_test.go
index 85273b7dd..176a37e97 100644
--- a/go/lib/infra/modules/itopo/validate_test.go
+++ b/go/lib/infra/modules/itopo/validate_test.go
@@ -15,6 +15,7 @@
 package itopo
 
 import (
+	"net"
 	"testing"
 	"time"
 
@@ -71,13 +72,13 @@ func TestSvcValidatorImmutable(t *testing.T) {
 		testGenImmutable(v, topo, oldTopo, t)
 		Convey("Modifying a different service of the same type is allowed", func() {
 			svcInfo := topo.CS[other]
-			svcInfo.Overlay = overlay.IPv6
+			svcInfo.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.CS[other] = svcInfo
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldBeNil)
 		})
 		Convey("Modifying the own service entry is not allowed", func() {
 			svcInfo := topo.CS[v.id]
-			svcInfo.Overlay = overlay.IPv6
+			svcInfo.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.CS[v.id] = svcInfo
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldNotBeNil)
 		})
@@ -107,26 +108,20 @@ func TestBrValidatorImmutable(t *testing.T) {
 		topo := loadTopo(fn, t)
 		testGenImmutable(v, topo, oldTopo, t)
 		Convey("Modifying a different br's internal address is allowed", func() {
-			brInfo := topo.BR[other]
-			brInfo.InternalAddrs.Overlay = overlay.IPv6
-			topo.BR[other] = brInfo
+			topo.BR[other].InternalAddrs.Port = 42
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldBeNil)
 		})
 		Convey("Modifying a different br's control address is allowed", func() {
-			brInfo := topo.BR[other]
-			brInfo.CtrlAddrs.Overlay = overlay.IPv6
-			topo.BR[other] = brInfo
+			topo.BR[other].CtrlAddrs.UnderlayAddress = &net.UDPAddr{Port: 42}
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldBeNil)
 		})
 		Convey("Modifying the own internal address is not allowed", func() {
-			brInfo := topo.BR[v.id]
-			brInfo.InternalAddrs.Overlay = overlay.IPv6
-			topo.BR[v.id] = brInfo
+			topo.BR[v.id].InternalAddrs.Port = 42
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldNotBeNil)
 		})
 		Convey("Modifying the own control address is not allowed", func() {
 			brInfo := topo.BR[v.id]
-			brInfo.CtrlAddrs.Overlay = overlay.IPv6
+			brInfo.CtrlAddrs.UnderlayAddress = &net.UDPAddr{Port: 42}
 			topo.BR[v.id] = brInfo
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldNotBeNil)
 		})
diff --git a/go/lib/infra/modules/trust/trust_test.go b/go/lib/infra/modules/trust/trust_test.go
index 49993c0fa..246b91856 100644
--- a/go/lib/infra/modules/trust/trust_test.go
+++ b/go/lib/infra/modules/trust/trust_test.go
@@ -42,10 +42,8 @@ import (
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/snet"
 	"github.com/scionproto/scion/go/lib/topology"
-	"github.com/scionproto/scion/go/lib/topology/topotestutil"
 	"github.com/scionproto/scion/go/lib/xtest"
 	"github.com/scionproto/scion/go/lib/xtest/p2p"
-	"github.com/scionproto/scion/go/proto"
 )
 
 const (
@@ -642,8 +640,8 @@ func initStore(t *testing.T, ctrl *gomock.Controller,
 	db, err := trustdbsqlite.New(":memory:")
 	xtest.FailOnErr(t, err)
 	topo := topology.NewTopo()
-	topotestutil.AddServer(topo, proto.ServiceType_cs, "foo",
-		topology.TestTopoAddr(nil, nil, nil, nil))
+	topo.CSNames = append(topo.CSNames, "foo")
+	topo.CS["foo"] = topology.TopoAddr{}
 	cfg := Config{
 		TopoProvider: &itopotest.TestTopoProvider{
 			Topo: topo,
diff --git a/go/lib/topology/addr_test.go b/go/lib/topology/addr_test.go
index 6a463c153..2b2d38685 100644
--- a/go/lib/topology/addr_test.go
+++ b/go/lib/topology/addr_test.go
@@ -15,14 +15,12 @@
 package topology
 
 import (
-	"fmt"
 	"net"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 
 	"github.com/scionproto/scion/go/lib/addr"
-	"github.com/scionproto/scion/go/lib/topology/overlay"
 	"github.com/scionproto/scion/go/lib/xtest"
 )
 
@@ -37,184 +35,258 @@ var (
 	bindBad    = &RawAddrPort{"BadIPAddress", 40000}
 )
 
-func Test_topoAddrFromRaw(t *testing.T) {
-	var basic_tests = []struct {
-		name    string
-		overlay overlay.Type
-		err     error
-		ram     RawAddrMap
-		ipv4    *pubBindAddr
-		ipv6    *pubBindAddr
+func TestRawAddrMap_ToTopoAddr(t *testing.T) {
+	testCases := []struct {
+		name string
+		err  error
+		ram  RawAddrMap
+		addr *TopoAddr
 	}{
-		// Common Errors
-		{"Unsupported Overlay", overlay.Invalid,
-			ErrUnsupportedOverlay,
-			nil, nil, nil},
-		{"Unsupported Address Type", overlay.IPv4,
-			ErrUnsupportedAddrType,
-			newRAMError("MPLS", pubIPv4, nil), nil, nil},
-		{"No Addresses", overlay.IPv4,
-			ErrAtLeastOnePub,
-			make(RawAddrMap), nil, nil},
-		// IPv4 Errors
-		{"IPv4 Overlay Mismatch", overlay.IPv6,
-			ErrMismatchOverlayAddr,
-			newRAM(pubIPv4, nil, nil, nil), nil, nil},
-		{"IPv4 Invalid Pub Address", overlay.IPv4,
-			ErrMismatchPubAddrType,
-			newRAM(pubIPv6, nil, nil, nil), nil, nil},
-		{"IPv4 Invalid Bind Address", overlay.IPv4,
-			ErrMismatchBindAddrType,
-			newRAM(pubIPv4, nil, bindIPv6, nil), nil, nil},
-		{"IPv4 Same Pub/Bind Address", overlay.IPv4,
-			ErrBindAddrEqPubAddr,
-			newRAM(pubIPv4, nil, &pubIPv4.RawAddrPort, nil), nil, nil},
-		// IPv6 Errors
-		{"IPv6 Overlay Mismatch", overlay.IPv4,
-			ErrMismatchOverlayAddr,
-			newRAM(nil, pubIPv6, nil, nil), nil, nil},
-		{"IPv6 Invalid Pub Address", overlay.IPv6,
-			ErrMismatchPubAddrType,
-			newRAM(nil, pubIPv4, nil, nil), nil, nil},
-		{"IPv6 Invalid Bind Address", overlay.IPv6,
-			ErrMismatchBindAddrType,
-			newRAM(nil, pubIPv6, nil, bindIPv4), nil, nil},
-		{"IPv6 Same Pub/Bind Address", overlay.IPv6,
-			ErrBindAddrEqPubAddr,
-			newRAM(nil, pubIPv6, nil, &pubIPv6.RawAddrPort), nil, nil},
-		// IPv46
-		{"IPv46 PubBind", overlay.IPv46, nil,
-			newRAM(pubIPv4, pubIPv6, bindIPv4, bindIPv6),
-			&pubBindAddr{newPub(pubIPv4), newBind(bindIPv4), newOverlay(pubIPv4)},
-			&pubBindAddr{newPub(pubIPv6), newBind(bindIPv6), newOverlay(pubIPv6)}},
-		// IPv46+UDP
-		{"IPv46+UDP PubBind", overlay.UDPIPv46, nil,
-			newRAM(pubUDPIPv4, pubUDPIPv6, bindIPv4, bindIPv6),
-			&pubBindAddr{newPub(pubUDPIPv4), newBind(bindIPv4), newOverlay(pubUDPIPv4)},
-			&pubBindAddr{newPub(pubUDPIPv6), newBind(bindIPv6), newOverlay(pubUDPIPv6)}},
+		{
+			name: "No addresses",
+			err:  ErrAtLeastOnePub,
+			ram:  make(RawAddrMap),
+			addr: nil,
+		},
+		{
+			name: "IPv4 invalid address",
+			err:  ErrInvalidPub,
+			ram: RawAddrMap{
+				"IPv4": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "foo",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 empty address",
+			err:  ErrInvalidPub,
+			ram: RawAddrMap{
+				"IPv4": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 address in IPv4 property",
+			err:  ErrInvalidPub,
+			ram: RawAddrMap{
+				"IPv4": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "2001:db8:f00:b43::1",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 address in IPv6 property",
+			err:  ErrInvalidPub,
+			ram: RawAddrMap{
+				"IPv6": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 invalid address",
+			err:  ErrInvalidPub,
+			ram: RawAddrMap{
+				"IPv6": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "foo",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 empty address",
+			err:  ErrInvalidPub,
+			ram: RawAddrMap{
+				"IPv6": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 good address",
+			ram: RawAddrMap{
+				"IPv4": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: &TopoAddr{
+				SCIONAddress: &addr.AppAddr{
+					L3: addr.HostFromIP(net.IP{192, 168, 1, 1}),
+					L4: 42,
+				},
+				UnderlayAddress: &net.UDPAddr{
+					IP:   net.IP{192, 168, 1, 1},
+					Port: 30041,
+				},
+			},
+		},
+		{
+			name: "IPv6 good address",
+			ram: RawAddrMap{
+				"IPv6": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "2001:db8:f00:b43::1",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: &TopoAddr{
+				SCIONAddress: &addr.AppAddr{
+					L3: addr.HostFromIP(net.ParseIP("2001:db8:f00:b43::1")),
+					L4: 42,
+				},
+				UnderlayAddress: &net.UDPAddr{
+					IP:   net.ParseIP("2001:db8:f00:b43::1"),
+					Port: 30041,
+				},
+			},
+		},
+		{
+			name: "IPv4 with bind",
+			err:  ErrBindNotSupported,
+			ram: RawAddrMap{
+				"IPv4": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+					},
+					Bind: &RawAddrPort{},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 with bind",
+			err:  ErrBindNotSupported,
+			ram: RawAddrMap{
+				"IPv6": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "2001:db8:f00:b43::1",
+							L4Port: 42,
+						},
+					},
+					Bind: &RawAddrPort{},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 with custom underlay",
+			err:  ErrCustomUnderlayPortNotSupported,
+			ram: RawAddrMap{
+				"IPv4": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+						OverlayPort: 73,
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 with custom underlay",
+			err:  ErrCustomUnderlayPortNotSupported,
+			ram: RawAddrMap{
+				"IPv6": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "2001:db8:f00:b43::1",
+							L4Port: 42,
+						},
+						OverlayPort: 73,
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 with IPv6",
+			ram: RawAddrMap{
+				"IPv4": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+					},
+				},
+				"IPv6": &RawPubBindOverlay{
+					Public: RawAddrPortOverlay{
+						RawAddrPort: RawAddrPort{
+							Addr:   "2001:db8:f00:b43::1",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: &TopoAddr{
+				SCIONAddress: &addr.AppAddr{
+					L3: addr.HostFromIP(net.ParseIP("2001:db8:f00:b43::1")),
+					L4: 42,
+				},
+				UnderlayAddress: &net.UDPAddr{
+					IP:   net.ParseIP("2001:db8:f00:b43::1"),
+					Port: 30041,
+				},
+			},
+		},
 	}
-	for i, test := range basic_tests {
-		desc := fmt.Sprintf("topoAddrFromRaw_%d. %s", i, test.name)
-		exp := &TopoAddr{
-			IPv4:    test.ipv4,
-			IPv6:    test.ipv6,
-			Overlay: test.overlay,
-		}
-		t.Run(desc, func(t *testing.T) {
-			ta, err := topoAddrFromRAM(test.ram, test.overlay)
-			xtest.AssertErrorsIs(t, err, test.err)
-			if test.err == nil {
-				assert.Equal(t, exp, ta)
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			topoAddr, err := tc.ram.ToTopoAddr()
+			xtest.AssertErrorsIs(t, err, tc.err)
+			if tc.err == nil {
+				assert.Equal(t, tc.addr, topoAddr)
 			}
 		})
 	}
 }
-
-func Test_pubBindAddr(t *testing.T) {
-	var basic_tests = []struct {
-		name       string
-		udpOverlay bool
-		pub        *RawAddrPortOverlay
-		overlay    *RawAddrPortOverlay
-		bind       *RawAddrPort
-		err        error
-	}{
-		// Errors
-		{"Invaild Public IP Address", false, pubBad, nil, nil, ErrInvalidPub},
-		{"Invaild Bind IP Address", false, pubIPv4, nil, bindBad, ErrInvalidBind},
-		// IPv4 Overlay
-		{"IPv4 Pub", false, pubIPv4, nil, nil, nil},
-		{"IPv4 PubBind", false, pubIPv4, nil, bindIPv4, nil},
-		// IPv4+UDP Overlay
-		{"IPv4+UDP Pub", true, pubUDPIPv4, nil, nil, nil},
-		{"IPv4+UDP Pub Default Port", true, pubIPv4, pubUDPIPv4, nil, nil},
-		{"IPv4+UDP PubBind", true, pubUDPIPv4, nil, bindIPv4, nil},
-		// IPv6 Overlay
-		{"IPv6 Pub", false, pubIPv6, nil, nil, nil},
-		{"IPv6 PubBind", false, pubIPv6, nil, bindIPv6, nil},
-		// IPv6+UDP Overlay
-		{"IPv6+UDP Pub", true, pubUDPIPv6, nil, nil, nil},
-		{"IPv6+UDP Pub Default Port", true, pubIPv6, pubUDPIPv6, nil, nil},
-		{"IPv6+UDP PubBind", true, pubUDPIPv6, nil, bindIPv6, nil},
-	}
-	for i, test := range basic_tests {
-		desc := fmt.Sprintf("pubBindAddr_%d. %s", i, test.name)
-		rpbo := &RawPubBindOverlay{*test.pub, test.bind}
-		overlay := test.pub
-		if test.overlay != nil {
-			overlay = test.overlay
-		}
-		exp := &pubBindAddr{newPub(test.pub), newBind(test.bind), newOverlay(overlay)}
-		t.Run(desc, func(t *testing.T) {
-			pbo := &pubBindAddr{}
-			err := pbo.fromRaw(rpbo, test.udpOverlay)
-			xtest.AssertErrorsIs(t, err, test.err)
-			if test.err == nil {
-				assert.Equal(t, exp, pbo)
-			}
-		})
-	}
-}
-
-func newRAM(pub4, pub6 *RawAddrPortOverlay, bind4, bind6 *RawAddrPort) RawAddrMap {
-	rai := make(RawAddrMap)
-	if pub4 != nil {
-		rai["IPv4"] = &RawPubBindOverlay{Public: *pub4, Bind: bind4}
-	}
-	if pub6 != nil {
-		rai["IPv6"] = &RawPubBindOverlay{Public: *pub6, Bind: bind6}
-	}
-	return rai
-}
-
-func newRAMError(t string, pub *RawAddrPortOverlay, bind *RawAddrPort) RawAddrMap {
-	ram := make(RawAddrMap)
-	ram[t] = &RawPubBindOverlay{Public: *pub, Bind: bind}
-	return ram
-}
-
-func shouldEqTopoAddr(actual interface{}, expected ...interface{}) string {
-	//fmt.Printf("\nExpected: %+v\nActual: %+v", expected[0], actual)
-	if actual.(*TopoAddr).Equal(expected[0].(*TopoAddr)) {
-		return ""
-	}
-	return fmt.Sprintf("Expected:\n\t%+v\nActual:\n\t%+v", expected[0], actual)
-}
-
-func newPub(rapo *RawAddrPortOverlay) *addr.AppAddr {
-	if rapo == nil {
-		return nil
-	}
-	return &addr.AppAddr{
-		L3: addr.HostFromIPStr(rapo.Addr),
-		L4: uint16(rapo.L4Port),
-	}
-}
-
-func newBind(rap *RawAddrPort) *addr.AppAddr {
-	if rap == nil {
-		return nil
-	}
-	return &addr.AppAddr{
-		L3: addr.HostFromIPStr(rap.Addr),
-		L4: uint16(rap.L4Port),
-	}
-}
-
-func newOverlay(rapo *RawAddrPortOverlay) *overlay.OverlayAddr {
-	if rapo == nil {
-		return nil
-	}
-	ip := net.ParseIP(rapo.Addr)
-	if ip.To4() != nil {
-		ip = ip.To4()
-	}
-	return overlay.NewOverlayAddr(ip, uint16(rapo.OverlayPort))
-}
-
-func shouldEqPubBindAddr(actual interface{}, expected ...interface{}) string {
-	if actual.(*pubBindAddr).Equal(expected[0].(*pubBindAddr)) {
-		return ""
-	}
-	return fmt.Sprintf("Expected:\n\t%+v\nActual:\n\t%+v", expected[0], actual)
-}
diff --git a/go/lib/topology/testdata/basic.json b/go/lib/topology/testdata/basic.json
index ce25f575f..c0e9ca8a6 100644
--- a/go/lib/topology/testdata/basic.json
+++ b/go/lib/topology/testdata/basic.json
@@ -9,39 +9,81 @@
     "BorderRouters": {
         "br1-ff00:0:311-1": {
             "InternalAddrs": {
-                "IPv4": {"PublicOverlay": {"Addr": "10.1.0.1"}},
-                "IPv6": {"PublicOverlay": {"Addr": "2001:db8:a0b:12f0::1"}}
+                "IPv4": {
+                    "PublicOverlay": {
+                        "Addr": "10.1.0.1"
+                    }
+                },
+                "IPv6": {
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::1"
+                    }
+                }
             },
             "CtrlAddr": {
-                "IPv4": {"Public": {"Addr": "10.1.0.1", "L4Port": 30098}},
-                "IPv6": {"Public": {"Addr": "2001:db8:a0b:12f0::1", "L4Port": 30098}}
+                "IPv4": {
+                    "Public": {
+                        "Addr": "10.1.0.1",
+                        "L4Port": 30098
+                    }
+                },
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:a0b:12f0::1",
+                        "L4Port": 30098
+                    }
+                }
             },
             "Interfaces": {
                 "1": {
                     "Overlay": "UDP/IPv4",
-                    "BindOverlay": {"Addr": "10.0.0.1"},
-                    "PublicOverlay": {"Addr": "192.0.2.1", "OverlayPort": 44997},
-                    "RemoteOverlay": {"Addr": "192.0.2.2", "OverlayPort": 44998},
+                    "BindOverlay": {
+                        "Addr": "10.0.0.1"
+                    },
+                    "PublicOverlay": {
+                        "Addr": "192.0.2.1",
+                        "OverlayPort": 44997
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "192.0.2.2",
+                        "OverlayPort": 44998
+                    },
                     "Bandwidth": 1000,
                     "ISD_AS": "1-ff00:0:312",
                     "LinkTo": "PARENT",
                     "MTU": 1472
                 },
                 "3": {
-                    "Overlay": "IPv6",
-                    "PublicOverlay": {"Addr": "2001:db8:a0b:12f0::1"},
-                    "RemoteOverlay": {"Addr":"2001:db8:a0b:12f0::2"},
-                    "BindOverlay": {"Addr":"2001:db8:a0b:12f0::8"},
+                    "Overlay": "UDP/IPv6",
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::1",
+                        "OverlayPort": 44997
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::2",
+                        "OverlayPort": 44998
+                    },
+                    "BindOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::8"
+                    },
                     "Bandwidth": 5000,
                     "ISD_AS": "1-ff00:0:314",
                     "LinkTo": "CHILD",
                     "MTU": 4430
                 },
                 "8": {
-                    "Overlay": "IPv4",
-                    "BindOverlay": {"Addr": "10.0.0.2"},
-                    "PublicOverlay": {"Addr": "192.0.2.2"},
-                    "RemoteOverlay": {"Addr": "192.0.2.3"},
+                    "Overlay": "UDP/IPv4",
+                    "BindOverlay": {
+                        "Addr": "10.0.0.2"
+                    },
+                    "PublicOverlay": {
+                        "Addr": "192.0.2.2",
+                        "OverlayPort": 44997
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "192.0.2.3",
+                        "OverlayPort": 44998
+                    },
                     "Bandwidth": 2000,
                     "ISD_AS": "1-ff00:0:313",
                     "LinkTo": "PEER",
@@ -51,55 +93,173 @@
         }
     },
     "BeaconService": {
-        "bs1-ff00:0:311-1": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.65", "L4Port": 30054}}}},
-        "bs1-ff00:0:311-2": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::65", "L4Port": 30054}}}},
-        "bs1-ff00:0:311-3": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::123", "L4Port": 10054}},
-            "IPv4": {"Public": {"Addr": "127.0.0.123", "L4Port": 10054}}}}
+        "bs1-ff00:0:311-1": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.65",
+                        "L4Port": 30054
+                    }
+                }
+            }
+        },
+        "bs1-ff00:0:311-2": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::65",
+                        "L4Port": 30054
+                    }
+                }
+            }
+        },
+        "bs1-ff00:0:311-3": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::123",
+                        "L4Port": 10054
+                    }
+                },
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.123",
+                        "L4Port": 10054
+                    }
+                }
+            }
+        }
     },
     "CertificateService": {
-        "cs1-ff00:0:311-1": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.66", "L4Port": 30081},
-                     "Bind": {"Addr": "127.0.0.67", "L4Port": 30081}}}
+        "cs1-ff00:0:311-2": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.67",
+                        "L4Port": 30073
+                    }
+                }
+            }
         },
-        "cs1-ff00:0:311-2": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.67", "L4Port": 30073}}}},
-        "cs1-ff00:0:311-3": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::1", "L4Port": 23421}}}},
-        "cs1-ff00:0:311-4": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::2", "L4Port": 23421},
-                     "Bind": {"Addr": "2001:db8:1714::1", "L4Port": 13373}}}}
+        "cs1-ff00:0:311-3": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::1",
+                        "L4Port": 23421
+                    }
+                }
+            }
+        }
     },
     "PathService": {
-        "ps1-ff00:0:311-1": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.73", "L4Port": 30091}}}},
-        "ps1-ff00:0:311-2": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::73", "L4Port": 30091}}}}
+        "ps1-ff00:0:311-1": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.73",
+                        "L4Port": 30091
+                    }
+                }
+            }
+        },
+        "ps1-ff00:0:311-2": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::73",
+                        "L4Port": 30091
+                    }
+                }
+            }
+        }
     },
     "SibraService": {
-        "sb1-ff00:0:311-1": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.76", "L4Port": 30058}}}},
-        "sb1-ff00:0:311-2": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::76", "L4Port": 30058}}}}
+        "sb1-ff00:0:311-1": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.76",
+                        "L4Port": 30058
+                    }
+                }
+            }
+        },
+        "sb1-ff00:0:311-2": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::76",
+                        "L4Port": 30058
+                    }
+                }
+            }
+        }
     },
     "RainsService": {
-        "rs1-ff00:0:311-1": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.78", "L4Port": 30098}}}},
-        "rs1-ff00:0:311-2": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::78", "L4Port": 30098}}}}
+        "rs1-ff00:0:311-1": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.78",
+                        "L4Port": 30098
+                    }
+                }
+            }
+        },
+        "rs1-ff00:0:311-2": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::78",
+                        "L4Port": 30098
+                    }
+                }
+            }
+        }
     },
     "SIG": {
-        "sig1-ff00:0:311-1": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.82", "L4Port": 30100}}}},
-        "sig1-ff00:0:311-2": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::82", "L4Port": 30100}}}}
+        "sig1-ff00:0:311-1": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.82",
+                        "L4Port": 30100
+                    }
+                }
+            }
+        },
+        "sig1-ff00:0:311-2": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::82",
+                        "L4Port": 30100
+                    }
+                }
+            }
+        }
     },
     "DiscoveryService": {
-        "ds1-ff00:0:311-1": {"Addrs": {
-            "IPv4": {"Public": {"Addr": "127.0.0.99", "L4Port": 53535}}}},
-        "ds1-ff00:0:311-2": {"Addrs": {
-            "IPv6": {"Public": {"Addr": "2001:db8:f00:b43::99", "L4Port": 53535}}}}
+        "ds1-ff00:0:311-1": {
+            "Addrs": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "127.0.0.99",
+                        "L4Port": 53535
+                    }
+                }
+            }
+        },
+        "ds1-ff00:0:311-2": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::99",
+                        "L4Port": 53535
+                    }
+                }
+            }
+        }
     }
 }
diff --git a/go/lib/topology/testdata/core.json b/go/lib/topology/testdata/core.json
index 9a776b7f9..40c38568c 100644
--- a/go/lib/topology/testdata/core.json
+++ b/go/lib/topology/testdata/core.json
@@ -8,19 +8,45 @@
     "BorderRouters": {
         "borderrouter6-ff00:0:362-1": {
             "InternalAddrs": {
-                "IPv4": {"PublicOverlay": {"Addr": "10.1.0.1"}},
-                "IPv6": {"PublicOverlay": {"Addr": "2001:db8:a0b:12f0::1"}}
+                "IPv4": {
+                    "PublicOverlay": {
+                        "Addr": "10.1.0.1"
+                    }
+                },
+                "IPv6": {
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::1"
+                    }
+                }
             },
             "CtrlAddr": {
-                "IPv4": {"Public": {"Addr": "10.1.0.1", "L4Port": 30098}},
-                "IPv6": {"Public": {"Addr": "2001:db8:a0b:12f0::1", "L4Port": 30098}}
+                "IPv4": {
+                    "Public": {
+                        "Addr": "10.1.0.1",
+                        "L4Port": 30098
+                    }
+                },
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:a0b:12f0::1",
+                        "L4Port": 30098
+                    }
+                }
             },
             "Interfaces": {
                 "91": {
                     "Overlay": "UDP/IPv4",
-                    "BindOverlay": {"Addr": "10.0.0.1"},
-                    "PublicOverlay": {"Addr": "192.0.2.1", "OverlayPort": 4997},
-                    "RemoteOverlay": {"Addr": "192.0.2.2", "OverlayPort": 4998},
+                    "BindOverlay": {
+                        "Addr": "10.0.0.1"
+                    },
+                    "PublicOverlay": {
+                        "Addr": "192.0.2.1",
+                        "OverlayPort": 4997
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "192.0.2.2",
+                        "OverlayPort": 4998
+                    },
                     "Bandwidth": 100000,
                     "ISD_AS": "6-ff00:0:363",
                     "LinkTo": "CORE",
@@ -30,19 +56,45 @@
         },
         "borderrouter6-ff00:0:362-9": {
             "InternalAddrs": {
-                "IPv4": {"PublicOverlay": {"Addr": "10.1.0.2"}},
-                "IPv6": {"PublicOverlay": {"Addr": "2001:db8:a0b:12f0::2"}}
+                "IPv4": {
+                    "PublicOverlay": {
+                        "Addr": "10.1.0.2"
+                    }
+                },
+                "IPv6": {
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::2"
+                    }
+                }
             },
             "CtrlAddr": {
-                "IPv4": {"Public": {"Addr": "10.1.0.2", "L4Port": 3098}},
-                "IPv6": {"Public": {"Addr": "2001:db8:a0b:12f0::2", "L4Port": 3098}}
+                "IPv4": {
+                    "Public": {
+                        "Addr": "10.1.0.2",
+                        "L4Port": 30098
+                    }
+                },
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:a0b:12f0::2",
+                        "L4Port": 30098
+                    }
+                }
             },
             "Interfaces": {
                 "32": {
-                    "Overlay": "IPv6",
-                    "PublicOverlay": {"Addr": "2001:db8:a0b:12f0::1"},
-                    "RemoteOverlay": {"Addr":"2001:db8:a0b:12f0::2"},
-                    "BindOverlay": {"Addr":"2001:db8:a0b:12f0::8"},
+                    "Overlay": "UDP/IPv6",
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::1",
+                        "OverlayPort": 4997
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::2",
+                        "OverlayPort": 4998
+                    },
+                    "BindOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::8"
+                    },
                     "Bandwidth": 5000,
                     "ISD_AS": "6-ff00:0:364",
                     "LinkTo": "CHILD",
diff --git a/go/lib/topology/topology_test.go b/go/lib/topology/topology_test.go
index e72627b12..d30354a5c 100644
--- a/go/lib/topology/topology_test.go
+++ b/go/lib/topology/topology_test.go
@@ -18,6 +18,7 @@ package topology
 import (
 	"encoding/json"
 	"fmt"
+	"net"
 	"testing"
 	"time"
 
@@ -28,65 +29,12 @@ import (
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/topology/overlay"
+	"github.com/scionproto/scion/go/lib/xtest"
 	"github.com/scionproto/scion/go/proto"
 )
 
-var testTopo *Topo
-
-func mkO(l3 addr.HostAddr, op int) *overlay.OverlayAddr {
-	if l3 == nil {
-		return nil
-	}
-	return overlay.NewOverlayAddr(l3.IP(), uint16(op))
-}
-
-func mkPBO(ip string, port int, bindip string, bindport int, op int) *pubBindAddr {
-	pub := addr.HostFromIPStr(ip)
-	pbo := &pubBindAddr{}
-	pbo.pub = &addr.AppAddr{L3: pub}
-	if port != 0 {
-		pbo.pub.L4 = uint16(port)
-	}
-	if bindip != "" {
-		pbo.bind = &addr.AppAddr{L3: addr.HostFromIPStr(bindip)}
-		if bindport != 0 {
-			pbo.bind.L4 = uint16(bindport)
-		}
-	}
-	pbo.overlay = mkO(pub, op)
-	return pbo
-}
-
-func mkOB(ip string, port int, bindip string) *OverBindAddr {
-	overlay := addr.HostFromIPStr(ip)
-	var bind addr.HostAddr
-	if bindip != "" {
-		bind = addr.HostFromIPStr(bindip)
-	}
-	return &OverBindAddr{
-		PublicOverlay: mkO(overlay, port),
-		BindOverlay:   mkO(bind, port),
-	}
-}
-
-func mkTAv4(ip string, port int, bindip string, bindport int, ot overlay.Type, op int) TopoAddr {
-	return TopoAddr{IPv4: mkPBO(ip, port, bindip, bindport, op), Overlay: ot}
-}
-
-func mkTAv6(ip string, port int, bindip string, bindport int, ot overlay.Type, op int) TopoAddr {
-	return TopoAddr{IPv6: mkPBO(ip, port, bindip, bindport, op), Overlay: ot}
-}
-
-func loadTopo(filename string, t *testing.T) {
-	topo, err := LoadFromFile(filename)
-	require.NoError(t, err, "Error loading config from '%s': %v", filename, err)
-	testTopo = topo
-}
-
 func TestMeta(t *testing.T) {
-	fn := "testdata/basic.json"
-	loadTopo(fn, t)
-	c := testTopo
+	c := MustLoadTopo(t, "testdata/basic.json")
 	assert.Equal(t, time.Unix(168570123, 0), c.Timestamp, "Field 'Timestamp'")
 	// Is testing this piece of data really useful?
 	assert.Contains(t, c.TimestampHuman, "1975-05-06 01:02:03.000000+0000",
@@ -99,26 +47,26 @@ func TestMeta(t *testing.T) {
 }
 
 func Test_Active(t *testing.T) {
-	fn := "testdata/basic.json"
-	Convey("Checking Active", t, func() {
-		loadTopo(fn, t)
-		c := testTopo
-		Convey("Given a positive TTL", func() {
-			SoMsg("Before TS inactive", c.Active(c.Timestamp.Add(-time.Second)), ShouldBeFalse)
-			SoMsg("TS in active range", c.Active(c.Timestamp), ShouldBeTrue)
-			SoMsg("End of active range", c.Active(c.Timestamp.Add(c.TTL-1)), ShouldBeTrue)
-			SoMsg("Expired inactive", c.Active(c.Timestamp.Add(time.Hour)), ShouldBeFalse)
-		})
-		Convey("Given a zero TTL", func() {
-			c.TTL = 0
-			SoMsg("Before TS inactive", c.Active(c.Timestamp.Add(-time.Second)), ShouldBeFalse)
-			SoMsg("TS in active range", c.Active(c.Timestamp), ShouldBeTrue)
-			SoMsg("Distant time active", c.Active(c.Timestamp.Add(100*time.Hour)), ShouldBeTrue)
-		})
+	t.Run("positive TTL", func(t *testing.T) {
+		c := MustLoadTopo(t, "testdata/basic.json")
+		assert.False(t, c.Active(c.Timestamp.Add(-time.Second)))
+		assert.True(t, c.Active(c.Timestamp))
+		assert.True(t, c.Active(c.Timestamp.Add(c.TTL-1)))
+		assert.False(t, c.Active(c.Timestamp.Add(time.Hour)))
+	})
+	t.Run("zero TTL", func(t *testing.T) {
+		c := MustLoadTopo(t, "testdata/basic.json")
+		c.TTL = 0
+		assert.False(t, c.Active(c.Timestamp.Add(-time.Second)))
+		assert.True(t, c.Active(c.Timestamp))
+		assert.True(t, c.Active(c.Timestamp.Add(100*time.Hour)))
+
 	})
 }
 
 func Test_BRs(t *testing.T) {
+	c := MustLoadTopo(t, "testdata/basic.json")
+
 	brs := map[string]BRInfo{
 		"br1-ff00:0:311-1": {
 			IFIDs: []common.IFIDType{1, 3, 8},
@@ -126,9 +74,6 @@ func Test_BRs(t *testing.T) {
 	}
 	brn := []string{"br1-ff00:0:311-1"}
 
-	fn := "testdata/basic.json"
-	loadTopo(fn, t)
-	c := testTopo
 	for name, info := range brs {
 		Convey(fmt.Sprintf("Checking BR details for %s", name), t, func() {
 			So(c.BR, ShouldContainKey, name)
@@ -146,21 +91,29 @@ func Test_BRs(t *testing.T) {
 }
 
 func TestServiceDetails(t *testing.T) {
-	fn := "testdata/basic.json"
-	ot := overlay.IPv46
-	// We do this just for CSs since the code for the other non-BR
+	c := MustLoadTopo(t, "testdata/basic.json")
 	cses := IDAddrMap{
-		// v4 with bind
-		"cs1-ff00:0:311-1": mkTAv4("127.0.0.66", 30081, "127.0.0.67", 30081, ot, 0),
-		// v4 without bind
-		"cs1-ff00:0:311-2": mkTAv4("127.0.0.67", 30073, "", 0, ot, 0),
-		// v6 without bind
-		"cs1-ff00:0:311-3": mkTAv6("2001:db8:f00:b43::1", 23421, "", 0, ot, 0),
-		// v6 with bind
-		"cs1-ff00:0:311-4": mkTAv6("2001:db8:f00:b43::2", 23421, "2001:db8:1714::1", 13373, ot, 0),
+		"cs1-ff00:0:311-2": TopoAddr{
+			SCIONAddress: &addr.AppAddr{
+				L3: addr.HostFromIP(net.IP{127, 0, 0, 67}),
+				L4: 30073,
+			},
+			UnderlayAddress: &net.UDPAddr{
+				IP:   net.IP{127, 0, 0, 67},
+				Port: 30041,
+			},
+		},
+		"cs1-ff00:0:311-3": TopoAddr{
+			SCIONAddress: &addr.AppAddr{
+				L3: addr.HostFromIP(net.ParseIP("2001:db8:f00:b43::1")),
+				L4: 23421,
+			},
+			UnderlayAddress: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:f00:b43::1"),
+				Port: 30041,
+			},
+		},
 	}
-	loadTopo(fn, t)
-	c := testTopo
 	assert.Equal(t, cses, c.CS)
 }
 
@@ -168,142 +121,169 @@ func TestServiceCount(t *testing.T) {
 	// This just checks the count of all the service types, actual population
 	// testing is done elsewhere
 	// The simple counting check for CS is done in the detailed population test as well
-	fn := "testdata/basic.json"
-	loadTopo(fn, t)
-	c := testTopo
+	c := MustLoadTopo(t, "testdata/basic.json")
 	assert.Len(t, c.BS, 3, "BS")
 	assert.Len(t, c.PS, 2, "PS")
-	assert.Len(t, c.SB, 2, "SB")
-	assert.Len(t, c.RS, 2, "RS")
 	assert.Len(t, c.SIG, 2, "SIG")
 	assert.Len(t, c.DS, 2, "DS")
 }
 
 func TestIFInfoMap(t *testing.T) {
-	ifm := make(IfInfoMap)
-	isdas, _ := addr.IAFromString("1-ff00:0:312")
-	ifm[1] = IFInfo{
-		Id:     1,
-		BRName: "br1-ff00:0:311-1",
-		InternalAddrs: &TopoBRAddr{
-			IPv4:    mkOB("10.1.0.1", 0, ""),
-			IPv6:    mkOB("2001:db8:a0b:12f0::1", 0, ""),
-			Overlay: overlay.IPv46},
-		CtrlAddrs: &TopoAddr{
-			IPv4:    mkPBO("10.1.0.1", 30098, "", 0, 0),
-			IPv6:    mkPBO("2001:db8:a0b:12f0::1", 30098, "", 0, 0),
-			Overlay: overlay.IPv46},
-		Overlay: overlay.UDPIPv4,
-		Local: &TopoBRAddr{
-			IPv4:    mkOB("192.0.2.1", 44997, "10.0.0.1"),
-			Overlay: overlay.UDPIPv4},
-		Remote:    mkO(addr.HostFromIPStr("192.0.2.2"), 44998),
-		Bandwidth: 1000,
-		ISD_AS:    isdas,
-		LinkType:  proto.LinkType_parent,
-		MTU:       1472,
-	}
-	isdas, _ = addr.IAFromString("1-ff00:0:314")
-	ifm[3] = IFInfo{
-		Id:     3,
-		BRName: "br1-ff00:0:311-1",
-		InternalAddrs: &TopoBRAddr{
-			IPv4:    mkOB("10.1.0.1", 0, ""),
-			IPv6:    mkOB("2001:db8:a0b:12f0::1", 0, ""),
-			Overlay: overlay.IPv46},
-		CtrlAddrs: &TopoAddr{
-			IPv4:    mkPBO("10.1.0.1", 30098, "", 0, 0),
-			IPv6:    mkPBO("2001:db8:a0b:12f0::1", 30098, "", 0, 0),
-			Overlay: overlay.IPv46},
-		Overlay: overlay.IPv6,
-		Local: &TopoBRAddr{
-			IPv6:    mkOB("2001:db8:a0b:12f0::1", 0, "2001:db8:a0b:12f0::8"),
-			Overlay: overlay.IPv6},
-		Remote:    mkO(addr.HostFromIPStr("2001:db8:a0b:12f0::2"), 0),
-		Bandwidth: 5000,
-		ISD_AS:    isdas,
-		LinkType:  proto.LinkType_child,
-		MTU:       4430,
-	}
-	isdas, _ = addr.IAFromString("1-ff00:0:313")
-	ifm[8] = IFInfo{
-		Id:     8,
-		BRName: "br1-ff00:0:311-1",
-		InternalAddrs: &TopoBRAddr{
-			IPv4:    mkOB("10.1.0.1", 0, ""),
-			IPv6:    mkOB("2001:db8:a0b:12f0::1", 0, ""),
-			Overlay: overlay.IPv46},
-		CtrlAddrs: &TopoAddr{
-			IPv4:    mkPBO("10.1.0.1", 30098, "", 0, 0),
-			IPv6:    mkPBO("2001:db8:a0b:12f0::1", 30098, "", 0, 0),
-			Overlay: overlay.IPv46},
-		Overlay: overlay.IPv4,
-		Local: &TopoBRAddr{
-			IPv4:    mkOB("192.0.2.2", 0, "10.0.0.2"),
-			Overlay: overlay.IPv4},
-		Remote:    mkO(addr.HostFromIPStr("192.0.2.3"), 0),
-		Bandwidth: 2000,
-		ISD_AS:    isdas,
-		LinkType:  proto.LinkType_peer,
-		MTU:       1480,
+	c := MustLoadTopo(t, "testdata/basic.json")
+	ifm := IfInfoMap{
+		1: IFInfo{
+			Id:     1,
+			BRName: "br1-ff00:0:311-1",
+			InternalAddrs: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
+				Port: 0,
+			},
+			CtrlAddrs: &TopoAddr{
+				SCIONAddress: &addr.AppAddr{
+					L3: addr.HostFromIP(net.ParseIP("2001:db8:a0b:12f0::1")),
+					L4: 30098,
+				},
+				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::1"), Port: 30041},
+			},
+			Overlay: overlay.UDPIPv4,
+			Local: &net.UDPAddr{
+				IP:   net.IP{10, 0, 0, 1},
+				Port: 44997,
+			},
+			Remote: &net.UDPAddr{
+				IP:   net.IP{192, 0, 2, 2},
+				Port: 44998,
+			},
+			Bandwidth: 1000,
+			ISD_AS:    xtest.MustParseIA("1-ff00:0:312"),
+			LinkType:  proto.LinkType_parent,
+			MTU:       1472,
+		},
+		3: IFInfo{
+			Id:     3,
+			BRName: "br1-ff00:0:311-1",
+			InternalAddrs: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
+				Port: 0,
+			},
+			CtrlAddrs: &TopoAddr{
+				SCIONAddress: &addr.AppAddr{
+					L3: addr.HostFromIP(net.ParseIP("2001:db8:a0b:12f0::1")),
+					L4: 30098,
+				},
+				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::1"), Port: 30041},
+			},
+			Overlay: overlay.UDPIPv6,
+			Local: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::8"),
+				Port: 44997,
+			},
+			Remote: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::2"),
+				Port: 44998,
+			},
+			Bandwidth: 5000,
+			ISD_AS:    xtest.MustParseIA("1-ff00:0:314"),
+			LinkType:  proto.LinkType_child,
+			MTU:       4430,
+		},
+		8: IFInfo{
+			Id:     8,
+			BRName: "br1-ff00:0:311-1",
+			InternalAddrs: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
+				Port: 0,
+			},
+			CtrlAddrs: &TopoAddr{
+				SCIONAddress: &addr.AppAddr{
+					L3: addr.HostFromIP(net.ParseIP("2001:db8:a0b:12f0::1")),
+					L4: 30098,
+				},
+				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::1"), Port: 30041},
+			},
+			Overlay: overlay.UDPIPv4,
+			Local: &net.UDPAddr{
+				IP:   net.IP{10, 0, 0, 2},
+				Port: 44997,
+			},
+			Remote: &net.UDPAddr{
+				IP:   net.IP{192, 0, 2, 3},
+				Port: 44998,
+			},
+			Bandwidth: 2000,
+			ISD_AS:    xtest.MustParseIA("1-ff00:0:313"),
+			LinkType:  proto.LinkType_peer,
+			MTU:       1480,
+		},
 	}
-	fn := "testdata/basic.json"
-	loadTopo(fn, t)
-	assert.Equal(t, ifm, testTopo.IFInfoMap)
+	assert.Equal(t, ifm, c.IFInfoMap)
 }
 
 func TestIFInfoMapCoreAS(t *testing.T) {
-	ifm := make(IfInfoMap)
-	isdas, _ := addr.IAFromString("6-ff00:0:363")
-	ifm[91] = IFInfo{
-		Id:     91,
-		BRName: "borderrouter6-ff00:0:362-1",
-		InternalAddrs: &TopoBRAddr{
-			IPv4:    mkOB("10.1.0.1", 0, ""),
-			IPv6:    mkOB("2001:db8:a0b:12f0::1", 0, ""),
-			Overlay: overlay.IPv46},
-		CtrlAddrs: &TopoAddr{
-			IPv4:    mkPBO("10.1.0.1", 30098, "", 0, 0),
-			IPv6:    mkPBO("2001:db8:a0b:12f0::1", 30098, "", 0, 0),
-			Overlay: overlay.IPv46},
-		Overlay: overlay.UDPIPv4,
-		Local: &TopoBRAddr{
-			IPv4:    mkOB("192.0.2.1", 4997, "10.0.0.1"),
-			Overlay: overlay.UDPIPv4},
-		Remote:    mkO(addr.HostFromIPStr("192.0.2.2"), 4998),
-		Bandwidth: 100000,
-		ISD_AS:    isdas,
-		LinkType:  proto.LinkType_core,
-		MTU:       1472,
-	}
-	isdas, _ = addr.IAFromString("6-ff00:0:364")
-	ifm[32] = IFInfo{
-		Id:     32,
-		BRName: "borderrouter6-ff00:0:362-9",
-		InternalAddrs: &TopoBRAddr{
-			IPv4:    mkOB("10.1.0.2", 0, ""),
-			IPv6:    mkOB("2001:db8:a0b:12f0::2", 0, ""),
-			Overlay: overlay.IPv46},
-		CtrlAddrs: &TopoAddr{
-			IPv4:    mkPBO("10.1.0.2", 3098, "", 0, 0),
-			IPv6:    mkPBO("2001:db8:a0b:12f0::2", 3098, "", 0, 0),
-			Overlay: overlay.IPv46},
-		Overlay: overlay.IPv6,
-		Local: &TopoBRAddr{
-			IPv6:    mkOB("2001:db8:a0b:12f0::1", 0, "2001:db8:a0b:12f0::8"),
-			Overlay: overlay.IPv6},
-		Remote:    mkO(addr.HostFromIPStr("2001:db8:a0b:12f0::2"), 0),
-		Bandwidth: 5000,
-		ISD_AS:    isdas,
-		LinkType:  proto.LinkType_child,
-		MTU:       4430,
+	c := MustLoadTopo(t, "testdata/core.json")
+	ifm := IfInfoMap{
+		91: IFInfo{
+			Id:     91,
+			BRName: "borderrouter6-ff00:0:362-1",
+			InternalAddrs: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
+				Port: 0,
+			},
+			CtrlAddrs: &TopoAddr{
+				SCIONAddress: &addr.AppAddr{
+					L3: addr.HostFromIP(net.ParseIP("2001:db8:a0b:12f0::1")),
+					L4: 30098,
+				},
+				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::1"), Port: 30041},
+			},
+			Overlay: overlay.UDPIPv4,
+			Local: &net.UDPAddr{
+				IP:   net.IP{10, 0, 0, 1},
+				Port: 4997,
+			},
+			Remote: &net.UDPAddr{
+				IP:   net.IP{192, 0, 2, 2},
+				Port: 4998,
+			},
+			Bandwidth: 100000,
+			ISD_AS:    xtest.MustParseIA("6-ff00:0:363"),
+			LinkType:  proto.LinkType_core,
+			MTU:       1472,
+		},
+		32: IFInfo{
+			Id:     32,
+			BRName: "borderrouter6-ff00:0:362-9",
+			InternalAddrs: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::2"),
+				Port: 0,
+			},
+			CtrlAddrs: &TopoAddr{
+				SCIONAddress: &addr.AppAddr{
+					L3: addr.HostFromIP(net.ParseIP("2001:db8:a0b:12f0::2")),
+					L4: 30098,
+				},
+				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::2"), Port: 30041},
+			},
+			Overlay: overlay.UDPIPv6,
+			Local: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::8"),
+				Port: 4997,
+			},
+			Remote: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::2"),
+				Port: 4998,
+			},
+			Bandwidth: 5000,
+			ISD_AS:    xtest.MustParseIA("6-ff00:0:364"),
+			LinkType:  proto.LinkType_child,
+			MTU:       4430,
+		},
 	}
-	fn := "testdata/core.json"
-	loadTopo(fn, t)
-	assert.Equal(t, ifm, testTopo.IFInfoMap)
+	assert.Equal(t, ifm, c.IFInfoMap)
 }
 
 func TestBRsCoreAS(t *testing.T) {
+	c := MustLoadTopo(t, "testdata/core.json")
 	brCases := []struct {
 		name    string
 		intfids []common.IFIDType
@@ -311,9 +291,6 @@ func TestBRsCoreAS(t *testing.T) {
 		{name: "borderrouter6-ff00:0:362-1", intfids: []common.IFIDType{91}},
 		{name: "borderrouter6-ff00:0:362-9", intfids: []common.IFIDType{32}},
 	}
-	fn := "testdata/core.json"
-	loadTopo(fn, t)
-	c := testTopo
 	for _, test := range brCases {
 		Convey(fmt.Sprintf("Checking BR details for %s", test.name), t, func() {
 			Convey(fmt.Sprintf("Checking whether topo has a BR named %s", test.name), func() {
@@ -359,3 +336,357 @@ func TestTopoFromStripped(t *testing.T) {
 		SoMsg("errParse", err, ShouldBeNil)
 	})
 }
+
+func TestInternalDataPlanePort(t *testing.T) {
+	testCases := []struct {
+		Name            string
+		Map             RawBRAddrMap
+		ExpectedAddress *net.UDPAddr
+		ExpectedError   assert.ErrorAssertionFunc
+	}{
+		{
+			Name:          "Empty",
+			Map:           RawBRAddrMap{},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Bad IPv4 only",
+			Map: RawBRAddrMap{
+				"IPv4": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "foo",
+						OverlayPort: 42,
+					},
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Good IPv4 only",
+			Map: RawBRAddrMap{
+				"IPv4": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "127.0.0.1",
+						OverlayPort: 42,
+					},
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.IP{127, 0, 0, 1},
+				Port: 42,
+			},
+		},
+		{
+			Name: "IPv4 contains IPv6",
+			Map: RawBRAddrMap{
+				"IPv4": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "::1",
+						OverlayPort: 42,
+					},
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "IPv4 with bind underlay",
+			Map: RawBRAddrMap{
+				"IPv4": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "127.0.0.1",
+						OverlayPort: 42,
+					},
+					BindOverlay: &RawAddr{
+						Addr: "127.255.255.255",
+					},
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Bad IPv6 only",
+			Map: RawBRAddrMap{
+				"IPv6": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "foo",
+						OverlayPort: 42,
+					},
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Good IPv6 only",
+			Map: RawBRAddrMap{
+				"IPv6": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "::1",
+						OverlayPort: 42,
+					},
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.ParseIP("::1"),
+				Port: 42,
+			},
+		},
+		{
+			Name: "IPv6 contains IPv4",
+			Map: RawBRAddrMap{
+				"IPv6": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "127.0.0.1",
+						OverlayPort: 42,
+					},
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "IPv6 with bind underlay",
+			Map: RawBRAddrMap{
+				"IPv6": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "::1",
+						OverlayPort: 42,
+					},
+					BindOverlay: &RawAddr{
+						Addr: "2001:db8::1",
+					},
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Prefer IPv6 to IPv4",
+			Map: RawBRAddrMap{
+				"IPv4": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "127.0.0.1",
+						OverlayPort: 42,
+					},
+				},
+				"IPv6": &RawOverlayBind{
+					PublicOverlay: RawAddrOverlay{
+						Addr:        "::1",
+						OverlayPort: 73,
+					},
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.ParseIP("::1"),
+				Port: 73,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.Name, func(t *testing.T) {
+			topoBRAddr, err := tc.Map.ToUDPAddr()
+			tc.ExpectedError(t, err)
+			assert.Equal(t, tc.ExpectedAddress, topoBRAddr)
+		})
+	}
+}
+
+func TestExternalDataPlanePort(t *testing.T) {
+	testCases := []struct {
+		Name            string
+		Raw             *RawBRIntf
+		ExpectedAddress *net.UDPAddr
+		ExpectedError   assert.ErrorAssertionFunc
+	}{
+		{
+			Name:          "Empty",
+			Raw:           &RawBRIntf{},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Empty with overlay",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv4",
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Bad IPv4 only",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv4",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "foo",
+					OverlayPort: 42,
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Good IPv4 only",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv4",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "127.0.0.1",
+					OverlayPort: 42,
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.IP{127, 0, 0, 1},
+				Port: 42,
+			},
+		},
+		{
+			Name: "IPv4 contains IPv6",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv4",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "::1",
+					OverlayPort: 42,
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "IPv4 with bind underlay",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv4",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "127.0.0.1",
+					OverlayPort: 42,
+				},
+				BindOverlay: &RawAddr{
+					Addr: "127.255.255.255",
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.IP{127, 255, 255, 255},
+				Port: 42,
+			},
+		},
+		{
+			Name: "IPv4 with bad underlay",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv4",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "127.0.0.1",
+					OverlayPort: 42,
+				},
+				BindOverlay: &RawAddr{
+					Addr: "foo",
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "IPv4 with IPv6 underlay",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv4",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "127.0.0.1",
+					OverlayPort: 42,
+				},
+				BindOverlay: &RawAddr{
+					Addr: "::1",
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Bad IPv6 only",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv6",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "foo",
+					OverlayPort: 42,
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "Good IPv6 only",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv6",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "::1",
+					OverlayPort: 42,
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.ParseIP("::1"),
+				Port: 42,
+			},
+		},
+		{
+			Name: "IPv6 contains IPv4",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv6",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "127.0.0.1",
+					OverlayPort: 42,
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "IPv6 with bind underlay",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv6",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "::1",
+					OverlayPort: 42,
+				},
+				BindOverlay: &RawAddr{
+					Addr: "2001:db8::1",
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8::1"),
+				Port: 42,
+			},
+		},
+		{
+			Name: "IPv6 with bad underlay",
+			Raw: &RawBRIntf{
+				Overlay: "UDP/IPv6",
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "::1",
+					OverlayPort: 42,
+				},
+				BindOverlay: &RawAddr{
+					Addr: "foo",
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+		{
+			Name: "IPv6 with IPv4 underlay",
+			Raw: &RawBRIntf{
+				PublicOverlay: &RawAddrOverlay{
+					Addr:        "::1",
+					OverlayPort: 42,
+				},
+				BindOverlay: &RawAddr{
+					Addr: "127.0.0.1",
+				},
+			},
+			ExpectedError: assert.Error,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.Name, func(t *testing.T) {
+			topoBRAddr, err := tc.Raw.TopoBRAddr()
+			tc.ExpectedError(t, err)
+			assert.Equal(t, tc.ExpectedAddress, topoBRAddr)
+		})
+	}
+}
+
+func MustLoadTopo(t *testing.T, filename string) *Topo {
+	topo, err := LoadFromFile(filename)
+	require.NoError(t, err, "Error loading config from '%s': %v", filename, err)
+	return topo
+}
diff --git a/go/lib/topology/topotestutil/BUILD.bazel b/go/lib/topology/topotestutil/BUILD.bazel
deleted file mode 100644
index 49be88634..000000000
--- a/go/lib/topology/topotestutil/BUILD.bazel
+++ /dev/null
@@ -1,12 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-
-go_library(
-    name = "go_default_library",
-    srcs = ["topotestutil.go"],
-    importpath = "github.com/scionproto/scion/go/lib/topology/topotestutil",
-    visibility = ["//visibility:public"],
-    deps = [
-        "//go/lib/topology:go_default_library",
-        "//go/proto:go_default_library",
-    ],
-)
diff --git a/go/lib/topology/topotestutil/topotestutil.go b/go/lib/topology/topotestutil/topotestutil.go
deleted file mode 100644
index a6b5bc1c4..000000000
--- a/go/lib/topology/topotestutil/topotestutil.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2018 ETH Zurich, Anapaya Systems
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package topotestutil
-
-import (
-	"fmt"
-
-	"github.com/scionproto/scion/go/lib/topology"
-	"github.com/scionproto/scion/go/proto"
-)
-
-func AddServer(topo *topology.Topo, svc proto.ServiceType, name string,
-	topoAddr topology.TopoAddr) {
-
-	switch svc {
-	case proto.ServiceType_bs:
-		topo.BSNames = append(topo.BSNames, name)
-		topo.BS[name] = topoAddr
-	case proto.ServiceType_ps:
-		topo.PSNames = append(topo.PSNames, name)
-		topo.PS[name] = topoAddr
-	case proto.ServiceType_cs:
-		topo.CSNames = append(topo.CSNames, name)
-		topo.CS[name] = topoAddr
-	case proto.ServiceType_sb:
-		topo.SBNames = append(topo.SBNames, name)
-		topo.SB[name] = topoAddr
-	case proto.ServiceType_ds:
-		topo.DSNames = append(topo.DSNames, name)
-		topo.DS[name] = topoAddr
-	default:
-		panic(fmt.Sprintf("service type error %v", svc))
-	}
-}