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}