Repository: scionproto/scion
Pull request #3440 (2019-11-29 10:31:52)

Simplify Topology-related packages and data types
This is a non-breaking change. The topology JSON format is not
affected.

In more detail:
- deleted topopruner because it was never used; we also no longer
support non-underlay binds and sibra, so the pruning is not needed;
- moved topology types from package itopo to package topology;
- moved most JSON-specific topology code to its own package;
- added test strictly for JSON serialization/deserialization;
- added strict name tags for all topology JSON fields; this prevents
refactors from inadvertently breaking backwards compatibility
re: the topology file format;
- removed duplicated fields from topology.RWTopology (used to be
topology.Topo);
- removed some duplicated APIs from topology.Topology (used to be
itopo.Topology);
- removed TimestampHuman topology field from high-level types, the
field was never used by applications; the field is still present
in the JSON low-level type, so applications that process the
JSON contents directly can still use it;
- added documentation for all exported entities in package topology;
- renamed non-idiomatic topology type fields;
- decoupled capnp proto-specific LinkType from most applications;
- renamed overlay to underlay in most application facing APIs;
this keeps names closer to established terminology, where
SCION is the overlay and the existing IP fabric is the underlay;

Fixes 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/3440)
<!-- Reviewable:end -->



Patch:
diff --git a/BUILD.bazel b/BUILD.bazel
index fde712f20..f384a9524 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -46,7 +46,6 @@ pkg_tar(
         "//go/integration/scmp_error_pyintegration:scmp_error_pyintegration",
         "//go/tools/scmp/scmp_integration:scmp_integration",
         "//go/acceptance/sig_ping_acceptance:sig_ping_acceptance",
-        "//go/tools/topopruner:topopruner",
     ],
     mode = "0755",
 )
diff --git a/go/beacon_srv/BUILD.bazel b/go/beacon_srv/BUILD.bazel
index 127b42f99..76d17bc55 100644
--- a/go/beacon_srv/BUILD.bazel
+++ b/go/beacon_srv/BUILD.bazel
@@ -37,6 +37,7 @@ go_library(
         "//go/lib/sock/reliable:go_default_library",
         "//go/lib/sock/reliable/reconnect:go_default_library",
         "//go/lib/spath:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/proto:go_default_library",
         "@com_github_burntsushi_toml//:go_default_library",
         "@com_github_opentracing_opentracing_go//:go_default_library",
diff --git a/go/beacon_srv/internal/beaconing/BUILD.bazel b/go/beacon_srv/internal/beaconing/BUILD.bazel
index 5958c1c82..ff3e066c7 100644
--- a/go/beacon_srv/internal/beaconing/BUILD.bazel
+++ b/go/beacon_srv/internal/beaconing/BUILD.bazel
@@ -27,7 +27,6 @@ go_library(
         "//go/lib/ctrl/seg:go_default_library",
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/messenger:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/infra/modules/segverifier:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/periodic:go_default_library",
@@ -35,6 +34,7 @@ go_library(
         "//go/lib/snet:go_default_library",
         "//go/lib/snet/addrutil:go_default_library",
         "//go/lib/spath:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/lib/util:go_default_library",
         "//go/proto:go_default_library",
     ],
@@ -63,7 +63,6 @@ go_test(
         "//go/lib/ctrl/seg:go_default_library",
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/mock_infra:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/infra/modules/itopo/itopotest:go_default_library",
         "//go/lib/infra/modules/trust:go_default_library",
         "//go/lib/log:go_default_library",
diff --git a/go/beacon_srv/internal/beaconing/extender.go b/go/beacon_srv/internal/beaconing/extender.go
index 131072546..c42dc54e9 100644
--- a/go/beacon_srv/internal/beaconing/extender.go
+++ b/go/beacon_srv/internal/beaconing/extender.go
@@ -146,10 +146,10 @@ func (s *segExtender) remoteInfo(ifid common.IFIDType) (
 	if topoInfo.RemoteIFID == 0 {
 		return 0, 0, 0, serrors.New("Remote ifid is not set")
 	}
-	if topoInfo.ISD_AS.IsWildcard() {
-		return 0, 0, 0, common.NewBasicError("Remote IA is wildcard", nil, "ia", topoInfo.ISD_AS)
+	if topoInfo.IA.IsWildcard() {
+		return 0, 0, 0, common.NewBasicError("Remote IA is wildcard", nil, "ia", topoInfo.IA)
 	}
-	return topoInfo.ISD_AS.IAInt(), topoInfo.RemoteIFID, uint16(topoInfo.MTU), nil
+	return topoInfo.IA.IAInt(), topoInfo.RemoteIFID, uint16(topoInfo.MTU), nil
 }
 
 // createHopF creates a hop field with the provided parameters. The previous hop
diff --git a/go/beacon_srv/internal/beaconing/handler.go b/go/beacon_srv/internal/beaconing/handler.go
index 6e968a378..c27c3479f 100644
--- a/go/beacon_srv/internal/beaconing/handler.go
+++ b/go/beacon_srv/internal/beaconing/handler.go
@@ -29,6 +29,7 @@ import (
 	"github.com/scionproto/scion/go/lib/log"
 	"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/proto"
 )
 
@@ -150,7 +151,7 @@ func (h *handler) getIFID() (common.IFIDType, addr.IA, error) {
 		return 0, ia, common.NewBasicError("Received beacon on non-existent ifid", nil,
 			"ifid", hopF.ConsIngress)
 	}
-	return hopF.ConsIngress, intf.TopoInfo().ISD_AS, nil
+	return hopF.ConsIngress, intf.TopoInfo().IA, nil
 }
 
 func (h *handler) verifyBeacon(b beacon.Beacon) error {
@@ -170,14 +171,14 @@ func (h *handler) validateASEntry(b beacon.Beacon) error {
 		return common.NewBasicError("Received beacon on non-existent ifid", nil, "ifid", b.InIfId)
 	}
 	topoInfo := intf.TopoInfo()
-	if topoInfo.LinkType != proto.LinkType_parent && topoInfo.LinkType != proto.LinkType_core {
+	if topoInfo.LinkType != topology.Parent && topoInfo.LinkType != topology.Core {
 		return common.NewBasicError("Beacon received on invalid link", nil,
 			"ifid", b.InIfId, "linkType", topoInfo.LinkType)
 	}
 	asEntry := b.Segment.ASEntries[b.Segment.MaxAEIdx()]
-	if !asEntry.IA().Equal(topoInfo.ISD_AS) {
+	if !asEntry.IA().Equal(topoInfo.IA) {
 		return common.NewBasicError("Invalid remote IA", nil,
-			"expected", topoInfo.ISD_AS, "actual", asEntry.IA())
+			"expected", topoInfo.IA, "actual", asEntry.IA())
 	}
 	for i, hopEntry := range asEntry.HopEntries {
 		if !hopEntry.OutIA().Equal(h.ia) {
diff --git a/go/beacon_srv/internal/beaconing/originator.go b/go/beacon_srv/internal/beaconing/originator.go
index fb8fdab65..74f8dda6e 100644
--- a/go/beacon_srv/internal/beaconing/originator.go
+++ b/go/beacon_srv/internal/beaconing/originator.go
@@ -27,8 +27,8 @@ import (
 	"github.com/scionproto/scion/go/lib/periodic"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/util"
-	"github.com/scionproto/scion/go/proto"
 )
 
 var _ periodic.Task = (*Originator)(nil)
@@ -72,15 +72,15 @@ func (o *Originator) Name() string {
 // Run originates core and downstream beacons.
 func (o *Originator) Run(ctx context.Context) {
 	o.tick.now = time.Now()
-	o.originateBeacons(ctx, proto.LinkType_core)
-	o.originateBeacons(ctx, proto.LinkType_child)
+	o.originateBeacons(ctx, topology.Core)
+	o.originateBeacons(ctx, topology.Child)
 	metrics.Originator.Runtime().Add(time.Since(o.tick.now).Seconds())
 	o.tick.updateLast()
 }
 
 // originateBeacons creates and sends a beacon for each active interface of
 // the specified link type.
-func (o *Originator) originateBeacons(ctx context.Context, linkType proto.LinkType) {
+func (o *Originator) originateBeacons(ctx context.Context, linkType topology.LinkType) {
 	logger := log.FromCtx(ctx)
 	active, nonActive := sortedIntfs(o.cfg.Intfs, linkType)
 	if len(nonActive) > 0 && o.tick.passed() {
@@ -135,7 +135,7 @@ func (o *Originator) needBeacon(active []common.IFIDType) []common.IFIDType {
 	return stale
 }
 
-func (o *Originator) logSummary(logger log.Logger, s *summary, linkType proto.LinkType) {
+func (o *Originator) logSummary(logger log.Logger, s *summary, linkType topology.LinkType) {
 	if o.tick.passed() {
 		logger.Info("[beaconing.Originator] Originated beacons",
 			"type", linkType.String(), "egIfIds", s.IfIds())
@@ -173,10 +173,10 @@ func (o *beaconOriginator) originateBeacon(ctx context.Context) error {
 	err = o.beaconSender.Send(
 		ctx,
 		bseg,
-		topoInfo.ISD_AS,
+		topoInfo.IA,
 		o.ifID,
 		o.cfg.Signer,
-		topoInfo.InternalAddrs,
+		topoInfo.InternalAddr,
 	)
 	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 ef0c73888..f935d247b 100644
--- a/go/beacon_srv/internal/beaconing/propagator.go
+++ b/go/beacon_srv/internal/beaconing/propagator.go
@@ -27,7 +27,7 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/periodic"
-	"github.com/scionproto/scion/go/proto"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 // BeaconProvider provides beacons to send to neighboring ASes.
@@ -104,7 +104,7 @@ func (p *Propagator) run(ctx context.Context) error {
 	if len(intfs) == 0 {
 		return nil
 	}
-	peers, nonActivePeers := sortedIntfs(p.cfg.Intfs, proto.LinkType_peer)
+	peers, nonActivePeers := sortedIntfs(p.cfg.Intfs, topology.Peer)
 	if len(nonActivePeers) > 0 && p.tick.passed() {
 		logger.Debug("[beaconing.Propagator] Ignore non-active peering interfaces",
 			"ifids", nonActivePeers)
@@ -146,9 +146,9 @@ func (p *Propagator) run(ctx context.Context) error {
 func (p *Propagator) needsBeacons(logger log.Logger) []common.IFIDType {
 	var activeIntfs, nonActiveIntfs []common.IFIDType
 	if p.core {
-		activeIntfs, nonActiveIntfs = sortedIntfs(p.cfg.Intfs, proto.LinkType_core)
+		activeIntfs, nonActiveIntfs = sortedIntfs(p.cfg.Intfs, topology.Core)
 	} else {
-		activeIntfs, nonActiveIntfs = sortedIntfs(p.cfg.Intfs, proto.LinkType_child)
+		activeIntfs, nonActiveIntfs = sortedIntfs(p.cfg.Intfs, topology.Child)
 	}
 	if len(nonActiveIntfs) > 0 && p.tick.passed() {
 		logger.Debug("[beaconing.Propagator] Ignore non-active interfaces", "ifids", nonActiveIntfs)
@@ -280,10 +280,10 @@ func (p *beaconPropagator) extendAndSend(ctx context.Context, bseg beacon.Beacon
 		err := p.beaconSender.Send(
 			ctx,
 			&seg.Beacon{Segment: bseg.Segment},
-			topoInfo.ISD_AS,
+			topoInfo.IA,
 			egIfid,
 			p.cfg.Signer,
-			topoInfo.InternalAddrs,
+			topoInfo.InternalAddr,
 		)
 		if err != nil {
 			p.logger.Error("[beaconing.Propagator] Unable to send packet",
@@ -305,7 +305,7 @@ func (p *beaconPropagator) shouldIgnore(bseg beacon.Beacon, egIfid common.IFIDTy
 	if intf == nil {
 		return true
 	}
-	if err := beacon.FilterLoop(bseg, intf.TopoInfo().ISD_AS, p.allowIsdLoop); err != nil {
+	if err := beacon.FilterLoop(bseg, intf.TopoInfo().IA, p.allowIsdLoop); err != nil {
 		p.logger.Trace("[beaconing.Propagator] Ignoring beacon on loop", "ifid", egIfid, "err", err)
 		return true
 	}
diff --git a/go/beacon_srv/internal/beaconing/registrar.go b/go/beacon_srv/internal/beaconing/registrar.go
index bfdcfba86..b83f8853b 100644
--- a/go/beacon_srv/internal/beaconing/registrar.go
+++ b/go/beacon_srv/internal/beaconing/registrar.go
@@ -28,11 +28,11 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/infra/messenger"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/periodic"
 	"github.com/scionproto/scion/go/lib/snet"
 	"github.com/scionproto/scion/go/lib/snet/addrutil"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/proto"
 )
 
@@ -48,7 +48,7 @@ var _ periodic.Task = (*Registrar)(nil)
 type RegistrarConf struct {
 	Config       ExtenderConf
 	SegProvider  SegmentProvider
-	TopoProvider itopo.ProviderI
+	TopoProvider topology.Provider
 	Msgr         infra.Messenger
 	Period       time.Duration
 	SegType      proto.PathSegType
@@ -61,7 +61,7 @@ type Registrar struct {
 	*segExtender
 	msgr         infra.Messenger
 	segProvider  SegmentProvider
-	topoProvider itopo.ProviderI
+	topoProvider topology.Provider
 	segType      proto.PathSegType
 
 	// mutable fields
@@ -112,7 +112,7 @@ func (r *Registrar) run(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-	peers, nonActivePeers := sortedIntfs(r.cfg.Intfs, proto.LinkType_peer)
+	peers, nonActivePeers := sortedIntfs(r.cfg.Intfs, topology.Peer)
 	if len(nonActivePeers) > 0 {
 		logger.Debug("[beaconing.Registrar] Ignore non-active peer interfaces", "type", r.segType,
 			"intfs", nonActivePeers)
diff --git a/go/beacon_srv/internal/beaconing/util.go b/go/beacon_srv/internal/beaconing/util.go
index 85f6e681b..1e983e729 100644
--- a/go/beacon_srv/internal/beaconing/util.go
+++ b/go/beacon_srv/internal/beaconing/util.go
@@ -27,7 +27,7 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/snet"
-	"github.com/scionproto/scion/go/proto"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 // packBeaconMsg packs the provided beacon and creates a one-hop message.
@@ -61,7 +61,7 @@ func packBeaconMsg(bseg *seg.Beacon, ia addr.IA, egIfid common.IFIDType,
 // sortedIntfs returns two sorted lists. The first list contains all active
 // interfaces of the given type. The second list contains all non-active
 // interfaces of the given type.
-func sortedIntfs(intfs *ifstate.Interfaces, linkType proto.LinkType) ([]common.IFIDType,
+func sortedIntfs(intfs *ifstate.Interfaces, linkType topology.LinkType) ([]common.IFIDType,
 	[]common.IFIDType) {
 
 	var active, nonActive []common.IFIDType
diff --git a/go/beacon_srv/internal/ifstate/BUILD.bazel b/go/beacon_srv/internal/ifstate/BUILD.bazel
index d908453ed..a987b9232 100644
--- a/go/beacon_srv/internal/ifstate/BUILD.bazel
+++ b/go/beacon_srv/internal/ifstate/BUILD.bazel
@@ -19,13 +19,13 @@ go_library(
         "//go/lib/ctrl/path_mgmt:go_default_library",
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/messenger:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/periodic:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/topology:go_default_library",
         "//go/lib/util:go_default_library",
+        "//go/proto:go_default_library",
         "@com_github_prometheus_client_golang//prometheus:go_default_library",
     ],
 )
@@ -48,7 +48,6 @@ go_test(
         "//go/lib/ctrl/path_mgmt:go_default_library",
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/mock_infra:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/infra/modules/itopo/itopotest:go_default_library",
         "//go/lib/infra/modules/trust:go_default_library",
         "//go/lib/log:go_default_library",
diff --git a/go/beacon_srv/internal/ifstate/metrics.go b/go/beacon_srv/internal/ifstate/metrics.go
index cfc160f16..af5b88ea1 100644
--- a/go/beacon_srv/internal/ifstate/metrics.go
+++ b/go/beacon_srv/internal/ifstate/metrics.go
@@ -43,7 +43,7 @@ func (c *Collector) Collect(mc chan<- prometheus.Metric) {
 		}
 		l := metrics.IfstateLabels{
 			IfID:    ifid,
-			NeighIA: intf.TopoInfo().ISD_AS,
+			NeighIA: intf.TopoInfo().IA,
 		}
 		mc <- metrics.Ifstate.IfstateMetric(l, up)
 	}
diff --git a/go/beacon_srv/internal/ifstate/pusher.go b/go/beacon_srv/internal/ifstate/pusher.go
index ce56ca339..0124a092a 100644
--- a/go/beacon_srv/internal/ifstate/pusher.go
+++ b/go/beacon_srv/internal/ifstate/pusher.go
@@ -21,12 +21,12 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/ctrl/path_mgmt"
 	"github.com/scionproto/scion/go/lib/infra"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 // PusherConf is the configuration to create a new pusher.
 type PusherConf struct {
-	TopoProvider itopo.ProviderI
+	TopoProvider topology.Provider
 	Intfs        *Interfaces
 	Msgr         infra.Messenger
 }
@@ -34,7 +34,7 @@ type PusherConf struct {
 // Pusher pushes interface state infos to all border routers to remove the
 // revocations. It is called when an interface comes back up.
 type Pusher struct {
-	topoProvider itopo.ProviderI
+	topoProvider topology.Provider
 	intfs        *Interfaces
 	pusher       brPusher
 }
diff --git a/go/beacon_srv/internal/ifstate/revoker.go b/go/beacon_srv/internal/ifstate/revoker.go
index d583d4f1d..f5716429f 100644
--- a/go/beacon_srv/internal/ifstate/revoker.go
+++ b/go/beacon_srv/internal/ifstate/revoker.go
@@ -26,11 +26,12 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/path_mgmt"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/infra/messenger"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/periodic"
 	"github.com/scionproto/scion/go/lib/snet"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/util"
+	"github.com/scionproto/scion/go/proto"
 )
 
 // RevInserter stores revocation into persistent storage.
@@ -49,7 +50,7 @@ type RevokerConf struct {
 	Intfs        *Interfaces
 	Msgr         infra.Messenger
 	Signer       infra.Signer
-	TopoProvider itopo.ProviderI
+	TopoProvider topology.Provider
 	RevInserter  RevInserter
 	RevConfig    RevConfig
 }
@@ -87,12 +88,12 @@ func (r *Revoker) Run(ctx context.Context) {
 	for ifid, intf := range r.cfg.Intfs.All() {
 		labelsIssued := metrics.IssuedLabels{
 			IfID:    ifid,
-			NeighIA: intf.TopoInfo().ISD_AS,
+			NeighIA: intf.TopoInfo().IA,
 			State:   metrics.RevRenew,
 		}
 		labelsDuration := metrics.DurationLabels{
 			IfID:    ifid,
-			NeighIA: intf.TopoInfo().ISD_AS,
+			NeighIA: intf.TopoInfo().IA,
 		}
 
 		if intf.Expire() && !r.hasValidRevocation(intf) {
@@ -142,7 +143,7 @@ func (r *Revoker) createSignedRev(ifid common.IFIDType) (*path_mgmt.SignedRevInf
 	revInfo := &path_mgmt.RevInfo{
 		IfID:         ifid,
 		RawIsdas:     r.cfg.TopoProvider.Get().IA().IAInt(),
-		LinkType:     r.cfg.TopoProvider.Get().IFInfoMap()[ifid].LinkType,
+		LinkType:     proto.LinkType(r.cfg.TopoProvider.Get().IFInfoMap()[ifid].LinkType),
 		RawTimestamp: now,
 		RawTTL:       uint32(r.cfg.RevConfig.RevTTL.Seconds()),
 	}
@@ -185,7 +186,7 @@ type brPusher struct {
 }
 
 func (p *brPusher) sendIfStateToAllBRs(ctx context.Context, msg *path_mgmt.IFStateInfos,
-	topo itopo.Topology, wg *sync.WaitGroup) {
+	topo topology.Topology, wg *sync.WaitGroup) {
 
 	for _, br := range topo.BRNames() {
 		p.sendIfStateToBr(ctx, msg, br, topo.SBRAddress(br), wg)
diff --git a/go/beacon_srv/internal/keepalive/BUILD.bazel b/go/beacon_srv/internal/keepalive/BUILD.bazel
index 8bdf7a7e9..7ee76d1a7 100644
--- a/go/beacon_srv/internal/keepalive/BUILD.bazel
+++ b/go/beacon_srv/internal/keepalive/BUILD.bazel
@@ -18,10 +18,10 @@ go_library(
         "//go/lib/ctrl:go_default_library",
         "//go/lib/ctrl/ifid:go_default_library",
         "//go/lib/infra:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/periodic:go_default_library",
         "//go/lib/snet:go_default_library",
+        "//go/lib/topology:go_default_library",
     ],
 )
 
diff --git a/go/beacon_srv/internal/keepalive/handler.go b/go/beacon_srv/internal/keepalive/handler.go
index fa5691b26..7c47ecc6a 100644
--- a/go/beacon_srv/internal/keepalive/handler.go
+++ b/go/beacon_srv/internal/keepalive/handler.go
@@ -108,7 +108,7 @@ func (h *handler) handle(logger log.Logger) (*infra.HandlerResult, error) {
 	if lastState := info.Activate(keepalive.OrigIfID); lastState != ifstate.Active {
 		logger.Info("[KeepaliveHandler] Activated interface", "ifid", ifid)
 		h.startPush(ifid)
-		if err := h.dropRevs(ifid, keepalive.OrigIfID, info.TopoInfo().ISD_AS); err != nil {
+		if err := h.dropRevs(ifid, keepalive.OrigIfID, info.TopoInfo().IA); err != nil {
 			metrics.Keepalive.Receives(labels).Inc()
 			return infra.MetricsErrInternal, common.NewBasicError("Unable to drop revocations", err)
 		}
@@ -134,8 +134,8 @@ func (h *handler) getIntfInfo() (common.IFIDType, *ifstate.Interface, error) {
 		return 0, nil, common.NewBasicError("Received keepalive for non-existent ifid", nil,
 			"ifid", hopF.ConsIngress)
 	}
-	originIA := info.TopoInfo().ISD_AS
-	if !info.TopoInfo().ISD_AS.Equal(peer.IA) {
+	originIA := info.TopoInfo().IA
+	if !info.TopoInfo().IA.Equal(peer.IA) {
 		return 0, nil, common.NewBasicError("Keepalive origin IA does not match", nil,
 			"ifid", hopF.ConsIngress, "expected", originIA, "actual", peer.IA)
 	}
diff --git a/go/beacon_srv/internal/keepalive/sender.go b/go/beacon_srv/internal/keepalive/sender.go
index 0d4ee1bfb..ba4d6dc50 100644
--- a/go/beacon_srv/internal/keepalive/sender.go
+++ b/go/beacon_srv/internal/keepalive/sender.go
@@ -25,10 +25,10 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl"
 	"github.com/scionproto/scion/go/lib/ctrl/ifid"
 	"github.com/scionproto/scion/go/lib/infra"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/periodic"
 	"github.com/scionproto/scion/go/lib/snet"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 var _ periodic.Task = (*Sender)(nil)
@@ -37,7 +37,7 @@ var _ periodic.Task = (*Sender)(nil)
 type Sender struct {
 	*onehop.Sender
 	Signer       infra.Signer
-	TopoProvider itopo.ProviderI
+	TopoProvider topology.Provider
 }
 
 // Name returns the tasks name.
@@ -64,14 +64,14 @@ func (s *Sender) Run(ctx context.Context) {
 		}
 		msg := &onehop.Msg{
 			Dst: snet.SCIONAddress{
-				IA:   intf.ISD_AS,
+				IA:   intf.IA,
 				Host: addr.SvcBS | addr.SVCMcast,
 			},
 			Ifid:     ifid,
 			InfoTime: time.Now(),
 			Pld:      pld,
 		}
-		if err := s.Send(msg, intf.InternalAddrs); err != nil {
+		if err := s.Send(msg, intf.InternalAddr); 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 c626793ba..c13b16a9e 100644
--- a/go/beacon_srv/main.go
+++ b/go/beacon_srv/main.go
@@ -61,6 +61,7 @@ import (
 	"github.com/scionproto/scion/go/lib/sock/reliable"
 	"github.com/scionproto/scion/go/lib/sock/reliable/reconnect"
 	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/proto"
 )
 
@@ -130,6 +131,7 @@ func realMain() int {
 	}
 	defer trCloser.Close()
 	opentracing.SetGlobalTracer(tracer)
+
 	nc := infraenv.NetworkConfig{
 		IA:                    topo.IA(),
 		Public:                topo.PublicAddress(addr.SvcBS, cfg.General.ID),
@@ -190,10 +192,10 @@ func realMain() int {
 	}
 	// We do not need to drain the connection, since the src address is spoofed
 	// to contain the topo address.
-	ovAddr := topo.PublicAddress(addr.SvcBS, cfg.General.ID)
-	t := addr.AppAddrFromUDP(ovAddr)
-	t.L4 = 0
-	conn, _, err := pktDisp.RegisterTimeout(topo.IA(), t, nil, addr.SvcNone, time.Second)
+	ohpAddress := topo.PublicAddress(addr.SvcBS, cfg.General.ID)
+	ohpAddress.Port = 0
+	conn, _, err := pktDisp.RegisterTimeout(topo.IA(), addr.AppAddrFromUDP(ohpAddress), nil,
+		addr.SvcNone, time.Second)
 	if err != nil {
 		log.Crit("Unable to create SCION packet conn", "err", err)
 		return 1
@@ -265,7 +267,7 @@ type periodicTasks struct {
 	trustDB         trustdb.TrustDB
 	store           beaconstorage.Store
 	msgr            infra.Messenger
-	topoProvider    itopo.ProviderI
+	topoProvider    topology.Provider
 	allowIsdLoop    bool
 	addressRewriter *messenger.AddressRewriter
 
@@ -295,6 +297,7 @@ func (t *periodicTasks) Start() error {
 	if bs == nil {
 		return serrors.New("Unable to find topo address")
 	}
+
 	var err error
 	if t.registrars, err = t.startSegRegRunners(); err != nil {
 		return err
@@ -451,7 +454,7 @@ func (t *periodicTasks) startSegRegRunners() (segRegRunners, error) {
 	return s, nil
 }
 
-func (t *periodicTasks) startRegistrar(topo itopo.Topology, segType proto.PathSegType,
+func (t *periodicTasks) startRegistrar(topo topology.Topology, segType proto.PathSegType,
 	policyType beacon.PolicyType) (*periodic.Runner, error) {
 
 	signer, err := t.createSigner(topo.IA())
@@ -551,7 +554,7 @@ func setup() error {
 	}
 	clbks := itopo.Callbacks{UpdateStatic: handleTopoUpdate}
 	itopo.Init(cfg.General.ID, proto.ServiceType_bs, clbks)
-	topo, err := itopo.LoadFromFile(cfg.General.Topology)
+	topo, err := topology.FromJSONFile(cfg.General.Topology)
 	if err != nil {
 		return common.NewBasicError("Unable to load topology", err)
 	}
diff --git a/go/border/BUILD.bazel b/go/border/BUILD.bazel
index b401b7f85..939cad35e 100644
--- a/go/border/BUILD.bazel
+++ b/go/border/BUILD.bazel
@@ -65,7 +65,6 @@ go_test(
         "//go/border/rctx:go_default_library",
         "//go/border/rpkt:go_default_library",
         "//go/lib/common:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/overlay/conn:go_default_library",
         "//go/lib/overlay/conn/mock_conn:go_default_library",
diff --git a/go/border/brconf/BUILD.bazel b/go/border/brconf/BUILD.bazel
index b5fee775d..382632859 100644
--- a/go/border/brconf/BUILD.bazel
+++ b/go/border/brconf/BUILD.bazel
@@ -16,7 +16,6 @@ go_library(
         "//go/lib/config:go_default_library",
         "//go/lib/env:go_default_library",
         "//go/lib/infra/modules/idiscovery:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/keyconf:go_default_library",
         "//go/lib/topology:go_default_library",
     ],
diff --git a/go/border/brconf/conf.go b/go/border/brconf/conf.go
index 904a43e30..4c7b671c9 100644
--- a/go/border/brconf/conf.go
+++ b/go/border/brconf/conf.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/keyconf"
 	"github.com/scionproto/scion/go/lib/topology"
 )
@@ -32,7 +31,7 @@ import (
 type BRConf struct {
 	// Topo contains the names of all local infrastructure elements, a map
 	// of interface IDs to routers, and the actual topology.
-	Topo itopo.Topology
+	Topo topology.Topology
 	// IA is the current ISD-AS.
 	IA addr.IA
 	// BR is the topology information of this router.
@@ -59,7 +58,7 @@ func Load(id, confDir string) (*BRConf, error) {
 
 // WithNewTopo creates config that shares all content except fields related
 // to topology with the oldConf.
-func WithNewTopo(id string, topo itopo.Topology, oldConf *BRConf) (*BRConf, error) {
+func WithNewTopo(id string, topo topology.Topology, oldConf *BRConf) (*BRConf, error) {
 	conf := &BRConf{
 		Dir:        oldConf.Dir,
 		MasterKeys: oldConf.MasterKeys,
@@ -73,8 +72,8 @@ func WithNewTopo(id string, topo itopo.Topology, oldConf *BRConf) (*BRConf, erro
 // loadTopo loads the topology from the config directory and initializes the
 // entries related to topo in the config.
 func (cfg *BRConf) loadTopo(id string) error {
-	topoPath := filepath.Join(cfg.Dir, topology.CfgName)
-	topo, err := itopo.LoadFromFile(topoPath)
+	topoPath := filepath.Join(cfg.Dir, "topology.json")
+	topo, err := topology.FromJSONFile(topoPath)
 	if err != nil {
 		return err
 	}
@@ -85,7 +84,7 @@ func (cfg *BRConf) loadTopo(id string) error {
 }
 
 // initTopo initializesthe entries related to topo in the config.
-func (cfg *BRConf) initTopo(id string, topo itopo.Topology) error {
+func (cfg *BRConf) initTopo(id string, topo topology.Topology) error {
 	cfg.Topo = topo
 	cfg.IA = cfg.Topo.IA()
 	// Find the config for this router.
diff --git a/go/border/ifstate/ifstate.go b/go/border/ifstate/ifstate.go
index 9d22b800f..3ae2b1d6f 100644
--- a/go/border/ifstate/ifstate.go
+++ b/go/border/ifstate/ifstate.go
@@ -114,7 +114,7 @@ func Process(ifStates *path_mgmt.IFStateInfos) {
 			log.Warn("Interface ID does not exist", "ifid", ifid)
 			continue
 		}
-		stateInfo := NewInfo(ifid, intf.ISD_AS, info.Active, info.SRevInfo, rawSRev)
+		stateInfo := NewInfo(ifid, intf.IA, info.Active, info.SRevInfo, rawSRev)
 		s, ok := states.Load(ifid)
 		if !ok {
 			log.Info("IFState: intf added", "ifid", ifid, "active", info.Active)
diff --git a/go/border/rpkt/BUILD.bazel b/go/border/rpkt/BUILD.bazel
index 66aced470..ef189ed0f 100644
--- a/go/border/rpkt/BUILD.bazel
+++ b/go/border/rpkt/BUILD.bazel
@@ -47,7 +47,6 @@ go_library(
         "//go/lib/spse/scmp_auth:go_default_library",
         "//go/lib/topology:go_default_library",
         "//go/lib/util:go_default_library",
-        "//go/proto:go_default_library",
         "@org_golang_x_xerrors//:go_default_library",
     ],
 )
diff --git a/go/border/rpkt/create.go b/go/border/rpkt/create.go
index 15f6ac865..27633943b 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
+	pub := rp.Ctx.Conf.BR.InternalAddr
 	sp.SrcHost = addr.HostFromIP(pub.IP)
 	return sp, nil
 }
diff --git a/go/border/rpkt/path.go b/go/border/rpkt/path.go
index 7ced9db9f..56c012bd3 100644
--- a/go/border/rpkt/path.go
+++ b/go/border/rpkt/path.go
@@ -30,7 +30,7 @@ import (
 	"github.com/scionproto/scion/go/lib/scmp"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/spath"
-	"github.com/scionproto/scion/go/proto"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 // validatePath validates the path header.
@@ -47,7 +47,7 @@ func (rp *RtrPkt) validatePath(dirFrom rcmn.Dir) error {
 	// Check for shorcuts in packets from core links
 	if rp.infoF.Shortcut {
 		currentLinkType := rp.Ctx.Conf.BR.IFs[*rp.ifCurr].LinkType
-		if currentLinkType == proto.LinkType_core {
+		if currentLinkType == topology.Core {
 			return serrors.New("Shortcut not allowed on core segment")
 		}
 	}
@@ -125,7 +125,7 @@ func (rp *RtrPkt) validateLocalIF(ifid *common.IFIDType) error {
 		// If the BR does not have a revocation for the current epoch, it considers
 		// the interface as active until it receives a new revocation.
 		intf := rp.Ctx.Conf.BR.IFs[*ifid]
-		newState := ifstate.NewInfo(*ifid, intf.ISD_AS, true, nil, nil)
+		newState := ifstate.NewInfo(*ifid, intf.IA, true, nil, nil)
 		ifstate.UpdateIfNew(*ifid, state, newState)
 		return nil
 	}
diff --git a/go/border/rpkt/process.go b/go/border/rpkt/process.go
index 65a7c7d18..b916d76be 100644
--- a/go/border/rpkt/process.go
+++ b/go/border/rpkt/process.go
@@ -24,7 +24,7 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/ctrl/path_mgmt"
 	"github.com/scionproto/scion/go/lib/scmp"
-	"github.com/scionproto/scion/go/proto"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 // NeedsLocalProcessing determines if the router needs to do more than just
@@ -188,8 +188,8 @@ func (rp *RtrPkt) processSCMPRevocation() error {
 
 	intf := rp.Ctx.Conf.BR.IFs[*rp.ifCurr]
 	rp.SrcIA() // Ensure that rp.srcIA has been set
-	if (rp.dstIA.I == rp.Ctx.Conf.Topo.IA().I && intf.LinkType == proto.LinkType_core) ||
-		(rp.srcIA.I == rp.Ctx.Conf.Topo.IA().I && intf.LinkType == proto.LinkType_parent) {
+	if (rp.dstIA.I == rp.Ctx.Conf.Topo.IA().I && intf.LinkType == topology.Core) ||
+		(rp.srcIA.I == rp.Ctx.Conf.Topo.IA().I && intf.LinkType == topology.Parent) {
 		// Case 1 & 2
 		args.Addrs = append(args.Addrs, addr.SvcBS)
 		if len(rp.Ctx.Conf.Topo.SVCNames(addr.SvcPS)) > 0 {
diff --git a/go/border/rpkt/route.go b/go/border/rpkt/route.go
index bb5b6e8bc..628d10ce1 100644
--- a/go/border/rpkt/route.go
+++ b/go/border/rpkt/route.go
@@ -29,7 +29,6 @@ import (
 	"github.com/scionproto/scion/go/lib/scmp"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/topology"
-	"github.com/scionproto/scion/go/proto"
 )
 
 // Route handles routing of packets. Registered hooks are called, allowing them
@@ -144,7 +143,7 @@ func (rp *RtrPkt) forwardFromExternal() (HookResult, error) {
 		return rp.reprocess()
 	}
 	nextBR := rp.Ctx.Conf.Topo.IFInfoMap()[*rp.ifNext]
-	rp.Egress = append(rp.Egress, EgressPair{S: rp.Ctx.LocSockOut, Dst: nextBR.InternalAddrs})
+	rp.Egress = append(rp.Egress, EgressPair{S: rp.Ctx.LocSockOut, Dst: nextBR.InternalAddr})
 	return HookContinue, nil
 }
 
@@ -200,13 +199,13 @@ func (rp *RtrPkt) xoverFromExternal() error {
 	prevLink := rp.Ctx.Conf.BR.IFs[origIFCurr].LinkType
 	nextLink := rp.Ctx.Conf.Topo.IFInfoMap()[*rp.ifNext].LinkType
 	// Never allowed to switch between core segments.
-	if prevLink == proto.LinkType_core && nextLink == proto.LinkType_core {
+	if prevLink == topology.Core && nextLink == topology.Core {
 		return common.NewBasicError("Segment change between CORE links",
 			scmp.NewError(scmp.C_Path, scmp.T_P_BadSegment,
 				rp.mkInfoPathOffsets(rp.CmnHdr.CurrInfoF, rp.CmnHdr.CurrHopF), nil))
 	}
 	// Only allowed to switch from up- to up-segment if the next link is CORE.
-	if !infoF.ConsDir && !rp.infoF.ConsDir && nextLink != proto.LinkType_core {
+	if !infoF.ConsDir && !rp.infoF.ConsDir && nextLink != topology.Core {
 		return common.NewBasicError(
 			"Segment change from up segment to up segment with non-CORE next link",
 			scmp.NewError(scmp.C_Path, scmp.T_P_BadSegment,
@@ -215,7 +214,7 @@ func (rp *RtrPkt) xoverFromExternal() error {
 		)
 	}
 	// Only allowed to switch from down- to down-segment if the previous link is CORE.
-	if infoF.ConsDir && rp.infoF.ConsDir && prevLink != proto.LinkType_core {
+	if infoF.ConsDir && rp.infoF.ConsDir && prevLink != topology.Core {
 		return common.NewBasicError(
 			"Segment change from down segment to down segment with non-CORE previous link",
 			scmp.NewError(scmp.C_Path, scmp.T_P_BadSegment,
@@ -224,7 +223,7 @@ func (rp *RtrPkt) xoverFromExternal() error {
 		)
 	}
 	// Make sure we drop packets with shortcuts in core links.
-	if rp.infoF.Shortcut && nextLink == proto.LinkType_core {
+	if rp.infoF.Shortcut && nextLink == topology.Core {
 		return serrors.New("Shortcut not allowed on core segment")
 	}
 	return nil
diff --git a/go/border/setup-posix.go b/go/border/setup-posix.go
index f5e341a7c..af67c34b2 100644
--- a/go/border/setup-posix.go
+++ b/go/border/setup-posix.go
@@ -45,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.String() == oldCtx.Conf.BR.InternalAddrs.String() {
+		if ctx.Conf.BR.InternalAddr.String() == oldCtx.Conf.BR.InternalAddr.String() {
 			log.Trace("No change detected for local socket.")
 			// Nothing changed. Copy I/O functions from old context.
 			ctx.LocSockIn = oldCtx.LocSockIn
@@ -64,7 +64,7 @@ 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.String() == oldCtx.Conf.BR.InternalAddrs.String() {
+		ctx.Conf.BR.InternalAddr.String() == oldCtx.Conf.BR.InternalAddr.String() {
 		return nil
 	}
 	// Remove new socket if it exists. It might not be set if the setup failed.
@@ -83,7 +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
-	bind := ctx.Conf.BR.InternalAddrs
+	bind := ctx.Conf.BR.InternalAddr
 	log.Debug("Setting up new local socket.", "bind", bind)
 	// Listen on the socket.
 	over, err := conn.New(bind, nil, nil)
@@ -109,17 +109,17 @@ func (p posixExt) Setup(r *Router, ctx *rctx.Ctx, intf *topology.IFInfo, oldCtx
 	if oldCtx == nil {
 		return p.addIntf(r, ctx, intf)
 	}
-	if oldIntf, ok := oldCtx.Conf.BR.IFs[intf.Id]; ok {
+	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)
-			ctx.ExtSockIn[intf.Id] = oldCtx.ExtSockIn[intf.Id]
-			ctx.ExtSockOut[intf.Id] = oldCtx.ExtSockOut[intf.Id]
+			ctx.ExtSockIn[intf.ID] = oldCtx.ExtSockIn[intf.ID]
+			ctx.ExtSockOut[intf.ID] = oldCtx.ExtSockOut[intf.ID]
 			return nil
 		}
 		log.Debug("Closing existing external socket", "old", oldIntf, "new", intf)
-		oldCtx.ExtSockIn[intf.Id].Stop()
-		oldCtx.ExtSockOut[intf.Id].Stop()
+		oldCtx.ExtSockIn[intf.ID].Stop()
+		oldCtx.ExtSockOut[intf.ID].Stop()
 	}
 	return p.addIntf(r, ctx, intf)
 }
@@ -129,21 +129,21 @@ func (p posixExt) Rollback(r *Router, ctx *rctx.Ctx, intf *topology.IFInfo,
 
 	var oldIntf *topology.IFInfo
 	if oldCtx != nil {
-		oldIntf = oldCtx.Conf.BR.IFs[intf.Id]
+		oldIntf = oldCtx.Conf.BR.IFs[intf.ID]
 	}
 	// Do not rollback socket if it is reused by new context.
 	if oldIntf != nil && !interfaceChanged(intf, oldIntf) {
 		return nil
 	}
 	// Stop new socket if it exists. It might not exist if the Setup failed.
-	if _, ok := ctx.ExtSockIn[intf.Id]; ok {
+	if _, ok := ctx.ExtSockIn[intf.ID]; ok {
 		log.Debug("Rolling back external socket", "intf", intf)
-		ctx.ExtSockIn[intf.Id].Stop()
-		ctx.ExtSockOut[intf.Id].Stop()
+		ctx.ExtSockIn[intf.ID].Stop()
+		ctx.ExtSockOut[intf.ID].Stop()
 	}
 	// No need to start socket if it is not present in old context or still running.
 	// The socket is still running if setupNet failed before iterating over this socket.
-	if oldIntf == nil || oldCtx.ExtSockIn[oldIntf.Id].Running() {
+	if oldIntf == nil || oldCtx.ExtSockIn[oldIntf.ID].Running() {
 		return nil
 	}
 	return p.addIntf(r, oldCtx, oldIntf)
@@ -158,31 +158,31 @@ func (p posixExt) addIntf(r *Router, ctx *rctx.Ctx, intf *topology.IFInfo) error
 		return common.NewBasicError("Unable to listen on external socket", err)
 	}
 	// Setup input goroutine.
-	ctx.ExtSockIn[intf.Id] = rctx.NewSock(
-		ringbuf.New(64, nil, fmt.Sprintf("ext_in_%s", intf.Id)),
-		c, rcmn.DirExternal, intf.Id, intf.ISD_AS.String(), r.posixInput, r.handleSock, PosixSock)
-	ctx.ExtSockOut[intf.Id] = rctx.NewSock(
-		ringbuf.New(64, nil, fmt.Sprintf("ext_out_%s", intf.Id)),
-		c, rcmn.DirExternal, intf.Id, intf.ISD_AS.String(), nil, r.posixOutput, PosixSock)
+	ctx.ExtSockIn[intf.ID] = rctx.NewSock(
+		ringbuf.New(64, nil, fmt.Sprintf("ext_in_%s", intf.ID)),
+		c, rcmn.DirExternal, intf.ID, intf.IA.String(), r.posixInput, r.handleSock, PosixSock)
+	ctx.ExtSockOut[intf.ID] = rctx.NewSock(
+		ringbuf.New(64, nil, fmt.Sprintf("ext_out_%s", intf.ID)),
+		c, rcmn.DirExternal, intf.ID, intf.IA.String(), nil, r.posixOutput, PosixSock)
 	log.Debug("Done setting up new external socket.", "intf", intf)
 	return nil
 }
 
 func (p posixExt) Teardown(r *Router, ctx *rctx.Ctx, intf *topology.IFInfo, oldCtx *rctx.Ctx) {
-	if oldCtx == nil || oldCtx.ExtSockIn[intf.Id] == nil {
+	if oldCtx == nil || oldCtx.ExtSockIn[intf.ID] == nil {
 		return
 	}
-	if _, ok := ctx.ExtSockIn[intf.Id]; !ok {
+	if _, ok := ctx.ExtSockIn[intf.ID]; !ok {
 		log.Debug("Tearing down socket from removed external interface", "intf", intf)
-		oldCtx.ExtSockIn[intf.Id].Stop()
-		oldCtx.ExtSockOut[intf.Id].Stop()
+		oldCtx.ExtSockIn[intf.ID].Stop()
+		oldCtx.ExtSockOut[intf.ID].Stop()
 	}
 }
 
 // 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 ||
+	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 0eda34ddb..cdd84351f 100644
--- a/go/border/setup.go
+++ b/go/border/setup.go
@@ -127,14 +127,14 @@ func (r *Router) setupCtxFromConfig(config *brconf.BRConf) error {
 	// We want to keep in sync itopo and the context that is set.
 	// We attempt to set the context with the topology that will be current
 	// after setting itopo. If setting itopo fails in the end, we rollback the context.
-	tx, err := itopo.BeginSetStatic(config.Topo.Raw(), true)
+	tx, err := itopo.BeginSetStatic(config.Topo.Writable(), true)
 	if err != nil {
 		return err
 	}
 	// Set config to use the appropriate topology. The returned topology is
 	// not necessarily the same as config.Topo. It can be another static
 	// or dynamic topology.
-	newConf, err := brconf.WithNewTopo(r.Id, itopo.NewTopologyFromRaw(tx.Get()), config)
+	newConf, err := brconf.WithNewTopo(r.Id, topology.FromRWTopology(tx.Get()), config)
 	if err != nil {
 		return err
 	}
@@ -143,7 +143,7 @@ func (r *Router) setupCtxFromConfig(config *brconf.BRConf) error {
 
 // setupCtxFromStatic sets up a new router context after receiving an updated
 // static topology from the discovey service.
-func (r *Router) setupCtxFromStatic(topo *topology.Topo) (bool, error) {
+func (r *Router) setupCtxFromStatic(topo *topology.RWTopology) (bool, error) {
 	r.setCtxMtx.Lock()
 	defer r.setCtxMtx.Unlock()
 	tx, err := itopo.BeginSetStatic(topo, cfg.Discovery.AllowSemiMutable)
@@ -152,7 +152,7 @@ func (r *Router) setupCtxFromStatic(topo *topology.Topo) (bool, error) {
 
 // setupCtxFromDynamic sets up a new router context after receiving an updated
 // dynamic topology from the discovey service.
-func (r *Router) setupCtxFromDynamic(topo *topology.Topo) (bool, error) {
+func (r *Router) setupCtxFromDynamic(topo *topology.RWTopology) (bool, error) {
 	r.setCtxMtx.Lock()
 	defer r.setCtxMtx.Unlock()
 	tx, err := itopo.BeginSetDynamic(topo)
@@ -170,7 +170,7 @@ func (r *Router) setupCtxFromTopoUpdate(mode discovery.Mode, tx itopo.Transactio
 		return false, nil
 	}
 	log.Trace("====> Setting up new context from topology update", "mode", mode)
-	newConf, err := brconf.WithNewTopo(r.Id, itopo.NewTopologyFromRaw(tx.Get()), rctx.Get().Conf)
+	newConf, err := brconf.WithNewTopo(r.Id, topology.FromRWTopology(tx.Get()), rctx.Get().Conf)
 	if err != nil {
 		return false, err
 	}
@@ -239,7 +239,7 @@ func (r *Router) setupNet(ctx *rctx.Ctx, oldCtx *rctx.Ctx, sockConf brconf.SockC
 	}
 	// Iterate over interfaces, configuring them via provided setup function.
 	for _, intf := range ctx.Conf.BR.IFs {
-		err := registeredExtSockOps[sockConf.Ext(intf.Id)].Setup(r, ctx, intf, oldCtx)
+		err := registeredExtSockOps[sockConf.Ext(intf.ID)].Setup(r, ctx, intf, oldCtx)
 		if err != nil {
 			return err
 		}
@@ -253,7 +253,7 @@ func (r *Router) rollbackNet(ctx, oldCtx *rctx.Ctx,
 
 	// Rollback of external interfaces.
 	for _, intf := range ctx.Conf.BR.IFs {
-		err := registeredExtSockOps[sockConf.Ext(intf.Id)].Rollback(r, ctx, intf, oldCtx)
+		err := registeredExtSockOps[sockConf.Ext(intf.ID)].Rollback(r, ctx, intf, oldCtx)
 		if err != nil {
 			handleErr(common.NewBasicError("Unable to rollback external interface",
 				err, "intf", intf))
@@ -277,7 +277,7 @@ func (r *Router) teardownNet(ctx, oldCtx *rctx.Ctx, sockConf brconf.SockConf) {
 	}
 	// Iterate on oldCtx to catch removed interfaces.
 	for _, intf := range oldCtx.Conf.BR.IFs {
-		registeredExtSockOps[sockConf.Ext(intf.Id)].Teardown(r, ctx, intf, oldCtx)
+		registeredExtSockOps[sockConf.Ext(intf.ID)].Teardown(r, ctx, intf, oldCtx)
 	}
 }
 
@@ -304,7 +304,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
+	internalAddr := rctx.Get().Conf.BR.InternalAddr
 	tcpAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", internalAddr.IP))
 	if err != nil {
 		return nil, err
@@ -370,33 +370,33 @@ func validateCtx(ctx, oldCtx *rctx.Ctx, sockConf brconf.SockConf) error {
 	}
 	// Validate interfaces.
 	for _, intf := range ctx.Conf.BR.IFs {
-		sockType := sockConf.Ext(intf.Id)
+		sockType := sockConf.Ext(intf.ID)
 		// Validate socket type is registered
 		if _, ok := registeredExtSockOps[sockType]; !ok {
 			return common.NewBasicError("No ExtSockOps found", nil,
-				"sockType", sockType, "ifid", intf.Id)
+				"sockType", sockType, "ifid", intf.ID)
 		}
 		// Validate same socket type.
-		if oldCtx.ExtSockIn[intf.Id] != nil && oldCtx.ExtSockIn[intf.Id].Type != sockType {
+		if oldCtx.ExtSockIn[intf.ID] != nil && oldCtx.ExtSockIn[intf.ID].Type != sockType {
 			return common.NewBasicError("Unable to switch external socket type", nil,
-				"expected", oldCtx.ExtSockIn[intf.Id].Type, "actual", sockType)
+				"expected", oldCtx.ExtSockIn[intf.ID].Type, "actual", sockType)
 		}
 
 		// Validate interface does not take over local address.
-		if intf.Local.String() == oldCtx.Conf.BR.InternalAddrs.String() {
+		if intf.Local.String() == oldCtx.Conf.BR.InternalAddr.String() {
 			return common.NewBasicError("Address must not switch from local", nil,
-				"intf", intf, "locAddr", oldCtx.Conf.BR.InternalAddrs)
+				"intf", intf, "locAddr", oldCtx.Conf.BR.InternalAddr)
 		}
 		for _, oldIntf := range oldCtx.Conf.BR.IFs {
 			// Validate interface does not take over the address of old interface.
-			if intf.Local.String() == oldIntf.Local.String() && 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.String() == oldIntf.Local.String() {
+			if ctx.Conf.BR.InternalAddr.String() == oldIntf.Local.String() {
 				return common.NewBasicError("Address must not switch to local", nil,
-					"oldIntf", intf, "locAddr", oldCtx.Conf.BR.InternalAddrs)
+					"oldIntf", intf, "locAddr", oldCtx.Conf.BR.InternalAddr)
 			}
 		}
 	}
diff --git a/go/cert_srv/BUILD.bazel b/go/cert_srv/BUILD.bazel
index 1648d0c04..92a9d32e9 100644
--- a/go/cert_srv/BUILD.bazel
+++ b/go/cert_srv/BUILD.bazel
@@ -29,6 +29,7 @@ go_library(
         "//go/lib/prom:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/proto:go_default_library",
         "@com_github_burntsushi_toml//:go_default_library",
         "@com_github_opentracing_opentracing_go//:go_default_library",
diff --git a/go/cert_srv/setup.go b/go/cert_srv/setup.go
index 945c20f27..6a2eed58e 100644
--- a/go/cert_srv/setup.go
+++ b/go/cert_srv/setup.go
@@ -36,6 +36,7 @@ import (
 	"github.com/scionproto/scion/go/lib/prom"
 	"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/proto"
 )
 
@@ -67,7 +68,7 @@ func setup() error {
 		return common.NewBasicError("Unable to validate config", err)
 	}
 	itopo.Init(cfg.General.ID, proto.ServiceType_cs, itopo.Callbacks{})
-	topo, err := itopo.LoadFromFile(cfg.General.Topology)
+	topo, err := topology.FromJSONFile(cfg.General.Topology)
 	if err != nil {
 		return common.NewBasicError("Unable to load topology", err)
 	}
@@ -198,7 +199,7 @@ func setMessenger(cfg *config.Config, router snet.Router) error {
 	return nil
 }
 
-func initTopo(topo itopo.Topology) error {
+func initTopo(topo topology.Topology) error {
 	if _, _, err := itopo.SetStatic(topo, false); err != nil {
 		return common.NewBasicError("Unable to set initial static topology", err)
 	}
diff --git a/go/examples/discovery_client/client.go b/go/examples/discovery_client/client.go
index 5c9318e58..abec5ae4e 100644
--- a/go/examples/discovery_client/client.go
+++ b/go/examples/discovery_client/client.go
@@ -81,11 +81,11 @@ func realMain() int {
 			Error: func(ctx context.Context, err error) {
 				log.FromCtx(ctx).Error("Unable to fetch topology", "err", err)
 			},
-			Update: func(ctx context.Context, topo *topology.Topo) {
+			Update: func(ctx context.Context, topo *topology.RWTopology) {
 				log.FromCtx(ctx).Info("Fetched new topology",
-					"ia", topo.ISD_AS, "ts", topo.Timestamp)
+					"ia", topo.IA, "ts", topo.Timestamp)
 			},
-			Raw: func(ctx context.Context, raw common.RawBytes, _ *topology.Topo) {
+			Raw: func(ctx context.Context, raw common.RawBytes, _ *topology.RWTopology) {
 				writeOnce.Do(func() {
 					fmt.Println(string(raw))
 					if *out == "" {
@@ -121,7 +121,7 @@ func validateFlags() error {
 	return nil
 }
 
-func getTopo() (*topology.Topo, error) {
+func getTopo() (*topology.RWTopology, error) {
 	if *ds != "" {
 		log.Info("Fetch initial topology from discovery service", "addr", ds)
 		ctx, cancelF := context.WithTimeout(context.Background(), *timeout)
@@ -141,7 +141,7 @@ func getTopo() (*topology.Topo, error) {
 		return ret, err
 	}
 	log.Info("Load initial topology from disk", "topo", topoPath)
-	return topology.LoadFromFile(*topoPath)
+	return topology.RWTopologyFromJSONFile(*topoPath)
 }
 
 func mode() discovery.Mode {
diff --git a/go/lib/discovery/discovery.go b/go/lib/discovery/discovery.go
index 985d7f401..d1df10f82 100644
--- a/go/lib/discovery/discovery.go
+++ b/go/lib/discovery/discovery.go
@@ -119,7 +119,7 @@ const (
 // discovery service. If client is nil, the default http client is used.
 // Both the topology and the raw response body are returned.
 func FetchTopoRaw(ctx context.Context, params FetchParams, ds *net.UDPAddr,
-	client *http.Client) (*topology.Topo, common.RawBytes, error) {
+	client *http.Client) (*topology.RWTopology, common.RawBytes, error) {
 
 	url, err := createURL(params, ds)
 	if err != nil {
@@ -137,7 +137,7 @@ func FetchTopoRaw(ctx context.Context, params FetchParams, ds *net.UDPAddr,
 	if err != nil {
 		return nil, nil, common.NewBasicError("Unable to read body", err)
 	}
-	topo, err := topology.Load(raw)
+	topo, err := topology.RWTopologyFromJSONBytes(raw)
 	if err != nil {
 		return nil, nil, common.NewBasicError("Unable to parse topo", err)
 	}
diff --git a/go/lib/discovery/discoverypool/BUILD.bazel b/go/lib/discovery/discoverypool/BUILD.bazel
index f81281409..a97583b93 100644
--- a/go/lib/discovery/discoverypool/BUILD.bazel
+++ b/go/lib/discovery/discoverypool/BUILD.bazel
@@ -21,7 +21,6 @@ go_test(
     embed = [":go_default_library"],
     deps = [
         "//go/lib/addr:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/topology:go_default_library",
         "//go/lib/xtest:go_default_library",
         "@com_github_smartystreets_goconvey//convey:go_default_library",
diff --git a/go/lib/discovery/topofetcher/fetcher.go b/go/lib/discovery/topofetcher/fetcher.go
index d5ed7d64c..73accd1e0 100644
--- a/go/lib/discovery/topofetcher/fetcher.go
+++ b/go/lib/discovery/topofetcher/fetcher.go
@@ -32,9 +32,9 @@ var _ discovery.Fetcher = (*Fetcher)(nil)
 // an associated event occurs. If the function is nil, it is ignored.
 type Callbacks struct {
 	// Raw is called with the raw body from the discovery service response and the parsed topology.
-	Raw func(context.Context, common.RawBytes, *topology.Topo)
+	Raw func(context.Context, common.RawBytes, *topology.RWTopology)
 	// Update is called with the parsed topology from the discovery service response.
-	Update func(context.Context, *topology.Topo)
+	Update func(context.Context, *topology.RWTopology)
 	// Error is called with any error that occurs.
 	Error func(context.Context, error)
 }
diff --git a/go/lib/env/BUILD.bazel b/go/lib/env/BUILD.bazel
index 274c6bc06..a07ad9ba4 100644
--- a/go/lib/env/BUILD.bazel
+++ b/go/lib/env/BUILD.bazel
@@ -20,6 +20,7 @@ go_library(
         "//go/lib/sciond:go_default_library",
         "//go/lib/scrypto:go_default_library",
         "//go/lib/serrors:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/lib/util:go_default_library",
         "@com_github_opentracing_opentracing_go//:go_default_library",
         "@com_github_prometheus_client_golang//prometheus/promhttp:go_default_library",
diff --git a/go/lib/env/env.go b/go/lib/env/env.go
index 1fc5a59ed..09ca71395 100644
--- a/go/lib/env/env.go
+++ b/go/lib/env/env.go
@@ -42,6 +42,7 @@ import (
 	"github.com/scionproto/scion/go/lib/sciond"
 	_ "github.com/scionproto/scion/go/lib/scrypto" // Make sure math/rand is seeded
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/util"
 )
 
@@ -189,7 +190,7 @@ func setupSignals(reloadF func()) {
 }
 
 func ReloadTopology(topologyPath string) {
-	topo, err := itopo.LoadFromFile(topologyPath)
+	topo, err := topology.FromJSONFile(topologyPath)
 	if err != nil {
 		log.Error("Unable to reload topology", "err", err)
 		return
diff --git a/go/lib/healthpool/svcinstance/BUILD.bazel b/go/lib/healthpool/svcinstance/BUILD.bazel
index fceebd84f..547816bf2 100644
--- a/go/lib/healthpool/svcinstance/BUILD.bazel
+++ b/go/lib/healthpool/svcinstance/BUILD.bazel
@@ -23,7 +23,6 @@ go_test(
     deps = [
         "//go/lib/addr:go_default_library",
         "//go/lib/healthpool:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/topology:go_default_library",
         "//go/lib/xtest:go_default_library",
         "@com_github_smartystreets_goconvey//convey:go_default_library",
diff --git a/go/lib/infra/messenger/BUILD.bazel b/go/lib/infra/messenger/BUILD.bazel
index 21dcc5ee3..b4bf610dc 100644
--- a/go/lib/infra/messenger/BUILD.bazel
+++ b/go/lib/infra/messenger/BUILD.bazel
@@ -27,13 +27,13 @@ go_library(
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/disp:go_default_library",
         "//go/lib/infra/messenger/internal/metrics:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/infra/rpc:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/scrypto:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/svc:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/lib/tracing:go_default_library",
         "//go/lib/util:go_default_library",
         "//go/proto:go_default_library",
diff --git a/go/lib/infra/messenger/addr.go b/go/lib/infra/messenger/addr.go
index 9627798b5..fb36d33bb 100644
--- a/go/lib/infra/messenger/addr.go
+++ b/go/lib/infra/messenger/addr.go
@@ -21,11 +21,11 @@ import (
 
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/snet"
 	"github.com/scionproto/scion/go/lib/svc"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 // Resolver performs SVC resolution for a remote AS, thus converting an anycast
@@ -236,14 +236,14 @@ type LocalSVCRouter interface {
 
 // NewSVCRouter build a SVC router backed by topology information from the
 // specified provider.
-func NewSVCRouter(tp itopo.ProviderI) LocalSVCRouter {
+func NewSVCRouter(tp topology.Provider) LocalSVCRouter {
 	return &baseSVCRouter{
 		topology: tp,
 	}
 }
 
 type baseSVCRouter struct {
-	topology itopo.ProviderI
+	topology topology.Provider
 }
 
 func (r *baseSVCRouter) GetOverlay(svc addr.HostSVC) (*net.UDPAddr, error) {
diff --git a/go/lib/infra/modules/idiscovery/idiscovery.go b/go/lib/infra/modules/idiscovery/idiscovery.go
index 582b44a5f..f81f21716 100644
--- a/go/lib/infra/modules/idiscovery/idiscovery.go
+++ b/go/lib/infra/modules/idiscovery/idiscovery.go
@@ -56,15 +56,15 @@ import (
 
 // TopoHandler handles a topology fetched from the discovery service, and
 // returns whether the provided topology is an update.
-type TopoHandler func(topo *topology.Topo) (bool, error)
+type TopoHandler func(topo *topology.RWTopology) (bool, error)
 
-func staticSetter(topo *topology.Topo) (bool, error) {
-	_, updated, err := itopo.SetStatic(itopo.NewTopologyFromRaw(topo), false)
+func staticSetter(topo *topology.RWTopology) (bool, error) {
+	_, updated, err := itopo.SetStatic(topology.FromRWTopology(topo), false)
 	return updated, err
 }
 
 // dynamicSetter sets the dynamic topology in itopo with the provided topology.
-func dynamicSetter(topo *topology.Topo) (bool, error) {
+func dynamicSetter(topo *topology.RWTopology) (bool, error) {
 	_, updated, err := itopo.SetDynamic(topo)
 	return updated, err
 }
@@ -273,7 +273,7 @@ func (r *Runner) startFetch(fetcher *task, timeout time.Duration) {
 
 // handler wraps the handler provided by the caller and sets the set flag if a topology
 // is handled successfully without error.
-func (r *Runner) handler(topo *topology.Topo) (bool, error) {
+func (r *Runner) handler(topo *topology.RWTopology) (bool, error) {
 	updated, err := r.rawHandler(topo)
 	if err != nil {
 		return updated, err
@@ -341,7 +341,7 @@ func (t *task) handleErr(ctx context.Context, err error) {
 	metrics.Fetcher.Sent(l).Inc()
 }
 
-func (t *task) handleRaw(ctx context.Context, raw common.RawBytes, topo *topology.Topo) {
+func (t *task) handleRaw(ctx context.Context, raw common.RawBytes, topo *topology.RWTopology) {
 	l := metrics.FetcherLabels{Static: t.static(), Result: metrics.Success}
 	updated, err := t.callHandler(ctx, topo)
 	switch {
@@ -373,7 +373,7 @@ func (t *task) writeFile(ctx context.Context, raw common.RawBytes) error {
 	return nil
 }
 
-func (t *task) callHandler(ctx context.Context, topo *topology.Topo) (bool, error) {
+func (t *task) callHandler(ctx context.Context, topo *topology.RWTopology) (bool, error) {
 	updated, err := t.handler(topo)
 	if err != nil {
 		t.logger(ctx).Error("[discovery] Unable to handle topology", "err", err)
diff --git a/go/lib/infra/modules/itopo/BUILD.bazel b/go/lib/infra/modules/itopo/BUILD.bazel
index b2a7c6377..5af1ca21a 100644
--- a/go/lib/infra/modules/itopo/BUILD.bazel
+++ b/go/lib/infra/modules/itopo/BUILD.bazel
@@ -6,22 +6,17 @@ go_library(
         "cleaner.go",
         "doc.go",
         "itopo.go",
-        "topology.go",
         "validate.go",
     ],
     importpath = "github.com/scionproto/scion/go/lib/infra/modules/itopo",
     visibility = ["//visibility:public"],
     deps = [
-        "//go/lib/addr:go_default_library",
         "//go/lib/common:go_default_library",
         "//go/lib/infra/modules/itopo/internal/metrics:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/periodic:go_default_library",
-        "//go/lib/scmp:go_default_library",
         "//go/lib/serrors:go_default_library",
-        "//go/lib/snet:go_default_library",
         "//go/lib/topology:go_default_library",
-        "//go/lib/topology/overlay:go_default_library",
         "//go/proto:go_default_library",
         "@com_github_google_go_cmp//cmp:go_default_library",
         "@com_github_google_go_cmp//cmp/cmpopts:go_default_library",
@@ -38,7 +33,6 @@ go_test(
     embed = [":go_default_library"],
     deps = [
         "//go/lib/topology:go_default_library",
-        "//go/lib/topology/overlay:go_default_library",
         "//go/lib/xtest:go_default_library",
         "//go/lib/xtest/mock_xtest:go_default_library",
         "//go/proto:go_default_library",
diff --git a/go/lib/infra/modules/itopo/itopo.go b/go/lib/infra/modules/itopo/itopo.go
index ff69ff3a7..49551e71d 100644
--- a/go/lib/infra/modules/itopo/itopo.go
+++ b/go/lib/infra/modules/itopo/itopo.go
@@ -42,15 +42,11 @@ type Callbacks struct {
 	UpdateStatic func()
 }
 
-type ProviderI interface {
-	Get() Topology
-}
-
 // providerFunc wraps the Get call as a topology provider.
-type providerFunc func() Topology
+type providerFunc func() topology.Topology
 
 // Provider returns a topology provider that calls Get internally.
-func Provider() ProviderI {
+func Provider() topology.Provider {
 	st.RLock()
 	defer st.RUnlock()
 	if st.topo.static == nil {
@@ -59,7 +55,7 @@ func Provider() ProviderI {
 	return providerFunc(Get)
 }
 
-func (f providerFunc) Get() Topology {
+func (f providerFunc) Get() topology.Topology {
 	return f()
 }
 
@@ -73,19 +69,17 @@ func Init(id string, svc proto.ServiceType, clbks Callbacks) {
 }
 
 // Get atomically gets the pointer to the current topology.
-func Get() Topology {
+func Get() topology.Topology {
 	st.RLock()
 	defer st.RUnlock()
-	return &topologyS{
-		Topology: st.topo.Get(),
-	}
+	return topology.FromRWTopology(st.topo.Get())
 }
 
 // SetDynamic atomically sets the dynamic topology. The returned topology is a pointer
 // to the currently active topology at the end of the function call. It might differ from
 // the input topology. The second return value indicates whether the in-memory
 // copy of the dynamic topology has been updated.
-func SetDynamic(dynamic *topology.Topo) (*topology.Topo, bool, error) {
+func SetDynamic(dynamic *topology.RWTopology) (*topology.RWTopology, bool, error) {
 	l := metrics.UpdateLabels{Type: metrics.Dynamic}
 	topo, updated, err := st.setDynamic(dynamic)
 	switch {
@@ -102,7 +96,7 @@ func SetDynamic(dynamic *topology.Topo) (*topology.Topo, bool, error) {
 
 // BeginSetDynamic checks whether setting the dynamic topology is permissible. The returned
 // transaction provides a view on which topology would be active, if committed.
-func BeginSetDynamic(dynamic *topology.Topo) (Transaction, error) {
+func BeginSetDynamic(dynamic *topology.RWTopology) (Transaction, error) {
 	tx, err := st.beginSetDynamic(dynamic)
 	if err != nil {
 		incUpdateMetric(metrics.UpdateLabels{Type: metrics.Dynamic, Result: metrics.ErrValidate})
@@ -116,9 +110,11 @@ func BeginSetDynamic(dynamic *topology.Topo) (Transaction, error) {
 // It might differ from the input topology (same contents as existing static,
 // or dynamic set and still valid). The second return value indicates whether the in-memory
 // copy of the static topology has been updated.
-func SetStatic(static Topology, semiMutAllowed bool) (Topology, bool, error) {
+func SetStatic(static topology.Topology,
+	semiMutAllowed bool) (topology.Topology, bool, error) {
+
 	l := metrics.UpdateLabels{Type: metrics.Static}
-	topo, updated, err := st.setStatic(static.Raw(), semiMutAllowed)
+	topo, updated, err := st.setStatic(static.Writable(), semiMutAllowed)
 	switch {
 	case err != nil:
 		l.Result = metrics.ErrValidate
@@ -128,12 +124,12 @@ func SetStatic(static Topology, semiMutAllowed bool) (Topology, bool, error) {
 		l.Result = metrics.OkIgnored
 	}
 	incUpdateMetric(l)
-	return NewTopologyFromRaw(topo), updated, err
+	return topology.FromRWTopology(topo), updated, err
 }
 
 // BeginSetStatic checks whether setting the static topology is permissible. The returned
 // transaction provides a view on which topology would be active, if committed.
-func BeginSetStatic(static *topology.Topo, semiMutAllowed bool) (Transaction, error) {
+func BeginSetStatic(static *topology.RWTopology, semiMutAllowed bool) (Transaction, error) {
 	tx, err := st.beginSetStatic(static, semiMutAllowed)
 	if err != nil {
 		incUpdateMetric(metrics.UpdateLabels{Type: metrics.Static, Result: metrics.ErrValidate})
@@ -149,11 +145,11 @@ type Transaction struct {
 	candidateTopo topo
 	// staticAtTxStart stores a snapshot of the currently active static
 	// topology at transaction start.
-	staticAtTxStart *topology.Topo
+	staticAtTxStart *topology.RWTopology
 	// inputStatic stores the provided static topology.
-	inputStatic *topology.Topo
+	inputStatic *topology.RWTopology
 	// inputDynamic stores the provided dynamic topology.
-	inputDynamic *topology.Topo
+	inputDynamic *topology.RWTopology
 }
 
 // Commit commits the change. An error is returned, if the static topology changed in the meantime.
@@ -185,7 +181,7 @@ func (tx *Transaction) Commit() error {
 }
 
 // Get returns the topology that will be active if the transaction is committed.
-func (tx *Transaction) Get() *topology.Topo {
+func (tx *Transaction) Get() *topology.RWTopology {
 	return tx.candidateTopo.Get()
 }
 
@@ -199,13 +195,13 @@ func (tx *Transaction) IsUpdate() bool {
 
 // topo stores the currently active static and dynamic topologies.
 type topo struct {
-	static  *topology.Topo
-	dynamic *topology.Topo
+	static  *topology.RWTopology
+	dynamic *topology.RWTopology
 }
 
 // Get returns the dynamic topology if it is set and has not expired. Otherwise,
 // the static topology is returned.
-func (t *topo) Get() *topology.Topo {
+func (t *topo) Get() *topology.RWTopology {
 	if t.dynamic != nil && t.dynamic.Active(time.Now()) {
 		return t.dynamic
 	}
@@ -229,7 +225,7 @@ func newState(id string, svc proto.ServiceType, clbks Callbacks) *state {
 }
 
 // setDynamic atomically sets the dynamic topology.
-func (s *state) setDynamic(dynamic *topology.Topo) (*topology.Topo, bool, error) {
+func (s *state) setDynamic(dynamic *topology.RWTopology) (*topology.RWTopology, bool, error) {
 	s.Lock()
 	defer s.Unlock()
 	if err := s.dynamicPreCheck(dynamic); err != nil {
@@ -245,7 +241,7 @@ func (s *state) setDynamic(dynamic *topology.Topo) (*topology.Topo, bool, error)
 	return s.topo.Get(), true, nil
 }
 
-func (s *state) updateDynamic(dynamic *topology.Topo) {
+func (s *state) updateDynamic(dynamic *topology.RWTopology) {
 	s.topo.dynamic = dynamic
 	cl := metrics.CurrentLabels{Type: metrics.Dynamic}
 	metrics.Current.Active().Set(1)
@@ -253,7 +249,7 @@ func (s *state) updateDynamic(dynamic *topology.Topo) {
 	metrics.Current.Expiry(cl).Set(metrics.Expiry(dynamic.Expiry()))
 }
 
-func (s *state) beginSetDynamic(dynamic *topology.Topo) (Transaction, error) {
+func (s *state) beginSetDynamic(dynamic *topology.RWTopology) (Transaction, error) {
 	s.Lock()
 	defer s.Unlock()
 	if err := s.dynamicPreCheck(dynamic); err != nil {
@@ -274,7 +270,7 @@ func (s *state) beginSetDynamic(dynamic *topology.Topo) (Transaction, error) {
 	return tx, nil
 }
 
-func (s *state) dynamicPreCheck(dynamic *topology.Topo) error {
+func (s *state) dynamicPreCheck(dynamic *topology.RWTopology) error {
 	if dynamic == nil {
 		return serrors.New("Provided topology must not be nil")
 	}
@@ -290,7 +286,9 @@ func (s *state) dynamicPreCheck(dynamic *topology.Topo) error {
 }
 
 // setStatic atomically sets the static topology.
-func (s *state) setStatic(static *topology.Topo, allowed bool) (*topology.Topo, bool, error) {
+func (s *state) setStatic(static *topology.RWTopology,
+	allowed bool) (*topology.RWTopology, bool, error) {
+
 	s.Lock()
 	defer s.Unlock()
 	if err := s.validator.Validate(static, s.topo.static, allowed); err != nil {
@@ -304,7 +302,7 @@ func (s *state) setStatic(static *topology.Topo, allowed bool) (*topology.Topo,
 	return s.topo.Get(), true, nil
 }
 
-func (s *state) beginSetStatic(static *topology.Topo, allowed bool) (Transaction, error) {
+func (s *state) beginSetStatic(static *topology.RWTopology, allowed bool) (Transaction, error) {
 	s.Lock()
 	defer s.Unlock()
 	if err := s.validator.Validate(static, s.topo.static, allowed); err != nil {
@@ -327,7 +325,7 @@ func (s *state) beginSetStatic(static *topology.Topo, allowed bool) (Transaction
 }
 
 // updateStatic updates the static topology, if necessary, and calls the corresponding callbacks.
-func (s *state) updateStatic(static *topology.Topo) {
+func (s *state) updateStatic(static *topology.RWTopology) {
 	// Drop dynamic topology if necessary.
 	if s.validator.MustDropDynamic(static, s.topo.static) && s.topo.dynamic != nil {
 		s.topo.dynamic = nil
@@ -341,16 +339,16 @@ func (s *state) updateStatic(static *topology.Topo) {
 	metrics.Current.Expiry(cl).Set(metrics.Expiry(static.Expiry()))
 }
 
-func keepOld(newTopo, oldTopo *topology.Topo) bool {
+func keepOld(newTopo, oldTopo *topology.RWTopology) bool {
 	return topoEq(newTopo, oldTopo) && !expiresLater(newTopo, oldTopo)
 }
 
-func topoEq(newTopo, oldTopo *topology.Topo) bool {
+func topoEq(newTopo, oldTopo *topology.RWTopology) bool {
 	return cmp.Equal(newTopo, oldTopo, cmpopts.IgnoreFields(
-		topology.Topo{}, "Timestamp", "TimestampHuman", "TTL"))
+		topology.RWTopology{}, "Timestamp", "TTL"))
 }
 
-func expiresLater(newTopo, oldTopo *topology.Topo) bool {
+func expiresLater(newTopo, oldTopo *topology.RWTopology) bool {
 	if oldTopo == nil {
 		return true
 	}
diff --git a/go/lib/infra/modules/itopo/validate.go b/go/lib/infra/modules/itopo/validate.go
index fd1d21076..80813e908 100644
--- a/go/lib/infra/modules/itopo/validate.go
+++ b/go/lib/infra/modules/itopo/validate.go
@@ -27,7 +27,7 @@ import (
 
 type dynDropper interface {
 	// MustDropDynamic indicates whether the dynamic topology shall be dropped.
-	MustDropDynamic(topo, oldTopo *topology.Topo) bool
+	MustDropDynamic(topo, oldTopo *topology.RWTopology) bool
 }
 
 // validator is used to validate that the topology update is permissible.
@@ -36,7 +36,7 @@ type validator interface {
 	// Validate checks that the topology update is valid according to the mutability rules.
 	// The validation rules differ between service types. However, at the very least, it
 	// is checked that topo is not nil and the immutable parts do not change.
-	Validate(topo, oldTopo *topology.Topo, allowed bool) error
+	Validate(topo, oldTopo *topology.RWTopology, allowed bool) error
 }
 
 func validatorFactory(id string, svc proto.ServiceType) validator {
@@ -58,7 +58,7 @@ type validatorWrap struct {
 }
 
 // Validate checks that the topology update is valid.
-func (v *validatorWrap) Validate(topo, oldTopo *topology.Topo, allowed bool) error {
+func (v *validatorWrap) Validate(topo, oldTopo *topology.RWTopology, allowed bool) error {
 	if err := v.General(topo); err != nil {
 		return err
 	}
@@ -76,11 +76,11 @@ type internalValidator interface {
 	// General checks that the topology is generally valid. The exact check is implementation
 	// specific to the validator. However, at the very least, this check ensures that the
 	// provided topology is non-nil.
-	General(topo *topology.Topo) error
+	General(topo *topology.RWTopology) error
 	// Immutable checks that the immutable parts of the topology do not change.
-	Immutable(topo, oldTopo *topology.Topo) error
+	Immutable(topo, oldTopo *topology.RWTopology) error
 	// SemiMutable checks that the semi-mutable parts of the topology update are valid.
-	SemiMutable(topo, oldTopo *topology.Topo, allowed bool) error
+	SemiMutable(topo, oldTopo *topology.RWTopology, allowed bool) error
 }
 
 var _ internalValidator = (*generalValidator)(nil)
@@ -90,29 +90,25 @@ var _ internalValidator = (*generalValidator)(nil)
 // not modified.
 type generalValidator struct{}
 
-func (v *generalValidator) General(topo *topology.Topo) error {
+func (v *generalValidator) General(topo *topology.RWTopology) error {
 	if topo == nil {
 		return serrors.New("Topo must not be nil")
 	}
 	return nil
 }
 
-func (v *generalValidator) Immutable(topo, oldTopo *topology.Topo) error {
+func (v *generalValidator) Immutable(topo, oldTopo *topology.RWTopology) error {
 	if oldTopo == nil {
 		return nil
 	}
-	if !topo.ISD_AS.Equal(oldTopo.ISD_AS) {
+	if !topo.IA.Equal(oldTopo.IA) {
 		return common.NewBasicError("IA is immutable", nil,
-			"expected", oldTopo.ISD_AS, "actual", topo.ISD_AS)
+			"expected", oldTopo.IA, "actual", topo.IA)
 	}
 	if topo.Core != oldTopo.Core {
 		return common.NewBasicError("Core is immutable", nil,
 			"expected", oldTopo.Core, "actual", topo.Core)
 	}
-	if topo.Overlay != oldTopo.Overlay {
-		return common.NewBasicError("Overlay is immutable", nil,
-			"expected", oldTopo.Overlay, "actual", topo.Overlay)
-	}
 	if topo.MTU != oldTopo.MTU {
 		return common.NewBasicError("MTU is immutable", nil,
 			"expected", oldTopo.MTU, "actual", topo.MTU)
@@ -120,11 +116,11 @@ func (v *generalValidator) Immutable(topo, oldTopo *topology.Topo) error {
 	return nil
 }
 
-func (*generalValidator) SemiMutable(_, _ *topology.Topo, _ bool) error {
+func (*generalValidator) SemiMutable(_, _ *topology.RWTopology, _ bool) error {
 	return nil
 }
 
-func (*generalValidator) MustDropDynamic(_, _ *topology.Topo) bool {
+func (*generalValidator) MustDropDynamic(_, _ *topology.RWTopology) bool {
 	return false
 }
 
@@ -138,7 +134,7 @@ type svcValidator struct {
 	svc proto.ServiceType
 }
 
-func (v *svcValidator) General(topo *topology.Topo) error {
+func (v *svcValidator) General(topo *topology.RWTopology) error {
 	if err := v.generalValidator.General(topo); err != nil {
 		return err
 	}
@@ -148,7 +144,7 @@ func (v *svcValidator) General(topo *topology.Topo) error {
 	return nil
 }
 
-func (v *svcValidator) Immutable(topo, oldTopo *topology.Topo) error {
+func (v *svcValidator) Immutable(topo, oldTopo *topology.RWTopology) error {
 	if oldTopo == nil {
 		return nil
 	}
@@ -178,7 +174,7 @@ type brValidator struct {
 	id string
 }
 
-func (v *brValidator) General(topo *topology.Topo) error {
+func (v *brValidator) General(topo *topology.RWTopology) error {
 	if err := v.generalValidator.General(topo); err != nil {
 		return err
 	}
@@ -188,16 +184,16 @@ func (v *brValidator) General(topo *topology.Topo) error {
 	return nil
 }
 
-func (v *brValidator) Immutable(topo, oldTopo *topology.Topo) error {
+func (v *brValidator) Immutable(topo, oldTopo *topology.RWTopology) error {
 	if oldTopo == nil {
 		return nil
 	}
 	if err := v.generalValidator.Immutable(topo, oldTopo); err != nil {
 		return err
 	}
-	if topo.BR[v.id].InternalAddrs.String() != oldTopo.BR[v.id].InternalAddrs.String() {
+	if topo.BR[v.id].InternalAddr.String() != oldTopo.BR[v.id].InternalAddr.String() {
 		return common.NewBasicError("InternalAddrs is immutable", nil, "expected",
-			oldTopo.BR[v.id].InternalAddrs, "actual", topo.BR[v.id].InternalAddrs)
+			oldTopo.BR[v.id].InternalAddr, "actual", topo.BR[v.id].InternalAddr)
 	}
 	if !reflect.DeepEqual(topo.BR[v.id].CtrlAddrs, oldTopo.BR[v.id].CtrlAddrs) {
 		return common.NewBasicError("CtrlAddrs is immutable", nil, "expected",
@@ -206,7 +202,7 @@ func (v *brValidator) Immutable(topo, oldTopo *topology.Topo) error {
 	return nil
 }
 
-func (v *brValidator) SemiMutable(topo, oldTopo *topology.Topo, allowed bool) error {
+func (v *brValidator) SemiMutable(topo, oldTopo *topology.RWTopology, allowed bool) error {
 	if oldTopo == nil {
 		return nil
 	}
@@ -217,14 +213,14 @@ func (v *brValidator) SemiMutable(topo, oldTopo *topology.Topo, allowed bool) er
 	return nil
 }
 
-func (v *brValidator) MustDropDynamic(topo, oldTopo *topology.Topo) bool {
+func (v *brValidator) MustDropDynamic(topo, oldTopo *topology.RWTopology) bool {
 	if oldTopo == nil {
 		return false
 	}
 	return v.interfacesChanged(topo, oldTopo)
 }
 
-func (v *brValidator) interfacesChanged(topo, oldTopo *topology.Topo) bool {
+func (v *brValidator) interfacesChanged(topo, oldTopo *topology.RWTopology) bool {
 	if v.interfaceSetChanged(topo, oldTopo) {
 		return true
 	}
@@ -236,7 +232,7 @@ func (v *brValidator) interfacesChanged(topo, oldTopo *topology.Topo) bool {
 	return false
 }
 
-func (v *brValidator) interfaceSetChanged(topo, oldTopo *topology.Topo) bool {
+func (v *brValidator) interfaceSetChanged(topo, oldTopo *topology.RWTopology) bool {
 	if len(topo.BR[v.id].IFIDs) != len(oldTopo.BR[v.id].IFIDs) {
 		return true
 	}
diff --git a/go/lib/infra/modules/trust/BUILD.bazel b/go/lib/infra/modules/trust/BUILD.bazel
index c8e2a31c2..dd116fb6f 100644
--- a/go/lib/infra/modules/trust/BUILD.bazel
+++ b/go/lib/infra/modules/trust/BUILD.bazel
@@ -20,7 +20,6 @@ go_library(
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/dedupe:go_default_library",
         "//go/lib/infra/messenger:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/infra/modules/trust/internal/metrics:go_default_library",
         "//go/lib/infra/modules/trust/trustdb:go_default_library",
         "//go/lib/log:go_default_library",
@@ -29,6 +28,7 @@ go_library(
         "//go/lib/scrypto/trc:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/lib/util:go_default_library",
         "//go/proto:go_default_library",
         "@com_github_opentracing_opentracing_go//:go_default_library",
diff --git a/go/lib/infra/modules/trust/config.go b/go/lib/infra/modules/trust/config.go
index 957334487..7e46322a9 100644
--- a/go/lib/infra/modules/trust/config.go
+++ b/go/lib/infra/modules/trust/config.go
@@ -15,8 +15,8 @@
 package trust
 
 import (
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/snet"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/proto"
 )
 
@@ -34,5 +34,5 @@ type Config struct {
 	// Router is used to determine paths to other ASes.
 	Router snet.Router
 	// TopoProvider provides the local topology.
-	TopoProvider itopo.ProviderI
+	TopoProvider topology.Provider
 }
diff --git a/go/lib/infra/modules/trust/trust.go b/go/lib/infra/modules/trust/trust.go
index 077093763..bf60d5306 100644
--- a/go/lib/infra/modules/trust/trust.go
+++ b/go/lib/infra/modules/trust/trust.go
@@ -33,7 +33,6 @@ import (
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/infra/dedupe"
 	"github.com/scionproto/scion/go/lib/infra/messenger"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/infra/modules/trust/internal/metrics"
 	"github.com/scionproto/scion/go/lib/infra/modules/trust/trustdb"
 	"github.com/scionproto/scion/go/lib/log"
@@ -42,6 +41,7 @@ import (
 	"github.com/scionproto/scion/go/lib/scrypto/trc"
 	"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/proto"
 )
 
@@ -776,7 +776,7 @@ func (store *Store) ChooseServer(ctx context.Context, destination addr.IA) (net.
 //  * a local core CS if destination is isd-local or any core CS.
 //  * a remote core CS if destination is remote isd.
 func (store *Store) chooseDestCSIsd(ctx context.Context, destination addr.IA,
-	topo itopo.Topology) (addr.ISD, error) {
+	topo topology.Topology) (addr.ISD, error) {
 
 	// For isd-local dests use local core.
 	if destination.I == topo.IA().I {
diff --git a/go/lib/integration/integration.go b/go/lib/integration/integration.go
index 6885a1536..569050b51 100644
--- a/go/lib/integration/integration.go
+++ b/go/lib/integration/integration.go
@@ -188,7 +188,7 @@ type HostAddr func(ia addr.IA) snet.Addr
 // connecting to the dispatcher of the services.
 var DispAddr HostAddr = func(ia addr.IA) snet.Addr {
 	path := fmt.Sprintf("gen/ISD%d/AS%s/endhost/topology.json", ia.I, ia.A.FileFmt())
-	topo, err := topology.LoadFromFile(path)
+	topo, err := topology.RWTopologyFromJSONFile(path)
 	if err != nil {
 		log.Error("Error loading topology", "err", err)
 		os.Exit(1)
diff --git a/go/lib/snet/BUILD.bazel b/go/lib/snet/BUILD.bazel
index 7ce681bd2..6e7b400c2 100644
--- a/go/lib/snet/BUILD.bazel
+++ b/go/lib/snet/BUILD.bazel
@@ -31,7 +31,7 @@ go_library(
         "//go/lib/sock/reliable:go_default_library",
         "//go/lib/spath:go_default_library",
         "//go/lib/spkt:go_default_library",
-        "//go/lib/topology:go_default_library",
+        "//go/lib/topology/overlay:go_default_library",
     ],
 )
 
@@ -54,7 +54,7 @@ go_test(
         "//go/lib/snet/internal/ctxmonitor/mock_ctxmonitor:go_default_library",
         "//go/lib/snet/mock_snet:go_default_library",
         "//go/lib/spath: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_stretchr_testify//assert:go_default_library",
diff --git a/go/lib/snet/addrutil/BUILD.bazel b/go/lib/snet/addrutil/BUILD.bazel
index 19b448d7b..1b45fc1a6 100644
--- a/go/lib/snet/addrutil/BUILD.bazel
+++ b/go/lib/snet/addrutil/BUILD.bazel
@@ -9,8 +9,8 @@ go_library(
         "//go/lib/addr:go_default_library",
         "//go/lib/common:go_default_library",
         "//go/lib/ctrl/seg:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/spath:go_default_library",
+        "//go/lib/topology:go_default_library",
     ],
 )
diff --git a/go/lib/snet/addrutil/addrutil.go b/go/lib/snet/addrutil/addrutil.go
index 45d5ec100..2f553412f 100644
--- a/go/lib/snet/addrutil/addrutil.go
+++ b/go/lib/snet/addrutil/addrutil.go
@@ -21,13 +21,13 @@ import (
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/snet"
 	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 // GetPath creates a path from the given segment and then creates a snet.Addr.
-func GetPath(svc addr.HostSVC, ps *seg.PathSegment, topoProv itopo.ProviderI) (net.Addr, error) {
+func GetPath(svc addr.HostSVC, ps *seg.PathSegment, topoProv topology.Provider) (net.Addr, error) {
 	x := &bytes.Buffer{}
 	if _, err := ps.RawWriteTo(x); err != nil {
 		return nil, common.NewBasicError("failed to write segment to buffer", err)
diff --git a/go/lib/snet/writer.go b/go/lib/snet/writer.go
index ba9692792..1aa5f1e1e 100644
--- a/go/lib/snet/writer.go
+++ b/go/lib/snet/writer.go
@@ -26,7 +26,7 @@ import (
 	"github.com/scionproto/scion/go/lib/l4"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/snet/internal/ctxmonitor"
-	"github.com/scionproto/scion/go/lib/topology"
+	"github.com/scionproto/scion/go/lib/topology/overlay"
 )
 
 // Possible write errors
@@ -213,6 +213,6 @@ func (r *remoteAddressResolver) addPath(address *Addr) (*Addr, error) {
 
 func addOverlayFromScionAddress(address *Addr) *Addr {
 	address = address.Copy()
-	address.NextHop = &net.UDPAddr{IP: address.Host.L3.IP(), Port: topology.EndhostPort}
+	address.NextHop = &net.UDPAddr{IP: address.Host.L3.IP(), Port: overlay.EndhostPort}
 	return address
 }
diff --git a/go/lib/topology/BUILD.bazel b/go/lib/topology/BUILD.bazel
index 24b3b0f63..0254d250b 100644
--- a/go/lib/topology/BUILD.bazel
+++ b/go/lib/topology/BUILD.bazel
@@ -3,19 +3,22 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 go_library(
     name = "go_default_library",
     srcs = [
-        "addr.go",
         "doc.go",
+        "error.go",
+        "interface.go",
+        "linktype.go",
         "raw.go",
-        "strip.go",
         "topology.go",
-        "types.go",
     ],
     importpath = "github.com/scionproto/scion/go/lib/topology",
     visibility = ["//visibility:public"],
     deps = [
         "//go/lib/addr:go_default_library",
         "//go/lib/common:go_default_library",
+        "//go/lib/scmp:go_default_library",
         "//go/lib/serrors:go_default_library",
+        "//go/lib/snet:go_default_library",
+        "//go/lib/topology/json:go_default_library",
         "//go/lib/topology/overlay:go_default_library",
         "//go/proto:go_default_library",
     ],
@@ -24,14 +27,16 @@ go_library(
 go_test(
     name = "go_default_test",
     srcs = [
-        "addr_test.go",
+        "export_test.go",
         "topology_test.go",
+        "types_test.go",
     ],
     data = glob(["testdata/**"]),
     embed = [":go_default_library"],
     deps = [
         "//go/lib/addr:go_default_library",
         "//go/lib/common:go_default_library",
+        "//go/lib/topology/json:go_default_library",
         "//go/lib/topology/overlay:go_default_library",
         "//go/lib/xtest:go_default_library",
         "//go/proto:go_default_library",
diff --git a/go/lib/topology/addr.go b/go/lib/topology/addr.go
deleted file mode 100644
index e5abae918..000000000
--- a/go/lib/topology/addr.go
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright 2017 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"
-
-	"github.com/scionproto/scion/go/lib/addr"
-	"github.com/scionproto/scion/go/lib/serrors"
-	"github.com/scionproto/scion/go/lib/topology/overlay"
-)
-
-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 underlay to be used for contacting said service.
-type TopoAddr struct {
-	SCIONAddress    *addr.AppAddr
-	UnderlayAddress net.Addr
-}
-
-func (t *TopoAddr) UnderlayAddr() *net.UDPAddr {
-	return t.UnderlayAddress.(*net.UDPAddr)
-}
-
-func (t *TopoAddr) String() string {
-	return fmt.Sprintf("TopoAddr{SCION: %v, Underlay: %v}", t.SCIONAddress, t.UnderlayAddress)
-}
-
-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)
-	}
-}
-
-func toUDPAddr(a net.Addr) *net.UDPAddr {
-	if a == nil {
-		return nil
-	}
-	udpAddr, ok := a.(*net.UDPAddr)
-	if !ok {
-		return nil
-	}
-	return &net.UDPAddr{
-		IP:   append(udpAddr.IP[:0:0], udpAddr.IP...),
-		Port: udpAddr.Port,
-	}
-}
diff --git a/go/lib/topology/doc.go b/go/lib/topology/doc.go
index 9c0475834..ad582bee7 100644
--- a/go/lib/topology/doc.go
+++ b/go/lib/topology/doc.go
@@ -13,10 +13,9 @@
 // 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 RWTopology, which permits
+other packages to change topology information. The second topology type is Topology. It is
+used by packages that only need read access to the topology.
 
 The full JSON format for a SCION address looks like the following:
 
diff --git a/go/lib/topology/error.go b/go/lib/topology/error.go
new file mode 100644
index 000000000..bdae358c5
--- /dev/null
+++ b/go/lib/topology/error.go
@@ -0,0 +1,29 @@
+// Copyright 2019 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 "github.com/scionproto/scion/go/lib/serrors"
+
+var (
+	errAtLeastOnePub         = serrors.New("underlay requires at least one public address")
+	errUnsupportedUnderlay   = serrors.New("unsupported underlay")
+	errInvalidPub            = serrors.New("invalid public address")
+	errBindNotSupported      = serrors.New("bind addresses are not supported for this address type")
+	errCustomUnderlayPort    = serrors.New("custom underlay port not supported")
+	errUnderlayPort          = serrors.New("underlay port set for non-UDP underlay")
+	errUnderlayAddrNotFound  = 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")
+)
diff --git a/go/lib/infra/modules/itopo/topology.go b/go/lib/topology/interface.go
similarity index 74%
rename from go/lib/infra/modules/itopo/topology.go
rename to go/lib/topology/interface.go
index abdc0c490..466cca115 100644
--- a/go/lib/infra/modules/itopo/topology.go
+++ b/go/lib/topology/interface.go
@@ -12,21 +12,32 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package itopo
+package topology
 
 import (
 	"net"
+	"sort"
 
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/scmp"
 	"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/overlay"
 	"github.com/scionproto/scion/go/proto"
 )
 
+// Provider provides a topology snapshot. The snapshot is guaranteed to not change.
+type Provider interface {
+	// Get returns a topology. The returned topology is guaranteed to not be
+	// nil.
+	Get() Topology
+}
+
+// Topology is the topology type for applications and libraries that only need read access to AS
+// topology information. This is the case of most applications and libraries that use the topology
+// file to discover information about the local AS. Libraries that need to edit the topology (e.g.,
+// a topology reloading library that computes a new topology file based on information found on
+// disk) should instead use the writable topology type present in this package.
 type Topology interface {
 	// IA returns the local ISD-AS number.
 	IA() addr.IA
@@ -76,13 +87,13 @@ type Topology interface {
 	// FIXME(scrye): Simplify return type and make it topology format agnostic.
 	//
 	// XXX(scrye): Return value is a shallow copy.
-	BR(name string) (topology.BRInfo, bool)
+	BR(name string) (BRInfo, bool)
 	// IFInfoMap returns the mapping between interface IDs an internal addresses.
 	//
 	// FIXME(scrye): Simplify return type and make it topology format agnostic.
 	//
 	// XXX(scrye): Return value is a shallow copy.
-	IFInfoMap() topology.IfInfoMap
+	IFInfoMap() IfInfoMap
 
 	// BRNames returns the names of all BRs in the topology.
 	//
@@ -96,46 +107,42 @@ type Topology interface {
 	// FIXME(scrye): Remove this, callers shouldn't care about names.
 	//
 	// XXX(scrye): Return value is a shallow copy.
-	SVCNames(svc addr.HostSVC) []string
+	SVCNames(svc addr.HostSVC) ServiceNames
 
-	// Raw returns a pointer to the underlying topology object. This is included for legacy
+	// Writable returns a pointer to the underlying topology object. This is included for legacy
 	// reasons and should never be used.
 	//
 	// FIXME(scrye): Remove this.
 	//
 	// XXX(scrye): Return value is a shallow copy.
-	Raw() *topology.Topo
-	// Overlay returns the overlay running in the current AS.
-	//
-	// FIXME(scrye): Remove this.
-	Overlay() overlay.Type
+	Writable() *RWTopology
 
 	// DS returns the discovery servers in the topology.
 	//
 	// FIXME(scrye): Simplify return type and make it topology format agnostic.
 	//
 	// XXX(scrye): Return value is a shallow copy.
-	DS() topology.IDAddrMap
+	DS() IDAddrMap
 }
 
 // NewTopology creates a new empty topology.
 func NewTopology() Topology {
 	return &topologyS{
-		Topology: &topology.Topo{},
+		Topology: &RWTopology{},
 	}
 }
 
-// NewTopologyFromRaw wraps the high level topology interface API around a raw topology object.
-func NewTopologyFromRaw(topo *topology.Topo) Topology {
+// FromRWTopology wraps the high level topology interface API around a raw topology object.
+func FromRWTopology(topo *RWTopology) Topology {
 	return &topologyS{
 		Topology: topo,
 	}
 }
 
-type ServiceType string
-
-func LoadFromFile(path string) (Topology, error) {
-	t, err := topology.LoadFromFile(path)
+// FromJSONFile extracts the topology from a file containing the JSON representation of the
+// topology.
+func FromJSONFile(path string) (Topology, error) {
+	t, err := RWTopologyFromJSONFile(path)
 	if err != nil {
 		return nil, err
 	}
@@ -145,15 +152,15 @@ func LoadFromFile(path string) (Topology, error) {
 }
 
 type topologyS struct {
-	Topology *topology.Topo
+	Topology *RWTopology
 }
 
-func (t *topologyS) DS() topology.IDAddrMap {
+func (t *topologyS) DS() IDAddrMap {
 	return t.Topology.DS
 }
 
 func (t *topologyS) IA() addr.IA {
-	return t.Topology.ISD_AS
+	return t.Topology.IA
 }
 
 func (t *topologyS) MTU() uint16 {
@@ -173,7 +180,7 @@ func (t *topologyS) OverlayNextHop(ifid common.IFIDType) (*net.UDPAddr, bool) {
 	if !ok {
 		return nil, false
 	}
-	return copyUDP(ifInfo.InternalAddrs), true
+	return copyUDP(ifInfo.InternalAddr), true
 }
 
 func (t *topologyS) OverlayNextHop2(ifid common.IFIDType) (*net.UDPAddr, bool) {
@@ -181,7 +188,7 @@ func (t *topologyS) OverlayNextHop2(ifid common.IFIDType) (*net.UDPAddr, bool) {
 	if !ok {
 		return nil, false
 	}
-	return copyUDP(ifInfo.InternalAddrs), true
+	return copyUDP(ifInfo.InternalAddr), true
 }
 
 func (t *topologyS) MakeHostInfos(st proto.ServiceType) []net.UDPAddr {
@@ -213,7 +220,7 @@ func (t *topologyS) Core() bool {
 	return t.Topology.Core
 }
 
-func (t *topologyS) BR(name string) (topology.BRInfo, bool) {
+func (t *topologyS) BR(name string) (BRInfo, bool) {
 	br, ok := t.Topology.BR[name]
 	return br, ok
 }
@@ -223,19 +230,25 @@ func (t *topologyS) PublicAddress(svc addr.HostSVC, name string) *net.UDPAddr {
 	if topoAddr == nil {
 		return nil
 	}
-	pa := topoAddr.SCIONAddress
-	if pa == nil {
+	if topoAddr.SCIONAddress == nil {
 		return nil
 	}
-	return &net.UDPAddr{IP: pa.L3.IP(), Port: int(pa.L4)}
+	// FIXME(scrye): The interface this type implements offers read-only access. The copy below
+	// shouldn't be needed, but this can cause tricky to find bugs. We should tackle removing
+	// the copy in a separate PR.
+	ip := topoAddr.SCIONAddress.L3.IP()
+	return &net.UDPAddr{
+		IP:   append(ip[:0:0], ip...),
+		Port: int(topoAddr.SCIONAddress.L4),
+	}
 }
 
 func (t *topologyS) Exists(svc addr.HostSVC, name string) bool {
 	return t.PublicAddress(svc, name) != nil
 }
 
-func (t *topologyS) topoAddress(svc addr.HostSVC, name string) *topology.TopoAddr {
-	var addresses topology.IDAddrMap
+func (t *topologyS) topoAddress(svc addr.HostSVC, name string) *TopoAddr {
+	var addresses IDAddrMap
 	switch svc.Base() {
 	case addr.SvcBS:
 		addresses = t.Topology.BS
@@ -249,23 +262,36 @@ func (t *topologyS) topoAddress(svc addr.HostSVC, name string) *topology.TopoAdd
 	if addresses == nil {
 		return nil
 	}
-	return addresses.GetById(name)
+	return addresses.GetByID(name)
 }
 
 func (t *topologyS) OverlayAnycast(svc addr.HostSVC) (*net.UDPAddr, error) {
-	st, err := toProtoServiceType(svc)
-	if err != nil {
-		return nil, err
-	}
-	topoAddr, err := t.Topology.GetAnyTopoAddr(st)
+	names := t.SVCNames(svc)
+	name, err := names.GetRandom()
 	if err != nil {
+		if supportedSVC(svc) {
+			// FIXME(scrye): Return this error because some calling code in the BR searches for it.
+			// Ideally, the error should be communicated in a more explicit way.
+			return nil, common.NewBasicError("No instances found for SVC address",
+				scmp.NewError(scmp.C_Routing, scmp.T_R_UnreachHost, nil, nil), "svc", svc)
+		}
 		// FIXME(scrye): Return this error because some calling code in the BR searches for it.
 		// Ideally, the error should be communicated in a more explicit way.
-		return nil, common.NewBasicError("No instances found for SVC address",
-			scmp.NewError(scmp.C_Routing, scmp.T_R_UnreachHost, nil, nil), "svc", svc)
+		return nil, common.NewBasicError("Unsupported SVC address",
+			scmp.NewError(scmp.C_Routing, scmp.T_R_BadHost, nil, nil), "svc", svc)
+	}
+	underlay, err := t.OverlayByName(svc, name)
+	if err != nil {
+		return nil, serrors.WrapStr("BUG! Selected random service name, but service info not found",
+			err, "service_names", names, "selected_name", name)
 	}
 	// FIXME(scrye): This should return net.Addr
-	return copyUDP(topoAddr.UnderlayAddr()), nil
+	return underlay, nil
+}
+
+func supportedSVC(svc addr.HostSVC) bool {
+	b := svc.Base()
+	return b == addr.SvcBS || b == addr.SvcCS || b == addr.SvcPS || b == addr.SvcSIG
 }
 
 func (t *topologyS) OverlayMulticast(svc addr.HostSVC) ([]*net.UDPAddr, error) {
@@ -316,7 +342,7 @@ func (t *topologyS) OverlayByName(svc addr.HostSVC, name string) (*net.UDPAddr,
 	if overlayAddr == nil {
 		return nil, serrors.New("overlay address not found", "svc", svc)
 	}
-	return overlayAddr, nil
+	return copyUDPAddr(overlayAddr), nil
 }
 
 func toProtoServiceType(svc addr.HostSVC) (proto.ServiceType, error) {
@@ -337,7 +363,7 @@ func toProtoServiceType(svc addr.HostSVC) (proto.ServiceType, error) {
 	}
 }
 
-func (t *topologyS) IFInfoMap() topology.IfInfoMap {
+func (t *topologyS) IFInfoMap() IfInfoMap {
 	return t.Topology.IFInfoMap
 }
 
@@ -357,26 +383,28 @@ func (t *topologyS) SBRAddress(name string) *snet.Addr {
 	}
 }
 
-func (t *topologyS) SVCNames(svc addr.HostSVC) []string {
+func (t *topologyS) SVCNames(svc addr.HostSVC) ServiceNames {
+	var m IDAddrMap
 	switch svc.Base() {
 	case addr.SvcBS:
-		return t.Topology.BSNames
+		m = t.Topology.BS
 	case addr.SvcCS:
-		return t.Topology.CSNames
+		m = t.Topology.CS
 	case addr.SvcPS:
-		return t.Topology.PSNames
+		m = t.Topology.PS
 	case addr.SvcSIG:
-		return t.Topology.SIGNames
-	default:
-		return nil
+		m = t.Topology.SIG
 	}
-}
 
-func (t *topologyS) Overlay() overlay.Type {
-	return t.Topology.Overlay
+	var names ServiceNames
+	for k := range m {
+		names = append(names, k)
+	}
+	sort.Strings(names)
+	return names
 }
 
-func (t *topologyS) Raw() *topology.Topo {
+func (t *topologyS) Writable() *RWTopology {
 	return t.Topology
 }
 
diff --git a/go/lib/topology/json/BUILD.bazel b/go/lib/topology/json/BUILD.bazel
new file mode 100644
index 000000000..0cbfb2543
--- /dev/null
+++ b/go/lib/topology/json/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["json.go"],
+    importpath = "github.com/scionproto/scion/go/lib/topology/json",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/lib/common:go_default_library",
+        "//go/lib/serrors:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["json_test.go"],
+    data = glob(["testdata/**"]),
+    embed = [":go_default_library"],
+    deps = [
+        "//go/lib/common:go_default_library",
+        "//go/lib/topology/overlay:go_default_library",
+        "@com_github_stretchr_testify//assert:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
+    ],
+)
diff --git a/go/lib/topology/json/json.go b/go/lib/topology/json/json.go
new file mode 100644
index 000000000..d4d0c67d5
--- /dev/null
+++ b/go/lib/topology/json/json.go
@@ -0,0 +1,203 @@
+// Copyright 2019 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 json encodes AS topology information via JSON. All types exposed by this package are
+// designed to be directly marshaled to / unmarshaled from JSON.
+package json
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"strings"
+
+	"github.com/scionproto/scion/go/lib/common"
+	"github.com/scionproto/scion/go/lib/serrors"
+)
+
+// Topology is the JSON type for the entire AS topology file.
+type Topology struct {
+	Timestamp          int64                  `json:"Timestamp"`
+	TimestampHuman     string                 `json:"TimestampHuman"`
+	TTL                uint32                 `json:"TTL"`
+	IA                 string                 `json:"ISD_AS"`
+	Overlay            string                 `json:"Overlay"`
+	MTU                int                    `json:"MTU"`
+	Core               bool                   `json:"Core"`
+	BorderRouters      map[string]*BRInfo     `json:"BorderRouters,omitempty"`
+	ZookeeperService   map[int]*Address       `json:"ZookeeperService,omitempty"`
+	BeaconService      map[string]*ServerInfo `json:"BeaconService,omitempty"`
+	CertificateService map[string]*ServerInfo `json:"CertificateService,omitempty"`
+	PathService        map[string]*ServerInfo `json:"PathService,omitempty"`
+	SIG                map[string]*ServerInfo `json:"SIG,omitempty"`
+	DiscoveryService   map[string]*ServerInfo `json:"DiscoveryService,omitempty"`
+}
+
+// ServerInfo contains the information for a SCION application running in the local AS.
+type ServerInfo struct {
+	Addrs NATSCIONAddressMap `json:"Addrs"`
+}
+
+// NATSCIONAddressMap maps address types (e.g., "IPv4") to their values.
+type NATSCIONAddressMap map[string]*NATSCIONAddress
+
+// NATSCIONAddress contains the address information for a single server socket. It is possible for a
+// server to have both a Public SCION address (i.e., globally scoped) and a Bind SCION address
+// (i.e., configured on the local machine). For example, 192.0.2.1 might be the public address, and
+// 10.0.0.1 might the bind address for the same socket. Note that some higher level libraries might
+// not include support for bind addresses.
+type NATSCIONAddress struct {
+	Public FullSCIONAddress `json:"Public"`
+	Bind   *Address         `json:"Bind,omitempty"`
+}
+
+// FullSCIONAddress describes a server's public SCION address and the associated underlay address.
+// The address can specify a custom underlay port for this socket (note that the JSON serialization
+// uses the old "overlay" term here), although usually this will be the AS default underlay port.
+type FullSCIONAddress struct {
+	Address
+	UnderlayPort int `json:"OverlayPort,omitempty"`
+}
+
+// Address is a standard layer 3 + layer 4 address.
+type Address struct {
+	Addr   string `json:"Addr"`
+	L4Port int    `json:"L4Port"`
+}
+
+// BRInfo contains Border Router specific information.
+type BRInfo struct {
+	InternalAddrs UnderlayAddressMap               `json:"InternalAddrs"`
+	CtrlAddr      NATSCIONAddressMap               `json:"CtrlAddr"`
+	Interfaces    map[common.IFIDType]*BRInterface `json:"Interfaces"`
+}
+
+// UnderlayAddressMap maps address types (e.g., "UDP/IPv4") to underlay address values.
+type UnderlayAddressMap map[string]*NATUnderlayAddress
+
+// NATUnderlayAddress contains the information for a single underlay (data-plane) socket. It is
+// possible for an application to have both a Public Underlay address (i.e., globally scoped) and a
+// Bind Underlay address (i.e., configured on the local machine). For example, 192.0.2.1 might be
+// the public underlay address, and 10.0.0.1 might be the bind underlay address for the same socket.
+type NATUnderlayAddress struct {
+	PublicUnderlay UnderlayAddress `json:"PublicOverlay"`
+	BindUnderlay   *L3Address      `json:"BindOverlay,omitempty"`
+}
+
+// BRInterface contains the information for an data-plane BR socket that is external (i.e., facing
+// the neighboring AS).
+type BRInterface struct {
+	Underlay       string           `json:"Overlay,omitempty"`
+	PublicUnderlay *UnderlayAddress `json:"PublicOverlay,omitempty"`
+	BindUnderlay   *L3Address       `json:"BindOverlay,omitempty"`
+	RemoteUnderlay *UnderlayAddress `json:"RemoteOverlay,omitempty"`
+	Bandwidth      int              `json:"Bandwidth"`
+	IA             string           `json:"ISD_AS"`
+	LinkTo         string           `json:"LinkTo"`
+	MTU            int              `json:"MTU"`
+}
+
+// UnderlayAddress is a standard layer 3 + layer 4 address. This is identical to the other basic
+// address type in this package, except the JSON property names are different.
+type UnderlayAddress struct {
+	Addr         string `json:"Addr"`
+	UnderlayPort int    `json:"OverlayPort,omitempty"`
+}
+
+// L3Address is a standard layer 3 address.
+type L3Address struct {
+	Addr string `json:"Addr"`
+}
+
+func (i ServerInfo) String() string {
+	return fmt.Sprintf("Addr: %s", i.Addrs)
+}
+
+func (m NATSCIONAddressMap) String() string {
+	var s []string
+	for k, v := range m {
+		s = append(s, fmt.Sprintf("%s: %s", k, v))
+	}
+	return strings.Join(s, "\n")
+}
+
+func (a NATSCIONAddress) String() string {
+	var s []string
+	s = append(s, fmt.Sprintf("Public: %s", a.Public))
+	if a.Bind != nil {
+		s = append(s, fmt.Sprintf("Bind: %s", a.Bind))
+	}
+	return strings.Join(s, ", ")
+}
+
+func (a FullSCIONAddress) String() string {
+	return fmt.Sprintf("%s:%d/%d", a.Addr, a.L4Port, a.UnderlayPort)
+}
+
+func (a Address) String() string {
+	return fmt.Sprintf("%s:%d", a.Addr, a.L4Port)
+}
+
+func (i BRInfo) String() string {
+	var s []string
+	s = append(s, fmt.Sprintf("Loc addrs:\n  %s\nControl addrs:\n  %s\nInterfaces:",
+		i.InternalAddrs, i.CtrlAddr))
+	for ifid, intf := range i.Interfaces {
+		s = append(s, fmt.Sprintf("%d: %+v", ifid, intf))
+	}
+	return strings.Join(s, "\n")
+}
+
+func (m UnderlayAddressMap) String() string {
+	var s []string
+	for k, v := range m {
+		s = append(s, fmt.Sprintf("%s: %s", k, v))
+	}
+	return strings.Join(s, "\n")
+}
+
+func (a NATUnderlayAddress) String() string {
+	var s []string
+	s = append(s, fmt.Sprintf("PublicOverlay: %s", a.PublicUnderlay))
+	if a.BindUnderlay != nil {
+		s = append(s, fmt.Sprintf("BindOverlay: %s", a.BindUnderlay))
+	}
+	return strings.Join(s, "\n")
+}
+
+func (a UnderlayAddress) String() string {
+	return fmt.Sprintf("%s:%d", a.Addr, a.UnderlayPort)
+}
+
+func (a L3Address) String() string {
+	return fmt.Sprintf("%s", a.Addr)
+}
+
+// Load parses a topology from its raw byte representation.
+func Load(b common.RawBytes) (*Topology, error) {
+	rt := &Topology{}
+	if err := json.Unmarshal(b, rt); err != nil {
+		return nil, serrors.WrapStr("unable to parse topology from JSON", err)
+	}
+	return rt, nil
+}
+
+// LoadFromFile parses a topology from a file.
+func LoadFromFile(path string) (*Topology, error) {
+	b, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, serrors.WrapStr("unable to open topology", err, "path", path)
+	}
+	return Load(b)
+}
diff --git a/go/lib/topology/linktype.go b/go/lib/topology/linktype.go
new file mode 100644
index 000000000..fa5d7d399
--- /dev/null
+++ b/go/lib/topology/linktype.go
@@ -0,0 +1,51 @@
+// Copyright 2017 ETH Zurich
+// Copyright 2019 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 topology
+
+import (
+	"strings"
+
+	"github.com/scionproto/scion/go/proto"
+)
+
+// LinkType describes inter-AS links.
+type LinkType int
+
+// XXX(scrye): These constants might end up in files or on the network, so they should not
+// change. They are defined here s.t. they are not subject to the capnp generator.
+
+const (
+	// Unset is used for unknown link types.
+	Unset LinkType = 0
+	// Core links connect core ASes.
+	Core LinkType = 1
+	// Parent links are configured on non-core links pointing towards the core of an ISD.
+	Parent LinkType = 2
+	// Child links are configured on non-core links pointing away from the core of an ISD.
+	Child LinkType = 3
+	// Peer links are configured for peering relationships.
+	Peer LinkType = 4
+)
+
+func (lt LinkType) String() string {
+	return proto.LinkType(lt).String()
+}
+
+// LinkTypeFromString returns the numerical link type associated with a string description. If the
+// string is not recognized, an Unset link type is returned. The matching is case-insensitive.
+func LinkTypeFromString(s string) LinkType {
+	return LinkType(proto.LinkTypeFromString(strings.ToLower(s)))
+}
diff --git a/go/lib/topology/mock_topology/BUILD.bazel b/go/lib/topology/mock_topology/BUILD.bazel
new file mode 100644
index 000000000..deb1f6618
--- /dev/null
+++ b/go/lib/topology/mock_topology/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["topology.go"],
+    importpath = "github.com/scionproto/scion/go/lib/topology/mock_topology",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/lib/addr:go_default_library",
+        "//go/lib/common:go_default_library",
+        "//go/lib/snet:go_default_library",
+        "//go/lib/topology:go_default_library",
+        "//go/proto:go_default_library",
+        "@com_github_golang_mock//gomock:go_default_library",
+    ],
+)
diff --git a/go/lib/topology/mock_topology/topology.go b/go/lib/topology/mock_topology/topology.go
new file mode 100644
index 000000000..9bca484d1
--- /dev/null
+++ b/go/lib/topology/mock_topology/topology.go
@@ -0,0 +1,311 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/scionproto/scion/go/lib/topology (interfaces: Topology)
+
+// Package mock_topology is a generated GoMock package.
+package mock_topology
+
+import (
+	gomock "github.com/golang/mock/gomock"
+	addr "github.com/scionproto/scion/go/lib/addr"
+	common "github.com/scionproto/scion/go/lib/common"
+	snet "github.com/scionproto/scion/go/lib/snet"
+	topology "github.com/scionproto/scion/go/lib/topology"
+	proto "github.com/scionproto/scion/go/proto"
+	net "net"
+	reflect "reflect"
+)
+
+// MockTopology is a mock of Topology interface
+type MockTopology struct {
+	ctrl     *gomock.Controller
+	recorder *MockTopologyMockRecorder
+}
+
+// MockTopologyMockRecorder is the mock recorder for MockTopology
+type MockTopologyMockRecorder struct {
+	mock *MockTopology
+}
+
+// NewMockTopology creates a new mock instance
+func NewMockTopology(ctrl *gomock.Controller) *MockTopology {
+	mock := &MockTopology{ctrl: ctrl}
+	mock.recorder = &MockTopologyMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockTopology) EXPECT() *MockTopologyMockRecorder {
+	return m.recorder
+}
+
+// BR mocks base method
+func (m *MockTopology) BR(arg0 string) (topology.BRInfo, bool) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BR", arg0)
+	ret0, _ := ret[0].(topology.BRInfo)
+	ret1, _ := ret[1].(bool)
+	return ret0, ret1
+}
+
+// BR indicates an expected call of BR
+func (mr *MockTopologyMockRecorder) BR(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BR", reflect.TypeOf((*MockTopology)(nil).BR), arg0)
+}
+
+// BRNames mocks base method
+func (m *MockTopology) BRNames() []string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BRNames")
+	ret0, _ := ret[0].([]string)
+	return ret0
+}
+
+// BRNames indicates an expected call of BRNames
+func (mr *MockTopologyMockRecorder) BRNames() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BRNames", reflect.TypeOf((*MockTopology)(nil).BRNames))
+}
+
+// Core mocks base method
+func (m *MockTopology) Core() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Core")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// Core indicates an expected call of Core
+func (mr *MockTopologyMockRecorder) Core() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Core", reflect.TypeOf((*MockTopology)(nil).Core))
+}
+
+// DS mocks base method
+func (m *MockTopology) DS() topology.IDAddrMap {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DS")
+	ret0, _ := ret[0].(topology.IDAddrMap)
+	return ret0
+}
+
+// DS indicates an expected call of DS
+func (mr *MockTopologyMockRecorder) DS() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DS", reflect.TypeOf((*MockTopology)(nil).DS))
+}
+
+// Exists mocks base method
+func (m *MockTopology) Exists(arg0 addr.HostSVC, arg1 string) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Exists", arg0, arg1)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// Exists indicates an expected call of Exists
+func (mr *MockTopologyMockRecorder) Exists(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockTopology)(nil).Exists), arg0, arg1)
+}
+
+// IA mocks base method
+func (m *MockTopology) IA() addr.IA {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "IA")
+	ret0, _ := ret[0].(addr.IA)
+	return ret0
+}
+
+// IA indicates an expected call of IA
+func (mr *MockTopologyMockRecorder) IA() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IA", reflect.TypeOf((*MockTopology)(nil).IA))
+}
+
+// IFInfoMap mocks base method
+func (m *MockTopology) IFInfoMap() topology.IfInfoMap {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "IFInfoMap")
+	ret0, _ := ret[0].(topology.IfInfoMap)
+	return ret0
+}
+
+// IFInfoMap indicates an expected call of IFInfoMap
+func (mr *MockTopologyMockRecorder) IFInfoMap() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IFInfoMap", reflect.TypeOf((*MockTopology)(nil).IFInfoMap))
+}
+
+// InterfaceIDs mocks base method
+func (m *MockTopology) InterfaceIDs() []common.IFIDType {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InterfaceIDs")
+	ret0, _ := ret[0].([]common.IFIDType)
+	return ret0
+}
+
+// InterfaceIDs indicates an expected call of InterfaceIDs
+func (mr *MockTopologyMockRecorder) InterfaceIDs() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterfaceIDs", reflect.TypeOf((*MockTopology)(nil).InterfaceIDs))
+}
+
+// MTU mocks base method
+func (m *MockTopology) MTU() uint16 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "MTU")
+	ret0, _ := ret[0].(uint16)
+	return ret0
+}
+
+// MTU indicates an expected call of MTU
+func (mr *MockTopologyMockRecorder) MTU() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MTU", reflect.TypeOf((*MockTopology)(nil).MTU))
+}
+
+// MakeHostInfos mocks base method
+func (m *MockTopology) MakeHostInfos(arg0 proto.ServiceType) []net.UDPAddr {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "MakeHostInfos", arg0)
+	ret0, _ := ret[0].([]net.UDPAddr)
+	return ret0
+}
+
+// MakeHostInfos indicates an expected call of MakeHostInfos
+func (mr *MockTopologyMockRecorder) MakeHostInfos(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeHostInfos", reflect.TypeOf((*MockTopology)(nil).MakeHostInfos), arg0)
+}
+
+// OverlayAnycast mocks base method
+func (m *MockTopology) OverlayAnycast(arg0 addr.HostSVC) (*net.UDPAddr, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OverlayAnycast", arg0)
+	ret0, _ := ret[0].(*net.UDPAddr)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// OverlayAnycast indicates an expected call of OverlayAnycast
+func (mr *MockTopologyMockRecorder) OverlayAnycast(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OverlayAnycast", reflect.TypeOf((*MockTopology)(nil).OverlayAnycast), arg0)
+}
+
+// OverlayByName mocks base method
+func (m *MockTopology) OverlayByName(arg0 addr.HostSVC, arg1 string) (*net.UDPAddr, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OverlayByName", arg0, arg1)
+	ret0, _ := ret[0].(*net.UDPAddr)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// OverlayByName indicates an expected call of OverlayByName
+func (mr *MockTopologyMockRecorder) OverlayByName(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OverlayByName", reflect.TypeOf((*MockTopology)(nil).OverlayByName), arg0, arg1)
+}
+
+// OverlayMulticast mocks base method
+func (m *MockTopology) OverlayMulticast(arg0 addr.HostSVC) ([]*net.UDPAddr, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OverlayMulticast", arg0)
+	ret0, _ := ret[0].([]*net.UDPAddr)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// OverlayMulticast indicates an expected call of OverlayMulticast
+func (mr *MockTopologyMockRecorder) OverlayMulticast(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OverlayMulticast", reflect.TypeOf((*MockTopology)(nil).OverlayMulticast), arg0)
+}
+
+// OverlayNextHop mocks base method
+func (m *MockTopology) OverlayNextHop(arg0 common.IFIDType) (*net.UDPAddr, bool) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OverlayNextHop", arg0)
+	ret0, _ := ret[0].(*net.UDPAddr)
+	ret1, _ := ret[1].(bool)
+	return ret0, ret1
+}
+
+// OverlayNextHop indicates an expected call of OverlayNextHop
+func (mr *MockTopologyMockRecorder) OverlayNextHop(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OverlayNextHop", reflect.TypeOf((*MockTopology)(nil).OverlayNextHop), arg0)
+}
+
+// OverlayNextHop2 mocks base method
+func (m *MockTopology) OverlayNextHop2(arg0 common.IFIDType) (*net.UDPAddr, bool) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OverlayNextHop2", arg0)
+	ret0, _ := ret[0].(*net.UDPAddr)
+	ret1, _ := ret[1].(bool)
+	return ret0, ret1
+}
+
+// OverlayNextHop2 indicates an expected call of OverlayNextHop2
+func (mr *MockTopologyMockRecorder) OverlayNextHop2(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OverlayNextHop2", reflect.TypeOf((*MockTopology)(nil).OverlayNextHop2), arg0)
+}
+
+// PublicAddress mocks base method
+func (m *MockTopology) PublicAddress(arg0 addr.HostSVC, arg1 string) *net.UDPAddr {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PublicAddress", arg0, arg1)
+	ret0, _ := ret[0].(*net.UDPAddr)
+	return ret0
+}
+
+// PublicAddress indicates an expected call of PublicAddress
+func (mr *MockTopologyMockRecorder) PublicAddress(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublicAddress", reflect.TypeOf((*MockTopology)(nil).PublicAddress), arg0, arg1)
+}
+
+// SBRAddress mocks base method
+func (m *MockTopology) SBRAddress(arg0 string) *snet.Addr {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SBRAddress", arg0)
+	ret0, _ := ret[0].(*snet.Addr)
+	return ret0
+}
+
+// SBRAddress indicates an expected call of SBRAddress
+func (mr *MockTopologyMockRecorder) SBRAddress(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SBRAddress", reflect.TypeOf((*MockTopology)(nil).SBRAddress), arg0)
+}
+
+// SVCNames mocks base method
+func (m *MockTopology) SVCNames(arg0 addr.HostSVC) topology.ServiceNames {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SVCNames", arg0)
+	ret0, _ := ret[0].(topology.ServiceNames)
+	return ret0
+}
+
+// SVCNames indicates an expected call of SVCNames
+func (mr *MockTopologyMockRecorder) SVCNames(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SVCNames", reflect.TypeOf((*MockTopology)(nil).SVCNames), arg0)
+}
+
+// Writable mocks base method
+func (m *MockTopology) Writable() *topology.RWTopology {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Writable")
+	ret0, _ := ret[0].(*topology.RWTopology)
+	return ret0
+}
+
+// Writable indicates an expected call of Writable
+func (mr *MockTopologyMockRecorder) Writable() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Writable", reflect.TypeOf((*MockTopology)(nil).Writable))
+}
diff --git a/go/lib/topology/raw.go b/go/lib/topology/raw.go
index 15b49ebfb..ba2057441 100644
--- a/go/lib/topology/raw.go
+++ b/go/lib/topology/raw.go
@@ -16,93 +16,50 @@
 package topology
 
 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"
+	jsontopo "github.com/scionproto/scion/go/lib/topology/json"
 	"github.com/scionproto/scion/go/lib/topology/overlay"
 )
 
-const CfgName = "topology.json"
-
-const (
-	ErrOpen    common.ErrMsg = "Unable to open topology"
-	ErrParse   common.ErrMsg = "Unable to parse topology from JSON"
-	ErrConvert common.ErrMsg = "Unable to convert RawTopo to Topo"
-)
-
-// Structures directly filled from JSON
-
-// RawTopo is used to un/marshal from/to JSON and should usually not be used by
-// Go code directly. Use Topo (from lib/topology/topology.go) instead.
-type RawTopo struct {
-	Timestamp          int64
-	TimestampHuman     string
-	TTL                uint32
-	ISD_AS             string
-	Overlay            string
-	MTU                int
-	Core               bool
-	BorderRouters      map[string]*RawBRInfo  `json:",omitempty"`
-	BeaconService      map[string]*RawSrvInfo `json:",omitempty"`
-	CertificateService map[string]*RawSrvInfo `json:",omitempty"`
-	PathService        map[string]*RawSrvInfo `json:",omitempty"`
-	SibraService       map[string]*RawSrvInfo `json:",omitempty"`
-	RainsService       map[string]*RawSrvInfo `json:",omitempty"`
-	SIG                map[string]*RawSrvInfo `json:",omitempty"`
-	DiscoveryService   map[string]*RawSrvInfo `json:",omitempty"`
-}
-
-type RawSrvInfo struct {
-	Addrs RawAddrMap
-}
-
-func (ras RawSrvInfo) String() string {
-	return fmt.Sprintf("Addr: %s", ras.Addrs)
-}
-
-type RawAddrMap map[string]*RawPubBindOverlay
-
-func (ram RawAddrMap) ToTopoAddr() (*TopoAddr, error) {
-	addressInfo, mustBeIPv6 := ram.extractAddressInfo()
+func rawAddrMapToTopoAddr(ram jsontopo.NATSCIONAddressMap) (*TopoAddr, error) {
+	addressInfo, mustBeIPv6 := rawAddrMapExtractAddressInfo(ram)
 	mustBeIPv4 := !mustBeIPv6
 	if addressInfo == nil {
-		return nil, serrors.WithCtx(ErrAtLeastOnePub, "address", ram)
+		return nil, serrors.WithCtx(errAtLeastOnePub, "address", ram)
 	}
 	if addressInfo.Bind != nil {
-		return nil, serrors.WithCtx(ErrBindNotSupported, "address", addressInfo.Bind)
+		return nil, serrors.WithCtx(errBindNotSupported, "address", addressInfo.Bind)
 	}
-	if addressInfo.Public.OverlayPort != 0 {
-		return nil, serrors.WithCtx(ErrCustomUnderlayPortNotSupported,
-			"port", addressInfo.Public.OverlayPort)
+	if addressInfo.Public.UnderlayPort != 0 {
+		return nil, serrors.WithCtx(errCustomUnderlayPort,
+			"port", addressInfo.Public.UnderlayPort)
 	}
 
-	ip := net.ParseIP(addressInfo.Public.RawAddrPort.Addr)
+	ip := net.ParseIP(addressInfo.Public.Address.Addr)
 	if ip == nil {
-		return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.Public.RawAddrPort.Addr)
+		return nil, serrors.WithCtx(errInvalidPub, "address", addressInfo.Public.Address.Addr)
 	}
 
 	if mustBeIPv6 && ip.To4() != nil {
-		return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.Public.RawAddrPort.Addr)
+		return nil, serrors.WithCtx(errInvalidPub, "address", addressInfo.Public.Address.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 nil, serrors.WithCtx(errInvalidPub,
+				"address", addressInfo.Public.Address.Addr)
 		}
 	}
 
 	return &TopoAddr{
 		SCIONAddress: &addr.AppAddr{
 			L3: addr.HostFromIP(ip),
-			L4: uint16(addressInfo.Public.RawAddrPort.L4Port),
+			L4: uint16(addressInfo.Public.Address.L4Port),
 		},
 		UnderlayAddress: &net.UDPAddr{
 			IP:   append(ip[:0:0], ip...),
@@ -111,7 +68,9 @@ func (ram RawAddrMap) ToTopoAddr() (*TopoAddr, error) {
 	}, nil
 }
 
-func (ram RawAddrMap) extractAddressInfo() (info *RawPubBindOverlay, is6 bool) {
+func rawAddrMapExtractAddressInfo(
+	ram jsontopo.NATSCIONAddressMap) (info *jsontopo.NATSCIONAddress, is6 bool) {
+
 	a, ok := ram["IPv6"]
 	if ok {
 		return a, true
@@ -123,100 +82,41 @@ func (ram RawAddrMap) extractAddressInfo() (info *RawPubBindOverlay, is6 bool) {
 	return nil, false
 }
 
-func (ram RawAddrMap) String() string {
-	var s []string
-	for k, v := range ram {
-		s = append(s, fmt.Sprintf("%s: %s", k, v))
-	}
-	return strings.Join(s, "\n")
-}
-
-type RawPubBindOverlay struct {
-	Public RawAddrPortOverlay
-	Bind   *RawAddrPort `json:",omitempty"`
-}
-
-func (rpbo RawPubBindOverlay) String() string {
-	var s []string
-	s = append(s, fmt.Sprintf("Public: %s", rpbo.Public))
-	if rpbo.Bind != nil {
-		s = append(s, fmt.Sprintf("Bind: %s", rpbo.Bind))
-	}
-	return strings.Join(s, ", ")
-}
-
-// Since Public addresses may be associated with an Overlay port, extend the
-// structure used for Bind addresses.
-type RawAddrPortOverlay struct {
-	RawAddrPort
-	OverlayPort int `json:",omitempty"`
-}
-
-func (a RawAddrPortOverlay) String() string {
-	return fmt.Sprintf("%s:%d/%d", a.Addr, a.L4Port, a.OverlayPort)
-}
-
-type RawAddrPort struct {
-	Addr   string
-	L4Port int
-}
-
-func (a RawAddrPort) String() string {
-	return fmt.Sprintf("%s:%d", a.Addr, a.L4Port)
-}
-
-// RawBRInfo contains Border Router specific information.
-type RawBRInfo struct {
-	InternalAddrs RawBRAddrMap
-	CtrlAddr      RawAddrMap
-	Interfaces    map[common.IFIDType]*RawBRIntf
-}
-
-func (b RawBRInfo) String() string {
-	var s []string
-	s = append(s, fmt.Sprintf("Loc addrs:\n  %s\nControl addrs:\n  %s\nInterfaces:",
-		b.InternalAddrs, b.CtrlAddr))
-	for ifid, intf := range b.Interfaces {
-		s = append(s, fmt.Sprintf("%d: %+v", ifid, intf))
-	}
-	return strings.Join(s, "\n")
-}
-
-type RawBRAddrMap map[string]*RawOverlayBind
-
-func (m RawBRAddrMap) ToUDPAddr() (*net.UDPAddr, error) {
-	addressInfo, mustBeIPv6 := m.extractAddressInfo()
+func rawBRAddrMapToUDPAddr(m jsontopo.UnderlayAddressMap) (*net.UDPAddr, error) {
+	addressInfo, mustBeIPv6 := rawBRAddrMapExtractAddressInfo(m)
 	mustBeIPv4 := !mustBeIPv6
 	if addressInfo == nil {
-		return nil, serrors.WithCtx(ErrAtLeastOnePub, "address", m)
+		return nil, serrors.WithCtx(errAtLeastOnePub, "address", m)
 	}
-	if addressInfo.BindOverlay != nil {
-		return nil, serrors.WithCtx(ErrBindNotSupported, "address", addressInfo.BindOverlay)
+	if addressInfo.BindUnderlay != nil {
+		return nil, serrors.WithCtx(errBindNotSupported, "address", addressInfo.BindUnderlay)
 	}
 
-	ip := net.ParseIP(addressInfo.PublicOverlay.Addr)
+	ip := net.ParseIP(addressInfo.PublicUnderlay.Addr)
 	if ip == nil {
-		return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.PublicOverlay.Addr)
+		return nil, serrors.WithCtx(errInvalidPub, "address", addressInfo.PublicUnderlay.Addr)
 	}
 
 	if mustBeIPv6 && ip.To4() != nil {
-		return nil, serrors.WithCtx(ErrInvalidPub, "address", addressInfo.PublicOverlay.Addr)
+		return nil, serrors.WithCtx(errInvalidPub, "address", addressInfo.PublicUnderlay.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 nil, serrors.WithCtx(errInvalidPub, "address", addressInfo.PublicUnderlay.Addr)
 		}
 	}
 
 	return &net.UDPAddr{
 		IP:   append(ip[:0:0], ip...),
-		Port: addressInfo.PublicOverlay.OverlayPort,
+		Port: addressInfo.PublicUnderlay.UnderlayPort,
 	}, nil
 }
 
-func (m RawBRAddrMap) extractAddressInfo() (info *RawOverlayBind, is6 bool) {
+func rawBRAddrMapExtractAddressInfo(
+	m jsontopo.UnderlayAddressMap) (info *jsontopo.NATUnderlayAddress, is6 bool) {
+
 	a, ok := m["IPv6"]
 	if ok {
 		return a, true
@@ -228,137 +128,50 @@ func (m RawBRAddrMap) extractAddressInfo() (info *RawOverlayBind, is6 bool) {
 	return nil, false
 }
 
-func (roa RawBRAddrMap) String() string {
-	var s []string
-	for k, v := range roa {
-		s = append(s, fmt.Sprintf("%s: %s", k, v))
-	}
-	return strings.Join(s, "\n")
-}
-
-type RawOverlayBind struct {
-	PublicOverlay RawAddrOverlay
-	BindOverlay   *RawAddr `json:",omitempty"`
-}
-
-func (b RawOverlayBind) String() string {
-	var s []string
-	s = append(s, fmt.Sprintf("PublicOverlay: %s", b.PublicOverlay))
-	if b.BindOverlay != nil {
-		s = append(s, fmt.Sprintf("BindOverlay: %s", b.BindOverlay))
-	}
-	return strings.Join(s, "\n")
-}
-
-type RawBRIntf struct {
-	Overlay       string          `json:",omitempty"`
-	PublicOverlay *RawAddrOverlay `json:",omitempty"`
-	BindOverlay   *RawAddr        `json:",omitempty"`
-	RemoteOverlay *RawAddrOverlay `json:",omitempty"`
-	Bandwidth     int
-	ISD_AS        string
-	LinkTo        string
-	MTU           int
-}
-
-// make an OverlayAddr object from a BR interface Remote entry
-func (b RawBRIntf) remoteBRAddr(o overlay.Type) (*net.UDPAddr, error) {
-	l3 := addr.HostFromIPStr(b.RemoteOverlay.Addr)
+func rawBRIntfRemoteBRAddr(b *jsontopo.BRInterface, o overlay.Type) (*net.UDPAddr, error) {
+	l3 := addr.HostFromIPStr(b.RemoteUnderlay.Addr)
 	if l3 == nil {
 		return nil, common.NewBasicError("Could not parse remote IP from string", nil,
-			"ip", b.RemoteOverlay.Addr)
+			"ip", b.RemoteUnderlay.Addr)
 	}
-	if !o.IsUDP() && (b.RemoteOverlay.OverlayPort != 0) {
-		return nil, serrors.WithCtx(ErrUnderlayPort, "addr", b.RemoteOverlay)
+	if !o.IsUDP() && (b.RemoteUnderlay.UnderlayPort != 0) {
+		return nil, serrors.WithCtx(errUnderlayPort, "addr", b.RemoteUnderlay)
 	}
-	return &net.UDPAddr{IP: l3.IP(), Port: b.RemoteOverlay.OverlayPort}, nil
+	return &net.UDPAddr{IP: l3.IP(), Port: b.RemoteUnderlay.UnderlayPort}, 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)
+func rawBRIntfTopoBRAddr(i *jsontopo.BRInterface) (*net.UDPAddr, error) {
+	if i.Underlay != "UDP/IPv4" && i.Underlay != "UDP/IPv6" {
+		return nil, serrors.WithCtx(errUnsupportedUnderlay, "underlay", i.Underlay)
 	}
 
-	mustBeIPv4 := i.Overlay == "UDP/IPv4"
-	mustBeIPv6 := i.Overlay == "UDP/IPv6"
+	mustBeIPv4 := i.Underlay == "UDP/IPv4"
+	mustBeIPv6 := i.Underlay == "UDP/IPv6"
 
 	var udpAddr net.UDPAddr
-	if i.BindOverlay != nil {
-		udpAddr.IP = net.ParseIP(i.BindOverlay.Addr)
+	if i.BindUnderlay != nil {
+		udpAddr.IP = net.ParseIP(i.BindUnderlay.Addr)
 	} else {
-		if i.PublicOverlay == nil {
-			return nil, serrors.WithCtx(ErrUnderlayAddressNotFound, "underlay", i.Overlay)
+		if i.PublicUnderlay == nil {
+			return nil, serrors.WithCtx(errUnderlayAddrNotFound, "underlay", i.Underlay)
 		}
-		udpAddr.IP = net.ParseIP(i.PublicOverlay.Addr)
+		udpAddr.IP = net.ParseIP(i.PublicUnderlay.Addr)
 	}
-	if i.PublicOverlay != nil {
-		udpAddr.Port = i.PublicOverlay.OverlayPort
+	if i.PublicUnderlay != nil {
+		udpAddr.Port = i.PublicUnderlay.UnderlayPort
 	}
 
 	if udpAddr.IP == nil {
-		return nil, serrors.WithCtx(ErrInvalidPub, "address", i.PublicOverlay.Addr)
+		return nil, serrors.WithCtx(errInvalidPub, "address", i.PublicUnderlay.Addr)
 	}
 	if mustBeIPv4 {
 		udpAddr.IP = udpAddr.IP.To4()
 		if udpAddr.IP == nil {
-			return nil, serrors.WithCtx(ErrExpectedIPv4FoundIPv6, "address", i.PublicOverlay.Addr)
+			return nil, serrors.WithCtx(errExpectedIPv4FoundIPv6, "address", i.PublicUnderlay.Addr)
 		}
 	}
 	if mustBeIPv6 && udpAddr.IP.To4() != nil {
-		return nil, serrors.WithCtx(ErrExpectedIPv6FoundIPv4, "address", i.PublicOverlay.Addr)
+		return nil, serrors.WithCtx(errExpectedIPv6FoundIPv4, "address", i.PublicUnderlay.Addr)
 	}
 	return &udpAddr, nil
 }
-
-type RawAddrOverlay struct {
-	Addr        string
-	OverlayPort int `json:",omitempty"`
-}
-
-func (a RawAddrOverlay) String() string {
-	return fmt.Sprintf("%s:%d", a.Addr, a.OverlayPort)
-}
-
-type RawAddr struct {
-	Addr string
-}
-
-func (a RawAddr) String() string {
-	return fmt.Sprintf("%s", a.Addr)
-}
-
-func Load(b common.RawBytes) (*Topo, error) {
-	rt := &RawTopo{}
-	if err := json.Unmarshal(b, rt); err != nil {
-		return nil, common.NewBasicError(ErrParse, err)
-	}
-	ct, err := TopoFromRaw(rt)
-	if err != nil {
-		return nil, common.NewBasicError(ErrConvert, err)
-	}
-	return ct, nil
-}
-
-func LoadFromFile(path string) (*Topo, error) {
-	b, err := ioutil.ReadFile(path)
-	if err != nil {
-		return nil, common.NewBasicError(ErrOpen, err, "path", path)
-	}
-	return Load(b)
-}
-
-func LoadRaw(b common.RawBytes) (*RawTopo, error) {
-	rt := &RawTopo{}
-	if err := json.Unmarshal(b, rt); err != nil {
-		return nil, common.NewBasicError(ErrParse, err)
-	}
-	return rt, nil
-}
-
-func LoadRawFromFile(path string) (*RawTopo, error) {
-	b, err := ioutil.ReadFile(path)
-	if err != nil {
-		return nil, common.NewBasicError(ErrOpen, err, "path", path)
-	}
-	return LoadRaw(b)
-}
diff --git a/go/lib/topology/strip.go b/go/lib/topology/strip.go
deleted file mode 100644
index d56300fff..000000000
--- a/go/lib/topology/strip.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright 2017 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
-
-func StripBind(rt *RawTopo) {
-	// These services may have Bind sections that we need to remove
-	removeSrvBind(rt.CertificateService)
-	removeSrvBind(rt.PathService)
-	removeSrvBind(rt.RainsService)
-	removeSrvBind(rt.DiscoveryService)
-
-	// Border Routers have Bind sections plus other link-specific information that we want to trim
-	removeBRBind(rt.BorderRouters)
-}
-
-func StripServices(rt *RawTopo) {
-	// Clear services that don't need to be publicly visible
-	rt.BeaconService = make(map[string]*RawSrvInfo)
-	rt.SibraService = make(map[string]*RawSrvInfo)
-}
-
-func removeSrvBind(svc map[string]*RawSrvInfo) {
-	for _, s := range svc {
-		removeRAMBind(s.Addrs)
-	}
-}
-
-func removeBRBind(brs map[string]*RawBRInfo) {
-	for _, bri := range brs {
-		removeRBRAMBind(bri.InternalAddrs)
-		removeRAMBind(bri.CtrlAddr)
-		for i := range bri.Interfaces {
-			// The nil elements are of no interest to the public
-			bri.Interfaces[i].Overlay = ""
-			bri.Interfaces[i].BindOverlay = nil
-			bri.Interfaces[i].PublicOverlay = nil
-			bri.Interfaces[i].RemoteOverlay = nil
-		}
-	}
-}
-
-func removeRAMBind(ram RawAddrMap) {
-	for _, v := range ram {
-		v.Bind = nil
-	}
-}
-
-func removeRBRAMBind(rbram RawBRAddrMap) {
-	for _, v := range rbram {
-		v.BindOverlay = nil
-	}
-}
diff --git a/go/lib/topology/topology.go b/go/lib/topology/topology.go
index 79e3e2291..c8326e348 100644
--- a/go/lib/topology/topology.go
+++ b/go/lib/topology/topology.go
@@ -16,7 +16,9 @@
 package topology
 
 import (
+	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"math/rand"
 	"net"
 	"sort"
@@ -25,6 +27,7 @@ import (
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/serrors"
+	jsontopo "github.com/scionproto/scion/go/lib/topology/json"
 	"github.com/scionproto/scion/go/lib/topology/overlay"
 	"github.com/scionproto/scion/go/proto"
 )
@@ -32,82 +35,96 @@ import (
 // EndhostPort is the overlay port that the dispatcher binds to on non-routers.
 const EndhostPort = overlay.EndhostPort
 
-type IDAddrMap map[string]TopoAddr
-
-// GetById returns the TopoAddr for the given ID, or nil if there is none.
-func (m IDAddrMap) GetById(id string) *TopoAddr {
-	if _, ok := m[id]; ok {
-		cp := m[id]
-		return &cp
+type (
+	// RWTopology is the topology type for applications and libraries that need write
+	// access to AS topology information (e.g., discovery, topology reloaders).
+	//
+	// The first section contains metadata about the topology. All of these fields
+	// should be self-explanatory. The unit of TTL is seconds, with the zero value
+	// indicating an infinite TTL.
+	//
+	// The second section concerns the Border routers. BRNames is just a sorted slice
+	// of the names of the BRs in this topolgy. Its contents is exactly the same as
+	// the keys in the BR map.
+	//
+	// The BR map points from border router names to BRInfo structs, which in turn
+	// are lists of IFID type slices, thus defines the IFIDs that belong to a
+	// particular border router. The IFInfoMap points from interface IDs to IFInfo structs.
+	//
+	// The third section in RWTopology concerns the SCION-specific services in the topology.
+	// The structure is identical between the various elements. For each service,
+	// 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.
+	RWTopology struct {
+		Timestamp time.Time
+		TTL       time.Duration
+		IA        addr.IA
+		Core      bool
+		MTU       int
+
+		BR        map[string]BRInfo
+		BRNames   []string
+		IFInfoMap IfInfoMap
+
+		BS  IDAddrMap
+		CS  IDAddrMap
+		PS  IDAddrMap
+		DS  IDAddrMap
+		SIG IDAddrMap
 	}
-	return nil
-}
 
-type ServiceNames []string
+	// BRInfo is a list of AS-wide unique interface IDs for a router. These IDs are also used
+	// to point to the specific internal address clients should send their traffic
+	// to in order to use that interface, via the IFInfoMap member of the Topo
+	// struct.
+	BRInfo struct {
+		Name string
+		// CtrlAddrs are the local control-plane addresses.
+		CtrlAddrs *TopoAddr
+		// InternalAddr is the local data-plane address.
+		InternalAddr *net.UDPAddr
+		// IFIDs is a sorted list of the interface IDs.
+		IFIDs []common.IFIDType
+		// IFs is a map of interface IDs.
+		IFs map[common.IFIDType]*IFInfo
+	}
 
-// GetRandom returns a random entry, or an error if the slice is empty.
-func (s ServiceNames) GetRandom() (string, error) {
-	numServers := len(s)
-	if numServers == 0 {
-		return "", serrors.New("No names present")
+	// IfInfoMap maps interface ids to the interface information.
+	IfInfoMap map[common.IFIDType]IFInfo
+
+	// IFInfo describes a border router link to another AS, including the internal data-plane
+	// address applications should send traffic to and information about the link itself and the
+	// remote side of it.
+	IFInfo struct {
+		// ID is the interface ID. It is unique per AS.
+		ID           common.IFIDType
+		BRName       string
+		CtrlAddrs    *TopoAddr
+		Underlay     overlay.Type
+		InternalAddr *net.UDPAddr
+		Local        *net.UDPAddr
+		Remote       *net.UDPAddr
+		RemoteIFID   common.IFIDType
+		Bandwidth    int
+		IA           addr.IA
+		LinkType     LinkType
+		MTU          int
 	}
-	return s[rand.Intn(numServers)], nil
-}
 
-// IfInfoMap maps interface ids to the interface information.
-type IfInfoMap map[common.IFIDType]IFInfo
+	// IDAddrMap maps process IDs to their topology addresses.
+	IDAddrMap map[string]TopoAddr
 
-// Topo is the main struct encompassing topology information for use in Go code.
-// The first section contains metadata about the topology. All of these fields
-// should be self-explanatory. The unit of TTL is seconds, with the zero value
-// indicating an infinite TTL.
-//
-// The second section concerns the Border routers. BRNames is just a sorted slice
-// of the names of the BRs in this topolgy. Its contents is exactly the same as
-// the keys in the BR map.
-//
-// The BR map points from border router names to BRInfo structs, which in turn
-// are lists of IFID type slices, thus defines the IFIDs that belong to a
-// particular border router. The IFInfoMap points from interface IDs to IFInfo structs.
-//
-// The third section in Topo concerns the SCION-specific services in the topology.
-// The structure is identical between the various elements. For each service,
-// 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 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
-	Core           bool
-
-	Overlay overlay.Type
-	MTU     int
-
-	BR      map[string]BRInfo
-	BRNames []string
-	// This maps Interface IDs to internal addresses. Clients use this to
-	// figure out which internal BR address they have to send their traffic to
-	// if they want to use a given interface.
-	IFInfoMap IfInfoMap
-
-	BS       IDAddrMap
-	BSNames  ServiceNames
-	CS       IDAddrMap
-	CSNames  ServiceNames
-	PS       IDAddrMap
-	PSNames  ServiceNames
-	DS       IDAddrMap
-	DSNames  ServiceNames
-	SIG      IDAddrMap
-	SIGNames ServiceNames
-}
+	// TopoAddr wraps the possible addresses of a SCION service and describes
+	// the underlay to be used for contacting said service.
+	TopoAddr struct {
+		SCIONAddress    *addr.AppAddr
+		UnderlayAddress net.Addr
+	}
+)
 
-// NewTopo creates new empty Topo object, including all possible service maps etc.
-func NewTopo() *Topo {
-	return &Topo{
+// NewRWTopology creates new empty Topo object, including all possible service maps etc.
+func NewRWTopology() *RWTopology {
+	return &RWTopology{
 		BR:        make(map[string]BRInfo),
 		BS:        make(IDAddrMap),
 		CS:        make(IDAddrMap),
@@ -118,10 +135,10 @@ func NewTopo() *Topo {
 	}
 }
 
-// TopoFromRaw converts a JSON-filled RawTopo to a Topo usable by Go code.
-func TopoFromRaw(raw *RawTopo) (*Topo, error) {
-	t := NewTopo()
-
+// RWTopologyFromJSONTopology converts a parsed JSON struct topology to a topology usable by Go
+// code.
+func RWTopologyFromJSONTopology(raw *jsontopo.Topology) (*RWTopology, error) {
+	t := NewRWTopology()
 	if err := t.populateMeta(raw); err != nil {
 		return nil, err
 	}
@@ -134,20 +151,42 @@ func TopoFromRaw(raw *RawTopo) (*Topo, error) {
 	return t, nil
 }
 
-func (t *Topo) populateMeta(raw *RawTopo) error {
+// RWTopologyFromJSONBytes extracts the topology from a JSON representation in raw byte format.
+func RWTopologyFromJSONBytes(b common.RawBytes) (*RWTopology, error) {
+	rt := &jsontopo.Topology{}
+	if err := json.Unmarshal(b, rt); err != nil {
+		return nil, err
+	}
+	ct, err := RWTopologyFromJSONTopology(rt)
+	if err != nil {
+		return nil, serrors.WrapStr("unable to convert raw topology to topology", err)
+	}
+	return ct, nil
+}
+
+// RWTopologyFromJSONFile extracts the topology from a file containing the JSON representation
+// of the topology.
+func RWTopologyFromJSONFile(path string) (*RWTopology, error) {
+	b, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	return RWTopologyFromJSONBytes(b)
+}
+
+func (t *RWTopology) populateMeta(raw *jsontopo.Topology) error {
 	// These fields can be simply copied
 	var err error
 	t.Timestamp = time.Unix(raw.Timestamp, 0)
-	t.TimestampHuman = raw.TimestampHuman
 	t.TTL = time.Duration(raw.TTL) * time.Second
 
-	if t.ISD_AS, err = addr.IAFromString(raw.ISD_AS); err != nil {
+	if t.IA, err = addr.IAFromString(raw.IA); err != nil {
 		return err
 	}
-	if t.ISD_AS.IsWildcard() {
-		return common.NewBasicError("IA contains wildcard", nil, "ia", t.ISD_AS)
+	if t.IA.IsWildcard() {
+		return common.NewBasicError("IA contains wildcard", nil, "ia", t.IA)
 	}
-	if t.Overlay, err = overlay.TypeFromString(raw.Overlay); err != nil {
+	if _, err = overlay.TypeFromString(raw.Overlay); err != nil {
 		return err
 	}
 	t.MTU = raw.MTU
@@ -155,7 +194,7 @@ func (t *Topo) populateMeta(raw *RawTopo) error {
 	return nil
 }
 
-func (t *Topo) populateBR(raw *RawTopo) error {
+func (t *RWTopology) populateBR(raw *jsontopo.Topology) error {
 	for name, rawBr := range raw.BorderRouters {
 		if rawBr.CtrlAddr == nil {
 			return common.NewBasicError("Missing Control Address", nil, "br", name)
@@ -163,19 +202,19 @@ func (t *Topo) populateBR(raw *RawTopo) error {
 		if rawBr.InternalAddrs == nil {
 			return common.NewBasicError("Missing Internal Address", nil, "br", name)
 		}
-		ctrlAddr, err := rawBr.CtrlAddr.ToTopoAddr()
+		ctrlAddr, err := rawAddrMapToTopoAddr(rawBr.CtrlAddr)
 		if err != nil {
 			return serrors.WrapStr("unable to extract SCION control-plane address", err)
 		}
-		intAddr, err := rawBr.InternalAddrs.ToUDPAddr()
+		intAddr, err := rawBRAddrMapToUDPAddr(rawBr.InternalAddrs)
 		if err != nil {
 			return serrors.WrapStr("unable to extract underlay internal data-plane address", err)
 		}
 		brInfo := BRInfo{
-			Name:          name,
-			CtrlAddrs:     ctrlAddr,
-			InternalAddrs: intAddr,
-			IFs:           make(map[common.IFIDType]*IFInfo),
+			Name:         name,
+			CtrlAddrs:    ctrlAddr,
+			InternalAddr: intAddr,
+			IFs:          make(map[common.IFIDType]*IFInfo),
 		}
 		for ifid, rawIntf := range rawBr.Interfaces {
 			var err error
@@ -185,37 +224,36 @@ func (t *Topo) populateBR(raw *RawTopo) error {
 			}
 			brInfo.IFIDs = append(brInfo.IFIDs, ifid)
 			ifinfo := IFInfo{
-				Id:            ifid,
-				BRName:        name,
-				InternalAddrs: intAddr,
-				CtrlAddrs:     ctrlAddr,
-				Bandwidth:     rawIntf.Bandwidth,
-				MTU:           rawIntf.MTU,
+				ID:           ifid,
+				BRName:       name,
+				InternalAddr: intAddr,
+				CtrlAddrs:    ctrlAddr,
+				Bandwidth:    rawIntf.Bandwidth,
+				MTU:          rawIntf.MTU,
 			}
-			if ifinfo.ISD_AS, err = addr.IAFromString(rawIntf.ISD_AS); err != nil {
+			if ifinfo.IA, err = addr.IAFromString(rawIntf.IA); err != nil {
 				return err
 			}
-			if ifinfo.LinkType, err = LinkTypeFromString(rawIntf.LinkTo); err != nil {
-				return err
-			}
-			if err = ifinfo.Verify(t.Core, name); err != nil {
+			ifinfo.LinkType = LinkTypeFromString(rawIntf.LinkTo)
+			if err = ifinfo.CheckLinks(t.Core, name); err != nil {
 				return err
 			}
 			// These fields are only necessary for the border router.
 			// Parsing should not fail if they are missing.
-			if rawIntf.Overlay == "" && rawIntf.BindOverlay == nil && rawIntf.RemoteOverlay == nil {
+			if rawIntf.Underlay == "" && rawIntf.BindUnderlay == nil &&
+				rawIntf.RemoteUnderlay == nil {
 				brInfo.IFs[ifid] = &ifinfo
 				t.IFInfoMap[ifid] = ifinfo
 				continue
 			}
-			if ifinfo.Overlay, err = overlay.TypeFromString(rawIntf.Overlay); err != nil {
+			if ifinfo.Underlay, err = overlay.TypeFromString(rawIntf.Underlay); err != nil {
 				return serrors.WrapStr("unable to extract underlay type", err)
 			}
-			if ifinfo.Local, err = rawIntf.TopoBRAddr(); err != nil {
+			if ifinfo.Local, err = rawBRIntfTopoBRAddr(rawIntf); 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 {
+			if ifinfo.Remote, err = rawBRIntfRemoteBRAddr(rawIntf, ifinfo.Underlay); err != nil {
 				return serrors.WrapStr("unable to extract "+
 					"underlay external data-plane remote address", err)
 			}
@@ -232,70 +270,62 @@ func (t *Topo) populateBR(raw *RawTopo) error {
 	return nil
 }
 
-func (t *Topo) populateServices(raw *RawTopo) error {
-	// Populate BS, CS, PS, SB, RS, SIG and DS maps
+func (t *RWTopology) populateServices(raw *jsontopo.Topology) error {
 	var err error
-	t.BSNames, err = svcMapFromRaw(raw.BeaconService, common.BS, t.BS, t.Overlay)
+	t.BS, err = svcMapFromRaw(raw.BeaconService)
 	if err != nil {
-		return err
+		return serrors.WrapStr("unable to extract BS address", err)
 	}
-	t.CSNames, err = svcMapFromRaw(raw.CertificateService, common.CS, t.CS, t.Overlay)
+	t.CS, err = svcMapFromRaw(raw.CertificateService)
 	if err != nil {
-		return err
+		return serrors.WrapStr("unable to extract CS address", err)
 	}
-	t.PSNames, err = svcMapFromRaw(raw.PathService, common.PS, t.PS, t.Overlay)
+	t.PS, err = svcMapFromRaw(raw.PathService)
 	if err != nil {
-		return err
+		return serrors.WrapStr("unable to extract PS address", err)
 	}
-	t.SIGNames, err = svcMapFromRaw(raw.SIG, common.SIG, t.SIG, t.Overlay)
+	t.SIG, err = svcMapFromRaw(raw.SIG)
 	if err != nil {
-		return err
+		return serrors.WrapStr("unable to extract SIG address", err)
 	}
-	t.DSNames, err = svcMapFromRaw(raw.DiscoveryService, common.DS, t.DS, t.Overlay)
+	t.DS, err = svcMapFromRaw(raw.DiscoveryService)
 	if err != nil {
-		return err
+		return serrors.WrapStr("unable to extract DS address", err)
 	}
 	return nil
 }
 
-func (t *Topo) Active(now time.Time) bool {
+// Active returns whether the topology is active at the point in time specified by the argument.
+// A topology is active if it is within a TTL window after the timestamp.
+func (t *RWTopology) Active(now time.Time) bool {
 	return !now.Before(t.Timestamp) && (t.TTL == 0 ||
 		now.Before(t.Timestamp.Add(t.TTL)))
 }
 
 // Expiry returns the expiration time of the topology. If TTL is zero, the zero value is returned.
-func (t *Topo) Expiry() time.Time {
+func (t *RWTopology) Expiry() time.Time {
 	if t.TTL == 0 {
 		return time.Time{}
 	}
 	return t.Timestamp.Add(t.TTL)
 }
 
-func (t *Topo) GetTopoAddr(id string, svc proto.ServiceType) (*TopoAddr, error) {
+// GetTopoAddr returns the address information for the process of the requested type with the
+// requested ID.
+func (t *RWTopology) GetTopoAddr(id string, svc proto.ServiceType) (*TopoAddr, error) {
 	svcInfo, err := t.getSvcInfo(svc)
 	if err != nil {
 		return nil, err
 	}
-	topoAddr := svcInfo.idTopoAddrMap.GetById(id)
+	topoAddr := svcInfo.idTopoAddrMap.GetByID(id)
 	if topoAddr == nil {
 		return nil, common.NewBasicError("Element not found", nil, "id", id)
 	}
 	return topoAddr, nil
 }
 
-func (t *Topo) GetAnyTopoAddr(svc proto.ServiceType) (*TopoAddr, error) {
-	svcInfo, err := t.getSvcInfo(svc)
-	if err != nil {
-		return nil, err
-	}
-	id, err := svcInfo.names.GetRandom()
-	if err != nil {
-		return nil, err
-	}
-	return svcInfo.idTopoAddrMap.GetById(id), nil
-}
-
-func (t *Topo) GetAllTopoAddrs(svc proto.ServiceType) ([]TopoAddr, error) {
+// GetAllTopoAddrs returns the address information of all processes of the requested type.
+func (t *RWTopology) GetAllTopoAddrs(svc proto.ServiceType) ([]TopoAddr, error) {
 	svcInfo, err := t.getSvcInfo(svc)
 	if err != nil {
 		return nil, err
@@ -307,20 +337,20 @@ func (t *Topo) GetAllTopoAddrs(svc proto.ServiceType) ([]TopoAddr, error) {
 	return topoAddrs, nil
 }
 
-func (t *Topo) getSvcInfo(svc proto.ServiceType) (*svcInfo, error) {
+func (t *RWTopology) getSvcInfo(svc proto.ServiceType) (*svcInfo, error) {
 	switch svc {
 	case proto.ServiceType_unset:
 		return nil, serrors.New("Service type unset")
 	case proto.ServiceType_bs:
-		return &svcInfo{overlay: t.Overlay, names: t.BSNames, idTopoAddrMap: t.BS}, nil
+		return &svcInfo{idTopoAddrMap: t.BS}, nil
 	case proto.ServiceType_ps:
-		return &svcInfo{overlay: t.Overlay, names: t.PSNames, idTopoAddrMap: t.PS}, nil
+		return &svcInfo{idTopoAddrMap: t.PS}, nil
 	case proto.ServiceType_cs:
-		return &svcInfo{overlay: t.Overlay, names: t.CSNames, idTopoAddrMap: t.CS}, nil
+		return &svcInfo{idTopoAddrMap: t.CS}, nil
 	case proto.ServiceType_sig:
-		return &svcInfo{overlay: t.Overlay, names: t.SIGNames, idTopoAddrMap: t.SIG}, nil
+		return &svcInfo{idTopoAddrMap: t.SIG}, nil
 	case proto.ServiceType_ds:
-		return &svcInfo{overlay: t.Overlay, names: t.DSNames, idTopoAddrMap: t.DS}, nil
+		return &svcInfo{idTopoAddrMap: t.DS}, nil
 	default:
 		return nil, common.NewBasicError("Unsupported service type", nil, "type", svc)
 	}
@@ -328,8 +358,6 @@ func (t *Topo) getSvcInfo(svc proto.ServiceType) (*svcInfo, error) {
 
 // svcInfo contains topology information for a single SCION service
 type svcInfo struct {
-	overlay       overlay.Type
-	names         ServiceNames
 	idTopoAddrMap IDAddrMap
 }
 
@@ -341,72 +369,40 @@ func (svc *svcInfo) getAllTopoAddrs() []TopoAddr {
 	return topoAddrs
 }
 
-// Convert map of Name->RawSrvInfo into map of Name->TopoAddr and sorted slice of Names
-// stype is only used for error reporting
-func svcMapFromRaw(ras map[string]*RawSrvInfo, stype string, smap IDAddrMap,
-	ot overlay.Type) ([]string, error) {
-
-	var snames []string
+func svcMapFromRaw(ras map[string]*jsontopo.ServerInfo) (IDAddrMap, error) {
+	svcMap := make(IDAddrMap)
 	for name, svc := range ras {
-		svcTopoAddr, err := svc.Addrs.ToTopoAddr()
+		svcTopoAddr, err := rawAddrMapToTopoAddr(svc.Addrs)
 		if err != nil {
-			return nil, common.NewBasicError(
-				"Could not convert RawAddrMap to TopoAddr", err,
-				"servicetype", stype, "RawAddrMap", svc.Addrs, "name", name)
+			return nil, serrors.WrapStr("could not extract address from address_map", err,
+				"address_map", svc.Addrs, "process_name", name)
 		}
-		smap[name] = *svcTopoAddr
-		snames = append(snames, name)
+		svcMap[name] = *svcTopoAddr
 	}
-	sort.Strings(snames)
-	return snames, nil
-}
-
-// BRInfo is a list of AS-wide unique interface IDs for a router. These IDs are also used
-// to point to the specific internal address clients should send their traffic
-// to in order to use that interface, via the IFInfoMap member of the Topo
-// struct.
-type BRInfo struct {
-	Name string
-	// CtrlAddrs are the local control-plane addresses.
-	CtrlAddrs *TopoAddr
-	// InternalAddrs are the local data-plane addresses.
-	InternalAddrs *net.UDPAddr
-	// IFIDs is a sorted list of the interface IDs.
-	IFIDs []common.IFIDType
-	// IFs is a map of interface IDs.
-	IFs map[common.IFIDType]*IFInfo
+	return svcMap, nil
 }
 
-// IFInfo describes a border router link to another AS, including the internal address
-// applications should send traffic for the link to (InternalAddrs) and information about
-// the link itself and the remote side of it.
-type IFInfo struct {
-	// Id is the interface ID. It is unique per AS.
-	Id            common.IFIDType
-	BRName        string
-	CtrlAddrs     *TopoAddr
-	Overlay       overlay.Type
-	InternalAddrs *net.UDPAddr
-	Local         *net.UDPAddr
-	Remote        *net.UDPAddr
-	RemoteIFID    common.IFIDType
-	Bandwidth     int
-	ISD_AS        addr.IA
-	LinkType      proto.LinkType
-	MTU           int
+// GetByID returns the TopoAddr for the given ID, or nil if there is none.
+func (m IDAddrMap) GetByID(id string) *TopoAddr {
+	if _, ok := m[id]; ok {
+		cp := m[id]
+		return &cp
+	}
+	return nil
 }
 
-func (i IFInfo) Verify(isCore bool, brName string) error {
+// CheckLinks checks whether the link types are compatible with whether the AS is core or not.
+func (i IFInfo) CheckLinks(isCore bool, brName string) error {
 	if isCore {
 		switch i.LinkType {
-		case proto.LinkType_core, proto.LinkType_child:
+		case Core, Child:
 		default:
 			return common.NewBasicError("Illegal link type for core AS", nil,
 				"type", i.LinkType, "br", brName)
 		}
 	} else {
 		switch i.LinkType {
-		case proto.LinkType_parent, proto.LinkType_child, proto.LinkType_peer:
+		case Parent, Child, Peer:
 		default:
 			return common.NewBasicError("Illegal link type for non-core AS", nil,
 				"type", i.LinkType, "br", brName)
@@ -417,8 +413,46 @@ func (i IFInfo) Verify(isCore bool, brName string) error {
 
 func (i IFInfo) String() string {
 	return fmt.Sprintf("IFinfo: Name[%s] IntAddr[%+v] CtrlAddr[%+v] Overlay:%s Local:%+v "+
-		"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)
+		"Remote:%+v Bw:%d IA:%s Type:%v MTU:%d", i.BRName, i.InternalAddr, i.CtrlAddrs, i.Underlay,
+		i.Local, i.Remote, i.Bandwidth, i.IA, i.LinkType, i.MTU)
+}
+
+// UnderlayAddr returns the underlay address interpreted as a net.UDPAddr.
+//
+// FIXME(scrye): This should be removed; applications should not need to look into the underlay
+// concrete type.
+func (a *TopoAddr) UnderlayAddr() *net.UDPAddr {
+	return a.UnderlayAddress.(*net.UDPAddr)
+}
+
+func (a *TopoAddr) String() string {
+	return fmt.Sprintf("TopoAddr{SCION: %v, Underlay: %v}", a.SCIONAddress, a.UnderlayAddress)
+}
+
+func toUDPAddr(a net.Addr) *net.UDPAddr {
+	if a == nil {
+		return nil
+	}
+	udpAddr, ok := a.(*net.UDPAddr)
+	if !ok {
+		return nil
+	}
+	return &net.UDPAddr{
+		IP:   append(udpAddr.IP[:0:0], udpAddr.IP...),
+		Port: udpAddr.Port,
+	}
+}
+
+// ServiceNames is a slice of process names (e.g., "bs-1", "bs-2").
+type ServiceNames []string
+
+// GetRandom returns a random entry, or an error if the slice is empty.
+func (s ServiceNames) GetRandom() (string, error) {
+	numServers := len(s)
+	if numServers == 0 {
+		return "", serrors.New("No names present")
+	}
+	return s[rand.Intn(numServers)], nil
 }
 
 func copyUDPAddr(a *net.UDPAddr) *net.UDPAddr {
diff --git a/go/lib/topology/types.go b/go/lib/topology/types.go
deleted file mode 100644
index 161435cda..000000000
--- a/go/lib/topology/types.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2017 ETH Zurich
-// Copyright 2019 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 topology
-
-import (
-	"strings"
-
-	"github.com/scionproto/scion/go/lib/common"
-	"github.com/scionproto/scion/go/proto"
-)
-
-const (
-	// CoreLinkName indicates a link going to a neighboring core AS.
-	CoreLinkName = "CORE"
-	// ParentLinkName indicates a link to a parent AS.
-	ParentLinkName = "PARENT"
-	// ChildLinkName indicates a link to a child AS.
-	ChildLinkName = "CHILD"
-	// PeerLinkName indicates a link to a neighboring peer AS.
-	PeerLinkName = "PEER"
-)
-
-// LinkTypeFromString parses the link type.
-func LinkTypeFromString(s string) (proto.LinkType, error) {
-	linkType := proto.LinkTypeFromString(strings.ToLower(s))
-	if linkType == 0 || linkType == proto.LinkType_unset {
-		return proto.LinkType_unset, common.NewBasicError("Unknown link type", nil, "type", s)
-	}
-	return linkType, nil
-}
-
-// Provider provides a topology.
-type Provider interface {
-	// Get returns a topology. The returned topology is guaranteed to not be
-	// nil.
-	Get() *Topo
-}
diff --git a/go/path_srv/BUILD.bazel b/go/path_srv/BUILD.bazel
index bd0a20132..8139e9456 100644
--- a/go/path_srv/BUILD.bazel
+++ b/go/path_srv/BUILD.bazel
@@ -25,6 +25,7 @@ go_library(
         "//go/lib/periodic:go_default_library",
         "//go/lib/prom:go_default_library",
         "//go/lib/revcache:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/path_srv/internal/config:go_default_library",
         "//go/path_srv/internal/cryptosyncer:go_default_library",
         "//go/path_srv/internal/handlers:go_default_library",
diff --git a/go/path_srv/internal/handlers/BUILD.bazel b/go/path_srv/internal/handlers/BUILD.bazel
index 1b995acb2..d9bb0fb10 100644
--- a/go/path_srv/internal/handlers/BUILD.bazel
+++ b/go/path_srv/internal/handlers/BUILD.bazel
@@ -19,7 +19,6 @@ go_library(
         "//go/lib/ctrl/seg:go_default_library",
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/messenger:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/infra/modules/segfetcher:go_default_library",
         "//go/lib/infra/modules/seghandler:go_default_library",
         "//go/lib/infra/modules/segverifier:go_default_library",
@@ -28,6 +27,7 @@ go_library(
         "//go/lib/pathdb/query:go_default_library",
         "//go/lib/revcache:go_default_library",
         "//go/lib/snet:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/path_srv/internal/metrics:go_default_library",
         "//go/proto:go_default_library",
         "@com_github_opentracing_opentracing_go//:go_default_library",
diff --git a/go/path_srv/internal/handlers/common.go b/go/path_srv/internal/handlers/common.go
index f868c0fbd..bc5a10d74 100644
--- a/go/path_srv/internal/handlers/common.go
+++ b/go/path_srv/internal/handlers/common.go
@@ -24,11 +24,11 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
 	"github.com/scionproto/scion/go/lib/infra"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/infra/modules/segfetcher"
 	"github.com/scionproto/scion/go/lib/pathdb"
 	"github.com/scionproto/scion/go/lib/pathdb/query"
 	"github.com/scionproto/scion/go/lib/revcache"
+	"github.com/scionproto/scion/go/lib/topology"
 )
 
 const (
@@ -43,7 +43,7 @@ type HandlerArgs struct {
 	VerifierFactory infra.VerificationFactory
 	QueryInterval   time.Duration
 	IA              addr.IA
-	TopoProvider    itopo.ProviderI
+	TopoProvider    topology.Provider
 	SegRequestAPI   segfetcher.RequestAPI
 }
 
@@ -53,7 +53,7 @@ type baseHandler struct {
 	revCache        revcache.RevCache
 	inspector       infra.ASInspector
 	verifierFactory infra.VerificationFactory
-	topoProvider    itopo.ProviderI
+	topoProvider    topology.Provider
 	retryInt        time.Duration
 	queryInt        time.Duration
 }
diff --git a/go/path_srv/internal/segreq/BUILD.bazel b/go/path_srv/internal/segreq/BUILD.bazel
index 96f317f08..313160ef5 100644
--- a/go/path_srv/internal/segreq/BUILD.bazel
+++ b/go/path_srv/internal/segreq/BUILD.bazel
@@ -20,7 +20,6 @@ go_library(
         "//go/lib/ctrl/seg:go_default_library",
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/messenger:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/infra/modules/segfetcher:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/pathdb:go_default_library",
@@ -28,6 +27,7 @@ go_library(
         "//go/lib/revcache:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet/addrutil:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/path_srv/internal/handlers:go_default_library",
         "//go/path_srv/internal/metrics:go_default_library",
         "//go/proto:go_default_library",
diff --git a/go/path_srv/internal/segreq/provider.go b/go/path_srv/internal/segreq/provider.go
index 088f46acf..fcb78a342 100644
--- a/go/path_srv/internal/segreq/provider.go
+++ b/go/path_srv/internal/segreq/provider.go
@@ -24,13 +24,13 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
 	"github.com/scionproto/scion/go/lib/infra"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/infra/modules/segfetcher"
 	"github.com/scionproto/scion/go/lib/pathdb"
 	"github.com/scionproto/scion/go/lib/pathdb/query"
 	"github.com/scionproto/scion/go/lib/revcache"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/snet/addrutil"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/proto"
 )
 
@@ -71,7 +71,7 @@ type nonCoreDstProvider struct {
 	coreChecker  CoreChecker
 	localIA      addr.IA
 	pathDB       pathdb.PathDB
-	topoProvider itopo.ProviderI
+	topoProvider topology.Provider
 }
 
 // Dst provides the server to lookup the segment for the given request.
@@ -120,7 +120,7 @@ type coreDstProvider struct {
 	SegSelector
 	localIA      addr.IA
 	pathDB       pathdb.PathDB
-	topoProvider itopo.ProviderI
+	topoProvider topology.Provider
 }
 
 func (p *coreDstProvider) Dst(ctx context.Context, req segfetcher.Request) (net.Addr, error) {
diff --git a/go/path_srv/internal/segsyncer/BUILD.bazel b/go/path_srv/internal/segsyncer/BUILD.bazel
index eed8702da..5acf97d77 100644
--- a/go/path_srv/internal/segsyncer/BUILD.bazel
+++ b/go/path_srv/internal/segsyncer/BUILD.bazel
@@ -12,7 +12,6 @@ go_library(
         "//go/lib/ctrl/seg:go_default_library",
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/messenger:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/pathdb:go_default_library",
         "//go/lib/pathdb/query:go_default_library",
@@ -20,6 +19,7 @@ go_library(
         "//go/lib/revcache:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet/addrutil:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/path_srv/internal/handlers:go_default_library",
         "//go/path_srv/internal/metrics:go_default_library",
         "//go/proto:go_default_library",
diff --git a/go/path_srv/internal/segsyncer/segsyncer.go b/go/path_srv/internal/segsyncer/segsyncer.go
index e8e6e7214..410d6a5eb 100644
--- a/go/path_srv/internal/segsyncer/segsyncer.go
+++ b/go/path_srv/internal/segsyncer/segsyncer.go
@@ -29,7 +29,6 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/infra/messenger"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/pathdb"
 	"github.com/scionproto/scion/go/lib/pathdb/query"
@@ -37,6 +36,7 @@ import (
 	"github.com/scionproto/scion/go/lib/revcache"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/snet/addrutil"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/path_srv/internal/handlers"
 	"github.com/scionproto/scion/go/path_srv/internal/metrics"
 	"github.com/scionproto/scion/go/proto"
@@ -58,7 +58,7 @@ type SegSyncer struct {
 	msger        infra.Messenger
 	dstIA        addr.IA
 	localIA      addr.IA
-	topoProvider itopo.ProviderI
+	topoProvider topology.Provider
 	repErrCnt    int
 }
 
diff --git a/go/path_srv/main.go b/go/path_srv/main.go
index 6c0d299f9..201788226 100644
--- a/go/path_srv/main.go
+++ b/go/path_srv/main.go
@@ -44,6 +44,7 @@ import (
 	"github.com/scionproto/scion/go/lib/periodic"
 	"github.com/scionproto/scion/go/lib/prom"
 	"github.com/scionproto/scion/go/lib/revcache"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/path_srv/internal/config"
 	"github.com/scionproto/scion/go/path_srv/internal/cryptosyncer"
 	"github.com/scionproto/scion/go/path_srv/internal/handlers"
@@ -122,6 +123,7 @@ func realMain() int {
 	}
 	defer trCloser.Close()
 	opentracing.SetGlobalTracer(tracer)
+
 	nc := infraenv.NetworkConfig{
 		IA:                    topo.IA(),
 		Public:                topo.PublicAddress(addr.SvcPS, cfg.General.ID),
@@ -268,7 +270,7 @@ func setup() error {
 		return common.NewBasicError("Unable to validate config", err)
 	}
 	itopo.Init(cfg.General.ID, proto.ServiceType_ps, itopo.Callbacks{})
-	topo, err := itopo.LoadFromFile(cfg.General.Topology)
+	topo, err := topology.FromJSONFile(cfg.General.Topology)
 	if err != nil {
 		return common.NewBasicError("Unable to load topology", err)
 	}
diff --git a/go/sciond/BUILD.bazel b/go/sciond/BUILD.bazel
index 00496a54d..81a59859f 100644
--- a/go/sciond/BUILD.bazel
+++ b/go/sciond/BUILD.bazel
@@ -24,6 +24,7 @@ go_library(
         "//go/lib/periodic:go_default_library",
         "//go/lib/prom:go_default_library",
         "//go/lib/revcache:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/proto:go_default_library",
         "//go/sciond/internal/config:go_default_library",
         "//go/sciond/internal/fetcher:go_default_library",
diff --git a/go/sciond/internal/fetcher/BUILD.bazel b/go/sciond/internal/fetcher/BUILD.bazel
index 6a8e53b13..cf62ceaa6 100644
--- a/go/sciond/internal/fetcher/BUILD.bazel
+++ b/go/sciond/internal/fetcher/BUILD.bazel
@@ -16,7 +16,6 @@ go_library(
         "//go/lib/hostinfo:go_default_library",
         "//go/lib/infra:go_default_library",
         "//go/lib/infra/modules/combinator:go_default_library",
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/infra/modules/segfetcher:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/pathdb:go_default_library",
@@ -26,6 +25,7 @@ go_library(
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/spath:go_default_library",
+        "//go/lib/topology:go_default_library",
         "//go/lib/util:go_default_library",
         "//go/sciond/internal/config:go_default_library",
         "//go/sciond/internal/metrics:go_default_library",
diff --git a/go/sciond/internal/fetcher/fetcher.go b/go/sciond/internal/fetcher/fetcher.go
index d1bf22172..19c4ecab2 100644
--- a/go/sciond/internal/fetcher/fetcher.go
+++ b/go/sciond/internal/fetcher/fetcher.go
@@ -28,7 +28,6 @@ import (
 	"github.com/scionproto/scion/go/lib/hostinfo"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/infra/modules/combinator"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/infra/modules/segfetcher"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/pathdb"
@@ -37,6 +36,7 @@ import (
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/snet"
 	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/util"
 	"github.com/scionproto/scion/go/sciond/internal/config"
 	"github.com/scionproto/scion/go/sciond/internal/metrics"
@@ -54,14 +54,13 @@ type TrustStore interface {
 type Fetcher struct {
 	pathDB          pathdb.PathDB
 	revocationCache revcache.RevCache
-	topoProvider    itopo.ProviderI
+	topoProvider    topology.Provider
 	config          config.SDConfig
 	segfetcher      *segfetcher.Fetcher
 }
 
 func NewFetcher(messenger infra.Messenger, pathDB pathdb.PathDB, trustStore TrustStore,
-	revCache revcache.RevCache, cfg config.SDConfig, topoProvider itopo.ProviderI,
-	logger log.Logger) *Fetcher {
+	revCache revcache.RevCache, cfg config.SDConfig, topoProvider topology.Provider) *Fetcher {
 
 	localIA := topoProvider.Get().IA()
 	return &Fetcher{
@@ -100,7 +99,7 @@ func (f *Fetcher) GetPaths(ctx context.Context, req *sciond.PathReq,
 // received by the Fetcher.
 type fetcherHandler struct {
 	*Fetcher
-	topology itopo.Topology
+	topology topology.Topology
 	logger   log.Logger
 }
 
diff --git a/go/sciond/main.go b/go/sciond/main.go
index 6ea95c92d..6872e36cf 100644
--- a/go/sciond/main.go
+++ b/go/sciond/main.go
@@ -44,6 +44,7 @@ import (
 	"github.com/scionproto/scion/go/lib/periodic"
 	"github.com/scionproto/scion/go/lib/prom"
 	"github.com/scionproto/scion/go/lib/revcache"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/proto"
 	"github.com/scionproto/scion/go/sciond/internal/config"
 	"github.com/scionproto/scion/go/sciond/internal/fetcher"
@@ -151,7 +152,6 @@ func realMain() int {
 				revCache,
 				cfg.SD,
 				itopo.Provider(),
-				log.Root(),
 			),
 		},
 		proto.SCIONDMsg_Which_asInfoReq: &servers.ASInfoRequestHandler{
@@ -206,7 +206,7 @@ func setup() error {
 		return common.NewBasicError("unable to validate config", err)
 	}
 	itopo.Init("", proto.ServiceType_unset, itopo.Callbacks{})
-	topo, err := itopo.LoadFromFile(cfg.General.Topology)
+	topo, err := topology.FromJSONFile(cfg.General.Topology)
 	if err != nil {
 		return common.NewBasicError("unable to load topology", err)
 	}
diff --git a/go/tools/topopruner/BUILD.bazel b/go/tools/topopruner/BUILD.bazel
deleted file mode 100644
index bbead38b8..000000000
--- a/go/tools/topopruner/BUILD.bazel
+++ /dev/null
@@ -1,19 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-load("//:scion.bzl", "scion_go_binary")
-
-go_library(
-    name = "go_default_library",
-    srcs = ["topopruner.go"],
-    importpath = "github.com/scionproto/scion/go/tools/topopruner",
-    visibility = ["//visibility:private"],
-    deps = [
-        "//go/lib/env:go_default_library",
-        "//go/lib/topology:go_default_library",
-    ],
-)
-
-scion_go_binary(
-    name = "topopruner",
-    embed = [":go_default_library"],
-    visibility = ["//visibility:public"],
-)
diff --git a/go/tools/topopruner/topopruner.go b/go/tools/topopruner/topopruner.go
deleted file mode 100644
index 391e48729..000000000
--- a/go/tools/topopruner/topopruner.go
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright 2017 ETH Zurich
-// 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 main
-
-import (
-	"encoding/json"
-	"flag"
-	"fmt"
-	"io/ioutil"
-	"os"
-
-	"github.com/scionproto/scion/go/lib/env"
-	"github.com/scionproto/scion/go/lib/topology"
-)
-
-var (
-	infn   = flag.String("in", "", "Input file name. Required.")
-	outfnf = flag.String("out", "", "Output file name for the full topology. Required.")
-	outfnr = flag.String("endhost", "",
-		"Output file name for the endhost topology. Defaults to not generating this output.")
-	verbose = flag.Bool("verbose", false, "Be more verbose about what is going on")
-	version = flag.Bool("version", false, "Output version information and exit.")
-)
-
-func main() {
-	flag.Parse()
-	if *version {
-		fmt.Print(env.VersionInfo())
-		os.Exit(0)
-	}
-	if *infn == "" || *outfnf == "" {
-		fmt.Fprintf(os.Stderr,
-			"You must specify an input file and an output file for the full topo.\n")
-		os.Exit(-1)
-	}
-	rt, err := topology.LoadRawFromFile(*infn)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Error reading input file: %s\n", err)
-		os.Exit(-1)
-	}
-	finfo, err := os.Stat(*infn)
-	if err != nil {
-		// This should be pretty rare since the open() worked above
-		fmt.Fprintf(os.Stderr, "Error stat()ing input file: %s\n", err)
-		os.Exit(-1)
-	}
-	topology.StripBind(rt)
-	marshalAndWriteOrDie(rt, *outfnf, "full", finfo.Mode())
-	if *verbose {
-		fmt.Printf("Wrote pruned full topo to '%s'.\n", *outfnf)
-	}
-	if *outfnr != "" {
-		topology.StripServices(rt)
-		marshalAndWriteOrDie(rt, *outfnr, "endhost", finfo.Mode())
-		if *verbose {
-			fmt.Printf("Wrote pruned endhost topo to '%s'.\n", *outfnr)
-		}
-	}
-}
-
-func marshalAndWriteOrDie(rt *topology.RawTopo, filename, ttype string, mode os.FileMode) {
-	b, err := json.MarshalIndent(rt, "", "  ")
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Could not marshal %s topo to JSON: %s\n", ttype, err)
-		os.Exit(-1)
-	}
-	err = ioutil.WriteFile(filename, b, mode)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Could not write out %s topo: %s\n", ttype, err)
-		os.Exit(-1)
-	}
-}
diff --git a/tools/gomocks b/tools/gomocks
index 414124a8e..9bfe7c9ae 100755
--- a/tools/gomocks
+++ b/tools/gomocks
@@ -63,6 +63,7 @@ MOCK_TARGETS = [
         (SCION_PACKAGE_PREFIX + "/go/lib/sock/reliable/reconnect", "IOOperation,Reconnecter"),
         (SCION_PACKAGE_PREFIX + "/go/lib/svc", "RequestHandler,RoundTripper"),
         (SCION_PACKAGE_PREFIX + "/go/lib/svc/internal/ctxconn", "DeadlineCloser"),
+        (SCION_PACKAGE_PREFIX + "/go/lib/topology", "Topology"),
         (SCION_PACKAGE_PREFIX + "/go/lib/periodic/internal/metrics", "ExportMetric"),
         (SCION_PACKAGE_PREFIX + "/go/lib/xtest", "Callback"),
         (SCION_PACKAGE_PREFIX + "/go/path_srv/internal/segreq", "LocalInfo"),


Test patch:
diff --git a/acceptance/topo_invalid_reloads_acceptance/test b/acceptance/topo_invalid_reloads_acceptance/test
index b530b793f..43a6d9d6e 100755
--- a/acceptance/topo_invalid_reloads_acceptance/test
+++ b/acceptance/topo_invalid_reloads_acceptance/test
@@ -33,9 +33,9 @@ test_run() {
 test_ps_immutable() {
     test_immutable $1 $2 "$3"
     jq '.PathService[].Addrs.IPv4.Public.Addr = "242.42.42.42"' $TOPO | sponge $2
-    check_no_reload $1 5 "$3: Addr"
+    check_no_reload $1 4 "$3: Addr"
     jq '.PathService[].Addrs.IPv4.Public.L4Port = 42424' $TOPO | sponge $2
-    check_no_reload $1 6 "$3: L4Port"
+    check_no_reload $1 5 "$3: L4Port"
 }
 
 test_immutable() {
@@ -43,10 +43,8 @@ test_immutable() {
     check_no_reload "$1" 1 "$3: ISD_AS"
     jq '.Core = true | del( .BorderRouters[].Interfaces )' $TOPO | sponge $2
     check_no_reload "$1" 2 "$3: Core"
-    jq '.Overlay = "UDP/IPv4+6"' $TOPO | sponge $2
-    check_no_reload "$1" 3 "$3: Overlay"
     jq '.MTU = 42' $TOPO | sponge $2
-    check_no_reload "$1" 4 "$3: MTU"
+    check_no_reload "$1" 3 "$3: MTU"
 }
 
 check_no_reload() {
diff --git a/go/beacon_srv/internal/beaconing/extender_test.go b/go/beacon_srv/internal/beaconing/extender_test.go
index 8175123e9..a36e78737 100644
--- a/go/beacon_srv/internal/beaconing/extender_test.go
+++ b/go/beacon_srv/internal/beaconing/extender_test.go
@@ -301,7 +301,7 @@ func testHopEntry(hop *seg.HopEntry, intfs *ifstate.Interfaces, inIfid, egIfid c
 	if inIfid != 0 {
 		intf := intfs.Get(inIfid)
 		SoMsg("Intf", intf, ShouldNotBeNil)
-		ia = intf.TopoInfo().ISD_AS
+		ia = intf.TopoInfo().IA
 		ifid = intf.TopoInfo().RemoteIFID
 		mtu = uint16(intf.TopoInfo().MTU)
 	}
@@ -313,7 +313,7 @@ func testHopEntry(hop *seg.HopEntry, intfs *ifstate.Interfaces, inIfid, egIfid c
 	// contain zero values.
 	if egIfid != 0 {
 		intf := intfs.Get(egIfid)
-		ia = intf.TopoInfo().ISD_AS
+		ia = intf.TopoInfo().IA
 		ifid = intf.TopoInfo().RemoteIFID
 	}
 	SoMsg("Hop.OutIA", hop.OutIA(), ShouldResemble, ia)
diff --git a/go/beacon_srv/internal/beaconing/handler_test.go b/go/beacon_srv/internal/beaconing/handler_test.go
index 05999f4b5..8d05c0b81 100644
--- a/go/beacon_srv/internal/beaconing/handler_test.go
+++ b/go/beacon_srv/internal/beaconing/handler_test.go
@@ -31,12 +31,12 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/infra/mock_infra"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/infra/modules/itopo/itopotest"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/snet"
 	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/xtest"
 	"github.com/scionproto/scion/go/lib/xtest/graph"
 )
@@ -223,7 +223,7 @@ func testPath(ingressIfid common.IFIDType) *spath.Path {
 	return path
 }
 
-func testInterfaces(topo itopo.Topology) *ifstate.Interfaces {
+func testInterfaces(topo topology.Topology) *ifstate.Interfaces {
 	intfs := ifstate.NewInterfaces(topo.IFInfoMap(), ifstate.Config{})
 	intfs.Get(graph.If_110_X_120_A).Activate(graph.If_120_A_110_X)
 	return intfs
diff --git a/go/beacon_srv/internal/beaconing/originator_test.go b/go/beacon_srv/internal/beaconing/originator_test.go
index fd3eca11f..b6e01cce5 100644
--- a/go/beacon_srv/internal/beaconing/originator_test.go
+++ b/go/beacon_srv/internal/beaconing/originator_test.go
@@ -186,7 +186,7 @@ func checkMsg(t *testing.T, msg msg, pub common.RawBytes, infos topology.IfInfoM
 		bHopF, err := pseg.ASEntries[pseg.MaxAEIdx()].HopEntries[0].HopField()
 		xtest.FailOnErr(t, err)
 		SoMsg("Egress", hopF.ConsEgress, ShouldEqual, bHopF.ConsEgress)
-		brAddr := infos[hopF.ConsEgress].InternalAddrs
+		brAddr := infos[hopF.ConsEgress].InternalAddr
 		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 19852c9aa..a04f7c732 100644
--- a/go/beacon_srv/internal/beaconing/registrar_test.go
+++ b/go/beacon_srv/internal/beaconing/registrar_test.go
@@ -175,7 +175,7 @@ func TestRegistrarRun(t *testing.T) {
 					SoMsg("err", err, ShouldBeNil)
 					SoMsg("HopField", []uint8(hopF.Pack()), ShouldResemble,
 						pseg.ASEntries[pseg.MaxAEIdx()].HopEntries[0].RawHopField)
-					a := topoProvider.Get().IFInfoMap()[hopF.ConsIngress].InternalAddrs
+					a := topoProvider.Get().IFInfoMap()[hopF.ConsIngress].InternalAddr
 					SoMsg("Next", s.Addr.NextHop, ShouldResemble, a)
 				})
 			}
diff --git a/go/beacon_srv/internal/ifstate/handler_test.go b/go/beacon_srv/internal/ifstate/handler_test.go
index 3a714577d..da29bd65e 100644
--- a/go/beacon_srv/internal/ifstate/handler_test.go
+++ b/go/beacon_srv/internal/ifstate/handler_test.go
@@ -26,8 +26,8 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/path_mgmt"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/infra/mock_infra"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/infra/modules/itopo/itopotest"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/xtest"
 )
 
@@ -111,7 +111,7 @@ func TestHandler(t *testing.T) {
 	}
 }
 
-func interfaces(t *testing.T, topoProvider itopo.ProviderI,
+func interfaces(t *testing.T, topoProvider topology.Provider,
 	expectedIfSate *path_mgmt.IFStateInfos) *Interfaces {
 
 	intfs := NewInterfaces(topoProvider.Get().IFInfoMap(), Config{})
diff --git a/go/beacon_srv/internal/ifstate/revoker_test.go b/go/beacon_srv/internal/ifstate/revoker_test.go
index a2dcf28f5..67a8db056 100644
--- a/go/beacon_srv/internal/ifstate/revoker_test.go
+++ b/go/beacon_srv/internal/ifstate/revoker_test.go
@@ -33,12 +33,12 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/path_mgmt"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/infra/mock_infra"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/infra/modules/itopo/itopotest"
 	"github.com/scionproto/scion/go/lib/infra/modules/trust"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/scrypto"
 	"github.com/scionproto/scion/go/lib/snet"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/util"
 	"github.com/scionproto/scion/go/lib/xtest"
 	"github.com/scionproto/scion/go/lib/xtest/matchers"
@@ -237,7 +237,7 @@ func TestRevokedInterfaceRevokedAgain(t *testing.T) {
 // TODO(lukedirtwalker): test revoking multiple interfaces at once.
 
 func expectMessengerCalls(msger *mock_infra.MockMessenger,
-	revokedIfId common.IFIDType, topoProvider itopo.ProviderI) func(*testing.T, revVerifier) {
+	revokedIfId common.IFIDType, topoProvider topology.Provider) func(*testing.T, revVerifier) {
 
 	var brMsgs []brMsg
 	var brMsgsMtx sync.Mutex
@@ -281,7 +281,7 @@ func expectMessengerCalls(msger *mock_infra.MockMessenger,
 }
 
 func checkBRMessage(t *testing.T, brId string, infos *path_mgmt.IFStateInfos,
-	revokedIfId common.IFIDType, verifier revVerifier, topoProvider itopo.ProviderI) {
+	revokedIfId common.IFIDType, verifier revVerifier, topoProvider topology.Provider) {
 
 	Convey(fmt.Sprintf("Check ifstateinfo for %s", brId), func() {
 		SoMsg("Should contain correct amount of infos", len(infos.Infos), ShouldEqual, 1)
@@ -292,7 +292,7 @@ func checkBRMessage(t *testing.T, brId string, infos *path_mgmt.IFStateInfos,
 }
 
 func checkRevocation(t *testing.T, srev *path_mgmt.SignedRevInfo,
-	revokedIfId common.IFIDType, verifier revVerifier, topoProvider itopo.ProviderI) {
+	revokedIfId common.IFIDType, verifier revVerifier, topoProvider topology.Provider) {
 
 	Convey("Check revocation", func() {
 		revInfo, err := srev.VerifiedRevInfo(context.Background(), verifier)
@@ -320,7 +320,7 @@ func checkInterfaces(intfs *Interfaces, nonActive map[common.IFIDType]State) {
 	})
 }
 
-func brId(t *testing.T, topoProvider itopo.ProviderI, saddr *snet.Addr) string {
+func brId(t *testing.T, topoProvider topology.Provider, saddr *snet.Addr) string {
 	topo := topoProvider.Get()
 	for _, brID := range topo.BRNames() {
 		if topo.SBRAddress(brID).Host.Equal(saddr.Host) {
@@ -332,7 +332,7 @@ func brId(t *testing.T, topoProvider itopo.ProviderI, saddr *snet.Addr) string {
 }
 
 // expectedBRs return a set of BR ids for which we expect a if state update push.
-func expectedBRs(topoProvider itopo.ProviderI) map[string]struct{} {
+func expectedBRs(topoProvider topology.Provider) map[string]struct{} {
 	brIds := make(map[string]struct{})
 	for _, brId := range topoProvider.Get().BRNames() {
 		brIds[brId] = struct{}{}
diff --git a/go/beacon_srv/internal/keepalive/handler_test.go b/go/beacon_srv/internal/keepalive/handler_test.go
index e5b5af0cc..7c8839fb8 100644
--- a/go/beacon_srv/internal/keepalive/handler_test.go
+++ b/go/beacon_srv/internal/keepalive/handler_test.go
@@ -143,7 +143,7 @@ func TestNewHandler(t *testing.T) {
 }
 
 func testInterfaces() *ifstate.Interfaces {
-	infoMap := topology.IfInfoMap{localIF: topology.IFInfo{ISD_AS: originIA}}
+	infoMap := topology.IfInfoMap{localIF: topology.IFInfo{IA: originIA}}
 	return ifstate.NewInterfaces(infoMap, ifstate.Config{})
 }
 
diff --git a/go/border/setup_test.go b/go/border/setup_test.go
index d7c836a14..0ac5f0b2b 100644
--- a/go/border/setup_test.go
+++ b/go/border/setup_test.go
@@ -25,9 +25,9 @@ import (
 	"github.com/scionproto/scion/go/border/rctx"
 	"github.com/scionproto/scion/go/border/rpkt"
 	"github.com/scionproto/scion/go/lib/common"
-	"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"
 	"github.com/scionproto/scion/go/lib/xtest"
 )
 
@@ -51,7 +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.
-		ctx.Conf.BR.InternalAddrs.IP[3] = 255
+		ctx.Conf.BR.InternalAddr.IP[3] = 255
 		SoMsg("In", oldCtx.LocSockIn, ShouldNotBeNil)
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
@@ -130,7 +130,7 @@ func TestRollbackNet(t *testing.T) {
 		r, oldCtx := setupTestRouter(t)
 		copyCtx := copyContext(oldCtx)
 		ctx := rctx.New(loadConfig(t))
-		ctx.Conf.BR.InternalAddrs.IP[3] = 255
+		ctx.Conf.BR.InternalAddr.IP[3] = 255
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
 		// Rollback the changes.
@@ -187,7 +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))
-		ctx.Conf.BR.InternalAddrs.IP[3] = 255
+		ctx.Conf.BR.InternalAddr.IP[3] = 255
 		clean := updateTestRouter(r, ctx, oldCtx)
 		defer clean()
 		// Start sockets on the new context.
@@ -356,7 +356,7 @@ func closeAllSocks(ctx *rctx.Ctx) {
 }
 
 func loadConfig(t *testing.T) *brconf.BRConf {
-	topo, err := itopo.LoadFromFile("testdata/topology.json")
+	topo, err := topology.FromJSONFile("testdata/topology.json")
 	xtest.FailOnErr(t, err)
 	topoBR, ok := topo.BR("br1-ff00_0_111-1")
 	if !ok {
diff --git a/go/lib/discovery/discoverypool/pool_test.go b/go/lib/discovery/discoverypool/pool_test.go
index ee3078854..840e3cb17 100644
--- a/go/lib/discovery/discoverypool/pool_test.go
+++ b/go/lib/discovery/discoverypool/pool_test.go
@@ -21,7 +21,6 @@ import (
 	. "github.com/smartystreets/goconvey/convey"
 
 	"github.com/scionproto/scion/go/lib/addr"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/xtest"
 )
@@ -140,7 +139,7 @@ func mustLoadPool(t *testing.T) *Pool {
 }
 
 func mustLoadSvcInfo(t *testing.T) topology.IDAddrMap {
-	topo, err := itopo.LoadFromFile("testdata/topology.json")
+	topo, err := topology.FromJSONFile("testdata/topology.json")
 	xtest.FailOnErr(t, err)
 	return topo.DS()
 }
diff --git a/go/lib/healthpool/svcinstance/pool_test.go b/go/lib/healthpool/svcinstance/pool_test.go
index a4ffabae5..9a9b4be92 100644
--- a/go/lib/healthpool/svcinstance/pool_test.go
+++ b/go/lib/healthpool/svcinstance/pool_test.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/healthpool"
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/xtest"
 )
@@ -163,7 +162,7 @@ func mustLoadPool(t *testing.T) *Pool {
 }
 
 func mustLoadSvcInfo(t *testing.T) topology.IDAddrMap {
-	topo, err := itopo.LoadFromFile("testdata/topology.json")
+	topo, err := topology.FromJSONFile("testdata/topology.json")
 	xtest.FailOnErr(t, err)
 	return topo.DS()
 }
diff --git a/go/lib/infra/modules/itopo/itopo_test.go b/go/lib/infra/modules/itopo/itopo_test.go
index 8e2abf778..fa405fd52 100644
--- a/go/lib/infra/modules/itopo/itopo_test.go
+++ b/go/lib/infra/modules/itopo/itopo_test.go
@@ -28,10 +28,10 @@ import (
 	"github.com/scionproto/scion/go/proto"
 )
 
-type updateTestFunc func(*topology.Topo) (*topology.Topo, bool, error)
+type updateTestFunc func(*topology.RWTopology) (*topology.RWTopology, bool, error)
 
 func setStaticTestFunc(s *state) updateTestFunc {
-	return func(t *topology.Topo) (*topology.Topo, bool, error) {
+	return func(t *topology.RWTopology) (*topology.RWTopology, bool, error) {
 		return s.setStatic(t, false)
 	}
 }
@@ -148,7 +148,7 @@ func TestStateSetStatic(t *testing.T) {
 			})
 		})
 		Convey("Modifying the internal address is not allowed", func() {
-			topo.BR[id].InternalAddrs.Port = 42
+			topo.BR[id].InternalAddr.Port = 42
 			Convey("If semi-mutation is allowed", func() {
 				_, updated, err := s.setStatic(topo, true)
 				SoMsg("err", err, ShouldNotBeNil)
@@ -270,7 +270,7 @@ func TestStateSetDynamic(t *testing.T) {
 			SoMsg("topo", newTopo, ShouldEqual, topo)
 		})
 		Convey("Modifying the internal address is not allowed", func() {
-			topo.BR[id].InternalAddrs.Port = 42
+			topo.BR[id].InternalAddr.Port = 42
 			_, updated, err := s.setDynamic(topo)
 			SoMsg("err", err, ShouldNotBeNil)
 			SoMsg("updated", updated, ShouldBeFalse)
@@ -296,7 +296,7 @@ func testNilTopo(update updateTestFunc, t *testing.T) {
 	})
 }
 
-func testNoModified(update updateTestFunc, prevTopo *topology.Topo,
+func testNoModified(update updateTestFunc, prevTopo *topology.RWTopology,
 	clbks *mockClbks, updateCalled bool, t *testing.T) {
 
 	t.Helper()
diff --git a/go/lib/infra/modules/itopo/itopotest/BUILD.bazel b/go/lib/infra/modules/itopo/itopotest/BUILD.bazel
index a63c19350..29e8a4264 100644
--- a/go/lib/infra/modules/itopo/itopotest/BUILD.bazel
+++ b/go/lib/infra/modules/itopo/itopotest/BUILD.bazel
@@ -6,7 +6,6 @@ go_library(
     importpath = "github.com/scionproto/scion/go/lib/infra/modules/itopo/itopotest",
     visibility = ["//visibility:public"],
     deps = [
-        "//go/lib/infra/modules/itopo:go_default_library",
         "//go/lib/topology:go_default_library",
         "@com_github_stretchr_testify//require:go_default_library",
     ],
diff --git a/go/lib/infra/modules/itopo/itopotest/itopotest.go b/go/lib/infra/modules/itopo/itopotest/itopotest.go
index ea6c8dc78..53f905d08 100644
--- a/go/lib/infra/modules/itopo/itopotest/itopotest.go
+++ b/go/lib/infra/modules/itopo/itopotest/itopotest.go
@@ -19,25 +19,24 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/scionproto/scion/go/lib/infra/modules/itopo"
 	"github.com/scionproto/scion/go/lib/topology"
 )
 
 // TestTopoProvider is a provider for a specific topology object.
 type TestTopoProvider struct {
-	*topology.Topo
+	*topology.RWTopology
 }
 
 // TopoProviderFromFile creates a topo provider from a topology file.
 // It fails the test if loading the file fails.
 func TopoProviderFromFile(t *testing.T, fName string) *TestTopoProvider {
 	t.Helper()
-	topo, err := itopo.LoadFromFile(fName)
+	topo, err := topology.FromJSONFile(fName)
 	require.NoError(t, err)
-	return &TestTopoProvider{Topo: topo.Raw()}
+	return &TestTopoProvider{RWTopology: topo.Writable()}
 }
 
 // Get returns the stored topology.
-func (t *TestTopoProvider) Get() itopo.Topology {
-	return itopo.NewTopologyFromRaw(t.Topo)
+func (t *TestTopoProvider) Get() topology.Topology {
+	return topology.FromRWTopology(t.RWTopology)
 }
diff --git a/go/lib/infra/modules/itopo/validate_test.go b/go/lib/infra/modules/itopo/validate_test.go
index 176a37e97..a1257231e 100644
--- a/go/lib/infra/modules/itopo/validate_test.go
+++ b/go/lib/infra/modules/itopo/validate_test.go
@@ -22,7 +22,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/proto"
 )
 
@@ -108,7 +107,7 @@ 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() {
-			topo.BR[other].InternalAddrs.Port = 42
+			topo.BR[other].InternalAddr.Port = 42
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldBeNil)
 		})
 		Convey("Modifying a different br's control address is allowed", func() {
@@ -116,7 +115,7 @@ func TestBrValidatorImmutable(t *testing.T) {
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldBeNil)
 		})
 		Convey("Modifying the own internal address is not allowed", func() {
-			topo.BR[v.id].InternalAddrs.Port = 42
+			topo.BR[v.id].InternalAddr.Port = 42
 			SoMsg("err", v.Immutable(topo, oldTopo), ShouldNotBeNil)
 		})
 		Convey("Modifying the own control address is not allowed", func() {
@@ -238,20 +237,16 @@ func TestBrValidatorMustDropDynamic(t *testing.T) {
 	})
 }
 
-func testGenImmutable(v internalValidator, topo, oldTopo *topology.Topo, t *testing.T) {
+func testGenImmutable(v internalValidator, topo, oldTopo *topology.RWTopology, t *testing.T) {
 	t.Helper()
 	Convey("Updating the IA is not allowed", func() {
-		topo.ISD_AS.I = 0
+		topo.IA.I = 0
 		SoMsg("err", v.Immutable(topo, oldTopo), ShouldNotBeNil)
 	})
 	Convey("Updating the core flag is not allowed", func() {
 		topo.Core = true
 		SoMsg("err", v.Immutable(topo, oldTopo), ShouldNotBeNil)
 	})
-	Convey("Updating the overlay is not allowed", func() {
-		topo.Overlay = overlay.IPv6
-		SoMsg("err", v.Immutable(topo, oldTopo), ShouldNotBeNil)
-	})
 	Convey("Updating the mtu is not allowed", func() {
 		topo.MTU = 42
 		SoMsg("err", v.Immutable(topo, oldTopo), ShouldNotBeNil)
@@ -261,9 +256,9 @@ func testGenImmutable(v internalValidator, topo, oldTopo *topology.Topo, t *test
 	})
 }
 
-func loadTopo(filename string, t *testing.T) *topology.Topo {
+func loadTopo(filename string, t *testing.T) *topology.RWTopology {
 	t.Helper()
-	topo, err := topology.LoadFromFile(filename)
+	topo, err := topology.RWTopologyFromJSONFile(filename)
 	if err != nil {
 		t.Fatalf("Error loading config from '%s': %v", filename, err)
 	}
diff --git a/go/lib/infra/modules/trust/trust_test.go b/go/lib/infra/modules/trust/trust_test.go
index 246b91856..660a65096 100644
--- a/go/lib/infra/modules/trust/trust_test.go
+++ b/go/lib/infra/modules/trust/trust_test.go
@@ -639,12 +639,11 @@ func initStore(t *testing.T, ctrl *gomock.Controller,
 	t.Helper()
 	db, err := trustdbsqlite.New(":memory:")
 	xtest.FailOnErr(t, err)
-	topo := topology.NewTopo()
-	topo.CSNames = append(topo.CSNames, "foo")
-	topo.CS["foo"] = topology.TopoAddr{}
+	topo := topology.NewRWTopology()
+	topo.CS = topology.IDAddrMap{"foo": topology.TopoAddr{}}
 	cfg := Config{
 		TopoProvider: &itopotest.TestTopoProvider{
-			Topo: topo,
+			RWTopology: topo,
 		},
 	}
 	store := NewStore(db, ia, cfg, log.Root())
diff --git a/go/lib/snet/writer_test.go b/go/lib/snet/writer_test.go
index 8ba7fdf3e..8c6e2706e 100644
--- a/go/lib/snet/writer_test.go
+++ b/go/lib/snet/writer_test.go
@@ -32,7 +32,7 @@ import (
 	"github.com/scionproto/scion/go/lib/snet/internal/ctxmonitor/mock_ctxmonitor"
 	"github.com/scionproto/scion/go/lib/snet/mock_snet"
 	"github.com/scionproto/scion/go/lib/spath"
-	"github.com/scionproto/scion/go/lib/topology"
+	"github.com/scionproto/scion/go/lib/topology/overlay"
 	"github.com/scionproto/scion/go/lib/xtest"
 )
 
@@ -107,7 +107,7 @@ func TestRemoteAddressResolver(t *testing.T) {
 			assert.NoError(t, err)
 			assert.NotNil(t, outAddress)
 			assert.Equal(t, outAddress.NextHop.IP, outAddress.Host.L3.IP(), "overlay addr")
-			assert.Equal(t, outAddress.NextHop.Port, topology.EndhostPort, "overlay port")
+			assert.Equal(t, outAddress.NextHop.Port, overlay.EndhostPort, "overlay port")
 		})
 	})
 	t.Run("if destination is not in local AS", func(t *testing.T) {
diff --git a/go/lib/topology/addr_test.go b/go/lib/topology/addr_test.go
deleted file mode 100644
index 2b2d38685..000000000
--- a/go/lib/topology/addr_test.go
+++ /dev/null
@@ -1,292 +0,0 @@
-// Copyright 2017 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 (
-	"net"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	"github.com/scionproto/scion/go/lib/addr"
-	"github.com/scionproto/scion/go/lib/xtest"
-)
-
-var (
-	pubIPv4    = &RawAddrPortOverlay{RawAddrPort{"192.168.1.1", 40000}, 0}
-	pubUDPIPv4 = &RawAddrPortOverlay{RawAddrPort{"192.168.1.1", 40001}, 30041}
-	bindIPv4   = &RawAddrPort{"127.0.0.1", 40002}
-	pubIPv6    = &RawAddrPortOverlay{RawAddrPort{"2001:db8:a0b:12f0::1", 60000}, 0}
-	pubUDPIPv6 = &RawAddrPortOverlay{RawAddrPort{"2001:db8:a0b:12f0::1", 60001}, 30041}
-	bindIPv6   = &RawAddrPort{"::1", 60002}
-	pubBad     = &RawAddrPortOverlay{RawAddrPort{"BadIPAddress", 40000}, 0}
-	bindBad    = &RawAddrPort{"BadIPAddress", 40000}
-)
-
-func TestRawAddrMap_ToTopoAddr(t *testing.T) {
-	testCases := []struct {
-		name string
-		err  error
-		ram  RawAddrMap
-		addr *TopoAddr
-	}{
-		{
-			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 _, 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)
-			}
-		})
-	}
-}
diff --git a/go/lib/topology/export_test.go b/go/lib/topology/export_test.go
new file mode 100644
index 000000000..212f3f210
--- /dev/null
+++ b/go/lib/topology/export_test.go
@@ -0,0 +1,24 @@
+// Copyright 2019 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
+
+var (
+	RawAddrMapToTopoAddr           = rawAddrMapToTopoAddr
+	RawAddrMapExtractAddressInfo   = rawAddrMapExtractAddressInfo
+	RawBRAddrMapToUDPAddr          = rawBRAddrMapToUDPAddr
+	RawBRAddrMapExtractAddressInfo = rawBRAddrMapExtractAddressInfo
+	RawBRIntfRemoteBRAddr          = rawBRIntfRemoteBRAddr
+	RawBRIntfTopoBRAddr            = rawBRIntfTopoBRAddr
+)
diff --git a/go/lib/topology/json/json_test.go b/go/lib/topology/json/json_test.go
new file mode 100644
index 000000000..958102a6e
--- /dev/null
+++ b/go/lib/topology/json/json_test.go
@@ -0,0 +1,175 @@
+// Copyright 2019 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 json_test
+
+import (
+	"encoding/json"
+	"flag"
+	"io/ioutil"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/scionproto/scion/go/lib/common"
+	jsontopo "github.com/scionproto/scion/go/lib/topology/json"
+	"github.com/scionproto/scion/go/lib/topology/overlay"
+)
+
+var (
+	update = flag.Bool("update", false, "set to true to update golden files")
+)
+
+func TestLoadRawFromFile(t *testing.T) {
+	referenceTopology := &jsontopo.Topology{
+		Timestamp:      168562800,
+		TimestampHuman: "May  6 00:00:00 CET 1975",
+		IA:             "6-ff00:0:362",
+		MTU:            1472,
+		Core:           true,
+		Overlay:        overlay.IPv46Name,
+		BorderRouters: map[string]*jsontopo.BRInfo{
+			"borderrouter6-f00:0:362-1": {
+				InternalAddrs: jsontopo.UnderlayAddressMap{
+					"IPv4": {
+						PublicUnderlay: jsontopo.UnderlayAddress{
+							Addr: "10.1.0.1",
+						},
+					},
+					"IPv6": {
+						PublicUnderlay: jsontopo.UnderlayAddress{
+							Addr: "2001:db8:a0b:12f0::1",
+						},
+					},
+				},
+				CtrlAddr: jsontopo.NATSCIONAddressMap{
+					"IPv4": &jsontopo.NATSCIONAddress{
+						Public: jsontopo.FullSCIONAddress{
+							Address: jsontopo.Address{
+								Addr:   "10.1.0.1",
+								L4Port: 30098,
+							},
+						},
+					},
+					"IPv6": &jsontopo.NATSCIONAddress{
+						Public: jsontopo.FullSCIONAddress{
+							Address: jsontopo.Address{
+								Addr:   "2001:db8:a0b:12f0::1",
+								L4Port: 30098,
+							},
+						},
+					},
+				},
+				Interfaces: map[common.IFIDType]*jsontopo.BRInterface{
+					91: {
+						Underlay: "UDP/IPv4",
+						BindUnderlay: &jsontopo.L3Address{
+							Addr: "10.0.0.1",
+						},
+						PublicUnderlay: &jsontopo.UnderlayAddress{
+							Addr:         "192.0.2.1",
+							UnderlayPort: 4997,
+						},
+						RemoteUnderlay: &jsontopo.UnderlayAddress{
+							Addr:         "192.0.2.2",
+							UnderlayPort: 4998,
+						},
+						Bandwidth: 100000,
+						IA:        "6-ff00:0:363",
+						LinkTo:    "CORE",
+						MTU:       1472,
+					},
+				},
+			},
+			"borderrouter6-f00:0:362-9": {
+				InternalAddrs: jsontopo.UnderlayAddressMap{
+					"IPv4": {
+						PublicUnderlay: jsontopo.UnderlayAddress{
+							Addr: "10.1.0.2",
+						},
+					},
+					"IPv6": {
+						PublicUnderlay: jsontopo.UnderlayAddress{
+							Addr: "2001:db8:a0b:12f0::2",
+						},
+					},
+				},
+				CtrlAddr: jsontopo.NATSCIONAddressMap{
+					"IPv4": &jsontopo.NATSCIONAddress{
+						Public: jsontopo.FullSCIONAddress{
+							Address: jsontopo.Address{
+								Addr:   "10.1.0.2",
+								L4Port: 30098,
+							},
+						},
+					},
+					"IPv6": &jsontopo.NATSCIONAddress{
+						Public: jsontopo.FullSCIONAddress{
+							Address: jsontopo.Address{
+								Addr:   "2001:db8:a0b:12f0::2",
+								L4Port: 30098,
+							},
+						},
+					},
+				},
+				Interfaces: map[common.IFIDType]*jsontopo.BRInterface{
+					32: {
+						Underlay: "UDP/IPv6",
+						BindUnderlay: &jsontopo.L3Address{
+							Addr: "2001:db8:a0b:12f0::8",
+						},
+						PublicUnderlay: &jsontopo.UnderlayAddress{
+							Addr:         "2001:db8:a0b:12f0::1",
+							UnderlayPort: 4997,
+						},
+						RemoteUnderlay: &jsontopo.UnderlayAddress{
+							Addr:         "2001:db8:a0b:12f0::2",
+							UnderlayPort: 4998,
+						},
+						Bandwidth: 5000,
+						IA:        "6-ff00:0:364",
+						LinkTo:    "CHILD",
+						MTU:       4430,
+					},
+				},
+			},
+		},
+	}
+
+	if *update {
+		b, err := json.MarshalIndent(referenceTopology, "", "    ")
+		require.NoError(t, err)
+		b = append(b, []byte("\n")...)
+		err = ioutil.WriteFile("testdata/topology.json", b, 0644)
+		require.NoError(t, err)
+	}
+
+	t.Run("unmarshaled struct matches", func(t *testing.T) {
+		loadedTopology, err := jsontopo.LoadFromFile("testdata/topology.json")
+		assert.NoError(t, err)
+		assert.Equal(t, referenceTopology, loadedTopology)
+	})
+	t.Run("marshaled bytes match", func(t *testing.T) {
+		referenceTopologyBytes, err := ioutil.ReadFile("testdata/topology.json")
+		require.NoError(t, err)
+		topologyBytes, err := json.MarshalIndent(referenceTopology, "", "    ")
+		require.NoError(t, err)
+		assert.Equal(t,
+			strings.TrimSpace(string(referenceTopologyBytes)),
+			strings.TrimSpace(string(topologyBytes)),
+		)
+	})
+}
diff --git a/go/lib/topology/json/testdata/topology.json b/go/lib/topology/json/testdata/topology.json
new file mode 100644
index 000000000..b41c30bcc
--- /dev/null
+++ b/go/lib/topology/json/testdata/topology.json
@@ -0,0 +1,107 @@
+{
+    "Timestamp": 168562800,
+    "TimestampHuman": "May  6 00:00:00 CET 1975",
+    "TTL": 0,
+    "ISD_AS": "6-ff00:0:362",
+    "Overlay": "IPv4+6",
+    "MTU": 1472,
+    "Core": true,
+    "BorderRouters": {
+        "borderrouter6-f00:0:362-1": {
+            "InternalAddrs": {
+                "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
+                    }
+                }
+            },
+            "Interfaces": {
+                "91": {
+                    "Overlay": "UDP/IPv4",
+                    "PublicOverlay": {
+                        "Addr": "192.0.2.1",
+                        "OverlayPort": 4997
+                    },
+                    "BindOverlay": {
+                        "Addr": "10.0.0.1"
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "192.0.2.2",
+                        "OverlayPort": 4998
+                    },
+                    "Bandwidth": 100000,
+                    "ISD_AS": "6-ff00:0:363",
+                    "LinkTo": "CORE",
+                    "MTU": 1472
+                }
+            }
+        },
+        "borderrouter6-f00:0:362-9": {
+            "InternalAddrs": {
+                "IPv4": {
+                    "PublicOverlay": {
+                        "Addr": "10.1.0.2"
+                    }
+                },
+                "IPv6": {
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::2"
+                    }
+                }
+            },
+            "CtrlAddr": {
+                "IPv4": {
+                    "Public": {
+                        "Addr": "10.1.0.2",
+                        "L4Port": 30098
+                    }
+                },
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:a0b:12f0::2",
+                        "L4Port": 30098
+                    }
+                }
+            },
+            "Interfaces": {
+                "32": {
+                    "Overlay": "UDP/IPv6",
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::1",
+                        "OverlayPort": 4997
+                    },
+                    "BindOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::8"
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::2",
+                        "OverlayPort": 4998
+                    },
+                    "Bandwidth": 5000,
+                    "ISD_AS": "6-ff00:0:364",
+                    "LinkTo": "CHILD",
+                    "MTU": 4430
+                }
+            }
+        }
+    }
+}
diff --git a/go/lib/topology/topology_test.go b/go/lib/topology/topology_test.go
index d30354a5c..7151a5a46 100644
--- a/go/lib/topology/topology_test.go
+++ b/go/lib/topology/topology_test.go
@@ -16,7 +16,6 @@
 package topology
 
 import (
-	"encoding/json"
 	"fmt"
 	"net"
 	"testing"
@@ -28,20 +27,16 @@ import (
 
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
+	jsontopo "github.com/scionproto/scion/go/lib/topology/json"
 	"github.com/scionproto/scion/go/lib/topology/overlay"
 	"github.com/scionproto/scion/go/lib/xtest"
-	"github.com/scionproto/scion/go/proto"
 )
 
 func TestMeta(t *testing.T) {
 	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",
-		"Field 'TimestampHuman'")
 	assert.Equal(t, time.Hour, c.TTL, "Field 'TTL'")
-	assert.Equal(t, addr.IA{I: 1, A: 0xff0000000311}, c.ISD_AS, "Field 'ISD_AS'")
-	assert.Equal(t, overlay.IPv46, c.Overlay, "Field 'Overlay'")
+	assert.Equal(t, addr.IA{I: 1, A: 0xff0000000311}, c.IA, "Field 'ISD_AS'")
 	assert.Equal(t, 1472, c.MTU, "Field 'MTU'")
 	assert.False(t, c.Core, "Field 'Core'")
 }
@@ -132,9 +127,9 @@ func TestIFInfoMap(t *testing.T) {
 	c := MustLoadTopo(t, "testdata/basic.json")
 	ifm := IfInfoMap{
 		1: IFInfo{
-			Id:     1,
+			ID:     1,
 			BRName: "br1-ff00:0:311-1",
-			InternalAddrs: &net.UDPAddr{
+			InternalAddr: &net.UDPAddr{
 				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
 				Port: 0,
 			},
@@ -145,7 +140,7 @@ func TestIFInfoMap(t *testing.T) {
 				},
 				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::1"), Port: 30041},
 			},
-			Overlay: overlay.UDPIPv4,
+			Underlay: overlay.UDPIPv4,
 			Local: &net.UDPAddr{
 				IP:   net.IP{10, 0, 0, 1},
 				Port: 44997,
@@ -155,14 +150,14 @@ func TestIFInfoMap(t *testing.T) {
 				Port: 44998,
 			},
 			Bandwidth: 1000,
-			ISD_AS:    xtest.MustParseIA("1-ff00:0:312"),
-			LinkType:  proto.LinkType_parent,
+			IA:        xtest.MustParseIA("1-ff00:0:312"),
+			LinkType:  Parent,
 			MTU:       1472,
 		},
 		3: IFInfo{
-			Id:     3,
+			ID:     3,
 			BRName: "br1-ff00:0:311-1",
-			InternalAddrs: &net.UDPAddr{
+			InternalAddr: &net.UDPAddr{
 				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
 				Port: 0,
 			},
@@ -173,7 +168,7 @@ func TestIFInfoMap(t *testing.T) {
 				},
 				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::1"), Port: 30041},
 			},
-			Overlay: overlay.UDPIPv6,
+			Underlay: overlay.UDPIPv6,
 			Local: &net.UDPAddr{
 				IP:   net.ParseIP("2001:db8:a0b:12f0::8"),
 				Port: 44997,
@@ -183,14 +178,14 @@ func TestIFInfoMap(t *testing.T) {
 				Port: 44998,
 			},
 			Bandwidth: 5000,
-			ISD_AS:    xtest.MustParseIA("1-ff00:0:314"),
-			LinkType:  proto.LinkType_child,
+			IA:        xtest.MustParseIA("1-ff00:0:314"),
+			LinkType:  Child,
 			MTU:       4430,
 		},
 		8: IFInfo{
-			Id:     8,
+			ID:     8,
 			BRName: "br1-ff00:0:311-1",
-			InternalAddrs: &net.UDPAddr{
+			InternalAddr: &net.UDPAddr{
 				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
 				Port: 0,
 			},
@@ -201,7 +196,7 @@ func TestIFInfoMap(t *testing.T) {
 				},
 				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::1"), Port: 30041},
 			},
-			Overlay: overlay.UDPIPv4,
+			Underlay: overlay.UDPIPv4,
 			Local: &net.UDPAddr{
 				IP:   net.IP{10, 0, 0, 2},
 				Port: 44997,
@@ -211,8 +206,8 @@ func TestIFInfoMap(t *testing.T) {
 				Port: 44998,
 			},
 			Bandwidth: 2000,
-			ISD_AS:    xtest.MustParseIA("1-ff00:0:313"),
-			LinkType:  proto.LinkType_peer,
+			IA:        xtest.MustParseIA("1-ff00:0:313"),
+			LinkType:  Peer,
 			MTU:       1480,
 		},
 	}
@@ -223,9 +218,9 @@ func TestIFInfoMapCoreAS(t *testing.T) {
 	c := MustLoadTopo(t, "testdata/core.json")
 	ifm := IfInfoMap{
 		91: IFInfo{
-			Id:     91,
+			ID:     91,
 			BRName: "borderrouter6-ff00:0:362-1",
-			InternalAddrs: &net.UDPAddr{
+			InternalAddr: &net.UDPAddr{
 				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
 				Port: 0,
 			},
@@ -236,7 +231,7 @@ func TestIFInfoMapCoreAS(t *testing.T) {
 				},
 				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::1"), Port: 30041},
 			},
-			Overlay: overlay.UDPIPv4,
+			Underlay: overlay.UDPIPv4,
 			Local: &net.UDPAddr{
 				IP:   net.IP{10, 0, 0, 1},
 				Port: 4997,
@@ -246,14 +241,14 @@ func TestIFInfoMapCoreAS(t *testing.T) {
 				Port: 4998,
 			},
 			Bandwidth: 100000,
-			ISD_AS:    xtest.MustParseIA("6-ff00:0:363"),
-			LinkType:  proto.LinkType_core,
+			IA:        xtest.MustParseIA("6-ff00:0:363"),
+			LinkType:  Core,
 			MTU:       1472,
 		},
 		32: IFInfo{
-			Id:     32,
+			ID:     32,
 			BRName: "borderrouter6-ff00:0:362-9",
-			InternalAddrs: &net.UDPAddr{
+			InternalAddr: &net.UDPAddr{
 				IP:   net.ParseIP("2001:db8:a0b:12f0::2"),
 				Port: 0,
 			},
@@ -264,7 +259,7 @@ func TestIFInfoMapCoreAS(t *testing.T) {
 				},
 				UnderlayAddress: &net.UDPAddr{IP: net.ParseIP("2001:db8:a0b:12f0::2"), Port: 30041},
 			},
-			Overlay: overlay.UDPIPv6,
+			Underlay: overlay.UDPIPv6,
 			Local: &net.UDPAddr{
 				IP:   net.ParseIP("2001:db8:a0b:12f0::8"),
 				Port: 4997,
@@ -274,8 +269,8 @@ func TestIFInfoMapCoreAS(t *testing.T) {
 				Port: 4998,
 			},
 			Bandwidth: 5000,
-			ISD_AS:    xtest.MustParseIA("6-ff00:0:364"),
-			LinkType:  proto.LinkType_child,
+			IA:        xtest.MustParseIA("6-ff00:0:364"),
+			LinkType:  Child,
 			MTU:       4430,
 		},
 	}
@@ -309,53 +304,25 @@ func TestBRsCoreAS(t *testing.T) {
 	})
 }
 
-func TestTopoFromStripped(t *testing.T) {
-	fn := "testdata/basic.json"
-	rt, err := LoadRawFromFile(fn)
-	require.NoError(t, err, "Error loading raw topo from '%s': %v", fn, err)
-	Convey("Check that stripped bind topology can be parsed", t, func() {
-		StripBind(rt)
-		b, err := json.Marshal(rt)
-		SoMsg("errPack", err, ShouldBeNil)
-		_, err = Load(b)
-		SoMsg("errParse", err, ShouldBeNil)
-	})
-	Convey("Check that stripped svc topology can be parsed", t, func() {
-		StripServices(rt)
-		b, err := json.Marshal(rt)
-		SoMsg("errPack", err, ShouldBeNil)
-		_, err = Load(b)
-		SoMsg("errParse", err, ShouldBeNil)
-	})
-	Convey("Check that stripped topology can be parsed", t, func() {
-		StripBind(rt)
-		StripServices(rt)
-		b, err := json.Marshal(rt)
-		SoMsg("errPack", err, ShouldBeNil)
-		_, err = Load(b)
-		SoMsg("errParse", err, ShouldBeNil)
-	})
-}
-
 func TestInternalDataPlanePort(t *testing.T) {
 	testCases := []struct {
 		Name            string
-		Map             RawBRAddrMap
+		Map             jsontopo.UnderlayAddressMap
 		ExpectedAddress *net.UDPAddr
 		ExpectedError   assert.ErrorAssertionFunc
 	}{
 		{
 			Name:          "Empty",
-			Map:           RawBRAddrMap{},
+			Map:           jsontopo.UnderlayAddressMap{},
 			ExpectedError: assert.Error,
 		},
 		{
 			Name: "Bad IPv4 only",
-			Map: RawBRAddrMap{
-				"IPv4": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "foo",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv4": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "foo",
+						UnderlayPort: 42,
 					},
 				},
 			},
@@ -363,11 +330,11 @@ func TestInternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "Good IPv4 only",
-			Map: RawBRAddrMap{
-				"IPv4": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "127.0.0.1",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv4": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "127.0.0.1",
+						UnderlayPort: 42,
 					},
 				},
 			},
@@ -379,11 +346,11 @@ func TestInternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv4 contains IPv6",
-			Map: RawBRAddrMap{
-				"IPv4": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "::1",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv4": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "::1",
+						UnderlayPort: 42,
 					},
 				},
 			},
@@ -391,13 +358,13 @@ func TestInternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv4 with bind underlay",
-			Map: RawBRAddrMap{
-				"IPv4": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "127.0.0.1",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv4": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "127.0.0.1",
+						UnderlayPort: 42,
 					},
-					BindOverlay: &RawAddr{
+					BindUnderlay: &jsontopo.L3Address{
 						Addr: "127.255.255.255",
 					},
 				},
@@ -406,11 +373,11 @@ func TestInternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "Bad IPv6 only",
-			Map: RawBRAddrMap{
-				"IPv6": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "foo",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv6": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "foo",
+						UnderlayPort: 42,
 					},
 				},
 			},
@@ -418,11 +385,11 @@ func TestInternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "Good IPv6 only",
-			Map: RawBRAddrMap{
-				"IPv6": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "::1",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv6": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "::1",
+						UnderlayPort: 42,
 					},
 				},
 			},
@@ -434,11 +401,11 @@ func TestInternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv6 contains IPv4",
-			Map: RawBRAddrMap{
-				"IPv6": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "127.0.0.1",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv6": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "127.0.0.1",
+						UnderlayPort: 42,
 					},
 				},
 			},
@@ -446,13 +413,13 @@ func TestInternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv6 with bind underlay",
-			Map: RawBRAddrMap{
-				"IPv6": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "::1",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv6": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "::1",
+						UnderlayPort: 42,
 					},
-					BindOverlay: &RawAddr{
+					BindUnderlay: &jsontopo.L3Address{
 						Addr: "2001:db8::1",
 					},
 				},
@@ -461,17 +428,17 @@ func TestInternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "Prefer IPv6 to IPv4",
-			Map: RawBRAddrMap{
-				"IPv4": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "127.0.0.1",
-						OverlayPort: 42,
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv4": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "127.0.0.1",
+						UnderlayPort: 42,
 					},
 				},
-				"IPv6": &RawOverlayBind{
-					PublicOverlay: RawAddrOverlay{
-						Addr:        "::1",
-						OverlayPort: 73,
+				"IPv6": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "::1",
+						UnderlayPort: 73,
 					},
 				},
 			},
@@ -484,7 +451,7 @@ func TestInternalDataPlanePort(t *testing.T) {
 	}
 	for _, tc := range testCases {
 		t.Run(tc.Name, func(t *testing.T) {
-			topoBRAddr, err := tc.Map.ToUDPAddr()
+			topoBRAddr, err := RawBRAddrMapToUDPAddr(tc.Map)
 			tc.ExpectedError(t, err)
 			assert.Equal(t, tc.ExpectedAddress, topoBRAddr)
 		})
@@ -494,40 +461,40 @@ func TestInternalDataPlanePort(t *testing.T) {
 func TestExternalDataPlanePort(t *testing.T) {
 	testCases := []struct {
 		Name            string
-		Raw             *RawBRIntf
+		Raw             *jsontopo.BRInterface
 		ExpectedAddress *net.UDPAddr
 		ExpectedError   assert.ErrorAssertionFunc
 	}{
 		{
 			Name:          "Empty",
-			Raw:           &RawBRIntf{},
+			Raw:           &jsontopo.BRInterface{},
 			ExpectedError: assert.Error,
 		},
 		{
 			Name: "Empty with overlay",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv4",
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv4",
 			},
 			ExpectedError: assert.Error,
 		},
 		{
 			Name: "Bad IPv4 only",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv4",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "foo",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv4",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "foo",
+					UnderlayPort: 42,
 				},
 			},
 			ExpectedError: assert.Error,
 		},
 		{
 			Name: "Good IPv4 only",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv4",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "127.0.0.1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv4",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "127.0.0.1",
+					UnderlayPort: 42,
 				},
 			},
 			ExpectedError: assert.NoError,
@@ -538,24 +505,24 @@ func TestExternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv4 contains IPv6",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv4",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "::1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv4",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "::1",
+					UnderlayPort: 42,
 				},
 			},
 			ExpectedError: assert.Error,
 		},
 		{
 			Name: "IPv4 with bind underlay",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv4",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "127.0.0.1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv4",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "127.0.0.1",
+					UnderlayPort: 42,
 				},
-				BindOverlay: &RawAddr{
+				BindUnderlay: &jsontopo.L3Address{
 					Addr: "127.255.255.255",
 				},
 			},
@@ -567,13 +534,13 @@ func TestExternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv4 with bad underlay",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv4",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "127.0.0.1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv4",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "127.0.0.1",
+					UnderlayPort: 42,
 				},
-				BindOverlay: &RawAddr{
+				BindUnderlay: &jsontopo.L3Address{
 					Addr: "foo",
 				},
 			},
@@ -581,13 +548,13 @@ func TestExternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv4 with IPv6 underlay",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv4",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "127.0.0.1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv4",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "127.0.0.1",
+					UnderlayPort: 42,
 				},
-				BindOverlay: &RawAddr{
+				BindUnderlay: &jsontopo.L3Address{
 					Addr: "::1",
 				},
 			},
@@ -595,22 +562,22 @@ func TestExternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "Bad IPv6 only",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv6",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "foo",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv6",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "foo",
+					UnderlayPort: 42,
 				},
 			},
 			ExpectedError: assert.Error,
 		},
 		{
 			Name: "Good IPv6 only",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv6",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "::1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv6",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "::1",
+					UnderlayPort: 42,
 				},
 			},
 			ExpectedError: assert.NoError,
@@ -621,24 +588,24 @@ func TestExternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv6 contains IPv4",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv6",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "127.0.0.1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv6",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "127.0.0.1",
+					UnderlayPort: 42,
 				},
 			},
 			ExpectedError: assert.Error,
 		},
 		{
 			Name: "IPv6 with bind underlay",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv6",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "::1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv6",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "::1",
+					UnderlayPort: 42,
 				},
-				BindOverlay: &RawAddr{
+				BindUnderlay: &jsontopo.L3Address{
 					Addr: "2001:db8::1",
 				},
 			},
@@ -650,13 +617,13 @@ func TestExternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv6 with bad underlay",
-			Raw: &RawBRIntf{
-				Overlay: "UDP/IPv6",
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "::1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv6",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "::1",
+					UnderlayPort: 42,
 				},
-				BindOverlay: &RawAddr{
+				BindUnderlay: &jsontopo.L3Address{
 					Addr: "foo",
 				},
 			},
@@ -664,12 +631,12 @@ func TestExternalDataPlanePort(t *testing.T) {
 		},
 		{
 			Name: "IPv6 with IPv4 underlay",
-			Raw: &RawBRIntf{
-				PublicOverlay: &RawAddrOverlay{
-					Addr:        "::1",
-					OverlayPort: 42,
+			Raw: &jsontopo.BRInterface{
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "::1",
+					UnderlayPort: 42,
 				},
-				BindOverlay: &RawAddr{
+				BindUnderlay: &jsontopo.L3Address{
 					Addr: "127.0.0.1",
 				},
 			},
@@ -678,15 +645,271 @@ func TestExternalDataPlanePort(t *testing.T) {
 	}
 	for _, tc := range testCases {
 		t.Run(tc.Name, func(t *testing.T) {
-			topoBRAddr, err := tc.Raw.TopoBRAddr()
+			topoBRAddr, err := RawBRIntfTopoBRAddr(tc.Raw)
 			tc.ExpectedError(t, err)
 			assert.Equal(t, tc.ExpectedAddress, topoBRAddr)
 		})
 	}
 }
 
-func MustLoadTopo(t *testing.T, filename string) *Topo {
-	topo, err := LoadFromFile(filename)
+func TestRawAddrMap_ToTopoAddr(t *testing.T) {
+	testCases := []struct {
+		name string
+		err  error
+		ram  jsontopo.NATSCIONAddressMap
+		addr *TopoAddr
+	}{
+		{
+			name: "No addresses",
+			err:  errAtLeastOnePub,
+			ram:  make(jsontopo.NATSCIONAddressMap),
+			addr: nil,
+		},
+		{
+			name: "IPv4 invalid address",
+			err:  errInvalidPub,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv4": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "foo",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 empty address",
+			err:  errInvalidPub,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv4": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 address in IPv4 property",
+			err:  errInvalidPub,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv4": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "2001:db8:f00:b43::1",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 address in IPv6 property",
+			err:  errInvalidPub,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv6": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 invalid address",
+			err:  errInvalidPub,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv6": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "foo",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 empty address",
+			err:  errInvalidPub,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv6": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 good address",
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv4": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							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: jsontopo.NATSCIONAddressMap{
+				"IPv6": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							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: jsontopo.NATSCIONAddressMap{
+				"IPv4": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+					},
+					Bind: &jsontopo.Address{},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 with bind",
+			err:  errBindNotSupported,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv6": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "2001:db8:f00:b43::1",
+							L4Port: 42,
+						},
+					},
+					Bind: &jsontopo.Address{},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 with custom underlay",
+			err:  errCustomUnderlayPort,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv4": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+						UnderlayPort: 73,
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv6 with custom underlay",
+			err:  errCustomUnderlayPort,
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv6": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "2001:db8:f00:b43::1",
+							L4Port: 42,
+						},
+						UnderlayPort: 73,
+					},
+				},
+			},
+			addr: nil,
+		},
+		{
+			name: "IPv4 with IPv6",
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv4": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "192.168.1.1",
+							L4Port: 42,
+						},
+					},
+				},
+				"IPv6": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							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 _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			topoAddr, err := RawAddrMapToTopoAddr(tc.ram)
+			xtest.AssertErrorsIs(t, err, tc.err)
+			if tc.err == nil {
+				assert.Equal(t, tc.addr, topoAddr)
+			}
+		})
+	}
+}
+
+func MustLoadTopo(t *testing.T, filename string) *RWTopology {
+	topo, err := RWTopologyFromJSONFile(filename)
 	require.NoError(t, err, "Error loading config from '%s': %v", filename, err)
 	return topo
 }
diff --git a/go/lib/topology/types_test.go b/go/lib/topology/types_test.go
new file mode 100644
index 000000000..b1b588e41
--- /dev/null
+++ b/go/lib/topology/types_test.go
@@ -0,0 +1,33 @@
+// Copyright 2019 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_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/scionproto/scion/go/lib/topology"
+	"github.com/scionproto/scion/go/proto"
+)
+
+func TestLinkType(t *testing.T) {
+	// Protect LinkType from diverging from generated capnp data.
+	assert.Equal(t, topology.Unset, topology.LinkType(proto.LinkType_unset))
+	assert.Equal(t, topology.Core, topology.LinkType(proto.LinkType_core))
+	assert.Equal(t, topology.Parent, topology.LinkType(proto.LinkType_parent))
+	assert.Equal(t, topology.Child, topology.LinkType(proto.LinkType_child))
+	assert.Equal(t, topology.Peer, topology.LinkType(proto.LinkType_peer))
+}