Repository: scionproto/scion
Pull request #3725 (2020-04-21 11:02:04)

ipv6: support zones
This PR adds support for parsing host addresses that contain a IPv6 zone.

Also slightly cleans up the topology interface and add a copy method for RWTopology.

<!-- 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/3725)
<!-- Reviewable:end -->



Patch:
diff --git a/go/cs/main.go b/go/cs/main.go
index 8c44cc1f6..678eb5eda 100644
--- a/go/cs/main.go
+++ b/go/cs/main.go
@@ -321,10 +321,8 @@ func realMain() int {
 	}
 	// We do not need to drain the connection, since the src address is spoofed
 	// to contain the topo address.
-	a := topo.PublicAddress(addr.SvcBS, cfg.General.ID)
-	ohpAddress := &net.UDPAddr{
-		IP: append(a.IP[:0:0], a.IP...), Port: 0,
-	}
+	ohpAddress := snet.CopyUDPAddr(topo.PublicAddress(addr.SvcBS, cfg.General.ID))
+	ohpAddress.Port = 0
 	conn, _, err := pktDisp.Register(context.Background(), topo.IA(), ohpAddress, addr.SvcNone)
 	if err != nil {
 		log.Crit("Unable to create SCION packet conn", "err", err)
diff --git a/go/lib/overlay/conn/conn.go b/go/lib/overlay/conn/conn.go
index 23f0bf6cc..2063f8298 100644
--- a/go/lib/overlay/conn/conn.go
+++ b/go/lib/overlay/conn/conn.go
@@ -1,4 +1,5 @@
 // Copyright 2017 ETH Zurich
+// Copyright 2020 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.
@@ -287,6 +288,7 @@ func (c *connUDPBase) Read(b common.RawBytes) (int, *ReadMeta, error) {
 		c.readMeta.Src = &net.UDPAddr{
 			IP:   src.IP,
 			Port: src.Port,
+			Zone: src.Zone,
 		}
 	}
 	return n, &c.readMeta, err
@@ -388,6 +390,7 @@ func (m *ReadMeta) setSrc(a *net.UDPAddr, raddr *net.UDPAddr, ot overlay.Type) {
 		m.Src = &net.UDPAddr{
 			IP:   raddr.IP,
 			Port: raddr.Port,
+			Zone: raddr.Zone,
 		}
 	}
 }
diff --git a/go/lib/snet/path.go b/go/lib/snet/path.go
index 2b33a4d90..bfd78cf71 100644
--- a/go/lib/snet/path.go
+++ b/go/lib/snet/path.go
@@ -120,17 +120,7 @@ func (p *partialPath) Copy() Path {
 	}
 	return &partialPath{
 		spath:       p.spath.Copy(),
-		overlay:     copyUDP(p.overlay),
+		overlay:     CopyUDPAddr(p.overlay),
 		destination: p.destination,
 	}
 }
-
-func copyUDP(udp *net.UDPAddr) *net.UDPAddr {
-	if udp == nil {
-		return nil
-	}
-	return &net.UDPAddr{
-		IP:   append(udp.IP[:0:0], udp.IP...),
-		Port: udp.Port,
-	}
-}
diff --git a/go/lib/snet/udpaddr.go b/go/lib/snet/udpaddr.go
index 717510d39..115399d11 100644
--- a/go/lib/snet/udpaddr.go
+++ b/go/lib/snet/udpaddr.go
@@ -18,7 +18,6 @@ import (
 	"fmt"
 	"net"
 	"regexp"
-	"strconv"
 	"strings"
 
 	"github.com/scionproto/scion/go/lib/addr"
@@ -40,15 +39,18 @@ type UDPAddr struct {
 // The supported formats are:
 //
 // Recommended:
-//  - isd-as,ipv4:port   (e.g., 1-ff00:0:300,192.168.1.1:8080)
-//  - isd-as,[ipv6]:port (e.g., 1-ff00:0:300,[f00d::1337]:808)
+//  - isd-as,ipv4:port        (e.g., 1-ff00:0:300,192.168.1.1:8080)
+//  - isd-as,[ipv6]:port      (e.g., 1-ff00:0:300,[f00d::1337]:8080)
+//  - isd-as,[ipv6%zone]:port (e.g., 1-ff00:0:300,[f00d::1337%zone]:8080)
 //
 // Others:
-//  - isd-as,[ipv4]:port (e.g., 1-ff00:0:300,[192.168.1.1]:80)
+//  - isd-as,[ipv4]:port (e.g., 1-ff00:0:300,[192.168.1.1]:8080)
 //  - isd-as,[ipv4]      (e.g., 1-ff00:0:300,[192.168.1.1])
 //  - isd-as,[ipv6]      (e.g., 1-ff00:0:300,[f00d::1337])
+//  - isd-as,[ipv6%zone] (e.g., 1-ff00:0:300,[f00d::1337%zone])
 //  - isd-as,ipv4        (e.g., 1-ff00:0:300,192.168.1.1)
 //  - isd-as,ipv6        (e.g., 1-ff00:0:300,f00d::1337)
+//  - isd-as,ipv6%zone   (e.g., 1-ff00:0:300,f00d::1337%zone)
 //
 // Not supported:
 //  - isd-as,ipv6:port    (caveat if ipv6:port builds a valid ipv6 address,
@@ -62,24 +64,20 @@ func ParseUDPAddr(s string) (*UDPAddr, error) {
 	if err != nil {
 		return nil, serrors.WrapStr("invalid address: IA not parsable", err, "ia", ia)
 	}
-	// First check if host is an IP without a port.
-	if ip := net.ParseIP(strings.Trim(rawHost, "[]")); ip != nil {
-		return &UDPAddr{IA: ia, Host: &net.UDPAddr{IP: ip, Port: 0}}, nil
+	if ipOnly(rawHost) {
+		addr, err := net.ResolveIPAddr("ip", strings.Trim(rawHost, "[]"))
+		if err == nil && addr.IP != nil {
+			return &UDPAddr{IA: ia, Host: &net.UDPAddr{IP: addr.IP, Port: 0, Zone: addr.Zone}}, nil
+		}
 	}
-
-	rawIP, rawPort, err := net.SplitHostPort(rawHost)
+	udp, err := net.ResolveUDPAddr("udp", rawHost)
 	if err != nil {
-		return nil, err
+		return nil, serrors.WrapStr("invalid address: host not parsable", err, "host", rawHost)
 	}
-	ip := net.ParseIP(rawIP)
-	if ip == nil {
-		return nil, serrors.New("invalid address: no IP specified", "host", rawHost)
+	if udp.IP == nil {
+		return nil, serrors.WrapStr("invalid address: ip not specified", err, "host", rawHost)
 	}
-	port, err := strconv.ParseUint(rawPort, 10, 16)
-	if err != nil {
-		return nil, serrors.New("invalid port", "host", rawHost)
-	}
-	return &UDPAddr{IA: ia, Host: &net.UDPAddr{IP: ip, Port: int(port)}}, nil
+	return &UDPAddr{IA: ia, Host: udp}, nil
 }
 
 // Network implements net.Addr interface.
@@ -158,3 +156,9 @@ func parseAddr(s string) (string, string, error) {
 	}
 	return match[1], match[2], nil
 }
+
+func ipOnly(s string) bool {
+	ipv4NoPort := strings.Contains(s, ".") && !strings.Contains(s, ":")
+	inBracketsWithNoPort := !strings.Contains(s, "]:")
+	return ipv4NoPort || inBracketsWithNoPort
+}
diff --git a/go/lib/snet/writer.go b/go/lib/snet/writer.go
index 1b94ba5d9..a6b15dc59 100644
--- a/go/lib/snet/writer.go
+++ b/go/lib/snet/writer.go
@@ -63,7 +63,12 @@ func (c *scionConnWriter) WriteTo(b []byte, raddr net.Addr) (int, error) {
 			a.Host.Port, a.Path
 		nextHop = a.NextHop
 		if nextHop == nil && c.base.scionNet.localIA.Equal(a.IA) {
-			nextHop = &net.UDPAddr{IP: a.Host.IP, Port: overlay.EndhostPort}
+			nextHop = &net.UDPAddr{
+				IP:   a.Host.IP,
+				Port: overlay.EndhostPort,
+				Zone: a.Host.Zone,
+			}
+
 		}
 	case *SVCAddr:
 		dst, port, path = SCIONAddress{IA: a.IA, Host: a.SVC}, 0, a.Path
diff --git a/go/lib/topology/interface.go b/go/lib/topology/interface.go
index 2c8b2d32a..c20686181 100644
--- a/go/lib/topology/interface.go
+++ b/go/lib/topology/interface.go
@@ -15,6 +15,7 @@
 package topology
 
 import (
+	"math/rand"
 	"net"
 	"sort"
 
@@ -60,7 +61,9 @@ type Topology interface {
 	SBRAddress(name string) *snet.UDPAddr
 
 	// Anycast returns the address for an arbitrary server of the requested type.
-	Anycast(svc addr.HostSVC) (*net.TCPAddr, error)
+	Anycast(svc addr.HostSVC) (*net.UDPAddr, error)
+	// Multicast returns all addresses for the requested type.
+	Multicast(svc addr.HostSVC) ([]*net.UDPAddr, error)
 
 	// OverlayAnycast returns the overlay address for an arbitrary server of the requested type.
 	OverlayAnycast(svc addr.HostSVC) (*net.UDPAddr, error)
@@ -173,7 +176,7 @@ func (t *topologyS) OverlayNextHop(ifid common.IFIDType) (*net.UDPAddr, bool) {
 	if !ok {
 		return nil, false
 	}
-	return copyUDP(ifInfo.InternalAddr), true
+	return copyUDPAddr(ifInfo.InternalAddr), true
 }
 
 func (t *topologyS) OverlayNextHop2(ifid common.IFIDType) (*net.UDPAddr, bool) {
@@ -181,7 +184,7 @@ func (t *topologyS) OverlayNextHop2(ifid common.IFIDType) (*net.UDPAddr, bool) {
 	if !ok {
 		return nil, false
 	}
-	return copyUDP(ifInfo.InternalAddr), true
+	return copyUDPAddr(ifInfo.InternalAddr), true
 }
 
 func (t *topologyS) MakeHostInfos(st proto.ServiceType) []net.UDPAddr {
@@ -235,25 +238,33 @@ func (t *topologyS) topoAddress(svc addr.HostSVC, name string) *TopoAddr {
 	return addresses.GetByID(name)
 }
 
-func (t *topologyS) Anycast(svc addr.HostSVC) (*net.TCPAddr, error) {
-	names := t.SVCNames(svc)
-	name, err := names.GetRandom()
+func (t *topologyS) Anycast(svc addr.HostSVC) (*net.UDPAddr, error) {
+	addrs, err := t.Multicast(svc)
 	if err != nil {
 		return nil, err
 	}
+	return addrs[rand.Intn(len(addrs))], nil
+}
+
+func (t *topologyS) Multicast(svc addr.HostSVC) ([]*net.UDPAddr, error) {
+	names := t.SVCNames(svc)
 	st, err := toProtoServiceType(svc)
 	if err != nil {
 		return nil, err
 	}
-	topoAddr, err := t.Topology.GetTopoAddr(name, st)
-	if err != nil {
-		return nil, serrors.WrapStr("SVC not supported", err, "svc", svc)
+	addrs := make([]*net.UDPAddr, 0, len(names))
+	for _, name := range names {
+		topoAddr, err := t.Topology.GetTopoAddr(name, st)
+		if err != nil {
+			return nil, serrors.Wrap(addr.ErrUnsupportedSVCAddress, err, "svc", svc)
+		}
+		addrs = append(addrs, &net.UDPAddr{
+			IP:   topoAddr.SCIONAddress.IP,
+			Port: topoAddr.SCIONAddress.Port,
+			Zone: topoAddr.SCIONAddress.Zone,
+		})
 	}
-	return &net.TCPAddr{
-		IP:   topoAddr.SCIONAddress.IP,
-		Port: topoAddr.SCIONAddress.Port,
-		Zone: topoAddr.SCIONAddress.Zone,
-	}, nil
+	return addrs, nil
 }
 
 func (t *topologyS) OverlayAnycast(svc addr.HostSVC) (*net.UDPAddr, error) {
@@ -263,12 +274,12 @@ func (t *topologyS) OverlayAnycast(svc addr.HostSVC) (*net.UDPAddr, error) {
 		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",
+			return nil, serrors.WrapStr("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("Unsupported SVC address",
+		return nil, serrors.Wrap(addr.ErrUnsupportedSVCAddress,
 			scmp.NewError(scmp.C_Routing, scmp.T_R_BadHost, nil, nil), "svc", svc)
 	}
 	underlay, err := t.OverlayByName(svc, name)
@@ -292,7 +303,7 @@ func (t *topologyS) OverlayMulticast(svc addr.HostSVC) ([]*net.UDPAddr, error) {
 	}
 	topoAddrs, err := t.Topology.GetAllTopoAddrs(st)
 	if err != nil {
-		return nil, serrors.WrapStr("SVC not supported", err, "svc", svc)
+		return nil, serrors.Wrap(addr.ErrUnsupportedSVCAddress, err, "svc", svc)
 	}
 
 	if len(topoAddrs) == 0 {
@@ -327,7 +338,7 @@ func (t *topologyS) OverlayByName(svc addr.HostSVC, name string) (*net.UDPAddr,
 	}
 	topoAddr, err := t.Topology.GetTopoAddr(name, st)
 	if err != nil {
-		return nil, serrors.WrapStr("SVC not supported", err, "svc", svc)
+		return nil, serrors.Wrap(addr.ErrUnsupportedSVCAddress, err, "svc", svc)
 	}
 	overlayAddr := topoAddr.UnderlayAddr()
 	if overlayAddr == nil {
@@ -345,7 +356,7 @@ func toProtoServiceType(svc addr.HostSVC) (proto.ServiceType, error) {
 	default:
 		// 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 0, common.NewBasicError("Unsupported SVC address",
+		return 0, serrors.Wrap(addr.ErrUnsupportedSVCAddress,
 			scmp.NewError(scmp.C_Routing, scmp.T_R_BadHost, nil, nil), "svc", svc)
 	}
 }
@@ -390,17 +401,3 @@ func (t *topologyS) SVCNames(svc addr.HostSVC) ServiceNames {
 func (t *topologyS) Writable() *RWTopology {
 	return t.Topology
 }
-
-func copyIP(ip net.IP) net.IP {
-	return append(ip[:0:0], ip...)
-}
-
-func copyUDP(a *net.UDPAddr) *net.UDPAddr {
-	if a == nil {
-		return nil
-	}
-	return &net.UDPAddr{
-		IP:   copyIP(a.IP),
-		Port: a.Port,
-	}
-}
diff --git a/go/lib/topology/json/json.go b/go/lib/topology/json/json.go
index 0335e72b1..df72287e0 100644
--- a/go/lib/topology/json/json.go
+++ b/go/lib/topology/json/json.go
@@ -41,11 +41,10 @@ type Topology struct {
 	// We use the []trc.Attribute type so that we don't validate according to
 	// trc.Attributes, because that contains a length 0 check which is not
 	// suitable for topology.
-	Attributes       []trc.Attribute        `json:"Attributes"`
-	BorderRouters    map[string]*BRInfo     `json:"BorderRouters,omitempty"`
-	ZookeeperService map[int]*Address       `json:"ZookeeperService,omitempty"`
-	ControlService   map[string]*ServerInfo `json:"ControlService,omitempty"`
-	SIG              map[string]*ServerInfo `json:"SIG,omitempty"`
+	Attributes     []trc.Attribute        `json:"Attributes"`
+	BorderRouters  map[string]*BRInfo     `json:"BorderRouters,omitempty"`
+	ControlService map[string]*ServerInfo `json:"ControlService,omitempty"`
+	SIG            map[string]*ServerInfo `json:"SIG,omitempty"`
 }
 
 // ServerInfo contains the information for a SCION application running in the local AS.
diff --git a/go/lib/topology/mock_topology/topology.go b/go/lib/topology/mock_topology/topology.go
index a180b3d9b..f75191719 100644
--- a/go/lib/topology/mock_topology/topology.go
+++ b/go/lib/topology/mock_topology/topology.go
@@ -39,10 +39,10 @@ func (m *MockTopology) EXPECT() *MockTopologyMockRecorder {
 }
 
 // Anycast mocks base method
-func (m *MockTopology) Anycast(arg0 addr.HostSVC) (*net.TCPAddr, error) {
+func (m *MockTopology) Anycast(arg0 addr.HostSVC) (*net.UDPAddr, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Anycast", arg0)
-	ret0, _ := ret[0].(*net.TCPAddr)
+	ret0, _ := ret[0].(*net.UDPAddr)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
@@ -180,6 +180,21 @@ func (mr *MockTopologyMockRecorder) MakeHostInfos(arg0 interface{}) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeHostInfos", reflect.TypeOf((*MockTopology)(nil).MakeHostInfos), arg0)
 }
 
+// Multicast mocks base method
+func (m *MockTopology) Multicast(arg0 addr.HostSVC) ([]*net.UDPAddr, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Multicast", arg0)
+	ret0, _ := ret[0].([]*net.UDPAddr)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Multicast indicates an expected call of Multicast
+func (mr *MockTopologyMockRecorder) Multicast(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Multicast", reflect.TypeOf((*MockTopology)(nil).Multicast), arg0)
+}
+
 // OverlayAnycast mocks base method
 func (m *MockTopology) OverlayAnycast(arg0 addr.HostSVC) (*net.UDPAddr, error) {
 	m.ctrl.T.Helper()
diff --git a/go/lib/topology/raw.go b/go/lib/topology/raw.go
index a6aa98f4f..6b59ef0af 100644
--- a/go/lib/topology/raw.go
+++ b/go/lib/topology/raw.go
@@ -18,8 +18,6 @@ package topology
 import (
 	"net"
 
-	"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"
@@ -39,18 +37,21 @@ func rawAddrMapToTopoAddr(ram jsontopo.NATSCIONAddressMap) (*TopoAddr, error) {
 			"port", addressInfo.Public.UnderlayPort)
 	}
 
-	ip := net.ParseIP(addressInfo.Public.Address.Addr)
-	if ip == nil {
+	ipAddr, err := net.ResolveIPAddr("ip", addressInfo.Public.Address.Addr)
+	if err != nil {
+		return nil, serrors.Wrap(errInvalidPub, err, "address", addressInfo.Public.Address.Addr)
+	}
+	// This can happen if the address is empty.
+	if ipAddr.IP == nil {
 		return nil, serrors.WithCtx(errInvalidPub, "address", addressInfo.Public.Address.Addr)
 	}
 
-	if mustBeIPv6 && ip.To4() != nil {
+	if mustBeIPv6 && ipAddr.IP.To4() != nil {
 		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 {
+		if ipAddr.IP = ipAddr.IP.To4(); ipAddr.IP == nil {
 			return nil, serrors.WithCtx(errInvalidPub,
 				"address", addressInfo.Public.Address.Addr)
 		}
@@ -58,12 +59,14 @@ func rawAddrMapToTopoAddr(ram jsontopo.NATSCIONAddressMap) (*TopoAddr, error) {
 
 	return &TopoAddr{
 		SCIONAddress: &net.UDPAddr{
-			IP:   ip,
+			IP:   ipAddr.IP,
 			Port: addressInfo.Public.Address.L4Port,
+			Zone: ipAddr.Zone,
 		},
 		UnderlayAddress: &net.UDPAddr{
-			IP:   append(ip[:0:0], ip...),
+			IP:   append(ipAddr.IP[:0:0], ipAddr.IP...),
 			Port: EndhostPort,
+			Zone: ipAddr.Zone,
 		},
 	}, nil
 }
@@ -92,25 +95,29 @@ func rawBRAddrMapToUDPAddr(m jsontopo.UnderlayAddressMap) (*net.UDPAddr, error)
 		return nil, serrors.WithCtx(errBindNotSupported, "address", addressInfo.BindUnderlay)
 	}
 
-	ip := net.ParseIP(addressInfo.PublicUnderlay.Addr)
-	if ip == nil {
+	ipAddr, err := net.ResolveIPAddr("ip", addressInfo.PublicUnderlay.Addr)
+	if err != nil {
+		return nil, serrors.Wrap(errInvalidPub, err, "address", addressInfo.PublicUnderlay.Addr)
+	}
+	// This can happen if the address is empty.
+	if ipAddr.IP == nil {
 		return nil, serrors.WithCtx(errInvalidPub, "address", addressInfo.PublicUnderlay.Addr)
 	}
 
-	if mustBeIPv6 && ip.To4() != nil {
+	if mustBeIPv6 && ipAddr.IP.To4() != nil {
 		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 {
+		if ipAddr.IP = ipAddr.IP.To4(); ipAddr.IP == nil {
 			return nil, serrors.WithCtx(errInvalidPub, "address", addressInfo.PublicUnderlay.Addr)
 		}
 	}
 
 	return &net.UDPAddr{
-		IP:   append(ip[:0:0], ip...),
+		IP:   append(ipAddr.IP[:0:0], ipAddr.IP...),
 		Port: addressInfo.PublicUnderlay.UnderlayPort,
+		Zone: ipAddr.Zone,
 	}, nil
 }
 
@@ -129,15 +136,26 @@ func rawBRAddrMapExtractAddressInfo(
 }
 
 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.RemoteUnderlay.Addr)
+	l3, err := net.ResolveIPAddr("ip", b.RemoteUnderlay.Addr)
+	if err != nil {
+		return nil, serrors.WrapStr("could not parse remote IP from string", err,
+			"input", b.RemoteUnderlay.Addr)
+	}
+	if l3.IP == nil {
+		return nil, serrors.New("empty remote IP", "input", b.RemoteUnderlay.Addr)
 	}
 	if !o.IsUDP() && (b.RemoteUnderlay.UnderlayPort != 0) {
 		return nil, serrors.WithCtx(errUnderlayPort, "addr", b.RemoteUnderlay)
 	}
-	return &net.UDPAddr{IP: l3.IP(), Port: b.RemoteUnderlay.UnderlayPort}, nil
+	// Convert to 4-byte format to simplify testing
+	if ipv4 := l3.IP.To4(); ipv4 != nil {
+		l3.IP = ipv4
+	}
+	return &net.UDPAddr{
+		IP:   l3.IP,
+		Port: b.RemoteUnderlay.UnderlayPort,
+		Zone: l3.Zone,
+	}, nil
 }
 
 func rawBRIntfTopoBRAddr(i *jsontopo.BRInterface) (*net.UDPAddr, error) {
@@ -148,30 +166,37 @@ func rawBRIntfTopoBRAddr(i *jsontopo.BRInterface) (*net.UDPAddr, error) {
 	mustBeIPv4 := i.Underlay == "UDP/IPv4"
 	mustBeIPv6 := i.Underlay == "UDP/IPv6"
 
-	var udpAddr net.UDPAddr
+	var input string
 	if i.BindUnderlay != nil {
-		udpAddr.IP = net.ParseIP(i.BindUnderlay.Addr)
+		input = i.BindUnderlay.Addr
+	} else if i.PublicUnderlay != nil {
+		input = i.PublicUnderlay.Addr
 	} else {
-		if i.PublicUnderlay == nil {
-			return nil, serrors.WithCtx(errUnderlayAddrNotFound, "underlay", i.Underlay)
-		}
-		udpAddr.IP = net.ParseIP(i.PublicUnderlay.Addr)
-	}
-	if i.PublicUnderlay != nil {
-		udpAddr.Port = i.PublicUnderlay.UnderlayPort
+		return nil, serrors.WithCtx(errUnderlayAddrNotFound, "underlay", i.Underlay)
 	}
 
-	if udpAddr.IP == nil {
+	ipAddr, err := net.ResolveIPAddr("ip", input)
+	if err != nil {
+		return nil, serrors.WithCtx(err, "underlay", i.Underlay)
+	}
+	if ipAddr.IP == nil {
 		return nil, serrors.WithCtx(errInvalidPub, "address", i.PublicUnderlay.Addr)
 	}
+	udpAddr := &net.UDPAddr{
+		IP:   ipAddr.IP,
+		Zone: ipAddr.Zone,
+	}
+	if i.PublicUnderlay != nil {
+		udpAddr.Port = i.PublicUnderlay.UnderlayPort
+	}
 	if mustBeIPv4 {
-		udpAddr.IP = udpAddr.IP.To4()
-		if udpAddr.IP == nil {
+		// Convert to 4-byte format to simplify testing
+		if udpAddr.IP = udpAddr.IP.To4(); udpAddr.IP == nil {
 			return nil, serrors.WithCtx(errExpectedIPv4FoundIPv6, "address", i.PublicUnderlay.Addr)
 		}
 	}
 	if mustBeIPv6 && udpAddr.IP.To4() != nil {
 		return nil, serrors.WithCtx(errExpectedIPv6FoundIPv4, "address", i.PublicUnderlay.Addr)
 	}
-	return &udpAddr, nil
+	return udpAddr, nil
 }
diff --git a/go/lib/topology/topology.go b/go/lib/topology/topology.go
index bb7068fc4..2ba992f70 100644
--- a/go/lib/topology/topology.go
+++ b/go/lib/topology/topology.go
@@ -333,6 +333,73 @@ func (t *RWTopology) getSvcInfo(svc proto.ServiceType) (*svcInfo, error) {
 	}
 }
 
+// Copy creates a deep copy of the object.
+func (t *RWTopology) Copy() *RWTopology {
+	if t == nil {
+		return nil
+	}
+	return &RWTopology{
+		Timestamp:  t.Timestamp,
+		TTL:        t.TTL,
+		IA:         t.IA,
+		MTU:        t.MTU,
+		Attributes: append(t.Attributes[:0:0], t.Attributes...),
+
+		BR:        copyBRMap(t.BR),
+		BRNames:   append(t.BRNames[:0:0], t.BRNames...),
+		IFInfoMap: t.IFInfoMap.copy(),
+
+		CS:  t.CS.copy(),
+		SIG: t.SIG.copy(),
+	}
+}
+
+func copyBRMap(m map[string]BRInfo) map[string]BRInfo {
+	if m == nil {
+		return nil
+	}
+	newM := make(map[string]BRInfo)
+	for k, v := range m {
+		newM[k] = *v.copy()
+	}
+	return newM
+}
+
+func (i *BRInfo) copy() *BRInfo {
+	if i == nil {
+		return nil
+	}
+	return &BRInfo{
+		Name:         i.Name,
+		CtrlAddrs:    i.CtrlAddrs.copy(),
+		InternalAddr: copyUDPAddr(i.InternalAddr),
+		IFIDs:        append(i.IFIDs[:0:0], i.IFIDs...),
+		IFs:          copyIFsMap(i.IFs),
+	}
+}
+
+func copyIFsMap(m map[common.IFIDType]*IFInfo) map[common.IFIDType]*IFInfo {
+	if m == nil {
+		return nil
+	}
+	newM := make(map[common.IFIDType]*IFInfo)
+	for k, v := range m {
+		newM[k] = v.copy()
+	}
+	return newM
+}
+
+func (m IfInfoMap) copy() IfInfoMap {
+	if m == nil {
+		return nil
+	}
+	newM := make(IfInfoMap)
+	for k, v := range m {
+		newM[k] = *v.copy()
+	}
+	return newM
+}
+
 // svcInfo contains topology information for a single SCION service
 type svcInfo struct {
 	idTopoAddrMap IDAddrMap
@@ -368,6 +435,19 @@ func (m IDAddrMap) GetByID(id string) *TopoAddr {
 	return nil
 }
 
+func (m IDAddrMap) copy() IDAddrMap {
+	if m == nil {
+		return nil
+	}
+	newM := make(IDAddrMap)
+	for k, v := range m {
+		// This has the potential of making a _lot_ of shallow copies, but we can't really avoid it
+		// due to the value type in the map.
+		newM[k] = *v.copy()
+	}
+	return newM
+}
+
 // 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 {
@@ -394,6 +474,26 @@ func (i IFInfo) String() string {
 		i.Local, i.Remote, i.Bandwidth, i.IA, i.LinkType, i.MTU)
 }
 
+func (i *IFInfo) copy() *IFInfo {
+	if i == nil {
+		return nil
+	}
+	return &IFInfo{
+		ID:           i.ID,
+		BRName:       i.BRName,
+		CtrlAddrs:    i.CtrlAddrs.copy(),
+		Underlay:     i.Underlay,
+		InternalAddr: copyUDPAddr(i.InternalAddr),
+		Local:        copyUDPAddr(i.Local),
+		Remote:       copyUDPAddr(i.Remote),
+		RemoteIFID:   i.RemoteIFID,
+		Bandwidth:    i.Bandwidth,
+		IA:           i.IA,
+		LinkType:     i.LinkType,
+		MTU:          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
@@ -412,10 +512,7 @@ func (a *TopoAddr) copy() *TopoAddr {
 		return nil
 	}
 	return &TopoAddr{
-		SCIONAddress: &net.UDPAddr{
-			IP:   append(a.SCIONAddress.IP[:0:0], a.SCIONAddress.IP...),
-			Port: a.SCIONAddress.Port,
-		},
+		SCIONAddress:    copyUDPAddr(a.SCIONAddress),
 		UnderlayAddress: toUDPAddr(a.UnderlayAddress),
 	}
 }
@@ -428,10 +525,7 @@ func toUDPAddr(a net.Addr) *net.UDPAddr {
 	if !ok {
 		return nil
 	}
-	return &net.UDPAddr{
-		IP:   append(udpAddr.IP[:0:0], udpAddr.IP...),
-		Port: udpAddr.Port,
-	}
+	return copyUDPAddr(udpAddr)
 }
 
 // ServiceNames is a slice of process names (e.g., "bs-1", "bs-2").
@@ -446,6 +540,10 @@ func (s ServiceNames) GetRandom() (string, error) {
 	return s[rand.Intn(numServers)], nil
 }
 
+func (s ServiceNames) copy() ServiceNames {
+	return append(s[:0:0], s...)
+}
+
 func copyUDPAddr(a *net.UDPAddr) *net.UDPAddr {
 	if a == nil {
 		return nil
@@ -453,5 +551,6 @@ func copyUDPAddr(a *net.UDPAddr) *net.UDPAddr {
 	return &net.UDPAddr{
 		IP:   append(a.IP[:0:0], a.IP...),
 		Port: a.Port,
+		Zone: a.Zone,
 	}
 }


Test patch:
diff --git a/go/lib/snet/udpaddr_test.go b/go/lib/snet/udpaddr_test.go
index d6fef549d..c0ae4dc3b 100644
--- a/go/lib/snet/udpaddr_test.go
+++ b/go/lib/snet/udpaddr_test.go
@@ -58,11 +58,18 @@ func TestUDPAddrString(t *testing.T) {
 			},
 			want: "1-ff00:0:320,[2001::1]:20000",
 		},
+		"ipv6-zone": {
+			input: &snet.UDPAddr{
+				IA:   xtest.MustParseIA("1-ff00:0:320"),
+				Host: &net.UDPAddr{IP: net.ParseIP("2001::1"), Port: 20000, Zone: "some-zone"},
+			},
+			want: "1-ff00:0:320,[2001::1%some-zone]:20000",
+		},
 	}
 	for n, tc := range tests {
 		t.Run(n, func(t *testing.T) {
 			a := tc.input.String()
-			assert.Equal(t, a, tc.want)
+			assert.Equal(t, tc.want, a)
 		})
 	}
 }
@@ -104,6 +111,18 @@ func TestUDPAddrSet(t *testing.T) {
 				},
 			},
 		},
+		"ipv6-zone": {
+			Input: "1-ff00:0:1,[dead::beef%some-zone]:80",
+			Error: assert.NoError,
+			Want: &snet.UDPAddr{
+				IA: xtest.MustParseIA("1-ff00:0:1"),
+				Host: &net.UDPAddr{
+					IP:   net.ParseIP("dead::beef"),
+					Port: 80,
+					Zone: "some-zone",
+				},
+			},
+		},
 	}
 	for n, tc := range testCases {
 		t.Run(n, func(t *testing.T) {
@@ -124,6 +143,7 @@ func TestParseUDPAddr(t *testing.T) {
 		ia      string
 		host    string
 		port    int
+		zone    string
 	}{
 		{address: "foo", isError: true},
 		{address: "5-", isError: true},
@@ -132,7 +152,7 @@ func TestParseUDPAddr(t *testing.T) {
 		{address: "5-ff00:0:300,[127.0.0.1]:", isError: true},
 		{address: "40-ff00:0:300,[]:19", isError: true},
 		{address: "1-ff00:0:300,[]:13,[f", isError: true},
-		{address: "1-ff00:0:300,[abc]:12", isError: true},
+		{address: "1-ff00:0:300,[hostthatdoesnotexistforsure]:12", isError: true},
 		{address: "1-ff00:0:300]:14,[1.2.3.4]", isError: true},
 		{address: "1-ff00:0:300,[1.2.3.4]:70000", isError: true},
 		{address: "1-ff00:0:300,[1.2.3.4]]", isError: true},
@@ -163,6 +183,12 @@ func TestParseUDPAddr(t *testing.T) {
 			host: "::1",
 			port: 60000,
 		},
+		{address: "1-ff00:0:302,[::1%some-zone]:60000",
+			ia:   "1-ff00:0:302",
+			host: "::1",
+			port: 60000,
+			zone: "some-zone",
+		},
 		{address: "1-ff00:0:301,1.2.3.4",
 			ia:   "1-ff00:0:301",
 			host: "1.2.3.4",
@@ -183,6 +209,18 @@ func TestParseUDPAddr(t *testing.T) {
 			host: "::1",
 			port: 0,
 		},
+		{address: "1-ff00:0:302,[::1%some-zone]",
+			ia:   "1-ff00:0:302",
+			host: "::1",
+			port: 0,
+			zone: "some-zone",
+		},
+		{address: "1-ff00:0:302,::1%some-zone",
+			ia:   "1-ff00:0:302",
+			host: "::1",
+			port: 0,
+			zone: "some-zone",
+		},
 	}
 	for _, test := range tests {
 		t.Log(fmt.Sprintf("given address %q", test.address))
@@ -194,6 +232,7 @@ func TestParseUDPAddr(t *testing.T) {
 			assert.Equal(t, test.ia, a.IA.String())
 			assert.Equal(t, net.ParseIP(test.host), a.Host.IP)
 			assert.Equal(t, test.port, a.Host.Port)
+			assert.Equal(t, test.zone, a.Host.Zone)
 		}
 	}
 }
diff --git a/go/lib/topology/testdata/basic.json b/go/lib/topology/testdata/basic.json
index 374bbd097..8ae13ab43 100644
--- a/go/lib/topology/testdata/basic.json
+++ b/go/lib/topology/testdata/basic.json
@@ -90,6 +90,43 @@
                     "MTU": 1480
                 }
             }
+        },
+        "br1-ff00:0:311-2": {
+            "InternalAddrs": {
+                "IPv6": {
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::1%some-internal-zone"
+                    }
+                }
+            },
+            "CtrlAddr": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:a0b:12f0::1%some-ctrl-zone",
+                        "L4Port": 30098
+                    }
+                }
+            },
+            "Interfaces": {
+                "11": {
+                    "Overlay": "UDP/IPv6",
+                    "PublicOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::1%some-public-zone",
+                        "OverlayPort": 44897
+                    },
+                    "RemoteOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::2%some-remote-zone",
+                        "OverlayPort": 44898
+                    },
+                    "BindOverlay": {
+                        "Addr": "2001:db8:a0b:12f0::8%some-bind-zone"
+                    },
+                    "Bandwidth": 5000,
+                    "ISD_AS": "1-ff00:0:314",
+                    "LinkTo": "CHILD",
+                    "MTU": 4430
+                }
+            }
         }
     },
     "ControlService": {
@@ -112,6 +149,16 @@
                     }
                 }
             }
+        },
+        "cs1-ff00:0:311-4": {
+            "Addrs": {
+                "IPv6": {
+                    "Public": {
+                        "Addr": "2001:db8:f00:b43::1%some-zone",
+                        "L4Port": 23425
+                    }
+                }
+            }
         }
     },
     "SibraService": {
diff --git a/go/lib/topology/topology_test.go b/go/lib/topology/topology_test.go
index 8e852b2b5..5d4c2417c 100644
--- a/go/lib/topology/topology_test.go
+++ b/go/lib/topology/topology_test.go
@@ -66,23 +66,21 @@ func Test_BRs(t *testing.T) {
 		"br1-ff00:0:311-1": {
 			IFIDs: []common.IFIDType{1, 3, 8},
 		},
+		"br1-ff00:0:311-2": {
+			IFIDs: []common.IFIDType{11},
+		},
 	}
-	brn := []string{"br1-ff00:0:311-1"}
+	brn := []string{"br1-ff00:0:311-1", "br1-ff00:0:311-2"}
 
 	for name, info := range brs {
-		Convey(fmt.Sprintf("Checking BR details for %s", name), t, func() {
-			So(c.BR, ShouldContainKey, name)
+		t.Run("checking BR details for "+name, func(t *testing.T) {
 			for _, i := range info.IFIDs {
-				Convey(fmt.Sprintf("Checking if %s has interface with id %v", name, i), func() {
-					So(c.BR[name].IFIDs, ShouldContain, i)
-				})
+				assert.Contains(t, c.BR[name].IFIDs, i)
 			}
-			So(c.BRNames, ShouldResemble, brn)
+			assert.ElementsMatch(t, c.BRNames, brn)
 		})
 	}
-	Convey("Checking that BR map has no extra entries ", t, func() {
-		So(len(c.BR), ShouldEqual, len(brn))
-	})
+	assert.Len(t, c.BR, 2)
 }
 
 func TestServiceDetails(t *testing.T) {
@@ -108,6 +106,18 @@ func TestServiceDetails(t *testing.T) {
 				Port: 30041,
 			},
 		},
+		"cs1-ff00:0:311-4": TopoAddr{
+			SCIONAddress: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:f00:b43::1"),
+				Port: 23425,
+				Zone: "some-zone",
+			},
+			UnderlayAddress: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:f00:b43::1"),
+				Port: 30041,
+				Zone: "some-zone",
+			},
+		},
 	}
 	assert.Equal(t, cses, c.CS)
 }
@@ -117,7 +127,7 @@ func TestServiceCount(t *testing.T) {
 	// testing is done elsewhere
 	// The simple counting check for CS is done in the detailed population test as well
 	c := MustLoadTopo(t, "testdata/basic.json")
-	assert.Len(t, c.CS, 2, "CS")
+	assert.Len(t, c.CS, 3, "CS")
 	assert.Len(t, c.SIG, 2, "SIG")
 }
 
@@ -208,6 +218,42 @@ func TestIFInfoMap(t *testing.T) {
 			LinkType:  Peer,
 			MTU:       1480,
 		},
+		11: IFInfo{
+			ID:     11,
+			BRName: "br1-ff00:0:311-2",
+			InternalAddr: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
+				Port: 0,
+				Zone: "some-internal-zone",
+			},
+			CtrlAddrs: &TopoAddr{
+				SCIONAddress: &net.UDPAddr{
+					IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
+					Port: 30098,
+					Zone: "some-ctrl-zone",
+				},
+				UnderlayAddress: &net.UDPAddr{
+					IP:   net.ParseIP("2001:db8:a0b:12f0::1"),
+					Port: 30041,
+					Zone: "some-ctrl-zone",
+				},
+			},
+			Underlay: overlay.UDPIPv6,
+			Local: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::8"),
+				Port: 44897,
+				Zone: "some-bind-zone",
+			},
+			Remote: &net.UDPAddr{
+				IP:   net.ParseIP("2001:db8:a0b:12f0::2"),
+				Port: 44898,
+				Zone: "some-remote-zone",
+			},
+			Bandwidth: 5000,
+			IA:        xtest.MustParseIA("1-ff00:0:314"),
+			LinkType:  Child,
+			MTU:       4430,
+		},
 	}
 	assert.Equal(t, ifm, c.IFInfoMap)
 }
@@ -302,6 +348,15 @@ func TestBRsCoreAS(t *testing.T) {
 	})
 }
 
+func TestCopy(t *testing.T) {
+	topo, err := RWTopologyFromJSONFile("testdata/core.json")
+	require.NoError(t, err)
+
+	newTopo := topo.Copy()
+	assert.Equal(t, topo.BR, newTopo.BR)
+	assert.Equal(t, topo, newTopo)
+}
+
 func TestInternalDataPlanePort(t *testing.T) {
 	testCases := []struct {
 		Name            string
@@ -397,6 +452,23 @@ func TestInternalDataPlanePort(t *testing.T) {
 				Port: 42,
 			},
 		},
+		{
+			Name: "Good IPv6 only with zone",
+			Map: jsontopo.UnderlayAddressMap{
+				"IPv6": &jsontopo.NATUnderlayAddress{
+					PublicUnderlay: jsontopo.UnderlayAddress{
+						Addr:         "::1%some-zone",
+						UnderlayPort: 42,
+					},
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.ParseIP("::1"),
+				Port: 42,
+				Zone: "some-zone",
+			},
+		},
 		{
 			Name: "IPv6 contains IPv4",
 			Map: jsontopo.UnderlayAddressMap{
@@ -584,6 +656,22 @@ func TestExternalDataPlanePort(t *testing.T) {
 				Port: 42,
 			},
 		},
+		{
+			Name: "Good IPv6 only with zone",
+			Raw: &jsontopo.BRInterface{
+				Underlay: "UDP/IPv6",
+				PublicUnderlay: &jsontopo.UnderlayAddress{
+					Addr:         "::1%some-zone",
+					UnderlayPort: 42,
+				},
+			},
+			ExpectedError: assert.NoError,
+			ExpectedAddress: &net.UDPAddr{
+				IP:   net.ParseIP("::1"),
+				Port: 42,
+				Zone: "some-zone",
+			},
+		},
 		{
 			Name: "IPv6 contains IPv4",
 			Raw: &jsontopo.BRInterface{
@@ -799,6 +887,31 @@ func TestRawAddrMap_ToTopoAddr(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "IPv6 good address with zone",
+			ram: jsontopo.NATSCIONAddressMap{
+				"IPv6": &jsontopo.NATSCIONAddress{
+					Public: jsontopo.FullSCIONAddress{
+						Address: jsontopo.Address{
+							Addr:   "2001:db8:f00:b43::1%some-zone",
+							L4Port: 42,
+						},
+					},
+				},
+			},
+			addr: &TopoAddr{
+				SCIONAddress: &net.UDPAddr{
+					IP:   net.ParseIP("2001:db8:f00:b43::1"),
+					Port: 42,
+					Zone: "some-zone",
+				},
+				UnderlayAddress: &net.UDPAddr{
+					IP:   net.ParseIP("2001:db8:f00:b43::1"),
+					Port: 30041,
+					Zone: "some-zone",
+				},
+			},
+		},
 		{
 			name: "IPv4 with bind",
 			err:  errBindNotSupported,