diff --git a/acceptance/common/topogen.bzl b/acceptance/common/topogen.bzl index b400803b2b..3a15ab97fa 100644 --- a/acceptance/common/topogen.bzl +++ b/acceptance/common/topogen.bzl @@ -11,6 +11,7 @@ def topogen_test( src, topo, gateway = False, + fabrid = False, debug = False, args = [], deps = [], @@ -57,6 +58,10 @@ def topogen_test( if gateway: common_args.append("--setup-params='--sig'") + if fabrid: + common_args.append("--setup-params='--fabrid'") + common_args.append("--setup-params='--endhost'") + common_data = [ "//scion-pki/cmd/scion-pki", "//tools:topogen", diff --git a/daemon/internal/servers/BUILD.bazel b/daemon/internal/servers/BUILD.bazel index 01e28ca333..290053034c 100644 --- a/daemon/internal/servers/BUILD.bazel +++ b/daemon/internal/servers/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "//pkg/addr:go_default_library", "//pkg/drkey:go_default_library", "//pkg/experimental/fabrid:go_default_library", + "//pkg/experimental/fabrid/graphutils:go_default_library", "//pkg/grpc:go_default_library", "//pkg/log:go_default_library", "//pkg/metrics:go_default_library", @@ -28,7 +29,6 @@ go_library( "//pkg/segment/extensions/fabrid:go_default_library", "//pkg/snet:go_default_library", "//pkg/snet/path:go_default_library", - "//private/path/combinator:go_default_library", "//private/revcache:go_default_library", "//private/topology:go_default_library", "//private/trust:go_default_library", diff --git a/daemon/internal/servers/grpc.go b/daemon/internal/servers/grpc.go index 5592ee5606..4928db80e1 100644 --- a/daemon/internal/servers/grpc.go +++ b/daemon/internal/servers/grpc.go @@ -31,6 +31,7 @@ import ( "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/drkey" "github.com/scionproto/scion/pkg/experimental/fabrid" + fabrid_utils "github.com/scionproto/scion/pkg/experimental/fabrid/graphutils" libgrpc "github.com/scionproto/scion/pkg/grpc" "github.com/scionproto/scion/pkg/log" "github.com/scionproto/scion/pkg/private/common" @@ -45,7 +46,6 @@ import ( fabrid_ext "github.com/scionproto/scion/pkg/segment/extensions/fabrid" "github.com/scionproto/scion/pkg/snet" snetpath "github.com/scionproto/scion/pkg/snet/path" - "github.com/scionproto/scion/private/path/combinator" "github.com/scionproto/scion/private/revcache" "github.com/scionproto/scion/private/topology" "github.com/scionproto/scion/private/trust" @@ -138,13 +138,13 @@ func updateFabridInfo(ctx context.Context, dialer libgrpc.Dialer, detachedHops [ } defer conn.Close() client := experimental.NewFABRIDIntraServiceClient(conn) - fabridMaps := make(map[addr.IA]combinator.FabridMapEntry) + fabridMaps := make(map[addr.IA]fabrid_utils.FabridMapEntry) for _, detachedHop := range detachedHops { if _, ok := fabridMaps[detachedHop.IA]; !ok { fabridMaps[detachedHop.IA] = fetchMaps(ctx, detachedHop.IA, client, detachedHop.Meta.FabridInfo[detachedHop.fiIdx].Digest) } - detachedHop.Meta.FabridInfo[detachedHop.fiIdx] = *combinator. + detachedHop.Meta.FabridInfo[detachedHop.fiIdx] = *fabrid_utils. GetFabridInfoForIntfs(detachedHop.IA, detachedHop.Ingress, detachedHop.Egress, fabridMaps, true) } @@ -194,7 +194,7 @@ func findDetachedHops(paths []snet.Path) []tempHopInfo { // It uses the provided client to communicate with the Control Service and returns a FabridMapEntry // to be used directly in the combinator. func fetchMaps(ctx context.Context, ia addr.IA, client experimental.FABRIDIntraServiceClient, - digest []byte) combinator.FabridMapEntry { + digest []byte) fabrid_utils.FabridMapEntry { maps, err := client.RemoteMaps(ctx, &experimental.RemoteMapsRequest{ Digest: digest, IsdAs: uint64(ia), @@ -202,14 +202,14 @@ func fetchMaps(ctx context.Context, ia addr.IA, client experimental.FABRIDIntraS if err != nil || maps.Maps == nil { log.FromCtx(ctx).Debug("Retrieving remote map from CS failed", "err", err, "ia", ia) - return combinator.FabridMapEntry{} + return fabrid_utils.FabridMapEntry{} } detached := fabrid_ext.Detached{ SupportedIndicesMap: fabrid_ext.SupportedIndicesMapFromPB(maps.Maps.SupportedIndicesMap), IndexIdentiferMap: fabrid_ext.IndexIdentifierMapFromPB(maps.Maps.IndexIdentifierMap), } - return combinator.FabridMapEntry{ + return fabrid_utils.FabridMapEntry{ Map: &detached, Ts: time.Now(), Digest: []byte{}, // leave empty, it can be calculated using detached.Hash() diff --git a/demo/fabrid/BUILD.bazel b/demo/fabrid/BUILD.bazel new file mode 100644 index 0000000000..647ca9099d --- /dev/null +++ b/demo/fabrid/BUILD.bazel @@ -0,0 +1,50 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary") +load("//:scion.bzl", "scion_go_binary") +load("//acceptance/common:topogen.bzl", "topogen_test") +load("//tools/lint:go.bzl", "go_library") + +topogen_test( + name = "test", + src = "test.py", + args = [ + "--executable=fabrid-demo:$(location //demo/fabrid:fabrid-demo)", + ], + data = ["//demo/fabrid:fabrid-demo"], + topo = "//topology:tiny4.topo", + fabrid = True, +) + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "github.com/scionproto/scion/demo/fabrid", + visibility = ["//visibility:private"], + deps = [ + "//pkg/addr:go_default_library", + "//pkg/daemon:go_default_library", + "//pkg/drkey:go_default_library", + "//pkg/drkey/generic:go_default_library", + "//pkg/drkey/specific:go_default_library", + "//pkg/private/serrors:go_default_library", + "//pkg/proto/control_plane:go_default_library", + "//pkg/proto/drkey:go_default_library", + "//pkg/scrypto/cppki:go_default_library", + "//pkg/snet:go_default_library", + "//private/app/flag:go_default_library", + "@com_github_spf13_pflag//:go_default_library", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", + ], +) + +scion_go_binary( + name = "fabrid-demo", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) + +go_binary( + name = "fabrid", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) diff --git a/demo/fabrid/README.md b/demo/fabrid/README.md new file mode 100644 index 0000000000..9f3d29f3e7 --- /dev/null +++ b/demo/fabrid/README.md @@ -0,0 +1,26 @@ +# FABRID demo + +This demo shows how two hosts can obtain a shared key with the DRKey system. +The "server" side host can locally derive keys for any other host. +The slower "client" side host can fetch its corresponding key from +the DRKey infrastructure running in the control services. + +Note that in this demo, no data is transmitted between "client" and "server". +In a practical usage, the server would derive the key for the client's address +after receiving a packet from the client. + +The demo consists of the following steps: + +1. Enable and configure DRKey and start the topology. +1. Demonstrate the server side key derivation +1. Demonstrate the client side key fetching +1. Compare the keys + +## Run the demo + +1. [set up the development environment](https://docs.scion.org/en/latest/build/setup.html) +1. `bazel test --test_output=streamed --cache_test_results=no //demo/fabrid:test` + +Note: this demo works on any SCION network topology. To run the demo on a +different network topology, modify the `topo` parameter in `BUILD.bazel` to +point to a different topology file. diff --git a/demo/fabrid/main.go b/demo/fabrid/main.go new file mode 100644 index 0000000000..9c2984f29c --- /dev/null +++ b/demo/fabrid/main.go @@ -0,0 +1,887 @@ +// Copyright 2024 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 main + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + libfabrid "github.com/scionproto/scion/pkg/experimental/fabrid" + common2 "github.com/scionproto/scion/pkg/experimental/fabrid/common" + fabridserver "github.com/scionproto/scion/pkg/experimental/fabrid/server" + "github.com/scionproto/scion/pkg/log" + "github.com/scionproto/scion/pkg/private/common" + "github.com/scionproto/scion/pkg/slayers" + "github.com/scionproto/scion/pkg/slayers/extension" + "github.com/scionproto/scion/pkg/slayers/path/scion" + snetpath "github.com/scionproto/scion/pkg/snet/path" + "github.com/scionproto/scion/private/tracing" + libint "github.com/scionproto/scion/tools/integration" + integration "github.com/scionproto/scion/tools/integration/integrationlib" + "net" + "net/netip" + "os" + "time" + + flag "github.com/spf13/pflag" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon" + "github.com/scionproto/scion/pkg/drkey" + "github.com/scionproto/scion/pkg/drkey/generic" + "github.com/scionproto/scion/pkg/drkey/specific" + "github.com/scionproto/scion/pkg/private/serrors" + cppb "github.com/scionproto/scion/pkg/proto/control_plane" + dkpb "github.com/scionproto/scion/pkg/proto/drkey" + "github.com/scionproto/scion/pkg/scrypto/cppki" + "github.com/scionproto/scion/pkg/snet" + env "github.com/scionproto/scion/private/app/flag" +) + +func main() { + os.Exit(realMain()) +} + +func realMain() int { + var serverMode bool + var serverAddrStr, clientAddrStr string + var protocol uint16 + var fetchSV bool + var scionEnv env.SCIONEnvironment + + scionEnv.Register(flag.CommandLine) + flag.BoolVar(&serverMode, "server", false, "Demonstrate server-side key derivation."+ + " (default demonstrate client-side key fetching)") + flag.StringVar(&serverAddrStr, "server-addr", "", "SCION address for the server-side.") + flag.StringVar(&clientAddrStr, "client-addr", "", "SCION address for the client-side.") + flag.Uint16Var(&protocol, "protocol", 1 /* SCMP */, "DRKey protocol identifier.") + flag.BoolVar(&fetchSV, "fetch-sv", false, + "Fetch protocol specific secret value to derive server-side keys.") + flag.Parse() + if err := scionEnv.LoadExternalVars(); err != nil { + fmt.Fprintln(os.Stderr, "Error reading SCION environment:", err) + return 2 + } + + // NOTE: should parse addresses as snet.SCIONAddress not snet.UDPAddress, but + // these parsing functions don't exist yet. + serverAddr, err := snet.ParseUDPAddr(serverAddrStr) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid --server-addr '%s': %s\n", serverAddrStr, err) + return 2 + } + clientAddr, err := snet.ParseUDPAddr(clientAddrStr) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid --client-addr '%s': %s\n", clientAddrStr, err) + return 2 + } + + if !serverMode && fetchSV { + fmt.Fprintf(os.Stderr, "Invalid flag --fetch-sv for client-side key derivation\n") + return 2 + } + + ctx, cancelF := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelF() + + // meta describes the key that both client and server derive + meta := drkey.HostHostMeta{ + ProtoId: drkey.Protocol(protocol), + // Validity timestamp; both sides need to use a validity time stamp in the same epoch. + // Usually this is coordinated by means of a timestamp in the message. + Validity: time.Now(), + // SrcIA is the AS on the "fast side" of the DRKey derivation; + // the server side in this example. + SrcIA: serverAddr.IA, + // DstIA is the AS on the "slow side" of the DRKey derivation; + // the client side in this example. + DstIA: clientAddr.IA, + SrcHost: serverAddr.Host.IP.String(), + DstHost: clientAddr.Host.IP.String(), + } + + daemon, err := daemon.NewService(scionEnv.Daemon()).Connect(ctx) + if err != nil { + fmt.Fprintln(os.Stderr, "Error dialing SCION Daemon:", err) + return 1 + } + + if serverMode { + // Server: get the Secret Value (SV) for the protocol and derive all + // subsequent keys in-process + server := Server{daemon} + var serverKey drkey.HostHostKey + var t0, t1, t2 time.Time + if fetchSV { + // Fetch the Secret Value (SV); in a real application, this is only done at + // startup and refreshed for each epoch. + t0 = time.Now() + sv, err := server.FetchSV(ctx, drkey.SecretValueMeta{ + ProtoId: meta.ProtoId, + Validity: meta.Validity, + }) + if err != nil { + fmt.Fprintln(os.Stderr, "Error fetching secret value:", err) + return 1 + } + t1 = time.Now() + serverKey, err = server.DeriveHostHostKeySpecific(sv, meta) + if err != nil { + fmt.Fprintln(os.Stderr, "Error deriving key:", err) + return 1 + } + t2 = time.Now() + } else { + // Fetch host-AS key (Level 2). This key can be used to derive keys for + // all hosts in the destination AS. Depending on the application, it can + // be cached and refreshed for each epoch. + t0 = time.Now() + hostASKey, err := server.FetchHostASKey(ctx, drkey.HostASMeta{ + ProtoId: meta.ProtoId, + Validity: meta.Validity, + SrcIA: meta.SrcIA, + DstIA: meta.DstIA, + SrcHost: meta.SrcHost, + }) + if err != nil { + fmt.Fprintln(os.Stderr, "Error fetching host-AS key:", err) + return 1 + } + t1 = time.Now() + serverKey, err = server.DeriveHostHostKeyGeneric(hostASKey, meta) + if err != nil { + fmt.Fprintln(os.Stderr, "Error deriving key:", err) + return 1 + } + t2 = time.Now() + } + fmt.Printf( + "Server: host key = %s, protocol = %s, fetch-sv = %v"+ + "\n\tduration without cache: %s\n\tduration with cache: %s\n", + hex.EncodeToString(serverKey.Key[:]), meta.ProtoId, fetchSV, t2.Sub(t0), t2.Sub(t1), + ) + } else { + // Client: fetch key from daemon + // The daemon will in turn obtain the key from the local CS + // The CS will fetch the Lvl1 key from the CS in the SrcIA (the server's AS) + // and derive the Host key based on this. + client := Client{daemon} + var t0, t1 time.Time + t0 = time.Now() + clientKey, err := client.FetchHostHostKey(ctx, meta) + if err != nil { + fmt.Fprintln(os.Stderr, "Error fetching key:", err) + return 1 + } + t1 = time.Now() + + fmt.Printf( + "Client: host key = %s, protocol = %s\n\tduration: %s\n", + hex.EncodeToString(clientKey.Key[:]), meta.ProtoId, t1.Sub(t0), + ) + } + return 0 +} + +type Client struct { + daemon daemon.Connector +} + +func (c Client) FetchHostHostKey( + ctx context.Context, meta drkey.HostHostMeta) (drkey.HostHostKey, error) { + + // get level 3 key: (slow path) + return c.daemon.DRKeyGetHostHostKey(ctx, meta) +} + +type Server struct { + daemon daemon.Connector +} + +func (s Server) DeriveHostHostKeySpecific( + sv drkey.SecretValue, + meta drkey.HostHostMeta, +) (drkey.HostHostKey, error) { + + var deriver specific.Deriver + lvl1, err := deriver.DeriveLevel1(meta.DstIA, sv.Key) + if err != nil { + return drkey.HostHostKey{}, serrors.WrapStr("deriving level 1 key", err) + } + asHost, err := deriver.DeriveHostAS(meta.SrcHost, lvl1) + if err != nil { + return drkey.HostHostKey{}, serrors.WrapStr("deriving host-AS key", err) + } + hosthost, err := deriver.DeriveHostHost(meta.DstHost, asHost) + if err != nil { + return drkey.HostHostKey{}, serrors.WrapStr("deriving host-host key", err) + } + return drkey.HostHostKey{ + ProtoId: sv.ProtoId, + Epoch: sv.Epoch, + SrcIA: meta.SrcIA, + DstIA: meta.DstIA, + SrcHost: meta.SrcHost, + DstHost: meta.DstHost, + Key: hosthost, + }, nil +} + +func (s Server) DeriveHostHostKeyGeneric( + hostAS drkey.HostASKey, + meta drkey.HostHostMeta, +) (drkey.HostHostKey, error) { + + deriver := generic.Deriver{ + Proto: hostAS.ProtoId, + } + hosthost, err := deriver.DeriveHostHost(meta.DstHost, hostAS.Key) + if err != nil { + return drkey.HostHostKey{}, serrors.WrapStr("deriving host-host key", err) + } + return drkey.HostHostKey{ + ProtoId: hostAS.ProtoId, + Epoch: hostAS.Epoch, + SrcIA: meta.SrcIA, + DstIA: meta.DstIA, + SrcHost: meta.SrcHost, + DstHost: meta.DstHost, + Key: hosthost, + }, nil +} + +// FetchSV obtains the Secret Value (SV) for the selected protocol/epoch. +// From this SV, all keys for this protocol/epoch can be derived locally. +// The IP address of the server must be explicitly allowed to abtain this SV +// from the the control server. +func (s Server) FetchSV( + ctx context.Context, + meta drkey.SecretValueMeta, +) (drkey.SecretValue, error) { + + // Obtain CS address from scion daemon + svcs, err := s.daemon.SVCInfo(ctx, nil) + if err != nil { + return drkey.SecretValue{}, serrors.WrapStr("obtaining control service address", err) + } + cs := svcs[addr.SvcCS] + if len(cs) == 0 { + return drkey.SecretValue{}, serrors.New("no control service address found") + } + + // Contact CS directly for SV + conn, err := grpc.DialContext(ctx, cs[0], grpc.WithInsecure()) + if err != nil { + return drkey.SecretValue{}, serrors.WrapStr("dialing control service", err) + } + defer conn.Close() + client := cppb.NewDRKeyIntraServiceClient(conn) + + rep, err := client.DRKeySecretValue(ctx, &cppb.DRKeySecretValueRequest{ + ValTime: timestamppb.New(meta.Validity), + ProtocolId: dkpb.Protocol(meta.ProtoId), + }) + if err != nil { + return drkey.SecretValue{}, serrors.WrapStr("requesting drkey secret value", err) + } + + key, err := getSecretFromReply(meta.ProtoId, rep) + if err != nil { + return drkey.SecretValue{}, serrors.WrapStr("validating drkey secret value reply", err) + } + + return key, nil +} + +func getSecretFromReply( + proto drkey.Protocol, + rep *cppb.DRKeySecretValueResponse, +) (drkey.SecretValue, error) { + + if err := rep.EpochBegin.CheckValid(); err != nil { + return drkey.SecretValue{}, err + } + if err := rep.EpochEnd.CheckValid(); err != nil { + return drkey.SecretValue{}, err + } + epoch := drkey.Epoch{ + Validity: cppki.Validity{ + NotBefore: rep.EpochBegin.AsTime(), + NotAfter: rep.EpochEnd.AsTime(), + }, + } + returningKey := drkey.SecretValue{ + ProtoId: proto, + Epoch: epoch, + } + copy(returningKey.Key[:], rep.Key) + return returningKey, nil +} + +func (s Server) FetchHostASKey( + ctx context.Context, meta drkey.HostASMeta) (drkey.HostASKey, error) { + + // get level 2 key: (fast path) + return s.daemon.DRKeyGetHostASKey(ctx, meta) +} + +type server struct { + fabridServer *fabridserver.Server +} + +func (s server) run() { + fmt.Printf("Starting server", "isd_as", integration.Local.IA) + defer fmt.Printf("Finished server", "isd_as", integration.Local.IA) + + sdConn := integration.SDConn() + defer sdConn.Close() + sn := &snet.SCIONNetwork{ + SCMPHandler: snet.DefaultSCMPHandler{ + RevocationHandler: daemon.RevHandler{Connector: sdConn}, + SCMPErrors: scmpErrorsCounter, + }, + PacketConnMetrics: scionPacketConnMetrics, + Topology: sdConn, + } + conn, err := sn.OpenRaw(context.Background(), integration.Local.Host) + if err != nil { + integration.LogFatal("Error listening", "err", err) + } + defer conn.Close() + localAddr := conn.LocalAddr().(*net.UDPAddr) + if len(os.Getenv(libint.GoIntegrationEnv)) > 0 { + // Needed for integration test ready signal. + fmt.Printf("Port=%d\n", localAddr.Port) + fmt.Printf("%s%s\n\n", libint.ReadySignal, integration.Local.IA) + } + fmt.Printf("Listening", "local", + fmt.Sprintf("%v:%d", integration.Local.Host.IP, localAddr.Port)) + s.fabridServer = fabridserver.NewFabridServer(&integration.Local, integration.SDConn()) + s.fabridServer.ValidationHandler = func(connection *fabridserver.ClientConnection, + option *extension.IdentifierOption, b bool) error { + log.Debug("Validation handler", "connection", connection, "success", b) + if !b { + return serrors.New("Failed validation") + } + return nil + } + // Receive ping message + for { + if err := s.handlePingFabrid(conn); err != nil { + log.Error("Error handling ping", "err", err) + } + } +} + +func (s server) handlePingFabrid(conn snet.PacketConn) error { + var p snet.Packet + var ov net.UDPAddr + err := readFromFabrid(conn, &p, &ov) + if err != nil { + return serrors.WrapStr("reading packet", err) + } + + var valResponse *slayers.EndToEndExtn + + // If the packet is from remote IA, validate the FABRID path + if p.Source.IA != integration.Local.IA { + if p.HbhExtension == nil { + return serrors.New("Missing HBH extension") + } + + // Check extensions for relevant options + var identifierOption *extension.IdentifierOption + var fabridOption *extension.FabridOption + var controlOptions []*extension.FabridControlOption + var err error + + for _, opt := range p.HbhExtension.Options { + switch opt.OptType { + case slayers.OptTypeIdentifier: + decoded := scion.Decoded{} + err = decoded.DecodeFromBytes(p.Path.(snet.RawPath).Raw) + if err != nil { + return err + } + baseTimestamp := decoded.InfoFields[0].Timestamp + identifierOption, err = extension.ParseIdentifierOption(opt, baseTimestamp) + if err != nil { + return err + } + case slayers.OptTypeFabrid: + fabridOption, err = extension.ParseFabridOptionFullExtension(opt, + (opt.OptDataLen-4)/4) + if err != nil { + return err + } + } + } + if p.E2eExtension != nil { + + for _, opt := range p.E2eExtension.Options { + switch opt.OptType { + case slayers.OptTypeFabridControl: + controlOption, err := extension.ParseFabridControlOption(opt) + if err != nil { + return err + } + controlOptions = append(controlOptions, controlOption) + } + } + } + + if identifierOption == nil { + return serrors.New("Missing identifier option") + } + + if fabridOption == nil { + return serrors.New("Missing FABRID option") + } + valResponse, err = s.fabridServer.HandleFabridPacket(p.Source, fabridOption, + identifierOption, controlOptions) + if err != nil { + return err + } + } + + udp, ok := p.Payload.(snet.UDPPayload) + if !ok { + return serrors.New("unexpected payload received", + "source", p.Source, + "destination", p.Destination, + "type", common.TypeOf(p.Payload), + ) + } + var pld Ping + if err := json.Unmarshal(udp.Payload, &pld); err != nil { + return serrors.New("invalid payload contents", + "source", p.Source, + "destination", p.Destination, + "data", string(udp.Payload), + ) + } + + spanCtx, err := opentracing.GlobalTracer().Extract( + opentracing.Binary, + bytes.NewReader(pld.Trace), + ) + if err != nil { + return serrors.WrapStr("extracting trace information", err) + } + span, _ := opentracing.StartSpanFromContext( + context.Background(), + "handle_ping", + ext.RPCServerOption(spanCtx), + ) + defer span.Finish() + withTag := func(err error) error { + tracing.Error(span, err) + return err + } + + if pld.Message != ping || !pld.Server.Equal(integration.Local.IA) { + return withTag(serrors.New("unexpected data in payload", + "source", p.Source, + "destination", p.Destination, + "data", pld, + )) + } + fmt.Printf(fmt.Sprintf("Ping received from %s, sending pong.", p.Source)) + raw, err := json.Marshal(Pong{ + Client: p.Source.IA, + Server: integration.Local.IA, + Message: pong, + Trace: pld.Trace, + }) + if err != nil { + return withTag(serrors.WrapStr("packing pong", err)) + } + + p.Destination, p.Source = p.Source, p.Destination + p.Payload = snet.UDPPayload{ + DstPort: udp.SrcPort, + SrcPort: udp.DstPort, + Payload: raw, + } + + // Remove header extension for reverse path + p.HbhExtension = nil + p.E2eExtension = valResponse + + // reverse path + rpath, ok := p.Path.(snet.RawPath) + if !ok { + return serrors.New("unexpected path", "type", common.TypeOf(p.Path)) + } + replypather := snet.DefaultReplyPather{} + replyPath, err := replypather.ReplyPath(rpath) + if err != nil { + return serrors.WrapStr("creating reply path", err) + } + p.Path = replyPath + // Send pong + if err := conn.WriteTo(&p, &ov); err != nil { + return withTag(serrors.WrapStr("sending reply", err)) + } + fmt.Printf("Sent pong to", "client", p.Destination) + return nil +} + +type client struct { + network *snet.SCIONNetwork + conn *snet.Conn + rawConn snet.PacketConn + sdConn daemon.Connector + + errorPaths map[snet.PathFingerprint]struct{} +} + +func (c *client) run() int { + pair := fmt.Sprintf("%s -> %s", integration.Local.IA, remote.IA) + fmt.Printf("Starting", "pair", pair) + defer fmt.Printf("Finished", "pair", pair) + defer integration.Done(integration.Local.IA, remote.IA) + c.sdConn = integration.SDConn() + defer c.sdConn.Close() + c.network = &snet.SCIONNetwork{ + SCMPHandler: snet.DefaultSCMPHandler{ + RevocationHandler: daemon.RevHandler{Connector: c.sdConn}, + SCMPErrors: scmpErrorsCounter, + }, + PacketConnMetrics: scionPacketConnMetrics, + Topology: c.sdConn, + } + fmt.Printf("Send", "local", + fmt.Sprintf("%v,[%v] -> %v,[%v]", + integration.Local.IA, integration.Local.Host, + remote.IA, remote.Host)) + c.errorPaths = make(map[snet.PathFingerprint]struct{}) + return integration.AttemptRepeatedly("End2End", c.attemptRequest) +} + +// attemptRequest sends one ping packet and expect a pong. +// Returns true (which means "stop") *if both worked*. +func (c *client) attemptRequest(n int) bool { + timeoutCtx, cancel := context.WithTimeout(context.Background(), timeout.Duration) + defer cancel() + span, ctx := tracing.CtxWith(timeoutCtx, "attempt") + span.SetTag("attempt", n) + span.SetTag("src", integration.Local.IA) + span.SetTag("dst", remote.IA) + defer span.Finish() + logger := log.FromCtx(ctx) + + path, err := c.getRemote(ctx, n) + if err != nil { + logger.Error("Could not get remote", "err", err) + return false + } + span, ctx = tracing.StartSpanFromCtx(ctx, "attempt.ping") + defer span.Finish() + withTag := func(err error) error { + tracing.Error(span, err) + return err + } + + for i := 0; i < 10; i++ { + + // Send ping + close, err := c.fabridPing(ctx, n, path) + if err != nil { + logger.Error("Could not send packet", "err", withTag(err)) + return false + } + defer close() + // Receive FABRID pong + if err := c.fabridPong(ctx); err != nil { + logger.Error("Error receiving pong", "err", withTag(err)) + if path != nil { + c.errorPaths[snet.Fingerprint(path)] = struct{}{} + } + return false + } + } + return true +} + +func (c *client) fabridPing(ctx context.Context, n int, path snet.Path) (func(), error) { + rawPing, err := json.Marshal(Ping{ + Server: remote.IA, + Message: ping, + Trace: tracing.IDFromCtx(ctx), + }) + if err != nil { + return nil, serrors.WrapStr("packing ping", err) + } + log.FromCtx(ctx).Info("Dialing", "remote", remote) + c.rawConn, err = c.network.OpenRaw(ctx, integration.Local.Host) + if err != nil { + return nil, serrors.WrapStr("dialing conn", err) + } + if err := c.rawConn.SetWriteDeadline(getDeadline(ctx)); err != nil { + return nil, serrors.WrapStr("setting write deadline", err) + } + fmt.Printf("sending ping", "attempt", n, "remote", remote, "local", c.rawConn.LocalAddr()) + localAddr := c.rawConn.LocalAddr().(*net.UDPAddr) + hostIP, _ := netip.AddrFromSlice(remote.Host.IP) + dst := snet.SCIONAddress{IA: remote.IA, Host: addr.HostIP(hostIP)} + localHostIP, _ := netip.AddrFromSlice(integration.Local.Host.IP) + pkt := &snet.Packet{ + Bytes: make([]byte, common.SupportedMTU), + PacketInfo: snet.PacketInfo{ + Destination: dst, + Source: snet.SCIONAddress{ + IA: integration.Local.IA, + Host: addr.HostIP(localHostIP), + }, + Path: remote.Path, + Payload: snet.UDPPayload{ + SrcPort: uint16(localAddr.Port), + DstPort: uint16(remote.Host.Port), + Payload: []byte("ping"), + }, + }, + } + fmt.Printf("sending packet", "packet", pkt) + if err := c.rawConn.WriteTo(pkt, remote.NextHop); err != nil { + return nil, err + } + closer := func() { + if err := c.rawConn.Close(); err != nil { + log.Error("Unable to close connection", "err", err) + } + } + return closer, nil +} + +func (c *client) getRemote(ctx context.Context, n int) (snet.Path, error) { + if remote.IA.Equal(integration.Local.IA) { + remote.Path = snetpath.Empty{} + return nil, nil + } + span, ctx := tracing.StartSpanFromCtx(ctx, "attempt.get_remote") + defer span.Finish() + withTag := func(err error) error { + tracing.Error(span, err) + return err + } + + paths, err := c.sdConn.Paths(ctx, remote.IA, integration.Local.IA, + daemon.PathReqFlags{Refresh: n != 0}) + if err != nil { + return nil, withTag(serrors.WrapStr("requesting paths", err)) + } + // If all paths had an error, let's try them again. + if len(paths) <= len(c.errorPaths) { + c.errorPaths = make(map[snet.PathFingerprint]struct{}) + } + // Select first path that didn't error before. + var path snet.Path + for _, p := range paths { + if _, ok := c.errorPaths[snet.Fingerprint(p)]; ok { + continue + } + path = p + break + } + if path == nil { + return nil, withTag(serrors.New("no path found", + "candidates", len(paths), + "errors", len(c.errorPaths), + )) + } + // If the fabrid flag is set, try to create FABRID dataplane path. + if len(path.Metadata().FabridInfo) > 0 { + // Check if fabrid info is available, otherwise the source + // AS does not support fabrid + + scionPath, ok := path.Dataplane().(snetpath.SCION) + if !ok { + return nil, serrors.New("provided path must be of type scion") + } + fabridConfig := &snetpath.FabridConfig{ + LocalIA: integration.Local.IA, + LocalAddr: integration.Local.Host.IP.String(), + DestinationIA: remote.IA, + DestinationAddr: remote.Host.IP.String(), + } + fabridConfig.ValidationHandler = func(ps *common2.PathState, + option *extension.FabridControlOption, b bool) error { + log.Debug("Validation handler", "pathState", ps, "success", b) + if !b { + return serrors.New("Failed validation") + } + return nil + } + hops := path.Metadata().Hops() + fmt.Printf("Fabrid path", "path", path, "hops", hops) + // Use ZERO policy for all hops with fabrid, to just do path validation + policies := make([]*libfabrid.PolicyID, len(hops)) + zeroPol := libfabrid.PolicyID(0) + for i, hop := range hops { + if hop.FabridEnabled { + policies[i] = &zeroPol + } + } + fabridPath, err := snetpath.NewFABRIDDataplanePath(scionPath, hops, + policies, fabridConfig, 125) + if err != nil { + return nil, serrors.New("Error creating FABRID path", "err", err) + } + remote.Path = fabridPath + fabridPath.RegisterDRKeyFetcher(c.sdConn.FabridKeys) + + } else { + fmt.Printf("FABRID flag was set for client in non-FABRID AS. Proceeding without FABRID.") + remote.Path = path.Dataplane() + } + remote.NextHop = path.UnderlayNextHop() + return path, nil +} + +func (c *client) pong(ctx context.Context) error { + if err := c.conn.SetReadDeadline(getDeadline(ctx)); err != nil { + return serrors.WrapStr("setting read deadline", err) + } + rawPld := make([]byte, common.MaxMTU) + n, serverAddr, err := readFrom(c.conn, rawPld) + if err != nil { + return serrors.WrapStr("reading packet", err) + } + + var pld Pong + if err := json.Unmarshal(rawPld[:n], &pld); err != nil { + return serrors.WrapStr("unpacking pong", err, "data", string(rawPld)) + } + + expected := Pong{ + Client: integration.Local.IA, + Server: remote.IA, + Message: pong, + } + if pld.Client != expected.Client || pld.Server != expected.Server || pld.Message != pong { + return serrors.New("unexpected contents received", "data", pld, "expected", expected) + } + fmt.Printf("Received pong", "server", serverAddr) + return nil +} + +func (c *client) fabridPong(ctx context.Context) error { + + if err := c.rawConn.SetReadDeadline(getDeadline(ctx)); err != nil { + return serrors.WrapStr("setting read deadline", err) + } + var p snet.Packet + var ov net.UDPAddr + err := readFromFabrid(c.rawConn, &p, &ov) + if err != nil { + return serrors.WrapStr("reading packet", err) + } + if p.Source.IA != integration.Local.IA { + // Check extensions for relevant options + var controlOptions []*extension.FabridControlOption + + if p.E2eExtension != nil { + + for _, opt := range p.E2eExtension.Options { + switch opt.OptType { + case slayers.OptTypeFabridControl: + controlOption, err := extension.ParseFabridControlOption(opt) + if err != nil { + return err + } + controlOptions = append(controlOptions, controlOption) + log.Debug("Parsed control option", "option", controlOption) + } + } + } + switch s := remote.Path.(type) { + case *snetpath.FABRID: + for _, option := range controlOptions { + err := s.HandleFabridControlOption(option, nil) + if err != nil { + return err + } + } + + default: + return serrors.New("unsupported path type") + } + } + + udp, ok := p.Payload.(snet.UDPPayload) + if !ok { + return serrors.New("unexpected payload received", + "source", p.Source, + "destination", p.Destination, + "type", common.TypeOf(p.Payload), + ) + } + var pld Pong + if err := json.Unmarshal(udp.Payload, &pld); err != nil { + return serrors.WrapStr("unpacking pong", err, "data", string(udp.Payload)) + } + + expected := Pong{ + Client: integration.Local.IA, + Server: remote.IA, + Message: pong, + } + if pld.Client != expected.Client || pld.Server != expected.Server || pld.Message != pong { + return serrors.New("unexpected contents received", "data", pld, "expected", expected) + } + fmt.Printf("Received pong", "server", ov) + return nil +} + +func getDeadline(ctx context.Context) time.Time { + dl, ok := ctx.Deadline() + if !ok { + integration.LogFatal("No deadline in context") + } + return dl +} + +func readFrom(conn *snet.Conn, pld []byte) (int, net.Addr, error) { + n, remoteAddr, err := conn.ReadFrom(pld) + // Attach more context to error + var opErr *snet.OpError + if !(errors.As(err, &opErr) && opErr.RevInfo() != nil) { + return n, remoteAddr, err + } + return n, remoteAddr, serrors.WithCtx(err, + "isd_as", opErr.RevInfo().IA(), + "interface", opErr.RevInfo().IfID, + ) +} + +func readFromFabrid(conn snet.PacketConn, pkt *snet.Packet, ov *net.UDPAddr) error { + err := conn.ReadFrom(pkt, ov) + // Attach more context to error + var opErr *snet.OpError + if !(errors.As(err, &opErr) && opErr.RevInfo() != nil) { + return err + } + return serrors.WithCtx(err, + "isd_as", opErr.RevInfo().IA(), + "interface", opErr.RevInfo().IfID, + ) +} diff --git a/demo/fabrid/test.py b/demo/fabrid/test.py new file mode 100644 index 0000000000..5ec082ce45 --- /dev/null +++ b/demo/fabrid/test.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +# Copyright 2024 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. + +import logging +import os +import random +import re +import time +import yaml + +from plumbum import local +from plumbum.path import LocalPath + +from acceptance.common import base, scion +from tools.topology.scion_addr import ISD_AS + +logger = logging.getLogger(__name__) + + +class Test(base.TestTopogen): + def _init_as_list(self): + # load list of ASes (generated by topogen scripts) + as_list = self.artifacts / "gen/as_list.yml" + self.isd_ases = scion.ASList.load(as_list).all + + # pick two "random" ASes (can be the same) for our "server" and "client" side key derivation + # demonstration. + # Note: fix random seed between invocations (prepare and run stages stages can be run + # separately) + random.seed(os.path.getctime(as_list)) + self.server_isd_as, self.client_isd_as = random.choices(self.isd_ases, k=2) + + def _run(self): + time.sleep(10) # wait until CSes are all up and running + + self._init_as_list() + + # install demo binary in tester containers: + fabrid_demo = local["realpath"](self.get_executable("fabrid-demo").executable).strip() + for ia in {self.server_isd_as, self.client_isd_as}: + self.dc("cp", fabrid_demo, "endhost_%s" % ia.file_fmt() + ":/bin/") + + # Define DRKey protocol identifiers and derivation typ for test + for test in [ + {"protocol": "1", "fetch_sv": "--fetch-sv"}, # SCMP based on protocol specific SV + {"protocol": "1", "fetch_sv": ""}, # SCMP based on generic key derivation + {"protocol": "7", "fetch_sv": ""}, # Generic "niche" protocol + ]: + # Determine server and client addresses for test. + # Because communication to the control services does not happen + # directly from the respective end hosts but via daemon processes on + # both sides, the IPs of the corresponding daemon hosts are used for + # this purpose. See also function _endhost_ip for more details. + server_ip = self._endhost_ip(self.server_isd_as) + client_ip = self._endhost_ip(self.client_isd_as) + server_addr = "%s,%s" % (self.server_isd_as, server_ip) + client_addr = "%s,%s" % (self.client_isd_as, client_ip) + + # Demonstrate deriving key (fast) on server side + rs = self.dc.execute("endhost_%s" % self.server_isd_as.file_fmt(), + "fabrid-demo", "--server", + "--protocol", test["protocol"], test["fetch_sv"], + "--server-addr", server_addr, "--client-addr", client_addr) + print(rs) + + # Demonstrate obtaining key (slow) on client side + rc = self.dc.execute("endhost_%s" % self.client_isd_as.file_fmt(), + "fabrid-demo", "--protocol", test["protocol"], + "--server-addr", server_addr, "--client-addr", client_addr) + print(rc) + + # Extract printed keys from output and verify that the keys match + key_regex = re.compile( + r"^(?:Client|Server):\s*host key\s*=\s*([a-f0-9]+)", re.MULTILINE) + server_key_match = key_regex.search(rs) + if server_key_match is None: + raise AssertionError("Key not found in server output") + server_key = server_key_match.group(1) + client_key_match = key_regex.search(rc) + if client_key_match is None: + raise AssertionError("Key not found in client output") + client_key = client_key_match.group(1) + if server_key != client_key: + raise AssertionError("Key derived by server does not match key derived by client!", + server_key, client_key) + + def _endhost_ip(self, isd_as: ISD_AS) -> str: + """ Determine the IP used for the end host (client or server) in the given ISD-AS """ + # The address must be the daemon IP (as it makes requests to the control + # service on behalf of the end host application). + return self._container_ip("endhost_%s" % isd_as.file_fmt()) + + def _container_ip(self, container: str) -> str: + """ Determine the IP of the container """ + dc_config = yaml.safe_load(self.dc.compose_file.read()) + networks = dc_config["services"][container]["networks"] + addresses = next(iter(networks.values())) + return next(iter(addresses.values())) + + def _conf_dir(self, isd_as: ISD_AS) -> LocalPath: + """ Returns the path of the configuration directory for the given ISD-AS """ + return self.artifacts / "gen" / ("AS" + isd_as.as_file_fmt()) + + +if __name__ == '__main__': + base.main(Test) diff --git a/pkg/experimental/fabrid/common/BUILD.bazel b/pkg/experimental/fabrid/common/BUILD.bazel new file mode 100644 index 0000000000..3bb103a82f --- /dev/null +++ b/pkg/experimental/fabrid/common/BUILD.bazel @@ -0,0 +1,13 @@ +load("//tools/lint:go.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["fabrid.go"], + importpath = "github.com/scionproto/scion/pkg/experimental/fabrid/common", + visibility = ["//visibility:public"], + deps = [ + "//pkg/log:go_default_library", + "//pkg/private/serrors:go_default_library", + "//pkg/slayers/extension:go_default_library", + ], +) diff --git a/pkg/experimental/fabrid/common/fabrid.go b/pkg/experimental/fabrid/common/fabrid.go new file mode 100644 index 0000000000..e65b4c9e50 --- /dev/null +++ b/pkg/experimental/fabrid/common/fabrid.go @@ -0,0 +1,97 @@ +// Copyright 2021 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 common + +import ( + "github.com/scionproto/scion/pkg/log" + "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/pkg/slayers/extension" +) + +// Testing options for failing validation +const CLIENT_FLAKINESS = 0 +const SERVER_FLAKINESS = 0 + +type Statistics struct { + TotalPackets uint32 + InvalidPackets uint32 +} + +type validationIdentifier struct { + timestamp uint32 + packetId uint32 +} + +type PathState struct { + ValidationRatio uint8 + UpdateValRatio bool + RequestStatistics bool + Stats Statistics + expectedValResponses map[validationIdentifier]uint32 +} + +func NewFabridPathState(valRatio uint8) *PathState { + state := &PathState{ + ValidationRatio: valRatio, + UpdateValRatio: true, + RequestStatistics: false, + expectedValResponses: make(map[validationIdentifier]uint32), + } + return state +} + +func (ps *PathState) StoreValidationResponse(validator uint32, + timestamp uint32, packetID uint32) error { + valIdent := validationIdentifier{ + timestamp: timestamp, + packetId: packetID, + } + _, found := ps.expectedValResponses[valIdent] + if found { + return serrors.New("Validation response already stored", "validationIdentifier", valIdent) + } + log.Debug("Storing validation response", "packetID", packetID, "timestamp", timestamp) + ps.expectedValResponses[valIdent] = validator + return nil +} + +func (ps *PathState) CheckValidationResponse(fco *extension.FabridControlOption) error { + timestamp, err := fco.Timestamp() + if err != nil { + return err + } + packetID, err := fco.PacketID() + if err != nil { + return err + } + validatorReply, err := fco.PathValidatorReply() + if err != nil { + return err + } + valIdent := validationIdentifier{ + timestamp: timestamp, + packetId: packetID, + } + log.Debug("Checking validation response", "timestamp", timestamp, "packetID", packetID) + validatorStored, found := ps.expectedValResponses[valIdent] + if !found { + return serrors.New("Unknown validation response", "validationIdentifier", valIdent) + } + if validatorStored != validatorReply { + return serrors.New("Wrong path validation response", "validationIdentifier", valIdent, + "expected", validatorStored, "actual", validatorReply) + } + return nil +} diff --git a/pkg/experimental/fabrid/crypto/fabrid_crypto.go b/pkg/experimental/fabrid/crypto/fabrid_crypto.go index 6428e9b7ad..4618ea338b 100644 --- a/pkg/experimental/fabrid/crypto/fabrid_crypto.go +++ b/pkg/experimental/fabrid/crypto/fabrid_crypto.go @@ -152,13 +152,14 @@ func EncryptPolicyID(f fabrid.PolicyID, id *ext.IdentifierOption, } // VerifyPathValidator recomputes the path validator from the updated HVFs and compares it -// with the path validator in the packet. Returns the secret 5th byte of the computed validator. +// with the path validator in the packet. Returns validation number and reply for path validation. // `tmpBuffer` requires at least (numHops*3 rounded up to next multiple of 16) + 16 bytes -func VerifyPathValidator(f *ext.FabridOption, tmpBuffer []byte, pathKey []byte) (uint8, error) { +func VerifyPathValidator(f *ext.FabridOption, tmpBuffer []byte, + pathKey []byte) (uint8, uint32, bool, error) { inputLength := 3 * len(f.HopfieldMetadata) requiredBufferLength := 16 + (inputLength+15)&^15 if len(tmpBuffer) < requiredBufferLength { - return 0, serrors.New("tmpBuffer length is invalid", "expected", + return 0, 0, false, serrors.New("tmpBuffer length is invalid", "expected", requiredBufferLength, "actual", len(tmpBuffer)) } @@ -167,22 +168,23 @@ func VerifyPathValidator(f *ext.FabridOption, tmpBuffer []byte, pathKey []byte) } err := macBlock(pathKey, tmpBuffer[:16], tmpBuffer[16:16+inputLength], tmpBuffer[16:]) if err != nil { - return 0, err + return 0, 0, false, err } validationNumber := tmpBuffer[20] + validationReply := binary.BigEndian.Uint32(tmpBuffer[21:25]) if !bytes.Equal(tmpBuffer[16:20], f.PathValidator[:]) { - return validationNumber, serrors.New("Path validator is not valid", + return validationNumber, validationReply, false, serrors.New("Path validator is not valid", "validator", base64.StdEncoding.EncodeToString(f.PathValidator[:]), "computed", base64.StdEncoding.EncodeToString(tmpBuffer[16:20])) } - return validationNumber, nil + return validationNumber, validationReply, true, nil } // InitValidators sets all HVFs of the FABRID option and computes the // path validator. func InitValidators(f *ext.FabridOption, id *ext.IdentifierOption, s *slayers.SCION, tmpBuffer []byte, pathKey *drkey.FabridKey, asHostKeys map[addr.IA]*drkey.FabridKey, - asAsKeys map[addr.IA]*drkey.FabridKey, hops []snet.HopInterface) error { + asAsKeys map[addr.IA]*drkey.FabridKey, hops []snet.HopInterface) (uint8, uint32, error) { outBuffer := make([]byte, 16) var pathValInputLength int @@ -201,14 +203,14 @@ func InitValidators(f *ext.FabridOption, id *ext.IdentifierOption, s *slayers.SC key, found = asHostKeys[hops[i].IA] } if !found { - return serrors.New("InitValidators expected AS to AS key but was not in"+ + return 0, 0, serrors.New("InitValidators expected AS to AS key but was not in"+ " dictionary", "AS", hops[i].IA) } err := computeFabridHVF(meta, id, s, tmpBuffer, outBuffer, key.Key[:], uint16(hops[i].IgIf), uint16(hops[i].EgIf)) if err != nil { - return err + return 0, 0, err } outBuffer[0] &= 0x3f // ignore first two (left) bits outBuffer[3] &= 0x3f // ignore first two (left) bits @@ -222,10 +224,76 @@ func InitValidators(f *ext.FabridOption, id *ext.IdentifierOption, s *slayers.SC err := macBlock(pathKey.Key[:], tmpBuffer[:16], pathValBuffer[:pathValInputLength], pathValBuffer) if err != nil { - return err + return 0, 0, err } copy(f.PathValidator[:4], pathValBuffer[:4]) } + return pathValBuffer[4], binary.BigEndian.Uint32(pathValBuffer[5:9]), nil +} + +func computeFabridControlValidator(fc *ext.FabridControlOption, id *ext.IdentifierOption, + resultBuffer []byte, pathKey []byte) error { + dataLen := ext.FabridControlOptionDataLen(fc.Type) + var fcMacInputLength int + switch fc.Type { + case ext.ValidationConfig, ext.StatisticsRequest: + fcMacInputLength = 1 + 8 + dataLen + case ext.ValidationConfigAck, ext.ValidationResponse, ext.StatisticsResponse: + fcMacInputLength = 1 + dataLen + } + macInputBuf := make([]byte, (fcMacInputLength+15)&^15) // Next multiple of 16 for macBlock() + tmpBuf := make([]byte, 16) + macInputBuf[0] = uint8(fc.Type) + copy(macInputBuf[1:1+dataLen], fc.Data) + switch fc.Type { + case ext.ValidationConfig, ext.StatisticsRequest: + binary.BigEndian.PutUint32(macInputBuf[1+dataLen:5+dataLen], id.GetRelativeTimestamp()) + binary.BigEndian.PutUint32(macInputBuf[5+dataLen:9+dataLen], id.PacketID) + } + + err := macBlock(pathKey, tmpBuf, macInputBuf[:fcMacInputLength], resultBuffer) + //log.Debug("Computing FABRID control validator", + // "key", base64.StdEncoding.EncodeToString(PathKey), + // "input", base64.StdEncoding.EncodeToString(macInputBuf[:fcMacInputLength]), + // "output", base64.StdEncoding.EncodeToString(resultBuffer[:4])) + if err != nil { + return err + } + return nil +} + +func InitFabridControlValidator(fc *ext.FabridControlOption, + id *ext.IdentifierOption, pathKey []byte) error { + outBuffer := make([]byte, 16) + err := computeFabridControlValidator(fc, id, outBuffer, pathKey) + if err != nil { + return err + } + outBuffer[0] &= 0xF // ignore first four bits + //log.Debug("Computing FABRID control validator", + // "key", base64.StdEncoding.EncodeToString(pathKey), + // "controlOption", fc, + // "computedValidator", base64.StdEncoding.EncodeToString(outBuffer[:4])) + copy(fc.Auth[:4], outBuffer[:4]) + return nil +} + +func VerifyFabridControlValidator(fc *ext.FabridControlOption, + id *ext.IdentifierOption, pathKey []byte) error { + computedValidator := make([]byte, 16) + err := computeFabridControlValidator(fc, id, computedValidator, pathKey) + if err != nil { + return err + } + computedValidator[0] &= 0xF // ignore first four bits + //log.Debug("Verifying FABRID control validator", + // "key", base64.StdEncoding.EncodeToString(pathKey), + // "controlOption", fc, + // "pktValidator", base64.StdEncoding.EncodeToString(fc.Auth[:]), + // "computedValidator", base64.StdEncoding.EncodeToString(computedValidator[:4])) + if !bytes.Equal(computedValidator[:4], fc.Auth[:]) { + return serrors.New("Fabrid control validator is not valid") + } return nil } diff --git a/pkg/experimental/fabrid/crypto/fabrid_crypto_test.go b/pkg/experimental/fabrid/crypto/fabrid_crypto_test.go index 9722efd4a3..a9c2fa01d1 100644 --- a/pkg/experimental/fabrid/crypto/fabrid_crypto_test.go +++ b/pkg/experimental/fabrid/crypto/fabrid_crypto_test.go @@ -115,7 +115,7 @@ func TestFailedValidation(t *testing.T) { } } - err := crypto.InitValidators(f, id, s, tmpBuffer, pathKey, asHostKeys, + _, _, err := crypto.InitValidators(f, id, s, tmpBuffer, pathKey, asHostKeys, asAsKeys, hops) assert.NoError(t, err) @@ -135,12 +135,12 @@ func TestFailedValidation(t *testing.T) { assert.NoError(t, err) } } - _, err = crypto.VerifyPathValidator(f, tmpBuffer, pathKey.Key[:]) + _, _, _, err = crypto.VerifyPathValidator(f, tmpBuffer, pathKey.Key[:]) assert.NoError(t, err) // until now we are in the success case. But now we modify a HVF to simulate // adversarial actions and make sure that the path validator fails f.HopfieldMetadata[0].HopValidationField = [3]byte{0, 0, 0} - _, err = crypto.VerifyPathValidator(f, tmpBuffer, pathKey.Key[:]) + _, _, _, err = crypto.VerifyPathValidator(f, tmpBuffer, pathKey.Key[:]) assert.ErrorContains(t, err, "Path validator is not valid") }, }, @@ -250,7 +250,7 @@ func TestSuccessfullValidators(t *testing.T) { } } - err := crypto.InitValidators(f, id, s, tmpBuffer, pathKey, asHostKeys, + _, _, err := crypto.InitValidators(f, id, s, tmpBuffer, pathKey, asHostKeys, asAsKeys, hops) assert.NoError(t, err) @@ -271,7 +271,7 @@ func TestSuccessfullValidators(t *testing.T) { assert.NoError(t, err) } } - _, err = crypto.VerifyPathValidator(f, tmpBuffer, pathKey.Key[:]) + _, _, _, err = crypto.VerifyPathValidator(f, tmpBuffer, pathKey.Key[:]) assert.NoError(t, err) } }) diff --git a/pkg/experimental/fabrid/defs.go b/pkg/experimental/fabrid/defs.go index 23a6ca3269..c08e06e96a 100644 --- a/pkg/experimental/fabrid/defs.go +++ b/pkg/experimental/fabrid/defs.go @@ -14,7 +14,9 @@ package fabrid -import "fmt" +import ( + "fmt" +) type PolicyID uint8 diff --git a/pkg/experimental/fabrid/graphutils/BUILD.bazel b/pkg/experimental/fabrid/graphutils/BUILD.bazel new file mode 100644 index 0000000000..dd0add83ca --- /dev/null +++ b/pkg/experimental/fabrid/graphutils/BUILD.bazel @@ -0,0 +1,14 @@ +load("//tools/lint:go.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["maps.go"], + importpath = "github.com/scionproto/scion/pkg/experimental/fabrid/graphutils", + visibility = ["//visibility:public"], + deps = [ + "//pkg/addr:go_default_library", + "//pkg/experimental/fabrid:go_default_library", + "//pkg/segment/extensions/fabrid:go_default_library", + "//pkg/snet:go_default_library", + ], +) diff --git a/private/path/combinator/fabrid_accumulator.go b/pkg/experimental/fabrid/graphutils/maps.go similarity index 97% rename from private/path/combinator/fabrid_accumulator.go rename to pkg/experimental/fabrid/graphutils/maps.go index 1040b3b65d..131f251e9f 100644 --- a/private/path/combinator/fabrid_accumulator.go +++ b/pkg/experimental/fabrid/graphutils/maps.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package combinator +package graphutils import ( "time" @@ -34,7 +34,7 @@ type FabridMapEntry struct { Digest []byte } -func collectFabridPolicies(ifaces []snet.PathInterface, +func CollectFabridPolicies(ifaces []snet.PathInterface, maps map[addr.IA]FabridMapEntry) []snet.FabridInfo { switch { diff --git a/pkg/experimental/fabrid/server/BUILD.bazel b/pkg/experimental/fabrid/server/BUILD.bazel new file mode 100644 index 0000000000..e89fc0c970 --- /dev/null +++ b/pkg/experimental/fabrid/server/BUILD.bazel @@ -0,0 +1,20 @@ +load("//tools/lint:go.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["server.go"], + importpath = "github.com/scionproto/scion/pkg/experimental/fabrid/server", + visibility = ["//visibility:public"], + deps = [ + "//pkg/addr:go_default_library", + "//pkg/daemon:go_default_library", + "//pkg/drkey:go_default_library", + "//pkg/experimental/fabrid/common:go_default_library", + "//pkg/experimental/fabrid/crypto:go_default_library", + "//pkg/log:go_default_library", + "//pkg/private/serrors:go_default_library", + "//pkg/slayers:go_default_library", + "//pkg/slayers/extension:go_default_library", + "//pkg/snet:go_default_library", + ], +) diff --git a/pkg/experimental/fabrid/server/server.go b/pkg/experimental/fabrid/server/server.go new file mode 100644 index 0000000000..25f7caeca7 --- /dev/null +++ b/pkg/experimental/fabrid/server/server.go @@ -0,0 +1,222 @@ +// Copyright 2021 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 server + +import ( + "context" + crand "crypto/rand" + "time" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon" + "github.com/scionproto/scion/pkg/drkey" + "github.com/scionproto/scion/pkg/experimental/fabrid/common" + "github.com/scionproto/scion/pkg/experimental/fabrid/crypto" + "github.com/scionproto/scion/pkg/log" + "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/pkg/slayers" + "github.com/scionproto/scion/pkg/slayers/extension" + "github.com/scionproto/scion/pkg/snet" +) + +type ClientConnection struct { + Source snet.SCIONAddress + ValidationRatio uint8 + Stats common.Statistics + fabridControlBuffer []byte + tmpBuffer []byte + pathKey drkey.Key +} + +type Server struct { + Local snet.UDPAddr + sdConn daemon.Connector + Connections map[string]*ClientConnection + ASKeyCache map[addr.IA]drkey.HostASKey + MaxValidationRatio uint8 + ValidationHandler func(*ClientConnection, *extension.IdentifierOption, bool) error +} + +func NewFabridServer(local *snet.UDPAddr, sdConn daemon.Connector) *Server { + server := &Server{ + Local: *local, + sdConn: sdConn, + Connections: make(map[string]*ClientConnection), + ASKeyCache: make(map[addr.IA]drkey.HostASKey), + ValidationHandler: func(_ *ClientConnection, _ *extension.IdentifierOption, _ bool) error { + return nil + }, + MaxValidationRatio: 255, + } + return server +} + +func (s *Server) FetchHostHostKey(dstHost snet.SCIONAddress, + validity time.Time) (drkey.Key, error) { + meta := drkey.HostHostMeta{ + Validity: validity, + SrcIA: s.Local.IA, + SrcHost: s.Local.Host.IP.String(), + DstIA: dstHost.IA, + DstHost: dstHost.Host.IP().String(), + ProtoId: drkey.FABRID, + } + hostHostKey, err := s.sdConn.DRKeyGetHostHostKey(context.Background(), meta) + if err != nil { + return drkey.Key{}, serrors.WrapStr("getting host key", err) + } + return hostHostKey.Key, nil +} + +func (s *Server) HandleFabridPacket(remote snet.SCIONAddress, fabridOption *extension.FabridOption, + identifierOption *extension.IdentifierOption, + controlOptions []*extension.FabridControlOption) (*slayers.EndToEndExtn, error) { + client, found := s.Connections[remote.String()] + if !found { + pathKey, err := s.FetchHostHostKey(remote, identifierOption.Timestamp) + if err != nil { + return nil, err + } + client = &ClientConnection{ + Source: remote, + ValidationRatio: 255, + Stats: common.Statistics{}, + fabridControlBuffer: make([]byte, 28*3), + tmpBuffer: make([]byte, 192), + pathKey: pathKey, + } + s.Connections[remote.String()] = client + log.Info("Opened new connection", "remote", remote.String()) + } + + client.Stats.TotalPackets++ + validationNumber, validationReply, success, err := crypto.VerifyPathValidator(fabridOption, + client.tmpBuffer, client.pathKey[:]) + if err != nil { + return nil, err + } + err = s.ValidationHandler(client, identifierOption, success) + if err != nil { + return nil, err + } + + var replyOpts []*extension.FabridControlOption + for _, controlOption := range controlOptions { + err = crypto.VerifyFabridControlValidator(controlOption, identifierOption, + client.pathKey[:]) + if err != nil { + return nil, err + } + controlReplyOpt := &extension.FabridControlOption{} + + switch controlOption.Type { + case extension.ValidationConfig: + requestedRatio, err := controlOption.ValidationRatio() + if err != nil { + return nil, err + } + if requestedRatio > s.MaxValidationRatio { + log.Debug("FABRID control: requested ratio too large", "requested", requestedRatio, + "max", s.MaxValidationRatio) + requestedRatio = s.MaxValidationRatio + } + log.Debug("FABRID control: updated validation ratio", "new", requestedRatio, + "old", client.ValidationRatio) + client.ValidationRatio = requestedRatio + + // Prepare ACK + controlReplyOpt.Type = extension.ValidationConfigAck + controlReplyOpt.Data = make([]byte, 9) + err = controlReplyOpt.SetValidationRatio(client.ValidationRatio) + if err != nil { + return nil, err + } + case extension.StatisticsRequest: + log.Debug("FABRID control: statistics request") + // Prepare statistics reply + controlReplyOpt.Type = extension.StatisticsResponse + controlReplyOpt.Data = make([]byte, 24) + err := controlReplyOpt.SetStatistics(client.Stats.TotalPackets, + client.Stats.InvalidPackets) + if err != nil { + return nil, err + } + } + ts, _ := controlOption.Timestamp() + err = controlReplyOpt.SetTimestamp(ts) + if err != nil { + return nil, err + } + packetID, _ := controlOption.PacketID() + err = controlReplyOpt.SetPacketID(packetID) + if err != nil { + return nil, err + } + replyOpts = append(replyOpts, controlReplyOpt) + } + if validationNumber < client.ValidationRatio { + log.Debug("Send validation response", "packetID", identifierOption.PacketID, + "timestamp", identifierOption.GetRelativeTimestamp()) + validationReplyOpt := extension.NewFabridControlOption(extension.ValidationResponse) + err = validationReplyOpt.SetTimestamp(identifierOption.GetRelativeTimestamp()) + if err != nil { + return nil, err + } + err = validationReplyOpt.SetPacketID(identifierOption.PacketID) + if err != nil { + return nil, err + } + replyOpts = append(replyOpts, validationReplyOpt) + // TODO: Remove testing code + randInt := make([]byte, 1) + _, err2 := crand.Read(randInt) + if err2 != nil { + return nil, err2 + } + if randInt[0] < common.SERVER_FLAKINESS { + validationReply ^= 0xFFFFFFFF + } + err = validationReplyOpt.SetPathValidatorReply(validationReply) + if err != nil { + return nil, err + } + } + + if len(replyOpts) > 0 { + e2eExt := &slayers.EndToEndExtn{} + for i, replyOpt := range replyOpts { + err = crypto.InitFabridControlValidator(replyOpt, identifierOption, client.pathKey[:]) + if err != nil { + return nil, err + } + buffer := client.fabridControlBuffer[i*28 : (i+1)*28] + err = replyOpt.SerializeTo(buffer) + if err != nil { + return nil, err + } + fabridReplyOptionLength := extension.BaseFabridControlLen + + extension.FabridControlOptionDataLen(replyOpt.Type) + e2eExt.Options = append(e2eExt.Options, + &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: buffer, + OptDataLen: uint8(fabridReplyOptionLength), + ActualLength: fabridReplyOptionLength, + }) + } + return e2eExt, nil + } + return nil, nil +} diff --git a/pkg/slayers/extension/BUILD.bazel b/pkg/slayers/extension/BUILD.bazel index 8c0cb605be..c5d96b55c0 100644 --- a/pkg/slayers/extension/BUILD.bazel +++ b/pkg/slayers/extension/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "fabrid.go", + "fabrid_control.go", "identifier.go", ], importpath = "github.com/scionproto/scion/pkg/slayers/extension", @@ -17,6 +18,7 @@ go_library( go_test( name = "go_default_test", srcs = [ + "fabrid_control_test.go", "fabrid_test.go", "identifier_test.go", ], diff --git a/pkg/slayers/extension/fabrid_control.go b/pkg/slayers/extension/fabrid_control.go new file mode 100644 index 0000000000..8c739f2587 --- /dev/null +++ b/pkg/slayers/extension/fabrid_control.go @@ -0,0 +1,246 @@ +// Copyright 2023 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. + +// The FABRID control option format is as follows: +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | NextHdr | ExtLen | OptType = 5 | OptLen | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Type | E2E Mac | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ... | +// | [Content] | +// | ... | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +package extension + +import ( + "encoding/binary" + + "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/pkg/slayers" +) + +const BaseFabridControlLen int = 4 + +type FabridControlOptionType uint8 + +// Definition of FABRID control option type constants +const ( + ValidationConfig FabridControlOptionType = iota + ValidationConfigAck + ValidationResponse + StatisticsRequest + StatisticsResponse +) + +type FabridControlOption struct { + // Type of the control message + Type FabridControlOptionType + // E2E MAC of the option content + Auth [4]byte + Data []byte +} + +func NewFabridControlOption(t FabridControlOptionType) *FabridControlOption { + return &FabridControlOption{ + Type: t, + Auth: [4]byte{}, + Data: make([]byte, BaseFabridControlLen+FabridControlOptionDataLen(t)), + } +} + +// Validates the length of FabridControlOption. Requires Type to be set +func (fc *FabridControlOption) validate(b []byte) error { + if fc == nil { + return serrors.New("Fabrid control option must not be nil") + } + if fc.Type > StatisticsResponse { + return serrors.New("Invalid fabrid control option type") + } + if len(b) < BaseFabridControlLen+FabridControlOptionDataLen(fc.Type) { + return serrors.New("Raw Fabrid control option too short", "is", len(b), + "expected", BaseFabridControlLen+FabridControlOptionDataLen(fc.Type)) + } + return nil +} + +func (fc *FabridControlOption) Decode(b []byte) error { + fc.Type = FabridControlOptionType(b[0] >> 4) + if err := fc.validate(b); err != nil { + return err + } + copy(fc.Auth[:], b[0:4]) + fc.Auth[0] &= 0xF + + fc.Data = make([]byte, FabridControlOptionDataLen(fc.Type)) + copy(fc.Data[:], b[4:4+FabridControlOptionDataLen(fc.Type)]) + return nil +} + +func (fc *FabridControlOption) SerializeTo(b []byte) error { + if fc == nil { + return serrors.New("Fabrid control option must not be nil") + } + if len(b) < BaseFabridControlLen+FabridControlOptionDataLen(fc.Type) { + return serrors.New("Buffer too short", "is", len(b), + "expected", BaseFabridControlLen+FabridControlOptionDataLen(fc.Type)) + } + // Set authenticator before type, so it is not overwritten + copy(b[0:4], fc.Auth[:]) + b[0] &= 0xF // clear the first 4 (left) bits + b[0] |= uint8(fc.Type) << 4 + if len(fc.Data) < FabridControlOptionDataLen(fc.Type) { + return serrors.New("Data too short", "is", len(fc.Data), + "expected", FabridControlOptionDataLen(fc.Type)) + } + + copy(b[4:], fc.Data[:FabridControlOptionDataLen(fc.Type)]) + return nil +} + +// Getter and Setter functions + +func (fc *FabridControlOption) Timestamp() (uint32, error) { + switch fc.Type { + case ValidationConfigAck, ValidationResponse, StatisticsResponse: + return binary.BigEndian.Uint32(fc.Data[:4]), nil + case ValidationConfig, StatisticsRequest: + return 0, serrors.New("Wrong option type", + "expected", string(ValidationConfigAck)+", "+string(ValidationResponse)+ + " or "+string(StatisticsResponse), "actual", fc.Type) + } + return 0, serrors.New("Invalid fabrid control option type", "type", fc.Type) +} + +func (fc *FabridControlOption) SetTimestamp(timestamp uint32) error { + switch fc.Type { + case ValidationConfigAck, ValidationResponse, StatisticsResponse: + binary.BigEndian.PutUint32(fc.Data[:4], timestamp) + return nil + case ValidationConfig, StatisticsRequest: + return serrors.New("Wrong option type", + "expected", string(ValidationConfigAck)+", "+string(ValidationResponse)+ + " or "+string(StatisticsResponse), "actual", fc.Type) + } + return serrors.New("Invalid fabrid control option type", "type", fc.Type) +} + +func (fc *FabridControlOption) PacketID() (uint32, error) { + switch fc.Type { + case ValidationConfigAck, ValidationResponse, StatisticsResponse: + return binary.BigEndian.Uint32(fc.Data[4:8]), nil + case ValidationConfig, StatisticsRequest: + return 0, serrors.New("Wrong option type", + "expected", string(ValidationConfigAck)+", "+string(ValidationResponse)+ + " or "+string(StatisticsResponse), "actual", fc.Type) + } + return 0, serrors.New("Invalid fabrid control option type", "type", fc.Type) +} + +func (fc *FabridControlOption) SetPacketID(packetID uint32) error { + switch fc.Type { + case ValidationConfigAck, ValidationResponse, StatisticsResponse: + binary.BigEndian.PutUint32(fc.Data[4:8], packetID) + return nil + case ValidationConfig, StatisticsRequest: + return serrors.New("Wrong option type", + "expected", string(ValidationConfigAck)+", "+string(ValidationResponse)+ + " or "+string(StatisticsResponse), "actual", fc.Type) + } + return serrors.New("Invalid fabrid control option type", "type", fc.Type) +} + +func (fc *FabridControlOption) ValidationRatio() (uint8, error) { + if fc.Type == ValidationConfig { + return fc.Data[0], nil + } else if fc.Type == ValidationConfigAck { + return fc.Data[8], nil + } + return 0, serrors.New("Wrong option type", + "expected", string(ValidationConfig)+" or "+string(ValidationConfigAck), "actual", fc.Type) +} + +func (fc *FabridControlOption) SetValidationRatio(valRatio uint8) error { + if fc.Type == ValidationConfig { + fc.Data[0] = valRatio + return nil + } else if fc.Type == ValidationConfigAck { + fc.Data[8] = valRatio + return nil + } + return serrors.New("Wrong option type", + "expected", string(ValidationConfig)+" or "+string(ValidationConfigAck), "actual", fc.Type) +} + +func (fc *FabridControlOption) PathValidatorReply() (uint32, error) { + if fc.Type == ValidationResponse { + return binary.BigEndian.Uint32(fc.Data[8:12]), nil + } + return 0, serrors.New("Wrong option type", "expected", ValidationResponse, "actual", fc.Type) +} + +func (fc *FabridControlOption) SetPathValidatorReply(pathValReply uint32) error { + if fc.Type == ValidationResponse { + binary.BigEndian.PutUint32(fc.Data[8:12], pathValReply) + return nil + } + return serrors.New("Wrong option type", "expected", ValidationResponse, "actual", fc.Type) +} + +func (fc *FabridControlOption) Statistics() (uint32, uint32, error) { + if fc.Type == StatisticsResponse { + return binary.BigEndian.Uint32(fc.Data[8:12]), binary.BigEndian.Uint32(fc.Data[12:16]), nil + } + return 0, 0, serrors.New("Wrong option type", "expected", StatisticsResponse, "actual", fc.Type) +} + +func (fc *FabridControlOption) SetStatistics(totalPackets uint32, invalidPackets uint32) error { + if fc.Type == StatisticsResponse { + binary.BigEndian.PutUint32(fc.Data[8:12], totalPackets) + binary.BigEndian.PutUint32(fc.Data[12:16], invalidPackets) + return nil + } + return serrors.New("Wrong option type", "expected", StatisticsResponse, "actual", fc.Type) +} + +func FabridControlOptionDataLen(controlOptionType FabridControlOptionType) int { + switch controlOptionType { + case ValidationConfig: + return 1 + case ValidationConfigAck: + return 9 + case ValidationResponse: + return 12 + case StatisticsRequest: + return 0 + case StatisticsResponse: + return 16 + default: + return 0 + } +} + +func ParseFabridControlOption(o *slayers.EndToEndOption) (*FabridControlOption, error) { + if o.OptType != slayers.OptTypeFabridControl { + return nil, + serrors.New("Wrong option type", "expected", slayers.OptTypeFabridControl, + "actual", o.OptType) + } + fc := &FabridControlOption{} + if err := fc.Decode(o.OptData); err != nil { + return nil, err + } + return fc, nil +} diff --git a/pkg/slayers/extension/fabrid_control_test.go b/pkg/slayers/extension/fabrid_control_test.go new file mode 100644 index 0000000000..396d691650 --- /dev/null +++ b/pkg/slayers/extension/fabrid_control_test.go @@ -0,0 +1,322 @@ +// Copyright 2023 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 extension_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/scionproto/scion/pkg/slayers" + "github.com/scionproto/scion/pkg/slayers/extension" +) + +func TestFabridControlDecode(t *testing.T) { + type test struct { + name string + o *slayers.EndToEndOption + validate func(*extension.FabridControlOption, error, *testing.T) + } + tests := []test{ + { + name: "Wrong option type", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeIdentifier, + OptData: make([]byte, 4), + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.Error(t, err) + }, + }, + { + name: "Wrong fabrid control option type", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: []byte{0x50}, + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.Error(t, err) + }, + }, + { + name: "Raw fabrid too short", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: make([]byte, 4), + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.Error(t, err) + }, + }, + { + name: "Raw fabrid parses with correct length", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: make([]byte, 5), + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.NoError(t, err) + }, + }, + { + name: "Parses fabrid validation config correctly", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: []byte{ + 0x0F, 0x22, 0x33, 0x44, + 0xaa, + }, + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, extension.ValidationConfig, fco.Type, "Wrong type") + assert.Equal(t, [4]byte{0x0F, 0x22, 0x33, 0x44}, fco.Auth, "Wrong auth") + assert.Equal(t, []byte{0xaa}, fco.Data, "Wrong data") + }, + }, + { + name: "Parses fabrid validation config ACK correctly", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: []byte{ + 0x1f, 0x22, 0x33, 0x44, + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0x11, + }, + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, extension.ValidationConfigAck, fco.Type, "Wrong type") + assert.Equal(t, [4]byte{0x0F, 0x22, 0x33, 0x44}, fco.Auth, "Wrong auth") + assert.Equal(t, []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0x11}, fco.Data, "Wrong data") + }, + }, + { + name: "Parses fabrid validation response correctly", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: []byte{ + 0x2f, 0x22, 0x33, 0x44, + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0xcc, 0xdd, 0xee, 0xff, + }, + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, extension.ValidationResponse, fco.Type, "Wrong type") + assert.Equal(t, [4]byte{0x0f, 0x22, 0x33, 0x44}, fco.Auth, "Wrong auth") + assert.Equal(t, []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0xcc, 0xdd, 0xee, 0xff}, + fco.Data, "Wrong path validator reply") + }, + }, + { + name: "Parses fabrid statistics request correctly", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: []byte{ + 0x3f, 0x22, 0x33, 0x44, + }, + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, extension.StatisticsRequest, fco.Type, "Wrong type") + assert.Equal(t, [4]byte{0x0f, 0x22, 0x33, 0x44}, fco.Auth, "Wrong auth") + assert.Empty(t, fco.Data, "Wrong data") + }, + }, + { + name: "Parses fabrid statistics response correctly", + o: &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: []byte{ + 0x4f, 0x22, 0x33, 0x44, + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0xaa, 0xbb, 0xcc, 0xdd, + 0x0a, 0x0b, 0x0c, 0x0d, + }, + }, + validate: func(fco *extension.FabridControlOption, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, extension.StatisticsResponse, fco.Type, "Wrong type") + assert.Equal(t, [4]byte{0x0f, 0x22, 0x33, 0x44}, fco.Auth, "Wrong auth") + assert.Equal(t, []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0xaa, 0xbb, 0xcc, 0xdd, + 0x0a, 0x0b, 0x0c, 0x0d, + }, fco.Data, "Wrong data") + }, + }, + } + + for _, tc := range tests { + func(tc test) { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + fc, err := extension.ParseFabridControlOption(tc.o) + tc.validate(fc, err, t) + }) + }(tc) + } +} + +func TestFabridControlSerialize(t *testing.T) { + type test struct { + name string + fc *extension.FabridControlOption + buffer []byte + validate func([]byte, error, *testing.T) + } + + tests := []test{ + { + name: "Fabrid control option is nil", + fc: nil, + validate: func(b []byte, err error, t *testing.T) { + assert.Error(t, err) + }, + }, + { + name: "Buffer too small", + fc: &extension.FabridControlOption{ + Type: extension.ValidationConfig, + Data: make([]byte, 1), + }, + buffer: make([]byte, 4), + validate: func(b []byte, err error, t *testing.T) { + assert.Error(t, err) + }, + }, + { + name: "Data buffer too small", + fc: &extension.FabridControlOption{ + Type: extension.ValidationConfig, + }, + buffer: make([]byte, 5), + validate: func(b []byte, err error, t *testing.T) { + assert.Error(t, err) + }, + }, + { + name: "Fabrid validation config serializes correctly", + fc: &extension.FabridControlOption{ + Type: extension.ValidationConfig, + Auth: [4]byte{0x07, 0xb2, 0xc3, 0xd4}, + Data: []byte{0x99}, + }, + buffer: make([]byte, 5), + validate: func(b []byte, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, []byte{0x07, 0xb2, 0xc3, 0xd4}, b[0:4], "Wrong type or Auth") + assert.Equal(t, []byte{0x99}, b[4:5], "Wrong Data") + }, + }, + { + name: "Fabrid validation configuration ACK serializes correctly", + fc: &extension.FabridControlOption{ + Type: extension.ValidationConfigAck, + Auth: [4]byte{0x0f, 0xb2, 0xc3, 0xd4}, + Data: []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0x11}, + }, + buffer: make([]byte, 13), + validate: func(b []byte, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, []byte{0x1f, 0xb2, 0xc3, 0xd4}, b[0:4], "Wrong type or Auth") + assert.Equal(t, []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0x11}, b[4:13], "Wrong Data") + }, + }, + { + name: "Fabrid validation response serializes correctly", + fc: &extension.FabridControlOption{ + Type: extension.ValidationResponse, + Auth: [4]byte{0x0f, 0xb2, 0xc3, 0xd4}, + Data: []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0x11, 0x00, 0x99, 0x88}, + }, + buffer: make([]byte, 16), + validate: func(b []byte, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, []byte{0x2f, 0xb2, 0xc3, 0xd4}, b[0:4], "Wrong type or Auth") + assert.Equal(t, []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0x11, 0x00, 0x99, 0x88, + }, b[4:16], "Wrong Data") + }, + }, + { + name: "Fabrid statistics request serializes correctly", + fc: &extension.FabridControlOption{ + Type: extension.StatisticsRequest, + Auth: [4]byte{0x0f, 0xb2, 0xc3, 0xd4}, + }, + buffer: make([]byte, 4), + validate: func(b []byte, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, []byte{0x3f, 0xb2, 0xc3, 0xd4}, b[0:4], "Wrong type or Auth") + }, + }, + { + name: "Fabrid statistics response serializes correctly", + fc: &extension.FabridControlOption{ + Type: extension.StatisticsResponse, + Auth: [4]byte{0x0f, 0xb2, 0xc3, 0xd4}, + Data: []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0x99, 0x88, 0x77, 0x66, + 0x55, 0x44, 0x33, 0x22}, + }, + buffer: make([]byte, 20), + validate: func(b []byte, err error, t *testing.T) { + assert.NoError(t, err) + assert.Equal(t, []byte{0x4f, 0xb2, 0xc3, 0xd4}, b[0:4], "Wrong type or Auth") + assert.Equal(t, []byte{ + 0x07, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0xaa, 0xbb, + 0x99, 0x88, 0x77, 0x66, + 0x55, 0x44, 0x33, 0x22, + }, b[4:20], "Wrong Data") + }, + }, + } + + for _, tc := range tests { + func(tc test) { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := tc.fc.SerializeTo(tc.buffer) + tc.validate(tc.buffer, err, t) + }) + }(tc) + } +} diff --git a/pkg/slayers/extn.go b/pkg/slayers/extn.go index fded8696a8..eb41a51bf2 100644 --- a/pkg/slayers/extn.go +++ b/pkg/slayers/extn.go @@ -36,6 +36,7 @@ const ( OptTypeAuthenticator OptTypeIdentifier OptTypeFabrid + OptTypeFabridControl ) type tlvOption struct { diff --git a/pkg/snet/path/BUILD.bazel b/pkg/snet/path/BUILD.bazel index 4f9c63b3a6..1115714a9b 100644 --- a/pkg/snet/path/BUILD.bazel +++ b/pkg/snet/path/BUILD.bazel @@ -17,7 +17,9 @@ go_library( "//pkg/drkey:go_default_library", "//pkg/experimental/epic:go_default_library", "//pkg/experimental/fabrid:go_default_library", + "//pkg/experimental/fabrid/common:go_default_library", "//pkg/experimental/fabrid/crypto:go_default_library", + "//pkg/log:go_default_library", "//pkg/private/serrors:go_default_library", "//pkg/private/util:go_default_library", "//pkg/slayers:go_default_library", diff --git a/pkg/snet/path/fabrid.go b/pkg/snet/path/fabrid.go index b6dd13aa40..186b7bfb2b 100644 --- a/pkg/snet/path/fabrid.go +++ b/pkg/snet/path/fabrid.go @@ -21,7 +21,9 @@ import ( "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/drkey" "github.com/scionproto/scion/pkg/experimental/fabrid" + "github.com/scionproto/scion/pkg/experimental/fabrid/common" "github.com/scionproto/scion/pkg/experimental/fabrid/crypto" + "github.com/scionproto/scion/pkg/log" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/slayers" "github.com/scionproto/scion/pkg/slayers/extension" @@ -30,10 +32,11 @@ import ( ) type FabridConfig struct { - LocalIA addr.IA - LocalAddr string - DestinationIA addr.IA - DestinationAddr string + LocalIA addr.IA + LocalAddr string + DestinationIA addr.IA + DestinationAddr string + ValidationHandler func(*common.PathState, *extension.FabridControlOption, bool) error } type FABRID struct { @@ -47,13 +50,15 @@ type FABRID struct { tmpBuffer []byte identifierBuffer []byte fabridBuffer []byte + e2eBuffer []byte policyIDs []*fabrid.PolicyID numHops int hops []snet.HopInterface + pathState *common.PathState } func NewFABRIDDataplanePath(p SCION, hops []snet.HopInterface, policyIDs []*fabrid.PolicyID, - conf *FabridConfig) (*FABRID, error) { + conf *FabridConfig, validationRatio uint8) (*FABRID, error) { numHops := len(hops) var decoded scion.Decoded if err := decoded.DecodeFromBytes(p.Raw); err != nil { @@ -79,8 +84,10 @@ func NewFABRIDDataplanePath(p SCION, hops []snet.HopInterface, policyIDs []*fabr tmpBuffer: make([]byte, 64), identifierBuffer: make([]byte, 8), fabridBuffer: make([]byte, 8+4*numHops), + e2eBuffer: make([]byte, 5*2), Raw: append([]byte(nil), p.Raw...), policyIDs: policyIDs, + pathState: common.NewFabridPathState(validationRatio), } // Get ingress/egress IFs and IAs from path interfaces @@ -146,8 +153,8 @@ func (f *FABRID) SetExtensions(s *slayers.SCION, p *snet.PacketInfo) error { } fabridOption.HopfieldMetadata[i] = meta } - err = crypto.InitValidators(fabridOption, identifierOption, s, f.tmpBuffer, f.pathKey, - f.keys, nil, f.hops) + valNumber, pathValReply, err := crypto.InitValidators(fabridOption, identifierOption, s, + f.tmpBuffer, f.pathKey, f.keys, nil, f.hops) if err != nil { return serrors.WrapStr("initializing validators failed", err) } @@ -173,7 +180,117 @@ func (f *FABRID) SetExtensions(s *slayers.SCION, p *snet.PacketInfo) error { OptDataLen: uint8(fabridLength), ActualLength: fabridLength, }) + + if valNumber < f.pathState.ValidationRatio { + err = f.pathState.StoreValidationResponse(pathValReply, + identifierOption.GetRelativeTimestamp(), + f.counter) + if err != nil { + return err + } + } + + var e2eOpts []*extension.FabridControlOption + if f.pathState.UpdateValRatio { + valConfigOption := extension.NewFabridControlOption(extension.ValidationConfig) + err = valConfigOption.SetValidationRatio(f.pathState.ValidationRatio) + if err != nil { + return err + } + e2eOpts = append(e2eOpts, valConfigOption) + f.pathState.UpdateValRatio = false + log.Debug("FABRID control: outgoing validation config", + "valRatio", f.pathState.ValidationRatio) + } + if f.pathState.RequestStatistics { + statisticsRequestOption := &extension.FabridControlOption{ + Type: extension.StatisticsRequest, + Auth: [4]byte{}, + } + err = statisticsRequestOption.SetTimestamp(identifierOption.GetRelativeTimestamp()) + if err != nil { + return err + } + err = statisticsRequestOption.SetPacketID(identifierOption.PacketID) + if err != nil { + return err + } + + e2eOpts = append(e2eOpts, statisticsRequestOption) + f.pathState.RequestStatistics = false + log.Debug("FABRID control: sending statistics request") + } + if len(e2eOpts) > 0 { + if p.E2eExtension == nil { + p.E2eExtension = &slayers.EndToEndExtn{} + } + for i, replyOpt := range e2eOpts { + err = crypto.InitFabridControlValidator(replyOpt, identifierOption, f.pathKey.Key[:]) + if err != nil { + return err + } + buffer := f.e2eBuffer[i*5 : (i+1)*5] + err = replyOpt.SerializeTo(buffer) + if err != nil { + return err + } + fabridReplyOptionLength := extension.BaseFabridControlLen + + extension.FabridControlOptionDataLen(replyOpt.Type) + p.E2eExtension.Options = append(p.E2eExtension.Options, + &slayers.EndToEndOption{ + OptType: slayers.OptTypeFabridControl, + OptData: buffer, + OptDataLen: uint8(fabridReplyOptionLength), + ActualLength: fabridReplyOptionLength, + }) + } + } + f.counter++ + f.pathState.Stats.TotalPackets++ + return nil +} + +func (f *FABRID) HandleFabridControlOption(controlOption *extension.FabridControlOption, + identifierOption *extension.IdentifierOption) error { + + err := crypto.VerifyFabridControlValidator(controlOption, identifierOption, f.pathKey.Key[:]) + if err != nil { + return err + } + switch controlOption.Type { + case extension.ValidationConfigAck: + confirmedRatio, err := controlOption.ValidationRatio() + if err != nil { + return err + } + if confirmedRatio == f.pathState.ValidationRatio { + log.Debug("FABRID control: validation ratio confirmed", "ratio", confirmedRatio) + } else if confirmedRatio < f.pathState.ValidationRatio { + log.Info("FABRID control: validation ratio reduced by server", + "requested", f.pathState.ValidationRatio, "confirmed", confirmedRatio) + f.pathState.ValidationRatio = confirmedRatio + } + case extension.ValidationResponse: + err = f.pathState.CheckValidationResponse(controlOption) + if err != nil { + return err + } + err = f.conf.ValidationHandler(f.pathState, controlOption, true) + if err != nil { + return err + } + //log.Debug("FABRID control: validation response", + //"packetID", controlOption.PacketID, "success", success) + + case extension.StatisticsResponse: + totalPkts, invalidPkts, err := controlOption.Statistics() + if err != nil { + return err + } + log.Info("FABRID control: statistics response", "totalPackets", totalPkts, + "invalidPackets", invalidPkts) + } return nil } diff --git a/private/app/path/path.go b/private/app/path/path.go index 921f1c4e3f..62585e1750 100644 --- a/private/app/path/path.go +++ b/private/app/path/path.go @@ -123,7 +123,7 @@ func Choose( continue } fabridPath, err := snetpath.NewFABRIDDataplanePath(scionPath, p.Metadata().Hops(), - pols.Policies(), &o.fabrid.FabridConfig) + pols.Policies(), &o.fabrid.FabridConfig, 0) if err != nil { return nil, serrors.WrapStr("creating fabrid path from scion path", err) } diff --git a/private/path/combinator/BUILD.bazel b/private/path/combinator/BUILD.bazel index de9b76cb07..f03deeb2ab 100644 --- a/private/path/combinator/BUILD.bazel +++ b/private/path/combinator/BUILD.bazel @@ -4,7 +4,6 @@ go_library( name = "go_default_library", srcs = [ "combinator.go", - "fabrid_accumulator.go", "graph.go", "staticinfo_accumulator.go", ], @@ -12,12 +11,11 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/addr:go_default_library", - "//pkg/experimental/fabrid:go_default_library", + "//pkg/experimental/fabrid/graphutils:go_default_library", "//pkg/private/common:go_default_library", "//pkg/private/ctrl/path_mgmt/proto:go_default_library", "//pkg/private/util:go_default_library", "//pkg/segment:go_default_library", - "//pkg/segment/extensions/fabrid:go_default_library", "//pkg/segment/extensions/staticinfo:go_default_library", "//pkg/slayers/path:go_default_library", "//pkg/slayers/path/scion:go_default_library", diff --git a/private/path/combinator/graph.go b/private/path/combinator/graph.go index 253d8e5576..cbedda1e52 100644 --- a/private/path/combinator/graph.go +++ b/private/path/combinator/graph.go @@ -23,6 +23,7 @@ import ( "time" "github.com/scionproto/scion/pkg/addr" + fabrid_utils "github.com/scionproto/scion/pkg/experimental/fabrid/graphutils" "github.com/scionproto/scion/pkg/private/common" "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt/proto" "github.com/scionproto/scion/pkg/private/util" @@ -314,7 +315,7 @@ type pathSolution struct { func (solution *pathSolution) Path() Path { mtu := ^uint16(0) var segments segmentList - fabridMaps := make(map[addr.IA]FabridMapEntry) + fabridMaps := make(map[addr.IA]fabrid_utils.FabridMapEntry) var epicPathAuths [][]byte for _, solEdge := range solution.edges { var hops []path.HopField @@ -384,7 +385,7 @@ func (solution *pathSolution) Path() Path { fabridMap, exists := fabridMaps[asEntry.Local] if (!exists || fabridMap.Ts.Before(solEdge.segment.Info.Timestamp)) && asEntry. Extensions.Digests != nil { - fabridMaps[asEntry.Local] = FabridMapEntry{ + fabridMaps[asEntry.Local] = fabrid_utils.FabridMapEntry{ Map: asEntry.UnsignedExtensions.FabridDetached, Digest: asEntry.Extensions.Digests.Fabrid.Digest, Ts: solEdge.segment.Info.Timestamp, @@ -425,7 +426,7 @@ func (solution *pathSolution) Path() Path { interfaces := segments.Interfaces() asEntries := segments.ASEntries() staticInfo := collectMetadata(interfaces, asEntries) - fabridInfo := collectFabridPolicies(interfaces, fabridMaps) + fabridInfo := fabrid_utils.CollectFabridPolicies(interfaces, fabridMaps) path := Path{ SCIONPath: segments.ScionPath(), Metadata: snet.PathMetadata{ diff --git a/tools/end2end/BUILD.bazel b/tools/end2end/BUILD.bazel index eabd6e2754..5cf4db5024 100644 --- a/tools/end2end/BUILD.bazel +++ b/tools/end2end/BUILD.bazel @@ -9,9 +9,9 @@ go_library( deps = [ "//pkg/addr:go_default_library", "//pkg/daemon:go_default_library", - "//pkg/drkey:go_default_library", "//pkg/experimental/fabrid:go_default_library", - "//pkg/experimental/fabrid/crypto:go_default_library", + "//pkg/experimental/fabrid/common:go_default_library", + "//pkg/experimental/fabrid/server:go_default_library", "//pkg/log:go_default_library", "//pkg/private/common:go_default_library", "//pkg/private/serrors:go_default_library", diff --git a/tools/end2end/main.go b/tools/end2end/main.go index 654d5c319d..1c477a2ab3 100644 --- a/tools/end2end/main.go +++ b/tools/end2end/main.go @@ -29,6 +29,7 @@ import ( "flag" "fmt" "net" + "net/netip" "os" "time" @@ -37,9 +38,9 @@ import ( "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/daemon" - "github.com/scionproto/scion/pkg/drkey" libfabrid "github.com/scionproto/scion/pkg/experimental/fabrid" - "github.com/scionproto/scion/pkg/experimental/fabrid/crypto" + common2 "github.com/scionproto/scion/pkg/experimental/fabrid/common" + fabridserver "github.com/scionproto/scion/pkg/experimental/fabrid/server" "github.com/scionproto/scion/pkg/log" "github.com/scionproto/scion/pkg/private/common" "github.com/scionproto/scion/pkg/private/serrors" @@ -137,7 +138,9 @@ func validateFlags() { log.Info("Flags", "timeout", timeout, "epic", epic, "fabrid", fabrid, "remote", remote) } -type server struct{} +type server struct { + fabridServer *fabridserver.Server +} func (s server) run() { log.Info("Starting server", "isd_as", integration.Local.IA) @@ -167,6 +170,15 @@ func (s server) run() { } log.Info("Listening", "local", fmt.Sprintf("%v:%d", integration.Local.Host.IP, localAddr.Port)) + s.fabridServer = fabridserver.NewFabridServer(&integration.Local, integration.SDConn()) + s.fabridServer.ValidationHandler = func(connection *fabridserver.ClientConnection, + option *extension.IdentifierOption, b bool) error { + log.Debug("Validation handler", "connection", connection, "success", b) + if !b { + return serrors.New("Failed validation") + } + return nil + } // Receive ping message for { if err := s.handlePingFabrid(conn); err != nil { @@ -259,6 +271,8 @@ func (s server) handlePingFabrid(conn snet.PacketConn) error { return serrors.WrapStr("reading packet", err) } + var valResponse *slayers.EndToEndExtn + // If the packet is from remote IA, validate the FABRID path if p.Source.IA != integration.Local.IA { if p.HbhExtension == nil { @@ -268,6 +282,7 @@ func (s server) handlePingFabrid(conn snet.PacketConn) error { // Check extensions for relevant options var identifierOption *extension.IdentifierOption var fabridOption *extension.FabridOption + var controlOptions []*extension.FabridControlOption var err error for _, opt := range p.HbhExtension.Options { @@ -291,6 +306,19 @@ func (s server) handlePingFabrid(conn snet.PacketConn) error { } } } + if p.E2eExtension != nil { + + for _, opt := range p.E2eExtension.Options { + switch opt.OptType { + case slayers.OptTypeFabridControl: + controlOption, err := extension.ParseFabridControlOption(opt) + if err != nil { + return err + } + controlOptions = append(controlOptions, controlOption) + } + } + } if identifierOption == nil { return serrors.New("Missing identifier option") @@ -299,22 +327,8 @@ func (s server) handlePingFabrid(conn snet.PacketConn) error { if fabridOption == nil { return serrors.New("Missing FABRID option") } - - meta := drkey.HostHostMeta{ - Validity: identifierOption.Timestamp, - SrcIA: integration.Local.IA, - SrcHost: integration.Local.Host.IP.String(), - DstIA: p.Source.IA, - DstHost: p.Source.Host.IP().String(), - ProtoId: drkey.FABRID, - } - hostHostKey, err := integration.SDConn().DRKeyGetHostHostKey(context.Background(), meta) - if err != nil { - return serrors.WrapStr("getting host key", err) - } - - tmpBuffer := make([]byte, (len(fabridOption.HopfieldMetadata)*3+15)&^15+16) - _, err = crypto.VerifyPathValidator(fabridOption, tmpBuffer, hostHostKey.Key[:]) + valResponse, err = s.fabridServer.HandleFabridPacket(p.Source, fabridOption, + identifierOption, controlOptions) if err != nil { return err } @@ -382,7 +396,7 @@ func (s server) handlePingFabrid(conn snet.PacketConn) error { // Remove header extension for reverse path p.HbhExtension = nil - p.E2eExtension = nil + p.E2eExtension = valResponse // reverse path rpath, ok := p.Path.(snet.RawPath) @@ -406,6 +420,7 @@ func (s server) handlePingFabrid(conn snet.PacketConn) error { type client struct { network *snet.SCIONNetwork conn *snet.Conn + rawConn snet.PacketConn sdConn daemon.Connector errorPaths map[snet.PathFingerprint]struct{} @@ -458,20 +473,42 @@ func (c *client) attemptRequest(n int) bool { return err } - // Send ping - close, err := c.ping(ctx, n, path) - if err != nil { - logger.Error("Could not send packet", "err", withTag(err)) - return false - } - defer close() - // Receive pong - if err := c.pong(ctx); err != nil { - logger.Error("Error receiving pong", "err", withTag(err)) - if path != nil { - c.errorPaths[snet.Fingerprint(path)] = struct{}{} + if fabrid && remote.IA != integration.Local.IA { + for i := 0; i < 10; i++ { + + // Send ping + close, err := c.fabridPing(ctx, n, path) + if err != nil { + logger.Error("Could not send packet", "err", withTag(err)) + return false + } + defer close() + // Receive FABRID pong + if err := c.fabridPong(ctx); err != nil { + logger.Error("Error receiving pong", "err", withTag(err)) + if path != nil { + c.errorPaths[snet.Fingerprint(path)] = struct{}{} + } + return false + } + } + } else { + // Send ping + close, err := c.ping(ctx, n, path) + if err != nil { + logger.Error("Could not send packet", "err", withTag(err)) + return false + } + defer close() + + // Receive pong + if err := c.pong(ctx); err != nil { + logger.Error("Error receiving pong", "err", withTag(err)) + if path != nil { + c.errorPaths[snet.Fingerprint(path)] = struct{}{} + } + return false } - return false } return true } @@ -505,6 +542,56 @@ func (c *client) ping(ctx context.Context, n int, path snet.Path) (func(), error return closer, nil } +func (c *client) fabridPing(ctx context.Context, n int, path snet.Path) (func(), error) { + rawPing, err := json.Marshal(Ping{ + Server: remote.IA, + Message: ping, + Trace: tracing.IDFromCtx(ctx), + }) + if err != nil { + return nil, serrors.WrapStr("packing ping", err) + } + log.FromCtx(ctx).Info("Dialing", "remote", remote) + c.rawConn, err = c.network.OpenRaw(ctx, integration.Local.Host) + if err != nil { + return nil, serrors.WrapStr("dialing conn", err) + } + if err := c.rawConn.SetWriteDeadline(getDeadline(ctx)); err != nil { + return nil, serrors.WrapStr("setting write deadline", err) + } + log.Info("sending ping", "attempt", n, "remote", remote, "local", c.rawConn.LocalAddr()) + localAddr := c.rawConn.LocalAddr().(*net.UDPAddr) + hostIP, _ := netip.AddrFromSlice(remote.Host.IP) + dst := snet.SCIONAddress{IA: remote.IA, Host: addr.HostIP(hostIP)} + localHostIP, _ := netip.AddrFromSlice(integration.Local.Host.IP) + pkt := &snet.Packet{ + Bytes: make([]byte, common.SupportedMTU), + PacketInfo: snet.PacketInfo{ + Destination: dst, + Source: snet.SCIONAddress{ + IA: integration.Local.IA, + Host: addr.HostIP(localHostIP), + }, + Path: remote.Path, + Payload: snet.UDPPayload{ + SrcPort: uint16(localAddr.Port), + DstPort: uint16(remote.Host.Port), + Payload: rawPing, + }, + }, + } + log.Info("sending packet", "packet", pkt) + if err := c.rawConn.WriteTo(pkt, remote.NextHop); err != nil { + return nil, err + } + closer := func() { + if err := c.rawConn.Close(); err != nil { + log.Error("Unable to close connection", "err", err) + } + } + return closer, nil +} + func (c *client) getRemote(ctx context.Context, n int) (snet.Path, error) { if remote.IA.Equal(integration.Local.IA) { remote.Path = snetpath.Empty{} @@ -569,6 +656,14 @@ func (c *client) getRemote(ctx context.Context, n int) (snet.Path, error) { DestinationIA: remote.IA, DestinationAddr: remote.Host.IP.String(), } + fabridConfig.ValidationHandler = func(ps *common2.PathState, + option *extension.FabridControlOption, b bool) error { + log.Debug("Validation handler", "pathState", ps, "success", b) + if !b { + return serrors.New("Failed validation") + } + return nil + } hops := path.Metadata().Hops() log.Info("Fabrid path", "path", path, "hops", hops) // Use ZERO policy for all hops with fabrid, to just do path validation @@ -580,12 +675,13 @@ func (c *client) getRemote(ctx context.Context, n int) (snet.Path, error) { } } fabridPath, err := snetpath.NewFABRIDDataplanePath(scionPath, hops, - policies, fabridConfig) + policies, fabridConfig, 125) if err != nil { return nil, serrors.New("Error creating FABRID path", "err", err) } remote.Path = fabridPath fabridPath.RegisterDRKeyFetcher(c.sdConn.FabridKeys) + } else { log.Info("FABRID flag was set for client in non-FABRID AS. Proceeding without FABRID.") remote.Path = path.Dataplane() @@ -624,6 +720,74 @@ func (c *client) pong(ctx context.Context) error { return nil } +func (c *client) fabridPong(ctx context.Context) error { + + if err := c.rawConn.SetReadDeadline(getDeadline(ctx)); err != nil { + return serrors.WrapStr("setting read deadline", err) + } + var p snet.Packet + var ov net.UDPAddr + err := readFromFabrid(c.rawConn, &p, &ov) + if err != nil { + return serrors.WrapStr("reading packet", err) + } + if p.Source.IA != integration.Local.IA { + // Check extensions for relevant options + var controlOptions []*extension.FabridControlOption + + if p.E2eExtension != nil { + + for _, opt := range p.E2eExtension.Options { + switch opt.OptType { + case slayers.OptTypeFabridControl: + controlOption, err := extension.ParseFabridControlOption(opt) + if err != nil { + return err + } + controlOptions = append(controlOptions, controlOption) + log.Debug("Parsed control option", "option", controlOption) + } + } + } + switch s := remote.Path.(type) { + case *snetpath.FABRID: + for _, option := range controlOptions { + err := s.HandleFabridControlOption(option, nil) + if err != nil { + return err + } + } + + default: + return serrors.New("unsupported path type") + } + } + + udp, ok := p.Payload.(snet.UDPPayload) + if !ok { + return serrors.New("unexpected payload received", + "source", p.Source, + "destination", p.Destination, + "type", common.TypeOf(p.Payload), + ) + } + var pld Pong + if err := json.Unmarshal(udp.Payload, &pld); err != nil { + return serrors.WrapStr("unpacking pong", err, "data", string(udp.Payload)) + } + + expected := Pong{ + Client: integration.Local.IA, + Server: remote.IA, + Message: pong, + } + if pld.Client != expected.Client || pld.Server != expected.Server || pld.Message != pong { + return serrors.New("unexpected contents received", "data", pld, "expected", expected) + } + log.Info("Received pong", "server", ov) + return nil +} + func getDeadline(ctx context.Context) time.Time { dl, ok := ctx.Deadline() if !ok { diff --git a/tools/end2end_integration/main.go b/tools/end2end_integration/main.go index 61540cfe20..ef526409f9 100644 --- a/tools/end2end_integration/main.go +++ b/tools/end2end_integration/main.go @@ -80,6 +80,7 @@ func realMain() int { fmt.Sprintf("-fabrid=%t", fabrid), } serverArgs := []string{ + "-log.console", "debug", "-mode", "server", "-local", integration.DstAddrPattern + ":0", fmt.Sprintf("-fabrid=%t", fabrid),