feat(gateway): fetch sealed Apollo session work
Verify Data Plane / gateway (push) Successful in 3m33s

This commit is contained in:
sechmachine
2026-07-29 11:27:31 +07:00
parent 8a302cd8cc
commit 844e548c95
9 changed files with 217 additions and 44 deletions
+57 -20
View File
@@ -2,12 +2,17 @@ package gateway
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
@@ -16,36 +21,35 @@ import (
)
// NativeApolloBackend keeps provider sockets inside the gateway process. The
// RTSP endpoint is configuration owned by the gateway and is never serialized
// into a client manifest or authority.
// session-scoped Server work is the sole source of provider endpoint and mTLS
// material; it is never serialized into a client manifest or authority.
type NativeApolloBackend struct {
ManagementURL string
RTSPAddress string
RTSPURL string
HTTPClient *http.Client
Dialer *net.Dialer
TLSConfig *tls.Config
Dialer *net.Dialer
mu sync.Mutex
pending map[string]net.Conn
}
func NewNativeApolloBackend(managementURL, rtspAddress, rtspURL string, client *http.Client) *NativeApolloBackend {
if client == nil {
client = &http.Client{Timeout: 5 * time.Second}
}
return &NativeApolloBackend{ManagementURL: managementURL, RTSPAddress: rtspAddress, RTSPURL: rtspURL, HTTPClient: client, Dialer: &net.Dialer{Timeout: 5 * time.Second}, pending: make(map[string]net.Conn)}
func NewNativeApolloBackend() *NativeApolloBackend {
return &NativeApolloBackend{Dialer: &net.Dialer{Timeout: 5 * time.Second}, pending: make(map[string]net.Conn)}
}
func (b *NativeApolloBackend) Management(ctx context.Context) ([]byte, error) {
if b.ManagementURL == "" {
func (b *NativeApolloBackend) Management(ctx context.Context, request LaunchRequest) ([]byte, error) {
work := request.ProviderWork
if err := work.Validate(); err != nil || work.ProviderProfile != ProviderProfileApollo {
return nil, ErrProviderMalformed
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, b.ManagementURL, nil)
tlsConfig, err := pinnedApolloTLSConfig(work)
if err != nil {
return nil, err
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}, Timeout: 5 * time.Second}
managementURL := "https://" + net.JoinHostPort(work.ManagementHost, strconv.FormatInt(work.ManagementPort, 10)) + "/serverinfo"
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, managementURL, nil)
if err != nil {
return nil, ErrProviderMalformed
}
response, err := b.HTTPClient.Do(request)
response, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
@@ -56,18 +60,51 @@ func (b *NativeApolloBackend) Management(ctx context.Context) ([]byte, error) {
return readBounded(response.Body, 64*1024)
}
func pinnedApolloTLSConfig(work protocol.ProviderSessionWork) (*tls.Config, error) {
identity, ok := providerIdentityFromKey(work.ProviderIdentity)
if !ok || !strings.HasPrefix(identity.Fingerprint, "sha256:") {
return nil, ErrProviderIdentity
}
pinned, err := hex.DecodeString(strings.TrimPrefix(identity.Fingerprint, "sha256:"))
if err != nil || len(pinned) != sha256.Size {
return nil, ErrProviderIdentity
}
certificate, err := tls.X509KeyPair([]byte(work.ClientCertificatePem), []byte(work.ClientPrivateKeyPem))
if err != nil {
return nil, ErrProviderIdentity
}
trust := x509.NewCertPool()
if !trust.AppendCertsFromPEM([]byte(work.ServerCertificatePem)) {
return nil, ErrProviderIdentity
}
return &tls.Config{
MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, RootCAs: trust,
VerifyPeerCertificate: func(rawCertificates [][]byte, _ [][]*x509.Certificate) error {
if len(rawCertificates) == 0 {
return ErrProviderIdentity
}
digest := sha256.Sum256(rawCertificates[0])
if !bytes.Equal(digest[:], pinned) {
return ErrProviderIdentity
}
return nil
},
}, nil
}
func (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest) ([]byte, error) {
if b.RTSPAddress == "" || b.RTSPURL == "" || request.SessionID == "" {
work := request.ProviderWork
if err := work.Validate(); err != nil || request.SessionID == "" {
return nil, ErrProviderMalformed
}
conn, err := b.Dialer.DialContext(ctx, "tcp", b.RTSPAddress)
conn, err := b.Dialer.DialContext(ctx, "tcp", net.JoinHostPort(work.StreamHost, strconv.FormatInt(work.StreamPort, 10)))
if err != nil {
return nil, err
}
if deadline, ok := ctx.Deadline(); ok {
_ = conn.SetDeadline(deadline)
}
requestText := "SETUP " + b.RTSPURL + " RTSP/1.0\r\nCSeq: 1\r\nTransport: RTP/AVP/TCP;interleaved=0-1\r\nSession: " + request.SessionID + "\r\n\r\n"
requestText := "SETUP rtsp://" + work.StreamHost + "/streamid=video/0/0 RTSP/1.0\r\nCSeq: 1\r\nTransport: RTP/AVP/TCP;interleaved=0-1\r\nSession: " + request.SessionID + "\r\n\r\n"
if _, err := io.WriteString(conn, requestText); err != nil {
_ = conn.Close()
return nil, err
+73
View File
@@ -0,0 +1,73 @@
package gateway
import (
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"net"
"net/http"
"net/http/httptest"
"strconv"
"testing"
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
)
func TestNativeApolloManagementUsesSessionScopedMTLS(t *testing.T) {
serverTLS, clientTLS := testTLS(t)
server := httptest.NewUnstartedServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/serverinfo" || request.TLS == nil || len(request.TLS.PeerCertificates) != 1 {
http.Error(response, "mTLS required", http.StatusUnauthorized)
return
}
_, _ = response.Write([]byte("<root><uniqueid>apollo-server</uniqueid></root>"))
}))
server.TLS = serverTLS
server.StartTLS()
defer server.Close()
host, portText, err := net.SplitHostPort(server.Listener.Addr().String())
if err != nil {
t.Fatal(err)
}
port, err := strconv.ParseInt(portText, 10, 64)
if err != nil {
t.Fatal(err)
}
pinned := sha256.Sum256(serverTLS.Certificates[0].Certificate[0])
work := protocol.ProviderSessionWork{
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 0,
ExpiresAt: "2099-01-01T00:00:00Z", ProviderProfile: ProviderProfileApollo,
ProviderIdentity: "apollo-server#sha256:" + hex.EncodeToString(pinned[:]), PolicyVersionID: "policy-1", ApplicationID: "1",
ManagementHost: host, ManagementPort: port, StreamHost: host, StreamPort: 47984,
ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]),
ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]),
ServerCertificatePem: certificatePEM(t, tls.Certificate{Certificate: [][]byte{serverTLS.Certificates[0].Certificate[1]}}),
}
data, err := NewNativeApolloBackend().Management(context.Background(), LaunchRequest{SessionID: "session-1", ProviderProfile: ProviderProfileApollo, ProviderWork: work})
if err != nil {
t.Fatalf("Management() error = %v", err)
}
if info, err := ParseManagementXML(data); err != nil || info.Identity.UniqueID != "apollo-server" {
t.Fatalf("ParseManagementXML() = %+v, %v", info, err)
}
}
func certificatePEM(t *testing.T, certificate tls.Certificate) string {
t.Helper()
if len(certificate.Certificate) == 0 {
t.Fatal("expected certificate")
}
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate.Certificate[0]}))
}
func privateKeyPEM(t *testing.T, certificate tls.Certificate) string {
t.Helper()
encoded, err := x509.MarshalPKCS8PrivateKey(certificate.PrivateKey)
if err != nil {
t.Fatal(err)
}
return string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: encoded}))
}
+12
View File
@@ -76,6 +76,18 @@ func (c *ControlPlaneClient) Release(ctx context.Context, authority protocol.Ses
return err
}
func (c *ControlPlaneClient) ProviderWork(ctx context.Context, authority protocol.SessionAuthority) (protocol.ProviderSessionWork, error) {
payload, err := protocol.EncodeSessionAuthority(authority)
if err != nil {
return protocol.ProviderSessionWork{}, err
}
response, err := c.post(ctx, "/api/v1/gateway/provider-work", payload)
if err != nil {
return protocol.ProviderSessionWork{}, err
}
return protocol.DecodeProviderSessionWork(response)
}
func (c *ControlPlaneClient) ReportProviderState(ctx context.Context, state protocol.ProviderState) error {
payload, err := protocol.EncodeProviderState(state)
if err != nil {
+15 -1
View File
@@ -307,7 +307,7 @@ func testTLS(t *testing.T) (*tls.Config, *tls.Config) {
if keyErr != nil {
t.Fatal(keyErr)
}
template := &x509.Certificate{SerialNumber: big.NewInt(serial), Subject: pkix.Name{CommonName: dns}, DNSNames: []string{dns}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), ExtKeyUsage: []x509.ExtKeyUsage{usage}, KeyUsage: x509.KeyUsageDigitalSignature}
template := &x509.Certificate{SerialNumber: big.NewInt(serial), Subject: pkix.Name{CommonName: dns}, DNSNames: []string{dns}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), ExtKeyUsage: []x509.ExtKeyUsage{usage}, KeyUsage: x509.KeyUsageDigitalSignature}
der, createErr := x509.CreateCertificate(rand.Reader, template, caCert, &key.PublicKey, caKey)
if createErr != nil {
t.Fatal(createErr)
@@ -353,6 +353,20 @@ func (a *oneTimeAdmission) Admit(context.Context, protocol.TunnelAdmissionReques
return a.authority, nil
}
func (a *oneTimeAdmission) ProviderWork(_ context.Context, authority protocol.SessionAuthority) (protocol.ProviderSessionWork, error) {
if authority != a.authority {
return protocol.ProviderSessionWork{}, ErrAdmissionRejected
}
return protocol.ProviderSessionWork{
Version: "1", SessionID: authority.SessionID, GatewayID: authority.GatewayID,
ReconnectSequence: authority.ReconnectSequence, ExpiresAt: authority.ExpiresAt,
ProviderProfile: ProviderProfileApollo, ProviderIdentity: authority.ProviderIdentity,
PolicyVersionID: "policy-1", ApplicationID: "1", ManagementHost: "apollo.test", ManagementPort: 47990,
StreamHost: "apollo.test", StreamPort: 47984, ClientCertificatePem: "certificate",
ClientPrivateKeyPem: "private-key", ServerCertificatePem: "server-certificate",
}, nil
}
func (a *oneTimeAdmission) Release(context.Context, protocol.SessionAuthority) error {
if a.releases.Add(1) == 1 {
close(a.released)
+30 -8
View File
@@ -44,8 +44,17 @@ func (i ProviderIdentity) Key() string {
return i.UniqueID + "#" + i.Fingerprint
}
func providerIdentityFromKey(value string) (ProviderIdentity, bool) {
uniqueID, fingerprint, ok := strings.Cut(strings.TrimSpace(value), "#")
if !ok || uniqueID == "" || fingerprint == "" || strings.Contains(fingerprint, "#") || len(uniqueID) > 128 || len(fingerprint) > 256 {
return ProviderIdentity{}, false
}
return ProviderIdentity{UniqueID: uniqueID, Fingerprint: fingerprint}, true
}
func (i ProviderIdentity) Validate(now time.Time, expected ProviderIdentity) error {
if i.UniqueID == "" || i.Fingerprint == "" || i.UniqueID != expected.UniqueID || i.Fingerprint != expected.Fingerprint {
if i.UniqueID == "" || expected.UniqueID == "" || i.UniqueID != expected.UniqueID ||
(i.Fingerprint != "" && i.Fingerprint != expected.Fingerprint) {
return ErrProviderIdentity
}
if !i.NotBefore.IsZero() && now.Before(i.NotBefore) {
@@ -68,7 +77,8 @@ func ParseManagementXML(data []byte) (ManagementInfo, error) {
}
var document struct {
XMLName xml.Name `xml:"root"`
UniqueID string `xml:"unique_id"`
UniqueID string `xml:"uniqueid"`
LegacyID string `xml:"unique_id"`
Fingerprint string `xml:"fingerprint"`
NotBefore string `xml:"not_before"`
NotAfter string `xml:"not_after"`
@@ -79,6 +89,9 @@ func ParseManagementXML(data []byte) (ManagementInfo, error) {
if err := decoder.Decode(&document); err != nil {
return ManagementInfo{}, fmt.Errorf("%w: %v", ErrProviderMalformed, err)
}
if document.UniqueID == "" {
document.UniqueID = document.LegacyID
}
identity := ProviderIdentity{UniqueID: document.UniqueID, Fingerprint: document.Fingerprint}
var err error
if document.NotBefore != "" {
@@ -93,7 +106,7 @@ func ParseManagementXML(data []byte) (ManagementInfo, error) {
return ManagementInfo{}, ErrProviderMalformed
}
}
if identity.UniqueID == "" || len(identity.UniqueID) > 128 || identity.Fingerprint == "" || len(identity.Fingerprint) > 256 {
if identity.UniqueID == "" || len(identity.UniqueID) > 128 || len(identity.Fingerprint) > 256 {
return ManagementInfo{}, ErrProviderMalformed
}
return ManagementInfo{Identity: identity, Name: document.Name}, nil
@@ -181,6 +194,7 @@ type LaunchRequest struct {
Capabilities protocol.CapabilityProfile
ProviderProfile string
ProviderIdentity string
ProviderWork protocol.ProviderSessionWork
}
type InputEvent struct {
@@ -213,7 +227,7 @@ type ProviderSession interface {
}
type ApolloBackend interface {
Management(context.Context) ([]byte, error)
Management(context.Context, LaunchRequest) ([]byte, error)
Setup(context.Context, LaunchRequest) ([]byte, error)
Open(context.Context, LaunchRequest, RTSPResponse) (ProviderSession, error)
}
@@ -233,7 +247,7 @@ func (a *ApolloAdapter) Start(ctx context.Context, request LaunchRequest) (Provi
if a == nil || a.backend == nil || request.ProviderProfile != ProviderProfileApollo {
return nil, ErrProviderIdentity
}
management, err := a.backend.Management(ctx)
management, err := a.backend.Management(ctx, request)
if err != nil {
return nil, err
}
@@ -241,10 +255,18 @@ func (a *ApolloAdapter) Start(ctx context.Context, request LaunchRequest) (Provi
if err != nil {
return nil, err
}
if err := info.Identity.Validate(a.now(), a.expected); err != nil {
expected := a.expected
if request.ProviderWork.ProviderIdentity != "" {
parsed, ok := providerIdentityFromKey(request.ProviderWork.ProviderIdentity)
if !ok {
return nil, ErrProviderIdentity
}
expected = parsed
}
if err := info.Identity.Validate(a.now(), expected); err != nil {
return nil, err
}
if request.ProviderIdentity != "" && request.ProviderIdentity != info.Identity.Key() {
if request.ProviderIdentity != "" && info.Identity.UniqueID != expected.UniqueID {
return nil, ErrProviderIdentity
}
rawRTSP, err := a.backend.Setup(ctx, request)
@@ -320,7 +342,7 @@ func NewFakeApollo(config FakeApolloConfig) *FakeApollo {
return &FakeApollo{config: config}
}
func (f *FakeApollo) Management(context.Context) ([]byte, error) {
func (f *FakeApollo) Management(context.Context, LaunchRequest) ([]byte, error) {
if f.config.Failure == FakeFailureMalformed {
return []byte("<root>"), nil
}
+24 -1
View File
@@ -33,6 +33,7 @@ var (
type Admission interface {
Admit(context.Context, protocol.TunnelAdmissionRequest) (protocol.SessionAuthority, error)
ProviderWork(context.Context, protocol.SessionAuthority) (protocol.ProviderSessionWork, error)
Release(context.Context, protocol.SessionAuthority) error
}
@@ -44,6 +45,10 @@ func (f AdmissionFunc) Admit(ctx context.Context, request protocol.TunnelAdmissi
func (AdmissionFunc) Release(context.Context, protocol.SessionAuthority) error { return nil }
func (AdmissionFunc) ProviderWork(context.Context, protocol.SessionAuthority) (protocol.ProviderSessionWork, error) {
return protocol.ProviderSessionWork{}, ErrAdmissionRejected
}
type ProviderStateReporter interface {
ReportProviderState(context.Context, protocol.ProviderState) error
}
@@ -210,6 +215,12 @@ func (s *Server) handleConnection(parent context.Context, connection *quic.Conn)
_ = writeStableError(stream, "invalid_authority", err, false)
return
}
work, err := s.config.Admission.ProviderWork(ctx, authority)
if err != nil || s.validateProviderWork(work, authority) != nil {
_ = s.config.Admission.Release(context.Background(), authority)
_ = writeStableError(stream, "provider_work_unavailable", ErrAdmissionRejected, err != nil)
return
}
selected, err := IntersectCapabilities(s.config.Capabilities, s.config.ProviderCapabilities, request.Capabilities, authority.Capabilities)
if err != nil {
_ = s.config.Admission.Release(context.Background(), authority)
@@ -222,7 +233,7 @@ func (s *Server) handleConnection(parent context.Context, connection *quic.Conn)
_ = writeStableError(stream, "provider_state_unavailable", err, true)
return
}
providerSession, err := s.config.Provider.Start(ctx, LaunchRequest{SessionID: request.SessionID, Capabilities: selected, ProviderProfile: authority.ProviderProfile, ProviderIdentity: authority.ProviderIdentity})
providerSession, err := s.config.Provider.Start(ctx, LaunchRequest{SessionID: request.SessionID, Capabilities: selected, ProviderProfile: authority.ProviderProfile, ProviderIdentity: work.ProviderIdentity, ProviderWork: work})
if err != nil {
s.metrics.ProviderErrors.Add(1)
_ = s.reportProviderState(context.Background(), protocol.ProviderState{Version: "1", SessionID: request.SessionID, State: ProviderStateFailed, CleanupPending: false, Channels: []string{"video", "audio", "input", "feedback"}})
@@ -279,6 +290,18 @@ func (s *Server) validateAuthority(authority protocol.SessionAuthority, request
return nil
}
func (s *Server) validateProviderWork(work protocol.ProviderSessionWork, authority protocol.SessionAuthority) error {
if err := work.Validate(); err != nil {
return err
}
if work.SessionID != authority.SessionID || work.GatewayID != authority.GatewayID ||
work.ReconnectSequence != authority.ReconnectSequence || work.ExpiresAt != authority.ExpiresAt ||
work.ProviderProfile != authority.ProviderProfile {
return ErrAdmissionRejected
}
return nil
}
func (s *Server) addSession(session *gatewaySession) {
s.mu.Lock()
s.sessions[session] = struct{}{}