From e80afbfc43a40acf1264559c571804a986fa51aa Mon Sep 17 00:00:00 2001 From: jiceatscion <139873336+jiceatscion@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:19:20 +0200 Subject: [PATCH] protocol: implement peering links (#4390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement support for peering links in the router. This is the first time that peering links are supported since the "new" SCION header, and small fixes in the segment combinator and some of the tools were needed. The main implementation was done by @benthor in #4299. This #4390 contains @jiceatscion's finalization pass of this, in particular adding tests. Closes #4299. Closes #4093. --------- Co-authored-by: Thorben Krüger --- acceptance/README.md | 16 +- control/beaconing/extender.go | 24 +- pkg/slayers/path/scion/base.go | 10 +- private/path/combinator/graph.go | 35 ++- private/topology/json/json.go | 11 +- private/topology/topology.go | 4 + router/control/conf.go | 5 +- router/dataplane.go | 120 ++++++-- router/dataplane_test.go | 410 +++++++++++++++++++++++--- scion/traceroute/traceroute.go | 5 +- tools/braccept/cases/BUILD.bazel | 3 + tools/braccept/cases/child_to_peer.go | 196 ++++++++++++ tools/braccept/cases/doc.go | 13 +- tools/braccept/cases/peer_to_child.go | 194 ++++++++++++ tools/braccept/main.go | 2 + tools/topology/topo.py | 18 +- topology/peering-test-multi.topo | 41 +++ topology/peering-test.topo | 40 +++ 18 files changed, 1059 insertions(+), 88 deletions(-) create mode 100644 tools/braccept/cases/child_to_peer.go create mode 100644 tools/braccept/cases/peer_to_child.go create mode 100644 topology/peering-test-multi.topo create mode 100644 topology/peering-test.topo diff --git a/acceptance/README.md b/acceptance/README.md index cab20b6f35..49b8b435a3 100644 --- a/acceptance/README.md +++ b/acceptance/README.md @@ -3,6 +3,9 @@ This directory contains a set of integration tests. Each test is defined as a bazel test target, with tags `integration` and `exclusive`. +Some integration tests use code outside this directory. For example, the +`router_multi` acceptance test cases and main executable are in `tools/braccept`. + ## Basic Commands To run all integration tests which include the acceptance tests, execute one of @@ -18,6 +21,7 @@ Run a subset of the tests by specifying a different list of targets: ```bash bazel test --config=integration //acceptance/cert_renewal:all //acceptance/trc_update/... +bazel test --config=integration //acceptance/router_multi:all --cache_test_results=no ``` The following the flags to bazel test can be helpful when running individual tests: @@ -27,8 +31,8 @@ The following the flags to bazel test can be helpful when running individual tes ## Manual Testing -Some of the tests are defined using a common framework, defined in the -bazel rules `topogen_test` and `raw_test`. +Some of the tests are defined using a common framework, implemented by the +bazel rules `topogen_test` and `raw_test` (in [raw.bzl](acceptance/common/raw.bzl)). These test cases allow more fine grained interaction. ```bash @@ -41,5 +45,13 @@ bazel run //:_run bazel run //:_teardown ``` +For example: + +```bash +bazel run //acceptance/router_multi:test_bfd_setup +bazel run //acceptance/router_multi:test_bfd_run +bazel run //acceptance/router_multi:test_bfd_teardown +``` + See [common/README](common/README.md) for more information about the internal structure of these tests. diff --git a/control/beaconing/extender.go b/control/beaconing/extender.go index 2c02c5e35f..7e5fe00a49 100644 --- a/control/beaconing/extender.go +++ b/control/beaconing/extender.go @@ -62,7 +62,7 @@ type DefaultExtender struct { EPIC bool } -// Extend extends the beacon with hop fields of the old format. +// Extend extends the beacon with hop fields. func (s *DefaultExtender) Extend( ctx context.Context, pseg *seg.PathSegment, @@ -85,11 +85,25 @@ func (s *DefaultExtender) Extend( } ts := pseg.Info.Timestamp - hopEntry, epicHopMac, err := s.createHopEntry(ingress, egress, ts, extractBeta(pseg)) + hopBeta := extractBeta(pseg) + hopEntry, epicHopMac, err := s.createHopEntry(ingress, egress, ts, hopBeta) if err != nil { return serrors.WrapStr("creating hop entry", err) } - peerBeta := extractBeta(pseg) ^ binary.BigEndian.Uint16(hopEntry.HopField.MAC[:2]) + + // The peer hop fields chain to the main hop field, just like any child hop field. + // The effect of this is that when a peer hop field is used in a path, both the + // peer hop field and its child are validated using the same SegID accumlator value: + // that originally intended for the child. + // + // The corrolary is that one cannot validate a hop field's MAC by looking at the + // parent hop field MAC when the parent is a peering hop field. This is ok: that + // is never done that way, it is always done by validating against the SegID + // accumulator supplied by the previous router on the forwarding path. The + // forwarding code takes care of not updating that accumulator when a peering hop + // is traversed. + + peerBeta := hopBeta ^ binary.BigEndian.Uint16(hopEntry.HopField.MAC[:2]) peerEntries, epicPeerMacs, err := s.createPeerEntries(egress, peers, ts, peerBeta) if err != nil { return err @@ -268,6 +282,10 @@ func (s *DefaultExtender) createHopF(ingress, egress uint16, ts time.Time, }, fullMAC[path.MacLen:] } +// extractBeta computes the beta value that must be used for the next hop to be +// added at the end of the segment. +// FIXME(jice): keeping an accumulator would be just as easy to do as it is during +// forwarding. What's the benefit of re-calculating the whole chain every time? func extractBeta(pseg *seg.PathSegment) uint16 { beta := pseg.Info.SegmentID for _, entry := range pseg.ASEntries { diff --git a/pkg/slayers/path/scion/base.go b/pkg/slayers/path/scion/base.go index dc6a9fc66e..b2a7812239 100644 --- a/pkg/slayers/path/scion/base.go +++ b/pkg/slayers/path/scion/base.go @@ -76,7 +76,9 @@ func (s *Base) IncPath() error { } if int(s.PathMeta.CurrHF) >= s.NumHops-1 { s.PathMeta.CurrHF = uint8(s.NumHops - 1) - return serrors.New("path already at end") + return serrors.New("path already at end", + "curr_hf", s.PathMeta.CurrHF, + "num_hops", s.NumHops) } s.PathMeta.CurrHF++ // Update CurrINF @@ -84,7 +86,11 @@ func (s *Base) IncPath() error { return nil } -// IsXover returns whether we are at a crossover point. +// IsXover returns whether we are at a crossover point. This includes +// all segment switches, even over a peering link. Note that handling +// of a regular segment switch and handling of a segment switch over a +// peering link are fundamentally different. To distinguish the two, +// you will need to extract the information from the info field. func (s *Base) IsXover() bool { return s.PathMeta.CurrHF+1 < uint8(s.NumHops) && s.PathMeta.CurrINF != s.infIndexForHF(s.PathMeta.CurrHF+1) diff --git a/private/path/combinator/graph.go b/private/path/combinator/graph.go index 6b90e4cba9..eb092ca709 100644 --- a/private/path/combinator/graph.go +++ b/private/path/combinator/graph.go @@ -239,7 +239,11 @@ func (g *dmg) GetPaths(src, dst vertex) pathSolutionList { } // inputSegment is a local representation of a path segment that includes the -// segment's type. +// segment's type. The type (up down or core) indicates the role that this +// segment holds in a path solution. That is, in which order the hops would +// be used for building an actual forwarding path (e.g. from the end in the +// case of an UP segment). However, the hops within the referred PathSegment +// *always* remain in construction order. type inputSegment struct { *seg.PathSegment Type proto.PathSegType @@ -316,7 +320,11 @@ func (solution *pathSolution) Path() Path { var pathASEntries []seg.ASEntry // ASEntries that on the path, eventually in path order. var epicSegAuths [][]byte - // Go through each ASEntry, starting from the last one, until we + // Segments are in construction order, regardless of whether they're + // up or down segments. We traverse them FROM THE END. So, in reverse + // forwarding order for down segments and in forwarding order for + // up segments. + // We go through each ASEntry, starting from the last one until we // find a shortcut (which can be 0, meaning the end of the segment). asEntries := solEdge.segment.ASEntries for asEntryIdx := len(asEntries) - 1; asEntryIdx >= solEdge.edge.Shortcut; asEntryIdx-- { @@ -378,6 +386,9 @@ func (solution *pathSolution) Path() Path { } } + // Put the hops in forwarding order. Needed for down segments + // since we collected hops from the end, just like for up + // segments. if solEdge.segment.Type == proto.PathSegType_down { reverseHops(hops) reverseIntfs(intfs) @@ -487,15 +498,30 @@ func reverseEpicAuths(s [][]byte) { } func calculateBeta(se *solutionEdge) uint16 { + + // If this is a peer hop, we need to set beta[i] = beta[i+1]. That is, the SegID + // accumulator must correspond to the next (in construction order) hop. + // + // This is because this peering hop has a MAC that chains to its non-peering + // counterpart, the same as what the next hop (in construction order) chains to. + // So both this and the next hop are to be validated from the same SegID + // accumulator value: the one for the *next* hop, calculated on the regular + // non-peering segment. + // + // Note that, when traversing peer hops, the SegID accumulator is left untouched for the + // next router on the path to use. + var index int if se.segment.IsDownSeg() { index = se.edge.Shortcut - // If this is a peer, we need to set beta i+1. if se.edge.Peer != 0 { index++ } } else { index = len(se.segment.ASEntries) - 1 + if index == se.edge.Shortcut && se.edge.Peer != 0 { + index++ + } } beta := se.segment.Info.SegmentID for i := 0; i < index; i++ { @@ -586,7 +612,8 @@ func validNextSeg(currSeg, nextSeg *inputSegment) bool { } // segment is a helper that represents a path segment during the conversion -// from the graph solution to the raw forwarding information. +// from the graph solution to the raw forwarding information. The hops should +// be in forwarding order. type segment struct { InfoField path.InfoField HopFields []path.HopField diff --git a/private/topology/json/json.go b/private/topology/json/json.go index 08e7cf333d..d07de43a01 100644 --- a/private/topology/json/json.go +++ b/private/topology/json/json.go @@ -107,11 +107,12 @@ type GatewayInfo struct { // BRInterface contains the information for an data-plane BR socket that is external (i.e., facing // the neighboring AS). type BRInterface struct { - Underlay Underlay `json:"underlay,omitempty"` - IA string `json:"isd_as"` - LinkTo string `json:"link_to"` - MTU int `json:"mtu"` - BFD *BFD `json:"bfd,omitempty"` + Underlay Underlay `json:"underlay,omitempty"` + IA string `json:"isd_as"` + LinkTo string `json:"link_to"` + MTU int `json:"mtu"` + BFD *BFD `json:"bfd,omitempty"` + RemoteIFID common.IFIDType `json:"remote_interface_id,omitempty"` } // Underlay is the underlay information for a BR interface. diff --git a/private/topology/topology.go b/private/topology/topology.go index 56bc023210..d2d3f5856e 100644 --- a/private/topology/topology.go +++ b/private/topology/topology.go @@ -243,6 +243,10 @@ func (t *RWTopology) populateBR(raw *jsontopo.Topology) error { return err } ifinfo.LinkType = LinkTypeFromString(rawIntf.LinkTo) + if ifinfo.LinkType == Peer { + ifinfo.RemoteIFID = rawIntf.RemoteIFID + } + if err = ifinfo.CheckLinks(t.IsCore, name); err != nil { return err } diff --git a/router/control/conf.go b/router/control/conf.go index 1e68e892cb..d5cf15a676 100644 --- a/router/control/conf.go +++ b/router/control/conf.go @@ -50,10 +50,11 @@ type LinkInfo struct { MTU int } -// LinkEnd represents on end of a link. +// LinkEnd represents one end of a link. type LinkEnd struct { IA addr.IA Addr *net.UDPAddr + IFID common.IFIDType } type ObservableDataplane interface { @@ -174,10 +175,12 @@ func confExternalInterfaces(dp Dataplane, cfg *Config) error { Local: LinkEnd{ IA: cfg.IA, Addr: snet.CopyUDPAddr(iface.Local), + IFID: iface.ID, }, Remote: LinkEnd{ IA: iface.IA, Addr: snet.CopyUDPAddr(iface.Remote), + IFID: iface.RemoteIFID, }, Instance: iface.BRName, BFD: WithDefaults(BFD(iface.BFD)), diff --git a/router/dataplane.go b/router/dataplane.go index 609ba33589..d47348f9cd 100644 --- a/router/dataplane.go +++ b/router/dataplane.go @@ -98,6 +98,7 @@ type DataPlane struct { external map[uint16]BatchConn linkTypes map[uint16]topology.LinkType neighborIAs map[uint16]addr.IA + peerInterfaces map[uint16]uint16 internal BatchConn internalIP netip.Addr internalNextHops map[uint16]*net.UDPAddr @@ -130,6 +131,11 @@ var ( noBFDSessionFound = serrors.New("no BFD sessions was found") noBFDSessionConfigured = serrors.New("no BFD sessions have been configured") errBFDDisabled = serrors.New("BFD is disabled") + errPeeringEmptySeg0 = serrors.New("zero-length segment[0] in peering path") + errPeeringEmptySeg1 = serrors.New("zero-length segment[1] in peering path") + errPeeringNonemptySeg2 = serrors.New("non-zero-length segment[2] in peering path") + errShortPacket = serrors.New("Packet is too short") + errBFDSessionDown = serrors.New("bfd session down") // zeroBuffer will be used to reset the Authenticator option in the // scionPacketProcessor.OptAuth zeroBuffer = make([]byte, 16) @@ -281,6 +287,24 @@ func (d *DataPlane) AddLinkType(ifID uint16, linkTo topology.LinkType) error { return nil } +// AddRemotePeer adds the remote peering interface ID for local +// interface ID. If the link type for the given ID is already set to +// a different type, this method will return an error. This can only +// be called on a not yet running dataplane. +func (d *DataPlane) AddRemotePeer(local, remote uint16) error { + if t, ok := d.linkTypes[local]; ok && t != topology.Peer { + return serrors.WithCtx(unsupportedPathType, "type", t) + } + if _, exists := d.peerInterfaces[local]; exists { + return serrors.WithCtx(alreadySet, "local_interface", local) + } + if d.peerInterfaces == nil { + d.peerInterfaces = make(map[uint16]uint16) + } + d.peerInterfaces[local] = remote + return nil +} + // AddExternalInterfaceBFD adds the inter AS connection BFD session. func (d *DataPlane) AddExternalInterfaceBFD(ifID uint16, conn BatchConn, src, dst control.LinkEnd, cfg control.BFD) error { @@ -621,13 +645,13 @@ func computeProcID(data []byte, numProcRoutines int, randomValue []byte, flowIDBuffer []byte, hasher hash.Hash32) (uint32, error) { if len(data) < slayers.CmnHdrLen { - return 0, serrors.New("Packet is too short") + return 0, errShortPacket } dstHostAddrLen := slayers.AddrType(data[9] >> 4 & 0xf).Length() srcHostAddrLen := slayers.AddrType(data[9] & 0xf).Length() addrHdrLen := 2*addr.IABytes + srcHostAddrLen + dstHostAddrLen if len(data) < slayers.CmnHdrLen+addrHdrLen { - return 0, serrors.New("Packet is too short") + return 0, errShortPacket } copy(flowIDBuffer[0:3], data[1:4]) flowIDBuffer[0] &= 0xF // the left 4 bits don't belong to the flowID @@ -819,7 +843,8 @@ func (p *scionPacketProcessor) reset() error { p.path = nil p.hopField = path.HopField{} p.infoField = path.InfoField{} - p.segmentChange = false + p.effectiveXover = false + p.peering = false if err := p.buffer.Clear(); err != nil { return serrors.WrapStr("Failed to clear buffer", err) } @@ -1010,8 +1035,10 @@ type scionPacketProcessor struct { hopField path.HopField // infoField is the current infoField field, is updated during processing. infoField path.InfoField - // segmentChange indicates if the path segment was changed during processing. - segmentChange bool + // effectiveXover indicates if a cross-over segment change was done during processing. + effectiveXover bool + // peering indicates that the hop field being processed is a peering hop field. + peering bool // cachedMac contains the full 16 bytes of the MAC. Will be set during processing. // For a hop performing an Xover, it is the MAC corresponding to the down segment. @@ -1081,6 +1108,32 @@ func (p *scionPacketProcessor) parsePath() (processResult, error) { return processResult{}, nil } +func (p *scionPacketProcessor) determinePeer() (processResult, error) { + if !p.infoField.Peer { + return processResult{}, nil + } + + if p.path.PathMeta.SegLen[0] == 0 { + return processResult{}, errPeeringEmptySeg0 + } + if p.path.PathMeta.SegLen[1] == 0 { + return processResult{}, errPeeringEmptySeg1 + + } + if p.path.PathMeta.SegLen[2] != 0 { + return processResult{}, errPeeringNonemptySeg2 + } + + // The peer hop fields are the last hop field on the first path + // segment (at SegLen[0] - 1) and the first hop field of the second + // path segment (at SegLen[0]). The below check applies only + // because we already know this is a well-formed peering path. + currHF := p.path.PathMeta.CurrHF + segLen := p.path.PathMeta.SegLen[0] + p.peering = currHF == segLen-1 || currHF == segLen + return processResult{}, nil +} + func (p *scionPacketProcessor) validateHopExpiry() (processResult, error) { expiration := util.SecsToTime(p.infoField.Timestamp). Add(path.ExpTimeToDuration(p.hopField.ExpTime)) @@ -1199,9 +1252,11 @@ func (p *scionPacketProcessor) validateEgressID() (processResult, error) { } ingress, egress := p.d.linkTypes[p.ingressID], p.d.linkTypes[pktEgressID] - if !p.segmentChange { + if !p.effectiveXover { // Check that the interface pair is valid within a single segment. // No check required if the packet is received from an internal interface. + // This case applies to peering hops as a peering hop isn't an effective + // cross-over (eventhough it is a segment change). switch { case p.ingressID == 0: return processResult{}, nil @@ -1211,6 +1266,10 @@ func (p *scionPacketProcessor) validateEgressID() (processResult, error) { return processResult{}, nil case ingress == topology.Parent && egress == topology.Child: return processResult{}, nil + case ingress == topology.Child && egress == topology.Peer: + return processResult{}, nil + case ingress == topology.Peer && egress == topology.Child: + return processResult{}, nil default: // malicious return p.packSCMP( slayers.SCMPTypeParameterProblem, @@ -1222,6 +1281,9 @@ func (p *scionPacketProcessor) validateEgressID() (processResult, error) { } // Check that the interface pair is valid on a segment switch. // Having a segment change received from the internal interface is never valid. + // We should never see a peering link traversal either. If that happens + // treat it as a routing error (not sure if that can happen without an internal + // error, though). switch { case ingress == topology.Core && egress == topology.Child: return processResult{}, nil @@ -1242,9 +1304,8 @@ func (p *scionPacketProcessor) validateEgressID() (processResult, error) { func (p *scionPacketProcessor) updateNonConsDirIngressSegID() error { // against construction dir the ingress router updates the SegID, ifID == 0 // means this comes from this AS itself, so nothing has to be done. - // TODO(lukedirtwalker): For packets destined to peer links this shouldn't - // be updated. - if !p.infoField.ConsDir && p.ingressID != 0 { + // For packets destined to peer links this shouldn't be updated. + if !p.infoField.ConsDir && p.ingressID != 0 && !p.peering { p.infoField.UpdateSegID(p.hopField.Mac) if err := p.path.SetInfoField(p.infoField, int(p.path.PathMeta.CurrINF)); err != nil { return serrors.WrapStr("update info field", err) @@ -1270,12 +1331,14 @@ func (p *scionPacketProcessor) verifyCurrentMAC() (processResult, error) { slayers.SCMPTypeParameterProblem, slayers.SCMPCodeInvalidHopFieldMAC, &slayers.SCMPParameterProblem{Pointer: p.currentHopPointer()}, - serrors.New("MAC verification failed", "expected", fmt.Sprintf( - "%x", fullMac[:path.MacLen]), + serrors.New("MAC verification failed", + "expected", fmt.Sprintf("%x", fullMac[:path.MacLen]), "actual", fmt.Sprintf("%x", p.hopField.Mac[:path.MacLen]), "cons_dir", p.infoField.ConsDir, - "if_id", p.ingressID, "curr_inf", p.path.PathMeta.CurrINF, - "curr_hf", p.path.PathMeta.CurrHF, "seg_id", p.infoField.SegID), + "if_id", p.ingressID, + "curr_inf", p.path.PathMeta.CurrINF, + "curr_hf", p.path.PathMeta.CurrHF, + "seg_id", p.infoField.SegID), ) } // Add the full MAC to the SCION packet processor, @@ -1300,9 +1363,12 @@ func (p *scionPacketProcessor) resolveInbound() (*net.UDPAddr, processResult, er } func (p *scionPacketProcessor) processEgress() error { - // we are the egress router and if we go in construction direction we - // need to update the SegID. - if p.infoField.ConsDir { + // We are the egress router and if we go in construction direction we + // need to update the SegID (unless we are effecting a peering hop). + // When we're at a peering hop, the SegID for this hop and for the next + // are one and the same, both hops chain to the same parent. So do not + // update SegID. + if p.infoField.ConsDir && !p.peering { p.infoField.UpdateSegID(p.hopField.Mac) if err := p.path.SetInfoField(p.infoField, int(p.path.PathMeta.CurrINF)); err != nil { // TODO parameter problem invalid path @@ -1317,7 +1383,7 @@ func (p *scionPacketProcessor) processEgress() error { } func (p *scionPacketProcessor) doXover() (processResult, error) { - p.segmentChange = true + p.effectiveXover = true if err := p.path.IncPath(); err != nil { // TODO parameter problem invalid path return processResult{}, serrors.WrapStr("incrementing path", err) @@ -1337,7 +1403,7 @@ func (p *scionPacketProcessor) doXover() (processResult, error) { func (p *scionPacketProcessor) ingressInterface() uint16 { info := p.infoField hop := p.hopField - if p.path.IsFirstHopAfterXover() { + if !p.peering && p.path.IsFirstHopAfterXover() { var err error info, err = p.path.GetInfoField(int(p.path.PathMeta.CurrINF) - 1) if err != nil { // cannot be out of range @@ -1378,7 +1444,7 @@ func (p *scionPacketProcessor) validateEgressUp() (processResult, error) { Egress: uint64(egressID), } } - return p.packSCMP(typ, 0, scmpP, serrors.New("bfd session down")) + return p.packSCMP(typ, 0, scmpP, errBFDSessionDown) } } return processResult{}, nil @@ -1475,10 +1541,12 @@ func (p *scionPacketProcessor) validatePktLen() (processResult, error) { } func (p *scionPacketProcessor) process() (processResult, error) { - if r, err := p.parsePath(); err != nil { return r, err } + if r, err := p.determinePeer(); err != nil { + return r, err + } if r, err := p.validateHopExpiry(); err != nil { return r, err } @@ -1514,10 +1582,14 @@ func (p *scionPacketProcessor) process() (processResult, error) { // Outbound: pkts leaving the local IA. // BRTransit: pkts leaving from the same BR different interface. - if p.path.IsXover() { + if p.path.IsXover() && !p.peering { + // An effective cross-over is a change of segment other than at + // a peering hop. if r, err := p.doXover(); err != nil { return r, err } + // doXover() has changed the current segment and hop field. + // We need to validate the new hop field. if r, err := p.validateHopExpiry(); err != nil { return r, serrors.WithCtx(err, "info", "after xover") } @@ -1819,7 +1891,9 @@ func (p *scionPacketProcessor) prepareSCMP( revPath := revPathTmp.(*scion.Decoded) // Revert potential path segment switches that were done during processing. - if revPath.IsXover() { + if revPath.IsXover() && !p.peering { + // An effective cross-over is a change of segment other than at + // a peering hop. if err := revPath.IncPath(); err != nil { return nil, serrors.Wrap(cannotRoute, err, "details", "reverting cross over for SCMP") } @@ -1829,7 +1903,7 @@ func (p *scionPacketProcessor) prepareSCMP( _, external := p.d.external[p.ingressID] if external { infoField := &revPath.InfoFields[revPath.PathMeta.CurrINF] - if infoField.ConsDir { + if infoField.ConsDir && !p.peering { hopField := revPath.HopFields[revPath.PathMeta.CurrHF] infoField.UpdateSegID(hopField.Mac) } diff --git a/router/dataplane_test.go b/router/dataplane_test.go index 1eb6b6d1d4..c83bc2701a 100644 --- a/router/dataplane_test.go +++ b/router/dataplane_test.go @@ -570,15 +570,17 @@ func TestProcessPkt(t *testing.T) { defer ctrl.Finish() key := []byte("testkey_xxxxxxxx") + otherKey := []byte("testkey_yyyyyyyy") now := time.Now() epicTS, err := libepic.CreateTimestamp(now, now) require.NoError(t, err) testCases := map[string]struct { - mockMsg func(bool) *ipv4.Message - prepareDP func(*gomock.Controller) *router.DataPlane - srcInterface uint16 - assertFunc assert.ErrorAssertionFunc + mockMsg func(bool) *ipv4.Message + prepareDP func(*gomock.Controller) *router.DataPlane + srcInterface uint16 + egressInterface uint16 + assertFunc assert.ErrorAssertionFunc }{ "inbound": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -604,8 +606,9 @@ func TestProcessPkt(t *testing.T) { } return ret }, - srcInterface: 1, - assertFunc: assert.NoError, + srcInterface: 1, + egressInterface: 0, + assertFunc: assert.NoError, }, "outbound": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -638,8 +641,9 @@ func TestProcessPkt(t *testing.T) { ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil return ret }, - srcInterface: 0, - assertFunc: assert.NoError, + srcInterface: 0, + egressInterface: 1, + assertFunc: assert.NoError, }, "brtransit": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -672,8 +676,9 @@ func TestProcessPkt(t *testing.T) { ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil return ret }, - srcInterface: 1, - assertFunc: assert.NoError, + srcInterface: 1, + egressInterface: 2, + assertFunc: assert.NoError, }, "brtransit non consdir": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -706,8 +711,320 @@ func TestProcessPkt(t *testing.T) { ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil return ret }, - srcInterface: 1, - assertFunc: assert.NoError, + srcInterface: 1, + egressInterface: 2, + assertFunc: assert.NoError, + }, + "brtransit peering consdir": { + prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { + return router.NewDP( + map[uint16]router.BatchConn{ + uint16(2): mock_router.NewMockBatchConn(ctrl), + }, + map[uint16]topology.LinkType{ + 1: topology.Peer, + 2: topology.Child, + }, + nil, nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key) + }, + mockMsg: func(afterProcessing bool) *ipv4.Message { + // Story: the packet just left segment 0 which ends at + // (peering) hop 0 and is landing on segment 1 which + // begins at (peering) hop 1. We do not care what hop 0 + // looks like. The forwarding code is looking at hop 1 and + // should leave the message in shape to be processed at hop 2. + spkt, _ := prepBaseMsg(now) + dpath := &scion.Decoded{ + Base: scion.Base{ + PathMeta: scion.MetaHdr{ + CurrHF: 1, + CurrINF: 1, + SegLen: [3]uint8{1, 2, 0}, + }, + NumINF: 2, + NumHops: 3, + }, + InfoFields: []path.InfoField{ + // up seg + {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true}, + // core seg + {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true}, + }, + HopFields: []path.HopField{ + {ConsIngress: 31, ConsEgress: 30}, + {ConsIngress: 1, ConsEgress: 2}, + {ConsIngress: 40, ConsEgress: 41}, + }, + } + + // Make obvious the unusual aspect of the path: two + // hopfield MACs (1 and 2) derive from the same SegID + // accumulator value. However, the forwarding code isn't + // supposed to even look at the second one. The SegID + // accumulator value can be anything (it comes from the + // parent hop of HF[1] in the original beaconned segment, + // which is not in the path). So, we use one from an + // info field because computeMAC makes that easy. + dpath.HopFields[1].Mac = computeMAC( + t, key, dpath.InfoFields[1], dpath.HopFields[1]) + dpath.HopFields[2].Mac = computeMAC( + t, otherKey, dpath.InfoFields[1], dpath.HopFields[2]) + if !afterProcessing { + return toMsg(t, spkt, dpath) + } + _ = dpath.IncPath() + + // ... The SegID accumulator wasn't updated from HF[1], + // it is still the same. That is the key behavior. + + ret := toMsg(t, spkt, dpath) + ret.Addr = nil + ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil + return ret + }, + srcInterface: 1, // from peering link + egressInterface: 2, + assertFunc: assert.NoError, + }, + "brtransit peering non consdir": { + prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { + return router.NewDP( + map[uint16]router.BatchConn{ + uint16(1): mock_router.NewMockBatchConn(ctrl), + }, + map[uint16]topology.LinkType{ + 1: topology.Peer, + 2: topology.Child, + }, + nil, nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key) + }, + mockMsg: func(afterProcessing bool) *ipv4.Message { + // Story: the packet lands on the last (peering) hop of + // segment 0. After processing, the packet is ready to + // be processed by the first (peering) hop of segment 1. + spkt, _ := prepBaseMsg(now) + dpath := &scion.Decoded{ + Base: scion.Base{ + PathMeta: scion.MetaHdr{ + CurrHF: 1, + CurrINF: 0, + SegLen: [3]uint8{2, 1, 0}, + }, + NumINF: 2, + NumHops: 3, + }, + InfoFields: []path.InfoField{ + // up seg + {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now), Peer: true}, + // down seg + {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true}, + }, + HopFields: []path.HopField{ + {ConsIngress: 31, ConsEgress: 30}, + {ConsIngress: 1, ConsEgress: 2}, + {ConsIngress: 40, ConsEgress: 41}, + }, + } + + // Make obvious the unusual aspect of the path: two + // hopfield MACs (0 and 1) derive from the same SegID + // accumulator value. However, the forwarding code isn't + // supposed to even look at the first one. The SegID + // accumulator value can be anything (it comes from the + // parent hop of HF[1] in the original beaconned segment, + // which is not in the path). So, we use one from an + // info field because computeMAC makes that easy. + dpath.HopFields[0].Mac = computeMAC( + t, otherKey, dpath.InfoFields[0], dpath.HopFields[0]) + dpath.HopFields[1].Mac = computeMAC( + t, key, dpath.InfoFields[0], dpath.HopFields[1]) + + // We're going against construction order, so the accumulator + // value is that of the previous hop in traversal order. The + // story starts with the packet arriving at hop 1, so the + // accumulator value must match hop field 0. In this case, + // it is identical to that for hop field 1, which we made + // identical to the original SegID. So, we're all set. + if !afterProcessing { + return toMsg(t, spkt, dpath) + } + + _ = dpath.IncPath() + + // The SegID should not get updated on arrival. If it is, then MAC validation + // of HF1 will fail. Otherwise, this isn't visible because we changed segment. + + ret := toMsg(t, spkt, dpath) + ret.Addr = nil + ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil + return ret + }, + srcInterface: 2, // from child link + egressInterface: 1, + assertFunc: assert.NoError, + }, + "peering consdir downstream": { + // Similar to previous test case but looking at what + // happens on the next hop. + prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { + return router.NewDP( + map[uint16]router.BatchConn{ + uint16(2): mock_router.NewMockBatchConn(ctrl), + }, + map[uint16]topology.LinkType{ + 1: topology.Peer, + 2: topology.Child, + }, + nil, nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key) + }, + mockMsg: func(afterProcessing bool) *ipv4.Message { + // Story: the packet just left hop 1 (the first hop + // of peering down segment 1) and is processed at hop 2 + // which is not a peering hop. + spkt, _ := prepBaseMsg(now) + dpath := &scion.Decoded{ + Base: scion.Base{ + PathMeta: scion.MetaHdr{ + CurrHF: 2, + CurrINF: 1, + SegLen: [3]uint8{1, 3, 0}, + }, + NumINF: 2, + NumHops: 4, + }, + InfoFields: []path.InfoField{ + // up seg + {SegID: 0x111, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true}, + // core seg + {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true}, + }, + HopFields: []path.HopField{ + {ConsIngress: 31, ConsEgress: 30}, + {ConsIngress: 40, ConsEgress: 41}, + {ConsIngress: 1, ConsEgress: 2}, + {ConsIngress: 50, ConsEgress: 51}, + // There has to be a 4th hop to make + // the 3rd router agree that the packet + // is not at destination yet. + }, + } + + // Make obvious the unusual aspect of the path: two + // hopfield MACs (1 and 2) derive from the same SegID + // accumulator value. The router shouldn't need to + // know this or do anything special. The SegID + // accumulator value can be anything (it comes from the + // parent hop of HF[1] in the original beaconned segment, + // which is not in the path). So, we use one from an + // info field because computeMAC makes that easy. + dpath.HopFields[1].Mac = computeMAC( + t, otherKey, dpath.InfoFields[1], dpath.HopFields[1]) + dpath.HopFields[2].Mac = computeMAC( + t, key, dpath.InfoFields[1], dpath.HopFields[2]) + if !afterProcessing { + // The SegID we provide is that of HF[2] which happens to be SEG[1]'s SegID, + // so, already set. + return toMsg(t, spkt, dpath) + } + _ = dpath.IncPath() + + // ... The SegID accumulator should have been updated. + dpath.InfoFields[1].UpdateSegID(dpath.HopFields[2].Mac) + + ret := toMsg(t, spkt, dpath) + ret.Addr = nil + ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil + return ret + }, + srcInterface: 1, + egressInterface: 2, + assertFunc: assert.NoError, + }, + "peering non consdir upstream": { + prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { + return router.NewDP( + map[uint16]router.BatchConn{ + uint16(1): mock_router.NewMockBatchConn(ctrl), + }, + map[uint16]topology.LinkType{ + 1: topology.Peer, + 2: topology.Child, + }, + nil, nil, nil, xtest.MustParseIA("1-ff00:0:110"), nil, key) + }, + mockMsg: func(afterProcessing bool) *ipv4.Message { + // Story: the packet lands on the second (non-peering) hop of + // segment 0 (a peering segment). After processing, the packet + // is ready to be processed by the third (peering) hop of segment 0. + spkt, _ := prepBaseMsg(now) + dpath := &scion.Decoded{ + Base: scion.Base{ + PathMeta: scion.MetaHdr{ + CurrHF: 1, + CurrINF: 0, + SegLen: [3]uint8{3, 1, 0}, + }, + NumINF: 2, + NumHops: 4, + }, + InfoFields: []path.InfoField{ + // up seg + {SegID: 0x111, ConsDir: false, Timestamp: util.TimeToSecs(now), Peer: true}, + // down seg + {SegID: 0x222, ConsDir: true, Timestamp: util.TimeToSecs(now), Peer: true}, + }, + HopFields: []path.HopField{ + {ConsIngress: 31, ConsEgress: 30}, + {ConsIngress: 1, ConsEgress: 2}, + {ConsIngress: 40, ConsEgress: 41}, + {ConsIngress: 50, ConsEgress: 51}, + // The second segment (4th hop) has to be + // there but the packet isn't processed + // at that hop for this test. + }, + } + + // Make obvious the unusual aspect of the path: two + // hopfield MACs (1 and 2) derive from the same SegID + // accumulator value. The SegID accumulator value can + // be anything (it comes from the parent hop of HF[1] + // in the original beaconned segment, which is not in + // the path). So, we use one from an info field because + // computeMAC makes that easy. + dpath.HopFields[1].Mac = computeMAC( + t, key, dpath.InfoFields[0], dpath.HopFields[1]) + dpath.HopFields[2].Mac = computeMAC( + t, otherKey, dpath.InfoFields[0], dpath.HopFields[2]) + + if !afterProcessing { + // We're going against construction order, so the + // before-processing accumulator value is that of + // the previous hop in traversal order. The story + // starts with the packet arriving at hop 1, so the + // accumulator value must match hop field 0, which + // derives from hop field[1]. HopField[0]'s MAC is + // not checked during this test. + dpath.InfoFields[0].UpdateSegID(dpath.HopFields[1].Mac) + + return toMsg(t, spkt, dpath) + } + + _ = dpath.IncPath() + + // After-processing, the SegID should have been updated + // (on ingress) to be that of HF[1], which happens to be + // the Segment's SegID. That is what we already have as + // we only change it in the before-processing version + // of the packet. + + ret := toMsg(t, spkt, dpath) + ret.Addr = nil + ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil + return ret + }, + srcInterface: 2, // from child link + egressInterface: 1, + assertFunc: assert.NoError, }, "astransit direct": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -736,8 +1053,9 @@ func TestProcessPkt(t *testing.T) { } return ret }, - srcInterface: 1, - assertFunc: assert.NoError, + srcInterface: 1, + egressInterface: 0, + assertFunc: assert.NoError, }, "astransit xover": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -788,8 +1106,9 @@ func TestProcessPkt(t *testing.T) { ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil return ret }, - srcInterface: 51, - assertFunc: assert.NoError, + srcInterface: 51, + egressInterface: 0, + assertFunc: assert.NoError, }, "svc": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -823,8 +1142,9 @@ func TestProcessPkt(t *testing.T) { } return ret }, - srcInterface: 1, - assertFunc: assert.NoError, + srcInterface: 1, + egressInterface: 0, + assertFunc: assert.NoError, }, "svc nobackend": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -846,8 +1166,9 @@ func TestProcessPkt(t *testing.T) { ret := toMsg(t, spkt, dpath) return ret }, - srcInterface: 1, - assertFunc: assertIsSCMPError(slayers.SCMPTypeDestinationUnreachable, 0), + srcInterface: 1, + egressInterface: 0, + assertFunc: assertIsSCMPError(slayers.SCMPTypeDestinationUnreachable, 0), }, "svc invalid": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -869,8 +1190,9 @@ func TestProcessPkt(t *testing.T) { ret := toMsg(t, spkt, dpath) return ret }, - srcInterface: 1, - assertFunc: assertIsSCMPError(slayers.SCMPTypeDestinationUnreachable, 0), + srcInterface: 1, + egressInterface: 0, + assertFunc: assertIsSCMPError(slayers.SCMPTypeDestinationUnreachable, 0), }, "onehop inbound": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -931,8 +1253,9 @@ func TestProcessPkt(t *testing.T) { ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil return ret }, - srcInterface: 1, - assertFunc: assert.NoError, + srcInterface: 1, + egressInterface: 0, + assertFunc: assert.NoError, }, "onehop inbound invalid src": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -967,8 +1290,9 @@ func TestProcessPkt(t *testing.T) { } return toMsg(t, spkt, dpath) }, - srcInterface: 1, - assertFunc: assert.Error, + srcInterface: 1, + egressInterface: 21, + assertFunc: assert.Error, }, "reversed onehop outbound": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -1026,8 +1350,9 @@ func TestProcessPkt(t *testing.T) { ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil return ret }, - srcInterface: 0, - assertFunc: assert.NoError, + srcInterface: 0, + egressInterface: 1, + assertFunc: assert.NoError, }, "onehop outbound": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -1072,8 +1397,9 @@ func TestProcessPkt(t *testing.T) { ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil return ret }, - srcInterface: 0, - assertFunc: assert.NoError, + srcInterface: 0, + egressInterface: 2, + assertFunc: assert.NoError, }, "invalid dest": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -1094,7 +1420,8 @@ func TestProcessPkt(t *testing.T) { ret := toMsg(t, spkt, dpath) return ret }, - srcInterface: 1, + srcInterface: 1, + egressInterface: 0, assertFunc: assertIsSCMPError( slayers.SCMPTypeParameterProblem, slayers.SCMPCodeInvalidDestinationAddress, @@ -1112,8 +1439,9 @@ func TestProcessPkt(t *testing.T) { prepareEpicCrypto(t, spkt, epicpath, dpath, key) return toIP(t, spkt, epicpath, afterProcessing) }, - srcInterface: 1, - assertFunc: assert.NoError, + srcInterface: 1, + egressInterface: 0, + assertFunc: assert.NoError, }, "epic malformed path": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -1128,8 +1456,9 @@ func TestProcessPkt(t *testing.T) { // Wrong path type return toIP(t, spkt, &scion.Decoded{}, afterProcessing) }, - srcInterface: 1, - assertFunc: assert.Error, + srcInterface: 1, + egressInterface: 0, + assertFunc: assert.Error, }, "epic invalid timestamp": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -1146,8 +1475,9 @@ func TestProcessPkt(t *testing.T) { prepareEpicCrypto(t, spkt, epicpath, dpath, key) return toIP(t, spkt, epicpath, afterProcessing) }, - srcInterface: 1, - assertFunc: assert.Error, + srcInterface: 1, + egressInterface: 0, + assertFunc: assert.Error, }, "epic invalid LHVF": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { @@ -1164,8 +1494,9 @@ func TestProcessPkt(t *testing.T) { return toIP(t, spkt, epicpath, afterProcessing) }, - srcInterface: 1, - assertFunc: assert.Error, + srcInterface: 1, + egressInterface: 0, + assertFunc: assert.Error, }, } @@ -1188,6 +1519,7 @@ func TestProcessPkt(t *testing.T) { outPkt.Addr = nil } assert.Equal(t, want, outPkt) + assert.Equal(t, tc.egressInterface, result.EgressID) }) } } diff --git a/scion/traceroute/traceroute.go b/scion/traceroute/traceroute.go index d2805a30bc..7ac6a6cb1a 100644 --- a/scion/traceroute/traceroute.go +++ b/scion/traceroute/traceroute.go @@ -163,7 +163,10 @@ func (t *tracerouter) Traceroute(ctx context.Context) (Stats, error) { t.updateHandler(u) } } - xover := idxPath.IsXover() + // Peering links do not count as regular cross + // overs. For peering links we probe all interfaces on + // the path. + xover := idxPath.IsXover() && !info.Peer // The last hop of the path isn't probed, only the ingress interface is // relevant. // At a crossover (segment change) only the ingress interface is diff --git a/tools/braccept/cases/BUILD.bazel b/tools/braccept/cases/BUILD.bazel index c25dd33e91..30cb91f472 100644 --- a/tools/braccept/cases/BUILD.bazel +++ b/tools/braccept/cases/BUILD.bazel @@ -7,12 +7,14 @@ go_library( "child_to_child_xover.go", "child_to_internal.go", "child_to_parent.go", + "child_to_peer.go", "doc.go", "internal_to_child.go", "jumbo.go", "onehop.go", "parent_to_child.go", "parent_to_internal.go", + "peer_to_child.go", "scmp.go", "scmp_dest_unreachable.go", "scmp_expired_hop.go", @@ -33,6 +35,7 @@ go_library( "//pkg/drkey:go_default_library", "//pkg/private/util:go_default_library", "//pkg/private/xtest:go_default_library", + "//pkg/scrypto:go_default_library", "//pkg/slayers:go_default_library", "//pkg/slayers/path:go_default_library", "//pkg/slayers/path/empty:go_default_library", diff --git a/tools/braccept/cases/child_to_peer.go b/tools/braccept/cases/child_to_peer.go new file mode 100644 index 0000000000..5d0d1081e9 --- /dev/null +++ b/tools/braccept/cases/child_to_peer.go @@ -0,0 +1,196 @@ +// Copyright 2023 SCION Association +// +// 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 cases + +import ( + "hash" + "net" + "path/filepath" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/private/util" + "github.com/scionproto/scion/pkg/private/xtest" + "github.com/scionproto/scion/pkg/scrypto" + "github.com/scionproto/scion/pkg/slayers" + "github.com/scionproto/scion/pkg/slayers/path" + "github.com/scionproto/scion/pkg/slayers/path/scion" + "github.com/scionproto/scion/tools/braccept/runner" +) + +// ChildToPeer tests transit traffic over one BR host and one peering hop. +// In this case, traffic enters via a regular link, and leaves via a peering link from +// the same router. To be valid, the path as to be constructed as one up segment over +// the normal link ending with a peering hop and one down segment starting at the +// peering link's destination. The peering hop is the second hop on the first segment +// as it crosses from a child interface to a peering interface. +// In this test case, the down segment is a one-hop segment. The peering link's destination +// is the only hop. +func ChildToPeer(artifactsDir string, mac hash.Hash) runner.Case { + options := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + // We inject the packet into A (at IF 151) as if coming from 5 (at IF 511) + ethernet := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0xbe, 0xef}, // IF 511 + DstMAC: net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x15}, // IF 151 + EthernetType: layers.EthernetTypeIPv4, + } + + ip := &layers.IPv4{ // On the 5->A link + Version: 4, + IHL: 5, + TTL: 64, + SrcIP: net.IP{192, 168, 15, 3}, // from 5's 511 IP + DstIP: net.IP{192, 168, 15, 2}, // to A's 151 IP + Protocol: layers.IPProtocolUDP, + Flags: layers.IPv4DontFragment, + } + + udp := &layers.UDP{ + SrcPort: layers.UDPPort(40000), + DstPort: layers.UDPPort(50000), + } + _ = udp.SetNetworkLayerForChecksum(ip) + + sp := &scion.Decoded{ + Base: scion.Base{ + PathMeta: scion.MetaHdr{ + CurrHF: 1, + CurrINF: 0, + SegLen: [3]uint8{2, 1, 0}, + }, + NumINF: 2, + NumHops: 3, + }, + InfoFields: []path.InfoField{ + // up seg + { + SegID: 0x111, + ConsDir: false, + Timestamp: util.TimeToSecs(time.Now()), + Peer: true, + }, + // down seg + { + SegID: 0x222, + ConsDir: true, + Timestamp: util.TimeToSecs(time.Now()), + }, + }, + HopFields: []path.HopField{ + {ConsIngress: 511, ConsEgress: 0}, // at 5 leaving to A + {ConsIngress: 121, ConsEgress: 151}, // at A in from 5 out to 2 + {ConsIngress: 211, ConsEgress: 0}, // at 2 in coming from A + }, + } + + // Make the packet look the way it should... We have three hops of interrest. + + // Hops are all signed with different keys. Only HF[1] was signed by + // the AS that we hand the packet to. The others can be anything as they + // couldn't be check at that AS anyway. + macGenX, err := scrypto.InitMac([]byte("1234567812345678")) + if err != nil { + panic(err) + } + macGenY, err := scrypto.InitMac([]byte("abcdefghabcdefgh")) + if err != nil { + panic(err) + } + + // HF[1] is a peering hop, so it has the same SegID acc value as the next one + // in construction direction, HF[0]. That is, SEG[0]'s SegID. + sp.HopFields[1].Mac = path.MAC(mac, sp.InfoFields[0], sp.HopFields[1], nil) + sp.HopFields[0].Mac = path.MAC(macGenX, sp.InfoFields[0], sp.HopFields[0], nil) + + // The second segment has just one hop. + sp.HopFields[2].Mac = path.MAC(macGenY, sp.InfoFields[1], sp.HopFields[2], nil) + + // The message is ready for ingest at A, that is at HF[1]. Going against consruction + // direction, the SegID acc value must match that of HF[0], which is the same + // as that of HF[1], which is also SEG[0]'s SegID. So it's already correct. + + // The end-to-end trip is from 5,172.16.5.1 to 2,172.16.2.1 + // That won't change through forwarding. + scionL := &slayers.SCION{ + Version: 0, + TrafficClass: 0xb8, + FlowID: 0xdead, + NextHdr: slayers.L4UDP, + PathType: scion.PathType, + SrcIA: xtest.MustParseIA("1-ff00:0:5"), + DstIA: xtest.MustParseIA("1-ff00:0:2"), + Path: sp, + } + if err := scionL.SetSrcAddr(addr.MustParseHost("172.16.5.1")); err != nil { + panic(err) + } + if err := scionL.SetDstAddr(addr.MustParseHost("174.16.2.1")); err != nil { + panic(err) + } + + scionudp := &slayers.UDP{} + scionudp.SrcPort = 40111 + scionudp.DstPort = 40222 + scionudp.SetNetworkLayerForChecksum(scionL) + + payload := []byte("actualpayloadbytes") + + // Prepare input packet + input := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(input, options, + ethernet, ip, udp, scionL, scionudp, gopacket.Payload(payload), + ); err != nil { + panic(err) + } + + // Prepare want packet + // We expect it out of A's 121 IF on its way to 4's 211 IF. + + want := gopacket.NewSerializeBuffer() + ethernet.SrcMAC = net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x12} // IF 121 + ethernet.DstMAC = net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0xbe, 0xef} // IF 211 + ip.SrcIP = net.IP{192, 168, 12, 2} // from A's 121 IP + ip.DstIP = net.IP{192, 168, 12, 3} // to 2's 211 IP + udp.SrcPort, udp.DstPort = udp.DstPort, udp.SrcPort + if err := sp.IncPath(); err != nil { + panic(err) + } + + // Out of A, the current segment is seg 1. The Current acc + // value matches HF[2], which is SEG[1]'s SegID since HF[2] is the first hop in + // construction direction of the segment. + + if err := gopacket.SerializeLayers(want, options, + ethernet, ip, udp, scionL, scionudp, gopacket.Payload(payload), + ); err != nil { + panic(err) + } + + return runner.Case{ + Name: "ChildToChildPeeringOut", + WriteTo: "veth_151_host", // Where we inject the test packet + ReadFrom: "veth_121_host", // Where we capture the forwarded packet + Input: input.Bytes(), + Want: want.Bytes(), + StoreDir: filepath.Join(artifactsDir, "ChildToChildXover"), + } +} diff --git a/tools/braccept/cases/doc.go b/tools/braccept/cases/doc.go index 9481e6215a..843568dad3 100644 --- a/tools/braccept/cases/doc.go +++ b/tools/braccept/cases/doc.go @@ -18,6 +18,13 @@ into the braccept binary. The process to add a new case is the following: +Step 0. Refer to the following for the test's setup: + - Overview: acceptance/router_multi/topology.drawio.png + - Topology Details: acceptance/router_multi/conf/topology.json + - MAC Addresses: acceptance/router_multi/test.py + Note that all MAC addresses of interfaces on the far side + of the A/B/C/D routers are identical: f00d:cafe:beef + Step 1. Add a new file with a representative name e.g. cases/child_to_child_xover.go @@ -45,7 +52,9 @@ Step 3. In the braccept/main.go, include the above function cases.ChildToChildXover(artifactsDir, mac), } -Step 4. Do a local run, which means set up a working router and execute the -braccept. +Step 4. Do a local run, which means set up a working router, execute the +braccept, shutdown the router. This is done in sequence by: + + bazel test acceptance/router_multi:all --config=integration --nocache_test_results */ package cases diff --git a/tools/braccept/cases/peer_to_child.go b/tools/braccept/cases/peer_to_child.go new file mode 100644 index 0000000000..8c9d561fe9 --- /dev/null +++ b/tools/braccept/cases/peer_to_child.go @@ -0,0 +1,194 @@ +// Copyright 2023 SCION Association +// +// 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 cases + +import ( + "hash" + "net" + "path/filepath" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/private/util" + "github.com/scionproto/scion/pkg/private/xtest" + "github.com/scionproto/scion/pkg/scrypto" + "github.com/scionproto/scion/pkg/slayers" + "github.com/scionproto/scion/pkg/slayers/path" + "github.com/scionproto/scion/pkg/slayers/path/scion" + "github.com/scionproto/scion/tools/braccept/runner" +) + +// PeerToChild tests transit traffic over one BR host and one peering hop. +// In this case, traffic enters via a peering link, and leaves via a regular link from +// the same router. To be valid, the path as to be constructed as one up +// segment ending at the peering link's origin and one down segment over +// the regular link. The peering hop is the first hop on the second segment as +// it crosses from a peering interface to a child interface. +// In this test case, the up segment is a one-hop segment. The peering link's +// origin is the only hop. +func PeerToChild(artifactsDir string, mac hash.Hash) runner.Case { + options := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + // We inject the packet into A (at IF 121) as if coming from 2 (at IF 211) + ethernet := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0xbe, 0xef}, // IF 211 + DstMAC: net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x12}, // IF 121 + EthernetType: layers.EthernetTypeIPv4, + } + + ip := &layers.IPv4{ // On the 2->A link + Version: 4, + IHL: 5, + TTL: 64, + SrcIP: net.IP{192, 168, 12, 3}, // from 2's 211 IP + DstIP: net.IP{192, 168, 12, 2}, // to A's 121 IP + Protocol: layers.IPProtocolUDP, + Flags: layers.IPv4DontFragment, + } + + udp := &layers.UDP{ + SrcPort: layers.UDPPort(40000), + DstPort: layers.UDPPort(50000), + } + _ = udp.SetNetworkLayerForChecksum(ip) + + sp := &scion.Decoded{ + Base: scion.Base{ + PathMeta: scion.MetaHdr{ + CurrHF: 1, + CurrINF: 1, + SegLen: [3]uint8{1, 2, 0}, + }, + NumINF: 2, + NumHops: 3, + }, + InfoFields: []path.InfoField{ + // up seg + { + SegID: 0x111, + ConsDir: false, + Timestamp: util.TimeToSecs(time.Now()), + }, + // down seg + { + SegID: 0x222, + ConsDir: true, + Timestamp: util.TimeToSecs(time.Now()), + Peer: true, + }, + }, + HopFields: []path.HopField{ + {ConsIngress: 211, ConsEgress: 0}, // at 2 out to A + {ConsIngress: 121, ConsEgress: 151}, // at A in from 2 out to 5 + {ConsIngress: 511, ConsEgress: 0}, // at 5 in from A + }, + } + + // Make the packet look the way it should... We have three hops of interrest. + + // Hops are all signed with different keys. Only HF[1] was signed by + // the AS that we hand the packet to. The others can be anything as they + // couldn't be check at that AS anyway. + macGenX, err := scrypto.InitMac([]byte("1234567812345678")) + if err != nil { + panic(err) + } + macGenY, err := scrypto.InitMac([]byte("abcdefghabcdefgh")) + if err != nil { + panic(err) + } + + // HF[0] is a regular hop. + sp.HopFields[0].Mac = path.MAC(macGenX, sp.InfoFields[0], sp.HopFields[0], nil) + + // HF[1] is a peering hop so it has the same SegID acc value as the next one + // in construction direction, HF[2]. That is, SEG[1]'s SegID. + sp.HopFields[1].Mac = path.MAC(mac, sp.InfoFields[1], sp.HopFields[1], nil) + sp.HopFields[2].Mac = path.MAC(macGenY, sp.InfoFields[1], sp.HopFields[2], nil) + + // The message if ready for ingest at A, that is at HF[1], the start of the + // second segment, in construction direction. So SegID is already correct. + + // The end-to-end trip is from 2,172.16.2.1 to 5,172.16.5.1 + // That won't change through forwarding. + scionL := &slayers.SCION{ + Version: 0, + TrafficClass: 0xb8, + FlowID: 0xdead, + NextHdr: slayers.L4UDP, + PathType: scion.PathType, + SrcIA: xtest.MustParseIA("1-ff00:0:2"), + DstIA: xtest.MustParseIA("1-ff00:0:5"), + Path: sp, + } + if err := scionL.SetSrcAddr(addr.MustParseHost("172.16.2.1")); err != nil { + panic(err) + } + if err := scionL.SetDstAddr(addr.MustParseHost("174.16.5.1")); err != nil { + panic(err) + } + + scionudp := &slayers.UDP{} + scionudp.SrcPort = 40111 + scionudp.DstPort = 40222 + scionudp.SetNetworkLayerForChecksum(scionL) + + payload := []byte("actualpayloadbytes") + + // Prepare input packet + input := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(input, options, + ethernet, ip, udp, scionL, scionudp, gopacket.Payload(payload), + ); err != nil { + panic(err) + } + + // Prepare want packet + // We expect it out of A's 151 IF on its way to 5's 511 IF. + + want := gopacket.NewSerializeBuffer() + ethernet.SrcMAC = net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x15} // IF 151 + ethernet.DstMAC = net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0xbe, 0xef} // IF 511 + ip.SrcIP = net.IP{192, 168, 15, 2} // from A's 151 IP + ip.DstIP = net.IP{192, 168, 15, 3} // to 5's 511 IP + udp.SrcPort, udp.DstPort = udp.DstPort, udp.SrcPort + if err := sp.IncPath(); err != nil { + panic(err) + } + + // Out of A, the current segment is seg 1. The Current acc + // value is still the same since HF[1] is a peering hop. + + if err := gopacket.SerializeLayers(want, options, + ethernet, ip, udp, scionL, scionudp, gopacket.Payload(payload), + ); err != nil { + panic(err) + } + + return runner.Case{ + Name: "ChildToChildPeeringTransit", + WriteTo: "veth_121_host", // Where we inject the test packet + ReadFrom: "veth_151_host", // Where we capture the forwarded packet + Input: input.Bytes(), + Want: want.Bytes(), + StoreDir: filepath.Join(artifactsDir, "ChildToChildXover"), + } +} diff --git a/tools/braccept/main.go b/tools/braccept/main.go index 9a54104d56..fc27bf7afc 100644 --- a/tools/braccept/main.go +++ b/tools/braccept/main.go @@ -128,6 +128,8 @@ func realMain() int { cases.OutgoingOneHop(artifactsDir, hfMAC), cases.SVC(artifactsDir, hfMAC), cases.JumboPacket(artifactsDir, hfMAC), + cases.ChildToPeer(artifactsDir, hfMAC), + cases.PeerToChild(artifactsDir, hfMAC), } if *bfd { diff --git a/tools/topology/topo.py b/tools/topology/topo.py index e92f8106cf..298605bdc8 100644 --- a/tools/topology/topo.py +++ b/tools/topology/topo.py @@ -298,6 +298,9 @@ def _gen_br_entry(self, local, l_ifid, remote, r_ifid, remote_type, attrs, r_ifid, link_addr_type) intl_addr = self._reg_addr(local, local_br + "_internal", addr_type) + + intf = self._gen_br_intf(remote, r_ifid, public_addr, remote_addr, attrs, remote_type) + if self.topo_dicts[local]["border_routers"].get(local_br) is None: intl_port = 30042 if not self.args.docker: @@ -306,24 +309,27 @@ def _gen_br_entry(self, local, l_ifid, remote, r_ifid, remote_type, attrs, self.topo_dicts[local]["border_routers"][local_br] = { 'internal_addr': join_host_port(intl_addr.ip, intl_port), 'interfaces': { - l_ifid: self._gen_br_intf(remote, public_addr, remote_addr, attrs, remote_type) + l_ifid: intf } } else: # There is already a BR entry, add interface - intf = self._gen_br_intf(remote, public_addr, remote_addr, attrs, remote_type) self.topo_dicts[local]["border_routers"][local_br]['interfaces'][l_ifid] = intf - def _gen_br_intf(self, remote, public_addr, remote_addr, attrs, remote_type): - return { + def _gen_br_intf(self, remote, r_ifid, public_addr, remote_addr, attrs, remote_type): + link_to = remote_type.name.lower() + intf = { 'underlay': { 'public': join_host_port(public_addr.ip, SCION_ROUTER_PORT), 'remote': join_host_port(remote_addr.ip, SCION_ROUTER_PORT), }, 'isd_as': str(remote), - 'link_to': remote_type.name.lower(), - 'mtu': attrs.get('mtu', self.args.default_mtu) + 'link_to': link_to, + 'mtu': attrs.get('mtu', self.args.default_mtu), } + if link_to == 'peer': + intf['remote_interface_id'] = r_ifid + return intf def _gen_sig_entries(self, topo_id, as_conf): addr_type = addr_type_from_underlay(as_conf.get('underlay', DEFAULT_UNDERLAY)) diff --git a/topology/peering-test-multi.topo b/topology/peering-test-multi.topo new file mode 100644 index 0000000000..b328739484 --- /dev/null +++ b/topology/peering-test-multi.topo @@ -0,0 +1,41 @@ +--- # Topology demonstrating peering, IPv4 Only +ASes: + "1-ff00:0:110": + core: true + voting: true + authoritative: true + issuing: true + mtu: 1400 + "1-ff00:0:111": + cert_issuer: 1-ff00:0:110 + "1-ff00:0:112": + cert_issuer: 1-ff00:0:110 + "1-ff00:0:113": + cert_issuer: 1-ff00:0:110 + "1-ff00:0:114": + cert_issuer: 1-ff00:0:110 + "2-ff00:0:210": + core: true + voting: true + authoritative: true + issuing: true + mtu: 1400 + "2-ff00:0:211": + cert_issuer: 2-ff00:0:210 + "2-ff00:0:212": + cert_issuer: 2-ff00:0:210 + "2-ff00:0:213": + cert_issuer: 2-ff00:0:210 + + +links: + - {a: "1-ff00:0:110#1", b: "1-ff00:0:111#41", linkAtoB: CHILD, mtu: 1280} + - {a: "1-ff00:0:110#2", b: "1-ff00:0:112#1", linkAtoB: CHILD, bw: 500} + - {a: "1-ff00:0:112#3", b: "1-ff00:0:113#3", linkAtoB: CHILD, bw: 500} + - {a: "1-ff00:0:113#4", b: "1-ff00:0:114#4", linkAtoB: CHILD, bw: 500} + - {a: "1-ff00:0:112#47", b: "1-ff00:0:114#11", linkAtoB: PEER} + - {a: "1-ff00:0:110#3", b: "2-ff00:0:210#3", linkAtoB: CORE} + - {a: "2-ff00:0:210#1", b: "2-ff00:0:211#3", linkAtoB: CHILD, mtu: 1280} + - {a: "2-ff00:0:210#2", b: "2-ff00:0:212#7", linkAtoB: CHILD, bw: 500} + - {a: "2-ff00:0:212#3", b: "2-ff00:0:213#3", linkAtoB: CHILD, bw: 500} + - {a: "1-ff00:0:112#42", b: "2-ff00:0:212#23", linkAtoB: PEER} diff --git a/topology/peering-test.topo b/topology/peering-test.topo new file mode 100644 index 0000000000..3f52f7cfec --- /dev/null +++ b/topology/peering-test.topo @@ -0,0 +1,40 @@ +--- # Topology demonstrating peering, IPv4 Only +ASes: + "1-ff00:0:110": + core: true + voting: true + authoritative: true + issuing: true + mtu: 1400 + "1-ff00:0:111": + cert_issuer: 1-ff00:0:110 + "1-ff00:0:112": + cert_issuer: 1-ff00:0:110 + "1-ff00:0:113": + cert_issuer: 1-ff00:0:110 + "1-ff00:0:114": + cert_issuer: 1-ff00:0:110 + "2-ff00:0:210": + core: true + voting: true + authoritative: true + issuing: true + mtu: 1400 + "2-ff00:0:211": + cert_issuer: 2-ff00:0:210 + "2-ff00:0:212": + cert_issuer: 2-ff00:0:210 + "2-ff00:0:213": + cert_issuer: 2-ff00:0:210 + + +links: + - {a: "1-ff00:0:110#1", b: "1-ff00:0:111#41", linkAtoB: CHILD, mtu: 1280} + - {a: "1-ff00:0:110#2", b: "1-ff00:0:112#1", linkAtoB: CHILD, bw: 500} + - {a: "1-ff00:0:112#3", b: "1-ff00:0:113#3", linkAtoB: CHILD, bw: 500} + - {a: "1-ff00:0:113#4", b: "1-ff00:0:114#4", linkAtoB: CHILD, bw: 500} + - {a: "1-ff00:0:110#3", b: "2-ff00:0:210#3", linkAtoB: CORE} + - {a: "2-ff00:0:210#1", b: "2-ff00:0:211#3", linkAtoB: CHILD, mtu: 1280} + - {a: "2-ff00:0:210#2", b: "2-ff00:0:212#7", linkAtoB: CHILD, bw: 500} + - {a: "2-ff00:0:212#3", b: "2-ff00:0:213#3", linkAtoB: CHILD, bw: 500} + - {a: "1-ff00:0:112#42", b: "2-ff00:0:212#23", linkAtoB: PEER}