diff --git a/acceptance/router_multi/BUILD.bazel b/acceptance/router_multi/BUILD.bazel index 21b526d717..8339785089 100644 --- a/acceptance/router_multi/BUILD.bazel +++ b/acceptance/router_multi/BUILD.bazel @@ -43,3 +43,15 @@ raw_test( # This test uses sudo and accesses /var/run/netns. local = True, ) + +raw_test( + name = "test_conndown", + src = "test.py", + args = args + [ + "--conndown", + ], + data = data, + homedir = "$(rootpath :conf)", + # This test uses sudo and accesses /var/run/netns. + local = True, +) diff --git a/acceptance/router_multi/test.py b/acceptance/router_multi/test.py index fc7da04a7a..7a791cca52 100644 --- a/acceptance/router_multi/test.py +++ b/acceptance/router_multi/test.py @@ -76,6 +76,11 @@ class RouterTest(base.TestBase): help="use BFD", ) + conndown = cli.Flag( + "conndown", + help="run connectivity down tests (BFD enabled)", + ) + def setup_prepare(self): super().setup_prepare() @@ -94,7 +99,7 @@ def setup_prepare(self): def setup_start(self): super().setup_start() - if self.bfd: + if self.bfd or self.conndown: exec_docker(f"run -v {self.artifacts}/conf:/etc/scion -d " "--network container:pause --name router " "scion/router:latest") @@ -107,10 +112,12 @@ def setup_start(self): def _run(self): braccept = self.get_executable("braccept") - bfd_arg = "" + extra_args = "" if self.bfd: - bfd_arg = "--bfd" - sudo("%s --artifacts %s %s" % (braccept.executable, self.artifacts, bfd_arg)) + extra_args = "--bfd" + elif self.conndown: + extra_args = "--conndown" + sudo("%s --artifacts %s %s" % (braccept.executable, self.artifacts, extra_args)) def teardown(self): cmd.docker["logs", "router"].run_fg(retcode=None) diff --git a/tools/braccept/cases/BUILD.bazel b/tools/braccept/cases/BUILD.bazel index 14cfa0a345..ac9a49e2e6 100644 --- a/tools/braccept/cases/BUILD.bazel +++ b/tools/braccept/cases/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "parent_to_internal.go", "peer_to_child.go", "scmp.go", + "scmp_conn_down.go", "scmp_dest_unreachable.go", "scmp_expired_hop.go", "scmp_invalid_hop.go", diff --git a/tools/braccept/cases/scmp_conn_down.go b/tools/braccept/cases/scmp_conn_down.go new file mode 100644 index 0000000000..a05780d394 --- /dev/null +++ b/tools/braccept/cases/scmp_conn_down.go @@ -0,0 +1,347 @@ +// Copyright 2026 ETH Zurich +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cases + +import ( + "hash" + "net" + "path/filepath" + "time" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/private/util" + "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" +) + +// SCMPExternalInterfaceDown tests that a transit packet (child to parent) gets an +// SCMP ExternalInterfaceDown error when the egress external interface's BFD session is down. +// This test case imitates ChildToParent, but generating SCMPExternalInterfaceDown error. +func SCMPExternalInterfaceDown(artifactsDir string, mac hash.Hash) runner.Case { + options := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + // Packet arriving on child interface 141 from AS 1-ff00:0:4, + // destined for AS 1-ff00:0:3 via parent interface 131. + ethernet := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0xbe, 0xef}, + DstMAC: net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x14}, + EthernetType: layers.EthernetTypeIPv4, + } + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + SrcIP: net.IP{192, 168, 14, 3}, + DstIP: net.IP{192, 168, 14, 2}, + 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, + SegLen: [3]uint8{3, 0, 0}, + }, + NumINF: 1, + NumHops: 3, + }, + InfoFields: []path.InfoField{ + { + SegID: 0x111, + Timestamp: util.TimeToSecs(time.Now()), + }, + }, + HopFields: []path.HopField{ + {ConsIngress: 411, ConsEgress: 0}, + {ConsIngress: 131, ConsEgress: 141}, + {ConsIngress: 0, ConsEgress: 311}, + }, + } + sp.HopFields[1].Mac = path.MAC(mac, sp.InfoFields[0], sp.HopFields[1], nil) + sp.InfoFields[0].UpdateSegID(sp.HopFields[1].Mac) + + scionL := &slayers.SCION{ + Version: 0, + TrafficClass: 0xb8, + FlowID: 0xdead, + NextHdr: slayers.L4UDP, + PathType: scion.PathType, + SrcIA: addr.MustParseIA("1-ff00:0:4"), + DstIA: addr.MustParseIA("1-ff00:0:3"), + Path: sp, + } + srcA := addr.MustParseHost("172.16.4.1") + if err := scionL.SetSrcAddr(addr.MustParseHost("172.16.4.1")); err != nil { + panic(err) + } + if err := scionL.SetDstAddr(addr.MustParseHost("174.16.3.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 quoted packet that is part of the SCMP error message. + sp.InfoFields[0].UpdateSegID(sp.HopFields[1].Mac) + quoted := gopacket.NewSerializeBuffer() // XXX: Why is the BR updating the SegID in the raw packet buffer before creating the quote? + if err := gopacket.SerializeLayers(quoted, options, + scionL, scionudp, gopacket.Payload(payload), + ); err != nil { + panic(err) + } + quote := quoted.Bytes() + + // Prepare want packet + want := gopacket.NewSerializeBuffer() + + ethernet.SrcMAC = net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x14} + ethernet.DstMAC = net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0xbe, 0xef} + ip.SrcIP = net.IP{192, 168, 14, 2} + ip.DstIP = net.IP{192, 168, 14, 3} + udp.SrcPort, udp.DstPort = udp.DstPort, udp.SrcPort + + scionL.DstIA = scionL.SrcIA + scionL.SrcIA = addr.MustParseIA("1-ff00:0:1") + if err := scionL.SetDstAddr(srcA); err != nil { + panic(err) + } + if err := scionL.SetSrcAddr(addr.MustParseHost("192.168.0.11")); err != nil { + panic(err) + } + + p, err := sp.Reverse() + if err != nil { + panic(err) + } + sp = p.(*scion.Decoded) + sp.InfoFields[0].UpdateSegID(sp.HopFields[1].Mac) + if err := sp.IncPath(); err != nil { + panic(err) + } + + scionL.NextHdr = slayers.End2EndClass + e2e := normalizedSCMPPacketAuthEndToEndExtn() + e2e.NextHdr = slayers.L4SCMP + + scmpH := &slayers.SCMP{ + TypeCode: slayers.CreateSCMPTypeCode(slayers.SCMPTypeExternalInterfaceDown, 0), + } + scmpH.SetNetworkLayerForChecksum(scionL) + scmpP := &slayers.SCMPExternalInterfaceDown{ + IA: addr.MustParseIA("1-ff00:0:1"), + IfID: 131, + } + + if err := gopacket.SerializeLayers(want, options, + ethernet, ip, udp, scionL, e2e, scmpH, scmpP, gopacket.Payload(quote), + ); err != nil { + panic(err) + } + + return runner.Case{ + Name: "SCMPExternalInterfaceDown", + WriteTo: "veth_141_host", + ReadFrom: "veth_141_host", + Input: input.Bytes(), + Want: want.Bytes(), + StoreDir: filepath.Join(artifactsDir, "SCMPExternalInterfaceDown"), + IgnoreNonMatching: true, + NormalizePacket: scmpNormalizePacket, + } +} + +// SCMPInternalConnectivityDown tests that a transit packet (child to sibling parent) gets an +// SCMP InternalConnectivityDown error when the egress sibling interface's BFD session is down. +// This test case imitates the ChildToInternalParent, but generating SCMPInternalInterfaceDown error. +func SCMPInternalConnectivityDown(artifactsDir string, mac hash.Hash) runner.Case { + options := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + // Packet arriving on child interface 141 from AS 1-ff00:0:4, + // destined for AS 1-ff00:0:9 via sibling interface 191 (on brD). + ethernet := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0xbe, 0xef}, + DstMAC: net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x14}, + EthernetType: layers.EthernetTypeIPv4, + } + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + SrcIP: net.IP{192, 168, 14, 3}, + DstIP: net.IP{192, 168, 14, 2}, + 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, + SegLen: [3]uint8{3, 0, 0}, + }, + NumINF: 1, + NumHops: 3, + }, + InfoFields: []path.InfoField{ + { + SegID: 0x111, + Timestamp: util.TimeToSecs(time.Now()), + }, + }, + HopFields: []path.HopField{ + {ConsIngress: 411, ConsEgress: 0}, + {ConsIngress: 191, ConsEgress: 141}, + {ConsIngress: 0, ConsEgress: 911}, + }, + } + sp.HopFields[1].Mac = path.MAC(mac, sp.InfoFields[0], sp.HopFields[1], nil) + sp.InfoFields[0].UpdateSegID(sp.HopFields[1].Mac) + + scionL := &slayers.SCION{ + Version: 0, + TrafficClass: 0xb8, + FlowID: 0xdead, + NextHdr: slayers.L4UDP, + PathType: scion.PathType, + SrcIA: addr.MustParseIA("1-ff00:0:4"), + DstIA: addr.MustParseIA("1-ff00:0:9"), + Path: sp, + } + srcA := addr.MustParseHost("172.16.4.1") + if err := scionL.SetSrcAddr(srcA); err != nil { + panic(err) + } + if err := scionL.SetDstAddr(addr.MustParseHost("172.16.9.1")); err != nil { + panic(err) + } + + scionudp := &slayers.UDP{} + scionudp.SrcPort = 2345 + scionudp.DstPort = 53 + 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 quoted packet that is part of the SCMP error message. + sp.InfoFields[0].UpdateSegID(sp.HopFields[1].Mac) // XXX: Why is the BR updating the SegID in the raw packet buffer before creating the quote? + quoted := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(quoted, options, + scionL, scionudp, gopacket.Payload(payload), + ); err != nil { + panic(err) + } + quote := quoted.Bytes() + + // Prepare want packet + want := gopacket.NewSerializeBuffer() + + ethernet.SrcMAC = net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x14} + ethernet.DstMAC = net.HardwareAddr{0xf0, 0x0d, 0xca, 0xfe, 0xbe, 0xef} + ip.SrcIP = net.IP{192, 168, 14, 2} + ip.DstIP = net.IP{192, 168, 14, 3} + udp.SrcPort, udp.DstPort = udp.DstPort, udp.SrcPort + + scionL.DstIA = scionL.SrcIA + scionL.SrcIA = addr.MustParseIA("1-ff00:0:1") + if err := scionL.SetDstAddr(srcA); err != nil { + panic(err) + } + if err := scionL.SetSrcAddr(addr.MustParseHost("192.168.0.11")); err != nil { + panic(err) + } + + p, err := sp.Reverse() + if err != nil { + panic(err) + } + sp = p.(*scion.Decoded) + sp.InfoFields[0].UpdateSegID(sp.HopFields[1].Mac) + if err := sp.IncPath(); err != nil { + panic(err) + } + + scionL.NextHdr = slayers.End2EndClass + e2e := normalizedSCMPPacketAuthEndToEndExtn() + e2e.NextHdr = slayers.L4SCMP + + scmpH := &slayers.SCMP{ + TypeCode: slayers.CreateSCMPTypeCode(slayers.SCMPTypeInternalConnectivityDown, 0), + } + scmpH.SetNetworkLayerForChecksum(scionL) + scmpP := &slayers.SCMPInternalConnectivityDown{ + IA: addr.MustParseIA("1-ff00:0:1"), + Ingress: 141, + Egress: 191, + } + + if err := gopacket.SerializeLayers(want, options, + ethernet, ip, udp, scionL, e2e, scmpH, scmpP, gopacket.Payload(quote), + ); err != nil { + panic(err) + } + + return runner.Case{ + Name: "SCMPInternalConnectivityDown", + WriteTo: "veth_141_host", + ReadFrom: "veth_141_host", + Input: input.Bytes(), + Want: want.Bytes(), + StoreDir: filepath.Join(artifactsDir, "SCMPInternalConnectivityDown"), + IgnoreNonMatching: true, + NormalizePacket: scmpNormalizePacket, + } +} diff --git a/tools/braccept/main.go b/tools/braccept/main.go index f8921bb337..600a2b88df 100644 --- a/tools/braccept/main.go +++ b/tools/braccept/main.go @@ -36,6 +36,7 @@ import ( var ( bfd = flag.Bool("bfd", false, "Run BFD tests instead of the common ones") + connDown = flag.Bool("conndown", false, "Run connectivity down tests (requires BFD enabled)") logConsole = flag.String("log.console", "debug", "Console logging level: debug|info|error") dir = flag.String("artifacts", "", "Artifacts directory") ) @@ -144,6 +145,13 @@ func realMain() int { } } + if *connDown { + multi = []runner.Case{ + cases.SCMPExternalInterfaceDown(artifactsDir, hfMAC), + cases.SCMPInternalConnectivityDown(artifactsDir, hfMAC), + } + } + ret := 0 for _, c := range multi { if err := c.Run(rc); err != nil {