diff --git a/acceptance/daemon_vs_standalone/BUILD.bazel b/acceptance/daemon_vs_standalone/BUILD.bazel new file mode 100644 index 0000000000..746ac3238c --- /dev/null +++ b/acceptance/daemon_vs_standalone/BUILD.bazel @@ -0,0 +1,24 @@ +load("//acceptance/common:topogen.bzl", "topogen_test") + +# Test with standalone daemon (embedded daemon using topology files) - default +topogen_test( + name = "test_standalone", + src = "test.py", + args = [ + "--executable=end2end_integration:$(location //tools/end2end_integration)", + ], + data = ["//tools/end2end_integration"], + topo = "//topology:tiny.topo", +) + +# Test with remote daemon (connecting to sciond via gRPC) +topogen_test( + name = "test_daemon", + src = "test.py", + args = [ + "--executable=end2end_integration:$(location //tools/end2end_integration)", + "--use-daemon", + ], + data = ["//tools/end2end_integration"], + topo = "//topology:tiny.topo", +) diff --git a/acceptance/daemon_vs_standalone/test.py b/acceptance/daemon_vs_standalone/test.py new file mode 100644 index 0000000000..1d6c7e02f6 --- /dev/null +++ b/acceptance/daemon_vs_standalone/test.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +# Copyright 2025 SCION Association +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test that compares daemon vs standalone mode for end2end connectivity. + +This test runs the end2end_integration test in two modes: +- Daemon mode: Uses remote SCION daemon connector (connecting via gRPC) +- Standalone mode: Uses embedded daemon with topology files (no sciond) + +The daemon mode can be selected via --use-daemon flag: +- With --use-daemon: Uses remote daemon connector (via gRPC) +- Without --use-daemon (default): Uses standalone daemon connector +""" + +from plumbum import cli + +from acceptance.common import base + + +class Test(base.TestTopogen): + """ + Tests end2end connectivity using either daemon or standalone mode. + """ + + use_daemon = cli.Flag( + "--use-daemon", + help="Use remote SCION daemon instead of standalone daemon", + ) + + def setup_start(self): + super().setup_start() + self.await_connectivity() + + def _run(self): + ping_test = self.get_executable("end2end_integration") + + if self.use_daemon: + print("=== Running with remote daemon (sciond) ===") + ping_test["-d", "-sciond", "-outDir", self.artifacts].run_fg() + else: + print("=== Running with standalone daemon ===") + ping_test["-d", "-outDir", self.artifacts].run_fg() + + +if __name__ == "__main__": + base.main(Test) diff --git a/daemon/BUILD.bazel b/daemon/BUILD.bazel index 09a0f7efd1..112b98f9a6 100644 --- a/daemon/BUILD.bazel +++ b/daemon/BUILD.bazel @@ -7,21 +7,18 @@ go_library( importpath = "github.com/scionproto/scion/daemon", visibility = ["//visibility:public"], deps = [ - "//daemon/drkey:go_default_library", - "//daemon/fetcher:go_default_library", - "//daemon/internal/servers:go_default_library", + "//daemon/grpc:go_default_library", "//pkg/addr:go_default_library", "//pkg/daemon:go_default_library", - "//pkg/grpc:go_default_library", - "//pkg/log:go_default_library", + "//pkg/daemon/asinfo:go_default_library", + "//pkg/daemon/fetcher:go_default_library", + "//pkg/daemon/private/engine:go_default_library", "//pkg/metrics:go_default_library", "//pkg/private/prom:go_default_library", - "//pkg/private/serrors:go_default_library", + "//private/drkey:go_default_library", "//private/env:go_default_library", "//private/revcache:go_default_library", "//private/trust:go_default_library", - "//private/trust/grpc:go_default_library", - "//private/trust/metrics:go_default_library", "@com_github_opentracing_opentracing_go//:go_default_library", "@com_github_prometheus_client_golang//prometheus:go_default_library", ], diff --git a/daemon/cmd/daemon/BUILD.bazel b/daemon/cmd/daemon/BUILD.bazel index 765db00e78..74cf160699 100644 --- a/daemon/cmd/daemon/BUILD.bazel +++ b/daemon/cmd/daemon/BUILD.bazel @@ -15,11 +15,10 @@ go_library( deps = [ "//daemon:go_default_library", "//daemon/config:go_default_library", - "//daemon/drkey:go_default_library", - "//daemon/drkey/grpc:go_default_library", - "//daemon/fetcher:go_default_library", "//daemon/mgmtapi:go_default_library", "//pkg/addr:go_default_library", + "//pkg/daemon/fetcher:go_default_library", + "//pkg/daemon/private/trust:go_default_library", "//pkg/experimental/hiddenpath:go_default_library", "//pkg/experimental/hiddenpath/grpc:go_default_library", "//pkg/grpc:go_default_library", @@ -27,12 +26,11 @@ go_library( "//pkg/metrics:go_default_library", "//pkg/private/prom:go_default_library", "//pkg/private/serrors:go_default_library", - "//pkg/proto/crypto:go_default_library", "//pkg/proto/daemon:go_default_library", - "//pkg/scrypto/cppki:go_default_library", - "//pkg/scrypto/signed:go_default_library", "//private/app:go_default_library", "//private/app/launcher:go_default_library", + "//private/drkey:go_default_library", + "//private/drkey/grpc:go_default_library", "//private/mgmtapi/cppki/api:go_default_library", "//private/mgmtapi/segments/api:go_default_library", "//private/pathdb:go_default_library", diff --git a/daemon/cmd/daemon/main.go b/daemon/cmd/daemon/main.go index f926377dee..cb4e8c93c2 100644 --- a/daemon/cmd/daemon/main.go +++ b/daemon/cmd/daemon/main.go @@ -35,11 +35,10 @@ import ( "github.com/scionproto/scion/daemon" "github.com/scionproto/scion/daemon/config" - sd_drkey "github.com/scionproto/scion/daemon/drkey" - sd_grpc "github.com/scionproto/scion/daemon/drkey/grpc" - "github.com/scionproto/scion/daemon/fetcher" api "github.com/scionproto/scion/daemon/mgmtapi" "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon/fetcher" + daemontrust "github.com/scionproto/scion/pkg/daemon/private/trust" "github.com/scionproto/scion/pkg/experimental/hiddenpath" hpgrpc "github.com/scionproto/scion/pkg/experimental/hiddenpath/grpc" libgrpc "github.com/scionproto/scion/pkg/grpc" @@ -47,12 +46,11 @@ import ( "github.com/scionproto/scion/pkg/metrics" "github.com/scionproto/scion/pkg/private/prom" "github.com/scionproto/scion/pkg/private/serrors" - cryptopb "github.com/scionproto/scion/pkg/proto/crypto" sdpb "github.com/scionproto/scion/pkg/proto/daemon" - "github.com/scionproto/scion/pkg/scrypto/cppki" - "github.com/scionproto/scion/pkg/scrypto/signed" "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/launcher" + sddrkey "github.com/scionproto/scion/private/drkey" + sdgrpc "github.com/scionproto/scion/private/drkey/grpc" cppkiapi "github.com/scionproto/scion/private/mgmtapi/cppki/api" segapi "github.com/scionproto/scion/private/mgmtapi/segments/api" "github.com/scionproto/scion/private/pathdb" @@ -60,7 +58,7 @@ import ( "github.com/scionproto/scion/private/revcache" "github.com/scionproto/scion/private/segment/segfetcher" segfetchergrpc "github.com/scionproto/scion/private/segment/segfetcher/grpc" - infra "github.com/scionproto/scion/private/segment/verifier" + segverifier "github.com/scionproto/scion/private/segment/verifier" "github.com/scionproto/scion/private/service" "github.com/scionproto/scion/private/storage" "github.com/scionproto/scion/private/storage/drkey/level2" @@ -155,8 +153,9 @@ func realMain(ctx context.Context) error { []string{"driver", "operation", prom.LabelResult}, ), }) - engine, err := daemon.TrustEngine( - errCtx, globalCfg.General.ConfigDir, topo.IA(), trustDB, dialer, + certsDir := filepath.Join(globalCfg.General.ConfigDir, "certs") + engine, err := daemontrust.NewEngine( + errCtx, certsDir, topo.IA(), trustDB, dialer, ) if err != nil { return serrors.Wrap("creating trust engine", err) @@ -168,7 +167,7 @@ func realMain(ctx context.Context) error { MaxCacheExpiration: globalCfg.TrustEngine.Cache.Expiration.Duration, } trcLoader := trust.TRCLoader{ - Dir: filepath.Join(globalCfg.General.ConfigDir, "certs"), + Dir: certsDir, DB: trustDB, } //nolint:staticcheck // SA1019: fix later (https://github.com/scionproto/scion/issues/4776). @@ -186,7 +185,7 @@ func realMain(ctx context.Context) error { }, 10*time.Second, 10*time.Second) defer trcLoaderTask.Stop() - var drkeyClientEngine *sd_drkey.ClientEngine + var drkeyClientEngine *sddrkey.ClientEngine if globalCfg.DRKeyLevel2DB.Connection != "" { backend, err := storage.NewDRKeyLevel2Storage(globalCfg.DRKeyLevel2DB) if err != nil { @@ -215,10 +214,10 @@ func realMain(ctx context.Context) error { } defer level2DB.Close() - drkeyFetcher := &sd_grpc.Fetcher{ + drkeyFetcher := &sdgrpc.Fetcher{ Dialer: dialer, } - drkeyClientEngine = &sd_drkey.ClientEngine{ + drkeyClientEngine = &sddrkey.ClientEngine{ IA: topo.IA(), DB: level2DB, Fetcher: drkeyFetcher, @@ -254,9 +253,9 @@ func realMain(ctx context.Context) error { } } - createVerifier := func() infra.Verifier { + createVerifier := func() segverifier.Verifier { if globalCfg.SD.DisableSegVerification { - return acceptAllVerifier{} + return segverifier.AcceptAllVerifier{} } return compat.Verifier{Verifier: trust.Verifier{ Engine: engine, @@ -272,21 +271,21 @@ func realMain(ctx context.Context) error { ) sdpb.RegisterDaemonServiceServer(server, daemon.NewServer( daemon.ServerConfig{ - IA: topo.IA(), - MTU: topo.MTU(), - Topology: topo, + IA: topo.IA(), + MTU: topo.MTU(), + LocalASInfo: topo, Fetcher: fetcher.NewFetcher( fetcher.FetcherConfig{ - IA: topo.IA(), - MTU: topo.MTU(), - Core: topo.Core(), - NextHopper: topo, - RPC: requester, - PathDB: pathDB, - Inspector: engine, - Verifier: createVerifier(), - RevCache: revCache, - Cfg: globalCfg.SD, + IA: topo.IA(), + MTU: topo.MTU(), + Core: topo.Core(), + NextHopper: topo, + RPC: requester, + PathDB: pathDB, + Inspector: engine, + Verifier: createVerifier(), + RevCache: revCache, + QueryInterval: globalCfg.SD.QueryInterval.Duration, }, ), Engine: engine, @@ -367,26 +366,6 @@ func realMain(ctx context.Context) error { return g.Wait() } -type acceptAllVerifier struct{} - -func (acceptAllVerifier) Verify(ctx context.Context, signedMsg *cryptopb.SignedMessage, - associatedData ...[]byte, -) (*signed.Message, error) { - return nil, nil -} - -func (v acceptAllVerifier) WithServer(net.Addr) infra.Verifier { - return v -} - -func (v acceptAllVerifier) WithIA(addr.IA) infra.Verifier { - return v -} - -func (v acceptAllVerifier) WithValidity(cppki.Validity) infra.Verifier { - return v -} - func loaderMetrics() topology.LoaderMetrics { updates := prom.NewCounterVec("", "", "topology_updates_total", diff --git a/daemon/daemon.go b/daemon/daemon.go index 37bbe95b3e..d30e1ec9f0 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -16,31 +16,25 @@ package daemon import ( - "context" - "errors" "io" "net" - "path/filepath" "strconv" - opentracing "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go" "github.com/prometheus/client_golang/prometheus" - "github.com/scionproto/scion/daemon/drkey" - "github.com/scionproto/scion/daemon/fetcher" - "github.com/scionproto/scion/daemon/internal/servers" + "github.com/scionproto/scion/daemon/grpc" "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/daemon" - libgrpc "github.com/scionproto/scion/pkg/grpc" - "github.com/scionproto/scion/pkg/log" + "github.com/scionproto/scion/pkg/daemon/asinfo" + "github.com/scionproto/scion/pkg/daemon/fetcher" + "github.com/scionproto/scion/pkg/daemon/private/engine" "github.com/scionproto/scion/pkg/metrics" "github.com/scionproto/scion/pkg/private/prom" - "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/private/drkey" "github.com/scionproto/scion/private/env" "github.com/scionproto/scion/private/revcache" "github.com/scionproto/scion/private/trust" - trustgrpc "github.com/scionproto/scion/private/trust/grpc" - trustmetrics "github.com/scionproto/scion/private/trust/metrics" ) // InitTracer initializes the global tracer. @@ -53,60 +47,6 @@ func InitTracer(tracing env.Tracing, id string) (io.Closer, error) { return trCloser, nil } -// TrustEngine builds the trust engine backed by the trust database. -func TrustEngine( - ctx context.Context, - cfgDir string, - ia addr.IA, - db trust.DB, - dialer libgrpc.Dialer, -) (trust.Engine, error) { - certsDir := filepath.Join(cfgDir, "certs") - loaded, err := trust.LoadTRCs(ctx, certsDir, db) - if err != nil { - return trust.Engine{}, serrors.Wrap("loading TRCs", err) - } - log.Info("TRCs loaded", "files", loaded.Loaded) - for f, r := range loaded.Ignored { - if errors.Is(r, trust.ErrAlreadyExists) { - log.Debug("Ignoring existing TRC", "file", f) - continue - } - log.Info("Ignoring non-TRC", "file", f, "reason", r) - } - loaded, err = trust.LoadChains(ctx, certsDir, db) - if err != nil { - return trust.Engine{}, serrors.Wrap("loading certificate chains", - err) - } - log.Info("Certificate chains loaded", "files", loaded.Loaded) - for f, r := range loaded.Ignored { - if errors.Is(r, trust.ErrAlreadyExists) { - log.Debug("Ignoring existing certificate chain", "file", f) - continue - } - if errors.Is(r, trust.ErrOutsideValidity) { - log.Debug("Ignoring certificate chain outside validity", "file", f) - continue - } - log.Info("Ignoring non-certificate chain", "file", f, "reason", r) - } - return trust.Engine{ - Inspector: trust.DBInspector{DB: db}, - Provider: trust.FetchingProvider{ - DB: db, - Fetcher: trustgrpc.Fetcher{ - IA: ia, - Dialer: dialer, - Requests: metrics.NewPromCounter(trustmetrics.RPC.Fetches), - }, - Recurser: trust.LocalOnlyRecurser{}, - Router: trust.LocalRouter{IA: ia}, - }, - DB: db, - }, nil -} - // ServerConfig is the configuration for the daemon API server. type ServerConfig struct { IA addr.IA @@ -114,91 +54,93 @@ type ServerConfig struct { Fetcher fetcher.Fetcher RevCache revcache.RevCache Engine trust.Engine - Topology servers.Topology + LocalASInfo asinfo.LocalASInfo DRKeyClient *drkey.ClientEngine } // NewServer constructs a daemon API server. -func NewServer(cfg ServerConfig) *servers.DaemonServer { - return &servers.DaemonServer{ - IA: cfg.IA, - MTU: cfg.MTU, - // TODO(JordiSubira): This will be changed in the future to fetch - // the information from the CS instead of feeding the configuration - // file into. - Topology: cfg.Topology, - Fetcher: cfg.Fetcher, - ASInspector: cfg.Engine.Inspector, - RevCache: cfg.RevCache, - DRKeyClient: cfg.DRKeyClient, - Metrics: servers.Metrics{ - PathsRequests: servers.RequestMetrics{ +func NewServer(cfg ServerConfig) *grpc.DaemonServer { + return &grpc.DaemonServer{ + Engine: &engine.DaemonEngine{ + IA: cfg.IA, + MTU: cfg.MTU, + // TODO(JordiSubira): This will be changed in the future to fetch + // the information from the CS instead of feeding the configuration + // file into. + LocalASInfo: cfg.LocalASInfo, + Fetcher: cfg.Fetcher, + ASInspector: cfg.Engine.Inspector, + RevCache: cfg.RevCache, + DRKeyClient: cfg.DRKeyClient, + }, + Metrics: grpc.Metrics{ + PathsRequests: grpc.RequestMetrics{ Requests: metrics.NewPromCounterFrom(prometheus.CounterOpts{ Namespace: "sd", Subsystem: "path", Name: "requests_total", Help: "The amount of path requests received.", - }, servers.PathsRequestsLabels), + }, grpc.PathsRequestsLabels), Latency: metrics.NewPromHistogramFrom(prometheus.HistogramOpts{ Namespace: "sd", Subsystem: "path", Name: "request_duration_seconds", Help: "Time to handle path requests.", Buckets: prom.DefaultLatencyBuckets, - }, servers.LatencyLabels), + }, grpc.LatencyLabels), }, - ASRequests: servers.RequestMetrics{ + ASRequests: grpc.RequestMetrics{ Requests: metrics.NewPromCounterFrom(prometheus.CounterOpts{ Namespace: "sd", Subsystem: "as_info", Name: "requests_total", Help: "The amount of AS requests received.", - }, servers.ASRequestsLabels), + }, grpc.ASRequestsLabels), Latency: metrics.NewPromHistogramFrom(prometheus.HistogramOpts{ Namespace: "sd", Subsystem: "as_info", Name: "request_duration_seconds", Help: "Time to handle AS requests.", Buckets: prom.DefaultLatencyBuckets, - }, servers.LatencyLabels), + }, grpc.LatencyLabels), }, - InterfacesRequests: servers.RequestMetrics{ + InterfacesRequests: grpc.RequestMetrics{ Requests: metrics.NewPromCounterFrom(prometheus.CounterOpts{ Namespace: "sd", Subsystem: "if_info", Name: "requests_total", Help: "The amount of interfaces requests received.", - }, servers.InterfacesRequestsLabels), + }, grpc.InterfacesRequestsLabels), Latency: metrics.NewPromHistogramFrom(prometheus.HistogramOpts{ Namespace: "sd", Subsystem: "if_info", Name: "request_duration_seconds", Help: "Time to handle interfaces requests.", Buckets: prom.DefaultLatencyBuckets, - }, servers.LatencyLabels), + }, grpc.LatencyLabels), }, - ServicesRequests: servers.RequestMetrics{ + ServicesRequests: grpc.RequestMetrics{ Requests: metrics.NewPromCounterFrom(prometheus.CounterOpts{ Namespace: "sd", Subsystem: "service_info", Name: "requests_total", Help: "The amount of services requests received.", - }, servers.ServicesRequestsLabels), + }, grpc.ServicesRequestsLabels), Latency: metrics.NewPromHistogramFrom(prometheus.HistogramOpts{ Namespace: "sd", Subsystem: "service_info", Name: "request_duration_seconds", Help: "Time to handle services requests.", Buckets: prom.DefaultLatencyBuckets, - }, servers.LatencyLabels), + }, grpc.LatencyLabels), }, - InterfaceDownNotifications: servers.RequestMetrics{ + InterfaceDownNotifications: grpc.RequestMetrics{ Requests: metrics.NewPromCounter(prom.SafeRegister( prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "sd", Name: "received_revocations_total", Help: "The amount of revocations received.", - }, servers.InterfaceDownNotificationsLabels)).(*prometheus.CounterVec), + }, grpc.InterfaceDownNotificationsLabels)).(*prometheus.CounterVec), ), Latency: metrics.NewPromHistogramFrom(prometheus.HistogramOpts{ Namespace: "sd", @@ -206,7 +148,7 @@ func NewServer(cfg ServerConfig) *servers.DaemonServer { Name: "notification_duration_seconds", Help: "Time to handle interface down notifications.", Buckets: prom.DefaultLatencyBuckets, - }, servers.LatencyLabels), + }, grpc.LatencyLabels), }, }, } diff --git a/daemon/internal/servers/BUILD.bazel b/daemon/grpc/BUILD.bazel similarity index 63% rename from daemon/internal/servers/BUILD.bazel rename to daemon/grpc/BUILD.bazel index ec1ae2a4ec..072871a751 100644 --- a/daemon/internal/servers/BUILD.bazel +++ b/daemon/grpc/BUILD.bazel @@ -3,35 +3,31 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ - "grpc.go", "metrics.go", + "server.go", ], - importpath = "github.com/scionproto/scion/daemon/internal/servers", + importpath = "github.com/scionproto/scion/daemon/grpc", visibility = ["//daemon:__subpackages__"], deps = [ - "//daemon/drkey:go_default_library", - "//daemon/fetcher:go_default_library", "//pkg/addr:go_default_library", + "//pkg/daemon/asinfo:go_default_library", + "//pkg/daemon/fetcher:go_default_library", + "//pkg/daemon/private/engine:go_default_library", + "//pkg/daemon/types:go_default_library", "//pkg/drkey:go_default_library", - "//pkg/log:go_default_library", "//pkg/metrics:go_default_library", - "//pkg/private/ctrl/path_mgmt:go_default_library", - "//pkg/private/ctrl/path_mgmt/proto:go_default_library", "//pkg/private/prom:go_default_library", "//pkg/private/serrors:go_default_library", - "//pkg/private/util:go_default_library", "//pkg/proto/daemon:go_default_library", - "//pkg/segment/iface:go_default_library", "//pkg/slices:go_default_library", "//pkg/snet:go_default_library", "//pkg/snet/path:go_default_library", + "//private/drkey:go_default_library", "//private/revcache:go_default_library", "//private/topology:go_default_library", "//private/trust:go_default_library", - "@com_github_opentracing_opentracing_go//:go_default_library", "@org_golang_google_protobuf//types/known/durationpb:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", - "@org_golang_x_sync//singleflight:go_default_library", ], ) diff --git a/daemon/internal/servers/metrics.go b/daemon/grpc/metrics.go similarity index 99% rename from daemon/internal/servers/metrics.go rename to daemon/grpc/metrics.go index c69d565e42..38e1db2261 100644 --- a/daemon/internal/servers/metrics.go +++ b/daemon/grpc/metrics.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package servers +package grpc import ( "github.com/scionproto/scion/pkg/addr" diff --git a/daemon/grpc/server.go b/daemon/grpc/server.go new file mode 100644 index 0000000000..97445228cf --- /dev/null +++ b/daemon/grpc/server.go @@ -0,0 +1,454 @@ +// Copyright 2020 Anapaya Systems +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package grpc + +import ( + "bytes" + "context" + "net" + "net/netip" + "time" + + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon/asinfo" + "github.com/scionproto/scion/pkg/daemon/fetcher" + "github.com/scionproto/scion/pkg/daemon/private/engine" + daemontypes "github.com/scionproto/scion/pkg/daemon/types" + "github.com/scionproto/scion/pkg/drkey" + "github.com/scionproto/scion/pkg/private/serrors" + sdpb "github.com/scionproto/scion/pkg/proto/daemon" + "github.com/scionproto/scion/pkg/slices" + "github.com/scionproto/scion/pkg/snet" + snetpath "github.com/scionproto/scion/pkg/snet/path" + drkeyengine "github.com/scionproto/scion/private/drkey" + "github.com/scionproto/scion/private/revcache" + "github.com/scionproto/scion/private/topology" + "github.com/scionproto/scion/private/trust" +) + +// Topology is the interface for accessing topology information. +type Topology interface { + IfIDs() []uint16 + UnderlayNextHop(uint16) *net.UDPAddr + ControlServiceAddresses() []*net.UDPAddr + PortRange() (uint16, uint16) +} + +// DaemonServer handles gRPC requests to the SCION daemon. +// It delegates business logic to the embedded DaemonEngine. +type DaemonServer struct { + Engine *engine.DaemonEngine + Metrics Metrics +} + +// NewDaemonServer creates a new DaemonServer with the given configuration. +func NewDaemonServer( + ia addr.IA, + mtu uint16, + localASInfo asinfo.LocalASInfo, + fetcher fetcher.Fetcher, + revCache revcache.RevCache, + asInspector trust.Inspector, + drkeyClient *drkeyengine.ClientEngine, + metrics Metrics, +) *DaemonServer { + return &DaemonServer{ + Engine: &engine.DaemonEngine{ + IA: ia, + MTU: mtu, + LocalASInfo: localASInfo, + Fetcher: fetcher, + RevCache: revCache, + ASInspector: asInspector, + DRKeyClient: drkeyClient, + }, + Metrics: metrics, + } +} + +// Paths serves the paths request. +func (s *DaemonServer) Paths( + ctx context.Context, + req *sdpb.PathsRequest, +) (*sdpb.PathsResponse, error) { + start := time.Now() + dstI := addr.IA(req.DestinationIsdAs).ISD() + response, err := s.paths(ctx, req) + s.Metrics.PathsRequests.inc( + pathReqLabels{Result: errToMetricResult(err), Dst: dstI}, + time.Since(start).Seconds(), + ) + return response, unwrapMetricsError(err) +} + +func (s *DaemonServer) paths( + ctx context.Context, + req *sdpb.PathsRequest, +) (*sdpb.PathsResponse, error) { + srcIA, dstIA := addr.IA(req.SourceIsdAs), addr.IA(req.DestinationIsdAs) + flags := daemontypes.PathReqFlags{ + Refresh: req.Refresh, + Hidden: req.Hidden, + } + paths, err := s.Engine.Paths(ctx, dstIA, srcIA, flags) + if err != nil { + return nil, err + } + reply := &sdpb.PathsResponse{} + for _, p := range paths { + reply.Paths = append(reply.Paths, pathToPB(p)) + } + return reply, nil +} + +// AS serves the AS request. +func (s *DaemonServer) AS(ctx context.Context, req *sdpb.ASRequest) (*sdpb.ASResponse, error) { + start := time.Now() + response, err := s.as(ctx, req) + s.Metrics.ASRequests.inc( + reqLabels{Result: errToMetricResult(err)}, + time.Since(start).Seconds(), + ) + return response, unwrapMetricsError(err) +} + +func (s *DaemonServer) as(ctx context.Context, req *sdpb.ASRequest) (*sdpb.ASResponse, error) { + asInfo, err := s.Engine.ASInfo(ctx, addr.IA(req.IsdAs)) + if err != nil { + return nil, err + } + // Note: We don't have the 'core' attribute in daemon.ASInfo, + // so we need to query it directly here. + reqIA := addr.IA(req.IsdAs) + if reqIA.IsZero() { + reqIA = s.Engine.IA + } + core, err := s.Engine.ASInspector.HasAttributes(ctx, reqIA, trust.Core) + if err != nil { + return nil, serrors.Wrap("inspecting ISD-AS", err, "isd_as", reqIA) + } + return &sdpb.ASResponse{ + IsdAs: uint64(asInfo.IA), + Core: core, + Mtu: uint32(asInfo.MTU), + }, nil +} + +// Interfaces serves the interfaces request. +func (s *DaemonServer) Interfaces( + ctx context.Context, + req *sdpb.InterfacesRequest, +) (*sdpb.InterfacesResponse, error) { + start := time.Now() + response, err := s.interfaces(ctx, req) + s.Metrics.InterfacesRequests.inc( + reqLabels{Result: errToMetricResult(err)}, + time.Since(start).Seconds(), + ) + return response, unwrapMetricsError(err) +} + +func (s *DaemonServer) interfaces( + ctx context.Context, + _ *sdpb.InterfacesRequest, +) (*sdpb.InterfacesResponse, error) { + intfs, err := s.Engine.Interfaces(ctx) + if err != nil { + return nil, err + } + reply := &sdpb.InterfacesResponse{ + Interfaces: make(map[uint64]*sdpb.Interface), + } + for ifID, addr := range intfs { + reply.Interfaces[uint64(ifID)] = &sdpb.Interface{ + Address: &sdpb.Underlay{ + Address: addr.String(), + }, + } + } + return reply, nil +} + +// Services serves the services request. +func (s *DaemonServer) Services( + ctx context.Context, + req *sdpb.ServicesRequest, +) (*sdpb.ServicesResponse, error) { + start := time.Now() + response, err := s.services(ctx, req) + s.Metrics.ServicesRequests.inc( + reqLabels{Result: errToMetricResult(err)}, + time.Since(start).Seconds(), + ) + return response, unwrapMetricsError(err) +} + +func (s *DaemonServer) services( + ctx context.Context, + _ *sdpb.ServicesRequest, +) (*sdpb.ServicesResponse, error) { + uris, err := s.Engine.SVCInfo(ctx) + if err != nil { + return nil, err + } + reply := &sdpb.ServicesResponse{ + Services: make(map[string]*sdpb.ListService), + } + list := &sdpb.ListService{} + for _, uri := range uris { + list.Services = append(list.Services, &sdpb.Service{Uri: uri}) + } + reply.Services[topology.Control.String()] = list + return reply, nil +} + +// NotifyInterfaceDown notifies the server about an interface that is down. +func (s *DaemonServer) NotifyInterfaceDown( + ctx context.Context, + req *sdpb.NotifyInterfaceDownRequest, +) (*sdpb.NotifyInterfaceDownResponse, error) { + start := time.Now() + response, err := s.notifyInterfaceDown(ctx, req) + s.Metrics.InterfaceDownNotifications.inc( + ifDownLabels{Result: errToMetricResult(err), Src: "notification"}, + time.Since(start).Seconds(), + ) + return response, unwrapMetricsError(err) +} + +func (s *DaemonServer) notifyInterfaceDown( + ctx context.Context, + req *sdpb.NotifyInterfaceDownRequest, +) (*sdpb.NotifyInterfaceDownResponse, error) { + err := s.Engine.NotifyInterfaceDown(ctx, addr.IA(req.IsdAs), req.Id) + if err != nil { + return nil, err + } + return &sdpb.NotifyInterfaceDownResponse{}, nil +} + +// PortRange returns the port range for the dispatched ports. +func (s *DaemonServer) PortRange( + ctx context.Context, + _ *emptypb.Empty, +) (*sdpb.PortRangeResponse, error) { + startPort, endPort, err := s.Engine.PortRange(ctx) + if err != nil { + return nil, err + } + return &sdpb.PortRangeResponse{ + DispatchedPortStart: uint32(startPort), + DispatchedPortEnd: uint32(endPort), + }, nil +} + +func (s *DaemonServer) DRKeyASHost( + ctx context.Context, + req *sdpb.DRKeyASHostRequest, +) (*sdpb.DRKeyASHostResponse, error) { + meta, err := requestToASHostMeta(req) + if err != nil { + return nil, serrors.Wrap("parsing protobuf ASHostReq", err) + } + lvl2Key, err := s.Engine.DRKeyGetASHostKey(ctx, meta) + if err != nil { + return nil, serrors.Wrap("getting AS-Host from client store", err) + } + return &sdpb.DRKeyASHostResponse{ + EpochBegin: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotBefore.Unix()}, + EpochEnd: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotAfter.Unix()}, + Key: lvl2Key.Key[:], + }, nil +} + +func (s *DaemonServer) DRKeyHostAS( + ctx context.Context, + req *sdpb.DRKeyHostASRequest, +) (*sdpb.DRKeyHostASResponse, error) { + meta, err := requestToHostASMeta(req) + if err != nil { + return nil, serrors.Wrap("parsing protobuf HostASReq", err) + } + lvl2Key, err := s.Engine.DRKeyGetHostASKey(ctx, meta) + if err != nil { + return nil, serrors.Wrap("getting Host-AS from client store", err) + } + return &sdpb.DRKeyHostASResponse{ + EpochBegin: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotBefore.Unix()}, + EpochEnd: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotAfter.Unix()}, + Key: lvl2Key.Key[:], + }, nil +} + +func (s *DaemonServer) DRKeyHostHost( + ctx context.Context, + req *sdpb.DRKeyHostHostRequest, +) (*sdpb.DRKeyHostHostResponse, error) { + meta, err := requestToHostHostMeta(req) + if err != nil { + return nil, serrors.Wrap("parsing protobuf HostHostReq", err) + } + lvl2Key, err := s.Engine.DRKeyGetHostHostKey(ctx, meta) + if err != nil { + return nil, serrors.Wrap("getting Host-Host from client store", err) + } + return &sdpb.DRKeyHostHostResponse{ + EpochBegin: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotBefore.Unix()}, + EpochEnd: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotAfter.Unix()}, + Key: lvl2Key.Key[:], + }, nil +} + +// Protobuf conversion helpers + +func pathToPB(path snet.Path) *sdpb.Path { + meta := path.Metadata() + interfaces := make([]*sdpb.PathInterface, len(meta.Interfaces)) + for i, intf := range meta.Interfaces { + interfaces[i] = &sdpb.PathInterface{ + Id: uint64(intf.ID), + IsdAs: uint64(intf.IA), + } + } + + latency := make([]*durationpb.Duration, len(meta.Latency)) + for i, v := range meta.Latency { + seconds := int64(v / time.Second) + nanos := int32(v - time.Duration(seconds)*time.Second) + latency[i] = &durationpb.Duration{Seconds: seconds, Nanos: nanos} + } + geo := make([]*sdpb.GeoCoordinates, len(meta.Geo)) + for i, v := range meta.Geo { + geo[i] = &sdpb.GeoCoordinates{ + Latitude: v.Latitude, + Longitude: v.Longitude, + Address: v.Address, + } + } + linkType := make([]sdpb.LinkType, len(meta.LinkType)) + for i, v := range meta.LinkType { + linkType[i] = linkTypeToPB(v) + } + + var raw []byte + scionPath, ok := path.Dataplane().(snetpath.SCION) + if ok { + raw = scionPath.Raw + } + nextHopStr := "" + if nextHop := path.UnderlayNextHop(); nextHop != nil { + nextHopStr = nextHop.String() + } + + epicAuths := &sdpb.EpicAuths{ + AuthPhvf: bytes.Clone(meta.EpicAuths.AuthPHVF), + AuthLhvf: bytes.Clone(meta.EpicAuths.AuthLHVF), + } + + var discovery map[uint64]*sdpb.DiscoveryInformation + if di := meta.DiscoveryInformation; di != nil { + discovery = make(map[uint64]*sdpb.DiscoveryInformation, len(di)) + for ia, info := range di { + discovery[uint64(ia)] = &sdpb.DiscoveryInformation{ + ControlServiceAddresses: slices.Transform( + info.ControlServices, + netip.AddrPort.String, + ), + DiscoveryServiceAddresses: slices.Transform( + info.DiscoveryServices, + netip.AddrPort.String, + ), + } + } + } + + return &sdpb.Path{ + Raw: raw, + Interface: &sdpb.Interface{ + Address: &sdpb.Underlay{Address: nextHopStr}, + }, + Interfaces: interfaces, + Mtu: uint32(meta.MTU), + Expiration: ×tamppb.Timestamp{Seconds: meta.Expiry.Unix()}, + Latency: latency, + Bandwidth: meta.Bandwidth, + Geo: geo, + LinkType: linkType, + InternalHops: meta.InternalHops, + Notes: meta.Notes, + EpicAuths: epicAuths, + DiscoveryInformation: discovery, + } +} + +func linkTypeToPB(lt snet.LinkType) sdpb.LinkType { + switch lt { + case snet.LinkTypeDirect: + return sdpb.LinkType_LINK_TYPE_DIRECT + case snet.LinkTypeMultihop: + return sdpb.LinkType_LINK_TYPE_MULTI_HOP + case snet.LinkTypeOpennet: + return sdpb.LinkType_LINK_TYPE_OPEN_NET + default: + return sdpb.LinkType_LINK_TYPE_UNSPECIFIED + } +} + +func requestToASHostMeta(req *sdpb.DRKeyASHostRequest) (drkey.ASHostMeta, error) { + err := req.ValTime.CheckValid() + if err != nil { + return drkey.ASHostMeta{}, serrors.Wrap("invalid valTime from pb request", err) + } + return drkey.ASHostMeta{ + ProtoId: drkey.Protocol(req.ProtocolId), + Validity: req.ValTime.AsTime(), + SrcIA: addr.IA(req.SrcIa), + DstIA: addr.IA(req.DstIa), + DstHost: req.DstHost, + }, nil +} + +func requestToHostASMeta(req *sdpb.DRKeyHostASRequest) (drkey.HostASMeta, error) { + err := req.ValTime.CheckValid() + if err != nil { + return drkey.HostASMeta{}, serrors.Wrap("invalid valTime from pb request", err) + } + return drkey.HostASMeta{ + ProtoId: drkey.Protocol(req.ProtocolId), + Validity: req.ValTime.AsTime(), + SrcIA: addr.IA(req.SrcIa), + DstIA: addr.IA(req.DstIa), + SrcHost: req.SrcHost, + }, nil +} + +func requestToHostHostMeta(req *sdpb.DRKeyHostHostRequest) (drkey.HostHostMeta, error) { + err := req.ValTime.CheckValid() + if err != nil { + return drkey.HostHostMeta{}, serrors.Wrap("invalid valTime from pb request", err) + } + return drkey.HostHostMeta{ + ProtoId: drkey.Protocol(req.ProtocolId), + Validity: req.ValTime.AsTime(), + SrcIA: addr.IA(req.SrcIa), + DstIA: addr.IA(req.DstIa), + SrcHost: req.SrcHost, + DstHost: req.DstHost, + }, nil +} diff --git a/daemon/internal/servers/grpc.go b/daemon/internal/servers/grpc.go deleted file mode 100644 index 06edc3b736..0000000000 --- a/daemon/internal/servers/grpc.go +++ /dev/null @@ -1,500 +0,0 @@ -// Copyright 2020 Anapaya Systems -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package servers - -import ( - "context" - "fmt" - "net" - "net/netip" - "time" - - "github.com/opentracing/opentracing-go" - "golang.org/x/sync/singleflight" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" - - drkey_daemon "github.com/scionproto/scion/daemon/drkey" - "github.com/scionproto/scion/daemon/fetcher" - "github.com/scionproto/scion/pkg/addr" - "github.com/scionproto/scion/pkg/drkey" - "github.com/scionproto/scion/pkg/log" - "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt" - "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt/proto" - "github.com/scionproto/scion/pkg/private/prom" - "github.com/scionproto/scion/pkg/private/serrors" - "github.com/scionproto/scion/pkg/private/util" - "github.com/scionproto/scion/pkg/proto/daemon" - "github.com/scionproto/scion/pkg/segment/iface" - "github.com/scionproto/scion/pkg/slices" - "github.com/scionproto/scion/pkg/snet" - snetpath "github.com/scionproto/scion/pkg/snet/path" - "github.com/scionproto/scion/private/revcache" - "github.com/scionproto/scion/private/topology" - "github.com/scionproto/scion/private/trust" -) - -type Topology interface { - IfIDs() []uint16 - UnderlayNextHop(uint16) *net.UDPAddr - ControlServiceAddresses() []*net.UDPAddr - PortRange() (uint16, uint16) -} - -// DaemonServer handles gRPC requests to the SCION daemon. -type DaemonServer struct { - IA addr.IA - MTU uint16 - Topology Topology - Fetcher fetcher.Fetcher - RevCache revcache.RevCache - ASInspector trust.Inspector - DRKeyClient *drkey_daemon.ClientEngine - - Metrics Metrics - - foregroundPathDedupe singleflight.Group - backgroundPathDedupe singleflight.Group -} - -// Paths serves the paths request. -func (s *DaemonServer) Paths(ctx context.Context, - req *daemon.PathsRequest, -) (*daemon.PathsResponse, error) { - start := time.Now() - dstI := addr.IA(req.DestinationIsdAs).ISD() - response, err := s.paths(ctx, req) - s.Metrics.PathsRequests.inc( - pathReqLabels{Result: errToMetricResult(err), Dst: dstI}, - time.Since(start).Seconds(), - ) - return response, unwrapMetricsError(err) -} - -func (s *DaemonServer) paths(ctx context.Context, - req *daemon.PathsRequest, -) (*daemon.PathsResponse, error) { - if _, ok := ctx.Deadline(); !ok { - var cancelF context.CancelFunc - ctx, cancelF = context.WithTimeout(ctx, 10*time.Second) - defer cancelF() - } - srcIA, dstIA := addr.IA(req.SourceIsdAs), addr.IA(req.DestinationIsdAs) - go func() { - defer log.HandlePanic() - s.backgroundPaths(ctx, srcIA, dstIA, req.Refresh) - }() - paths, err := s.fetchPaths(ctx, &s.foregroundPathDedupe, srcIA, dstIA, req.Refresh) - if err != nil { - log.FromCtx(ctx).Debug("Fetching paths", "err", err, - "src", srcIA, "dst", dstIA, "refresh", req.Refresh) - return nil, err - } - reply := &daemon.PathsResponse{} - for _, p := range paths { - reply.Paths = append(reply.Paths, pathToPB(p)) - } - return reply, nil -} - -func (s *DaemonServer) fetchPaths( - ctx context.Context, - group *singleflight.Group, - src, dst addr.IA, - refresh bool, -) ([]snet.Path, error) { - r, err, _ := group.Do(fmt.Sprintf("%s%s%t", src, dst, refresh), - func() (any, error) { - return s.Fetcher.GetPaths(ctx, src, dst, refresh) - }, - ) - // just cast to the correct type, ignore the "ok", since that can only be - // false in case of a nil result. - paths, _ := r.([]snet.Path) - return paths, err -} - -func pathToPB(path snet.Path) *daemon.Path { - meta := path.Metadata() - interfaces := make([]*daemon.PathInterface, len(meta.Interfaces)) - for i, intf := range meta.Interfaces { - interfaces[i] = &daemon.PathInterface{ - Id: uint64(intf.ID), - IsdAs: uint64(intf.IA), - } - } - - latency := make([]*durationpb.Duration, len(meta.Latency)) - for i, v := range meta.Latency { - seconds := int64(v / time.Second) - nanos := int32(v - time.Duration(seconds)*time.Second) - latency[i] = &durationpb.Duration{Seconds: seconds, Nanos: nanos} - } - geo := make([]*daemon.GeoCoordinates, len(meta.Geo)) - for i, v := range meta.Geo { - geo[i] = &daemon.GeoCoordinates{ - Latitude: v.Latitude, - Longitude: v.Longitude, - Address: v.Address, - } - } - linkType := make([]daemon.LinkType, len(meta.LinkType)) - for i, v := range meta.LinkType { - linkType[i] = linkTypeToPB(v) - } - - var raw []byte - scionPath, ok := path.Dataplane().(snetpath.SCION) - if ok { - raw = scionPath.Raw - } - nextHopStr := "" - if nextHop := path.UnderlayNextHop(); nextHop != nil { - nextHopStr = nextHop.String() - } - - epicAuths := &daemon.EpicAuths{ - AuthPhvf: append([]byte(nil), meta.EpicAuths.AuthPHVF...), - AuthLhvf: append([]byte(nil), meta.EpicAuths.AuthLHVF...), - } - - var discovery map[uint64]*daemon.DiscoveryInformation - if di := meta.DiscoveryInformation; di != nil { - discovery = make(map[uint64]*daemon.DiscoveryInformation, len(di)) - for ia, info := range di { - discovery[uint64(ia)] = &daemon.DiscoveryInformation{ - ControlServiceAddresses: slices.Transform( - info.ControlServices, - netip.AddrPort.String, - ), - DiscoveryServiceAddresses: slices.Transform( - info.DiscoveryServices, - netip.AddrPort.String, - ), - } - } - } - - return &daemon.Path{ - Raw: raw, - Interface: &daemon.Interface{ - Address: &daemon.Underlay{Address: nextHopStr}, - }, - Interfaces: interfaces, - Mtu: uint32(meta.MTU), - Expiration: ×tamppb.Timestamp{Seconds: meta.Expiry.Unix()}, - Latency: latency, - Bandwidth: meta.Bandwidth, - Geo: geo, - LinkType: linkType, - InternalHops: meta.InternalHops, - Notes: meta.Notes, - EpicAuths: epicAuths, - DiscoveryInformation: discovery, - } -} - -func linkTypeToPB(lt snet.LinkType) daemon.LinkType { - switch lt { - case snet.LinkTypeDirect: - return daemon.LinkType_LINK_TYPE_DIRECT - case snet.LinkTypeMultihop: - return daemon.LinkType_LINK_TYPE_MULTI_HOP - case snet.LinkTypeOpennet: - return daemon.LinkType_LINK_TYPE_OPEN_NET - default: - return daemon.LinkType_LINK_TYPE_UNSPECIFIED - } -} - -func (s *DaemonServer) backgroundPaths(origCtx context.Context, src, dst addr.IA, refresh bool) { - backgroundTimeout := 5 * time.Second - deadline, ok := origCtx.Deadline() - if !ok || time.Until(deadline) > backgroundTimeout { - // the original context is large enough no need to spin a background fetch. - return - } - // We're not passing origCtx because this is a background fetch that - // should continue even in case origCtx is cancelled. - ctx, cancelF := context.WithTimeout(context.Background(), backgroundTimeout) - defer cancelF() - var spanOpts []opentracing.StartSpanOption - if span := opentracing.SpanFromContext(origCtx); span != nil { - spanOpts = append(spanOpts, opentracing.FollowsFrom(span.Context())) - } - span, ctx := opentracing.StartSpanFromContext(ctx, "fetch.paths.background", spanOpts...) - defer span.Finish() - //nolint:contextcheck // false positive. - if _, err := s.fetchPaths(ctx, &s.backgroundPathDedupe, src, dst, refresh); err != nil { - log.FromCtx(ctx).Debug("Error fetching paths (background)", "err", err, - "src", src, "dst", dst, "refresh", refresh) - } -} - -// AS serves the AS request. -func (s *DaemonServer) AS(ctx context.Context, req *daemon.ASRequest) (*daemon.ASResponse, error) { - start := time.Now() - response, err := s.as(ctx, req) - s.Metrics.ASRequests.inc( - reqLabels{Result: errToMetricResult(err)}, - time.Since(start).Seconds(), - ) - return response, unwrapMetricsError(err) -} - -func (s *DaemonServer) as(ctx context.Context, req *daemon.ASRequest) (*daemon.ASResponse, error) { - reqIA := addr.IA(req.IsdAs) - if reqIA.IsZero() { - reqIA = s.IA - } - mtu := uint32(0) - if reqIA.Equal(s.IA) { - mtu = uint32(s.MTU) - } - core, err := s.ASInspector.HasAttributes(ctx, reqIA, trust.Core) - if err != nil { - log.FromCtx(ctx).Error("Inspecting ISD-AS", "err", err, "isd_as", reqIA) - return nil, serrors.Wrap("inspecting ISD-AS", err, "isd_as", reqIA) - } - reply := &daemon.ASResponse{ - IsdAs: uint64(reqIA), - Core: core, - Mtu: mtu, - } - return reply, nil -} - -// Interfaces serves the interfaces request. -func (s *DaemonServer) Interfaces(ctx context.Context, - req *daemon.InterfacesRequest, -) (*daemon.InterfacesResponse, error) { - start := time.Now() - response, err := s.interfaces(ctx, req) - s.Metrics.InterfacesRequests.inc( - reqLabels{Result: errToMetricResult(err)}, - time.Since(start).Seconds(), - ) - return response, unwrapMetricsError(err) -} - -func (s *DaemonServer) interfaces(ctx context.Context, - _ *daemon.InterfacesRequest, -) (*daemon.InterfacesResponse, error) { - reply := &daemon.InterfacesResponse{ - Interfaces: make(map[uint64]*daemon.Interface), - } - topo := s.Topology - for _, ifID := range topo.IfIDs() { - nextHop := topo.UnderlayNextHop(ifID) - if nextHop == nil { - continue - } - reply.Interfaces[uint64(ifID)] = &daemon.Interface{ - Address: &daemon.Underlay{ - Address: nextHop.String(), - }, - } - } - return reply, nil -} - -// Services serves the services request. -func (s *DaemonServer) Services(ctx context.Context, - req *daemon.ServicesRequest, -) (*daemon.ServicesResponse, error) { - start := time.Now() - respsonse, err := s.services(ctx, req) - s.Metrics.ServicesRequests.inc( - reqLabels{Result: errToMetricResult(err)}, - time.Since(start).Seconds(), - ) - return respsonse, unwrapMetricsError(err) -} - -func (s *DaemonServer) services(ctx context.Context, - _ *daemon.ServicesRequest, -) (*daemon.ServicesResponse, error) { - reply := &daemon.ServicesResponse{ - Services: make(map[string]*daemon.ListService), - } - list := &daemon.ListService{} - for _, h := range s.Topology.ControlServiceAddresses() { - // TODO(lukedirtwalker): build actual URI after it's defined (anapapaya/scion#3587) - list.Services = append(list.Services, &daemon.Service{Uri: h.String()}) - } - reply.Services[topology.Control.String()] = list - return reply, nil -} - -// NotifyInterfaceDown notifies the server about an interface that is down. -func (s *DaemonServer) NotifyInterfaceDown(ctx context.Context, - req *daemon.NotifyInterfaceDownRequest, -) (*daemon.NotifyInterfaceDownResponse, error) { - start := time.Now() - response, err := s.notifyInterfaceDown(ctx, req) - s.Metrics.InterfaceDownNotifications.inc( - ifDownLabels{Result: errToMetricResult(err), Src: "notification"}, - time.Since(start).Seconds(), - ) - return response, unwrapMetricsError(err) -} - -func (s *DaemonServer) notifyInterfaceDown(ctx context.Context, - req *daemon.NotifyInterfaceDownRequest, -) (*daemon.NotifyInterfaceDownResponse, error) { - revInfo := &path_mgmt.RevInfo{ - RawIsdas: addr.IA(req.IsdAs), - IfID: iface.ID(req.Id), - LinkType: proto.LinkType_core, - RawTTL: 10, - RawTimestamp: util.TimeToSecs(time.Now()), - } - _, err := s.RevCache.Insert(ctx, revInfo) - if err != nil { - log.FromCtx(ctx).Error("Inserting revocation", "err", err, "req", req) - return nil, metricsError{ - err: serrors.Wrap("inserting revocation", err), - result: prom.ErrDB, - } - } - return &daemon.NotifyInterfaceDownResponse{}, nil -} - -// PortRange returns the port range for the dispatched ports. -func (s *DaemonServer) PortRange( - _ context.Context, - _ *emptypb.Empty, -) (*daemon.PortRangeResponse, error) { - startPort, endPort := s.Topology.PortRange() - return &daemon.PortRangeResponse{ - DispatchedPortStart: uint32(startPort), - DispatchedPortEnd: uint32(endPort), - }, nil -} - -func (s *DaemonServer) DRKeyASHost( - ctx context.Context, - req *daemon.DRKeyASHostRequest, -) (*daemon.DRKeyASHostResponse, error) { - if s.DRKeyClient == nil { - return nil, serrors.New("DRKey is not available") - } - meta, err := requestToASHostMeta(req) - if err != nil { - return nil, serrors.Wrap("parsing protobuf ASHostReq", err) - } - - lvl2Key, err := s.DRKeyClient.GetASHostKey(ctx, meta) - if err != nil { - return nil, serrors.Wrap("getting AS-Host from client store", err) - } - - return &daemon.DRKeyASHostResponse{ - EpochBegin: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotBefore.Unix()}, - EpochEnd: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotAfter.Unix()}, - Key: lvl2Key.Key[:], - }, nil -} - -func (s *DaemonServer) DRKeyHostAS( - ctx context.Context, - req *daemon.DRKeyHostASRequest, -) (*daemon.DRKeyHostASResponse, error) { - if s.DRKeyClient == nil { - return nil, serrors.New("DRKey is not available") - } - meta, err := requestToHostASMeta(req) - if err != nil { - return nil, serrors.Wrap("parsing protobuf HostASReq", err) - } - - lvl2Key, err := s.DRKeyClient.GetHostASKey(ctx, meta) - if err != nil { - return nil, serrors.Wrap("getting Host-AS from client store", err) - } - - return &daemon.DRKeyHostASResponse{ - EpochBegin: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotBefore.Unix()}, - EpochEnd: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotAfter.Unix()}, - Key: lvl2Key.Key[:], - }, nil -} - -func (s *DaemonServer) DRKeyHostHost( - ctx context.Context, - req *daemon.DRKeyHostHostRequest, -) (*daemon.DRKeyHostHostResponse, error) { - if s.DRKeyClient == nil { - return nil, serrors.New("DRKey is not available") - } - meta, err := requestToHostHostMeta(req) - if err != nil { - return nil, serrors.Wrap("parsing protobuf HostHostReq", err) - } - lvl2Key, err := s.DRKeyClient.GetHostHostKey(ctx, meta) - if err != nil { - return nil, serrors.Wrap("getting Host-Host from client store", err) - } - - return &daemon.DRKeyHostHostResponse{ - EpochBegin: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotBefore.Unix()}, - EpochEnd: ×tamppb.Timestamp{Seconds: lvl2Key.Epoch.NotAfter.Unix()}, - Key: lvl2Key.Key[:], - }, nil -} - -func requestToASHostMeta(req *daemon.DRKeyASHostRequest) (drkey.ASHostMeta, error) { - err := req.ValTime.CheckValid() - if err != nil { - return drkey.ASHostMeta{}, serrors.Wrap("invalid valTime from pb request", err) - } - return drkey.ASHostMeta{ - ProtoId: drkey.Protocol(req.ProtocolId), - Validity: req.ValTime.AsTime(), - SrcIA: addr.IA(req.SrcIa), - DstIA: addr.IA(req.DstIa), - DstHost: req.DstHost, - }, nil -} - -func requestToHostASMeta(req *daemon.DRKeyHostASRequest) (drkey.HostASMeta, error) { - err := req.ValTime.CheckValid() - if err != nil { - return drkey.HostASMeta{}, serrors.Wrap("invalid valTime from pb request", err) - } - return drkey.HostASMeta{ - ProtoId: drkey.Protocol(req.ProtocolId), - Validity: req.ValTime.AsTime(), - SrcIA: addr.IA(req.SrcIa), - DstIA: addr.IA(req.DstIa), - SrcHost: req.SrcHost, - }, nil -} - -func requestToHostHostMeta(req *daemon.DRKeyHostHostRequest) (drkey.HostHostMeta, error) { - err := req.ValTime.CheckValid() - if err != nil { - return drkey.HostHostMeta{}, serrors.Wrap("invalid valTime from pb request", err) - } - return drkey.HostHostMeta{ - ProtoId: drkey.Protocol(req.ProtocolId), - Validity: req.ValTime.AsTime(), - SrcIA: addr.IA(req.SrcIa), - DstIA: addr.IA(req.DstIa), - SrcHost: req.SrcHost, - DstHost: req.DstHost, - }, nil -} diff --git a/demo/drkey/main.go b/demo/drkey/main.go index 51b3767356..62e046d8b7 100644 --- a/demo/drkey/main.go +++ b/demo/drkey/main.go @@ -100,7 +100,7 @@ func realMain() int { DstHost: clientAddr.Host.IP.String(), } - daemon, err := daemon.NewService(scionEnv.Daemon()).Connect(ctx) + daemon, err := daemon.NewAutoConnector(ctx, daemon.WithDaemon(scionEnv.Daemon())) if err != nil { fmt.Fprintln(os.Stderr, "Error dialing SCION Daemon:", err) return 1 diff --git a/demo/stun/test-client/BUILD.bazel b/demo/stun/test-client/BUILD.bazel index ecf9e06c6f..5a8b109565 100644 --- a/demo/stun/test-client/BUILD.bazel +++ b/demo/stun/test-client/BUILD.bazel @@ -8,6 +8,7 @@ go_library( deps = [ "//pkg/addr:go_default_library", "//pkg/daemon:go_default_library", + "//pkg/daemon/types:go_default_library", "//pkg/snet:go_default_library", "@com_tailscale//net/stun:go_default_library", ], diff --git a/demo/stun/test-client/main.go b/demo/stun/test-client/main.go index f5ed0fea68..44ceadaac2 100644 --- a/demo/stun/test-client/main.go +++ b/demo/stun/test-client/main.go @@ -24,6 +24,7 @@ import ( "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/daemon" + daemontypes "github.com/scionproto/scion/pkg/daemon/types" "github.com/scionproto/scion/pkg/snet" "tailscale.com/net/stun" @@ -53,7 +54,7 @@ func main() { log.Fatalf("Failed to create SCION daemon connector: %v", err) } - ps, err := dc.Paths(ctx, remoteAddr.IA, localAddr.IA, daemon.PathReqFlags{Refresh: true}) + ps, err := dc.Paths(ctx, remoteAddr.IA, localAddr.IA, daemontypes.PathReqFlags{Refresh: true}) if err != nil { log.Fatalf("Failed to lookup paths: %v", err) } diff --git a/doc/command/scion-pki/scion-pki_certificate_renew.rst b/doc/command/scion-pki/scion-pki_certificate_renew.rst index a9c57ea899..b56eb885d3 100644 --- a/doc/command/scion-pki/scion-pki_certificate_renew.rst +++ b/doc/command/scion-pki/scion-pki_certificate_renew.rst @@ -119,6 +119,9 @@ Options The CAs are tried in order until success or all of them failed. --ca is mutually exclusive with --remote --common-name string The common name that replaces the common name in the subject template + --config-dir string Directory containing topology.json and certs/ for standalone mode. + If both --sciond and --config-dir are set, --sciond takes priority. + Defaults to /etc/scion on Linux. --curve string The elliptic curve to use (P-256|P-384|P-521) (default "P-256") --expires-in string Remaining time threshold for renewal --features strings enable development features () @@ -140,7 +143,9 @@ Options and all specified remotes are tried in order until success or all of them failed. --remote is mutually exclusive with --ca. --reuse-key Reuse the provided private key instead of creating a fresh private key - --sciond string SCION Daemon address. (default "127.0.0.1:30255") + --sciond string Connect to SCION Daemon at the specified address instead of using + the local topology.json (IP:Port or "default" for 127.0.0.1:30255). + If both --sciond and --config-dir are set, --sciond takes priority. --sequence string Space separated list of hop predicates --subject string The path to the custom subject for the CSR --timeout duration The timeout for the renewal request per CA (default 10s) diff --git a/doc/command/scion/scion_address.rst b/doc/command/scion/scion_address.rst index 0102aeae0f..b76f91af19 100644 --- a/doc/command/scion/scion_address.rst +++ b/doc/command/scion/scion_address.rst @@ -35,11 +35,16 @@ Options :: - -h, --help help for address - --isd-as isd-as The local ISD-AS to use. (default 0-0) - --json Write the output as machine readable json - -l, --local ip Local IP address to listen on. (default invalid IP) - --sciond string SCION Daemon address. (default "127.0.0.1:30255") + --config-dir string Directory containing topology.json and certs/ for standalone mode. + If both --sciond and --config-dir are set, --sciond takes priority. + Defaults to /etc/scion on Linux. + -h, --help help for address + --isd-as isd-as The local ISD-AS to use. (default 0-0) + --json Write the output as machine readable json + -l, --local ip Local IP address to listen on. (default invalid IP) + --sciond string Connect to SCION Daemon at the specified address instead of using + the local topology.json (IP:Port or "default" for 127.0.0.1:30255). + If both --sciond and --config-dir are set, --sciond takes priority. SEE ALSO ~~~~~~~~ diff --git a/doc/command/scion/scion_ping.rst b/doc/command/scion/scion_ping.rst index 313d8b24cc..6800840c8b 100644 --- a/doc/command/scion/scion_ping.rst +++ b/doc/command/scion/scion_ping.rst @@ -86,6 +86,9 @@ Options :: + --config-dir string Directory containing topology.json and certs/ for standalone mode. + If both --sciond and --config-dir are set, --sciond takes priority. + Defaults to /etc/scion on Linux. -c, --count uint16 total number of packets to send --epic Enable EPIC for path probing. --format string Specify the output format (human|json|yaml) (default "human") @@ -107,7 +110,9 @@ Options the total size of the packet is still variable size due to the variable size of the SCION path. --refresh set refresh flag for path request - --sciond string SCION Daemon address. (default "127.0.0.1:30255") + --sciond string Connect to SCION Daemon at the specified address instead of using + the local topology.json (IP:Port or "default" for 127.0.0.1:30255). + If both --sciond and --config-dir are set, --sciond takes priority. --sequence string Space separated list of hop predicates --timeout duration timeout per packet (default 1s) --tracing.agent string Tracing agent address diff --git a/doc/command/scion/scion_showpaths.rst b/doc/command/scion/scion_showpaths.rst index 103ddbc389..22753fbad7 100644 --- a/doc/command/scion/scion_showpaths.rst +++ b/doc/command/scion/scion_showpaths.rst @@ -90,6 +90,9 @@ Options :: + --config-dir string Directory containing topology.json and certs/ for standalone mode. + If both --sciond and --config-dir are set, --sciond takes priority. + Defaults to /etc/scion on Linux. --epic Enable EPIC. -e, --extended Show extended path meta data information --format string Specify the output format (human|json|yaml) (default "human") @@ -101,7 +104,9 @@ Options --no-color disable colored output --no-probe Do not probe the paths and print the health status -r, --refresh Set refresh flag for SCION Daemon path request - --sciond string SCION Daemon address. (default "127.0.0.1:30255") + --sciond string Connect to SCION Daemon at the specified address instead of using + the local topology.json (IP:Port or "default" for 127.0.0.1:30255). + If both --sciond and --config-dir are set, --sciond takes priority. --sequence string Space separated list of hop predicates --timeout duration Timeout (default 5s) --tracing.agent string Tracing agent address diff --git a/doc/command/scion/scion_traceroute.rst b/doc/command/scion/scion_traceroute.rst index bae43e5f8b..757b32ef24 100644 --- a/doc/command/scion/scion_traceroute.rst +++ b/doc/command/scion/scion_traceroute.rst @@ -79,6 +79,9 @@ Options :: + --config-dir string Directory containing topology.json and certs/ for standalone mode. + If both --sciond and --config-dir are set, --sciond takes priority. + Defaults to /etc/scion on Linux. --epic Enable EPIC. --format string Specify the output format (human|json|yaml) (default "human") -h, --help help for traceroute @@ -88,7 +91,9 @@ Options --log.level string Console logging level verbosity (debug|info|error) --no-color disable colored output --refresh set refresh flag for path request - --sciond string SCION Daemon address. (default "127.0.0.1:30255") + --sciond string Connect to SCION Daemon at the specified address instead of using + the local topology.json (IP:Port or "default" for 127.0.0.1:30255). + If both --sciond and --config-dir are set, --sciond takes priority. --sequence string Space separated list of hop predicates --timeout duration timeout per packet (default 1s) --tracing.agent string Tracing agent address diff --git a/pkg/daemon/BUILD.bazel b/pkg/daemon/BUILD.bazel index 3377fca1b9..094910fc22 100644 --- a/pkg/daemon/BUILD.bazel +++ b/pkg/daemon/BUILD.bazel @@ -5,16 +5,24 @@ go_library( name = "go_default_library", srcs = [ "apitypes.go", - "daemon.go", + "auto.go", + "connector.go", + "doc.go", "grpc.go", - "metrics.go", + "grpc_metrics.go", + "standalone.go", "topology.go", ], importpath = "github.com/scionproto/scion/pkg/daemon", visibility = ["//visibility:public"], deps = [ "//pkg/addr:go_default_library", - "//pkg/daemon/internal/metrics:go_default_library", + "//pkg/daemon/asinfo:go_default_library", + "//pkg/daemon/fetcher:go_default_library", + "//pkg/daemon/private/engine:go_default_library", + "//pkg/daemon/private/standalone:go_default_library", + "//pkg/daemon/private/trust:go_default_library", + "//pkg/daemon/types:go_default_library", "//pkg/drkey:go_default_library", "//pkg/grpc:go_default_library", "//pkg/log:go_default_library", @@ -27,9 +35,23 @@ go_library( "//pkg/segment/iface:go_default_library", "//pkg/snet:go_default_library", "//pkg/snet/path:go_default_library", + "//private/pathdb:go_default_library", + "//private/periodic:go_default_library", + "//private/revcache:go_default_library", + "//private/segment/segfetcher:go_default_library", + "//private/segment/segfetcher/grpc:go_default_library", + "//private/segment/verifier:go_default_library", + "//private/storage:go_default_library", + "//private/storage/trust/metrics:go_default_library", "//private/topology:go_default_library", + "//private/trust:go_default_library", + "//private/trust/compat:go_default_library", + "//private/trust/metrics:go_default_library", + "@com_github_patrickmn_go_cache//:go_default_library", + "@com_github_prometheus_client_golang//prometheus:go_default_library", "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//credentials/insecure:go_default_library", + "@org_golang_google_grpc//resolver:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", ], diff --git a/pkg/daemon/apitypes.go b/pkg/daemon/apitypes.go index c3c66a005c..3299afb16b 100644 --- a/pkg/daemon/apitypes.go +++ b/pkg/daemon/apitypes.go @@ -20,30 +20,20 @@ import ( "net" "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon/types" "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/snet" "github.com/scionproto/scion/private/topology" ) -type PathReqFlags struct { - Refresh bool - Hidden bool -} - -// ASInfo provides information about the local AS. -type ASInfo struct { - IA addr.IA - MTU uint16 -} - type Querier struct { Connector Connector IA addr.IA } func (q Querier) Query(ctx context.Context, dst addr.IA) ([]snet.Path, error) { - paths, err := q.Connector.Paths(ctx, dst, q.IA, PathReqFlags{}) + paths, err := q.Connector.Paths(ctx, dst, q.IA, types.PathReqFlags{}) if err != nil { return paths, serrors.Wrap("querying paths", err, "local_isd_as", q.IA) } diff --git a/pkg/daemon/asinfo/BUILD.bazel b/pkg/daemon/asinfo/BUILD.bazel new file mode 100644 index 0000000000..b93736c3c9 --- /dev/null +++ b/pkg/daemon/asinfo/BUILD.bazel @@ -0,0 +1,15 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["asinfo.go"], + importpath = "github.com/scionproto/scion/pkg/daemon/asinfo", + visibility = ["//visibility:public"], + deps = [ + "//pkg/addr:go_default_library", + "//pkg/metrics:go_default_library", + "//pkg/private/prom:go_default_library", + "//pkg/private/serrors:go_default_library", + "//private/topology:go_default_library", + ], +) diff --git a/pkg/daemon/asinfo/asinfo.go b/pkg/daemon/asinfo/asinfo.go new file mode 100644 index 0000000000..cdfd8fa274 --- /dev/null +++ b/pkg/daemon/asinfo/asinfo.go @@ -0,0 +1,85 @@ +// Copyright 2019 Anapaya Systems +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asinfo + +import ( + "net" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/metrics" + "github.com/scionproto/scion/pkg/private/prom" + "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/private/topology" +) + +// LocalASInfo provides control plane information for the daemon engine. +type LocalASInfo interface { + // IA returns the local ISD-AS number. + IA() addr.IA + // MTU returns the MTU of the local AS. + MTU() uint16 + // Core returns whether the local AS is core. + Core() bool + // IfIDs InterfaceIDs returns all interface IDS from the local AS. + IfIDs() []uint16 + // UnderlayNextHop returns the internal underlay address of the router + // containing the interface ID. + UnderlayNextHop(uint16) *net.UDPAddr + // ControlServiceAddresses returns the addresses of the control services + ControlServiceAddresses() []*net.UDPAddr + // PortRange returns the first and last ports of the port range (both included), + // in which endhost listen for SCION/UDP application using the UDP/IP underlay. + PortRange() (uint16, uint16) +} + +// LoadFromTopoFile loads a control plane info from a file. +// The returned LocalASInfo can be passed to NewStandaloneConnector. +func LoadFromTopoFile(topoFile string) (LocalASInfo, error) { + loader, err := topology.NewLoader( + topology.LoaderCfg{ + File: topoFile, + Reload: nil, + Validator: &topology.DefaultValidator{}, + Metrics: newLoaderMetrics(), + }, + ) + if err != nil { + return nil, serrors.Wrap("creating topology loader", err) + } + return loader, nil +} + +// newLoaderMetrics creates metrics for the topology loader. +func newLoaderMetrics() topology.LoaderMetrics { + updates := prom.NewCounterVec( + "", "", + "topology_updates_total", + "The total number of updates.", + []string{prom.LabelResult}, + ) + return topology.LoaderMetrics{ + ValidationErrors: metrics.NewPromCounter(updates).With(prom.LabelResult, "err_validate"), + ReadErrors: metrics.NewPromCounter(updates).With(prom.LabelResult, "err_read"), + LastUpdate: metrics.NewPromGauge( + prom.NewGaugeVec( + "", "", + "topology_last_update_time", + "Timestamp of the last successful update.", + []string{}, + ), + ), + Updates: metrics.NewPromCounter(updates).With(prom.LabelResult, prom.Success), + } +} diff --git a/pkg/daemon/auto.go b/pkg/daemon/auto.go new file mode 100644 index 0000000000..8277c14561 --- /dev/null +++ b/pkg/daemon/auto.go @@ -0,0 +1,105 @@ +// Copyright 2025 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 daemon + +import ( + "context" + "net" + "path/filepath" + "time" + + "github.com/scionproto/scion/pkg/private/serrors" +) + +// AutoConnectorOption is a functional option for NewAutoConnector and +// overrides the default options. +type AutoConnectorOption func(*autoConnectorOptions) + +type autoConnectorOptions struct { + sciond string + configDir string +} + +// WithDaemon sets the daemon address for a gRPC connector. +// When set, the connector will connect to the specified daemon via gRPC. +// If both WithDaemon and WithConfigDir are set, WithDaemon takes priority. +func WithDaemon(addr string) AutoConnectorOption { + return func(o *autoConnectorOptions) { + o.sciond = addr + } +} + +// WithConfigDir sets the configuration directory for standalone mode. +// The directory should contain topology.json and a certs/ subdirectory. +// If both WithDaemon and WithConfigDir are set, WithDaemon takes priority. +func WithConfigDir(dir string) AutoConnectorOption { + return func(o *autoConnectorOptions) { + o.configDir = dir + } +} + +// NewAutoConnector creates a new Connector based on supplied options. +// +// Priority order: +// 1. If WithDaemon was supplied, return a gRPC connector to the specified daemon. +// 2. If WithConfigDir was supplied, use standalone mode with the specified directory. +// 3. Return error if neither option was provided. +// +// Note: In standalone mode, topology information is loaded once and never reloaded. +// For dynamic updates, use [NewStandaloneConnector] with a custom [LocalASInfo]. +func NewAutoConnector(ctx context.Context, opts ...AutoConnectorOption) (Connector, error) { + options := &autoConnectorOptions{} + for _, opt := range opts { + opt(options) + } + + // Priority 1: Use provided daemon address + if options.sciond != "" { + if !isReachable(options.sciond, defaultConnectionTimeout) { + return nil, serrors.New("daemon not reachable", "address", options.sciond) + } + ctx, cancel := context.WithTimeout(ctx, defaultConnectionTimeout) + defer cancel() + return NewService(options.sciond).Connect(ctx) + } + + // Priority 2: Use provided config directory for standalone mode + if options.configDir != "" { + topoFile := filepath.Join(options.configDir, "topology.json") + certsDir := filepath.Join(options.configDir, "certs") + localASInfo, err := LoadASInfoFromFile(topoFile) + if err != nil { + return nil, serrors.Wrap("loading topology from file", err, + "topology_file", topoFile) + } + return NewStandaloneConnector(ctx, localASInfo, WithCertsDir(certsDir)) + } + + // TODO(emairoll): Include bootstrapping functionality + + return nil, serrors.New( + "no suitable daemon connection method found: " + + "either WithDaemon or WithConfigDir must be specified", + ) +} + +func isReachable(addr string, timeout time.Duration) bool { + conn, err := net.DialTimeout("tcp", addr, timeout) + if err != nil { + return false + } + _ = conn.Close() + return true +} diff --git a/pkg/daemon/daemon.go b/pkg/daemon/connector.go similarity index 66% rename from pkg/daemon/daemon.go rename to pkg/daemon/connector.go index c6d41eda4b..a54543a218 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/connector.go @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package daemon provides APIs for querying SCION Daemons. package daemon import ( @@ -21,43 +20,12 @@ import ( "net/netip" "github.com/scionproto/scion/pkg/addr" - "github.com/scionproto/scion/pkg/daemon/internal/metrics" + "github.com/scionproto/scion/pkg/daemon/types" "github.com/scionproto/scion/pkg/drkey" - libmetrics "github.com/scionproto/scion/pkg/metrics" "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt" - "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/snet" ) -// Errors for SCION Daemon API requests -var ( - ErrUnableToConnect = serrors.New("unable to connect to the SCION Daemon") -) - -const ( - // DefaultAPIAddress contains the system default for a daemon API socket. - DefaultAPIAddress = "127.0.0.1:30255" - // DefaultAPIPort contains the default port for a daemon client API socket. - DefaultAPIPort = 30255 -) - -// NewService returns a SCION Daemon API connection factory. -// Deprecated: Use Service struct directly instead. -func NewService(name string) Service { - return Service{ - Address: name, - Metrics: Metrics{ - Connects: libmetrics.NewPromCounter(metrics.Conns.CounterVec()), - PathsRequests: libmetrics.NewPromCounter( - metrics.PathRequests.CounterVec()), - ASRequests: libmetrics.NewPromCounter(metrics.ASInfos.CounterVec()), - InterfacesRequests: libmetrics.NewPromCounter(metrics.IFInfos.CounterVec()), - ServicesRequests: libmetrics.NewPromCounter(metrics.SVCInfos.CounterVec()), - InterfaceDownNotifications: libmetrics.NewPromCounter(metrics.Revocations.CounterVec()), - }, - } -} - // A Connector is used to query the SCION daemon. All connector methods block until // either an error occurs, or the method successfully returns. type Connector interface { @@ -70,10 +38,10 @@ type Connector interface { // Interfaces returns the map of interface identifiers to the underlay internal address. Interfaces(ctx context.Context) (map[uint16]netip.AddrPort, error) // Paths requests from the daemon a set of end to end paths between the source and destination. - Paths(ctx context.Context, dst, src addr.IA, f PathReqFlags) ([]snet.Path, error) + Paths(ctx context.Context, dst, src addr.IA, f types.PathReqFlags) ([]snet.Path, error) // ASInfo requests from the daemon information about AS ia, the zero IA can be // used to detect the local IA. - ASInfo(ctx context.Context, ia addr.IA) (ASInfo, error) + ASInfo(ctx context.Context, ia addr.IA) (types.ASInfo, error) // SVCInfo requests from the daemon information about addresses and ports of // infrastructure services. Slice svcTypes contains a list of desired // service types. If unset, a fresh (i.e., uncached) answer containing all diff --git a/pkg/daemon/doc.go b/pkg/daemon/doc.go new file mode 100644 index 0000000000..eaa7891d09 --- /dev/null +++ b/pkg/daemon/doc.go @@ -0,0 +1,133 @@ +// Copyright 2026 ETH Zurich +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package daemon provides APIs for SCION applications to interact with the +SCION control plane. It supports two modes of operation: + + - Standalone mode: Runs daemon functionality in-process, communicating + directly with the control service. No separate daemon process required. + - Remote mode: Connects to a SCION daemon process via gRPC. + +# Quick Start + +Use NewAutoConnector with WithDaemon (for remote daemon) or WithConfigDir +(for standalone mode): + + // Remote daemon connection + conn, err := daemon.NewAutoConnector(ctx, daemon.WithDaemon("127.0.0.1:30255")) + + // Or standalone mode with local config + conn, err := daemon.NewAutoConnector(ctx, daemon.WithConfigDir("/etc/scion")) + + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + // Query paths to a destination + paths, err := conn.Paths(ctx, dstIA, srcIA, daemon.PathReqFlags{}) + +If both WithDaemon and WithConfigDir are set, WithDaemon takes priority. +Empty string options are ignored, making it safe to pass values from CLI flags: + + conn, err := daemon.NewAutoConnector(ctx, + daemon.WithDaemon(daemonAddr), // may be empty + daemon.WithConfigDir(configDir), // may be empty + ) + +# Standalone Mode + +Standalone mode runs the daemon logic in-process, which is useful for: + - Deployments without a separate daemon process + - CLI tools that need minimal dependencies + - Testing and development + +For more control over standalone mode, use NewStandaloneConnector directly: + + // Load topology information + localASInfo, err := daemon.LoadASInfoFromFile("/etc/scion/topology.json") + if err != nil { + log.Fatal(err) + } + + // Create standalone connector with options + conn, err := daemon.NewStandaloneConnector(ctx, localASInfo, + daemon.WithCertsDir("/etc/scion/certs"), // TRC certificates location + daemon.WithMetrics(), // Enable Prometheus metrics + daemon.WithPeriodicCleanup(), // Enable path DB cleanup + ) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + +Standalone mode requires: + - topology.json: Network topology file with control service addresses + - certs/: Directory containing TRC files for segment verification (required via WithCertsDir) + +To disable segment verification (NOT recommended for production): + + conn, err := daemon.NewStandaloneConnector(ctx, localASInfo, + daemon.WithDisabledSegVerification(), + ) + +# Remote Mode + +Remote mode connects to a running SCION daemon via gRPC. Use NewService to +create a connection factory: + + svc := daemon.NewService("127.0.0.1:30255") + conn, err := svc.Connect(ctx) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + +# The Connector Interface + +The Connector interface is the central abstraction of this package. All connection +modes (standalone, remote) implement this interface, allowing applications to work +with any backend transparently. + +See [Connector] for all available methods. + +# Loading Topology + +To get topology information (local IA, port range, interfaces) from a connector: + + // One-time load + topo, err := daemon.LoadTopology(ctx, conn) + + // Auto-reloading topology (for long-running applications) + reloadingTopo, err := daemon.NewReloadingTopology(ctx, conn) + go reloadingTopo.Run(ctx, 30*time.Second) + topo := reloadingTopo.Topology() + +# Helper Types + +The package provides helper types for common patterns: + + // Querier wraps a Connector for path queries + querier := daemon.Querier{Connector: conn, IA: localIA} + paths, err := querier.Query(ctx, dstIA) + + // RevHandler adapts Connector for snet.RevocationHandler + revHandler := daemon.RevHandler{Connector: conn} + + // TopoQuerier provides topology queries + topoQuerier := daemon.TopoQuerier{Connector: conn} + addr, err := topoQuerier.UnderlayAnycast(ctx, addr.SvcCS) +*/ +package daemon diff --git a/daemon/fetcher/BUILD.bazel b/pkg/daemon/fetcher/BUILD.bazel similarity index 85% rename from daemon/fetcher/BUILD.bazel rename to pkg/daemon/fetcher/BUILD.bazel index ff080f4d6a..62e86ef8eb 100644 --- a/daemon/fetcher/BUILD.bazel +++ b/pkg/daemon/fetcher/BUILD.bazel @@ -3,10 +3,9 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = ["fetcher.go"], - importpath = "github.com/scionproto/scion/daemon/fetcher", + importpath = "github.com/scionproto/scion/pkg/daemon/fetcher", visibility = ["//visibility:public"], deps = [ - "//daemon/config:go_default_library", "//pkg/addr:go_default_library", "//pkg/private/serrors:go_default_library", "//pkg/snet:go_default_library", diff --git a/daemon/fetcher/fetcher.go b/pkg/daemon/fetcher/fetcher.go similarity index 92% rename from daemon/fetcher/fetcher.go rename to pkg/daemon/fetcher/fetcher.go index 63206b0995..d558517177 100644 --- a/daemon/fetcher/fetcher.go +++ b/pkg/daemon/fetcher/fetcher.go @@ -21,7 +21,6 @@ import ( "net" "time" - "github.com/scionproto/scion/daemon/config" "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/snet" @@ -33,10 +32,6 @@ import ( "github.com/scionproto/scion/private/trust" ) -const ( - DefaultMinWorkerLifetime = 10 * time.Second -) - type TrustStore interface { trust.Inspector } @@ -47,7 +42,6 @@ type Fetcher interface { type fetcher struct { pather segfetcher.Pather - config config.SDConfig } type FetcherConfig struct { @@ -62,9 +56,9 @@ type FetcherConfig struct { PathDB pathdb.DB Inspector trust.Inspector - Verifier infra.Verifier - RevCache revcache.RevCache - Cfg config.SDConfig + Verifier infra.Verifier + RevCache revcache.RevCache + QueryInterval time.Duration } func NewFetcher(cfg FetcherConfig) Fetcher { @@ -75,7 +69,7 @@ func NewFetcher(cfg FetcherConfig) Fetcher { NextHopper: cfg.NextHopper, RevCache: cfg.RevCache, Fetcher: &segfetcher.Fetcher{ - QueryInterval: cfg.Cfg.QueryInterval.Duration, + QueryInterval: cfg.QueryInterval, PathDB: cfg.PathDB, Resolver: segfetcher.NewResolver( cfg.PathDB, @@ -101,7 +95,6 @@ func NewFetcher(cfg FetcherConfig) Fetcher { Inspector: cfg.Inspector, }, }, - config: cfg.Cfg, } } diff --git a/daemon/fetcher/mock_fetcher/BUILD.bazel b/pkg/daemon/fetcher/mock_fetcher/BUILD.bazel similarity index 76% rename from daemon/fetcher/mock_fetcher/BUILD.bazel rename to pkg/daemon/fetcher/mock_fetcher/BUILD.bazel index f87ba9414e..ea2bf02cb5 100644 --- a/daemon/fetcher/mock_fetcher/BUILD.bazel +++ b/pkg/daemon/fetcher/mock_fetcher/BUILD.bazel @@ -4,14 +4,14 @@ gomock( name = "go_default_mock", out = "mock.go", interfaces = ["Fetcher"], - library = "//daemon/fetcher:go_default_library", + library = "//pkg/daemon/fetcher:go_default_library", package = "mock_fetcher", ) go_library( name = "go_default_library", srcs = ["mock.go"], - importpath = "github.com/scionproto/scion/daemon/fetcher/mock_fetcher", + importpath = "github.com/scionproto/scion/pkg/daemon/fetcher/mock_fetcher", visibility = ["//visibility:public"], deps = [ "//pkg/addr:go_default_library", diff --git a/daemon/fetcher/mock_fetcher/mock.go b/pkg/daemon/fetcher/mock_fetcher/mock.go similarity index 95% rename from daemon/fetcher/mock_fetcher/mock.go rename to pkg/daemon/fetcher/mock_fetcher/mock.go index 6ef59091ee..999d583b57 100644 --- a/daemon/fetcher/mock_fetcher/mock.go +++ b/pkg/daemon/fetcher/mock_fetcher/mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/scionproto/scion/daemon/fetcher (interfaces: Fetcher) +// Source: github.com/scionproto/scion/pkg/daemon/fetcher (interfaces: Fetcher) // Package mock_fetcher is a generated GoMock package. package mock_fetcher diff --git a/pkg/daemon/grpc.go b/pkg/daemon/grpc.go index ce28397d41..a43d7aa742 100644 --- a/pkg/daemon/grpc.go +++ b/pkg/daemon/grpc.go @@ -26,9 +26,12 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/scionproto/scion/pkg/addr" + daemontypes "github.com/scionproto/scion/pkg/daemon/types" "github.com/scionproto/scion/pkg/drkey" libgrpc "github.com/scionproto/scion/pkg/grpc" + libmetrics "github.com/scionproto/scion/pkg/metrics" "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt" + "github.com/scionproto/scion/pkg/private/prom" "github.com/scionproto/scion/pkg/private/serrors" sdpb "github.com/scionproto/scion/pkg/proto/daemon" dkpb "github.com/scionproto/scion/pkg/proto/drkey" @@ -38,6 +41,66 @@ import ( "github.com/scionproto/scion/private/topology" ) +const ( + // DefaultAPIAddress contains the system default for a daemon API socket. + DefaultAPIAddress = "127.0.0.1:30255" + // DefaultAPIPort contains the default port for a daemon client API socket. + DefaultAPIPort = 30255 + // defaultConnectionTimeout contains the default timeout for a daemon connection. + defaultConnectionTimeout = time.Second +) + +// NewService returns a SCION Daemon API connection factory. +func NewService(name string) Service { + return Service{ + Address: name, + Metrics: Metrics{ + Connects: libmetrics.NewPromCounter( + prom.NewCounterVecWithLabels( + "lib_sciond", "conn", "connections_total", + "The amount of SCIOND connection attempts.", promLabels{}, + ), + ), + PathsRequests: libmetrics.NewPromCounter( + prom.NewCounterVecWithLabels( + "lib_sciond", "path", "requests_total", + "The amount of Path requests sent.", promLabels{}, + ), + ), + ASRequests: libmetrics.NewPromCounter( + prom.NewCounterVecWithLabels( + "lib_sciond", "as_info", "requests_total", + "The amount of AS info requests sent.", promLabels{}, + ), + ), + InterfacesRequests: libmetrics.NewPromCounter( + prom.NewCounterVecWithLabels( + "lib_sciond", "if_info", "requests_total", + "The amount of IF info requests sent.", promLabels{}, + ), + ), + ServicesRequests: libmetrics.NewPromCounter( + prom.NewCounterVecWithLabels( + "lib_sciond", "service_info", "requests_total", + "The amount of SVC info requests sent.", promLabels{}, + ), + ), + InterfaceDownNotifications: libmetrics.NewPromCounter( + prom.NewCounterVecWithLabels( + "lib_sciond", "revocation", "requests_total", + "The amount of Revocation requests sent.", promLabels{}, + ), + ), + }, + } +} + +// promLabels implements prom.Labels for result label. +type promLabels struct{} + +func (promLabels) Labels() []string { return []string{prom.LabelResult} } +func (promLabels) Values() []string { return []string{""} } + // Service exposes the API to connect to a SCION daemon service. type Service struct { // Address is the address of the SCION daemon to connect to. @@ -47,7 +110,7 @@ type Service struct { Metrics Metrics } -func (s Service) Connect(ctx context.Context) (Connector, error) { +func (s Service) Connect(_ context.Context) (Connector, error) { conn, err := grpc.NewClient(s.Address, grpc.WithTransportCredentials(insecure.NewCredentials()), libgrpc.UnaryClientInterceptor(), @@ -105,7 +168,7 @@ func (c grpcConn) Interfaces(ctx context.Context) (map[uint16]netip.AddrPort, er } func (c grpcConn) Paths(ctx context.Context, dst, src addr.IA, - f PathReqFlags) ([]snet.Path, error) { + f daemontypes.PathReqFlags) ([]snet.Path, error) { client := sdpb.NewDaemonServiceClient(c.conn) response, err := client.Paths(ctx, &sdpb.PathsRequest{ @@ -123,15 +186,15 @@ func (c grpcConn) Paths(ctx context.Context, dst, src addr.IA, return paths, err } -func (c grpcConn) ASInfo(ctx context.Context, ia addr.IA) (ASInfo, error) { +func (c grpcConn) ASInfo(ctx context.Context, ia addr.IA) (daemontypes.ASInfo, error) { client := sdpb.NewDaemonServiceClient(c.conn) response, err := client.AS(ctx, &sdpb.ASRequest{IsdAs: uint64(ia)}) if err != nil { c.metrics.incAS(err) - return ASInfo{}, err + return daemontypes.ASInfo{}, err } c.metrics.incAS(nil) - return ASInfo{ + return daemontypes.ASInfo{ IA: addr.IA(response.IsdAs), MTU: uint16(response.Mtu), }, nil diff --git a/pkg/daemon/metrics.go b/pkg/daemon/grpc_metrics.go similarity index 100% rename from pkg/daemon/metrics.go rename to pkg/daemon/grpc_metrics.go diff --git a/pkg/daemon/internal/metrics/BUILD.bazel b/pkg/daemon/internal/metrics/BUILD.bazel deleted file mode 100644 index 40203989fa..0000000000 --- a/pkg/daemon/internal/metrics/BUILD.bazel +++ /dev/null @@ -1,12 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library") - -go_library( - name = "go_default_library", - srcs = ["metrics.go"], - importpath = "github.com/scionproto/scion/pkg/daemon/internal/metrics", - visibility = ["//pkg/daemon:__subpackages__"], - deps = [ - "//pkg/private/prom:go_default_library", - "@com_github_prometheus_client_golang//prometheus:go_default_library", - ], -) diff --git a/pkg/daemon/internal/metrics/metrics.go b/pkg/daemon/internal/metrics/metrics.go deleted file mode 100644 index 5f28d475df..0000000000 --- a/pkg/daemon/internal/metrics/metrics.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2019 ETH Zurich -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package metrics - -import ( - "github.com/prometheus/client_golang/prometheus" - - "github.com/scionproto/scion/pkg/private/prom" -) - -const ( - // Namespace is the metrics namespace for the SCIOND client API. - Namespace = "lib_sciond" - - subsystemConn = "conn" - subsystemPath = "path" - subsystemASInfo = "as_info" - subsystemIFInfo = "if_info" - subsystemSVCInfo = "service_info" - subsystemRevocation = "revocation" -) - -// Result values -const ( - OkSuccess = prom.Success - ErrTimeout = prom.ErrTimeout - ErrNotClassified = prom.ErrNotClassified -) - -type resultLabel struct { - Result string -} - -// Labels returns the labels. -func (l resultLabel) Labels() []string { - return []string{prom.LabelResult} -} - -// Values returns the values for the labels. -func (l resultLabel) Values() []string { - return []string{l.Result} -} - -// Metric accessors. -var ( - // PathRequests contains metrics for path requests. - PathRequests = newPathRequest() - // Revocations contains metrics for revocations. - Revocations = newRevocation() - // ASInfos contains metrics for AS info requests. - ASInfos = newASInfoRequest() - // IFInfos contains metrics for IF info requests. - IFInfos = newIFInfo() - // SVCInfos contains metrics for SVC info requests. - SVCInfos = newSVCInfo() - // Conns contains metrics for connections to SCIOND. - Conns = newConn() -) - -// Request is the generic metric for requests. -type Request struct { - count *prometheus.CounterVec -} - -func (r *Request) CounterVec() *prometheus.CounterVec { - return r.count -} - -// Inc increases the metric count. The result parameter is used to label the increment. -func (r Request) Inc(result string) { - r.count.WithLabelValues(result).Inc() -} - -func newConn() Request { - return Request{ - count: prom.NewCounterVecWithLabels(Namespace, subsystemConn, "connections_total", - "The amount of SCIOND connection attempts.", resultLabel{}), - } -} - -func newPathRequest() Request { - return Request{ - count: prom.NewCounterVecWithLabels(Namespace, subsystemPath, "requests_total", - "The amount of Path requests sent.", resultLabel{}), - } -} - -func newRevocation() Request { - return Request{ - count: prom.NewCounterVecWithLabels(Namespace, subsystemRevocation, "requests_total", - "The amount of Revocation requests sent.", resultLabel{}), - } -} - -func newASInfoRequest() Request { - return Request{ - count: prom.NewCounterVecWithLabels(Namespace, subsystemASInfo, "requests_total", - "The amount of AS info requests sent.", resultLabel{}), - } -} - -func newSVCInfo() Request { - return Request{ - count: prom.NewCounterVecWithLabels(Namespace, subsystemSVCInfo, "requests_total", - "The amount of SVC info requests sent.", resultLabel{}), - } -} - -func newIFInfo() Request { - return Request{ - count: prom.NewCounterVecWithLabels(Namespace, subsystemIFInfo, "requests_total", - "The amount of IF info requests sent.", resultLabel{}), - } -} diff --git a/pkg/daemon/mock_daemon/BUILD.bazel b/pkg/daemon/mock_daemon/BUILD.bazel index d28cdfe2f2..1a7669964b 100644 --- a/pkg/daemon/mock_daemon/BUILD.bazel +++ b/pkg/daemon/mock_daemon/BUILD.bazel @@ -15,7 +15,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/addr:go_default_library", - "//pkg/daemon:go_default_library", + "//pkg/daemon/types:go_default_library", "//pkg/drkey:go_default_library", "//pkg/private/ctrl/path_mgmt:go_default_library", "//pkg/snet:go_default_library", diff --git a/pkg/daemon/mock_daemon/mock.go b/pkg/daemon/mock_daemon/mock.go index 05a868518c..4ae9db9e38 100644 --- a/pkg/daemon/mock_daemon/mock.go +++ b/pkg/daemon/mock_daemon/mock.go @@ -11,7 +11,7 @@ import ( gomock "github.com/golang/mock/gomock" addr "github.com/scionproto/scion/pkg/addr" - daemon "github.com/scionproto/scion/pkg/daemon" + types "github.com/scionproto/scion/pkg/daemon/types" drkey "github.com/scionproto/scion/pkg/drkey" path_mgmt "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt" snet "github.com/scionproto/scion/pkg/snet" @@ -41,10 +41,10 @@ func (m *MockConnector) EXPECT() *MockConnectorMockRecorder { } // ASInfo mocks base method. -func (m *MockConnector) ASInfo(arg0 context.Context, arg1 addr.IA) (daemon.ASInfo, error) { +func (m *MockConnector) ASInfo(arg0 context.Context, arg1 addr.IA) (types.ASInfo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ASInfo", arg0, arg1) - ret0, _ := ret[0].(daemon.ASInfo) + ret0, _ := ret[0].(types.ASInfo) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -145,7 +145,7 @@ func (mr *MockConnectorMockRecorder) LocalIA(arg0 interface{}) *gomock.Call { } // Paths mocks base method. -func (m *MockConnector) Paths(arg0 context.Context, arg1, arg2 addr.IA, arg3 daemon.PathReqFlags) ([]snet.Path, error) { +func (m *MockConnector) Paths(arg0 context.Context, arg1, arg2 addr.IA, arg3 types.PathReqFlags) ([]snet.Path, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Paths", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]snet.Path) diff --git a/pkg/daemon/private/engine/BUILD.bazel b/pkg/daemon/private/engine/BUILD.bazel new file mode 100644 index 0000000000..d72174d529 --- /dev/null +++ b/pkg/daemon/private/engine/BUILD.bazel @@ -0,0 +1,28 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["engine.go"], + importpath = "github.com/scionproto/scion/pkg/daemon/private/engine", + visibility = ["//visibility:public"], + deps = [ + "//pkg/addr:go_default_library", + "//pkg/daemon/asinfo:go_default_library", + "//pkg/daemon/fetcher:go_default_library", + "//pkg/daemon/types:go_default_library", + "//pkg/drkey:go_default_library", + "//pkg/log:go_default_library", + "//pkg/private/ctrl/path_mgmt:go_default_library", + "//pkg/private/ctrl/path_mgmt/proto:go_default_library", + "//pkg/private/prom:go_default_library", + "//pkg/private/serrors:go_default_library", + "//pkg/private/util:go_default_library", + "//pkg/segment/iface:go_default_library", + "//pkg/snet:go_default_library", + "//private/drkey:go_default_library", + "//private/revcache:go_default_library", + "//private/trust:go_default_library", + "@com_github_opentracing_opentracing_go//:go_default_library", + "@org_golang_x_sync//singleflight:go_default_library", + ], +) diff --git a/pkg/daemon/private/engine/engine.go b/pkg/daemon/private/engine/engine.go new file mode 100644 index 0000000000..dc6f329c26 --- /dev/null +++ b/pkg/daemon/private/engine/engine.go @@ -0,0 +1,237 @@ +// Copyright 2020 Anapaya Systems +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package engine + +import ( + "context" + "fmt" + "net/netip" + "time" + + "github.com/opentracing/opentracing-go" + "golang.org/x/sync/singleflight" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon/asinfo" + "github.com/scionproto/scion/pkg/daemon/fetcher" + "github.com/scionproto/scion/pkg/daemon/types" + "github.com/scionproto/scion/pkg/drkey" + "github.com/scionproto/scion/pkg/log" + "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt" + "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt/proto" + "github.com/scionproto/scion/pkg/private/prom" + "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/pkg/private/util" + "github.com/scionproto/scion/pkg/segment/iface" + "github.com/scionproto/scion/pkg/snet" + drkey_daemon "github.com/scionproto/scion/private/drkey" + "github.com/scionproto/scion/private/revcache" + "github.com/scionproto/scion/private/trust" +) + +// DaemonEngine contains the core daemon logic, independent of the transport layer. +// It can be used directly by in-process clients or wrapped by the gRPC server. +type DaemonEngine struct { + IA addr.IA + MTU uint16 + LocalASInfo asinfo.LocalASInfo + Fetcher fetcher.Fetcher + RevCache revcache.RevCache + ASInspector trust.Inspector + DRKeyClient *drkey_daemon.ClientEngine + + foregroundPathDedupe singleflight.Group + backgroundPathDedupe singleflight.Group +} + +// LocalIA returns the local ISD-AS number. +func (e *DaemonEngine) LocalIA(_ context.Context) (addr.IA, error) { + return e.IA, nil +} + +// PortRange returns the dispatched port range. +func (e *DaemonEngine) PortRange(_ context.Context) (uint16, uint16, error) { + start, end := e.LocalASInfo.PortRange() + return start, end, nil +} + +// Interfaces returns the map of interface identifiers to the underlay internal address. +func (e *DaemonEngine) Interfaces(_ context.Context) (map[uint16]netip.AddrPort, error) { + result := make(map[uint16]netip.AddrPort) + topo := e.LocalASInfo + for _, ifID := range topo.IfIDs() { + nextHop := topo.UnderlayNextHop(ifID) + if nextHop == nil { + continue + } + result[ifID] = nextHop.AddrPort() + } + return result, nil +} + +// Paths requests a set of end to end paths between the source and destination. +func (e *DaemonEngine) Paths( + ctx context.Context, + dst, src addr.IA, + flags types.PathReqFlags, +) ([]snet.Path, error) { + if _, ok := ctx.Deadline(); !ok { + var cancelF context.CancelFunc + ctx, cancelF = context.WithTimeout(ctx, 10*time.Second) + defer cancelF() + } + go func() { + defer log.HandlePanic() + e.backgroundPaths(ctx, src, dst, flags.Refresh) + }() + paths, err := e.fetchPaths(ctx, &e.foregroundPathDedupe, src, dst, flags.Refresh) + if err != nil { + log.FromCtx(ctx).Debug( + "Fetching paths", "err", err, + "src", src, "dst", dst, "refresh", flags.Refresh, + ) + return nil, err + } + return paths, nil +} + +func (e *DaemonEngine) fetchPaths( + ctx context.Context, + group *singleflight.Group, + src, dst addr.IA, + refresh bool, +) ([]snet.Path, error) { + r, err, _ := group.Do( + fmt.Sprintf("%s%s%t", src, dst, refresh), + func() (any, error) { + return e.Fetcher.GetPaths(ctx, src, dst, refresh) + }, + ) + paths, _ := r.([]snet.Path) + return paths, err +} + +func (e *DaemonEngine) backgroundPaths(origCtx context.Context, src, dst addr.IA, refresh bool) { + backgroundTimeout := 5 * time.Second + deadline, ok := origCtx.Deadline() + if !ok || time.Until(deadline) > backgroundTimeout { + return + } + ctx, cancelF := context.WithTimeout(context.Background(), backgroundTimeout) + defer cancelF() + var spanOpts []opentracing.StartSpanOption + if span := opentracing.SpanFromContext(origCtx); span != nil { + spanOpts = append(spanOpts, opentracing.FollowsFrom(span.Context())) + } + span, ctx := opentracing.StartSpanFromContext(ctx, "fetch.paths.background", spanOpts...) + defer span.Finish() + //nolint:contextcheck + if _, err := e.fetchPaths(ctx, &e.backgroundPathDedupe, src, dst, refresh); err != nil { + log.FromCtx(ctx).Debug( + "Error fetching paths (background)", "err", err, + "src", src, "dst", dst, "refresh", refresh, + ) + } +} + +// ASInfo requests information about an AS. The zero IA returns local AS info. +func (e *DaemonEngine) ASInfo(_ context.Context, ia addr.IA) (types.ASInfo, error) { + reqIA := ia + if reqIA.IsZero() { + reqIA = e.IA + } + mtu := uint16(0) + if reqIA.Equal(e.IA) { + mtu = e.MTU + } + return types.ASInfo{ + IA: reqIA, + MTU: mtu, + }, nil +} + +// SVCInfo requests information about addresses and ports of infrastructure services. +func (e *DaemonEngine) SVCInfo(_ context.Context) ([]string, error) { + var uris []string + for _, h := range e.LocalASInfo.ControlServiceAddresses() { + uris = append(uris, h.String()) + } + return uris, nil +} + +// NotifyInterfaceDown notifies about an interface that is down. +func (e *DaemonEngine) NotifyInterfaceDown(ctx context.Context, ia addr.IA, ifID uint64) error { + revInfo := &path_mgmt.RevInfo{ + RawIsdas: ia, + IfID: iface.ID(ifID), + LinkType: proto.LinkType_core, + RawTTL: 10, + RawTimestamp: util.TimeToSecs(time.Now()), + } + _, err := e.RevCache.Insert(ctx, revInfo) + if err != nil { + log.FromCtx(ctx).Error( + "Inserting revocation", "err", err, + "isd_as", ia, "if_id", ifID, + ) + return metricsError{ + err: serrors.Wrap("inserting revocation", err), + result: prom.ErrDB, + } + } + return nil +} + +// DRKeyGetASHostKey requests an AS-Host Key. +func (e *DaemonEngine) DRKeyGetASHostKey( + ctx context.Context, + meta drkey.ASHostMeta, +) (drkey.ASHostKey, error) { + if e.DRKeyClient == nil { + return drkey.ASHostKey{}, serrors.New("DRKey is not available") + } + return e.DRKeyClient.GetASHostKey(ctx, meta) +} + +// DRKeyGetHostASKey requests a Host-AS Key. +func (e *DaemonEngine) DRKeyGetHostASKey( + ctx context.Context, + meta drkey.HostASMeta, +) (drkey.HostASKey, error) { + if e.DRKeyClient == nil { + return drkey.HostASKey{}, serrors.New("DRKey is not available") + } + return e.DRKeyClient.GetHostASKey(ctx, meta) +} + +// DRKeyGetHostHostKey requests a Host-Host Key. +func (e *DaemonEngine) DRKeyGetHostHostKey( + ctx context.Context, + meta drkey.HostHostMeta, +) (drkey.HostHostKey, error) { + if e.DRKeyClient == nil { + return drkey.HostHostKey{}, serrors.New("DRKey is not available") + } + return e.DRKeyClient.GetHostHostKey(ctx, meta) +} + +type metricsError struct { + err error + result string +} + +func (e metricsError) Error() string { + return e.err.Error() +} diff --git a/pkg/daemon/private/standalone/BUILD.bazel b/pkg/daemon/private/standalone/BUILD.bazel new file mode 100644 index 0000000000..2bbc769473 --- /dev/null +++ b/pkg/daemon/private/standalone/BUILD.bazel @@ -0,0 +1,27 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "metrics.go", + "standalone.go", + ], + importpath = "github.com/scionproto/scion/pkg/daemon/private/standalone", + visibility = ["//visibility:public"], + deps = [ + "//pkg/addr:go_default_library", + "//pkg/daemon/asinfo:go_default_library", + "//pkg/daemon/private/engine:go_default_library", + "//pkg/daemon/types:go_default_library", + "//pkg/drkey:go_default_library", + "//pkg/metrics:go_default_library", + "//pkg/private/ctrl/path_mgmt:go_default_library", + "//pkg/private/prom:go_default_library", + "//pkg/private/serrors:go_default_library", + "//pkg/snet:go_default_library", + "//private/periodic:go_default_library", + "//private/revcache:go_default_library", + "//private/storage:go_default_library", + "@com_github_prometheus_client_golang//prometheus:go_default_library", + ], +) diff --git a/pkg/daemon/private/standalone/metrics.go b/pkg/daemon/private/standalone/metrics.go new file mode 100644 index 0000000000..19dbdd058b --- /dev/null +++ b/pkg/daemon/private/standalone/metrics.go @@ -0,0 +1,109 @@ +// Copyright 2018 ETH Zurich, Anapaya Systems +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standalone + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/scionproto/scion/pkg/metrics" + "github.com/scionproto/scion/pkg/private/prom" + "github.com/scionproto/scion/pkg/private/serrors" +) + +// Metrics contains metrics for all StandaloneDaemon operations. +type Metrics struct { + LocalIA RequestMetric + PortRange RequestMetric + Interfaces RequestMetric + Paths RequestMetric + ASInfo RequestMetric + SVCInfo RequestMetric + InterfaceDown RequestMetric + DRKeyASHost RequestMetric + DRKeyHostAS RequestMetric + DRKeyHostHost RequestMetric +} + +// RequestMetric contains the metrics for a given request type. +type RequestMetric struct { + Requests metrics.Counter + Latency metrics.Histogram +} + +func (m RequestMetric) Observe(err error, latency time.Duration, extraLabels ...string) { + result := standaloneResultFromErr(err) + if m.Requests != nil { + m.Requests.With(append([]string{prom.LabelResult, result}, extraLabels...)...).Add(1) + } + if m.Latency != nil { + m.Latency.With(prom.LabelResult, result).Observe(latency.Seconds()) + } +} + +func standaloneResultFromErr(err error) string { + if err == nil { + return prom.Success + } + if serrors.IsTimeout(err) { + return prom.ErrTimeout + } + return prom.ErrNotClassified +} + +// NewStandaloneMetrics creates metrics for StandaloneDaemon operations. +func NewStandaloneMetrics() Metrics { + resultLabels := []string{prom.LabelResult} + pathLabels := []string{prom.LabelResult, prom.LabelDst} + return Metrics{ + LocalIA: newRequestMetric("local_ia", "local IA", resultLabels), + PortRange: newRequestMetric("port_range", "port range", resultLabels), + Interfaces: newRequestMetric("interfaces", "interfaces", resultLabels), + Paths: newRequestMetric("paths", "path", pathLabels), + ASInfo: newRequestMetric("as_info", "AS info", resultLabels), + SVCInfo: newRequestMetric("svc_info", "SVC info", resultLabels), + InterfaceDown: newRequestMetric( + "interface_down", "interface down notification", resultLabels, + ), + DRKeyASHost: newRequestMetric("drkey_as_host", "DRKey AS-Host", resultLabels), + DRKeyHostAS: newRequestMetric("drkey_host_as", "DRKey Host-AS", resultLabels), + DRKeyHostHost: newRequestMetric("drkey_host_host", "DRKey Host-Host", resultLabels), + } +} + +func newRequestMetric(subsystem, description string, labels []string) RequestMetric { + return RequestMetric{ + Requests: metrics.NewPromCounterFrom( + prometheus.CounterOpts{ + Namespace: "standalone_daemon", + Subsystem: subsystem, + Name: "requests_total", + Help: "The amount of " + description + " requests.", + }, + labels, + ), + Latency: metrics.NewPromHistogramFrom( + prometheus.HistogramOpts{ + Namespace: "standalone_daemon", + Subsystem: subsystem, + Name: "request_duration_seconds", + Help: "Time to handle " + description + " requests.", + Buckets: prom.DefaultLatencyBuckets, + }, + []string{prom.LabelResult}, + ), + } +} diff --git a/pkg/daemon/private/standalone/standalone.go b/pkg/daemon/private/standalone/standalone.go new file mode 100644 index 0000000000..8b948e216a --- /dev/null +++ b/pkg/daemon/private/standalone/standalone.go @@ -0,0 +1,191 @@ +// Copyright 2020 Anapaya Systems +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standalone + +import ( + "context" + "errors" + "io" + "net/netip" + "time" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon/asinfo" + "github.com/scionproto/scion/pkg/daemon/private/engine" + "github.com/scionproto/scion/pkg/daemon/types" + "github.com/scionproto/scion/pkg/drkey" + "github.com/scionproto/scion/pkg/private/ctrl/path_mgmt" + "github.com/scionproto/scion/pkg/private/prom" + "github.com/scionproto/scion/pkg/snet" + "github.com/scionproto/scion/private/periodic" + "github.com/scionproto/scion/private/revcache" + "github.com/scionproto/scion/private/storage" +) + +// Daemon implements the daemon.Connector interface by directly +// delegating to a DaemonEngine. This allows in-process usage of daemon +// functionality without going through gRPC. +// Also collects metrics for all operations. +// +// Close() will clean up all resources, including LocalASInfo if it implements +// io.Closer. +type Daemon struct { + Engine *engine.DaemonEngine + Metrics Metrics + LocalASInfo asinfo.LocalASInfo + PathDBCleaner *periodic.Runner + PathDB storage.PathDB + RevCache revcache.RevCache + RcCleaner *periodic.Runner + TrustDB storage.TrustDB + TRCLoaderTask *periodic.Runner +} + +// LocalIA returns the local ISD-AS number. +func (s *Daemon) LocalIA(ctx context.Context) (addr.IA, error) { + start := time.Now() + ia, err := s.Engine.LocalIA(ctx) + s.Metrics.LocalIA.Observe(err, time.Since(start)) + return ia, err +} + +// PortRange returns the beginning and the end of the SCION/UDP endhost port range. +func (s *Daemon) PortRange(ctx context.Context) (uint16, uint16, error) { + start := time.Now() + startPort, endPort, err := s.Engine.PortRange(ctx) + s.Metrics.PortRange.Observe(err, time.Since(start)) + return startPort, endPort, err +} + +// Interfaces returns the map of interface identifiers to the underlay internal address. +func (s *Daemon) Interfaces(ctx context.Context) (map[uint16]netip.AddrPort, error) { + start := time.Now() + result, err := s.Engine.Interfaces(ctx) + s.Metrics.Interfaces.Observe(err, time.Since(start)) + return result, err +} + +// Paths requests from the daemon a set of end to end paths between the source and destination. +func (s *Daemon) Paths( + ctx context.Context, + dst, src addr.IA, + f types.PathReqFlags, +) ([]snet.Path, error) { + start := time.Now() + paths, err := s.Engine.Paths(ctx, dst, src, f) + s.Metrics.Paths.Observe(err, time.Since(start), prom.LabelDst, dst.ISD().String()) + return paths, err +} + +// ASInfo requests information about an AS. The zero IA returns local AS info. +func (s *Daemon) ASInfo(ctx context.Context, ia addr.IA) (types.ASInfo, error) { + start := time.Now() + asInfo, err := s.Engine.ASInfo(ctx, ia) + s.Metrics.ASInfo.Observe(err, time.Since(start)) + return asInfo, err +} + +// SVCInfo requests information about addresses and ports of infrastructure services. +func (s *Daemon) SVCInfo( + ctx context.Context, + _ []addr.SVC, +) (map[addr.SVC][]string, error) { + start := time.Now() + uris, err := s.Engine.SVCInfo(ctx) + s.Metrics.SVCInfo.Observe(err, time.Since(start)) + if err != nil { + return nil, err + } + result := make(map[addr.SVC][]string) + if len(uris) > 0 { + result[addr.SvcCS] = uris + } + return result, nil +} + +// RevNotification sends a RevocationInfo message to the daemon. +func (s *Daemon) RevNotification( + ctx context.Context, + revInfo *path_mgmt.RevInfo, +) error { + start := time.Now() + err := s.Engine.NotifyInterfaceDown(ctx, revInfo.RawIsdas, uint64(revInfo.IfID)) + s.Metrics.InterfaceDown.Observe(err, time.Since(start)) + return err +} + +// DRKeyGetASHostKey requests an AS-Host Key from the daemon. +func (s *Daemon) DRKeyGetASHostKey( + ctx context.Context, + meta drkey.ASHostMeta, +) (drkey.ASHostKey, error) { + start := time.Now() + key, err := s.Engine.DRKeyGetASHostKey(ctx, meta) + s.Metrics.DRKeyASHost.Observe(err, time.Since(start)) + return key, err +} + +// DRKeyGetHostASKey requests a Host-AS Key from the daemon. +func (s *Daemon) DRKeyGetHostASKey( + ctx context.Context, + meta drkey.HostASMeta, +) (drkey.HostASKey, error) { + start := time.Now() + key, err := s.Engine.DRKeyGetHostASKey(ctx, meta) + s.Metrics.DRKeyHostAS.Observe(err, time.Since(start)) + return key, err +} + +// DRKeyGetHostHostKey requests a Host-Host Key from the daemon. +func (s *Daemon) DRKeyGetHostHostKey( + ctx context.Context, + meta drkey.HostHostMeta, +) (drkey.HostHostKey, error) { + start := time.Now() + key, err := s.Engine.DRKeyGetHostHostKey(ctx, meta) + s.Metrics.DRKeyHostHost.Observe(err, time.Since(start)) + return key, err +} + +func (s *Daemon) Close() error { + var err error + if s.PathDBCleaner != nil { + s.PathDBCleaner.Stop() + } + if s.PathDB != nil { + err1 := s.PathDB.Close() + err = errors.Join(err, err1) + } + if s.RevCache != nil { + err1 := s.RevCache.Close() + err = errors.Join(err, err1) + } + if s.RcCleaner != nil { + s.RcCleaner.Stop() + } + if s.TrustDB != nil { + err1 := s.TrustDB.Close() + err = errors.Join(err, err1) + } + if s.TRCLoaderTask != nil { + s.TRCLoaderTask.Stop() + } + // Close LocalASInfo if it implements io.Closer. + if closer, ok := s.LocalASInfo.(io.Closer); ok { + err1 := closer.Close() + err = errors.Join(err, err1) + } + return err +} diff --git a/pkg/daemon/private/trust/BUILD.bazel b/pkg/daemon/private/trust/BUILD.bazel new file mode 100644 index 0000000000..144251894b --- /dev/null +++ b/pkg/daemon/private/trust/BUILD.bazel @@ -0,0 +1,18 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["trust.go"], + importpath = "github.com/scionproto/scion/pkg/daemon/private/trust", + visibility = ["//visibility:public"], + deps = [ + "//pkg/addr:go_default_library", + "//pkg/grpc:go_default_library", + "//pkg/log:go_default_library", + "//pkg/metrics:go_default_library", + "//pkg/private/serrors:go_default_library", + "//private/trust:go_default_library", + "//private/trust/grpc:go_default_library", + "//private/trust/metrics:go_default_library", + ], +) diff --git a/pkg/daemon/private/trust/trust.go b/pkg/daemon/private/trust/trust.go new file mode 100644 index 0000000000..36417b04a2 --- /dev/null +++ b/pkg/daemon/private/trust/trust.go @@ -0,0 +1,82 @@ +// Copyright 2025 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 trust + +import ( + "context" + "errors" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/grpc" + "github.com/scionproto/scion/pkg/log" + "github.com/scionproto/scion/pkg/metrics" + "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/private/trust" + trustgrpc "github.com/scionproto/scion/private/trust/grpc" + trustmetrics "github.com/scionproto/scion/private/trust/metrics" +) + +// NewEngine builds the trust engine backed by the trust database. +func NewEngine( + ctx context.Context, + certsDir string, + ia addr.IA, + db trust.DB, + dialer grpc.Dialer, +) (trust.Engine, error) { + loaded, err := trust.LoadTRCs(ctx, certsDir, db) + if err != nil { + return trust.Engine{}, serrors.Wrap("loading TRCs", err) + } + log.Info("TRCs loaded", "files", loaded.Loaded) + for f, r := range loaded.Ignored { + if errors.Is(r, trust.ErrAlreadyExists) { + log.Debug("Ignoring existing TRC", "file", f) + continue + } + log.Info("Ignoring non-TRC", "file", f, "reason", r) + } + loaded, err = trust.LoadChains(ctx, certsDir, db) + if err != nil { + return trust.Engine{}, serrors.Wrap("loading certificate chains", + err) + } + log.Info("Certificate chains loaded", "files", loaded.Loaded) + for f, r := range loaded.Ignored { + if errors.Is(r, trust.ErrAlreadyExists) { + log.Debug("Ignoring existing certificate chain", "file", f) + continue + } + if errors.Is(r, trust.ErrOutsideValidity) { + log.Debug("Ignoring certificate chain outside validity", "file", f) + continue + } + log.Info("Ignoring non-certificate chain", "file", f, "reason", r) + } + return trust.Engine{ + Inspector: trust.DBInspector{DB: db}, + Provider: trust.FetchingProvider{ + DB: db, + Fetcher: trustgrpc.Fetcher{ + IA: ia, + Dialer: dialer, + Requests: metrics.NewPromCounter(trustmetrics.RPC.Fetches), + }, + Recurser: trust.LocalOnlyRecurser{}, + Router: trust.LocalRouter{IA: ia}, + }, + DB: db, + }, nil +} diff --git a/pkg/daemon/standalone.go b/pkg/daemon/standalone.go new file mode 100644 index 0000000000..a15bfd7b8a --- /dev/null +++ b/pkg/daemon/standalone.go @@ -0,0 +1,282 @@ +// Copyright 2020 Anapaya Systems +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package daemon + +import ( + "context" + "time" + + "github.com/patrickmn/go-cache" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc/resolver" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon/asinfo" + "github.com/scionproto/scion/pkg/daemon/fetcher" + "github.com/scionproto/scion/pkg/daemon/private/engine" + "github.com/scionproto/scion/pkg/daemon/private/standalone" + "github.com/scionproto/scion/pkg/daemon/private/trust" + "github.com/scionproto/scion/pkg/grpc" + "github.com/scionproto/scion/pkg/log" + pkgmetrics "github.com/scionproto/scion/pkg/metrics" + "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/private/pathdb" + "github.com/scionproto/scion/private/periodic" + "github.com/scionproto/scion/private/revcache" + "github.com/scionproto/scion/private/segment/segfetcher" + segfetchergrpc "github.com/scionproto/scion/private/segment/segfetcher/grpc" + segverifier "github.com/scionproto/scion/private/segment/verifier" + "github.com/scionproto/scion/private/storage" + truststoragemetrics "github.com/scionproto/scion/private/storage/trust/metrics" + privtrust "github.com/scionproto/scion/private/trust" + "github.com/scionproto/scion/private/trust/compat" + trustmetrics "github.com/scionproto/scion/private/trust/metrics" +) + +// StandaloneConnectorOption is a functional option for NewStandaloneConnector. +type StandaloneConnectorOption func(*standaloneConnectorOptions) + +type standaloneConnectorOptions struct { + certsDir string + disableSegVerification bool + enablePeriodicCleanup bool + enableMetrics bool +} + +// WithCertsDir sets the directory containing TRC certificates for trust material. +// This option is required unless segment verification is disabled. +func WithCertsDir(dir string) StandaloneConnectorOption { + return func(o *standaloneConnectorOptions) { + o.certsDir = dir + } +} + +// WithDisabledSegVerification disables segment verification. +// WARNING: This should NOT be used in production! +func WithDisabledSegVerification() StandaloneConnectorOption { + return func(o *standaloneConnectorOptions) { + o.disableSegVerification = true + } +} + +// WithPeriodicCleanup enables periodic cleanup of path database and revocation cache. +func WithPeriodicCleanup() StandaloneConnectorOption { + return func(o *standaloneConnectorOptions) { + o.enablePeriodicCleanup = true + } +} + +// WithMetrics enables metrics collection for the standalone daemon. +func WithMetrics() StandaloneConnectorOption { + return func(o *standaloneConnectorOptions) { + o.enableMetrics = true + } +} + +// LoadASInfoFromFile loads local AS Information from a file. +// The returned struct can be passed to NewStandaloneConnector. +func LoadASInfoFromFile(topoFile string) (asinfo.LocalASInfo, error) { + return asinfo.LoadFromTopoFile(topoFile) +} + +// NewStandaloneConnector creates a daemon Connector that runs locally without a daemon process. +// It requires a LocalASInfo (use LoadASInfoFromFile to create one from a file) and accepts +// functional options for configuration. +// +// The returned Connector can be used directly by SCION applications instead of connecting +// to a daemon via gRPC. +// +// Example: +// +// localASInfo, err := daemon.LoadASInfoFromFile("/path/to/topology.json") +// if err != nil { ... } +// conn, err := daemon.NewStandaloneConnector(ctx, localASInfo, +// daemon.WithCertsDir("/path/to/certs"), +// daemon.WithMetrics(), +// ) +func NewStandaloneConnector( + ctx context.Context, localASInfo asinfo.LocalASInfo, opts ...StandaloneConnectorOption, +) (Connector, error) { + + options := &standaloneConnectorOptions{} + for _, opt := range opts { + opt(options) + } + + // Validate that certsDir is set unless segment verification is disabled + if options.certsDir == "" && !options.disableSegVerification { + return nil, serrors.New("WithCertsDir is required unless segment verification is disabled") + } + + // Create dialer for control service + dialer := &grpc.TCPDialer{ + SvcResolver: func(dst addr.SVC) []resolver.Address { + if base := dst.Base(); base != addr.SvcCS { + panic("unsupported address type, possible implementation error: " + + base.String()) + } + var targets []resolver.Address + for _, entry := range localASInfo.ControlServiceAddresses() { + targets = append(targets, resolver.Address{Addr: entry.String()}) + } + return targets + }, + } + + // Create RPC requester for segment fetching + var requester segfetcher.RPC = &segfetchergrpc.Requester{ + Dialer: dialer, + } + + // Initialize in-memory path storage + pathDB, err := storage.NewInMemoryPathStorage() + if err != nil { + return nil, serrors.Wrap("initializing path storage", err) + } + + // Initialize revocation cache + revCache := storage.NewRevocationStorage() + + // Start periodic cleaners if enabled + var cleaner *periodic.Runner + var rcCleaner *periodic.Runner + if options.enablePeriodicCleanup { + //nolint:staticcheck // SA1019: fix later (https://github.com/scionproto/scion/issues/4776). + cleaner = periodic.Start(pathdb.NewCleaner(pathDB, "sd_segments"), + 300*time.Second, 295*time.Second) + + //nolint:staticcheck // SA1019: fix later (https://github.com/scionproto/scion/issues/4776). + rcCleaner = periodic.Start(revcache.NewCleaner(revCache, "sd_revocation"), + 10*time.Second, 10*time.Second) + } + + var trustDB storage.TrustDB + var inspector privtrust.Inspector + var verifier segverifier.Verifier + var trcLoaderTask *periodic.Runner + + // Create trust engine unless verification is disabled + if options.disableSegVerification { + log.Info("SEGMENT VERIFICATION DISABLED -- SHOULD NOT USE IN PRODUCTION!") + inspector = nil // avoids requiring trust material + verifier = segverifier.AcceptAllVerifier{} + } else { + trustDB, err = storage.NewInMemoryTrustStorage() + if err != nil { + return nil, serrors.Wrap("initializing trust database", err) + } + trustDB = truststoragemetrics.WrapDB(trustDB, truststoragemetrics.Config{ + Driver: string(storage.BackendSqlite), + QueriesTotal: pkgmetrics.NewPromCounterFrom( + prometheus.CounterOpts{ + Name: "trustengine_db_queries_total", + Help: "Total queries to the database", + }, + []string{"driver", "operation", "result"}, + ), + }) + trustEngine, err := trust.NewEngine( + ctx, options.certsDir, localASInfo.IA(), trustDB, dialer, + ) + if err != nil { + return nil, serrors.Wrap("creating trust engine", err) + } + trustEngine.Inspector = privtrust.CachingInspector{ + Inspector: trustEngine.Inspector, + Cache: cache.New(time.Minute, time.Minute), + CacheHits: pkgmetrics.NewPromCounter(trustmetrics.CacheHitsTotal), + MaxCacheExpiration: time.Minute, + } + trcLoader := privtrust.TRCLoader{ + Dir: options.certsDir, + DB: trustDB, + } + //nolint:staticcheck // SA1019: fix later (https://github.com/scionproto/scion/issues/4776). + trcLoaderTask = periodic.Start( + periodic.Func{ + Task: func(ctx context.Context) { + res, err := trcLoader.Load(ctx) + if err != nil { + log.SafeInfo(log.FromCtx(ctx), "TRC loading failed", "err", err) + } + if len(res.Loaded) > 0 { + log.SafeInfo( + log.FromCtx(ctx), + "Loaded TRCs from disk", "trcs", res.Loaded, + ) + } + }, + TaskName: "daemon_trc_loader", + }, 10*time.Second, 10*time.Second, + ) + + verifier = compat.Verifier{ + Verifier: privtrust.Verifier{ + Engine: trustEngine, + Cache: cache.New(time.Minute, time.Minute), + CacheHits: pkgmetrics.NewPromCounter(trustmetrics.CacheHitsTotal), + MaxCacheExpiration: time.Minute, + }, + } + inspector = trustEngine.Inspector + } + + // Create fetcher + newFetcher := fetcher.NewFetcher( + fetcher.FetcherConfig{ + IA: localASInfo.IA(), + MTU: localASInfo.MTU(), + Core: localASInfo.Core(), + NextHopper: localASInfo, + RPC: requester, + PathDB: pathDB, + Inspector: inspector, + Verifier: verifier, + RevCache: revCache, + QueryInterval: 0, + }, + ) + + // Create the daemon engine + daemonEngine := &engine.DaemonEngine{ + IA: localASInfo.IA(), + MTU: localASInfo.MTU(), + LocalASInfo: localASInfo, + Fetcher: newFetcher, + RevCache: revCache, + ASInspector: inspector, + // TODO(emairoll): Implement DRKey for standalone mode + DRKeyClient: nil, + } + + var standaloneMetrics standalone.Metrics + if options.enableMetrics { + standaloneMetrics = standalone.NewStandaloneMetrics() + } + + standaloneDaemon := &standalone.Daemon{ + Engine: daemonEngine, + Metrics: standaloneMetrics, + LocalASInfo: localASInfo, + PathDBCleaner: cleaner, + PathDB: pathDB, + RevCache: revCache, + RcCleaner: rcCleaner, + TrustDB: trustDB, + TRCLoaderTask: trcLoaderTask, + } + + return standaloneDaemon, nil +} diff --git a/pkg/daemon/topology.go b/pkg/daemon/topology.go index b1a76b6e96..bb47b2cd2a 100644 --- a/pkg/daemon/topology.go +++ b/pkg/daemon/topology.go @@ -108,7 +108,7 @@ func (t *ReloadingTopology) Run(ctx context.Context, period time.Duration) { defer ticker.Stop() reload := func() { - ctx, cancel := context.WithTimeout(ctx, time.Second) + ctx, cancel := context.WithTimeout(ctx, defaultConnectionTimeout) defer cancel() if err := t.loadInterfaces(ctx); err != nil { log.FromCtx(ctx).Error("Failed to reload interfaces", "err", err) diff --git a/pkg/daemon/types/BUILD.bazel b/pkg/daemon/types/BUILD.bazel new file mode 100644 index 0000000000..4e4e3d2541 --- /dev/null +++ b/pkg/daemon/types/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["types.go"], + importpath = "github.com/scionproto/scion/pkg/daemon/types", + visibility = ["//visibility:public"], + deps = ["//pkg/addr:go_default_library"], +) diff --git a/pkg/daemon/types/types.go b/pkg/daemon/types/types.go new file mode 100644 index 0000000000..e0284a8603 --- /dev/null +++ b/pkg/daemon/types/types.go @@ -0,0 +1,31 @@ +// Copyright 2019 Anapaya Systems +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/scionproto/scion/pkg/addr" +) + +// PathReqFlags contains flags for path requests. +type PathReqFlags struct { + Refresh bool + Hidden bool +} + +// ASInfo provides information about the local AS. +type ASInfo struct { + IA addr.IA + MTU uint16 +} diff --git a/private/app/flag/BUILD.bazel b/private/app/flag/BUILD.bazel index e360774bf5..e9fad87888 100644 --- a/private/app/flag/BUILD.bazel +++ b/private/app/flag/BUILD.bazel @@ -29,7 +29,6 @@ go_test( embed = [":go_default_library"], deps = [ "//pkg/addr:go_default_library", - "//pkg/daemon:go_default_library", "//private/app/env:go_default_library", "@com_github_spf13_pflag//:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", diff --git a/private/app/flag/env.go b/private/app/flag/env.go index fb863ebbc7..7408746b8d 100644 --- a/private/app/flag/env.go +++ b/private/app/flag/env.go @@ -20,6 +20,7 @@ import ( "io/fs" "net/netip" "os" + "runtime" "sync" "github.com/spf13/pflag" @@ -34,6 +35,8 @@ const ( defaultDaemon = daemon.DefaultAPIAddress defaultEnvironmentFile = "/etc/scion/environment.json" + + defaultConfigDirLinux = "/etc/scion" ) type stringVal string @@ -77,15 +80,17 @@ func (v *ipVal) String() string { return netip.Addr(*v).String() } // SCIONEnvironment can be used to access the common SCION configuration values, // like the SCION daemon address and the local IP as well as the local ISD-AS. type SCIONEnvironment struct { - sciondFlag *pflag.Flag - sciondEnv *string - ia addr.IA - iaFlag *pflag.Flag - local netip.Addr - localEnv *netip.Addr - localFlag *pflag.Flag - file env.SCION - filepath string + sciondFlag *pflag.Flag + sciondEnv *string + ia addr.IA + iaFlag *pflag.Flag + local netip.Addr + localEnv *netip.Addr + localFlag *pflag.Flag + configDir string + configDirFlag *pflag.Flag + file env.SCION + filepath string mtx sync.Mutex } @@ -98,13 +103,59 @@ func (e *SCIONEnvironment) Register(flagSet *pflag.FlagSet) { e.mtx.Lock() defer e.mtx.Unlock() - sciond := defaultDaemon - e.sciondFlag = flagSet.VarPF((*stringVal)(&sciond), "sciond", "", - "SCION Daemon address.") e.iaFlag = flagSet.VarPF((*iaVal)(&e.ia), "isd-as", "", "The local ISD-AS to use.") e.localFlag = flagSet.VarPF((*ipVal)(&e.local), "local", "l", "Local IP address to listen on.") + sciond := "" + e.sciondFlag = flagSet.VarPF( + (*stringVal)(&sciond), "sciond", "", + `Connect to SCION Daemon at the specified address instead of using +the local topology.json (IP:Port or "default" for `+defaultDaemon+`). +If both --sciond and --config-dir are set, --sciond takes priority.`, + ) + + configDirHelp := `Directory containing topology.json and certs/ for standalone mode. +If both --sciond and --config-dir are set, --sciond takes priority. +` + if runtime.GOOS == "linux" { + configDirHelp += `Defaults to ` + defaultConfigDirLinux + ` on Linux.` + } else { + configDirHelp += `Required on this platform (no default).` + } + e.configDirFlag = flagSet.VarPF( + (*stringVal)(&e.configDir), "config-dir", "", + configDirHelp, + ) +} + +// Validate checks that the flags are consistent. +// Returns an error if neither --sciond nor --config-dir is set and there's no default +// (i.e., on non-Linux platforms where --config-dir has no default). +func (e *SCIONEnvironment) Validate() error { + e.mtx.Lock() + defer e.mtx.Unlock() + + sciondSet := e.sciondFlag != nil && e.sciondFlag.Changed + configDirSet := e.configDirFlag != nil && e.configDirFlag.Changed + + // If either flag is explicitly set, we're good + if sciondSet || configDirSet { + return nil + } + + // Check if there's a daemon configured via environment + if e.sciondEnv != nil { + return nil + } + + // On Linux, we have a default config directory + if runtime.GOOS == "linux" { + return nil + } + + // On non-Linux platforms with no flags set, we need either --sciond or --config-dir + return serrors.New("either --sciond or --config-dir must be specified on this platform") } // LoadExternalVar loads variables from the SCION environment file and from the @@ -163,18 +214,25 @@ func (e *SCIONEnvironment) loadEnv() error { return nil } -// Daemon returns the path to the SCION daemon. The value is loaded from one of -// the following sources with the precedence as listed: -// 1. Command line flag -// 2. Environment variable +// Daemon returns the SCION daemon address if explicitly configured. +// Returns empty string if no daemon was configured, allowing the caller +// to fall back to using the local topology. +// The value is loaded from one of the following sources with precedence: +// 1. Command line flag (--sciond) +// 2. Environment variable (SCION_DAEMON) // 3. Environment configuration file -// 4. Default value. +// +// If none are set, returns empty string (not the default address). func (e *SCIONEnvironment) Daemon() string { e.mtx.Lock() defer e.mtx.Unlock() if e.sciondFlag != nil && e.sciondFlag.Changed { - return e.sciondFlag.Value.String() + value := e.sciondFlag.Value.String() + if value == "default" { + return defaultDaemon + } + return value } if e.sciondEnv != nil { return *e.sciondEnv @@ -186,7 +244,7 @@ func (e *SCIONEnvironment) Daemon() string { if as, ok := e.file.ASes[ia]; ok && as.DaemonAddress != "" { return as.DaemonAddress } - return defaultDaemon + return "" } // Local returns the loca IP to listen on. The value is loaded from one of the @@ -206,3 +264,22 @@ func (e *SCIONEnvironment) Local() netip.Addr { } return netip.Addr{} } + +// ConfigDir returns the configuration directory for standalone mode. +// The value is determined with the following precedence: +// 1. Command line flag (--config-dir) +// 2. On Linux: defaults to /etc/scion +// 3. On other platforms: returns empty string (must be specified via flag) +func (e *SCIONEnvironment) ConfigDir() string { + e.mtx.Lock() + defer e.mtx.Unlock() + + if e.configDirFlag != nil && e.configDirFlag.Changed { + return e.configDir + } + // Default to /etc/scion on Linux only + if runtime.GOOS == "linux" { + return defaultConfigDirLinux + } + return "" +} diff --git a/private/app/flag/env_test.go b/private/app/flag/env_test.go index 4b0c395fd4..425a38b2eb 100644 --- a/private/app/flag/env_test.go +++ b/private/app/flag/env_test.go @@ -18,6 +18,7 @@ import ( "encoding/json" "net/netip" "os" + "runtime" "testing" "github.com/spf13/pflag" @@ -25,7 +26,6 @@ import ( "github.com/stretchr/testify/require" "github.com/scionproto/scion/pkg/addr" - "github.com/scionproto/scion/pkg/daemon" "github.com/scionproto/scion/private/app/env" "github.com/scionproto/scion/private/app/flag" ) @@ -80,7 +80,7 @@ func TestSCIONEnvironment(t *testing.T) { flags: noFlags, env: noEnv, file: noFile, - daemon: daemon.DefaultAPIAddress, + daemon: "", local: netip.Addr{}, }, "flag values set": { @@ -140,3 +140,67 @@ func tempEnv(t *testing.T, key, val string) { require.NoError(t, os.Setenv(key, val)) t.Cleanup(func() { require.NoError(t, os.Unsetenv(key)) }) } + +func TestSCIONEnvironmentConfigDir(t *testing.T) { + defaultDir := "" + if runtime.GOOS == "linux" { + defaultDir = "/etc/scion" + } + + testCases := map[string]struct { + flags []string + configDir string + }{ + "no flag": { + flags: []string{}, + configDir: defaultDir, + }, + "config-dir flag set": { + flags: []string{"--config-dir", "/custom/path"}, + configDir: "/custom/path", + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + var env flag.SCIONEnvironment + fs := pflag.NewFlagSet("testSet", pflag.ContinueOnError) + env.Register(fs) + require.NoError(t, fs.Parse(tc.flags)) + assert.Equal(t, tc.configDir, env.ConfigDir()) + }) + } +} + +func TestSCIONEnvironmentValidate(t *testing.T) { + testCases := map[string]struct { + flags []string + wantErr bool + }{ + "sciond set": { + flags: []string{"--sciond", "127.0.0.1:30255"}, + wantErr: false, + }, + "config-dir set": { + flags: []string{"--config-dir", "/custom/path"}, + wantErr: false, + }, + "both flags set - sciond takes priority": { + flags: []string{"--sciond", "127.0.0.1:30255", "--config-dir", "/custom/path"}, + wantErr: false, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + var env flag.SCIONEnvironment + fs := pflag.NewFlagSet("testSet", pflag.ContinueOnError) + env.Register(fs) + require.NoError(t, fs.Parse(tc.flags)) + err := env.Validate() + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/private/app/path/BUILD.bazel b/private/app/path/BUILD.bazel index bd4d560f45..fa6e9ae6cd 100644 --- a/private/app/path/BUILD.bazel +++ b/private/app/path/BUILD.bazel @@ -9,6 +9,7 @@ go_library( deps = [ "//pkg/addr:go_default_library", "//pkg/daemon:go_default_library", + "//pkg/daemon/types:go_default_library", "//pkg/private/serrors:go_default_library", "//pkg/snet:go_default_library", "//pkg/snet/path:go_default_library", diff --git a/private/app/path/path.go b/private/app/path/path.go index b003a4bbfe..126e12a64d 100644 --- a/private/app/path/path.go +++ b/private/app/path/path.go @@ -30,6 +30,7 @@ import ( "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/daemon" + daemontypes "github.com/scionproto/scion/pkg/daemon/types" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/snet" snetpath "github.com/scionproto/scion/pkg/snet/path" @@ -173,7 +174,7 @@ func fetchPaths( refresh bool, seq string, ) ([]snet.Path, error) { - allPaths, err := conn.Paths(ctx, remote, 0, daemon.PathReqFlags{Refresh: refresh}) + allPaths, err := conn.Paths(ctx, remote, 0, daemontypes.PathReqFlags{Refresh: refresh}) if err != nil { return nil, serrors.Wrap("retrieving paths", err) } diff --git a/daemon/drkey/BUILD.bazel b/private/drkey/BUILD.bazel similarity index 85% rename from daemon/drkey/BUILD.bazel rename to private/drkey/BUILD.bazel index a32adb9e83..734c0feae5 100644 --- a/daemon/drkey/BUILD.bazel +++ b/private/drkey/BUILD.bazel @@ -3,7 +3,7 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = ["client_engine.go"], - importpath = "github.com/scionproto/scion/daemon/drkey", + importpath = "github.com/scionproto/scion/private/drkey", visibility = ["//visibility:public"], deps = [ "//pkg/addr:go_default_library", diff --git a/daemon/drkey/client_engine.go b/private/drkey/client_engine.go similarity index 100% rename from daemon/drkey/client_engine.go rename to private/drkey/client_engine.go diff --git a/daemon/drkey/grpc/BUILD.bazel b/private/drkey/grpc/BUILD.bazel similarity index 91% rename from daemon/drkey/grpc/BUILD.bazel rename to private/drkey/grpc/BUILD.bazel index 65b1a706c4..d165842555 100644 --- a/daemon/drkey/grpc/BUILD.bazel +++ b/private/drkey/grpc/BUILD.bazel @@ -7,7 +7,7 @@ go_library( "fetcher.go", "protobuf.go", ], - importpath = "github.com/scionproto/scion/daemon/drkey/grpc", + importpath = "github.com/scionproto/scion/private/drkey/grpc", visibility = ["//visibility:public"], deps = [ "//pkg/addr:go_default_library", @@ -26,11 +26,11 @@ go_test( srcs = ["fetching_test.go"], deps = [ ":go_default_library", - "//daemon/drkey:go_default_library", "//pkg/drkey:go_default_library", "//pkg/private/xtest:go_default_library", "//pkg/proto/control_plane:go_default_library", "//pkg/proto/control_plane/mock_control_plane:go_default_library", + "//private/drkey:go_default_library", "@com_github_golang_mock//gomock:go_default_library", "@com_github_stretchr_testify//require:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", diff --git a/daemon/drkey/grpc/fetcher.go b/private/drkey/grpc/fetcher.go similarity index 100% rename from daemon/drkey/grpc/fetcher.go rename to private/drkey/grpc/fetcher.go diff --git a/daemon/drkey/grpc/fetching_test.go b/private/drkey/grpc/fetching_test.go similarity index 89% rename from daemon/drkey/grpc/fetching_test.go rename to private/drkey/grpc/fetching_test.go index 25edceeab5..a5dc3f8af2 100644 --- a/daemon/drkey/grpc/fetching_test.go +++ b/private/drkey/grpc/fetching_test.go @@ -23,15 +23,15 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" - sd_drkey "github.com/scionproto/scion/daemon/drkey" - sd_grpc "github.com/scionproto/scion/daemon/drkey/grpc" "github.com/scionproto/scion/pkg/drkey" "github.com/scionproto/scion/pkg/private/xtest" cppb "github.com/scionproto/scion/pkg/proto/control_plane" mock_cppb "github.com/scionproto/scion/pkg/proto/control_plane/mock_control_plane" + drkeyengine "github.com/scionproto/scion/private/drkey" + drkeygrpc "github.com/scionproto/scion/private/drkey/grpc" ) -var _ sd_drkey.Fetcher = (*sd_grpc.Fetcher)(nil) +var _ drkeyengine.Fetcher = (*drkeygrpc.Fetcher)(nil) func TestGetHostHost(t *testing.T) { ctrl := gomock.NewController(t) @@ -57,7 +57,7 @@ func TestGetHostHost(t *testing.T) { cppb.RegisterDRKeyIntraServiceServer(server.Server(), daemonSrv) server.Start(t) - fetcher := sd_grpc.Fetcher{ + fetcher := drkeygrpc.Fetcher{ Dialer: server, } diff --git a/daemon/drkey/grpc/protobuf.go b/private/drkey/grpc/protobuf.go similarity index 100% rename from daemon/drkey/grpc/protobuf.go rename to private/drkey/grpc/protobuf.go diff --git a/private/segment/segfetcher/splitter.go b/private/segment/segfetcher/splitter.go index 5df22ae456..d5ac43da28 100644 --- a/private/segment/segfetcher/splitter.go +++ b/private/segment/segfetcher/splitter.go @@ -18,6 +18,7 @@ import ( "context" "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/private/serrors" seg "github.com/scionproto/scion/pkg/segment" "github.com/scionproto/scion/private/trust" ) @@ -45,6 +46,25 @@ func (s *MultiSegmentSplitter) Split(ctx context.Context, dst addr.IA) (Requests src := s.LocalIA srcCore := s.Core + + if s.Inspector == nil { // In case inspector is not set, fall back to basic splitting + if srcCore { + return Requests{ + {SegType: Down, Src: src, Dst: dst}, + {SegType: Core, Src: src, Dst: dst}, + {SegType: Core, Src: src, Dst: toWildCard(dst)}, + {SegType: Down, Src: toWildCard(dst), Dst: dst}, + }, nil + } + reqs := Requests{ + {SegType: Up, Src: src, Dst: toWildCard(src)}, + {SegType: Core, Src: toWildCard(src), Dst: toWildCard(dst)}, + {SegType: Core, Src: toWildCard(src), Dst: dst}, + {SegType: Down, Src: toWildCard(dst), Dst: dst}, + } + return reqs, nil + } + singleCore, dstCore, err := s.inspect(ctx, src, dst) if err != nil { return nil, err @@ -87,6 +107,10 @@ func (s *MultiSegmentSplitter) Split(ctx context.Context, dst addr.IA) (Requests func (s *MultiSegmentSplitter) inspect(ctx context.Context, src, dst addr.IA) (addr.IA, bool, error) { + if s.Inspector == nil { + return 0, false, serrors.New("inspector is nil, cannot inspect ASes") + } + if src.ISD() != dst.ISD() { isCore, err := s.isCore(ctx, dst) return 0, isCore, err diff --git a/private/segment/verifier/BUILD.bazel b/private/segment/verifier/BUILD.bazel index 86d68f7125..90c24718df 100644 --- a/private/segment/verifier/BUILD.bazel +++ b/private/segment/verifier/BUILD.bazel @@ -2,12 +2,17 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["verifier.go"], + srcs = [ + "acceptall.go", + "verifier.go", + ], importpath = "github.com/scionproto/scion/private/segment/verifier", visibility = ["//visibility:public"], deps = [ "//pkg/addr:go_default_library", + "//pkg/proto/crypto:go_default_library", "//pkg/scrypto/cppki:go_default_library", + "//pkg/scrypto/signed:go_default_library", "//pkg/segment:go_default_library", ], ) diff --git a/private/segment/verifier/acceptall.go b/private/segment/verifier/acceptall.go new file mode 100644 index 0000000000..d888e4e92d --- /dev/null +++ b/private/segment/verifier/acceptall.go @@ -0,0 +1,48 @@ +// Copyright 2025 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 verifier + +import ( + "context" + "net" + + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/proto/crypto" + "github.com/scionproto/scion/pkg/scrypto/cppki" + "github.com/scionproto/scion/pkg/scrypto/signed" +) + +// AcceptAllVerifier accepts all path segments without verification. +// It is only intended for testing purposes. +type AcceptAllVerifier struct{} + +func (AcceptAllVerifier) Verify( + ctx context.Context, signedMsg *crypto.SignedMessage, + associatedData ...[]byte, +) (*signed.Message, error) { + return nil, nil +} + +func (v AcceptAllVerifier) WithServer(net.Addr) Verifier { + return v +} + +func (v AcceptAllVerifier) WithIA(addr.IA) Verifier { + return v +} + +func (v AcceptAllVerifier) WithValidity(cppki.Validity) Verifier { + return v +} diff --git a/private/storage/storage.go b/private/storage/storage.go index 2a74409b0d..c67b28f53e 100644 --- a/private/storage/storage.go +++ b/private/storage/storage.go @@ -232,6 +232,40 @@ func NewPathStorage(c DBConfig) (PathDB, error) { }, nil } +func NewInMemoryPathStorage() (PathDB, error) { + log.Info("Creating in-memory PathDB", "backend", BackendSqlite) + // Use timestamp to create unique database name for each instance + dbName := fmt.Sprintf("in_memory_path_db_%d", time.Now().UnixNano()) + db, err := sqlitepathdb.New(dbName, &db.SqliteConfig{ + InMemory: true, + MaxOpenReadConns: 1, + MaxIdleReadConns: 1, + }) + + if err != nil { + return nil, err + } + + // Start a periodic task that cleans up the expired path segments. + //nolint:staticcheck // SA1019: fix later (https://github.com/scionproto/scion/issues/4776). + cleaner := periodic.Start( + cleaner.New( + func(ctx context.Context) (int, error) { + checkpoint(ctx, db.DB()) + return db.DeleteExpired(ctx, time.Now()) + }, + "control_pathstorage_cleaner", + ), + 30*time.Second, + 30*time.Second, + ) + return pathDBWithCleaner{ + DB: db, + cleaner: cleaner, + dbCloser: db, + }, nil +} + // pathDBWithCleaner implements the path DB interface and stops both the // database and the cleanup task on Close. type pathDBWithCleaner struct { @@ -264,6 +298,19 @@ func NewTrustStorage(c DBConfig) (TrustDB, error) { return db, nil } +func NewInMemoryTrustStorage() (TrustDB, error) { + log.Info("Creating in-memory TrustDB", "backend", BackendSqlite) + dbName := fmt.Sprintf("in_memory_trust_db_%d", time.Now().UnixNano()) + return sqlitetrustdb.New( + dbName, + &db.SqliteConfig{ + MaxOpenReadConns: 1, + MaxIdleReadConns: 1, + InMemory: true, + }, + ) +} + func NewDRKeySecretValueStorage(c DBConfig) (drkey.SecretValueDB, error) { log.Info("Connecting DRKeySecretValueDB", " ", BackendSqlite, "connection", c.Connection) db, err := sqlitesecret.NewBackend( diff --git a/scion-pki/certs/renew.go b/scion-pki/certs/renew.go index 497a2bdea0..4b7342180f 100644 --- a/scion-pki/certs/renew.go +++ b/scion-pki/certs/renew.go @@ -271,7 +271,7 @@ The template is expressed in JSON. A valid example:: // Setup basic state. daemonCtx, daemonCancel := context.WithTimeout(ctx, time.Second) defer daemonCancel() - sd, err := daemon.NewService(daemonAddr).Connect(daemonCtx) + sd, err := daemon.NewAutoConnector(ctx, daemon.WithDaemon(daemonAddr)) if err != nil { return serrors.Wrap("connecting to SCION Daemon", err) } diff --git a/scion/cmd/scion/address.go b/scion/cmd/scion/address.go index 91ef1314c2..ae0cf425b5 100644 --- a/scion/cmd/scion/address.go +++ b/scion/cmd/scion/address.go @@ -25,10 +25,12 @@ import ( "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/daemon" + "github.com/scionproto/scion/pkg/log" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/snet/addrutil" "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/flag" + "github.com/scionproto/scion/private/tracing" ) type addrInfo struct { @@ -58,16 +60,31 @@ case, the host could have multiple SCION addresses. if err := envFlags.LoadExternalVars(); err != nil { return err } - daemonAddr := envFlags.Daemon() - + if err := envFlags.Validate(); err != nil { + return err + } cmd.SilenceUsage = true - ctx, cancelF := context.WithTimeout(cmd.Context(), time.Second) - defer cancelF() - sd, err := daemon.NewService(daemonAddr).Connect(ctx) + + span, traceCtx := tracing.CtxWith(context.Background(), "run") + defer span.Finish() + + sd, err := daemon.NewAutoConnector(traceCtx, + daemon.WithDaemon(envFlags.Daemon()), + daemon.WithConfigDir(envFlags.ConfigDir()), + ) if err != nil { - return serrors.Wrap("connecting to SCION Daemon", err) + return serrors.Wrap("getting daemon connector", err) } - defer sd.Close() + + defer func(sd daemon.Connector) { + err := sd.Close() + if err != nil { + log.Error("Closing SCION Daemon connection", "err", err) + } + }(sd) + + ctx, cancelF := context.WithTimeout(cmd.Context(), time.Second) + defer cancelF() info, err := app.QueryASInfo(ctx, sd) if err != nil { @@ -96,7 +113,8 @@ case, the host could have multiple SCION addresses. }, } envFlags.Register(cmd.Flags()) - cmd.Flags().BoolVar(&flags.json, "json", false, "Write the output as machine readable json") + cmd.Flags().BoolVar(&flags.json, "json", false, + "Write the output as machine readable json") return cmd } diff --git a/scion/cmd/scion/ping.go b/scion/cmd/scion/ping.go index 0b54c75c53..00e21a3659 100644 --- a/scion/cmd/scion/ping.go +++ b/scion/cmd/scion/ping.go @@ -131,27 +131,34 @@ On other errors, ping will exit with code 2. if err := envFlags.LoadExternalVars(); err != nil { return err } - daemonAddr := envFlags.Daemon() - localIP := net.IP(envFlags.Local().AsSlice()) - log.Debug("Resolved SCION environment flags", - "daemon", daemonAddr, - "local", localIP, - ) + if err := envFlags.Validate(); err != nil { + return err + } span, traceCtx := tracing.CtxWith(context.Background(), "run") span.SetTag("dst.isd_as", remote.IA) span.SetTag("dst.host", remote.Host.IP()) defer span.Finish() - ctx, cancelF := context.WithTimeout(traceCtx, time.Second) - defer cancelF() - sd, err := daemon.NewService(daemonAddr).Connect(ctx) + sd, err := daemon.NewAutoConnector(traceCtx, + daemon.WithDaemon(envFlags.Daemon()), + daemon.WithConfigDir(envFlags.ConfigDir()), + ) if err != nil { - return serrors.Wrap("connecting to SCION Daemon", err) + return serrors.Wrap("getting daemon connector", err) } - defer sd.Close() - topo, err := daemon.LoadTopology(ctx, sd) + defer func(sd daemon.Connector) { + err := sd.Close() + if err != nil { + log.Error("Closing SCION Daemon connection", "err", err) + } + }(sd) + + localIP := net.IP(envFlags.Local().AsSlice()) + log.Debug("Using local IP", "local", localIP) + + topo, err := daemon.LoadTopology(traceCtx, sd) if err != nil { return serrors.Wrap("loading topology", err) } @@ -243,7 +250,7 @@ On other errors, ping will exit with code 2. printf("PING %s pld=%dB scion_pkt=%dB\n", remote, pldSize, pktSize) start := time.Now() - ctx = app.WithSignal(traceCtx, os.Interrupt, syscall.SIGTERM) + ctx := app.WithSignal(traceCtx, os.Interrupt, syscall.SIGTERM) count := flags.count if count == 0 { count = math.MaxUint16 diff --git a/scion/cmd/scion/showpaths.go b/scion/cmd/scion/showpaths.go index b755e63e60..3f9640f2e4 100644 --- a/scion/cmd/scion/showpaths.go +++ b/scion/cmd/scion/showpaths.go @@ -26,6 +26,7 @@ import ( "gopkg.in/yaml.v3" "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/daemon" "github.com/scionproto/scion/pkg/log" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/private/app" @@ -97,13 +98,9 @@ On other errors, showpaths will exit with code 2. if err := envFlags.LoadExternalVars(); err != nil { return err } - - flags.cfg.Daemon = envFlags.Daemon() - flags.cfg.Local = net.IP(envFlags.Local().AsSlice()) - log.Debug("Resolved SCION environment flags", - "daemon", flags.cfg.Daemon, - "local", flags.cfg.Local, - ) + if err := envFlags.Validate(); err != nil { + return err + } span, traceCtx := tracing.CtxWith(context.Background(), "run") span.SetTag("dst.isd_as", dst) @@ -111,6 +108,26 @@ On other errors, showpaths will exit with code 2. ctx, cancel := context.WithTimeout(traceCtx, flags.timeout) defer cancel() + + sd, err := daemon.NewAutoConnector(ctx, + daemon.WithDaemon(envFlags.Daemon()), + daemon.WithConfigDir(envFlags.ConfigDir()), + ) + if err != nil { + return serrors.Wrap("getting daemon connector", err) + } + flags.cfg.Connector = sd + + defer func(sd daemon.Connector) { + err := sd.Close() + if err != nil { + log.Error("Closing SCION Daemon connection", "err", err) + } + }(flags.cfg.Connector) + + flags.cfg.Local = net.IP(envFlags.Local().AsSlice()) + log.Debug("Using local IP", "local", flags.cfg.Local) + res, err := showpaths.Run(ctx, dst, flags.cfg) if err != nil { return err diff --git a/scion/cmd/scion/traceroute.go b/scion/cmd/scion/traceroute.go index 2dae638ff8..343263d1c3 100644 --- a/scion/cmd/scion/traceroute.go +++ b/scion/cmd/scion/traceroute.go @@ -106,26 +106,34 @@ On other errors, traceroute will exit with code 2. if err := envFlags.LoadExternalVars(); err != nil { return err } - daemonAddr := envFlags.Daemon() - localIP := net.IP(envFlags.Local().AsSlice()) - log.Debug("Resolved SCION environment flags", - "daemon", daemonAddr, - "local", localIP, - ) + if err := envFlags.Validate(); err != nil { + return err + } span, traceCtx := tracing.CtxWith(context.Background(), "run") span.SetTag("dst.isd_as", remote.IA) span.SetTag("dst.host", remote.Host.IP()) defer span.Finish() - ctx, cancelF := context.WithTimeout(traceCtx, time.Second) - defer cancelF() - sd, err := daemon.NewService(daemonAddr).Connect(ctx) + sd, err := daemon.NewAutoConnector(traceCtx, + daemon.WithDaemon(envFlags.Daemon()), + daemon.WithConfigDir(envFlags.ConfigDir()), + ) if err != nil { - return serrors.Wrap("connecting to SCION Daemon", err) + return serrors.Wrap("getting daemon connector", err) } - defer sd.Close() - topo, err := daemon.LoadTopology(ctx, sd) + + defer func(sd daemon.Connector) { + err := sd.Close() + if err != nil { + log.Error("Closing SCION Daemon connection", "err", err) + } + }(sd) + + localIP := net.IP(envFlags.Local().AsSlice()) + log.Debug("Using local IP", "local", localIP) + + topo, err := daemon.LoadTopology(traceCtx, sd) if err != nil { return serrors.Wrap("loading topology", err) } @@ -183,7 +191,7 @@ On other errors, traceroute will exit with code 2. IA: topo.LocalIA, Host: addr.HostIP(asNetipAddr), } - ctx = app.WithSignal(traceCtx, os.Interrupt, syscall.SIGTERM) + ctx := app.WithSignal(traceCtx, os.Interrupt, syscall.SIGTERM) var stats traceroute.Stats var updates []traceroute.Update cfg := traceroute.Config{ diff --git a/scion/showpaths/BUILD.bazel b/scion/showpaths/BUILD.bazel index 892930288c..94d564eab8 100644 --- a/scion/showpaths/BUILD.bazel +++ b/scion/showpaths/BUILD.bazel @@ -11,6 +11,7 @@ go_library( deps = [ "//pkg/addr:go_default_library", "//pkg/daemon:go_default_library", + "//pkg/daemon/types:go_default_library", "//pkg/private/serrors:go_default_library", "//pkg/segment/iface:go_default_library", "//pkg/slices:go_default_library", diff --git a/scion/showpaths/config.go b/scion/showpaths/config.go index dcc4bdcad1..bd83d54bb5 100644 --- a/scion/showpaths/config.go +++ b/scion/showpaths/config.go @@ -16,6 +16,8 @@ package showpaths import ( "net" + + "github.com/scionproto/scion/pkg/daemon" ) // DefaultMaxPaths is the maximum number of paths that are displayed by default. @@ -26,8 +28,9 @@ type Config struct { // Local configures the local IP address to use. If this option is not provided, // a local IP that can reach SCION hosts is selected with the help of the kernel. Local net.IP - // Daemon configures a specific SCION Daemon address. - Daemon string + // Connector optionally provides a daemon connector. If set, this is used + // instead of connecting to the Daemon address. + Connector daemon.Connector // MaxPaths configures the maximum number of displayed paths. If this option is // not provided, the DefaultMaxPaths is used. MaxPaths int diff --git a/scion/showpaths/showpaths.go b/scion/showpaths/showpaths.go index 2bdc397374..30e88a35ba 100644 --- a/scion/showpaths/showpaths.go +++ b/scion/showpaths/showpaths.go @@ -26,6 +26,7 @@ import ( "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/daemon" + daemontypes "github.com/scionproto/scion/pkg/daemon/types" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/segment/iface" "github.com/scionproto/scion/pkg/slices" @@ -330,11 +331,8 @@ func (r Result) Alive() int { // Run lists the paths to the specified ISD-AS to stdout. func Run(ctx context.Context, dst addr.IA, cfg Config) (*Result, error) { - sdConn, err := daemon.NewService(cfg.Daemon).Connect(ctx) - if err != nil { - return nil, serrors.Wrap("connecting to the SCION Daemon", err, "addr", cfg.Daemon) - } - defer sdConn.Close() + sdConn := cfg.Connector + topo, err := daemon.LoadTopology(ctx, sdConn) if err != nil { return nil, serrors.Wrap("loading topology", err) @@ -351,7 +349,7 @@ func Run(ctx context.Context, dst addr.IA, cfg Config) (*Result, error) { // possibility to have the same functionality, i.e. refresh, fetch all paths. // https://github.com/scionproto/scion/issues/3348 allPaths, err := sdConn.Paths(ctx, dst, 0, - daemon.PathReqFlags{Refresh: cfg.Refresh}) + daemontypes.PathReqFlags{Refresh: cfg.Refresh}) if err != nil { return nil, serrors.Wrap("retrieving paths from the SCION Daemon", err) } diff --git a/tools/end2end/BUILD.bazel b/tools/end2end/BUILD.bazel index cdad18e433..fdefde1cba 100644 --- a/tools/end2end/BUILD.bazel +++ b/tools/end2end/BUILD.bazel @@ -9,6 +9,7 @@ go_library( deps = [ "//pkg/addr:go_default_library", "//pkg/daemon:go_default_library", + "//pkg/daemon/types: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 7c15964678..9f9e31931c 100644 --- a/tools/end2end/main.go +++ b/tools/end2end/main.go @@ -37,6 +37,7 @@ import ( "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/daemon" + daemontypes "github.com/scionproto/scion/pkg/daemon/types" "github.com/scionproto/scion/pkg/log" "github.com/scionproto/scion/pkg/private/common" "github.com/scionproto/scion/pkg/private/serrors" @@ -349,7 +350,7 @@ func (c *client) getRemote(ctx context.Context, n int) (snet.Path, error) { } paths, err := c.sdConn.Paths(ctx, remote.IA, integration.Local.IA, - daemon.PathReqFlags{Refresh: n != 0}) + daemontypes.PathReqFlags{Refresh: n != 0}) if err != nil { return nil, withTag(serrors.Wrap("requesting paths", err)) } diff --git a/tools/end2end_integration/main.go b/tools/end2end_integration/main.go index 30a5637d98..3832d92986 100644 --- a/tools/end2end_integration/main.go +++ b/tools/end2end_integration/main.go @@ -44,6 +44,7 @@ var ( cmd string features string epic bool + useSciond bool ) func getCmd() (string, bool) { @@ -85,9 +86,14 @@ func realMain() int { clientArgs = append(clientArgs, "--features", features) serverArgs = append(serverArgs, "--features", features) } - if !*integration.Docker { + if useSciond { + // Use remote daemon (connect to sciond via gRPC) clientArgs = append(clientArgs, "-sciond", integration.Daemon) serverArgs = append(serverArgs, "-sciond", integration.Daemon) + } else { + // Use standalone daemon by default (with topology file) + clientArgs = append(clientArgs, "-topoDir", integration.TopoDir) + serverArgs = append(serverArgs, "-topoDir", integration.TopoDir) } in := integration.NewBinaryIntegration(name, cmd, clientArgs, serverArgs) @@ -119,6 +125,9 @@ func addFlags() { flag.StringVar(&features, "features", "", fmt.Sprintf("enable development features (%v)", feature.String(&feature.Default{}, "|"))) flag.BoolVar(&epic, "epic", false, "Enable EPIC.") + flag.BoolVar(&useSciond, "sciond", false, + "Use remote SCION daemon instead of standalone daemon. "+ + "By default, standalone daemon with topology file is used.") } // runTests runs the end2end tests for all pairs. In case of an error the @@ -304,8 +313,12 @@ func clientTemplate(progressSock string) integration.Cmd { if progress { cmd.Args = append(cmd.Args, "-progress", progressSock) } - if !*integration.Docker { + if useSciond { + // Use remote daemon (connect to sciond via gRPC) cmd.Args = append(cmd.Args, "-sciond", integration.Daemon) + } else { + // Use standalone daemon by default (with topology file) + cmd.Args = append(cmd.Args, "-topoDir", integration.TopoDir) } return cmd } diff --git a/tools/integration/binary.go b/tools/integration/binary.go index bff4289782..b1a34c1afa 100644 --- a/tools/integration/binary.go +++ b/tools/integration/binary.go @@ -36,6 +36,8 @@ import ( const ( // Daemon is a placeholder for the Daemon server in the arguments. Daemon = "" + // TopoDir is a placeholder for the topology directory in the arguments. + TopoDir = "" // ServerPortReplace is a placeholder for the server port in the arguments. ServerPortReplace = "" // SrcIAReplace is a placeholder for the source IA in the arguments. @@ -119,6 +121,14 @@ func (bi *binaryIntegration) StartServer(ctx context.Context, dst *snet.UDPAddr) } args = replacePattern(Daemon, daemonAddr, args) } + if needTopoDir(args) { + // In Docker mode, the gen directory is mounted at /share/gen inside the container + if *Docker { + args = replacePattern(TopoDir, "/share/gen", args) + } else { + args = replacePattern(TopoDir, GenFile(""), args) + } + } r := exec.CommandContext(ctx, bi.cmd, args...) log.Info(fmt.Sprintf("%v %v\n", bi.cmd, strings.Join(args, " "))) r.Env = os.Environ() @@ -189,6 +199,14 @@ func (bi *binaryIntegration) StartClient(ctx context.Context, } args = replacePattern(Daemon, daemonAddr, args) } + if needTopoDir(args) { + // In Docker mode, the gen directory is mounted at /share/gen inside the container + if *Docker { + args = replacePattern(TopoDir, "/share/gen", args) + } else { + args = replacePattern(TopoDir, GenFile(""), args) + } + } r := &BinaryWaiter{ cmd: exec.CommandContext(ctx, bi.cmd, args...), logsWritten: make(chan struct{}), @@ -262,6 +280,15 @@ func needSCIOND(args []string) bool { return false } +func needTopoDir(args []string) bool { + for _, arg := range args { + if strings.Contains(arg, TopoDir) { + return true + } + } + return false +} + func clientID(src, dst *snet.UDPAddr) string { return fmt.Sprintf("%s_%s", addr.FormatIA(src.IA, addr.WithFileSeparator()), diff --git a/tools/integration/cmd.go b/tools/integration/cmd.go index d68da625b4..5e3733e535 100644 --- a/tools/integration/cmd.go +++ b/tools/integration/cmd.go @@ -47,6 +47,14 @@ func (c Cmd) Template(src, dst *snet.UDPAddr) (Cmd, error) { } args = replacePattern(Daemon, daemonAddr, args) } + if needTopoDir(args) { + // In Docker mode, the gen directory is mounted at /share/gen inside the container + if *Docker { + args = replacePattern(TopoDir, "/share/gen", args) + } else { + args = replacePattern(TopoDir, GenFile(""), args) + } + } return Cmd{Binary: c.Binary, Args: args}, nil } diff --git a/tools/integration/integrationlib/common.go b/tools/integration/integrationlib/common.go index b84877db40..f739355c4c 100644 --- a/tools/integration/integrationlib/common.go +++ b/tools/integration/integrationlib/common.go @@ -22,6 +22,7 @@ import ( "fmt" "net" "os" + "path/filepath" "time" "github.com/opentracing/opentracing-go" @@ -51,6 +52,7 @@ var ( Mode string Progress string daemonAddr string + topoDir string Attempts int logConsole string features string @@ -75,11 +77,20 @@ func addFlags() error { flag.Var(&Local, "local", "(Mandatory) address to listen on") flag.StringVar(&Mode, "mode", ModeClient, "Run in "+ModeClient+" or "+ModeServer+" mode") flag.StringVar(&Progress, "progress", "", "Socket to write progress to") - flag.StringVar(&daemonAddr, "sciond", envFlags.Daemon(), "SCION Daemon address") + flag.StringVar( + &daemonAddr, "sciond", "", + "SCION Daemon address. If set, uses remote daemon instead of standalone daemon.", + ) + flag.StringVar( + &topoDir, "topoDir", "", + "Directory containing topology files. Used for standalone daemon (default mode).", + ) flag.IntVar(&Attempts, "attempts", 1, "Number of attempts before giving up") flag.StringVar(&logConsole, "log.console", "info", "Console logging level: debug|info|error") - flag.StringVar(&features, "features", "", - fmt.Sprintf("enable development features (%v)", feature.String(&feature.Default{}, "|"))) + flag.StringVar( + &features, "features", "", + fmt.Sprintf("enable development features (%v)", feature.String(&feature.Default{}, "|")), + ) return nil } @@ -128,12 +139,43 @@ func validateFlags() { } } +// SDConn returns a daemon connector. +// If -sciond is specified, it connects to a remote daemon. +// Otherwise (default), it creates a standalone daemon connector using the topology file +// from -topoDir. func SDConn() daemon.Connector { - ctx, cancelF := context.WithTimeout(context.Background(), DefaultIOTimeout) - defer cancelF() - conn, err := daemon.NewService(daemonAddr).Connect(ctx) + // If sciond address is specified, use remote daemon + if daemonAddr != "" { + ctx, cancelF := context.WithTimeout(context.Background(), DefaultIOTimeout) + defer cancelF() + conn, err := daemon.NewService(daemonAddr).Connect(ctx) + if err != nil { + LogFatal("Unable to initialize SCION Daemon connection", "err", err) + } + return conn + } + + // Use standalone daemon by default (with topology file) + if topoDir == "" { + LogFatal("Either -sciond or -topoDir must be specified") + } + + // Construct topology file path from the local IA + asDir := addr.FormatAS(Local.IA.AS(), addr.WithDefaultPrefix(), addr.WithFileSeparator()) + asPath := filepath.Join(topoDir, asDir) + topoFile := filepath.Join(asPath, "topology.json") + + log.Debug("Using standalone daemon", "topology", topoFile) + topo, err := daemon.LoadASInfoFromFile(topoFile) + if err != nil { + LogFatal("Unable to load topology", "err", err, "topoFile", topoFile) + } + ctx := context.Background() + conn, err := daemon.NewStandaloneConnector( + ctx, topo, daemon.WithCertsDir(filepath.Join(asPath, "certs")), + ) if err != nil { - LogFatal("Unable to initialize SCION Daemon connection", "err", err) + LogFatal("Unable to create standalone daemon", "err", err, "topoFile", topoFile) } return conn }