package controlplane import ( "context" "crypto/sha256" "errors" "fmt" "io" "math" "time" controlplanev1 "proxy-pool/gen/controlplane/v1" proxyDomain "proxy-pool/internal/domain/proxy" "proxy-pool/internal/domain/workerruntime" "proxy-pool/internal/gateway/snapshot" "google.golang.org/grpc" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" ) var ( ErrInvalidSnapshotWatcher = errors.New("invalid gateway snapshot watcher") ErrSnapshotChecksum = errors.New("worker snapshot checksum mismatch") ErrSnapshotDelta = errors.New("worker snapshot delta requires a full snapshot") ) type SnapshotStream interface { Recv() (*controlplanev1.SnapshotEnvelope, error) } type SnapshotRPCClient interface { Watch(context.Context, *controlplanev1.WatchSnapshotsRequest) (SnapshotStream, error) Acknowledge(context.Context, *controlplanev1.AcknowledgeSnapshotRequest) (*emptypb.Empty, error) } type SnapshotWatcherOptions struct { ClusterID string WorkerID string } type SnapshotWatcher struct { client SnapshotRPCClient store *snapshot.Store options SnapshotWatcherOptions } func NewSnapshotWatcher(client SnapshotRPCClient, store *snapshot.Store, options SnapshotWatcherOptions) (*SnapshotWatcher, error) { if client == nil || store == nil || !workerruntime.ValidIdentifier(options.ClusterID) || !workerruntime.ValidIdentifier(options.WorkerID) { return nil, ErrInvalidSnapshotWatcher } return &SnapshotWatcher{client: client, store: store, options: options}, nil } // Watch applies verified full snapshots in stream order. Delta support remains // intentionally fail-closed until the Gateway can validate a complete routing view. func (watcher *SnapshotWatcher) Watch(ctx context.Context, sessionID string) error { if watcher == nil || ctx == nil || !workerruntime.ValidIdentifier(sessionID) { return ErrInvalidSnapshotWatcher } request := &controlplanev1.WatchSnapshotsRequest{WorkerId: watcher.options.WorkerID, SessionId: sessionID} if current := watcher.store.Current(); current != nil { request.LastAppliedVersion = current.Version } stream, err := watcher.client.Watch(ctx, request) if err != nil { return err } for { envelope, err := stream.Recv() if errors.Is(err, io.EOF) { return nil } if err != nil { return err } if err := watcher.applyAndAcknowledge(ctx, sessionID, envelope); err != nil { return err } } } func (watcher *SnapshotWatcher) applyAndAcknowledge(ctx context.Context, sessionID string, envelope *controlplanev1.SnapshotEnvelope) error { if envelope == nil || envelope.GetFull() == nil { return ErrSnapshotDelta } full := envelope.GetFull() applyErr := watcher.applyFull(full) if len(full.GetChecksum()) != sha256.Size { return errors.Join(applyErr, ErrSnapshotChecksum) } acknowledgement := &controlplanev1.AcknowledgeSnapshotRequest{ WorkerId: watcher.options.WorkerID, SessionId: sessionID, Version: full.GetVersion(), OwnershipEpoch: full.GetOwnershipEpoch(), Checksum: append([]byte(nil), full.GetChecksum()...), Applied: applyErr == nil, } if applyErr != nil { acknowledgement.ErrorCode = "snapshot_apply_failed" } _, acknowledgeErr := watcher.client.Acknowledge(ctx, acknowledgement) if applyErr != nil || acknowledgeErr != nil { return errors.Join(applyErr, acknowledgeErr) } return nil } func (watcher *SnapshotWatcher) applyFull(full *controlplanev1.WorkerSnapshot) error { if full == nil || full.GetVersion() == 0 || full.GetOwnershipEpoch() == 0 { return ErrInvalidSnapshotWatcher } checksum, err := workerSnapshotChecksum(full) if err != nil { return err } if len(full.GetChecksum()) != sha256.Size || string(checksum[:]) != string(full.GetChecksum()) { return ErrSnapshotChecksum } proxies, err := wireProxies(full.GetProxies()) if err != nil { return err } envelope := snapshot.Envelope{ ClusterID: watcher.options.ClusterID, WorkerID: watcher.options.WorkerID, Epoch: full.GetOwnershipEpoch(), Version: full.GetVersion(), Full: true, Proxies: proxies, } envelope.Checksum = snapshot.Checksum(proxies) return watcher.store.Apply(envelope) } func workerSnapshotChecksum(full *controlplanev1.WorkerSnapshot) ([sha256.Size]byte, error) { if full == nil { return [sha256.Size]byte{}, ErrInvalidSnapshotWatcher } copy := proto.Clone(full).(*controlplanev1.WorkerSnapshot) copy.Checksum = nil encoded, err := proto.MarshalOptions{Deterministic: true}.Marshal(copy) if err != nil { return [sha256.Size]byte{}, fmt.Errorf("marshal worker snapshot: %w", err) } return sha256.Sum256(encoded), nil } func wireProxies(source []*controlplanev1.OwnedProxy) ([]proxyDomain.Proxy, error) { proxies := make([]proxyDomain.Proxy, len(source)) for index, item := range source { proxy, err := wireProxy(item) if err != nil { return nil, err } proxies[index] = proxy } return proxies, nil } func wireProxy(source *controlplanev1.OwnedProxy) (proxyDomain.Proxy, error) { if source == nil || !workerruntime.ValidIdentifier(source.GetId()) || !workerruntime.ValidIdentifier(source.GetUpstream()) || source.GetHost() == "" || source.GetPort() == 0 || source.GetPort() > math.MaxUint16 || source.GetMaxConcurrency() == 0 { return proxyDomain.Proxy{}, ErrInvalidSnapshotWatcher } scheme, ok := wireScheme(source.GetProtocol()) if !ok { return proxyDomain.Proxy{}, ErrInvalidSnapshotWatcher } expiresAt, err := wireTimestamp(source.GetExpiresAt()) if err != nil { return proxyDomain.Proxy{}, err } usableUntil, err := wireTimestamp(source.GetUsableUntil()) if err != nil { return proxyDomain.Proxy{}, err } tags := make(map[string]string, len(source.GetTags())) for key, value := range source.GetTags() { tags[key] = value } return proxyDomain.Proxy{ ID: source.GetId(), Scheme: scheme, Host: source.GetHost(), Port: uint16(source.GetPort()), Username: source.GetUsername(), CredentialVersion: source.GetCredentialVersion(), SecretRef: source.GetSecretRef(), SourceUpstream: source.GetUpstream(), ExpiresAt: expiresAt, UsableUntil: usableUntil, MaxConcurrency: int64(source.GetMaxConcurrency()), State: proxyDomain.StateAvailable, Tags: tags, }, nil } func wireScheme(protocol controlplanev1.ProxyProtocol) (proxyDomain.Scheme, bool) { switch protocol { case controlplanev1.ProxyProtocol_PROXY_PROTOCOL_HTTP: return proxyDomain.SchemeHTTP, true case controlplanev1.ProxyProtocol_PROXY_PROTOCOL_HTTPS: return proxyDomain.SchemeHTTPS, true case controlplanev1.ProxyProtocol_PROXY_PROTOCOL_SOCKS5: return proxyDomain.SchemeSOCKS5, true default: return "", false } } func wireTimestamp(value *timestamppb.Timestamp) (*time.Time, error) { if value == nil { return nil, nil } if err := value.CheckValid(); err != nil { return nil, ErrInvalidSnapshotWatcher } converted := value.AsTime().UTC() return &converted, nil } type generatedSnapshotRPCClient struct { client controlplanev1.WorkerControlPlaneClient } func NewGeneratedSnapshotRPCClient(client controlplanev1.WorkerControlPlaneClient) SnapshotRPCClient { return generatedSnapshotRPCClient{client: client} } func (client generatedSnapshotRPCClient) Watch(ctx context.Context, request *controlplanev1.WatchSnapshotsRequest) (SnapshotStream, error) { return client.client.WatchSnapshots(ctx, request, grpc.WaitForReady(true)) } func (client generatedSnapshotRPCClient) Acknowledge(ctx context.Context, request *controlplanev1.AcknowledgeSnapshotRequest) (*emptypb.Empty, error) { return client.client.AcknowledgeSnapshot(ctx, request, grpc.WaitForReady(true)) }