Pin QUIC fabric endpoint certificates
This commit is contained in:
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -373,6 +375,7 @@ type syntheticMeshState struct {
|
||||
StopListener func()
|
||||
QUICFabricServer *mesh.QUICFabricServer
|
||||
QUICFabricListenAddr string
|
||||
QUICFabricCertSHA256 string
|
||||
QUICFabricError string
|
||||
ConfigLoadError string
|
||||
}
|
||||
@@ -788,7 +791,7 @@ func startSyntheticMeshEndpoint(ctx context.Context, _ context.CancelFunc, cfg c
|
||||
listenerCfg := meshListenerRuntimeConfig(cfg, loadedConfig.MeshListener)
|
||||
listenerReport, stopListener := startSyntheticMeshHTTPServer(ctx, listenerCfg, identity, dynamicListenerHandler, len(peerEndpoints), len(routes), gateEnabled, runtimeEnabled)
|
||||
vpnFabricSessionPeers := mesh.NewFabricSessionPeerManager()
|
||||
quicFabricServer, quicFabricAddr, quicFabricErr := startQUICFabricEndpoint(ctx, cfg, identity)
|
||||
quicFabricServer, quicFabricAddr, quicFabricCertSHA256, quicFabricErr := startQUICFabricEndpoint(ctx, cfg, identity)
|
||||
return &syntheticMeshState{
|
||||
Runtime: runtime,
|
||||
Routes: routes,
|
||||
@@ -827,6 +830,7 @@ func startSyntheticMeshEndpoint(ctx context.Context, _ context.CancelFunc, cfg c
|
||||
StopListener: stopListener,
|
||||
QUICFabricServer: quicFabricServer,
|
||||
QUICFabricListenAddr: quicFabricAddr,
|
||||
QUICFabricCertSHA256: quicFabricCertSHA256,
|
||||
QUICFabricError: errorString(quicFabricErr),
|
||||
ConfigLoadError: errorString(err),
|
||||
}, stopListener, nil
|
||||
@@ -1166,16 +1170,16 @@ func bindSyntheticMeshListener(cfg config.Config) (net.Listener, string, bool, e
|
||||
return nil, "", false, err
|
||||
}
|
||||
|
||||
func startQUICFabricEndpoint(ctx context.Context, cfg config.Config, identity state.Identity) (*mesh.QUICFabricServer, string, error) {
|
||||
func startQUICFabricEndpoint(ctx context.Context, cfg config.Config, identity state.Identity) (*mesh.QUICFabricServer, string, string, error) {
|
||||
if !cfg.MeshQUICFabricEnabled {
|
||||
return nil, "", nil
|
||||
return nil, "", "", nil
|
||||
}
|
||||
if strings.TrimSpace(cfg.MeshQUICFabricListenAddr) == "" {
|
||||
return nil, "", fmt.Errorf("quic fabric enabled but listen addr is empty")
|
||||
return nil, "", "", fmt.Errorf("quic fabric enabled but listen addr is empty")
|
||||
}
|
||||
tlsConfig, err := quicFabricTLSConfig(identity)
|
||||
tlsConfig, certSHA256, err := quicFabricTLSConfig(identity)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", "", err
|
||||
}
|
||||
server, err := mesh.StartQUICFabricServer(ctx, mesh.QUICFabricServerConfig{
|
||||
ListenAddr: cfg.MeshQUICFabricListenAddr,
|
||||
@@ -1190,20 +1194,20 @@ func startQUICFabricEndpoint(ctx context.Context, cfg config.Config, identity st
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", "", err
|
||||
}
|
||||
addr := ""
|
||||
if server.Addr() != nil {
|
||||
addr = server.Addr().String()
|
||||
}
|
||||
log.Printf("quic fabric endpoint enabled: listen_addr=%s effective_addr=%s node_id=%s cluster_id=%s", cfg.MeshQUICFabricListenAddr, addr, identity.NodeID, identity.ClusterID)
|
||||
return server, addr, nil
|
||||
return server, addr, certSHA256, nil
|
||||
}
|
||||
|
||||
func quicFabricTLSConfig(identity state.Identity) (*tls.Config, error) {
|
||||
func quicFabricTLSConfig(identity state.Identity) (*tls.Config, string, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
commonName := firstNonEmpty(identity.NodeID, "rap-fabric-node")
|
||||
template := x509.Certificate{
|
||||
@@ -1217,15 +1221,16 @@ func quicFabricTLSConfig(identity state.Identity) (*tls.Config, error) {
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
sum := sha256.Sum256(certDER)
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{{
|
||||
Certificate: [][]byte{certDER},
|
||||
PrivateKey: key,
|
||||
}},
|
||||
NextProtos: []string{"rap-fabric-data-session-v1"},
|
||||
}, nil
|
||||
}, hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func isAddressInUse(err error) bool {
|
||||
@@ -1825,17 +1830,20 @@ func applyQUICFabricConfigIfChanged(ctx context.Context, cfg config.Config, iden
|
||||
_ = meshState.QUICFabricServer.Close()
|
||||
meshState.QUICFabricServer = nil
|
||||
meshState.QUICFabricListenAddr = ""
|
||||
meshState.QUICFabricCertSHA256 = ""
|
||||
}
|
||||
if !cfg.MeshQUICFabricEnabled {
|
||||
meshState.QUICFabricError = ""
|
||||
meshState.QUICFabricCertSHA256 = ""
|
||||
return
|
||||
}
|
||||
if meshState.QUICFabricServer != nil {
|
||||
return
|
||||
}
|
||||
server, addr, err := startQUICFabricEndpoint(ctx, cfg, identity)
|
||||
server, addr, certSHA256, err := startQUICFabricEndpoint(ctx, cfg, identity)
|
||||
meshState.QUICFabricServer = server
|
||||
meshState.QUICFabricListenAddr = addr
|
||||
meshState.QUICFabricCertSHA256 = certSHA256
|
||||
meshState.QUICFabricError = errorString(err)
|
||||
if err != nil {
|
||||
log.Printf("quic fabric endpoint unavailable: listen_addr=%s node_id=%s cluster_id=%s err=%v", cfg.MeshQUICFabricListenAddr, identity.NodeID, identity.ClusterID, err)
|
||||
@@ -2610,6 +2618,7 @@ func heartbeatPayload(cfg config.Config, identity state.Identity, meshState *syn
|
||||
"enabled": meshState.QUICFabricServer != nil,
|
||||
"listen_addr": cfg.MeshQUICFabricListenAddr,
|
||||
"effective_listen_addr": meshState.QUICFabricListenAddr,
|
||||
"tls_cert_sha256": meshState.QUICFabricCertSHA256,
|
||||
"error": meshState.QUICFabricError,
|
||||
}
|
||||
}
|
||||
@@ -3860,6 +3869,7 @@ func advertisedEndpointCandidates(cfg config.Config, identity state.Identity, me
|
||||
Region: cfg.MeshRegion,
|
||||
Priority: 5,
|
||||
PolicyTags: []string{"fast-path"},
|
||||
Metadata: quicFabricEndpointMetadata(meshState.QUICFabricCertSHA256),
|
||||
})
|
||||
}
|
||||
candidates = append(candidates, interfaceEndpointCandidates(cfg, identity, meshState, observedAt)...)
|
||||
@@ -3916,6 +3926,18 @@ func advertisedEndpointCandidates(cfg config.Config, identity state.Identity, me
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func quicFabricEndpointMetadata(certSHA256 string) json.RawMessage {
|
||||
certSHA256 = strings.TrimSpace(certSHA256)
|
||||
if certSHA256 == "" {
|
||||
return nil
|
||||
}
|
||||
payload, err := json.Marshal(map[string]string{"tls_cert_sha256": certSHA256})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func interfaceEndpointCandidates(cfg config.Config, identity state.Identity, meshState *syntheticMeshState, observedAt time.Time) []mesh.PeerEndpointCandidate {
|
||||
if meshState == nil {
|
||||
return nil
|
||||
@@ -4675,8 +4697,9 @@ func vpnFabricSessionTarget(meshState *syntheticMeshState, nextHop string) (mesh
|
||||
continue
|
||||
}
|
||||
return mesh.FabricTransportTarget{
|
||||
Endpoint: endpoint,
|
||||
Transport: item.Candidate.Transport,
|
||||
Endpoint: endpoint,
|
||||
Transport: item.Candidate.Transport,
|
||||
PeerCertSHA256: endpointCandidateTLSCertSHA256(item.Candidate),
|
||||
}, true
|
||||
}
|
||||
}
|
||||
@@ -4687,6 +4710,19 @@ func vpnFabricSessionTarget(meshState *syntheticMeshState, nextHop string) (mesh
|
||||
return mesh.FabricTransportTarget{Endpoint: endpoint}, true
|
||||
}
|
||||
|
||||
func endpointCandidateTLSCertSHA256(candidate mesh.PeerEndpointCandidate) string {
|
||||
if len(candidate.Metadata) == 0 {
|
||||
return ""
|
||||
}
|
||||
var metadata struct {
|
||||
TLSCertSHA256 string `json:"tls_cert_sha256"`
|
||||
}
|
||||
if err := json.Unmarshal(candidate.Metadata, &metadata); err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(metadata.TLSCertSHA256)
|
||||
}
|
||||
|
||||
func fabricSessionGatewayToken(identity state.Identity, assignment client.NodeVPNAssignment, nextHop string) string {
|
||||
tokenParts := []string{
|
||||
"rap_fsn_vpn",
|
||||
|
||||
@@ -782,6 +782,7 @@ func TestVPNFabricSessionTargetPrefersRankedQUICCandidate(t *testing.T) {
|
||||
ConnectivityMode: "direct",
|
||||
Priority: 10,
|
||||
LastVerifiedAt: &now,
|
||||
Metadata: json.RawMessage(`{"tls_cert_sha256":"abcdef"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -789,7 +790,7 @@ func TestVPNFabricSessionTargetPrefersRankedQUICCandidate(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("target missing")
|
||||
}
|
||||
if target.Endpoint != "quic://node-b.example.test:19443" || target.Transport != "direct_quic" {
|
||||
if target.Endpoint != "quic://node-b.example.test:19443" || target.Transport != "direct_quic" || target.PeerCertSHA256 != "abcdef" {
|
||||
t.Fatalf("target = %+v, want direct quic candidate", target)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user