Files
VerseVDI-Data-Plane/gateway/apollo_native.go
T
sechmachine 67eeb9e898
Verify Data Plane / gateway (push) Successful in 1m52s
feat(gateway): bind Apollo launch to encrypted session work
2026-07-29 11:44:17 +07:00

367 lines
11 KiB
Go

package gateway
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
)
// NativeApolloBackend keeps provider sockets inside the gateway process. The
// 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 {
Dialer *net.Dialer
mu sync.Mutex
pending 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, request LaunchRequest) ([]byte, error) {
work := request.ProviderWork
if err := work.Validate(); err != nil || request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
return nil, ErrProviderMalformed
}
client, err := newPinnedApolloHTTPClient(work)
if err != nil {
return nil, err
}
return apolloGet(ctx, client, work, "/serverinfo", nil)
}
func newPinnedApolloHTTPClient(work protocol.ProviderSessionWork) (*http.Client, error) {
tlsConfig, err := pinnedApolloTLSConfig(work)
if err != nil {
return nil, err
}
return &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}, Timeout: 5 * time.Second}, nil
}
func apolloGet(ctx context.Context, client *http.Client, work protocol.ProviderSessionWork, path string, values url.Values) ([]byte, error) {
endpoint := url.URL{Scheme: "https", Host: net.JoinHostPort(work.ManagementHost, strconv.FormatInt(work.ManagementPort, 10)), Path: path}
endpoint.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, ErrProviderMalformed
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("provider management status %d", response.StatusCode)
}
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) {
work := request.ProviderWork
if err := work.Validate(); err != nil || request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
return nil, ErrProviderMalformed
}
client, err := newPinnedApolloHTTPClient(work)
if err != nil {
return nil, err
}
inventory, err := apolloGet(ctx, client, work, "/applist", url.Values{"uniqueid": []string{work.ClientID}})
if err != nil || !apolloInventoryContains(inventory, work.ApplicationID) {
return nil, ErrProviderMalformed
}
key := make([]byte, 16)
if _, err := rand.Read(key); err != nil {
return nil, err
}
var keyID [4]byte
if _, err := rand.Read(keyID[:]); err != nil {
return nil, err
}
launch, err := apolloGet(ctx, client, work, "/launch", url.Values{
"uniqueid": {work.ClientID}, "appid": {work.ApplicationID}, "rikey": {hex.EncodeToString(key)},
"rikeyid": {strconv.FormatUint(uint64(binary.BigEndian.Uint32(keyID[:])), 10)}, "localAudioPlayMode": {"0"},
"corever": {"1"},
})
if err != nil {
return nil, err
}
launchResponse, err := parseApolloLaunchResponse(launch)
if err != nil {
return nil, err
}
streamURL, err := url.Parse(launchResponse.SessionURL)
if err != nil || streamURL.Scheme != "rtspenc" || streamURL.Hostname() != work.StreamHost {
return nil, ErrProviderMalformed
}
streamPort, err := strconv.ParseInt(streamURL.Port(), 10, 64)
if err != nil || streamPort != work.StreamPort {
return nil, ErrProviderMalformed
}
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)
}
codec, err := newEncryptedRTSPCodec(key)
if err != nil {
_ = conn.Close()
return nil, err
}
requestText := "SETUP rtsp://" + work.StreamHost + "/streamid=video/0/0 RTSP/1.0\r\nCSeq: 1\r\nTransport: RTP/AVP/TCP;interleaved=0-1\r\n\r\n"
encoded, err := codec.SealClient([]byte(requestText))
if err != nil {
_ = conn.Close()
return nil, err
}
if _, err := conn.Write(encoded); err != nil {
_ = conn.Close()
return nil, err
}
response, err := readEncryptedRTSPHeaders(conn, codec)
if err != nil {
_ = conn.Close()
return nil, err
}
b.mu.Lock()
b.pending[request.SessionID] = conn
b.mu.Unlock()
return response, nil
}
func (b *NativeApolloBackend) Open(_ context.Context, request LaunchRequest, _ RTSPResponse) (ProviderSession, error) {
b.mu.Lock()
conn, ok := b.pending[request.SessionID]
delete(b.pending, request.SessionID)
b.mu.Unlock()
if !ok || conn == nil {
return nil, ErrProviderDisconnected
}
session := newNativeApolloSession(conn, request.SessionID)
go session.readMedia()
return session, nil
}
func readBounded(reader io.Reader, max int) ([]byte, error) {
data, err := io.ReadAll(io.LimitReader(reader, int64(max)+1))
if err != nil {
return nil, err
}
if len(data) > max {
return nil, ErrProviderMalformed
}
return data, nil
}
func readEncryptedRTSPHeaders(conn net.Conn, codec *encryptedRTSPCodec) ([]byte, error) {
header := make([]byte, encryptedRTSPHeaderSize)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, err
}
length := binary.BigEndian.Uint32(header[:4]) & 0x7fffffff
if length == 0 || length > encryptedRTSPMaxPayload {
return nil, ErrProviderMalformed
}
frame := make([]byte, encryptedRTSPHeaderSize+int(length))
copy(frame, header)
if _, err := io.ReadFull(conn, frame[encryptedRTSPHeaderSize:]); err != nil {
return nil, err
}
plaintext, err := codec.OpenHost(frame)
if err != nil || len(plaintext) > 16*1024 || !strings.HasSuffix(string(plaintext), "\r\n\r\n") {
return nil, ErrProviderMalformed
}
return plaintext, nil
}
type nativeApolloSession struct {
conn net.Conn
sessionID string
video chan []byte
audio chan []byte
mu sync.Mutex
state protocol.ProviderState
closeOnce sync.Once
done chan struct{}
readDone chan struct{}
}
func newNativeApolloSession(conn net.Conn, sessionID string) *nativeApolloSession {
return &nativeApolloSession{conn: conn, sessionID: sessionID, video: make(chan []byte, 16), audio: make(chan []byte, 16), state: protocol.ProviderState{Version: "1", SessionID: sessionID, State: ProviderStateStarting, Channels: []string{"video", "audio", "input", "feedback"}}, done: make(chan struct{}), readDone: make(chan struct{})}
}
func (s *nativeApolloSession) Ready(context.Context) error {
s.mu.Lock()
s.state.State = ProviderStateReady
s.mu.Unlock()
return nil
}
func (s *nativeApolloSession) Video() <-chan []byte { return s.video }
func (s *nativeApolloSession) Audio() <-chan []byte { return s.audio }
func (s *nativeApolloSession) Input(ctx context.Context, event InputEvent) error {
payload, err := EncodeInputEvent(event)
if err != nil {
return err
}
return s.writeControl(ctx, ControlPacket{Kind: 1, Sequence: event.Sequence, Payload: payload})
}
func (s *nativeApolloSession) Feedback(ctx context.Context, feedback Feedback) error {
return s.writeControl(ctx, ControlPacket{Kind: 3, Sequence: feedback.Sequence, Payload: feedback.Payload})
}
func (s *nativeApolloSession) Reconnect(ctx context.Context) error {
return s.writeControl(ctx, ControlPacket{Kind: 4, Payload: []byte("RECN")})
}
func (s *nativeApolloSession) ReleaseAll(ctx context.Context) error {
return s.writeControl(ctx, ControlPacket{Kind: 2, Payload: []byte("RELEASE_ALL")})
}
func (s *nativeApolloSession) Terminate(ctx context.Context) error {
_ = s.writeControl(ctx, ControlPacket{Kind: 5, Payload: []byte("TEAR")})
var timedOut bool
s.closeOnce.Do(func() {
close(s.done)
_ = s.conn.Close()
select {
case <-s.readDone:
case <-ctx.Done():
timedOut = true
}
if !timedOut {
close(s.video)
close(s.audio)
}
})
s.mu.Lock()
if timedOut {
s.state.State = ProviderStateCleanup
s.state.CleanupPending = true
} else {
s.state.State = ProviderStateTerminated
}
s.mu.Unlock()
if timedOut {
return ctx.Err()
}
return nil
}
func (s *nativeApolloSession) State() protocol.ProviderState {
s.mu.Lock()
defer s.mu.Unlock()
return s.state
}
func (s *nativeApolloSession) writeControl(ctx context.Context, packet ControlPacket) error {
encoded, err := EncodeControlPacket(packet)
if err != nil {
return err
}
if deadline, ok := ctx.Deadline(); ok {
_ = s.conn.SetWriteDeadline(deadline)
}
if _, err := s.conn.Write(encoded); err != nil {
return err
}
return nil
}
func (s *nativeApolloSession) readMedia() {
defer close(s.readDone)
header := make([]byte, 4)
for {
if _, err := io.ReadFull(s.conn, header); err != nil {
return
}
if header[0] != '$' || (header[1] != 0 && header[1] != 1) {
return
}
length := int(header[2])<<8 | int(header[3])
if length > 65536 {
return
}
payload := make([]byte, length)
if _, err := io.ReadFull(s.conn, payload); err != nil {
return
}
if header[1] == 0 {
pushLatest(s.video, payload)
} else {
pushLatest(s.audio, payload)
}
}
}
func pushLatest(channel chan []byte, payload []byte) {
select {
case channel <- payload:
default:
select {
case <-channel:
default:
}
select {
case channel <- payload:
default:
}
}
}
var _ ApolloBackend = (*NativeApolloBackend)(nil)
var _ ProviderSession = (*nativeApolloSession)(nil)