diff --git a/README.md b/README.md index 345f3f5..ef2fcce 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ Connection Server authority into a client. - The Protocol repository is the sole wire-contract authority; consumers pin an immutable release without a filesystem `replace`. +The gateway requires an explicit `-advertise-address host:port`; it is the +only endpoint placed in the Server registration and must be client-reachable. +Apollo management and RTSP addresses remain private gateway-side inputs. + ## Verification The baseline uses Go 1.26.5 and OpenSpec 1.5.0: diff --git a/cmd/verse-gateway/main.go b/cmd/verse-gateway/main.go index 0642f07..951f838 100644 --- a/cmd/verse-gateway/main.go +++ b/cmd/verse-gateway/main.go @@ -8,6 +8,7 @@ import ( "flag" "fmt" "log" + "net" "os" "os/signal" "strings" @@ -27,10 +28,11 @@ func main() { } func run() error { - var listen, controlPlane, certFile, keyFile, clientCAFile string + var listen, advertiseAddress, controlPlane, certFile, keyFile, clientCAFile string var gatewayID, instanceIdentity, certificateIdentity, publicIdentity string var providerManagement, providerRTSPAddress, providerRTSPURL, providerIdentity string flag.StringVar(&listen, "listen", "0.0.0.0:443", "gateway QUIC listen address") + flag.StringVar(&advertiseAddress, "advertise-address", "", "client-visible gateway address host:port") flag.StringVar(&controlPlane, "control-plane", "", "Connection Server HTTPS base URL") flag.StringVar(&certFile, "cert", "", "gateway certificate PEM") flag.StringVar(&keyFile, "key", "", "gateway private key PEM") @@ -44,11 +46,14 @@ func run() error { flag.StringVar(&providerRTSPURL, "provider-rtsp-url", "", "internal Apollo RTSP URL") flag.StringVar(&providerIdentity, "provider-identity", "", "enrolled Apollo identity unique-id#fingerprint") flag.Parse() - for name, value := range map[string]string{"control-plane": controlPlane, "cert": certFile, "key": keyFile, "client-ca": clientCAFile, "gateway-id": gatewayID, "instance-identity": instanceIdentity, "certificate-identity": certificateIdentity, "provider-management": providerManagement, "provider-rtsp-address": providerRTSPAddress, "provider-rtsp-url": providerRTSPURL, "provider-identity": providerIdentity} { + for name, value := range map[string]string{"control-plane": controlPlane, "advertise-address": advertiseAddress, "cert": certFile, "key": keyFile, "client-ca": clientCAFile, "gateway-id": gatewayID, "instance-identity": instanceIdentity, "certificate-identity": certificateIdentity, "provider-management": providerManagement, "provider-rtsp-address": providerRTSPAddress, "provider-rtsp-url": providerRTSPURL, "provider-identity": providerIdentity} { if value == "" { return fmt.Errorf("-%s is required", name) } } + if err := validateAdvertisedAddress(advertiseAddress); err != nil { + return err + } expectedIdentity, err := parseProviderIdentity(providerIdentity) if err != nil { return err @@ -66,7 +71,7 @@ func run() error { if err != nil { return err } - registration := protocol.GatewayRegistration{Version: "1", GatewayID: gatewayID, InstanceIdentity: instanceIdentity, CertificateIdentity: certificateIdentity, PublicIdentity: publicIdentity, Address: server.Addr().String(), ProviderIdentity: providerIdentity, ProtocolMinVersion: 1, ProtocolMaxVersion: 1, ConnectionCapacity: 8, BandwidthCapacityKbps: 100000, Features: []string{"quic-tls13", "datagram.media", "apollo"}, Capabilities: capabilities} + registration := protocol.GatewayRegistration{Version: "1", GatewayID: gatewayID, InstanceIdentity: instanceIdentity, CertificateIdentity: certificateIdentity, PublicIdentity: publicIdentity, Address: advertiseAddress, ProviderIdentity: providerIdentity, ProtocolMinVersion: 1, ProtocolMaxVersion: 1, ConnectionCapacity: 8, BandwidthCapacityKbps: 100000, Features: []string{"quic-tls13", "datagram.media", "apollo"}, Capabilities: capabilities} if _, err := controlPlaneClient.Register(context.Background(), registration); err != nil { _ = server.Close() return err @@ -77,6 +82,20 @@ func run() error { return server.Serve(ctx) } +func validateAdvertisedAddress(address string) error { + host, port, err := net.SplitHostPort(address) + if err != nil || host == "" || port == "" { + return fmt.Errorf("-advertise-address must be a host:port address") + } + if parsed := net.ParseIP(host); parsed != nil && parsed.IsUnspecified() { + return fmt.Errorf("-advertise-address must not be an unspecified address") + } + if net.JoinHostPort(host, port) != address { + return fmt.Errorf("-advertise-address must use canonical host:port syntax") + } + return nil +} + func heartbeatLoop(ctx context.Context, client *gateway.ControlPlaneClient, server *gateway.Server, registration protocol.GatewayRegistration) { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop()