diff --git a/relay/client/dialer/quic/quic.go b/relay/client/dialer/quic/quic.go index 0d26dd4b9..b81a78a08 100644 --- a/relay/client/dialer/quic/quic.go +++ b/relay/client/dialer/quic/quic.go @@ -2,7 +2,7 @@ package quic import ( "context" - "crypto/tls" + "errors" "fmt" "net" "strings" @@ -11,27 +11,16 @@ import ( "github.com/quic-go/quic-go" log "github.com/sirupsen/logrus" + quictls "github.com/netbirdio/netbird/relay/tls" nbnet "github.com/netbirdio/netbird/util/net" ) -const ( - dialTimeout = 30 * time.Second -) - -func Dial(address string) (net.Conn, error) { +func Dial(ctx context.Context, address string) (net.Conn, error) { quicURL, err := prepareURL(address) if err != nil { return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), dialTimeout) - defer cancel() - - tlsConf := &tls.Config{ - InsecureSkipVerify: true, // Set to true only for testing - NextProtos: []string{"netbird-relay"}, // Ensure this matches the server's ALPN - } - quicConfig := &quic.Config{ KeepAlivePeriod: 15 * time.Second, MaxIdleTimeout: 60 * time.Second, @@ -50,8 +39,12 @@ func Dial(address string) (net.Conn, error) { return nil, err } - session, err := quic.Dial(ctx, udpConn, udpAddr, tlsConf, quicConfig) + session, err := quic.Dial(ctx, udpConn, udpAddr, quictls.ClientQUICTLSConfig(), quicConfig) if err != nil { + if errors.Is(ctx.Err(), context.Canceled) { + log.Infof("QUIC connection aborted to: %s", quicURL) + return nil, err + } log.Errorf("failed to dial to Relay server via QUIC '%s': %s", quicURL, err) return nil, err } diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go index 364676b88..338e2aa6d 100644 --- a/relay/client/dialer/ws/ws.go +++ b/relay/client/dialer/ws/ws.go @@ -2,7 +2,7 @@ package ws import ( "context" - "crypto/tls" + "errors" "fmt" "net" "net/http" @@ -16,7 +16,7 @@ import ( nbnet "github.com/netbirdio/netbird/util/net" ) -func Dial(address string) (net.Conn, error) { +func Dial(ctx context.Context, address string) (net.Conn, error) { wsURL, err := prepareURL(address) if err != nil { return nil, err @@ -32,8 +32,12 @@ func Dial(address string) (net.Conn, error) { } parsedURL.Path = ws.URLPath - wsConn, resp, err := websocket.Dial(context.Background(), parsedURL.String(), opts) + wsConn, resp, err := websocket.Dial(ctx, parsedURL.String(), opts) if err != nil { + if errors.Is(ctx.Err(), context.Canceled) { + log.Infof("WebSocket connection aborted to: %s", wsURL) + return nil, err + } log.Errorf("failed to dial to Relay server '%s': %s", wsURL, err) return nil, err } @@ -60,10 +64,6 @@ func httpClientNbDialer() *http.Client { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return customDialer.DialContext(ctx, network, addr) }, - // Set up a TLS configuration that skips certificate verification - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, // This accepts invalid TLS certificates - }, } return &http.Client{ diff --git a/relay/server/server.go b/relay/server/server.go index a09f6c16a..1e9efe306 100644 --- a/relay/server/server.go +++ b/relay/server/server.go @@ -2,19 +2,10 @@ package server import ( "context" - "crypto/rand" - "crypto/rsa" "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" - "net" "sync" - "time" "github.com/hashicorp/go-multierror" - log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/metric" nberrors "github.com/netbirdio/netbird/client/errors" @@ -22,6 +13,7 @@ import ( "github.com/netbirdio/netbird/relay/server/listener" "github.com/netbirdio/netbird/relay/server/listener/quic" "github.com/netbirdio/netbird/relay/server/listener/ws" + quictls "github.com/netbirdio/netbird/relay/tls" ) // ListenerConfig is the configuration for the listener. @@ -64,19 +56,16 @@ func (r *Server) Listen(cfg ListenerConfig) error { } r.listeners = append(r.listeners, wSListener) - quicListener := &quic.Listener{ - Address: cfg.Address, + tlsConfigQUIC, err := quictls.ServerQUICTLSConfig(cfg.TLSConfig) + if err != nil { + return err } - if cfg.TLSConfig != nil { - quicListener.TLSConfig = cfg.TLSConfig - } else { - tlsConfig, err := generateTestTLSConfig() - if err != nil { - return err - } - quicListener.TLSConfig = tlsConfig + quicListener := &quic.Listener{ + Address: cfg.Address, + TLSConfig: tlsConfigQUIC, } + r.listeners = append(r.listeners, quicListener) errChan := make(chan error, len(r.listeners)) @@ -117,54 +106,3 @@ func (r *Server) Shutdown(ctx context.Context) error { func (r *Server) InstanceURL() string { return r.relay.instanceURL } - -// GenerateTestTLSConfig creates a self-signed certificate for testing -func generateTestTLSConfig() (*tls.Config, error) { - log.Infof("generating test TLS config") - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, err - } - - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - Organization: []string{"Test Organization"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 180), // Valid for 180 days - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - }, - BasicConstraintsValid: true, - DNSNames: []string{"localhost"}, - IPAddresses: []net.IP{net.ParseIP("192.168.0.10")}, - } - - // Create certificate - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) - if err != nil { - return nil, err - } - - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: certDER, - }) - - privateKeyPEM := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - }) - - tlsCert, err := tls.X509KeyPair(certPEM, privateKeyPEM) - if err != nil { - return nil, err - } - - return &tls.Config{ - Certificates: []tls.Certificate{tlsCert}, - NextProtos: []string{"netbird-relay"}, - }, nil -} diff --git a/relay/tls/alpn.go b/relay/tls/alpn.go new file mode 100644 index 000000000..29497d401 --- /dev/null +++ b/relay/tls/alpn.go @@ -0,0 +1,3 @@ +package tls + +const nbalpn = "nb-quic" diff --git a/relay/tls/client_dev.go b/relay/tls/client_dev.go new file mode 100644 index 000000000..2be2bcb12 --- /dev/null +++ b/relay/tls/client_dev.go @@ -0,0 +1,12 @@ +//go:build dev + +package tls + +import "crypto/tls" + +func ClientQUICTLSConfig() *tls.Config { + return &tls.Config{ + InsecureSkipVerify: true, // Debug mode allows insecure connections + NextProtos: []string{nbalpn}, // Ensure this matches the server's ALPN + } +} diff --git a/relay/tls/client_prod.go b/relay/tls/client_prod.go new file mode 100644 index 000000000..36fdcef20 --- /dev/null +++ b/relay/tls/client_prod.go @@ -0,0 +1,11 @@ +//go:build !dev + +package tls + +import "crypto/tls" + +func ClientQUICTLSConfig() *tls.Config { + return &tls.Config{ + NextProtos: []string{nbalpn}, + } +} diff --git a/relay/tls/doc.go b/relay/tls/doc.go new file mode 100644 index 000000000..ade5b9c74 --- /dev/null +++ b/relay/tls/doc.go @@ -0,0 +1,37 @@ +// Package tls provides utilities for configuring and managing +// Transport Layer Security (TLS) in server environments, with +// a focus on QUIC protocol support and testing configurations. +// +// The package includes functions for cloning and customizing +// TLS configurations as well as generating self-signed +// certificates for development and testing purposes. +// +// Key Features: +// +// - `ServerQUICTLSConfig`: Creates a server-side TLS configuration +// tailored for QUIC protocol with specified or default settings. +// QUIC requires a specific TLS configuration with proper ALPN +// (Application-Layer Protocol Negotiation) support, making the +// TLS settings crucial for establishing secure connections. +// +// - `ClientQUICTLSConfig`: Provides a client-side TLS configuration +// suitable for QUIC protocol. The configuration differs between +// development (insecure testing) and production (strict verification). +// +// - `generateTestTLSConfig`: Generates a self-signed TLS configuration +// for use in local development and testing scenarios. +// +// Usage: +// +// This package provides separate implementations for development +// and production environments. The development implementation +// (guarded by `//go:build dev`) supports testing configurations +// with self-signed certificates and insecure client connections. +// The production implementation (guarded by `//go:build !dev`) +// ensures that valid and secure TLS configurations are supplied +// and used. +// +// The QUIC protocol is highly reliant on properly configured TLS +// settings, and this package ensures that configurations meet the +// requirements for secure and efficient QUIC communication. +package tls diff --git a/relay/tls/server_dev.go b/relay/tls/server_dev.go new file mode 100644 index 000000000..35967ab47 --- /dev/null +++ b/relay/tls/server_dev.go @@ -0,0 +1,71 @@ +//go:build dev + +package tls + +import ( + "crypto/tls" + + log "github.com/sirupsen/logrus" +) + +func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) { + if originTLSCfg == nil { + log.Warnf("QUIC server will use self signed certificate for testing!") + return generateTestTLSConfig() + } + + cfg := originTLSCfg.Clone() + cfg.NextProtos = []string{"nb"} + return cfg, nil +} + +// GenerateTestTLSConfig creates a self-signed certificate for testing +func generateTestTLSConfig() (*tls.Config, error) { + log.Infof("generating test TLS config") + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Test Organization"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 180), // Valid for 180 days + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + + // Create certificate + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, err + } + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certDER, + }) + + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + + tlsCert, err := tls.X509KeyPair(certPEM, privateKeyPEM) + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{nbalpn}, + }, nil +} diff --git a/relay/tls/server_prod.go b/relay/tls/server_prod.go new file mode 100644 index 000000000..cae6e324f --- /dev/null +++ b/relay/tls/server_prod.go @@ -0,0 +1,17 @@ +//go:build !dev + +package tls + +import ( + "crypto/tls" + "fmt" +) + +func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) { + if originTLSCfg == nil { + return nil, fmt.Errorf("valid TLS config is required for QUIC listener") + } + cfg := originTLSCfg.Clone() + cfg.NextProtos = []string{nbalpn} + return cfg, nil +}