Repository: scionproto/scion
Pull request #3817 (2020-07-03 13:03:24)

header: further improvements
Add header v2 one hop path
Enable header v2 beaconing in CS with feature flag.
Enable header v2 through feature bools in a lot of places.

<!-- 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/3817)
<!-- Reviewable:end -->


Oncilla: /rebase


Patch:
diff --git a/go/cs/beaconing/BUILD.bazel b/go/cs/beaconing/BUILD.bazel
index f5c2e8241..27bee06f8 100644
--- a/go/cs/beaconing/BUILD.bazel
+++ b/go/cs/beaconing/BUILD.bazel
@@ -32,6 +32,7 @@ go_library(
         "//go/lib/periodic:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/slayers/path:go_default_library",
+        "//go/lib/slayers/path/scion: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/cs/beaconing/handler.go b/go/cs/beaconing/handler.go
index 7be2f772b..24082e01c 100644
--- a/go/cs/beaconing/handler.go
+++ b/go/cs/beaconing/handler.go
@@ -28,7 +28,9 @@ import (
 	"github.com/scionproto/scion/go/lib/infra/modules/segverifier"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/lib/slayers/path/scion"
 	"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/proto"
 )
@@ -142,16 +144,16 @@ func (h *handler) getIFID() (common.IFIDType, addr.IA, error) {
 		return 0, ia, common.NewBasicError("Invalid peer address type, expected *snet.UDPAddr", nil,
 			"peer", h.request.Peer, "type", common.TypeOf(h.request.Peer))
 	}
-	hopF, err := peer.Path.GetHopField(peer.Path.HopOff)
+	ingressIfID, err := ingressIfID(peer.Path)
 	if err != nil {
-		return 0, ia, common.NewBasicError("Unable to extract hop field", err)
+		return 0, ia, err
 	}
-	intf := h.intfs.Get(hopF.ConsIngress)
+	intf := h.intfs.Get(ingressIfID)
 	if intf == nil {
 		return 0, ia, common.NewBasicError("Received beacon on non-existent ifid", nil,
-			"ifid", hopF.ConsIngress)
+			"ifid", ingressIfID)
 	}
-	return hopF.ConsIngress, intf.TopoInfo().IA, nil
+	return ingressIfID, intf.TopoInfo().IA, nil
 }
 
 func (h *handler) verifyBeacon(b beacon.Beacon) error {
@@ -207,3 +209,22 @@ func (h *handler) verifySegment(segment *seg.PathSegment) error {
 	}
 	return segverifier.VerifySegment(h.request.Context(), h.verifier, svcToQuery, segment)
 }
+
+func ingressIfID(path *spath.Path) (common.IFIDType, error) {
+	if path.IsHeaderV2() {
+		var sp scion.Raw
+		if err := sp.DecodeFromBytes(path.Raw); err != nil {
+			return 0, serrors.WrapStr("decoding path (v2)", err)
+		}
+		hf, err := sp.GetCurrentHopField()
+		if err != nil {
+			return 0, serrors.WrapStr("getting current hop field", err)
+		}
+		return common.IFIDType(hf.ConsIngress), nil
+	}
+	hopF, err := path.GetHopField(path.HopOff)
+	if err != nil {
+		return 0, common.NewBasicError("Unable to extract hop field", err)
+	}
+	return hopF.ConsIngress, nil
+}
diff --git a/go/cs/handlers/common.go b/go/cs/handlers/common.go
index d9d1dfbe6..7b08159ef 100644
--- a/go/cs/handlers/common.go
+++ b/go/cs/handlers/common.go
@@ -36,4 +36,5 @@ type HandlerArgs struct {
 	IA            addr.IA
 	TopoProvider  topology.Provider
 	SegRequestAPI segfetcher.RequestAPI
+	HeaderV2      bool
 }
diff --git a/go/cs/keepalive/BUILD.bazel b/go/cs/keepalive/BUILD.bazel
index d1e1caa08..36ebfac79 100644
--- a/go/cs/keepalive/BUILD.bazel
+++ b/go/cs/keepalive/BUILD.bazel
@@ -20,7 +20,10 @@ go_library(
         "//go/lib/infra:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/periodic:go_default_library",
+        "//go/lib/serrors:go_default_library",
+        "//go/lib/slayers/path/scion: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/cs/keepalive/handler.go b/go/cs/keepalive/handler.go
index d8f8348b3..7a13282e9 100644
--- a/go/cs/keepalive/handler.go
+++ b/go/cs/keepalive/handler.go
@@ -25,7 +25,10 @@ import (
 	"github.com/scionproto/scion/go/lib/ctrl/ifid"
 	"github.com/scionproto/scion/go/lib/infra"
 	"github.com/scionproto/scion/go/lib/log"
+	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/lib/slayers/path/scion"
 	"github.com/scionproto/scion/go/lib/snet"
+	"github.com/scionproto/scion/go/lib/spath"
 )
 
 const (
@@ -125,21 +128,21 @@ func (h *handler) getIntfInfo() (common.IFIDType, *ifstate.Interface, error) {
 		return 0, nil, common.NewBasicError("Invalid peer address type, expected *snet.UDPAddr",
 			nil, "peer", h.request.Peer, "type", common.TypeOf(h.request.Peer))
 	}
-	hopF, err := peer.Path.GetHopField(peer.Path.HopOff)
+	ingressIfID, err := ingressIfID(peer.Path)
 	if err != nil {
-		return 0, nil, common.NewBasicError("Unable to extract hop field", err)
+		return 0, nil, err
 	}
-	info := h.intfs.Get(hopF.ConsIngress)
+	info := h.intfs.Get(ingressIfID)
 	if info == nil {
 		return 0, nil, common.NewBasicError("Received keepalive for non-existent ifid", nil,
-			"ifid", hopF.ConsIngress)
+			"ifid", ingressIfID)
 	}
 	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)
+			"ifid", ingressIfID, "expected", originIA, "actual", peer.IA)
 	}
-	return hopF.ConsIngress, info, nil
+	return ingressIfID, info, nil
 }
 
 func (h *handler) startPush(ifid common.IFIDType) {
@@ -159,3 +162,22 @@ func (h *handler) dropRevs(localIfid, originIfid common.IFIDType, originIA addr.
 	}
 	return h.tasks.RevDropper.DeleteRevocation(subCtx, originIA, originIfid)
 }
+
+func ingressIfID(path *spath.Path) (common.IFIDType, error) {
+	if path.IsHeaderV2() {
+		var sp scion.Raw
+		if err := sp.DecodeFromBytes(path.Raw); err != nil {
+			return 0, serrors.WrapStr("decoding path (v2)", err)
+		}
+		hf, err := sp.GetCurrentHopField()
+		if err != nil {
+			return 0, serrors.WrapStr("getting current hop field", err)
+		}
+		return common.IFIDType(hf.ConsIngress), nil
+	}
+	hopF, err := path.GetHopField(path.HopOff)
+	if err != nil {
+		return 0, common.NewBasicError("Unable to extract hop field", err)
+	}
+	return hopF.ConsIngress, nil
+}
diff --git a/go/cs/segreq/BUILD.bazel b/go/cs/segreq/BUILD.bazel
index 3473589be..9c63489d9 100644
--- a/go/cs/segreq/BUILD.bazel
+++ b/go/cs/segreq/BUILD.bazel
@@ -29,7 +29,6 @@ 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/pkg/trust:go_default_library",
         "//go/proto:go_default_library",
     ],
diff --git a/go/cs/segreq/handler.go b/go/cs/segreq/handler.go
index ee1be84b0..051fe104f 100644
--- a/go/cs/segreq/handler.go
+++ b/go/cs/segreq/handler.go
@@ -15,6 +15,8 @@
 package segreq
 
 import (
+	"net"
+
 	"github.com/scionproto/scion/go/cs/handlers"
 	"github.com/scionproto/scion/go/cs/metrics"
 	"github.com/scionproto/scion/go/lib/common"
@@ -25,6 +27,7 @@ import (
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/pathdb"
 	"github.com/scionproto/scion/go/lib/revcache"
+	"github.com/scionproto/scion/go/lib/snet/addrutil"
 	"github.com/scionproto/scion/go/proto"
 )
 
@@ -140,20 +143,28 @@ func CreateDstProvider(args handlers.HandlerArgs, core bool) segfetcher.DstProvi
 		PathDB:   args.PathDB,
 		RevCache: args.RevCache,
 	}
+	var pather pather = addrutil.LegacyPather{TopoProvider: args.TopoProvider}
+	if args.HeaderV2 {
+		pather = addrutil.Pather{
+			UnderlayNextHop: func(ifID uint16) (*net.UDPAddr, bool) {
+				return args.TopoProvider.Get().UnderlayNextHop2(common.IFIDType(ifID))
+			},
+		}
+	}
 	if core {
 		return &coreDstProvider{
-			SegSelector:  selector,
-			localIA:      args.IA,
-			pathDB:       args.PathDB,
-			topoProvider: args.TopoProvider,
+			SegSelector: selector,
+			localIA:     args.IA,
+			pathDB:      args.PathDB,
+			pather:      pather,
 		}
 	}
 	return &nonCoreDstProvider{
-		SegSelector:  selector,
-		inspector:    args.ASInspector,
-		coreChecker:  CoreChecker{Inspector: args.ASInspector},
-		localIA:      args.IA,
-		pathDB:       args.PathDB,
-		topoProvider: args.TopoProvider,
+		SegSelector: selector,
+		inspector:   args.ASInspector,
+		coreChecker: CoreChecker{Inspector: args.ASInspector},
+		localIA:     args.IA,
+		pathDB:      args.PathDB,
+		pather:      pather,
 	}
 }
diff --git a/go/cs/segreq/provider.go b/go/cs/segreq/provider.go
index 2e5a1c623..b8c0faf64 100644
--- a/go/cs/segreq/provider.go
+++ b/go/cs/segreq/provider.go
@@ -28,12 +28,14 @@ import (
 	"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/pkg/trust"
 	"github.com/scionproto/scion/go/proto"
 )
 
+type pather interface {
+	GetPath(svc addr.HostSVC, ps *seg.PathSegment) (net.Addr, error)
+}
+
 // SegSelector selects segments to use for a connection to a remote server.
 type SegSelector struct {
 	PathDB   pathdb.PathDB
@@ -63,11 +65,11 @@ func (s *SegSelector) SelectSeg(ctx context.Context,
 
 type nonCoreDstProvider struct {
 	SegSelector
-	inspector    trust.Inspector
-	coreChecker  CoreChecker
-	localIA      addr.IA
-	pathDB       pathdb.PathDB
-	topoProvider topology.Provider
+	inspector   trust.Inspector
+	coreChecker CoreChecker
+	localIA     addr.IA
+	pathDB      pathdb.PathDB
+	pather      pather
 }
 
 // Dst provides the server to lookup the segment for the given request.
@@ -107,14 +109,14 @@ func (p *nonCoreDstProvider) coreSvcAddr(ctx context.Context, svc addr.HostSVC,
 	if err != nil {
 		return nil, serrors.Wrap(segfetcher.ErrNotReachable, err)
 	}
-	return addrutil.GetPath(svc, seg, p.topoProvider)
+	return p.pather.GetPath(svc, seg)
 }
 
 type coreDstProvider struct {
 	SegSelector
-	localIA      addr.IA
-	pathDB       pathdb.PathDB
-	topoProvider topology.Provider
+	localIA addr.IA
+	pathDB  pathdb.PathDB
+	pather  pather
 }
 
 func (p *coreDstProvider) Dst(ctx context.Context, req segfetcher.Request) (net.Addr, error) {
@@ -135,5 +137,5 @@ func (p *coreDstProvider) corePSAddr(ctx context.Context, destISD addr.ISD) (net
 	if err != nil {
 		return nil, serrors.Wrap(segfetcher.ErrNotReachable, err)
 	}
-	return addrutil.GetPath(addr.SvcPS, seg, p.topoProvider)
+	return p.pather.GetPath(addr.SvcPS, seg)
 }
diff --git a/go/cs/segsyncer/BUILD.bazel b/go/cs/segsyncer/BUILD.bazel
index c934c75b7..9b1941dc6 100644
--- a/go/cs/segsyncer/BUILD.bazel
+++ b/go/cs/segsyncer/BUILD.bazel
@@ -21,7 +21,6 @@ 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/pkg/trust:go_default_library",
         "//go/proto:go_default_library",
     ],
diff --git a/go/cs/segsyncer/segsyncer.go b/go/cs/segsyncer/segsyncer.go
index 67f5a2975..ad89aab18 100644
--- a/go/cs/segsyncer/segsyncer.go
+++ b/go/cs/segsyncer/segsyncer.go
@@ -37,7 +37,6 @@ 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/pkg/trust"
 	"github.com/scionproto/scion/go/proto"
 )
@@ -51,6 +50,10 @@ var (
 	errNet      = serrors.New("network error")
 )
 
+type pather interface {
+	GetPath(svc addr.HostSVC, ps *seg.PathSegment) (net.Addr, error)
+}
+
 type SegSyncer struct {
 	latestUpdate *time.Time
 	pathDB       pathdb.PathDB
@@ -58,7 +61,7 @@ type SegSyncer struct {
 	msger        infra.Messenger
 	dstIA        addr.IA
 	localIA      addr.IA
-	topoProvider topology.Provider
+	pather       pather
 	repErrCnt    int
 }
 
@@ -70,18 +73,27 @@ func StartAll(args handlers.HandlerArgs, msger infra.Messenger) ([]*periodic.Run
 		return nil, common.NewBasicError("Failed to get local core ASes", err)
 	}
 
+	var pather pather = addrutil.LegacyPather{TopoProvider: args.TopoProvider}
+	if args.HeaderV2 {
+		pather = addrutil.Pather{
+			UnderlayNextHop: func(ifID uint16) (*net.UDPAddr, bool) {
+				return args.TopoProvider.Get().UnderlayNextHop2(common.IFIDType(ifID))
+			},
+		}
+	}
+
 	segSyncers := make([]*periodic.Runner, 0, len(coreASes)-1)
 	for _, coreAS := range coreASes {
 		if coreAS.Equal(args.IA) {
 			continue
 		}
 		syncer := &SegSyncer{
-			pathDB:       args.PathDB,
-			revCache:     args.RevCache,
-			msger:        msger,
-			dstIA:        coreAS,
-			localIA:      args.IA,
-			topoProvider: args.TopoProvider,
+			pathDB:   args.PathDB,
+			revCache: args.RevCache,
+			msger:    msger,
+			dstIA:    coreAS,
+			localIA:  args.IA,
+			pather:   pather,
 		}
 		// TODO(lukedirtwalker): either log or add metric to indicate
 		// if task takes longer than ticker often.
@@ -137,7 +149,7 @@ func (s *SegSyncer) getDstAddr(ctx context.Context) (net.Addr, error) {
 	var cPs net.Addr
 	// select a seg to reach the dst
 	for _, ps := range coreSegs {
-		cPs, err = addrutil.GetPath(addr.SvcPS, ps, s.topoProvider)
+		cPs, err = s.pather.GetPath(addr.SvcPS, ps)
 		if err == nil {
 			return cPs, nil
 		}
diff --git a/go/cs/segutil/router.go b/go/cs/segutil/router.go
index 9328bf9ab..c54c9a6b7 100644
--- a/go/cs/segutil/router.go
+++ b/go/cs/segutil/router.go
@@ -84,7 +84,7 @@ func (r *Router) translate(comb *combinator.Path, dst addr.IA) (path, error) {
 			return path{}, err
 		}
 	} else {
-		sp = spath.NewV2(buf.Bytes())
+		sp = spath.NewV2(buf.Bytes(), false)
 	}
 	nextHop, ok := r.Pather.TopoProvider.Get().UnderlayNextHop(comb.Interfaces[0].IfID)
 	if !ok {
diff --git a/go/dispatcher/BUILD.bazel b/go/dispatcher/BUILD.bazel
index 28eec7d69..1711d6b9f 100644
--- a/go/dispatcher/BUILD.bazel
+++ b/go/dispatcher/BUILD.bazel
@@ -36,9 +36,9 @@ go_test(
         "//go/lib/common:go_default_library",
         "//go/lib/hpkt:go_default_library",
         "//go/lib/l4:go_default_library",
-        "//go/lib/log:go_default_library",
         "//go/lib/scmp:go_default_library",
         "//go/lib/sock/reliable:go_default_library",
+        "//go/lib/spath:go_default_library",
         "//go/lib/spkt:go_default_library",
         "//go/lib/xtest:go_default_library",
     ],
diff --git a/go/dispatcher/config/config.go b/go/dispatcher/config/config.go
index aa3fda27a..42d63d574 100644
--- a/go/dispatcher/config/config.go
+++ b/go/dispatcher/config/config.go
@@ -86,5 +86,5 @@ func (cfg *Config) Sample(dst io.Writer, path config.Path, _ config.CtxMap) {
 }
 
 func (cfg *Config) ConfigName() string {
-	return "godispatcher_config"
+	return "dispatcher_config"
 }
diff --git a/go/dispatcher/dispatcher/BUILD.bazel b/go/dispatcher/dispatcher/BUILD.bazel
index f0eb900f4..096cccdad 100644
--- a/go/dispatcher/dispatcher/BUILD.bazel
+++ b/go/dispatcher/dispatcher/BUILD.bazel
@@ -18,7 +18,6 @@ go_library(
         "//go/lib/common:go_default_library",
         "//go/lib/hpkt:go_default_library",
         "//go/lib/l4:go_default_library",
-        "//go/lib/layers:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/ringbuf:go_default_library",
         "//go/lib/scmp:go_default_library",
diff --git a/go/dispatcher/dispatcher/dispatcher.go b/go/dispatcher/dispatcher/dispatcher.go
index 6bc356e68..ad5d7603f 100644
--- a/go/dispatcher/dispatcher/dispatcher.go
+++ b/go/dispatcher/dispatcher/dispatcher.go
@@ -33,6 +33,8 @@ type Server struct {
 	routingTable *IATable
 	ipv4Conn     net.PacketConn
 	ipv6Conn     net.PacketConn
+	// HeaderV2 indicates whether the new header format is used.
+	HeaderV2 bool
 }
 
 // NewServer creates new instance of Server. Internally, it opens the dispatcher ports
@@ -73,6 +75,7 @@ func (as *Server) Serve() error {
 		netToRingDataplane := &NetToRingDataplane{
 			UnderlayConn: as.ipv4Conn,
 			RoutingTable: as.routingTable,
+			HeaderV2:     as.HeaderV2,
 		}
 		errChan <- netToRingDataplane.Run()
 	}()
@@ -81,6 +84,7 @@ func (as *Server) Serve() error {
 		netToRingDataplane := &NetToRingDataplane{
 			UnderlayConn: as.ipv6Conn,
 			RoutingTable: as.routingTable,
+			HeaderV2:     as.HeaderV2,
 		}
 		errChan <- netToRingDataplane.Run()
 	}()
@@ -123,11 +127,21 @@ type Conn struct {
 	ring *ringbuf.Ring
 	// regReference is the reference to the registration in the routing table.
 	regReference registration.RegReference
+	// HeaderV2 indicates whether the new header format is used.
+	HeaderV2 bool
 }
 
 func (ac *Conn) WriteTo(p []byte, addr net.Addr) (int, error) {
 	var info spkt.ScnPkt
-	hpkt.ParseScnPkt(&info, p)
+	if ac.HeaderV2 {
+		if err := hpkt.ParseScnPkt2(&info, p); err != nil {
+			return 0, err
+		}
+	} else {
+		if err := hpkt.ParseScnPkt(&info, p); err != nil {
+			return 0, err
+		}
+	}
 	if err := registerIfSCMPRequest(ac.regReference, &info); err != nil {
 		log.Info("SCMP Request ID error, packet still sent", "err", err)
 	}
diff --git a/go/dispatcher/dispatcher/underlay.go b/go/dispatcher/dispatcher/underlay.go
index b0145b3b2..9abf856c5 100644
--- a/go/dispatcher/dispatcher/underlay.go
+++ b/go/dispatcher/dispatcher/underlay.go
@@ -23,7 +23,6 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/hpkt"
 	"github.com/scionproto/scion/go/lib/l4"
-	"github.com/scionproto/scion/go/lib/layers"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/ringbuf"
 	"github.com/scionproto/scion/go/lib/scmp"
@@ -46,11 +45,13 @@ const (
 type NetToRingDataplane struct {
 	UnderlayConn net.PacketConn
 	RoutingTable *IATable
+	// HeaderV2 indicates whether the new header format is used.
+	HeaderV2 bool
 }
 
 func (dp *NetToRingDataplane) Run() error {
 	for {
-		pkt := respool.GetPacket()
+		pkt := respool.GetPacket(dp.HeaderV2)
 		// XXX(scrye): we don't release the reference on error conditions, and
 		// let the GC take care of this situation as they should be fairly
 		// rare.
@@ -60,9 +61,7 @@ func (dp *NetToRingDataplane) Run() error {
 			continue
 		}
 
-		logDebugE2E(&pkt.Info)
-
-		dst, err := ComputeDestination(&pkt.Info)
+		dst, err := ComputeDestination(&pkt.Info, dp.HeaderV2)
 		if err != nil {
 			log.Debug("unable to route packet", "err", err)
 			metrics.M.NetReadPkts(
@@ -75,18 +74,20 @@ func (dp *NetToRingDataplane) Run() error {
 	}
 }
 
-func ComputeDestination(packet *spkt.ScnPkt) (Destination, error) {
+func ComputeDestination(packet *spkt.ScnPkt, headerV2 bool) (Destination, error) {
 	switch header := packet.L4.(type) {
 	case *l4.UDP:
-		return ComputeUDPDestination(packet, header)
+		return ComputeUDPDestination(packet, header, headerV2)
 	case *scmp.Hdr:
-		return ComputeSCMPDestination(packet, header)
+		return ComputeSCMPDestination(packet, header, headerV2)
 	default:
 		return nil, common.NewBasicError(ErrUnsupportedL4, nil, "type", header.L4Type())
 	}
 }
 
-func ComputeUDPDestination(packet *spkt.ScnPkt, header *l4.UDP) (Destination, error) {
+func ComputeUDPDestination(packet *spkt.ScnPkt, header *l4.UDP,
+	headerV2 bool) (Destination, error) {
+
 	switch packet.DstHost.Type() {
 	case addr.HostTypeIPv4, addr.HostTypeIPv6:
 		return &UDPDestination{IP: packet.DstHost.IP(), Port: int(header.DstPort)}, nil
@@ -100,7 +101,9 @@ func ComputeUDPDestination(packet *spkt.ScnPkt, header *l4.UDP) (Destination, er
 
 // ComputeSCMPDestination decides which application to send the SCMP packet to. It also increments
 // SCMP-related metrics.
-func ComputeSCMPDestination(packet *spkt.ScnPkt, header *scmp.Hdr) (Destination, error) {
+func ComputeSCMPDestination(packet *spkt.ScnPkt, header *scmp.Hdr,
+	headerV2 bool) (Destination, error) {
+
 	metrics.M.SCMPReadPkts(
 		metrics.SCMP{
 			Class: header.Class.String(),
@@ -112,13 +115,15 @@ func ComputeSCMPDestination(packet *spkt.ScnPkt, header *scmp.Hdr) (Destination,
 			"type", packet.DstHost.Type())
 	}
 	if header.Class == scmp.C_General {
-		return ComputeSCMPGeneralDestination(packet, header)
+		return ComputeSCMPGeneralDestination(packet, header, headerV2)
 	} else {
 		return ComputeSCMPErrorDestination(packet, header)
 	}
 }
 
-func ComputeSCMPGeneralDestination(s *spkt.ScnPkt, header *scmp.Hdr) (Destination, error) {
+func ComputeSCMPGeneralDestination(s *spkt.ScnPkt, header *scmp.Hdr,
+	headerV2 bool) (Destination, error) {
+
 	id := getSCMPGeneralID(s)
 	if id == 0 {
 		return nil, common.NewBasicError("Invalid SCMP ID", nil, "id", id)
@@ -126,7 +131,7 @@ func ComputeSCMPGeneralDestination(s *spkt.ScnPkt, header *scmp.Hdr) (Destinatio
 	switch {
 	case isSCMPGeneralRequest(header):
 		invertSCMPGeneralType(header)
-		return SCMPHandlerDestination{}, nil
+		return SCMPHandlerDestination{HeaderV2: headerV2}, nil
 	case isSCMPGeneralReply(header):
 		return &SCMPAppDestination{ID: id}, nil
 	default:
@@ -229,7 +234,10 @@ func sendPacket(routingEntry *TableEntry, pkt *respool.Packet) {
 
 var _ Destination = (*SCMPHandlerDestination)(nil)
 
-type SCMPHandlerDestination struct{}
+type SCMPHandlerDestination struct {
+	// HeaderV2 switches SCMP messages to the new SCION header format.
+	HeaderV2 bool
+}
 
 func (h SCMPHandlerDestination) Send(dp *NetToRingDataplane, pkt *respool.Packet) {
 	if err := pkt.Info.Reverse(); err != nil {
@@ -239,7 +247,13 @@ func (h SCMPHandlerDestination) Send(dp *NetToRingDataplane, pkt *respool.Packet
 
 	b := respool.GetBuffer()
 	pkt.Info.HBHExt = removeSCMPHBH(pkt.Info.HBHExt)
-	n, err := hpkt.WriteScnPkt(&pkt.Info, b)
+	var n int
+	var err error
+	if h.HeaderV2 {
+		n, err = hpkt.WriteScnPkt2(&pkt.Info, b)
+	} else {
+		n, err = hpkt.WriteScnPkt(&pkt.Info, b)
+	}
 	if err != nil {
 		log.Info("Unable to create reply SCMP packet", "err", err)
 		return
@@ -264,11 +278,3 @@ func (h SCMPHandlerDestination) Send(dp *NetToRingDataplane, pkt *respool.Packet
 	respool.PutBuffer(b)
 	pkt.Free()
 }
-
-func logDebugE2E(pkt *spkt.ScnPkt) {
-	for _, e := range pkt.E2EExt {
-		if extnData, ok := e.(*layers.ExtnE2EDebug); ok {
-			log.Debug("Recv'd packet with debug extension", "debug_id", extnData.ID, "pkt", pkt)
-		}
-	}
-}
diff --git a/go/dispatcher/internal/respool/packet.go b/go/dispatcher/internal/respool/packet.go
index e159d748c..239341289 100644
--- a/go/dispatcher/internal/respool/packet.go
+++ b/go/dispatcher/internal/respool/packet.go
@@ -31,9 +31,10 @@ var packetPool = sync.Pool{
 	},
 }
 
-func GetPacket() *Packet {
+func GetPacket(headerV2 bool) *Packet {
 	pkt := packetPool.Get().(*Packet)
 	*pkt.refCount = 1
+	pkt.HeaderV2 = headerV2
 	return pkt
 }
 
@@ -43,6 +44,8 @@ func GetPacket() *Packet {
 type Packet struct {
 	Info           spkt.ScnPkt
 	UnderlayRemote *net.UDPAddr
+	// HeaderV2 indicates whether the new header format is used.
+	HeaderV2 bool
 
 	// buffer contains the raw slice that other fields reference
 	buffer common.RawBytes
@@ -114,6 +117,17 @@ func (pkt *Packet) DecodeFromConn(conn net.PacketConn) error {
 	metrics.M.NetReadBytes().Add(float64(n))
 
 	pkt.UnderlayRemote = readExtra.(*net.UDPAddr)
+	if pkt.HeaderV2 {
+		if err = hpkt.ParseScnPkt2(&pkt.Info, pkt.buffer); err != nil {
+			metrics.M.NetReadPkts(
+				metrics.IncomingPacket{
+					Result: metrics.PacketResultParseError,
+				},
+			).Inc()
+			return err
+		}
+		return nil
+	}
 	if err = hpkt.ParseScnPkt(&pkt.Info, pkt.buffer); err != nil {
 		metrics.M.NetReadPkts(
 			metrics.IncomingPacket{
@@ -137,13 +151,10 @@ func (pkt *Packet) DecodeFromReliableConn(conn net.PacketConn) error {
 	}
 	pkt.UnderlayRemote = readExtra.(*net.UDPAddr)
 
-	// XXX(scrye): We ignore the return value of packet parsing on egress
-	// because some tests (e.g., the Python SCMP error test) rely on being able
-	// to dump bad SCION packets on the network. If the error here is taken
-	// into account, the dispatcher drops the packet and the SCMP error reply
-	// never comes back from the BR.
-	_ = hpkt.ParseScnPkt(&pkt.Info, pkt.buffer)
-	return nil
+	if pkt.HeaderV2 {
+		return hpkt.ParseScnPkt2(&pkt.Info, pkt.buffer)
+	}
+	return hpkt.ParseScnPkt(&pkt.Info, pkt.buffer)
 }
 
 func (pkt *Packet) SendOnConn(conn net.PacketConn, address net.Addr) (int, error) {
diff --git a/go/dispatcher/main.go b/go/dispatcher/main.go
index 280dd8036..2dfd7238c 100644
--- a/go/dispatcher/main.go
+++ b/go/dispatcher/main.go
@@ -82,6 +82,7 @@ func realMain() int {
 			cfg.Dispatcher.ApplicationSocket,
 			os.FileMode(cfg.Dispatcher.SocketFileMode),
 			cfg.Dispatcher.UnderlayPort,
+			cfg.Features.HeaderV2,
 		)
 		if err != nil {
 			fatal.Fatal(err)
@@ -125,7 +126,7 @@ func setupBasic() error {
 }
 
 func RunDispatcher(deleteSocketFlag bool, applicationSocket string, socketFileMode os.FileMode,
-	underlayPort int) error {
+	underlayPort int, headerV2 bool) error {
 
 	if deleteSocketFlag {
 		if err := deleteSocket(cfg.Dispatcher.ApplicationSocket); err != nil {
@@ -136,6 +137,7 @@ func RunDispatcher(deleteSocketFlag bool, applicationSocket string, socketFileMo
 		UnderlaySocket:    fmt.Sprintf(":%d", underlayPort),
 		ApplicationSocket: applicationSocket,
 		SocketFileMode:    socketFileMode,
+		HeaderV2:          headerV2,
 	}
 	log.Debug("Dispatcher starting", "appSocket", applicationSocket, "underlayPort", underlayPort)
 	return dispatcher.ListenAndServe()
diff --git a/go/dispatcher/network/app_socket.go b/go/dispatcher/network/app_socket.go
index e9f463309..d4d885bb4 100644
--- a/go/dispatcher/network/app_socket.go
+++ b/go/dispatcher/network/app_socket.go
@@ -33,6 +33,8 @@ import (
 type AppSocketServer struct {
 	Listener   *reliable.Listener
 	DispServer *dispatcher.Server
+	// HeaderV2 indicates whether the new header format is used.
+	HeaderV2 bool
 }
 
 func (s *AppSocketServer) Serve() error {
@@ -49,8 +51,9 @@ func (s *AppSocketServer) Serve() error {
 // Handle passes conn off to a per-connection state handler.
 func (h *AppSocketServer) Handle(conn net.PacketConn) {
 	ch := &AppConnHandler{
-		Conn:   conn,
-		Logger: log.New("clientID", fmt.Sprintf("%p", conn)),
+		Conn:     conn,
+		Logger:   log.New("clientID", fmt.Sprintf("%p", conn)),
+		HeaderV2: h.HeaderV2,
 	}
 	go func() {
 		defer log.HandlePanic()
@@ -64,6 +67,8 @@ type AppConnHandler struct {
 	Conn     net.PacketConn
 	DispConn *dispatcher.Conn
 	Logger   log.Logger
+	// HeaderV2 indicates whether the new header format is used.
+	HeaderV2 bool
 }
 
 func (h *AppConnHandler) Handle(appServer *dispatcher.Server) {
@@ -164,7 +169,7 @@ func (h *AppConnHandler) sendConfirmation(b common.RawBytes, c *reliable.Confirm
 func (h *AppConnHandler) RunAppToNetDataplane() {
 
 	for {
-		pkt := respool.GetPacket()
+		pkt := respool.GetPacket(h.HeaderV2)
 		// XXX(scrye): we don't release the reference on error conditions, and
 		// let the GC take care of this situation as they should be fairly
 		// rare.
diff --git a/go/dispatcher/network/dispatcher.go b/go/dispatcher/network/dispatcher.go
index cb8ec3452..c5fbd6c9e 100644
--- a/go/dispatcher/network/dispatcher.go
+++ b/go/dispatcher/network/dispatcher.go
@@ -27,6 +27,8 @@ type Dispatcher struct {
 	UnderlaySocket    string
 	ApplicationSocket string
 	SocketFileMode    os.FileMode
+	// HeaderV2 indicates whether the new header format is used.
+	HeaderV2 bool
 }
 
 func (d *Dispatcher) ListenAndServe() error {
@@ -35,6 +37,7 @@ func (d *Dispatcher) ListenAndServe() error {
 		return err
 	}
 	defer dispServer.Close()
+	dispServer.HeaderV2 = d.HeaderV2
 
 	dispServerConn, err := reliable.Listen(d.ApplicationSocket)
 	if err != nil {
@@ -56,6 +59,7 @@ func (d *Dispatcher) ListenAndServe() error {
 		dispServer := &AppSocketServer{
 			Listener:   dispServerConn,
 			DispServer: dispServer,
+			HeaderV2:   d.HeaderV2,
 		}
 		errChan <- dispServer.Serve()
 	}()
diff --git a/go/examples/pingpong/pingpong.go b/go/examples/pingpong/pingpong.go
index 636ec7f7f..909b88c83 100644
--- a/go/examples/pingpong/pingpong.go
+++ b/go/examples/pingpong/pingpong.go
@@ -245,7 +245,13 @@ func (c *client) run() {
 	// IP address needs to be supplied explicitly. When supplied a local
 	// port of 0, Dial will assign a random free local port.
 
-	c.qsess, err = squic.Dial(network, local.Host, &remote, addr.SvcNone, nil)
+	remoteUDP := &snet.UDPAddr{
+		IA:      remote.IA,
+		Path:    remote.Path,
+		NextHop: remote.NextHop,
+		Host:    remote.Host,
+	}
+	c.qsess, err = squic.Dial(network, local.Host, remoteUDP, addr.SvcNone, nil)
 	if err != nil {
 		LogFatal("Unable to dial", "err", err)
 	}
diff --git a/go/integration/common.go b/go/integration/common.go
index 9daf50e27..7b343a1db 100644
--- a/go/integration/common.go
+++ b/go/integration/common.go
@@ -41,13 +41,13 @@ const (
 )
 
 var (
-	Local        snet.UDPAddr
-	Mode         string
-	Progress     string
-	sciondAddr   string
-	networksFile string
-	Attempts     int
-	logConsole   string
+	Local      snet.UDPAddr
+	Mode       string
+	Progress   string
+	sciondAddr string
+	Attempts   int
+	logConsole string
+	HeaderV2   bool
 )
 
 func Setup() {
@@ -60,10 +60,9 @@ func addFlags() {
 	flag.StringVar(&Mode, "mode", ModeClient, "Run in "+ModeClient+" or "+ModeServer+" mode")
 	flag.StringVar(&Progress, "progress", "", "Socket to write progress to")
 	flag.StringVar(&sciondAddr, "sciond", sciond.DefaultSCIONDAddress, "SCIOND address")
-	flag.StringVar(&networksFile, "networks", integration.SCIONDAddressesFile,
-		"File containing network definitions")
 	flag.IntVar(&Attempts, "attempts", 1, "Number of attempts before giving up")
 	flag.StringVar(&logConsole, "log.console", "info", "Console logging level: debug|info|error")
+	flag.BoolVar(&HeaderV2, "header_v2", false, "Use the new header format.")
 }
 
 // InitTracer initializes the global tracer and returns a closer function.
@@ -115,9 +114,9 @@ func InitNetwork() *snet.SCIONNetwork {
 			SCMPHandler: snet.NewSCMPHandler(
 				sciond.RevHandler{Connector: sciondConn},
 			),
-			// TODO(scrye): set this when we have CLI support for features
-			Version2: false,
+			Version2: HeaderV2,
 		},
+		Version2: HeaderV2,
 	}
 	log.Debug("SCION network successfully initialized")
 	return n
diff --git a/go/integration/end2end/BUILD.bazel b/go/integration/end2end/BUILD.bazel
index ce38e254f..f661ade28 100644
--- a/go/integration/end2end/BUILD.bazel
+++ b/go/integration/end2end/BUILD.bazel
@@ -12,7 +12,6 @@ go_library(
         "//go/lib/common:go_default_library",
         "//go/lib/integration:go_default_library",
         "//go/lib/l4:go_default_library",
-        "//go/lib/layers:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/sciond:go_default_library",
         "//go/lib/serrors:go_default_library",
diff --git a/go/integration/end2end/main.go b/go/integration/end2end/main.go
index 441ec2563..5c7be1ae4 100644
--- a/go/integration/end2end/main.go
+++ b/go/integration/end2end/main.go
@@ -31,7 +31,6 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	libint "github.com/scionproto/scion/go/lib/integration"
 	"github.com/scionproto/scion/go/lib/l4"
-	"github.com/scionproto/scion/go/lib/layers"
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/sciond"
 	"github.com/scionproto/scion/go/lib/serrors"
@@ -107,8 +106,7 @@ func (s server) run() {
 		SCMPHandler: snet.NewSCMPHandler(
 			sciond.RevHandler{Connector: integration.SDConn()},
 		),
-		// TODO(scrye): set this when we have CLI support for features
-		Version2: false,
+		Version2: integration.HeaderV2,
 	}
 	conn, port, err := connFactory.Register(context.Background(), integration.Local.IA,
 		integration.Local.Host, addr.SvcNone)
@@ -159,7 +157,7 @@ type client struct {
 
 func (c client) run() int {
 	pair := fmt.Sprintf("%s -> %s", integration.Local.IA, remote.IA)
-	log.Info("Starting", "pair", pair)
+	log.Info("Starting", "pair", pair, "header_v2", integration.HeaderV2)
 	defer log.Info("Finished", "pair", pair)
 	defer integration.Done(integration.Local.IA, remote.IA)
 	connFactory := &snet.DefaultPacketDispatcherService{
@@ -167,8 +165,7 @@ func (c client) run() int {
 		SCMPHandler: snet.NewSCMPHandler(
 			sciond.RevHandler{Connector: integration.SDConn()},
 		),
-		// TODO(scrye): set this when we have CLI support for features
-		Version2: false,
+		Version2: integration.HeaderV2,
 	}
 
 	var err error
@@ -234,11 +231,6 @@ func (c client) ping(ctx context.Context, n int) error {
 					Host: addr.HostFromIP(integration.Local.Host.IP),
 				},
 				Path: remote.Path,
-				Extensions: []common.Extension{
-					layers.ExtnE2EDebug{
-						ID: debugID,
-					},
-				},
 				L4Header: &l4.UDP{
 					SrcPort: c.port,
 					DstPort: uint16(remote.Host.Port),
diff --git a/go/integration/end2end_integration/main.go b/go/integration/end2end_integration/main.go
index 6da448e81..b2fa716c1 100644
--- a/go/integration/end2end_integration/main.go
+++ b/go/integration/end2end_integration/main.go
@@ -41,6 +41,7 @@ var (
 	parallelism int
 	name        string
 	cmd         string
+	headerV2    bool
 )
 
 func getCmd() (string, bool) {
@@ -72,6 +73,10 @@ func realMain() int {
 		"-sciond", integration.SCIOND,
 		"-local", integration.DstAddrPattern + ":0",
 	}
+	if headerV2 {
+		clientArgs = append(clientArgs, "-header_v2")
+		serverArgs = append(serverArgs, "-header_v2")
+	}
 
 	in := integration.NewBinaryIntegration(name, cmd, clientArgs, serverArgs)
 	pairs, err := getPairs()
@@ -97,6 +102,7 @@ func addFlags() {
 	flag.StringVar(&subset, "subset", "all", "Subset of pairs to run (all|core-core|"+
 		"noncore-localcore|noncore-core|noncore-noncore)")
 	flag.IntVar(&parallelism, "parallelism", 1, "How many end2end tests run in parallel.")
+	flag.BoolVar(&headerV2, "header_v2", false, "Use new header format")
 }
 
 // runTests runs the end2end tests for all pairs. In case of an error the
@@ -232,6 +238,9 @@ func clientTemplate(progressSock string) integration.Cmd {
 			"-remote", integration.DstAddrPattern + ":" + integration.ServerPortReplace,
 		},
 	}
+	if headerV2 {
+		cmd.Args = append(cmd.Args, "-header_v2")
+	}
 	if progress {
 		cmd.Args = append(cmd.Args, "-progress", progressSock)
 	}
diff --git a/go/lib/common/extn.go b/go/lib/common/extn.go
index 6210beb97..662dc310b 100644
--- a/go/lib/common/extn.go
+++ b/go/lib/common/extn.go
@@ -30,10 +30,6 @@ var (
 	ExtnPathTransType           = ExtnType{End2EndClass, 0}
 	ExtnPathProbeType           = ExtnType{End2EndClass, 1}
 	ExtnSCIONPacketSecurityType = ExtnType{End2EndClass, 2}
-	// ExtnE2EDebugType can be used to trace specific packets through the
-	// network. This is intended only for local use, and is not a recognized
-	// SCION extension.
-	ExtnE2EDebugType = ExtnType{End2EndClass, 254}
 )
 
 func (e ExtnType) String() string {
@@ -50,8 +46,6 @@ func (e ExtnType) String() string {
 		return "PathProbe"
 	case ExtnSCIONPacketSecurityType:
 		return "SCIONPacketSecurity"
-	case ExtnE2EDebugType:
-		return "E2EDebug"
 	}
 	return fmt.Sprintf("UNKNOWN (%d)", e)
 }
diff --git a/go/lib/hpkt/BUILD.bazel b/go/lib/hpkt/BUILD.bazel
index da544ed4a..7ba19b32f 100644
--- a/go/lib/hpkt/BUILD.bazel
+++ b/go/lib/hpkt/BUILD.bazel
@@ -17,6 +17,8 @@ go_library(
         "//go/lib/scmp:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/slayers:go_default_library",
+        "//go/lib/slayers/path:go_default_library",
+        "//go/lib/slayers/path/onehop:go_default_library",
         "//go/lib/slayers/path/scion:go_default_library",
         "//go/lib/spath:go_default_library",
         "//go/lib/spkt:go_default_library",
diff --git a/go/lib/hpkt/read.go b/go/lib/hpkt/read.go
index 5430d5bbe..d79671e28 100644
--- a/go/lib/hpkt/read.go
+++ b/go/lib/hpkt/read.go
@@ -296,7 +296,7 @@ func ParseScnPkt2(s *spkt.ScnPkt, b []byte) error {
 
 	decoded := []gopacket.LayerType{}
 	if err := parser.DecodeLayers(b, &decoded); err != nil {
-		return err
+		return serrors.WrapStr("decoding layers", err)
 	}
 
 	for _, layerType := range decoded {
@@ -306,32 +306,38 @@ func ParseScnPkt2(s *spkt.ScnPkt, b []byte) error {
 			s.SrcIA = scionLayer.SrcIA
 			dstAddr, err := scionLayer.DstAddr()
 			if err != nil {
-				return serrors.WrapStr("unable to extract destination address", err)
+				return serrors.WrapStr("extracting destination address", err)
 			}
 			s.DstHost, err = netAddrToHostAddr(dstAddr)
 			if err != nil {
-				return serrors.WrapStr("unable to convert address to HostAddr", err)
+				return serrors.WrapStr("converting dst address to HostAddr", err)
 			}
 			srcAddr, err := scionLayer.SrcAddr()
 			if err != nil {
-				return serrors.WrapStr("unable to extract source address", err)
+				return serrors.WrapStr("extracting source address", err)
 			}
 			s.SrcHost, err = netAddrToHostAddr(srcAddr)
 			if err != nil {
-				return serrors.WrapStr("unable to convert address to HostAddr", err)
+				return serrors.WrapStr("converting src address to HostAddr", err)
 			}
 
 			pathCopy := make([]byte, scionLayer.Path.Len())
-			if err := scionLayer.Path.SerializeTo(pathCopy); err != nil {
-				return serrors.WrapStr("unable to extract path", err)
+			// A path of length 4 is an empty path, because it only contains the mandatory
+			// minimal header. Applications model empty paths via nil, so we return nil here.
+			if len(pathCopy) != 4 {
+				if err := scionLayer.Path.SerializeTo(pathCopy); err != nil {
+					return serrors.WrapStr("extracting path", err)
+				}
+				s.Path = spath.NewV2(pathCopy, scionLayer.PathType == slayers.PathTypeOneHop)
+			} else {
+				s.Path = nil
 			}
-			s.Path = spath.NewV2(pathCopy)
 		case slayers.LayerTypeSCIONUDP:
 			s.L4 = &l4.UDP{
 				SrcPort:  uint16(udpLayer.SrcPort),
 				DstPort:  uint16(udpLayer.DstPort),
 				TotalLen: udpLayer.Length,
-				Checksum: []byte{byte(udpLayer.Checksum % 256), byte(udpLayer.Checksum / 256)},
+				Checksum: []byte{byte(udpLayer.Checksum / 256), byte(udpLayer.Checksum % 256)},
 			}
 			s.Pld = common.RawBytes(payloadLayer.Payload())
 		case slayers.LayerTypeSCMP:
@@ -342,7 +348,14 @@ func ParseScnPkt2(s *spkt.ScnPkt, b []byte) error {
 				Checksum:  scmpLayer.Checksum,
 				Timestamp: scmpLayer.Timestamp,
 			}
-			s.Pld = common.RawBytes(scmpLayer.Payload)
+			pld, err := scmp.PldFromRaw(scmpLayer.Payload, scmp.ClassType{
+				Class: scmpLayer.Class,
+				Type:  scmpLayer.Type,
+			})
+			if err != nil {
+				return err
+			}
+			s.Pld = pld
 		}
 	}
 	return nil
diff --git a/go/lib/hpkt/write.go b/go/lib/hpkt/write.go
index 3c9e21fe5..edc324366 100644
--- a/go/lib/hpkt/write.go
+++ b/go/lib/hpkt/write.go
@@ -32,7 +32,10 @@ import (
 	"github.com/scionproto/scion/go/lib/scmp"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/slayers"
+	"github.com/scionproto/scion/go/lib/slayers/path"
+	"github.com/scionproto/scion/go/lib/slayers/path/onehop"
 	"github.com/scionproto/scion/go/lib/slayers/path/scion"
+	"github.com/scionproto/scion/go/lib/spath"
 	"github.com/scionproto/scion/go/lib/spkt"
 	"github.com/scionproto/scion/go/lib/util"
 )
@@ -194,6 +197,9 @@ func WriteScnPkt2(s *spkt.ScnPkt, b []byte) (int, error) {
 	var packetLayers []gopacket.SerializableLayer
 
 	var scionLayer slayers.SCION
+	// XXX(scrye): Set version 2 for debugging, although this is not part of the spec. This
+	// should be removed once the migration to the new SCION header is finished.
+	scionLayer.Version = 2
 	// XXX(scrye): Do not set TrafficClass and FlowID, even though the latter is mandatory,
 	// to keep things simple while we transition to HeaderV2. These should be added once
 	// the transition is complete.
@@ -201,34 +207,90 @@ func WriteScnPkt2(s *spkt.ScnPkt, b []byte) (int, error) {
 	scionLayer.SrcIA = s.SrcIA
 	netDstAddr, err := hostAddrToNetAddr(s.DstHost)
 	if err != nil {
-		return 0, serrors.WrapStr("unable to convert destination addr.HostAddr to net.Addr", err,
+		return 0, serrors.WrapStr("converting destination addr.HostAddr to net.Addr", err,
 			"address", s.DstHost)
 	}
 	if err := scionLayer.SetDstAddr(netDstAddr); err != nil {
-		return 0, serrors.WrapStr("unable to set destination address", err)
+		return 0, serrors.WrapStr("setting destination address", err)
 	}
 	netSrcAddr, err := hostAddrToNetAddr(s.SrcHost)
 	if err != nil {
-		return 0, serrors.WrapStr("unable to convert source addr.HostAddr to net.Addr", err,
+		return 0, serrors.WrapStr("converting source addr.HostAddr to net.Addr", err,
 			"address", s.SrcHost)
 	}
 	if err := scionLayer.SetSrcAddr(netSrcAddr); err != nil {
-		return 0, serrors.WrapStr("unable to set source address", err)
+		return 0, serrors.WrapStr("settting source address", err)
 	}
 	scionLayer.PathType = slayers.PathTypeSCION
 
-	// Use decoded for simplicity, easier to work with when debugging with delve.
-	var decodedPath scion.Decoded
-	if err := decodedPath.DecodeFromBytes(s.Path.Raw); err != nil {
-		return 0, nil
+	isOneHop := func() bool {
+		if len(s.HBHExt) != 0 {
+			_, ok := s.HBHExt[0].(*deprecatedlayers.ExtnOHP)
+			return ok
+		}
+		return false
+	}
+	if isOneHop() {
+		if !s.Path.IsHeaderV2() {
+			info, err := s.Path.GetInfoField(0)
+			if err != nil {
+				return 0, serrors.WrapStr("extracing one hop info field", err)
+			}
+			hf, err := s.Path.GetHopField(spath.InfoFieldLength)
+			if err != nil {
+				return 0, serrors.WrapStr("extracting one hop hop field", err)
+			}
+			scionLayer.PathType = slayers.PathTypeOneHop
+			scionLayer.Path = &onehop.Path{
+				Info: path.InfoField{
+					ConsDir:   true,
+					Timestamp: info.TsInt,
+				},
+				FirstHop: path.HopField{
+					ConsEgress: uint16(hf.ConsEgress),
+					ExpTime:    uint8(hf.ExpTime),
+				},
+			}
+		} else {
+			var path onehop.Path
+			if err := path.DecodeFromBytes(s.Path.Raw); err != nil {
+				return 0, serrors.WrapStr("decoding path", err)
+			}
+			scionLayer.PathType = slayers.PathTypeOneHop
+			scionLayer.Path = &path
+		}
+	} else {
+		switch {
+		case s.Path == nil:
+			// Default nil paths to an empty SCION path
+			decodedPath := scion.Decoded{
+				Base: scion.Base{
+					PathMeta: scion.MetaHdr{},
+				},
+			}
+			scionLayer.Path = &decodedPath
+		case s.Path.IsHeaderV2() && s.Path.IsOHP():
+			var path onehop.Path
+			if err := path.DecodeFromBytes(s.Path.Raw); err != nil {
+				return 0, serrors.WrapStr("decoding path", err)
+			}
+			scionLayer.PathType = slayers.PathTypeOneHop
+			scionLayer.Path = &path
+		default:
+			// Use decoded for simplicity, easier to work with when debugging with delve.
+			var decodedPath scion.Decoded
+			if err := decodedPath.DecodeFromBytes(s.Path.Raw); err != nil {
+				return 0, serrors.WrapStr("decoding path", err)
+			}
+			scionLayer.Path = &decodedPath
+		}
 	}
-	scionLayer.Path = &decodedPath
 	packetLayers = append(packetLayers, &scionLayer)
 
 	// XXX(scrye): No extensions are defined for the V2 header format. However,
 	// application code uses some V1 extensions like the One-Hop Path, and these
 	// will need to be converted for V2 to the new One-Hop path type.
-	if len(s.HBHExt) != 0 {
+	if len(s.HBHExt) != 0 && !isOneHop() {
 		return 0, serrors.New("HBH extensions are not supported for Header V2")
 	}
 	if len(s.E2EExt) != 0 {
@@ -248,14 +310,24 @@ func WriteScnPkt2(s *spkt.ScnPkt, b []byte) (int, error) {
 		var scmpLayer slayers.SCMP
 		scmpLayer.Class = layer.Class
 		scmpLayer.Type = layer.Type
-		scmpLayer.TotalLen = layer.TotalLen
+		if layer.TotalLen == 0 {
+			scmpLayer.TotalLen = uint16(scmp.HdrLen + s.Pld.Len())
+		} else {
+			scmpLayer.TotalLen = layer.TotalLen
+		}
 		scmpLayer.SetNetworkLayerForChecksum(&scionLayer)
 		scmpLayer.Timestamp = layer.Timestamp
-		scmpLayer.Payload = []byte(s.Pld.(common.RawBytes))
+		buf := make([]byte, s.Pld.Len())
+		if _, err := s.Pld.WritePld(buf); err != nil {
+			return 0, serrors.WrapStr("writing SCMP payload", err)
+		}
+		scmpLayer.Payload = buf
 		packetLayers = append(packetLayers, &scmpLayer)
 	}
-	payloadLayer := gopacket.Payload(s.Pld.(common.RawBytes))
-	packetLayers = append(packetLayers, &payloadLayer)
+	if _, ok := s.L4.(*scmp.Hdr); !ok {
+		payloadLayer := gopacket.Payload(s.Pld.(common.RawBytes))
+		packetLayers = append(packetLayers, &payloadLayer)
+	}
 
 	buffer := gopacket.NewSerializeBuffer()
 	options := gopacket.SerializeOptions{
diff --git a/go/lib/infra/infraenv/infraenv.go b/go/lib/infra/infraenv/infraenv.go
index 50e5146b3..813dc941c 100644
--- a/go/lib/infra/infraenv/infraenv.go
+++ b/go/lib/infra/infraenv/infraenv.go
@@ -221,6 +221,7 @@ func (nc *NetworkConfig) initQUICSocket() (net.PacketConn, error) {
 			SCMPHandler: ignoreSCMP{},
 			Version2:    nc.Version2,
 		},
+		Version2: nc.Version2,
 	}
 	udpAddr, err := net.ResolveUDPAddr("udp", nc.QUIC.Address)
 	if err != nil {
diff --git a/go/lib/infra/modules/segfetcher/pather.go b/go/lib/infra/modules/segfetcher/pather.go
index be8d0f6b1..30e3e6915 100644
--- a/go/lib/infra/modules/segfetcher/pather.go
+++ b/go/lib/infra/modules/segfetcher/pather.go
@@ -42,6 +42,7 @@ type Pather struct {
 	RevCache     revcache.RevCache
 	TopoProvider topology.Provider
 	Fetcher      *Fetcher
+	HeaderV2     bool
 }
 
 // GetPaths returns all non-revoked and non-expired paths to the destination.
@@ -89,6 +90,7 @@ func (p *Pather) buildAllPaths(src, dst addr.IA, segs Segments) []*combinator.Pa
 	var validPaths []*combinator.Path
 	for _, path := range paths {
 		if path.ComputeExpTime().After(now) {
+			path.HeaderV2 = p.HeaderV2
 			validPaths = append(validPaths, path)
 		}
 	}
diff --git a/go/lib/l4/common.go b/go/lib/l4/common.go
index cf13b3f10..c75d564cd 100644
--- a/go/lib/l4/common.go
+++ b/go/lib/l4/common.go
@@ -60,11 +60,15 @@ func SetCSum(h L4Header, addr, pld common.RawBytes) error {
 }
 
 func CheckCSum(h L4Header, addr, pld common.RawBytes) error {
+	exp := h.GetCSum()
+	// UDP allows zero check sums when used in tunnels.
+	if h.L4Type() == common.L4UDP && bytes.Compare(exp, []byte{0, 0}) == 0 {
+		return nil
+	}
 	calc, err := CalcCSum(h, addr, pld)
 	if err != nil {
 		return err
 	}
-	exp := h.GetCSum()
 	if bytes.Compare(exp, calc) != 0 {
 		return common.NewBasicError(ErrInvalidChksum, nil,
 			"expected", exp, "actual", calc, "proto", h.L4Type())
diff --git a/go/lib/layers/BUILD.bazel b/go/lib/layers/BUILD.bazel
index 35870e990..50bd7cdcb 100644
--- a/go/lib/layers/BUILD.bazel
+++ b/go/lib/layers/BUILD.bazel
@@ -3,7 +3,6 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 go_library(
     name = "go_default_library",
     srcs = [
-        "debug_extn.go",
         "extensions.go",
         "extensions_layer.go",
     ],
diff --git a/go/lib/layers/debug_extn.go b/go/lib/layers/debug_extn.go
deleted file mode 100644
index 9b39a8699..000000000
--- a/go/lib/layers/debug_extn.go
+++ /dev/null
@@ -1,79 +0,0 @@
-// 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 layers
-
-import "github.com/scionproto/scion/go/lib/common"
-
-var _ common.Extension = (*ExtnE2EDebug)(nil)
-
-type ExtnE2EDebug struct {
-	ID [5]byte
-}
-
-const E2EDebugLen = common.ExtnFirstLineLen
-
-func NewExtnE2EDebugFromLayer(extension *Extension) (*ExtnE2EDebug, error) {
-	var extn ExtnE2EDebug
-	if err := extn.DecodeFromLayer(extension); err != nil {
-		return nil, err
-	}
-	return &extn, nil
-}
-
-func (o *ExtnE2EDebug) DecodeFromLayer(extension *Extension) error {
-	if len(extension.Data) != common.LineLen-3 {
-		return common.NewBasicError("bad length for E2E debug extension", nil,
-			"actual", len(extension.Data), "want", common.LineLen-3)
-	}
-	copy(o.ID[:], extension.Data)
-	return nil
-}
-
-func (o ExtnE2EDebug) Write(b common.RawBytes) error {
-	copy(b[:5], o.ID[:])
-	return nil
-}
-
-func (o ExtnE2EDebug) Pack() (common.RawBytes, error) {
-	b := make(common.RawBytes, o.Len())
-	if err := o.Write(b); err != nil {
-		return nil, err
-	}
-	return b, nil
-}
-
-func (o ExtnE2EDebug) Copy() common.Extension {
-	return &ExtnE2EDebug{}
-}
-
-func (o ExtnE2EDebug) Reverse() (bool, error) {
-	return true, nil
-}
-
-func (o ExtnE2EDebug) Len() int {
-	return E2EDebugLen
-}
-
-func (o ExtnE2EDebug) Class() common.L4ProtocolType {
-	return common.End2EndClass
-}
-
-func (o ExtnE2EDebug) Type() common.ExtnType {
-	return common.ExtnE2EDebugType
-}
-
-func (o ExtnE2EDebug) String() string {
-	return "E2EDebug"
-}
diff --git a/go/lib/layers/extensions.go b/go/lib/layers/extensions.go
index bf3313e0a..f4e6759d2 100644
--- a/go/lib/layers/extensions.go
+++ b/go/lib/layers/extensions.go
@@ -33,8 +33,6 @@ func ExtensionFactory(class common.L4ProtocolType, extension *Extension) (common
 		}
 	case common.End2EndClass:
 		switch extension.Type {
-		case common.ExtnE2EDebugType.Type:
-			return NewExtnE2EDebugFromLayer(extension)
 		default:
 			return NewExtnUnknownFromLayer(common.End2EndClass, extension)
 		}
diff --git a/go/lib/sciond/apitypes.go b/go/lib/sciond/apitypes.go
index db2439cde..77c934812 100644
--- a/go/lib/sciond/apitypes.go
+++ b/go/lib/sciond/apitypes.go
@@ -141,7 +141,7 @@ func pathReplyEntryToPath(pe PathReplyEntry, dst addr.IA) (Path, error) {
 			return Path{}, serrors.WrapStr("path error", err)
 		}
 	} else {
-		sp = spath.NewV2(pe.Path.FwdPath)
+		sp = spath.NewV2(pe.Path.FwdPath, false)
 	}
 
 	underlayAddr := pe.HostInfo.Underlay()
diff --git a/go/lib/sciond/pathprobe/paths.go b/go/lib/sciond/pathprobe/paths.go
index 89601ae55..c558d8b8f 100644
--- a/go/lib/sciond/pathprobe/paths.go
+++ b/go/lib/sciond/pathprobe/paths.go
@@ -124,6 +124,7 @@ func (p Prober) GetStatuses(ctx context.Context,
 			SCMPHandler: scmpH,
 			Version2:    p.Version2,
 		},
+		Version2: p.Version2,
 	}
 	snetConn, err := network.Listen(ctx, "udp", &net.UDPAddr{IP: p.LocalIP}, addr.SvcNone)
 	if err != nil {
diff --git a/go/lib/slayers/path/BUILD.bazel b/go/lib/slayers/path/BUILD.bazel
index 5cd30a404..eca937663 100644
--- a/go/lib/slayers/path/BUILD.bazel
+++ b/go/lib/slayers/path/BUILD.bazel
@@ -8,7 +8,10 @@ go_library(
     ],
     importpath = "github.com/scionproto/scion/go/lib/slayers/path",
     visibility = ["//visibility:public"],
-    deps = ["//go/lib/serrors:go_default_library"],
+    deps = [
+        "//go/lib/serrors:go_default_library",
+        "//go/lib/util:go_default_library",
+    ],
 )
 
 go_test(
diff --git a/go/lib/slayers/path/infofield.go b/go/lib/slayers/path/infofield.go
index 97eeaeae0..753ef6a92 100644
--- a/go/lib/slayers/path/infofield.go
+++ b/go/lib/slayers/path/infofield.go
@@ -16,8 +16,10 @@ package path
 
 import (
 	"encoding/binary"
+	"fmt"
 
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/lib/util"
 )
 
 // InfoLen is the size of an InfoField in bytes.
@@ -84,3 +86,8 @@ func (inf *InfoField) SerializeTo(b []byte) error {
 
 	return nil
 }
+
+func (inf *InfoField) String() string {
+	return fmt.Sprintf("{Peer: %t, ConsDir: %t, SegID: %d, Timestamp: %s}",
+		inf.Peer, inf.ConsDir, inf.SegID, util.SecsToCompact(inf.Timestamp))
+}
diff --git a/go/lib/slayers/path/onehop/BUILD.bazel b/go/lib/slayers/path/onehop/BUILD.bazel
index ac8508ec8..06366e233 100644
--- a/go/lib/slayers/path/onehop/BUILD.bazel
+++ b/go/lib/slayers/path/onehop/BUILD.bazel
@@ -8,6 +8,7 @@ go_library(
     deps = [
         "//go/lib/serrors:go_default_library",
         "//go/lib/slayers/path:go_default_library",
+        "//go/lib/slayers/path/scion:go_default_library",
     ],
 )
 
@@ -17,6 +18,7 @@ go_test(
     embed = [":go_default_library"],
     deps = [
         "//go/lib/slayers/path:go_default_library",
+        "//go/lib/slayers/path/scion:go_default_library",
         "@com_github_stretchr_testify//assert:go_default_library",
     ],
 )
diff --git a/go/lib/slayers/path/onehop/onehop.go b/go/lib/slayers/path/onehop/onehop.go
index f166580b0..73586e393 100644
--- a/go/lib/slayers/path/onehop/onehop.go
+++ b/go/lib/slayers/path/onehop/onehop.go
@@ -17,6 +17,7 @@ package onehop
 import (
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/slayers/path"
+	"github.com/scionproto/scion/go/lib/slayers/path/scion"
 )
 
 // PathLen is the length of a serialized one hop path in bytes
@@ -65,6 +66,49 @@ func (o *Path) SerializeTo(b []byte) error {
 	return o.SecondHop.SerializeTo(b[offset : offset+path.HopLen])
 }
 
+// ToSCIONDecoded converts the one hop path in to a normal SCION path in the
+// decoded format.
+func (o *Path) ToSCIONDecoded() (*scion.Decoded, error) {
+	if o.SecondHop.ConsIngress == 0 {
+		return nil, serrors.New("incomplete path can't be converted")
+	}
+	p := &scion.Decoded{
+		Base: scion.Base{
+			PathMeta: scion.MetaHdr{
+				SegLen: [3]uint8{2, 0, 0},
+			},
+			NumHops: 2,
+			NumINF:  1,
+		},
+		InfoFields: []*path.InfoField{
+			{
+				ConsDir:   true,
+				SegID:     o.Info.SegID,
+				Timestamp: o.Info.Timestamp,
+			},
+		},
+		HopFields: []*path.HopField{
+			{
+				IngressRouterAlert: o.FirstHop.IngressRouterAlert,
+				EgressRouterAlert:  o.FirstHop.EgressRouterAlert,
+				ConsIngress:        o.FirstHop.ConsIngress,
+				ConsEgress:         o.FirstHop.ConsEgress,
+				ExpTime:            o.FirstHop.ExpTime,
+				Mac:                append([]byte(nil), o.FirstHop.Mac...),
+			},
+			{
+				IngressRouterAlert: o.SecondHop.IngressRouterAlert,
+				EgressRouterAlert:  o.SecondHop.EgressRouterAlert,
+				ConsIngress:        o.SecondHop.ConsIngress,
+				ConsEgress:         o.SecondHop.ConsEgress,
+				ExpTime:            o.SecondHop.ExpTime,
+				Mac:                append([]byte(nil), o.SecondHop.Mac...),
+			},
+		},
+	}
+	return p, nil
+}
+
 func (o *Path) Reverse() error {
 	return serrors.New("OneHop path cannot be reversed")
 }
diff --git a/go/lib/slayers/path/scion/base.go b/go/lib/slayers/path/scion/base.go
index 17d4672ef..da0402a47 100644
--- a/go/lib/slayers/path/scion/base.go
+++ b/go/lib/slayers/path/scion/base.go
@@ -26,8 +26,8 @@ const MetaLen = 4
 
 // Base holds the basic information that is used by both raw and fully decoded paths.
 type Base struct {
-	// PathMeta is the SCION path meta header. It is always instantiated in when decoding a path
-	// from bytes.
+	// PathMeta is the SCION path meta header. It is always instantiated when
+	// decoding a path from bytes.
 	PathMeta MetaHdr
 	// NumINF is the number of InfoFields in the path.
 	NumINF int
@@ -64,20 +64,33 @@ func (s *Base) IncPath() error {
 	if s.NumINF == 0 {
 		return serrors.New("empty path cannot be increased")
 	}
-	if int(s.PathMeta.CurrHF) == s.NumHops-1 {
+	if int(s.PathMeta.CurrHF) >= s.NumHops-1 {
+		s.PathMeta.CurrHF = uint8(s.NumHops - 1)
 		return serrors.New("path already at end")
 	}
 	s.PathMeta.CurrHF++
 	// Update CurrINF
+	s.PathMeta.CurrINF = s.infIndexForHF(s.PathMeta.CurrHF)
+	return nil
+}
+
+// IsXover returns whether we are at a crossover point.
+func (s *Base) IsXover() bool {
+	return s.PathMeta.CurrINF != s.infIndexForHF(s.PathMeta.CurrHF+1)
+}
+
+func (s *Base) infIndexForHF(hf uint8) uint8 {
 	left := uint8(0)
-	for i := 0; i < int(s.NumINF); i++ {
-		if s.PathMeta.CurrHF >= left && s.PathMeta.CurrHF < left+s.PathMeta.SegLen[i] {
-			s.PathMeta.CurrINF = uint8(i)
-			break
+	for i := 0; i < s.NumINF; i++ {
+		if hf >= left {
+			if hf < left+s.PathMeta.SegLen[i] {
+				return uint8(i)
+			}
 		}
 		left += s.PathMeta.SegLen[i]
 	}
-	return nil
+	// at the end we just return the last index.
+	return uint8(s.NumINF - 1)
 }
 
 // MetaHdr is the PathMetaHdr of a SCION (data-plane) path type.
@@ -117,3 +130,7 @@ func (m *MetaHdr) SerializeTo(b []byte) error {
 
 	return nil
 }
+
+func (m MetaHdr) String() string {
+	return fmt.Sprintf("{CurrInf: %d, CurrHF: %d, SegLen: %v}", m.CurrINF, m.CurrHF, m.SegLen)
+}
diff --git a/go/lib/slayers/scion.go b/go/lib/slayers/scion.go
index ed5218729..97741d9eb 100644
--- a/go/lib/slayers/scion.go
+++ b/go/lib/slayers/scion.go
@@ -80,7 +80,7 @@ const (
 // four possible types per address length.
 type AddrType uint8
 
-// AddrLen4 types
+// AddrType constants
 const (
 	T4Ip AddrType = iota
 	T4Svc
@@ -252,6 +252,10 @@ func (s *SCION) DecodeFromBytes(data []byte, df gopacket.DecodeFeedback) error {
 	var err error
 	hdrBytes := int(s.HdrLen) * LineLen
 	pathLen := hdrBytes - CmnHdrLen - addrHdrLen
+	if pathLen < 0 {
+		return serrors.New("invalid header, negative pathLen",
+			"hdrBytes", hdrBytes, "addrHdrLen", addrHdrLen, "CmdHdrLen", CmnHdrLen)
+	}
 	switch s.PathType {
 	case PathTypeSCION:
 		// Only allocate a SCION path if necessary. This reduces memory allocation and GC overhead
diff --git a/go/lib/snet/addrutil/addrutil.go b/go/lib/snet/addrutil/addrutil.go
index e197fed68..dad65c253 100644
--- a/go/lib/snet/addrutil/addrutil.go
+++ b/go/lib/snet/addrutil/addrutil.go
@@ -57,7 +57,7 @@ func (p Pather) GetPath(svc addr.HostSVC, ps *seg.PathSegment) (net.Addr, error)
 		beta = beta ^ binary.BigEndian.Uint16(entry.HopEntries[0].HopField.MAC[:2])
 	}
 
-	hops := len(hopFields) - 1
+	hops := len(hopFields)
 	dec := scion.Decoded{
 		Base: scion.Base{
 			PathMeta: scion.MetaHdr{
@@ -86,7 +86,7 @@ func (p Pather) GetPath(svc addr.HostSVC, ps *seg.PathSegment) (net.Addr, error)
 	}
 	return &snet.SVCAddr{
 		IA:      ps.FirstIA(),
-		Path:    spath.NewV2(raw),
+		Path:    spath.NewV2(raw, false),
 		NextHop: nextHop,
 		SVC:     svc,
 	}, nil
@@ -98,11 +98,11 @@ type LegacyPather struct {
 }
 
 func (p LegacyPather) GetPath(svc addr.HostSVC, ps *seg.PathSegment) (net.Addr, error) {
-	return GetPath(svc, ps, p.TopoProvider)
+	return getPath(svc, ps, p.TopoProvider)
 }
 
 // GetPath creates a path from the given segment and then creates a snet.SVCAddr.
-func GetPath(svc addr.HostSVC, ps *seg.PathSegment, topoProv topology.Provider) (net.Addr, error) {
+func getPath(svc addr.HostSVC, ps *seg.PathSegment, topoProv topology.Provider) (net.Addr, error) {
 	p, err := legacyPath(ps)
 	if err != nil {
 		return nil, serrors.WrapStr("constructing path from segment", err)
diff --git a/go/lib/snet/mock_snet/BUILD.bazel b/go/lib/snet/mock_snet/BUILD.bazel
index 405e3b838..d76fbe622 100644
--- a/go/lib/snet/mock_snet/BUILD.bazel
+++ b/go/lib/snet/mock_snet/BUILD.bazel
@@ -7,6 +7,7 @@ go_library(
     visibility = ["//visibility:public"],
     deps = [
         "//go/lib/addr:go_default_library",
+        "//go/lib/common:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/spath:go_default_library",
         "@com_github_golang_mock//gomock:go_default_library",
diff --git a/go/lib/snet/mock_snet/snet.go b/go/lib/snet/mock_snet/snet.go
index 207046925..697720970 100644
--- a/go/lib/snet/mock_snet/snet.go
+++ b/go/lib/snet/mock_snet/snet.go
@@ -1,5 +1,5 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/scionproto/scion/go/lib/snet (interfaces: PacketDispatcherService,Network,PacketConn,Path,PathQuerier,Router)
+// Source: github.com/scionproto/scion/go/lib/snet (interfaces: PacketDispatcherService,Network,PacketConn,Path,PathQuerier,Router,RevocationHandler)
 
 // Package mock_snet is a generated GoMock package.
 package mock_snet
@@ -8,6 +8,7 @@ import (
 	context "context"
 	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"
 	spath "github.com/scionproto/scion/go/lib/spath"
 	net "net"
@@ -439,3 +440,38 @@ func (mr *MockRouterMockRecorder) Route(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Route", reflect.TypeOf((*MockRouter)(nil).Route), arg0, arg1)
 }
+
+// MockRevocationHandler is a mock of RevocationHandler interface
+type MockRevocationHandler struct {
+	ctrl     *gomock.Controller
+	recorder *MockRevocationHandlerMockRecorder
+}
+
+// MockRevocationHandlerMockRecorder is the mock recorder for MockRevocationHandler
+type MockRevocationHandlerMockRecorder struct {
+	mock *MockRevocationHandler
+}
+
+// NewMockRevocationHandler creates a new mock instance
+func NewMockRevocationHandler(ctrl *gomock.Controller) *MockRevocationHandler {
+	mock := &MockRevocationHandler{ctrl: ctrl}
+	mock.recorder = &MockRevocationHandlerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockRevocationHandler) EXPECT() *MockRevocationHandlerMockRecorder {
+	return m.recorder
+}
+
+// RevokeRaw mocks base method
+func (m *MockRevocationHandler) RevokeRaw(arg0 context.Context, arg1 common.RawBytes) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "RevokeRaw", arg0, arg1)
+}
+
+// RevokeRaw indicates an expected call of RevokeRaw
+func (mr *MockRevocationHandlerMockRecorder) RevokeRaw(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeRaw", reflect.TypeOf((*MockRevocationHandler)(nil).RevokeRaw), arg0, arg1)
+}
diff --git a/go/lib/snet/path.go b/go/lib/snet/path.go
index 1ca1b4c6f..9a687b073 100644
--- a/go/lib/snet/path.go
+++ b/go/lib/snet/path.go
@@ -15,6 +15,7 @@
 package snet
 
 import (
+	"fmt"
 	"net"
 	"time"
 
@@ -124,3 +125,7 @@ func (p *partialPath) Copy() Path {
 		destination: p.destination,
 	}
 }
+
+func (p *partialPath) String() string {
+	return fmt.Sprintf("{spath: %s, underlay: %s, dest: %s}", p.spath, p.underlay, p.destination)
+}
diff --git a/go/lib/snet/snet.go b/go/lib/snet/snet.go
index dd7b7c890..f34ed9b9e 100644
--- a/go/lib/snet/snet.go
+++ b/go/lib/snet/snet.go
@@ -60,6 +60,7 @@ var _ Network = (*SCIONNetwork)(nil)
 type SCIONNetwork struct {
 	LocalIA    addr.IA
 	Dispatcher PacketDispatcherService
+	Version2   bool
 }
 
 // NewNetwork creates a new networking context.
@@ -137,6 +138,7 @@ func (n *SCIONNetwork) Listen(ctx context.Context, network string, listen *net.U
 		scionNet: n,
 		svc:      svc,
 		listen:   CopyUDPAddr(listen),
+		version2: n.Version2,
 	}
 	packetConn, port, err := conn.scionNet.Dispatcher.Register(ctx, n.LocalIA, listen, svc)
 	if err != nil {
diff --git a/go/lib/snet/udpaddr.go b/go/lib/snet/udpaddr.go
index e8349dd6b..dfee6b460 100644
--- a/go/lib/snet/udpaddr.go
+++ b/go/lib/snet/udpaddr.go
@@ -107,7 +107,7 @@ func (a *UDPAddr) GetPath() (Path, error) {
 	}, nil
 }
 
-// Set implements the flag.Value interface
+// Set is used to satisfy the flag.Value and pflag.Value interface.
 func (a *UDPAddr) Set(s string) error {
 	other, err := ParseUDPAddr(s)
 	if err != nil {
@@ -117,6 +117,11 @@ func (a *UDPAddr) Set(s string) error {
 	return nil
 }
 
+// Type is used to satisfy the pflag.Value interface.
+func (a *UDPAddr) Type() string {
+	return "SCION UDP Address"
+}
+
 func CopyUDPAddr(a *net.UDPAddr) *net.UDPAddr {
 	if a == nil {
 		return nil
diff --git a/go/lib/spath/BUILD.bazel b/go/lib/spath/BUILD.bazel
index c76a89a59..c7a1eebb4 100644
--- a/go/lib/spath/BUILD.bazel
+++ b/go/lib/spath/BUILD.bazel
@@ -13,6 +13,7 @@ go_library(
         "//go/lib/addr:go_default_library",
         "//go/lib/common:go_default_library",
         "//go/lib/serrors:go_default_library",
+        "//go/lib/slayers/path/onehop:go_default_library",
         "//go/lib/slayers/path/scion:go_default_library",
         "//go/lib/util:go_default_library",
     ],
diff --git a/go/lib/spath/path.go b/go/lib/spath/path.go
index f51911074..913478336 100644
--- a/go/lib/spath/path.go
+++ b/go/lib/spath/path.go
@@ -16,6 +16,7 @@
 package spath
 
 import (
+	"fmt"
 	"hash"
 	"math"
 	"time"
@@ -23,6 +24,7 @@ import (
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/lib/slayers/path/onehop"
 	"github.com/scionproto/scion/go/lib/slayers/path/scion"
 	"github.com/scionproto/scion/go/lib/util"
 )
@@ -44,14 +46,15 @@ type Path struct {
 
 	// version is a temporary solution for supporting V2 paths in method calls.
 	version int
+	ohp     bool
 }
 
 func New(raw common.RawBytes) *Path {
 	return &Path{Raw: raw}
 }
 
-func NewV2(raw common.RawBytes) *Path {
-	return &Path{Raw: raw, version: 2}
+func NewV2(raw []byte, ohp bool) *Path {
+	return &Path{Raw: raw, version: 2, ohp: ohp}
 }
 
 // NewOneHop creates a new one hop path with. If necessary, the caller has
@@ -85,13 +88,34 @@ func (p *Path) Copy() *Path {
 		InfOff:  p.InfOff,
 		HopOff:  p.HopOff,
 		version: p.version,
+		ohp:     p.ohp,
 	}
 }
 
 func (p *Path) reverse2() error {
 	var path scion.Decoded
-	if err := path.DecodeFromBytes(p.Raw); err != nil {
-		return err
+	if p.ohp {
+		//  Since a OHP can't be reversed we create a proper SCION path instead,
+		//  and reverse that.
+		var ohp onehop.Path
+		if err := ohp.DecodeFromBytes(p.Raw); err != nil {
+			return serrors.WrapStr("decoding v2 OHP path", err)
+		}
+		sp, err := ohp.ToSCIONDecoded()
+		if err != nil {
+			return serrors.WrapStr("converting to scion path", err)
+		}
+		// increment the path, since we are at the receiver side.
+		if err := sp.IncPath(); err != nil {
+			return serrors.WrapStr("incrementing path", err)
+		}
+		path = *sp
+		p.Raw = make([]byte, sp.Len())
+		p.ohp = false
+	} else {
+		if err := path.DecodeFromBytes(p.Raw); err != nil {
+			return err
+		}
 	}
 	if err := path.Reverse(); err != nil {
 		return err
@@ -168,6 +192,15 @@ func (p *Path) Reverse() error {
 	return nil
 }
 
+// IsHeaderV2 returns whether the path is in the new format.
+func (path *Path) IsHeaderV2() bool {
+	return path.version == 2
+}
+
+func (path *Path) IsOHP() bool {
+	return path.ohp
+}
+
 // InitOffsets computes the initial Hop Field offset (in bytes) for a newly
 // created packet.
 func (path *Path) InitOffsets() error {
@@ -284,3 +317,27 @@ func (path *Path) GetHopField(offset int) (*HopField, error) {
 	}
 	return hopF, nil
 }
+
+func (path *Path) String() string {
+	var p string
+	switch {
+	case path.version == 2 && path.ohp:
+		var op onehop.Path
+		if err := op.DecodeFromBytes(path.Raw); err != nil {
+			p = fmt.Sprintf("err decoding: %v", err)
+		} else {
+			p = fmt.Sprintf("{consdir: %t, 1st: %s, 2nd %s", op.Info.ConsDir,
+				fmt.Sprintf("I: %d, E: %d", op.FirstHop.ConsIngress, op.FirstHop.ConsEgress),
+				fmt.Sprintf("I: %d, E: %d", op.SecondHop.ConsIngress, op.SecondHop.ConsEgress))
+		}
+	case path.version == 2:
+		var sp scion.Decoded
+		if err := sp.DecodeFromBytes(path.Raw); err != nil {
+			p = fmt.Sprintf("err decoding: %v", err)
+		} else {
+			p = fmt.Sprintf("{Meta: %s, NumINF: %d, NumHops: %d}",
+				sp.PathMeta, sp.NumINF, sp.NumHops)
+		}
+	}
+	return fmt.Sprintf("{version: %d, ohp: %t, p: %s}", path.version, path.ohp, p)
+}
diff --git a/go/pkg/cs/main.go b/go/pkg/cs/main.go
index 488e7905a..06722b02b 100644
--- a/go/pkg/cs/main.go
+++ b/go/pkg/cs/main.go
@@ -282,6 +282,7 @@ func (app *App) Run() int {
 			PathDB:       pathDB,
 			RevCache:     revCache,
 			TopoProvider: itopo.Provider(),
+			HeaderV2:     Cfg.Features.HeaderV2,
 			// Fetcher needs to be initialized with a provider.
 		},
 	}
@@ -309,6 +310,7 @@ func (app *App) Run() int {
 		IA:            topo.IA(),
 		TopoProvider:  itopo.Provider(),
 		SegRequestAPI: msgr,
+		HeaderV2:      Cfg.Features.HeaderV2,
 	}
 
 	trustRouter.Pather.Fetcher = segfetcher.FetcherConfig{
diff --git a/go/pkg/ping/BUILD.bazel b/go/pkg/ping/BUILD.bazel
index 93ca44e6e..d6277168d 100644
--- a/go/pkg/ping/BUILD.bazel
+++ b/go/pkg/ping/BUILD.bazel
@@ -2,17 +2,22 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
 
 go_library(
     name = "go_default_library",
-    srcs = ["ping.go"],
+    srcs = [
+        "ping.go",
+        "util.go",
+    ],
     importpath = "github.com/scionproto/scion/go/pkg/ping",
     visibility = ["//visibility:public"],
     deps = [
         "//go/lib/addr:go_default_library",
         "//go/lib/common:go_default_library",
+        "//go/lib/hpkt:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/scmp:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/sock/reliable:go_default_library",
+        "//go/lib/spkt:go_default_library",
         "//go/lib/topology/underlay:go_default_library",
     ],
 )
diff --git a/go/pkg/ping/ping.go b/go/pkg/ping/ping.go
index c6eddcae7..b8d24b3ff 100644
--- a/go/pkg/ping/ping.go
+++ b/go/pkg/ping/ping.go
@@ -77,6 +77,8 @@ type Config struct {
 	// Update handler is invoked for every ping reply. Execution time must be
 	// small, as it is run synchronous.
 	UpdateHandler func(Update)
+
+	HeaderV2 bool
 }
 
 // Run ping with the configuration. This blocks until the configured number
@@ -95,6 +97,7 @@ func Run(ctx context.Context, cfg Config) (Stats, error) {
 			id:      id,
 			replies: replies,
 		},
+		Version2: cfg.HeaderV2,
 	}
 	conn, port, err := svc.Register(ctx, cfg.Local.IA, cfg.Local.Host, addr.SvcNone)
 	if err != nil {
@@ -178,34 +181,10 @@ func (p *pinger) Ping(ctx context.Context, remote *snet.UDPAddr) (Stats, error)
 func (p *pinger) send(remote *snet.UDPAddr) error {
 	sequence := p.sentSequence + 1
 
-	info := &scmp.InfoEcho{Id: p.id, Seq: uint16(sequence)}
-	pld := make([]byte, scmp.MetaLen+info.Len()+p.pldSize)
-	meta := scmp.Meta{InfoLen: uint8(info.Len() / common.LineLen)}
-	meta.Write(pld)
-	info.Write(pld[scmp.MetaLen:])
-
-	pkt := &snet.Packet{
-		PacketInfo: snet.PacketInfo{
-			Destination: snet.SCIONAddress{
-				IA:   remote.IA,
-				Host: addr.HostFromIP(remote.Host.IP),
-			},
-			Source: snet.SCIONAddress{
-				IA:   p.local.IA,
-				Host: addr.HostFromIP(p.local.Host.IP),
-			},
-			Path: remote.Path,
-			L4Header: scmp.NewHdr(
-				scmp.ClassType{
-					Class: scmp.C_General,
-					Type:  scmp.T_G_EchoRequest,
-				},
-				len(pld),
-			),
-			Payload: common.RawBytes(pld),
-		},
+	pkt, err := newEcho(p.local, remote, p.pldSize, scmp.InfoEcho{Id: p.id, Seq: uint16(sequence)})
+	if err != nil {
+		return err
 	}
-
 	nextHop := remote.NextHop
 	if nextHop == nil && p.local.IA.Equal(remote.IA) {
 		nextHop = &net.UDPAddr{
diff --git a/go/pkg/ping/util.go b/go/pkg/ping/util.go
new file mode 100644
index 000000000..dc675ce97
--- /dev/null
+++ b/go/pkg/ping/util.go
@@ -0,0 +1,104 @@
+// Copyright 2020 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 ping
+
+import (
+	"github.com/scionproto/scion/go/lib/addr"
+	"github.com/scionproto/scion/go/lib/common"
+	"github.com/scionproto/scion/go/lib/hpkt"
+	"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/spkt"
+)
+
+// SizeLegacy computes the full SCION packet size for an address pair with a
+// given payload size.
+//
+// Deprecated: This calculates the size of the SCMP packet with header v1.
+// This function will be deleted when we switch to header v2 completely.
+func SizeLegacy(local, remote *snet.UDPAddr, pldSize int) (int, error) {
+	pkt, err := newEcho(local, remote, pldSize, scmp.InfoEcho{})
+	if err != nil {
+		return 0, err
+	}
+	raw := make([]byte, common.MaxMTU)
+	n, err := hpkt.WriteScnPkt(translate(pkt), raw)
+	if err != nil {
+		return 0, err
+	}
+	return n, nil
+}
+
+// Size computes the full SCION packet size for an address pair with a given
+// payload size.
+func Size(local, remote *snet.UDPAddr, pldSize int) (int, error) {
+	pkt, err := newEcho(local, remote, pldSize, scmp.InfoEcho{})
+	if err != nil {
+		return 0, err
+	}
+	raw := make([]byte, common.MaxMTU)
+	n, err := hpkt.WriteScnPkt2(translate(pkt), raw)
+	if err != nil {
+		return 0, err
+	}
+	return n, nil
+}
+
+// XXX(roosd): does not handle e2e or hbh extensions.
+func translate(pkt *snet.Packet) *spkt.ScnPkt {
+	return &spkt.ScnPkt{
+		DstIA:   pkt.Destination.IA,
+		SrcIA:   pkt.Source.IA,
+		DstHost: pkt.Destination.Host,
+		SrcHost: pkt.Source.Host,
+		Path:    pkt.Path,
+		L4:      pkt.L4Header,
+		Pld:     pkt.Payload,
+	}
+}
+
+func newEcho(local, remote *snet.UDPAddr, pldSize int, info scmp.InfoEcho) (*snet.Packet, error) {
+	if remote.Path == nil && !local.IA.Equal(remote.IA) {
+		return nil, serrors.New("no path for remote ISD-AS", "local", local.IA, "remote", remote.IA)
+	}
+	pld := make([]byte, scmp.MetaLen+info.Len()+pldSize)
+	meta := scmp.Meta{InfoLen: uint8(info.Len() / common.LineLen)}
+	meta.Write(pld)
+	info.Write(pld[scmp.MetaLen:])
+
+	pkt := &snet.Packet{
+		PacketInfo: snet.PacketInfo{
+			Destination: snet.SCIONAddress{
+				IA:   remote.IA,
+				Host: addr.HostFromIP(remote.Host.IP),
+			},
+			Source: snet.SCIONAddress{
+				IA:   local.IA,
+				Host: addr.HostFromIP(local.Host.IP),
+			},
+			Path: remote.Path,
+			L4Header: scmp.NewHdr(
+				scmp.ClassType{
+					Class: scmp.C_General,
+					Type:  scmp.T_G_EchoRequest,
+				},
+				len(pld),
+			),
+			Payload: common.RawBytes(pld),
+		},
+	}
+	return pkt, nil
+}
diff --git a/go/pkg/sciond/fetcher/fetcher.go b/go/pkg/sciond/fetcher/fetcher.go
index 90ba6f51b..0aa2948a7 100644
--- a/go/pkg/sciond/fetcher/fetcher.go
+++ b/go/pkg/sciond/fetcher/fetcher.go
@@ -61,7 +61,7 @@ type fetcher struct {
 
 func NewFetcher(requestAPI segfetcher.RequestAPI, pathDB pathdb.PathDB, inspector trust.Inspector,
 	verifier infra.Verifier, revCache revcache.RevCache, cfg config.SDConfig,
-	topoProvider topology.Provider) Fetcher {
+	topoProvider topology.Provider, headerV2 bool) Fetcher {
 
 	localIA := topoProvider.Get().IA()
 	return &fetcher{
@@ -84,6 +84,7 @@ func NewFetcher(requestAPI segfetcher.RequestAPI, pathDB pathdb.PathDB, inspecto
 				MetricsNamespace: metrics.Namespace,
 				LocalInfo:        neverLocal{},
 			}.New(),
+			HeaderV2: headerV2,
 		},
 		config: cfg,
 	}
@@ -156,6 +157,7 @@ func (f *fetcher) translate(path *combinator.Path) (sciond.PathReplyEntry, error
 				Mtu:        f.pather.TopoProvider.Get().MTU(),
 				Interfaces: []sciond.PathInterface{},
 				ExpTime:    util.TimeToSecs(time.Now().Add(spath.MaxTTL * time.Second)),
+				HeaderV2:   path.HeaderV2,
 			},
 		}
 		return entry, nil
diff --git a/go/pkg/showpaths/showpaths.go b/go/pkg/showpaths/showpaths.go
index 83fdc2371..e06200090 100644
--- a/go/pkg/showpaths/showpaths.go
+++ b/go/pkg/showpaths/showpaths.go
@@ -77,6 +77,7 @@ func (r Result) Human(w io.Writer, showExpiration bool) {
 func (r Result) JSON(w io.Writer) error {
 	enc := json.NewEncoder(w)
 	enc.SetIndent("", "  ")
+	enc.SetEscapeHTML(false)
 	return enc.Encode(r)
 }
 
@@ -110,6 +111,7 @@ func Run(ctx context.Context, dst addr.IA, cfg Config) (*Result, error) {
 				return nil, serrors.WrapStr("failed to determine local IP", err)
 			}
 		}
+		paths := pathprobe.FilterEmptyPaths(paths)
 		statuses, err = pathprobe.Prober{
 			DstIA:   dst,
 			LocalIA: localIA,
@@ -124,13 +126,22 @@ func Run(ctx context.Context, dst addr.IA, cfg Config) (*Result, error) {
 
 	res := &Result{Destination: dst}
 	for _, path := range paths {
+		fingerprint := "local"
+		if fp := path.Fingerprint().String(); fp != "" {
+			fingerprint = fp[:16]
+		}
+		var nextHop string
+		if nh := path.UnderlayNextHop(); nh != nil {
+			nextHop = path.UnderlayNextHop().String()
+		}
 		rpath := Path{
 			FullPath:    path,
-			Fingerprint: path.Fingerprint().String()[:16],
-			NextHop:     path.UnderlayNextHop().String(),
+			Fingerprint: fingerprint,
+			NextHop:     nextHop,
 			Expiry:      path.Expiry(),
 			MTU:         path.MTU(),
 			Local:       localIP,
+			Hops:        []Hop{},
 		}
 		for _, hop := range path.Interfaces() {
 			rpath.Hops = append(rpath.Hops, Hop{IA: hop.IA(), IfID: hop.ID()})
diff --git a/go/scion/BUILD.bazel b/go/scion/BUILD.bazel
index 0080172b1..862ae30d5 100644
--- a/go/scion/BUILD.bazel
+++ b/go/scion/BUILD.bazel
@@ -5,6 +5,7 @@ go_library(
     name = "go_default_library",
     srcs = [
         "completion.go",
+        "features.go",
         "ping.go",
         "scion.go",
         "showpaths.go",
diff --git a/go/scion/features.go b/go/scion/features.go
new file mode 100644
index 000000000..cdb7af7ac
--- /dev/null
+++ b/go/scion/features.go
@@ -0,0 +1,53 @@
+// Copyright 2020 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 (
+	"fmt"
+	"strings"
+
+	"github.com/scionproto/scion/go/lib/serrors"
+)
+
+type features struct {
+	HeaderV2 bool
+}
+
+func parseFeatures(input []string) (features, error) {
+	var f features
+	m := f.mapping()
+	for _, key := range input {
+		val, ok := m[key]
+		if !ok {
+			return features{}, serrors.New("feature not supported", "feature", key)
+		}
+		*val = true
+	}
+	return f, nil
+}
+
+func (f *features) mapping() map[string]*bool {
+	return map[string]*bool{
+		"header_v2": &f.HeaderV2,
+	}
+}
+
+func (f features) supported() string {
+	var s []string
+	for k := range f.mapping() {
+		s = append(s, k)
+	}
+	return fmt.Sprintf("(%s)", strings.Join(s, "|"))
+}
diff --git a/go/scion/ping.go b/go/scion/ping.go
index 00878db4e..f4f462069 100644
--- a/go/scion/ping.go
+++ b/go/scion/ping.go
@@ -45,6 +45,9 @@ func newPing() *cobra.Command {
 		sciond      string
 		dispatcher  string
 		timeout     time.Duration
+		maxMTU      bool
+
+		features []string
 	}
 
 	var cmd = &cobra.Command{
@@ -57,6 +60,10 @@ func newPing() *cobra.Command {
 			if err != nil {
 				return serrors.WrapStr("parsing remote", err)
 			}
+			features, err := parseFeatures(flags.features)
+			if err != nil {
+				return err
+			}
 			cmd.SilenceUsage = true
 
 			ctx, cancelF := context.WithTimeout(context.Background(), time.Second)
@@ -78,19 +85,35 @@ func newPing() *cobra.Command {
 			remote.Path = path.Path()
 			remote.NextHop = path.UnderlayNextHop()
 
-			local := flags.local
-			if local == nil {
+			localIP := flags.local
+			if localIP == nil {
 				target := remote.Host.IP
 				if remote.NextHop != nil {
 					target = remote.NextHop.IP
 				}
-				if local, err = addrutil.ResolveLocal(target); err != nil {
+				if localIP, err = addrutil.ResolveLocal(target); err != nil {
 					return serrors.WrapStr("resolving local address", err)
 
 				}
-				fmt.Printf("Resolved local address:\n  %s\n", local)
+				fmt.Printf("Resolved local address:\n  %s\n", localIP)
 			}
 			fmt.Printf("Using path:\n  %s\n\n", path)
+			local := &snet.UDPAddr{
+				IA:   info.IA,
+				Host: &net.UDPAddr{IP: localIP},
+			}
+			pldSize := int(flags.size)
+			if flags.maxMTU {
+				pldSize, err = calcMaxPldSize(local, remote, int(path.MTU()), features.HeaderV2)
+				if err != nil {
+					return err
+				}
+			}
+			pktSize, err := calcPktSize(local, remote, pldSize, features.HeaderV2)
+			if err != nil {
+				return err
+			}
+			fmt.Printf("PING %s pld=%dB scion_pkt=%dB\n", remote, pldSize, pktSize)
 
 			start := time.Now()
 			ctx = app.WithSignal(context.Background(), os.Interrupt, syscall.SIGTERM)
@@ -99,14 +122,11 @@ func newPing() *cobra.Command {
 				count = math.MaxUint16
 			}
 			stats, err := ping.Run(ctx, ping.Config{
-				Dispatcher: reliable.NewDispatcher(flags.dispatcher),
-				Attempts:   count,
-				Interval:   flags.interval,
-				Timeout:    flags.timeout,
-				Local: &snet.UDPAddr{
-					IA:   info.IA,
-					Host: &net.UDPAddr{IP: local},
-				},
+				Dispatcher:  reliable.NewDispatcher(flags.dispatcher),
+				Attempts:    count,
+				Interval:    flags.interval,
+				Timeout:     flags.timeout,
+				Local:       local,
 				Remote:      remote,
 				PayloadSize: int(flags.size),
 				ErrHandler: func(err error) {
@@ -126,6 +146,7 @@ func newPing() *cobra.Command {
 						update.Size, update.Source.IA, update.Source.Host, update.Sequence,
 						update.RTT, additional)
 				},
+				HeaderV2: features.HeaderV2,
 			})
 			pingSummary(stats, remote, time.Since(start))
 			if err != nil {
@@ -149,10 +170,31 @@ func newPing() *cobra.Command {
 the total size of the packet is still variable size due to the variable size of
 the SCION path.`,
 	)
+	cmd.Flags().BoolVar(&flags.maxMTU, "max_mtu", false,
+		`choose the payload size such that the sent SCION packet including the SCION Header,
+SCMP echo header and payload are equal to the MTU of the path. This flag overrides the
+'payload_size' flag.`)
+	cmd.Flags().StringSliceVar(&flags.features, "features", nil,
+		"enable development features "+features{}.supported())
 
 	return cmd
 }
 
+func calcMaxPldSize(local, remote *snet.UDPAddr, mtu int, headerV2 bool) (int, error) {
+	overhead, err := calcPktSize(local, remote, 0, headerV2)
+	if err != nil {
+		return 0, err
+	}
+	return mtu - overhead, nil
+}
+
+func calcPktSize(local, remote *snet.UDPAddr, pldSize int, headerV2 bool) (int, error) {
+	if headerV2 {
+		return ping.Size(local, remote, pldSize)
+	}
+	return ping.SizeLegacy(local, remote, pldSize)
+}
+
 func pingSummary(stats ping.Stats, remote *snet.UDPAddr, run time.Duration) {
 	var pktLoss int
 	if stats.Sent != 0 {
diff --git a/go/sciond/main.go b/go/sciond/main.go
index eaffb3341..4af6b4f91 100644
--- a/go/sciond/main.go
+++ b/go/sciond/main.go
@@ -128,6 +128,7 @@ func run(file string) error {
 			revCache,
 			cfg.SD,
 			itopo.Provider(),
+			cfg.Features.HeaderV2,
 		),
 		Engine:   engine,
 		PathDB:   pathDB,


Test patch:
diff --git a/go/dispatcher/dispatcher/underlay_test.go b/go/dispatcher/dispatcher/underlay_test.go
index 6e41ddddb..d3684c327 100644
--- a/go/dispatcher/dispatcher/underlay_test.go
+++ b/go/dispatcher/dispatcher/underlay_test.go
@@ -211,7 +211,7 @@ func TestComputeDestination(t *testing.T) {
 	}
 	for _, test := range testCases {
 		t.Run(test.Description, func(t *testing.T) {
-			destination, err := ComputeDestination(test.Packet)
+			destination, err := ComputeDestination(test.Packet, false)
 			xtest.AssertErrorsIs(t, err, test.ExpectedErr)
 			assert.Equal(t, test.ExpectedDst, destination)
 		})
diff --git a/go/dispatcher/main_test.go b/go/dispatcher/main_test.go
index 8fe8de708..c84f780e9 100644
--- a/go/dispatcher/main_test.go
+++ b/go/dispatcher/main_test.go
@@ -17,6 +17,7 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"io/ioutil"
 	"net"
 	"os"
@@ -29,15 +30,14 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/hpkt"
 	"github.com/scionproto/scion/go/lib/l4"
-	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/scmp"
 	"github.com/scionproto/scion/go/lib/sock/reliable"
+	"github.com/scionproto/scion/go/lib/spath"
 	"github.com/scionproto/scion/go/lib/spkt"
 	"github.com/scionproto/scion/go/lib/xtest"
 )
 
 const (
-	dispatcherTestPort  = 40031
 	defaultTimeout      = 2 * time.Second
 	defaultWaitDuration = 200 * time.Millisecond
 )
@@ -45,9 +45,10 @@ const (
 type TestSettings struct {
 	ApplicationSocket string
 	UnderlayPort      int
+	HeaderV2          bool
 }
 
-func InitTestSettings(t *testing.T) *TestSettings {
+func InitTestSettings(t *testing.T, dispatcherTestPort int) *TestSettings {
 	socketName, err := getSocketName("/tmp")
 	if err != nil {
 		t.Fatal(err)
@@ -75,27 +76,6 @@ type ClientAddress struct {
 	UnderlayPort    uint16
 }
 
-// Addressing information
-var (
-	commonIA              = xtest.MustParseIA("1-ff00:0:1")
-	commonPublicL3Address = addr.HostFromIP(net.IP{127, 0, 0, 1})
-	commonUnderlayAddress = &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: dispatcherTestPort}
-	clientXAddress        = &ClientAddress{
-		IA:              commonIA,
-		PublicAddress:   commonPublicL3Address,
-		PublicPort:      8080,
-		ServiceAddress:  addr.SvcNone,
-		UnderlayAddress: commonUnderlayAddress,
-	}
-	clientYAddress = &ClientAddress{
-		IA:              commonIA,
-		PublicAddress:   commonPublicL3Address,
-		PublicPort:      8081,
-		ServiceAddress:  addr.SvcPS,
-		UnderlayAddress: commonUnderlayAddress,
-	}
-)
-
 type TestCase struct {
 	Name            string
 	ClientAddress   *ClientAddress
@@ -104,141 +84,197 @@ type TestCase struct {
 	ExpectedPacket  *spkt.ScnPkt
 }
 
-var testCases = []*TestCase{
-	{
-		Name:          "UDP/IPv4 packet",
-		ClientAddress: clientXAddress,
-		TestPackets: []*spkt.ScnPkt{
-			{
+func genTestCases(dispatcherPort int) []*TestCase {
+	// Addressing information
+	var (
+		commonIA              = xtest.MustParseIA("1-ff00:0:1")
+		commonPublicL3Address = addr.HostFromIP(net.IP{127, 0, 0, 1})
+		commonUnderlayAddress = &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: dispatcherPort}
+		clientXAddress        = &ClientAddress{
+			IA:              commonIA,
+			PublicAddress:   commonPublicL3Address,
+			PublicPort:      8080,
+			ServiceAddress:  addr.SvcNone,
+			UnderlayAddress: commonUnderlayAddress,
+		}
+		clientYAddress = &ClientAddress{
+			IA:              commonIA,
+			PublicAddress:   commonPublicL3Address,
+			PublicPort:      8081,
+			ServiceAddress:  addr.SvcPS,
+			UnderlayAddress: commonUnderlayAddress,
+		}
+	)
+
+	var testCases = []*TestCase{
+		{
+			Name:          "UDP/IPv4 packet",
+			ClientAddress: clientXAddress,
+			TestPackets: []*spkt.ScnPkt{
+				{
+					SrcIA:   clientXAddress.IA,
+					DstIA:   clientXAddress.IA,
+					SrcHost: clientXAddress.PublicAddress,
+					DstHost: clientXAddress.PublicAddress,
+					L4: &l4.UDP{
+						SrcPort: clientXAddress.PublicPort,
+						DstPort: clientXAddress.PublicPort,
+					},
+					Pld: common.RawBytes{1, 2, 3, 4},
+				},
+			},
+			UnderlayAddress: clientXAddress.UnderlayAddress,
+			ExpectedPacket: &spkt.ScnPkt{
 				SrcIA:   clientXAddress.IA,
 				DstIA:   clientXAddress.IA,
 				SrcHost: clientXAddress.PublicAddress,
 				DstHost: clientXAddress.PublicAddress,
 				L4: &l4.UDP{
-					SrcPort: clientXAddress.PublicPort,
-					DstPort: clientXAddress.PublicPort,
+					SrcPort:  clientXAddress.PublicPort,
+					DstPort:  clientXAddress.PublicPort,
+					TotalLen: 12,
+					Checksum: common.RawBytes{0xc0, 0xb3},
 				},
 				Pld: common.RawBytes{1, 2, 3, 4},
 			},
 		},
-		UnderlayAddress: clientXAddress.UnderlayAddress,
-		ExpectedPacket: &spkt.ScnPkt{
-			SrcIA:   clientXAddress.IA,
-			DstIA:   clientXAddress.IA,
-			SrcHost: clientXAddress.PublicAddress,
-			DstHost: clientXAddress.PublicAddress,
-			L4: &l4.UDP{
-				SrcPort:  clientXAddress.PublicPort,
-				DstPort:  clientXAddress.PublicPort,
-				TotalLen: 12,
-				Checksum: common.RawBytes{0xc0, 0xb3},
+		{
+			Name:          "UDP/SVC packet",
+			ClientAddress: clientYAddress,
+			TestPackets: []*spkt.ScnPkt{
+				{
+					SrcIA:   clientYAddress.IA,
+					DstIA:   clientYAddress.IA,
+					SrcHost: clientYAddress.PublicAddress,
+					DstHost: clientYAddress.ServiceAddress,
+					L4: &l4.UDP{
+						SrcPort: clientYAddress.PublicPort,
+						DstPort: clientYAddress.PublicPort,
+					},
+					Pld: common.RawBytes{5, 6, 7, 8},
+				},
 			},
-			Pld: common.RawBytes{1, 2, 3, 4},
-		},
-	},
-	{
-		Name:          "UDP/SVC packet",
-		ClientAddress: clientYAddress,
-		TestPackets: []*spkt.ScnPkt{
-			{
+			UnderlayAddress: clientXAddress.UnderlayAddress,
+			ExpectedPacket: &spkt.ScnPkt{
 				SrcIA:   clientYAddress.IA,
 				DstIA:   clientYAddress.IA,
 				SrcHost: clientYAddress.PublicAddress,
 				DstHost: clientYAddress.ServiceAddress,
 				L4: &l4.UDP{
-					SrcPort: clientYAddress.PublicPort,
-					DstPort: clientYAddress.PublicPort,
+					SrcPort:  clientYAddress.PublicPort,
+					DstPort:  clientYAddress.PublicPort,
+					TotalLen: 12,
+					Checksum: common.RawBytes{0x37, 0xaa},
 				},
 				Pld: common.RawBytes{5, 6, 7, 8},
 			},
 		},
-		UnderlayAddress: clientXAddress.UnderlayAddress,
-		ExpectedPacket: &spkt.ScnPkt{
-			SrcIA:   clientYAddress.IA,
-			DstIA:   clientYAddress.IA,
-			SrcHost: clientYAddress.PublicAddress,
-			DstHost: clientYAddress.ServiceAddress,
-			L4: &l4.UDP{
-				SrcPort:  clientYAddress.PublicPort,
-				DstPort:  clientYAddress.PublicPort,
-				TotalLen: 12,
-				Checksum: common.RawBytes{0x37, 0xaa},
+		{
+			Name:            "SCMP::Error, UDP quote",
+			ClientAddress:   clientXAddress,
+			UnderlayAddress: clientXAddress.UnderlayAddress,
+			TestPackets: []*spkt.ScnPkt{
+				{
+					SrcIA:   clientXAddress.IA,
+					DstIA:   clientXAddress.IA,
+					SrcHost: clientXAddress.PublicAddress,
+					DstHost: clientXAddress.PublicAddress,
+					L4: &scmp.Hdr{
+						Class: scmp.C_Routing, Type: scmp.T_R_UnreachNet,
+					},
+					Pld: &scmp.Payload{
+						Meta:  &scmp.Meta{L4Proto: common.L4UDP, L4HdrLen: 1},
+						L4Hdr: MustPackL4Header(&l4.UDP{SrcPort: clientXAddress.PublicPort}),
+					},
+				},
 			},
-			Pld: common.RawBytes{5, 6, 7, 8},
-		},
-	},
-	{
-		Name:            "SCMP::Error, UDP quote",
-		ClientAddress:   clientXAddress,
-		UnderlayAddress: clientXAddress.UnderlayAddress,
-		TestPackets: []*spkt.ScnPkt{
-			{
+			ExpectedPacket: &spkt.ScnPkt{
 				SrcIA:   clientXAddress.IA,
 				DstIA:   clientXAddress.IA,
 				SrcHost: clientXAddress.PublicAddress,
 				DstHost: clientXAddress.PublicAddress,
 				L4: &scmp.Hdr{
 					Class: scmp.C_Routing, Type: scmp.T_R_UnreachNet,
+					TotalLen: 32,
+					Checksum: common.RawBytes{0xd3, 0x43},
 				},
 				Pld: &scmp.Payload{
-					Meta:  &scmp.Meta{L4Proto: common.L4UDP, L4HdrLen: 1},
-					L4Hdr: MustPackL4Header(&l4.UDP{SrcPort: clientXAddress.PublicPort}),
+					Meta:    &scmp.Meta{L4Proto: common.L4UDP, L4HdrLen: 1},
+					CmnHdr:  common.RawBytes{},
+					AddrHdr: common.RawBytes{},
+					PathHdr: common.RawBytes{},
+					ExtHdrs: common.RawBytes{},
+					L4Hdr:   MustPackL4Header(&l4.UDP{SrcPort: clientXAddress.PublicPort}),
 				},
 			},
 		},
-		ExpectedPacket: &spkt.ScnPkt{
-			SrcIA:   clientXAddress.IA,
-			DstIA:   clientXAddress.IA,
-			SrcHost: clientXAddress.PublicAddress,
-			DstHost: clientXAddress.PublicAddress,
-			L4: &scmp.Hdr{
-				Class: scmp.C_Routing, Type: scmp.T_R_UnreachNet,
-				TotalLen: 32,
-				Checksum: common.RawBytes{0xd3, 0x43},
-			},
-			Pld: &scmp.Payload{
-				Meta:    &scmp.Meta{L4Proto: common.L4UDP, L4HdrLen: 1},
-				CmnHdr:  common.RawBytes{},
-				AddrHdr: common.RawBytes{},
-				PathHdr: common.RawBytes{},
-				ExtHdrs: common.RawBytes{},
-				L4Hdr:   MustPackL4Header(&l4.UDP{SrcPort: clientXAddress.PublicPort}),
-			},
-		},
-	},
-	{
-		Name:            "SCMP::Error, SCMP quote",
-		ClientAddress:   clientXAddress,
-		UnderlayAddress: clientXAddress.UnderlayAddress,
-		TestPackets: []*spkt.ScnPkt{
-			{
-				// Force a SCMP General ID registration to happen, but route it
-				// from nowhere so we don't get it back
-				SrcIA:   xtest.MustParseIA("1-ff00:0:42"), // middle of nowhere
-				DstIA:   clientYAddress.IA,
-				SrcHost: clientXAddress.PublicAddress,
-				DstHost: clientYAddress.PublicAddress,
-				L4: &scmp.Hdr{
-					Class: scmp.C_General, Type: scmp.T_G_EchoRequest,
+		{
+			Name:            "SCMP::Error, SCMP quote",
+			ClientAddress:   clientXAddress,
+			UnderlayAddress: clientXAddress.UnderlayAddress,
+			TestPackets: []*spkt.ScnPkt{
+				{
+					// Force a SCMP General ID registration to happen, but route it
+					// from nowhere so we don't get it back
+					SrcIA:   xtest.MustParseIA("1-ff00:0:42"), // middle of nowhere
+					DstIA:   clientYAddress.IA,
+					SrcHost: clientXAddress.PublicAddress,
+					DstHost: clientYAddress.PublicAddress,
+					L4: &scmp.Hdr{
+						Class: scmp.C_General, Type: scmp.T_G_EchoRequest,
+					},
+					Pld: &scmp.Payload{
+						Meta: &scmp.Meta{InfoLen: uint8((&scmp.InfoEcho{}).Len()) / 8},
+						Info: &scmp.InfoEcho{Id: 0xabbacafe},
+					},
 				},
-				Pld: &scmp.Payload{
-					Meta: &scmp.Meta{InfoLen: uint8((&scmp.InfoEcho{}).Len()) / 8},
-					Info: &scmp.InfoEcho{Id: 0xabbacafe},
+				{
+					SrcIA:   clientXAddress.IA,
+					DstIA:   clientXAddress.IA,
+					SrcHost: clientXAddress.PublicAddress,
+					DstHost: clientXAddress.PublicAddress,
+					L4: &scmp.Hdr{
+						Class: scmp.C_Routing, Type: scmp.T_R_UnreachNet,
+					},
+					Pld: &scmp.Payload{
+						Meta: &scmp.Meta{
+							L4Proto:  common.L4SCMP,
+							L4HdrLen: 3 + uint8((&scmp.InfoEcho{}).Len()/common.LineLen),
+						},
+						L4Hdr: MustPackQuotedSCMPL4Header(
+							&scmp.Hdr{
+								Class: scmp.C_General,
+								Type:  scmp.T_G_EchoRequest,
+							},
+							&scmp.Meta{
+								InfoLen: uint8((&scmp.InfoEcho{}).Len()) / 8,
+							},
+							&scmp.InfoEcho{
+								Id: 0xabbacafe,
+							},
+						),
+					},
 				},
 			},
-			{
+			ExpectedPacket: &spkt.ScnPkt{
 				SrcIA:   clientXAddress.IA,
 				DstIA:   clientXAddress.IA,
 				SrcHost: clientXAddress.PublicAddress,
 				DstHost: clientXAddress.PublicAddress,
 				L4: &scmp.Hdr{
 					Class: scmp.C_Routing, Type: scmp.T_R_UnreachNet,
+					TotalLen: 64,
+					Checksum: common.RawBytes{0x89, 0xf5},
 				},
 				Pld: &scmp.Payload{
 					Meta: &scmp.Meta{
 						L4Proto:  common.L4SCMP,
 						L4HdrLen: 3 + uint8((&scmp.InfoEcho{}).Len()/common.LineLen),
 					},
+					CmnHdr:  common.RawBytes{},
+					AddrHdr: common.RawBytes{},
+					PathHdr: common.RawBytes{},
+					ExtHdrs: common.RawBytes{},
 					L4Hdr: MustPackQuotedSCMPL4Header(
 						&scmp.Hdr{
 							Class: scmp.C_General,
@@ -254,178 +290,172 @@ var testCases = []*TestCase{
 				},
 			},
 		},
-		ExpectedPacket: &spkt.ScnPkt{
-			SrcIA:   clientXAddress.IA,
-			DstIA:   clientXAddress.IA,
-			SrcHost: clientXAddress.PublicAddress,
-			DstHost: clientXAddress.PublicAddress,
-			L4: &scmp.Hdr{
-				Class: scmp.C_Routing, Type: scmp.T_R_UnreachNet,
-				TotalLen: 64,
-				Checksum: common.RawBytes{0x89, 0xf5},
-			},
-			Pld: &scmp.Payload{
-				Meta: &scmp.Meta{
-					L4Proto:  common.L4SCMP,
-					L4HdrLen: 3 + uint8((&scmp.InfoEcho{}).Len()/common.LineLen),
-				},
-				CmnHdr:  common.RawBytes{},
-				AddrHdr: common.RawBytes{},
-				PathHdr: common.RawBytes{},
-				ExtHdrs: common.RawBytes{},
-				L4Hdr: MustPackQuotedSCMPL4Header(
-					&scmp.Hdr{
-						Class: scmp.C_General,
-						Type:  scmp.T_G_EchoRequest,
-					},
-					&scmp.Meta{
-						InfoLen: uint8((&scmp.InfoEcho{}).Len()) / 8,
+		{
+			Name:            "SCMP::General::EchoRequest",
+			ClientAddress:   clientXAddress,
+			UnderlayAddress: clientYAddress.UnderlayAddress,
+			TestPackets: []*spkt.ScnPkt{
+				{
+					SrcIA:   clientXAddress.IA,
+					DstIA:   clientYAddress.IA,
+					SrcHost: clientXAddress.PublicAddress,
+					DstHost: clientYAddress.PublicAddress,
+					L4: &scmp.Hdr{
+						Class: scmp.C_General, Type: scmp.T_G_EchoRequest,
 					},
-					&scmp.InfoEcho{
-						Id: 0xabbacafe,
+					Pld: &scmp.Payload{
+						Meta: &scmp.Meta{InfoLen: uint8((&scmp.InfoEcho{}).Len())},
+						Info: &scmp.InfoEcho{Id: 0xdeadcafe},
 					},
-				),
+				},
 			},
-		},
-	},
-	{
-		Name:            "SCMP::General::EchoRequest",
-		ClientAddress:   clientXAddress,
-		UnderlayAddress: clientYAddress.UnderlayAddress,
-		TestPackets: []*spkt.ScnPkt{
-			{
-				SrcIA:   clientXAddress.IA,
-				DstIA:   clientYAddress.IA,
-				SrcHost: clientXAddress.PublicAddress,
-				DstHost: clientYAddress.PublicAddress,
+			ExpectedPacket: &spkt.ScnPkt{
+				SrcIA:   clientYAddress.IA,
+				DstIA:   clientXAddress.IA,
+				SrcHost: clientYAddress.PublicAddress,
+				DstHost: clientXAddress.PublicAddress,
 				L4: &scmp.Hdr{
-					Class: scmp.C_General, Type: scmp.T_G_EchoRequest,
+					Class: scmp.C_General, Type: scmp.T_G_EchoReply,
+					TotalLen: 40,
+					Checksum: common.RawBytes{0x4a, 0x1f},
 				},
 				Pld: &scmp.Payload{
-					Meta: &scmp.Meta{InfoLen: uint8((&scmp.InfoEcho{}).Len())},
-					Info: &scmp.InfoEcho{Id: 0xdeadcafe},
+					Meta: &scmp.Meta{
+						InfoLen: uint8((&scmp.InfoEcho{}).Len()),
+					},
+					Info:    &scmp.InfoEcho{Id: 0xdeadcafe},
+					CmnHdr:  common.RawBytes{},
+					AddrHdr: common.RawBytes{},
+					PathHdr: common.RawBytes{},
+					ExtHdrs: common.RawBytes{},
+					L4Hdr:   common.RawBytes{},
 				},
 			},
 		},
-		ExpectedPacket: &spkt.ScnPkt{
-			SrcIA:   clientYAddress.IA,
-			DstIA:   clientXAddress.IA,
-			SrcHost: clientYAddress.PublicAddress,
-			DstHost: clientXAddress.PublicAddress,
-			L4: &scmp.Hdr{
-				Class: scmp.C_General, Type: scmp.T_G_EchoReply,
-				TotalLen: 40,
-				Checksum: common.RawBytes{0x4a, 0x1f},
-			},
-			Pld: &scmp.Payload{
-				Meta: &scmp.Meta{
-					InfoLen: uint8((&scmp.InfoEcho{}).Len()),
+		{
+			Name:            "SCMP::General::TraceRouteRequest",
+			ClientAddress:   clientXAddress,
+			UnderlayAddress: clientYAddress.UnderlayAddress,
+			TestPackets: []*spkt.ScnPkt{
+				{
+					SrcIA:   clientXAddress.IA,
+					DstIA:   clientYAddress.IA,
+					SrcHost: clientXAddress.PublicAddress,
+					DstHost: clientYAddress.PublicAddress,
+					L4: &scmp.Hdr{
+						Class: scmp.C_General, Type: scmp.T_G_TraceRouteRequest,
+					},
+					Pld: &scmp.Payload{
+						Meta: &scmp.Meta{InfoLen: uint8((&scmp.InfoTraceRoute{}).Len()) / 8},
+						Info: &scmp.InfoTraceRoute{Id: 0xcafecafe},
+					},
 				},
-				Info:    &scmp.InfoEcho{Id: 0xdeadcafe},
-				CmnHdr:  common.RawBytes{},
-				AddrHdr: common.RawBytes{},
-				PathHdr: common.RawBytes{},
-				ExtHdrs: common.RawBytes{},
-				L4Hdr:   common.RawBytes{},
 			},
-		},
-	},
-	{
-		Name:            "SCMP::General::TraceRouteRequest",
-		ClientAddress:   clientXAddress,
-		UnderlayAddress: clientYAddress.UnderlayAddress,
-		TestPackets: []*spkt.ScnPkt{
-			{
-				SrcIA:   clientXAddress.IA,
-				DstIA:   clientYAddress.IA,
-				SrcHost: clientXAddress.PublicAddress,
-				DstHost: clientYAddress.PublicAddress,
+			ExpectedPacket: &spkt.ScnPkt{
+				SrcIA:   clientYAddress.IA,
+				DstIA:   clientXAddress.IA,
+				SrcHost: clientYAddress.PublicAddress,
+				DstHost: clientXAddress.PublicAddress,
 				L4: &scmp.Hdr{
-					Class: scmp.C_General, Type: scmp.T_G_TraceRouteRequest,
+					Class: scmp.C_General, Type: scmp.T_G_TraceRouteReply,
+					TotalLen: 56,
+					Checksum: common.RawBytes{0x69, 0xbc},
 				},
 				Pld: &scmp.Payload{
-					Meta: &scmp.Meta{InfoLen: uint8((&scmp.InfoTraceRoute{}).Len()) / 8},
-					Info: &scmp.InfoTraceRoute{Id: 0xcafecafe},
+					Meta: &scmp.Meta{
+						InfoLen: uint8((&scmp.InfoTraceRoute{}).Len()) / 8,
+					},
+					Info:    &scmp.InfoTraceRoute{Id: 0xcafecafe},
+					CmnHdr:  common.RawBytes{},
+					AddrHdr: common.RawBytes{},
+					PathHdr: common.RawBytes{},
+					ExtHdrs: common.RawBytes{},
+					L4Hdr:   common.RawBytes{},
 				},
 			},
 		},
-		ExpectedPacket: &spkt.ScnPkt{
-			SrcIA:   clientYAddress.IA,
-			DstIA:   clientXAddress.IA,
-			SrcHost: clientYAddress.PublicAddress,
-			DstHost: clientXAddress.PublicAddress,
-			L4: &scmp.Hdr{
-				Class: scmp.C_General, Type: scmp.T_G_TraceRouteReply,
-				TotalLen: 56,
-				Checksum: common.RawBytes{0x69, 0xbc},
-			},
-			Pld: &scmp.Payload{
-				Meta: &scmp.Meta{
-					InfoLen: uint8((&scmp.InfoTraceRoute{}).Len()) / 8,
+		{
+			Name:            "SCMP::General::RecordPathRequest",
+			ClientAddress:   clientXAddress,
+			UnderlayAddress: clientYAddress.UnderlayAddress,
+			TestPackets: []*spkt.ScnPkt{
+				{
+					SrcIA:   clientXAddress.IA,
+					DstIA:   clientYAddress.IA,
+					SrcHost: clientXAddress.PublicAddress,
+					DstHost: clientYAddress.PublicAddress,
+					L4: &scmp.Hdr{
+						Class: scmp.C_General, Type: scmp.T_G_RecordPathRequest,
+					},
+					Pld: &scmp.Payload{
+						Meta: &scmp.Meta{InfoLen: uint8((&scmp.InfoRecordPath{}).Len()) / 8},
+						Info: &scmp.InfoRecordPath{Id: 0xf00dcafe},
+					},
 				},
-				Info:    &scmp.InfoTraceRoute{Id: 0xcafecafe},
-				CmnHdr:  common.RawBytes{},
-				AddrHdr: common.RawBytes{},
-				PathHdr: common.RawBytes{},
-				ExtHdrs: common.RawBytes{},
-				L4Hdr:   common.RawBytes{},
 			},
-		},
-	},
-	{
-		Name:            "SCMP::General::RecordPathRequest",
-		ClientAddress:   clientXAddress,
-		UnderlayAddress: clientYAddress.UnderlayAddress,
-		TestPackets: []*spkt.ScnPkt{
-			{
-				SrcIA:   clientXAddress.IA,
-				DstIA:   clientYAddress.IA,
-				SrcHost: clientXAddress.PublicAddress,
-				DstHost: clientYAddress.PublicAddress,
+			ExpectedPacket: &spkt.ScnPkt{
+				SrcIA:   clientYAddress.IA,
+				DstIA:   clientXAddress.IA,
+				SrcHost: clientYAddress.PublicAddress,
+				DstHost: clientXAddress.PublicAddress,
 				L4: &scmp.Hdr{
-					Class: scmp.C_General, Type: scmp.T_G_RecordPathRequest,
+					Class: scmp.C_General, Type: scmp.T_G_RecordPathReply,
+					TotalLen: 40,
+					Checksum: common.RawBytes{0x46, 0xbb},
 				},
 				Pld: &scmp.Payload{
-					Meta: &scmp.Meta{InfoLen: uint8((&scmp.InfoRecordPath{}).Len()) / 8},
-					Info: &scmp.InfoRecordPath{Id: 0xf00dcafe},
-				},
-			},
-		},
-		ExpectedPacket: &spkt.ScnPkt{
-			SrcIA:   clientYAddress.IA,
-			DstIA:   clientXAddress.IA,
-			SrcHost: clientYAddress.PublicAddress,
-			DstHost: clientXAddress.PublicAddress,
-			L4: &scmp.Hdr{
-				Class: scmp.C_General, Type: scmp.T_G_RecordPathReply,
-				TotalLen: 40,
-				Checksum: common.RawBytes{0x46, 0xbb},
-			},
-			Pld: &scmp.Payload{
-				Meta: &scmp.Meta{
-					InfoLen: uint8((&scmp.InfoRecordPath{}).Len()) / 8,
+					Meta: &scmp.Meta{
+						InfoLen: uint8((&scmp.InfoRecordPath{}).Len()) / 8,
+					},
+					Info: &scmp.InfoRecordPath{
+						Id:      0xf00dcafe,
+						Entries: []*scmp.RecordPathEntry{},
+					},
+					CmnHdr:  common.RawBytes{},
+					AddrHdr: common.RawBytes{},
+					PathHdr: common.RawBytes{},
+					ExtHdrs: common.RawBytes{},
+					L4Hdr:   common.RawBytes{},
 				},
-				Info:    &scmp.InfoRecordPath{Id: 0xf00dcafe, Entries: []*scmp.RecordPathEntry{}},
-				CmnHdr:  common.RawBytes{},
-				AddrHdr: common.RawBytes{},
-				PathHdr: common.RawBytes{},
-				ExtHdrs: common.RawBytes{},
-				L4Hdr:   common.RawBytes{},
 			},
 		},
-	},
+	}
+	return testCases
 }
 
 func TestDataplaneIntegration(t *testing.T) {
-	settings := InitTestSettings(t)
+	dispatcherTestPort := 40031
+	settings := InitTestSettings(t, dispatcherTestPort)
+
+	go func() {
+		err := RunDispatcher(false, settings.ApplicationSocket, reliable.DefaultDispSocketFileMode,
+			settings.UnderlayPort, settings.HeaderV2)
+		xtest.FailOnErr(t, err, "dispatcher error")
+	}()
+	time.Sleep(defaultWaitDuration)
+
+	testCases := genTestCases(dispatcherTestPort)
+	for _, tc := range testCases {
+		t.Run(tc.Name, func(t *testing.T) {
+			RunTestCase(t, tc, settings)
+		})
+		time.Sleep(defaultWaitDuration)
+	}
+}
+
+func TestDataplaneIntegrationV2(t *testing.T) {
+	dispatcherTestPort := 40032
+	settings := InitTestSettings(t, dispatcherTestPort)
+	settings.HeaderV2 = true
 
 	go func() {
 		err := RunDispatcher(false, settings.ApplicationSocket, reliable.DefaultDispSocketFileMode,
-			settings.UnderlayPort)
+			settings.UnderlayPort, settings.HeaderV2)
 		xtest.FailOnErr(t, err, "dispatcher error")
 	}()
 	time.Sleep(defaultWaitDuration)
 
+	testCases := genTestCases(dispatcherTestPort)
 	for _, tc := range testCases {
 		t.Run(tc.Name, func(t *testing.T) {
 			RunTestCase(t, tc, settings)
@@ -453,25 +483,38 @@ func RunTestCase(t *testing.T, tc *TestCase, settings *TestSettings) {
 	defer conn.Close()
 
 	for _, packet := range tc.TestPackets {
-		send_buffer := make([]byte, common.MaxMTU)
-		n, err := hpkt.WriteScnPkt(packet, send_buffer)
+		sendBuffer := make([]byte, common.MaxMTU)
+		var n int
+		var err error
+		if settings.HeaderV2 {
+			packet.Path = spath.NewV2(generatePath(), false)
+			n, err = hpkt.WriteScnPkt2(packet, sendBuffer)
+		} else {
+			n, err = hpkt.WriteScnPkt(packet, sendBuffer)
+		}
 		xtest.FailOnErr(t, err, "unable to serialize packet for sending")
-		send_buffer = send_buffer[:n]
+		sendBuffer = sendBuffer[:n]
 
-		_, err = conn.WriteTo(send_buffer, tc.UnderlayAddress)
+		fmt.Printf("sending packet: %x\n", sendBuffer)
+		_, err = conn.WriteTo(sendBuffer, tc.UnderlayAddress)
 		xtest.FailOnErr(t, err, "unable to write message")
 	}
 
 	err = conn.SetReadDeadline(time.Now().Add(defaultTimeout))
 	xtest.FailOnErr(t, err, "unable to set read deadline")
 
-	recv_buffer := make([]byte, common.MaxMTU)
-	n, _, err := conn.ReadFrom(recv_buffer)
+	recvBuffer := make([]byte, common.MaxMTU)
+	n, _, err := conn.ReadFrom(recvBuffer)
 	xtest.FailOnErr(t, err, "unable to read message")
-	recv_buffer = recv_buffer[:n]
+	recvBuffer = recvBuffer[:n]
 
 	var packet spkt.ScnPkt
-	err = hpkt.ParseScnPkt(&packet, recv_buffer)
+	if settings.HeaderV2 {
+		err = hpkt.ParseScnPkt2(&packet, recvBuffer)
+		packet.Path = nil
+	} else {
+		err = hpkt.ParseScnPkt(&packet, recvBuffer)
+	}
 	xtest.FailOnErr(t, err, "unable to parse received packet")
 
 	err = conn.Close()
@@ -512,6 +555,20 @@ func MustPackQuotedSCMPL4Header(header *scmp.Hdr, meta *scmp.Meta, info scmp.Inf
 }
 
 func TestMain(m *testing.M) {
-	log.Discard()
+	// log.Discard()
 	os.Exit(m.Run())
 }
+
+func generatePath() []byte {
+	return []byte{
+		0x0, 0x0, 0x20, 0x80, 0x0, 0x0, 0x1, 0x11,
+		0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x2, 0x22,
+		0x0, 0x0, 0x1, 0x0, 0x0, 0x3f, 0x0, 0x1,
+		0x0, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x0,
+		0x3f, 0x0, 0x3, 0x0, 0x2, 0x1, 0x2, 0x3,
+		0x4, 0x5, 0x6, 0x0, 0x3f, 0x0, 0x0, 0x0,
+		0x2, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x0,
+		0x3f, 0x0, 0x1, 0x0, 0x0, 0x1, 0x2, 0x3,
+		0x4, 0x5, 0x6,
+	}
+}
diff --git a/go/lib/hpkt/hpkt_test.go b/go/lib/hpkt/hpkt_test.go
index 603c22bd0..0d4ab4223 100644
--- a/go/lib/hpkt/hpkt_test.go
+++ b/go/lib/hpkt/hpkt_test.go
@@ -214,27 +214,92 @@ func TestParseScnPkt2(t *testing.T) {
 }
 
 func TestScnPktWrite2(t *testing.T) {
-	expectedPacket := &spkt.ScnPkt{
-		SrcIA:   xtest.MustParseIA("2-ff00:0:222"),
-		DstIA:   xtest.MustParseIA("1-ff00:0:111"),
-		SrcHost: addr.HostFromIP(net.IP{10, 0, 0, 100}),
-		DstHost: addr.HostFromIP(net.IP{0x20, 0x1, 0xd, 0xb8, 0x0, 0x0, 0x0, 0x0,
-			0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x68}),
-		Path: spath.NewV2(generatePath()),
-		L4:   &l4.UDP{SrcPort: 1280, DstPort: 80, TotalLen: 1032, Checksum: []byte{0xda, 0xbb}},
-		Pld:  common.RawBytes(generatePayload()),
+	testCases := []struct {
+		Name           string
+		ExpectedPacket *spkt.ScnPkt
+	}{
+		{
+			Name: "udp",
+			ExpectedPacket: &spkt.ScnPkt{
+				SrcIA:   xtest.MustParseIA("2-ff00:0:222"),
+				DstIA:   xtest.MustParseIA("1-ff00:0:111"),
+				SrcHost: addr.HostFromIP(net.IP{10, 0, 0, 100}),
+				DstHost: addr.HostFromIP(net.IP{0x20, 0x1, 0xd, 0xb8, 0x0, 0x0, 0x0, 0x0,
+					0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x68}),
+				Path: spath.NewV2(generatePath(), false),
+				L4: &l4.UDP{
+					SrcPort:  1280,
+					DstPort:  80,
+					TotalLen: 1032,
+					Checksum: []byte{0xbb, 0xda},
+				},
+				Pld: common.RawBytes(generatePayload()),
+			},
+		},
+		{
+			Name: "EchoRequest",
+			ExpectedPacket: &spkt.ScnPkt{
+				SrcIA:   xtest.MustParseIA("1-ff00:0:1"),
+				DstIA:   xtest.MustParseIA("1-ff00:0:1"),
+				SrcHost: addr.HostFromIP(net.IP{127, 0, 0, 1}),
+				DstHost: addr.HostFromIP(net.IP{127, 0, 0, 1}),
+				L4: &scmp.Hdr{
+					Class: scmp.C_General, Type: scmp.T_G_EchoRequest,
+					TotalLen: 40,
+					Checksum: []byte{0x4a, 0x20},
+				},
+				Pld: &scmp.Payload{
+					Meta:    &scmp.Meta{InfoLen: uint8((&scmp.InfoEcho{}).Len())},
+					Info:    &scmp.InfoEcho{Id: 0xdeadcafe},
+					CmnHdr:  common.RawBytes{},
+					AddrHdr: common.RawBytes{},
+					PathHdr: common.RawBytes{},
+					ExtHdrs: common.RawBytes{},
+					L4Hdr:   common.RawBytes{},
+				},
+			},
+		},
+		{
+			Name: "EchoReply",
+			ExpectedPacket: &spkt.ScnPkt{
+				SrcIA:   xtest.MustParseIA("1-ff00:0:1"),
+				DstIA:   xtest.MustParseIA("1-ff00:0:1"),
+				SrcHost: addr.HostFromIP(net.IP{127, 0, 0, 1}),
+				DstHost: addr.HostFromIP(net.IP{127, 0, 0, 1}),
+				L4: &scmp.Hdr{
+					Class: scmp.C_General, Type: scmp.T_G_EchoReply,
+					TotalLen: 40,
+					Checksum: common.RawBytes{0x4a, 0x1f},
+				},
+				Pld: &scmp.Payload{
+					Meta: &scmp.Meta{
+						InfoLen: uint8((&scmp.InfoEcho{}).Len()),
+					},
+					Info:    &scmp.InfoEcho{Id: 0xdeadcafe},
+					CmnHdr:  common.RawBytes{},
+					AddrHdr: common.RawBytes{},
+					PathHdr: common.RawBytes{},
+					ExtHdrs: common.RawBytes{},
+					L4Hdr:   common.RawBytes{},
+				},
+			},
+		},
 	}
 
-	b := make(common.RawBytes, common.MaxMTU)
-	n, err := WriteScnPkt2(expectedPacket, b)
-	fmt.Println(b[:n])
-	assert.NoError(t, err, "Write error")
+	for _, tc := range testCases {
+		t.Run(tc.Name, func(t *testing.T) {
+			b := make(common.RawBytes, common.MaxMTU)
+			n, err := WriteScnPkt2(tc.ExpectedPacket, b)
+			fmt.Println(b[:n])
+			assert.NoError(t, err, "Write error")
 
-	parsedPacket := &spkt.ScnPkt{}
-	err = ParseScnPkt2(parsedPacket, b[:n])
-	require.NoError(t, err, "Read error")
+			parsedPacket := &spkt.ScnPkt{}
+			err = ParseScnPkt2(parsedPacket, b[:n])
+			require.NoError(t, err, "Read error")
 
-	assert.Equal(t, expectedPacket, parsedPacket)
+			assert.Equal(t, tc.ExpectedPacket, parsedPacket)
+		})
+	}
 }
 
 func generatePayload() []byte {
diff --git a/go/lib/slayers/path/onehop/onehop_test.go b/go/lib/slayers/path/onehop/onehop_test.go
index 16596f4c4..b4575b3ea 100644
--- a/go/lib/slayers/path/onehop/onehop_test.go
+++ b/go/lib/slayers/path/onehop/onehop_test.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/scionproto/scion/go/lib/slayers/path"
 	"github.com/scionproto/scion/go/lib/slayers/path/onehop"
+	"github.com/scionproto/scion/go/lib/slayers/path/scion"
 )
 
 func TestSerializeDecode(t *testing.T) {
@@ -54,3 +55,69 @@ func TestSerializeDecode(t *testing.T) {
 	assert.NoError(t, got.DecodeFromBytes(b))
 	assert.Equal(t, want, got)
 }
+
+func TestPathToSCIONDecoded(t *testing.T) {
+	t.Run("complete path converts correctly", func(t *testing.T) {
+		t.Parallel()
+		input := onehop.Path{
+			Info: path.InfoField{
+				ConsDir:   true,
+				SegID:     0x222,
+				Timestamp: 0x100,
+			},
+			FirstHop: path.HopField{
+				IngressRouterAlert: true,
+				EgressRouterAlert:  true,
+				ExpTime:            63,
+				ConsIngress:        0,
+				ConsEgress:         1,
+				Mac:                []byte{1, 2, 3, 4, 5, 6},
+			},
+			SecondHop: path.HopField{
+				IngressRouterAlert: true,
+				EgressRouterAlert:  true,
+				ExpTime:            63,
+				ConsIngress:        2,
+				ConsEgress:         0,
+				Mac:                []byte{1, 2, 3, 4, 5, 6},
+			},
+		}
+		want := &scion.Decoded{
+			Base: scion.Base{
+				PathMeta: scion.MetaHdr{
+					CurrHF:  0,
+					CurrINF: 0,
+					SegLen:  [3]uint8{2, 0, 0},
+				},
+				NumHops: 2,
+				NumINF:  1,
+			},
+			HopFields:  []*path.HopField{&input.FirstHop, &input.SecondHop},
+			InfoFields: []*path.InfoField{&input.Info},
+		}
+		sp, err := input.ToSCIONDecoded()
+		assert.NoError(t, err)
+		assert.Equal(t, want, sp)
+	})
+	t.Run("incomplete path", func(t *testing.T) {
+		t.Parallel()
+		input := onehop.Path{
+			Info: path.InfoField{
+				ConsDir:   true,
+				SegID:     0x222,
+				Timestamp: 0x100,
+			},
+			FirstHop: path.HopField{
+				IngressRouterAlert: true,
+				EgressRouterAlert:  true,
+				ExpTime:            63,
+				ConsIngress:        0,
+				ConsEgress:         1,
+				Mac:                []byte{1, 2, 3, 4, 5, 6},
+			},
+		}
+		sp, err := input.ToSCIONDecoded()
+		assert.Error(t, err)
+		assert.Nil(t, sp)
+	})
+}
diff --git a/go/lib/slayers/path/scion/base_test.go b/go/lib/slayers/path/scion/base_test.go
index cac3243a2..21d5cb2bb 100644
--- a/go/lib/slayers/path/scion/base_test.go
+++ b/go/lib/slayers/path/scion/base_test.go
@@ -91,3 +91,63 @@ func TestIncPath(t *testing.T) {
 		}
 	}
 }
+
+func TestBaseIsXOver(t *testing.T) {
+	testCases := map[string]struct {
+		nsegs, nhops int
+		segLens      [3]uint8
+		inIdxs       [][2]int
+		xover        []bool
+	}{
+		"1 segment, 2 hops": {
+			nsegs:   1,
+			nhops:   2,
+			segLens: [3]uint8{2, 0, 0},
+			inIdxs:  [][2]int{{0, 0}, {0, 1}},
+			xover:   []bool{false, false},
+		},
+		"1 segment, 5 hops": {
+			nsegs:   1,
+			nhops:   5,
+			segLens: [3]uint8{5, 0, 0},
+			inIdxs:  [][2]int{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}},
+			xover:   []bool{false, false, false, false, false},
+		},
+		"2 segments, 5 hops": {
+			nsegs:   2,
+			nhops:   5,
+			segLens: [3]uint8{2, 3, 0},
+			inIdxs:  [][2]int{{0, 0}, {0, 1}, {1, 2}, {1, 3}, {1, 4}},
+			xover:   []bool{false, true, false, false, false},
+		},
+		"3 segments, 9 hops": {
+			nsegs:   3,
+			nhops:   9,
+			segLens: [3]uint8{2, 4, 3},
+			inIdxs: [][2]int{
+				{0, 0}, {0, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {2, 6}, {2, 7}, {2, 8},
+			},
+			xover: []bool{false, true, false, false, false, true, false, false, false},
+		},
+	}
+
+	for name, tc := range testCases {
+		name, tc := name, tc
+		for i := range tc.xover {
+			i := i
+			t.Run(fmt.Sprintf("%s case %d", name, i+1), func(t *testing.T) {
+				t.Parallel()
+				s := scion.Base{
+					PathMeta: scion.MetaHdr{
+						CurrINF: uint8(tc.inIdxs[i][0]),
+						CurrHF:  uint8(tc.inIdxs[i][1]),
+						SegLen:  tc.segLens,
+					},
+					NumINF:  tc.nsegs,
+					NumHops: tc.nhops,
+				}
+				assert.Equal(t, tc.xover[i], s.IsXover())
+			})
+		}
+	}
+}