diff --git a/control/beaconing/connect/BUILD.bazel b/control/beaconing/connect/BUILD.bazel index 6227377850..8d6268aff2 100644 --- a/control/beaconing/connect/BUILD.bazel +++ b/control/beaconing/connect/BUILD.bazel @@ -2,12 +2,22 @@ load("//tools/lint:go.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["server.go"], + srcs = [ + "sender.go", + "server.go", + ], importpath = "github.com/scionproto/scion/control/beaconing/connect", visibility = ["//visibility:public"], deps = [ + "//bufgen/proto/control_plane/v1/control_planeconnect:go_default_library", + "//control/beaconing:go_default_library", "//control/beaconing/grpc:go_default_library", + "//control/onehop:go_default_library", + "//pkg/addr:go_default_library", "//pkg/proto/control_plane:go_default_library", + "//pkg/segment:go_default_library", + "//pkg/snet/squic:go_default_library", "@com_connectrpc_connect//:go_default_library", + "@com_github_quic_go_quic_go//http3:go_default_library", ], ) diff --git a/control/beaconing/connect/sender.go b/control/beaconing/connect/sender.go new file mode 100644 index 0000000000..f3ada995e1 --- /dev/null +++ b/control/beaconing/connect/sender.go @@ -0,0 +1,71 @@ +package connect + +import ( + "context" + "net" + "net/http" + + "connectrpc.com/connect" + "github.com/quic-go/quic-go/http3" + "github.com/scionproto/scion/bufgen/proto/control_plane/v1/control_planeconnect" + "github.com/scionproto/scion/control/beaconing" + "github.com/scionproto/scion/control/onehop" + "github.com/scionproto/scion/pkg/addr" + control_plane "github.com/scionproto/scion/pkg/proto/control_plane" + seg "github.com/scionproto/scion/pkg/segment" + "github.com/scionproto/scion/pkg/snet/squic" +) + +type BeaconSenderFactory struct { + Dialer func(net.Addr) squic.EarlyDialer +} + +func (f *BeaconSenderFactory) NewSender( + ctx context.Context, + dstIA addr.IA, + egIfId uint16, + nextHop *net.UDPAddr, +) (beaconing.Sender, error) { + addr := &onehop.Addr{ + IA: dstIA, + Egress: egIfId, + SVC: addr.SvcCS, + NextHop: nextHop, + } + dialer := f.Dialer(addr) + return &BeaconSender{ + Addr: addr.String(), + Client: &HTTPClient{ + RoundTripper: &http3.RoundTripper{ + Dial: dialer.DialEarly, + }, + }, + }, nil + +} + +type BeaconSender struct { + Addr string + Client *HTTPClient +} + +func (s BeaconSender) Send(ctx context.Context, b *seg.PathSegment) error { + client := control_planeconnect.NewSegmentCreationServiceClient(s.Client, s.Addr) + _, err := client.Beacon(ctx, connect.NewRequest(&control_plane.BeaconRequest{ + Segment: seg.PathSegmentToPB(b), + })) + return err +} + +// Close closes the BeaconSender and releases all underlying resources. +func (s BeaconSender) Close() error { + return s.Client.RoundTripper.Close() +} + +type HTTPClient struct { + RoundTripper *http3.RoundTripper +} + +func (c HTTPClient) Do(req *http.Request) (*http.Response, error) { + return c.RoundTripper.RoundTrip(req) +} diff --git a/control/beaconing/happy/BUILD.bazel b/control/beaconing/happy/BUILD.bazel new file mode 100644 index 0000000000..8607fb8404 --- /dev/null +++ b/control/beaconing/happy/BUILD.bazel @@ -0,0 +1,15 @@ +load("//tools/lint:go.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["sender.go"], + importpath = "github.com/scionproto/scion/control/beaconing/happy", + visibility = ["//visibility:public"], + deps = [ + "//control/beaconing:go_default_library", + "//pkg/addr:go_default_library", + "//pkg/log:go_default_library", + "//pkg/private/serrors:go_default_library", + "//pkg/segment:go_default_library", + ], +) diff --git a/control/beaconing/happy/sender.go b/control/beaconing/happy/sender.go new file mode 100644 index 0000000000..ff7e1d2926 --- /dev/null +++ b/control/beaconing/happy/sender.go @@ -0,0 +1,90 @@ +package happy + +import ( + "context" + "net" + "time" + + "github.com/scionproto/scion/control/beaconing" + "github.com/scionproto/scion/pkg/addr" + "github.com/scionproto/scion/pkg/log" + "github.com/scionproto/scion/pkg/private/serrors" + seg "github.com/scionproto/scion/pkg/segment" +) + +// BeaconSenderFactory can be used to create beacon senders. +type BeaconSenderFactory struct { + Connect beaconing.SenderFactory + Grpc beaconing.SenderFactory +} + +// NewSender returns a beacon sender that can be used to send beacons to a remote CS. +func (f *BeaconSenderFactory) NewSender( + ctx context.Context, + dstIA addr.IA, + egIfId uint16, + nextHop *net.UDPAddr, +) (beaconing.Sender, error) { + connectSender, err := f.Connect.NewSender(ctx, dstIA, egIfId, nextHop) + if err != nil { + return nil, err + } + grpcSender, err := f.Grpc.NewSender(ctx, dstIA, egIfId, nextHop) + if err != nil { + return nil, err + } + return BeaconSender{ + Connect: connectSender, + Grpc: grpcSender, + }, nil +} + +type BeaconSender struct { + Connect beaconing.Sender + Grpc beaconing.Sender +} + +func (s BeaconSender) Send(ctx context.Context, b *seg.PathSegment) error { + abortCtx, cancel := context.WithCancel(ctx) + defer cancel() + + connectCh := make(chan error, 1) + grpcCh := make(chan error, 1) + + go func() { + defer log.HandlePanic() + err := s.Connect.Send(abortCtx, b) + if abortCtx.Err() == nil { + log.Debug("Sent beacon via connect") + } + connectCh <- err + }() + + go func() { + defer log.HandlePanic() + time.Sleep(500 * time.Millisecond) + err := s.Grpc.Send(abortCtx, b) + if abortCtx.Err() == nil { + log.Debug("Sent beacon via gRPC") + } + grpcCh <- err + }() + + select { + case err := <-connectCh: + return err + case err := <-grpcCh: + return err + } +} + +func (s BeaconSender) Close() error { + var errs serrors.List + if err := s.Connect.Close(); err != nil { + errs = append(errs, err) + } + if err := s.Grpc.Close(); err != nil { + errs = append(errs, err) + } + return errs.ToError() +} diff --git a/control/cmd/control/BUILD.bazel b/control/cmd/control/BUILD.bazel index f9e7a77846..6d11bcd232 100644 --- a/control/cmd/control/BUILD.bazel +++ b/control/cmd/control/BUILD.bazel @@ -16,6 +16,7 @@ go_library( "//control/beaconing:go_default_library", "//control/beaconing/connect:go_default_library", "//control/beaconing/grpc:go_default_library", + "//control/beaconing/happy:go_default_library", "//control/config:go_default_library", "//control/drkey:go_default_library", "//control/drkey/grpc:go_default_library", diff --git a/control/cmd/control/main.go b/control/cmd/control/main.go index 63ae5e0ec8..dc08bb30d7 100644 --- a/control/cmd/control/main.go +++ b/control/cmd/control/main.go @@ -43,8 +43,10 @@ import ( cs "github.com/scionproto/scion/control" "github.com/scionproto/scion/control/beacon" "github.com/scionproto/scion/control/beaconing" + "github.com/scionproto/scion/control/beaconing/connect" beaconingconnect "github.com/scionproto/scion/control/beaconing/connect" beaconinggrpc "github.com/scionproto/scion/control/beaconing/grpc" + "github.com/scionproto/scion/control/beaconing/happy" "github.com/scionproto/scion/control/config" "github.com/scionproto/scion/control/drkey" drkeygrpc "github.com/scionproto/scion/control/drkey/grpc" @@ -825,8 +827,15 @@ func realMain(ctx context.Context) error { TrustDB: trustDB, PathDB: pathDB, RevCache: revCache, - BeaconSenderFactory: &beaconinggrpc.BeaconSenderFactory{ - Dialer: dialer, + BeaconSenderFactory: &happy.BeaconSenderFactory{ + Connect: &connect.BeaconSenderFactory{ + Dialer: (&squic.EarlyDialerFactory{ + Transport: quicStack.InsecureDialer.Transport, + }).NewDialer, + }, + Grpc: &beaconinggrpc.BeaconSenderFactory{ + Dialer: dialer, + }, }, SegmentRegister: beaconinggrpc.Registrar{Dialer: dialer}, BeaconStore: beaconStore, diff --git a/pkg/snet/squic/BUILD.bazel b/pkg/snet/squic/BUILD.bazel index b05803f897..600d0667c5 100644 --- a/pkg/snet/squic/BUILD.bazel +++ b/pkg/snet/squic/BUILD.bazel @@ -2,7 +2,10 @@ load("//tools/lint:go.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["net.go"], + srcs = [ + "early.go", + "net.go", + ], importpath = "github.com/scionproto/scion/pkg/snet/squic", visibility = ["//visibility:public"], deps = [ diff --git a/pkg/snet/squic/early.go b/pkg/snet/squic/early.go new file mode 100644 index 0000000000..c34108a8d9 --- /dev/null +++ b/pkg/snet/squic/early.go @@ -0,0 +1,84 @@ +// 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 squic + +import ( + "context" + "crypto/tls" + "errors" + mrand "math/rand" + "net" + "time" + + "github.com/quic-go/quic-go" + + "github.com/scionproto/scion/pkg/private/serrors" +) + +type EarlyDialerFactory struct { + Transport *quic.Transport +} + +func (f *EarlyDialerFactory) NewDialer(a net.Addr) EarlyDialer { + return EarlyDialer{ + Transport: f.Transport, + Addr: a, + } +} + +type EarlyDialer struct { + Transport *quic.Transport + Addr net.Addr +} + +func (d *EarlyDialer) DialEarly(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + serverName := tlsCfg.ServerName + if serverName == "" { + serverName = computeServerName(d.Addr) + } + + var session quic.EarlyConnection + for sleep := 2 * time.Millisecond; ctx.Err() == nil; sleep = sleep * 2 { + // Clone TLS config to avoid data races. + tlsConfig := tlsCfg.Clone() + tlsConfig.ServerName = serverName + // Clone QUIC config to avoid data races, if it exists. + var quicConfig *quic.Config + if cfg != nil { + quicConfig = cfg.Clone() + } + + var err error + session, err = d.Transport.DialEarly(ctx, d.Addr, tlsConfig, quicConfig) + if err == nil { + break + } + var transportErr *quic.TransportError + if !errors.As(err, &transportErr) || transportErr.ErrorCode != quic.ConnectionRefused { + return nil, serrors.WrapStr("dialing QUIC/SCION", err) + } + + jitter := time.Duration(mrand.Int63n(int64(5 * time.Millisecond))) + select { + case <-time.After(sleep + jitter): + case <-ctx.Done(): + return nil, serrors.WrapStr("timed out connecting to busy server", err) + } + } + if err := ctx.Err(); err != nil { + return nil, serrors.WrapStr("dialing QUIC/SCION, after loop", err) + } + return session, nil +}