From 1367b4ba46efe638805d2988a6de8a1e4dc1eec7 Mon Sep 17 00:00:00 2001 From: Kim Date: Tue, 24 Mar 2026 16:01:58 +0900 Subject: [PATCH] fix bootstrap logics --- .env.example | 4 +- README.md | 20 +++++-- cmd/demo-app/main.go | 42 +++++++------- cmd/portal-tunnel/README.md | 10 ++-- cmd/portal-tunnel/main.go | 45 +++++++-------- cmd/portal-tunnel/relays.go | 4 +- cmd/relay-server/main.go | 9 ++- docker-compose.yml | 4 +- docs/architecture.md | 4 +- docs/deployment.md | 2 + portal/transport/datagram_client.go | 8 +-- sdk/expose.go | 88 +++++++++-------------------- sdk/listener.go | 4 +- utils/registry.go | 52 +++++++++++++++++ 14 files changed, 167 insertions(+), 129 deletions(-) create mode 100644 utils/registry.go diff --git a/.env.example b/.env.example index 8bd5150b..784a37d0 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ -# Public routing +# Public routing and discovery PORTAL_URL=https://localhost:4017 +BOOTSTRAPS=https://localhost:4017 +DISCOVERY=true # Listener ports API_PORT=4017 diff --git a/README.md b/README.md index 1f2d4f31..601fb045 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,22 @@ For deployment to a public domain, see [docs/deployment.md](docs/deployment.md). ### Expose Local Service via Tunnel -1. Run your local service. -2. Open the Portal relay site. -3. Click `Add your server` button. -4. Use the generated command to connect your local service. +For a local relay started with `docker compose up`: + +```bash +curl -ksSL https://localhost:4017/install.sh | bash +portal expose 3000 --relays https://localhost:4017 +``` + +```powershell +$ProgressPreference = 'SilentlyContinue' +irm https://localhost:4017/install.ps1 | iex +portal expose 3000 --relays https://localhost:4017 +``` + +Replace `https://localhost:4017` with your relay URL when using a hosted relay. +The relay landing page also generates the exact install command for the current relay. +For CLI usage and install details, see [cmd/portal-tunnel/README.md](cmd/portal-tunnel/README.md). ### Use the Go SDK (Advanced) diff --git a/cmd/demo-app/main.go b/cmd/demo-app/main.go index d69bcda7..b4d7cae5 100644 --- a/cmd/demo-app/main.go +++ b/cmd/demo-app/main.go @@ -31,23 +31,23 @@ func main() { } type demoConfig struct { - relayURLs string - defaultRelays bool - addr string - name string - desc string - tags string - owner string - hide bool - thumbnail string + relayURLs string + discovery bool + addr string + name string + desc string + tags string + owner string + hide bool + thumbnail string } func runTCPCommand(args []string) error { cfg := demoConfig{} fs := utils.NewFlagSet("demo-app", printTCPUsage) - utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://localhost:4017", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays unless --default-relays=false is set)", "RELAYS") - utils.BoolFlagEnv(fs, &cfg.defaultRelays, "default-relays", true, "include public registry relays", "DEFAULT_RELAYS") + utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://gosunuts.xyz", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays when discovery is enabled)", "RELAYS") + utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY") utils.StringFlag(fs, &cfg.addr, "addr", "127.0.0.1:8092", "local demo HTTP listen address (host:port or URL; disable if empty)") utils.StringFlag(fs, &cfg.name, "name", "demo-app", "public hostname prefix (single DNS label)") utils.StringFlag(fs, &cfg.desc, "description", "Portal demo connectivity app", "lease description") @@ -77,8 +77,8 @@ func runUDPCommand(args []string) error { cfg := demoConfig{} fs := utils.NewFlagSet("demo-app-udp", printUDPUsage) - utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://localhost:4017", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays unless --default-relays=false is set)", "RELAYS") - utils.BoolFlagEnv(fs, &cfg.defaultRelays, "default-relays", false, "include public registry relays", "DEFAULT_RELAYS") + utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://localhost:4017", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays when discovery is enabled)", "RELAYS") + utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY") utils.StringFlag(fs, &cfg.name, "name", "demo-udp", "public hostname prefix (single DNS label)") utils.StringFlag(fs, &cfg.desc, "description", "Portal demo UDP echo service", "lease description") utils.StringFlag(fs, &cfg.tags, "tags", "demo,udp,echo", "comma-separated lease tags") @@ -131,9 +131,9 @@ func runHelpCommand(args []string) error { func runTCPDemo(ctx context.Context, cfg demoConfig) error { exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{ - RelayURLs: utils.SplitCSV(cfg.relayURLs), - DefaultRelayEnabled: cfg.defaultRelays, - Name: cfg.name, + RelayURLs: utils.SplitCSV(cfg.relayURLs), + Name: cfg.name, + Discovery: cfg.discovery, Metadata: types.LeaseMetadata{ Description: cfg.desc, Tags: utils.SplitCSV(cfg.tags), @@ -170,10 +170,10 @@ func runTCPDemo(ctx context.Context, cfg demoConfig) error { func runUDPDemo(ctx context.Context, cfg demoConfig) error { exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{ - RelayURLs: utils.SplitCSV(cfg.relayURLs), - DefaultRelayEnabled: cfg.defaultRelays, - Name: cfg.name, - UDPEnabled: true, + RelayURLs: utils.SplitCSV(cfg.relayURLs), + Name: cfg.name, + UDPEnabled: true, + Discovery: cfg.discovery, Metadata: types.LeaseMetadata{ Description: cfg.desc, Tags: utils.SplitCSV(cfg.tags), @@ -273,7 +273,7 @@ func printUDPUsage(w io.Writer) { []string{ "demo-app udp", "demo-app udp --name my-udp-demo", - "demo-app udp --default-relays=true", + "demo-app udp --discovery=true", }, ) } diff --git a/cmd/portal-tunnel/README.md b/cmd/portal-tunnel/README.md index c0bc7a04..3a280fdb 100644 --- a/cmd/portal-tunnel/README.md +++ b/cmd/portal-tunnel/README.md @@ -36,13 +36,13 @@ portal expose localhost:8080 \ - Bare ports resolve to `127.0.0.1:`. - `--name` is optional. When omitted, the CLI generates a name for that run. - `--relays` sets the relay API URLs for that run. -- `--default-relays=false` disables the public registry list for that run. +- `--discovery=false` disables the public registry seed list and the discovery expansion loop for that run. Flags: ```text --relays Portal relay API URLs (comma-separated, https only) ---default-relays Include public registry relays +--discovery Include public registry relays and discover additional relay bootstraps --name Public hostname prefix (single DNS label); auto-generated when omitted --description Service description metadata --tags Service tags metadata (comma-separated) @@ -54,7 +54,7 @@ Flags: ### `portal list [flags]` - Prints the relay URLs that the CLI will use for the current invocation. -- `--relays` and `--default-relays=false` follow the same semantics as `portal expose`. +- `--relays` adds explicit relay URLs, and `--default-relays=false` disables the public registry list for the current listing run. Legacy execution compatibility has been removed: @@ -67,7 +67,7 @@ Legacy execution compatibility has been removed: - `install.sh` installs the downloaded binary as `portal`. - `install.ps1` installs `portal.exe` for the current Windows user and updates the user `PATH`. - The installer does not write a config file. -- `portal expose 3000` still works after install because default relays are enabled. +- `portal expose 3000` still works after install because discovery is enabled by default. - Use `--relays https://portal.example.com` only when you want to target a specific relay explicitly. ## Notes @@ -77,7 +77,7 @@ Legacy execution compatibility has been removed: - The tunnel consumes one aggregate SDK listener, so the CLI no longer manages per-relay listener loops itself. - Relay startup and reconnect failures are retried independently in the background. A relay that is down does not stop healthy relays from continuing to serve traffic. - The tunnel starts once relay URLs pass local validation. Remote compatibility checks, lease registration, and reconnects continue in the background until each relay becomes ready. -- The configured relay list is either `public registry + --relays values` or, with `--default-relays=false`, just the explicit relay URLs. Published public URLs appear only for relays that have registered successfully. +- With discovery enabled, the configured relay list starts with `public registry + --relays values` and can expand through relay discovery. With `--discovery=false`, only the explicit relay URLs are used. Published public URLs appear only for relays that have registered successfully. - SDK callers that do not set `ListenerConfig.RetryCount` use infinite retry semantics for each relay. - Tenant TLS is provisioned automatically through the relay keyless signer. The SDK fetches the relay certificate chain and uses `/v1/sign` for remote signing. - When the local service is unreachable, the tunnel returns an HTTP 503 page. diff --git a/cmd/portal-tunnel/main.go b/cmd/portal-tunnel/main.go index b23a6ccc..4151fc7c 100644 --- a/cmd/portal-tunnel/main.go +++ b/cmd/portal-tunnel/main.go @@ -32,19 +32,18 @@ func main() { } type exposeFlags struct { - relayCSV string - defaultRelays bool - discoveryEnabled bool - privateKey string - name string - desc string - tags string - owner string - thumbnail string - hide bool - targetAddr string - udp bool - udpAddr string + relayCSV string + discovery bool + privateKey string + name string + desc string + tags string + owner string + thumbnail string + hide bool + targetAddr string + udp bool + udpAddr string } func runExposeCommand(args []string) error { @@ -52,8 +51,7 @@ func runExposeCommand(args []string) error { fs := utils.NewFlagSet("expose", printExposeUsage) utils.StringFlag(fs, &flags.relayCSV, "relays", "", "Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https)") - utils.BoolFlag(fs, &flags.defaultRelays, "default-relays", true, "Include public registry relays") - utils.BoolFlag(fs, &flags.discoveryEnabled, "discovery", false, "Advertise known relay URLs and discover additional relay bootstraps") + utils.BoolFlag(fs, &flags.discovery, "discovery", true, "Include public registry relays and discover additional relay bootstraps") utils.StringFlag(fs, &flags.privateKey, "private-key", "", "Owner private key used to derive a discovery address") utils.StringFlag(fs, &flags.name, "name", "", "Public hostname prefix (single DNS label); auto-generated when omitted") utils.StringFlag(fs, &flags.desc, "description", "", "Service description metadata") @@ -92,13 +90,12 @@ func runExposeCommand(args []string) error { defer stop() exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{ - RelayURLs: utils.SplitCSV(flags.relayCSV), - DefaultRelayEnabled: flags.defaultRelays, - Name: flags.name, - TargetAddr: flags.targetAddr, - UDPAddr: flags.udpAddr, - UDPEnabled: flags.udp, - Discovery: flags.discoveryEnabled, + RelayURLs: utils.SplitCSV(flags.relayCSV), + Name: flags.name, + TargetAddr: flags.targetAddr, + UDPAddr: flags.udpAddr, + UDPEnabled: flags.udp, + Discovery: flags.discovery, Metadata: types.LeaseMetadata{ Description: flags.desc, Tags: utils.SplitCSV(flags.tags), @@ -142,7 +139,7 @@ func runListCommand(args []string) error { relayInputs := utils.SplitCSV(flags.relayCSV) - relayURLs, err := sdk.ResolveRelayURLs(ctx, relayInputs, flags.defaultRelays) + relayURLs, err := utils.ResolvePortalRelayURLs(ctx, relayInputs, flags.defaultRelays) if err != nil { return fmt.Errorf("resolve relay urls: %w", err) } @@ -255,7 +252,7 @@ func printExposeUsage(w io.Writer) { "portal expose 3000", "portal expose localhost:8080 --name my-app", "portal expose 3000 --udp --udp-addr 127.0.0.1:5353", - "portal expose 3000 --relays https://portal.example.com --default-relays=false", + "portal expose 3000 --relays https://portal.example.com --discovery=false", }, ) } diff --git a/cmd/portal-tunnel/relays.go b/cmd/portal-tunnel/relays.go index 57a880b6..bb9a5435 100644 --- a/cmd/portal-tunnel/relays.go +++ b/cmd/portal-tunnel/relays.go @@ -79,7 +79,7 @@ func proxyExposure(ctx context.Context, exposure *sdk.Exposure, serviceName stri log.Error().Err(udpErr).Msg("udp proxy exited with error") } if closeErr != nil { - log.Error().Err(closeErr).Msg("relay shutdown failed") + log.Warn().Err(closeErr).Msg("relay shutdown completed with cleanup errors") } if ctx.Err() != nil { @@ -126,7 +126,7 @@ func proxyRelayConnections(ctx context.Context, exposure *sdk.Exposure, localAdd go func(connID int64, relayConn net.Conn) { defer connWG.Done() if err := proxyConnection(ctx, localAddr, relayConn); err != nil { - log.Error().Err(err).Int64("conn_id", connID).Msg("proxy connection failed") + log.Debug().Err(err).Int64("conn_id", connID).Msg("proxy connection closed with an I/O error") } log.Info().Int64("conn_id", connID).Msg("proxy connection closed") }(connID, relayConn) diff --git a/cmd/relay-server/main.go b/cmd/relay-server/main.go index 7090d662..8067eb19 100644 --- a/cmd/relay-server/main.go +++ b/cmd/relay-server/main.go @@ -63,7 +63,7 @@ func runServeCommand(args []string) error { utils.IntFlagEnv(fs, &cfg.UDPPortCount, "udp-port-count", 0, utils.ParseNonNegativeInt, "Number of UDP ports to allocate for leases, starting at port 50000 (0=disabled)", "UDP_PORT_COUNT") utils.BoolFlagEnv(fs, &cfg.LandingPageEnabled, "landing-page-enabled", false, "enable landing page by default when no admin setting has been saved yet", "LANDING_PAGE_ENABLED") utils.StringFlagEnv(fs, &cfg.Bootstraps, "bootstraps", "", "additional bootstrap relay API URLs used for discovery expansion", "BOOTSTRAPS") - utils.BoolFlagEnv(fs, &cfg.DiscoveryEnabled, "discovery", false, "serve relay discovery endpoints and poll discovery peers", "DISCOVERY_ENABLED") + utils.BoolFlagEnv(fs, &cfg.DiscoveryEnabled, "discovery", false, "serve relay discovery endpoints and poll discovery peers", "DISCOVERY") utils.StringFlagEnv(fs, &cfg.OwnerPrivateKey, "owner-private-key", "", "relay owner private key used to derive a discovery address", "OWNER_PRIVATE_KEY") utils.StringFlagEnv(fs, &cfg.AdminSecretKey, "admin-secret-key", "", "admin auth secret", "ADMIN_SECRET_KEY") utils.BoolFlagEnv(fs, &cfg.TrustProxyHeaders, "trust-proxy-headers", false, "trust X-Forwarded-* and X-Real-IP headers from trusted proxies", "TRUST_PROXY_HEADERS") @@ -106,10 +106,15 @@ func runServeCommand(args []string) error { } func runServer(ctx context.Context, cfg relayServerConfig) error { + bootstraps, err := utils.ResolvePortalRelayURLs(ctx, utils.SplitCSV(cfg.Bootstraps), cfg.DiscoveryEnabled) + if err != nil { + return fmt.Errorf("resolve discovery bootstraps: %w", err) + } + server, err := portal.NewServer(portal.ServerConfig{ PortalURL: cfg.PortalURL, OwnerPrivateKey: cfg.OwnerPrivateKey, - Bootstraps: []string{cfg.Bootstraps}, + Bootstraps: bootstraps, ACME: acme.Config{ KeyDir: cfg.KeylessDir, DNSProvider: cfg.ACMEDNSProvider, diff --git a/docker-compose.yml b/docker-compose.yml index c037c7f0..8a64b9b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,8 +12,10 @@ services: # - "${SNI_PORT:-443}:${SNI_PORT:-443}/udp" # - "50000-50009:50000-50009/udp" # adjust range to match UDP_PORT_COUNT environment: - # Public routing + # Public routing and discovery PORTAL_URL: ${PORTAL_URL:-https://localhost:${API_PORT:-4017}} + BOOTSTRAPS: ${BOOTSTRAPS:-} + DISCOVERY: ${DISCOVERY:-true} # Listener ports (published to the host below) API_PORT: ${API_PORT:-4017} diff --git a/docs/architecture.md b/docs/architecture.md index e64c2f8d..c4402ca6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -115,13 +115,13 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h ### SDK (`sdk/`) -- `ExposeConfig.DefaultRelayEnabled`: when true, `Expose` fetches the default Portal relay registry, merges it with explicit relay inputs, and normalizes the result +- `ExposeConfig.Discovery`: when true, `Expose` fetches the default Portal relay registry, merges it with explicit relay inputs, normalizes the result, and runs the relay discovery loop - Entry points can opt out of registry defaults and call `utils.NormalizeRelayURLs` directly when they need explicit relay inputs only - `Listener`: validates one relay URL locally, then starts relay compatibility checks, lease registration, reverse session maintenance, and lease renewal in the background until ready - `api_client.go`: internal relay client for control-plane requests, reverse session dialing, and internal QUIC tunnel setup - `ListenerConfig.RetryCount <= 0` means retry forever; positive values close the listener after the retry budget is exhausted - `NewListener` callers provide explicit normalized relay URLs -- Default exposure flow is `Expose{DefaultRelayEnabled: true} -> PublicURLs -> http.Server.Serve(exposure)`, with an opt-out path for explicit relay inputs only +- Default exposure flow is `Expose{Discovery: true} -> PublicURLs -> http.Server.Serve(exposure)`, with an opt-out path for explicit relay inputs only - `expose.go`: optional `RunHTTP` helper for serving one handler on both a local HTTP port and the relay listener - `Expose` keeps one listener per configured relay URL. Relay startup and reconnect failures are retried independently per relay, and successful relays remain available while failed relays keep retrying in the background - `Exposure.RelayURLs()` returns the configured normalized relay URLs, while `Exposure.PublicURLs()` returns only relays that are currently registered and ready diff --git a/docs/deployment.md b/docs/deployment.md index 621d9ab7..5bf91fc9 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -178,6 +178,8 @@ Navigate to `/admin`, toggle UDP transport to "Enabled", and optionally set a ma ```bash PORTAL_URL=https://example.com +BOOTSTRAPS= +DISCOVERY=true SNI_PORT=443 ADMIN_SECRET_KEY=your-admin-secret KEYLESS_DIR=./.portal-certs diff --git a/portal/transport/datagram_client.go b/portal/transport/datagram_client.go index 2bc53c65..879d914c 100644 --- a/portal/transport/datagram_client.go +++ b/portal/transport/datagram_client.go @@ -51,11 +51,11 @@ func (d *ClientDatagram) RunLoop( conn, err := open(ctx, state) if err != nil { - log.Warn(). + log.Info(). Err(err). Str("component", "sdk-datagram-plane"). Str("lease_id", state.LeaseID). - Msg("quic session open failed, retrying") + Msg("quic datagram plane unavailable; retrying") if !utils.SleepOrDone(ctx, 2*time.Second) { d.session.Stop("listener context closed") return @@ -74,11 +74,11 @@ func (d *ClientDatagram) RunLoop( if ctx.Err() != nil { return } - log.Warn(). + log.Info(). Err(err). Str("component", "sdk-datagram-plane"). Str("lease_id", state.LeaseID). - Msg("quic session bind failed") + Msg("quic datagram plane did not bind cleanly; retrying") if !utils.SleepOrDone(ctx, time.Second) { return } diff --git a/sdk/expose.go b/sdk/expose.go index e079220c..f2d4d0f2 100644 --- a/sdk/expose.go +++ b/sdk/expose.go @@ -2,7 +2,6 @@ package sdk import ( "context" - "encoding/json" "errors" "fmt" "net" @@ -49,23 +48,22 @@ type Exposure struct { } type ExposeConfig struct { - RelayURLs []string - DefaultRelayEnabled bool - Name string - TargetAddr string - UDPAddr string - ReverseToken string - UDPEnabled bool - Discovery bool - Metadata types.LeaseMetadata - OwnerPrivateKey string - RootCAPEM []byte + RelayURLs []string + Name string + TargetAddr string + UDPAddr string + ReverseToken string + UDPEnabled bool + Discovery bool + Metadata types.LeaseMetadata + OwnerPrivateKey string + RootCAPEM []byte } // Expose creates relay listeners for each normalized relay URL and exposes a // dynamic listener hub for accepting traffic from all of them. func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) { - relayURLs, err := ResolveRelayURLs(ctx, cfg.RelayURLs, cfg.DefaultRelayEnabled) + relayURLs, err := utils.ResolvePortalRelayURLs(ctx, cfg.RelayURLs, cfg.Discovery) if err != nil { return nil, err } @@ -134,50 +132,6 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) { return exposure, nil } -const defaultDiscoveryInterval = 30 * time.Second - -func ResolveRelayURLs(ctx context.Context, explicit []string, includeDefaults bool) ([]string, error) { - explicit, err := utils.NormalizeRelayURLs(explicit...) - if err != nil { - return nil, err - } - if !includeDefaults { - return explicit, nil - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, types.PortalRelayRegistryURL, nil) - if err != nil { - return explicit, nil - } - - client := &http.Client{Timeout: defaultRequestTimeout} - resp, err := client.Do(req) - if err != nil { - return explicit, nil - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return explicit, nil - } - - var registry struct { - Relays []string `json:"relays"` - } - if err := json.NewDecoder(resp.Body).Decode(®istry); err != nil { - return explicit, nil - } - - defaults, err := utils.NormalizeRelayURLs(registry.Relays...) - if err != nil { - return explicit, nil - } - if len(defaults) == 0 { - return explicit, nil - } - return utils.MergeRelayURLs(defaults, nil, explicit) -} - func (e *Exposure) KnownRelayURLs() []string { e.mu.RLock() defer e.mu.RUnlock() @@ -801,9 +755,12 @@ func (e *Exposure) monitorStartupCounts() { } } +const defaultDiscoveryInterval = 30 * time.Second + func (e *Exposure) runDiscoveryLoop(ctx context.Context) { ticker := time.NewTicker(defaultDiscoveryInterval) defer ticker.Stop() + discoveryFailed := false for { peers := e.KnownRelayURLs() @@ -811,6 +768,12 @@ func (e *Exposure) runDiscoveryLoop(ctx context.Context) { relayURLs, err := discovery.DiscoverBootstraps(ctx, peers, types.DiscoverRequest{}, e.rootCAPEM) switch { case err == nil: + if discoveryFailed { + log.Info(). + Int("peer_count", len(peers)). + Msg("relay discovery recovered") + } + discoveryFailed = false added, err := e.applyRelayURLs(relayURLs, false) if err != nil { log.Warn(). @@ -828,10 +791,13 @@ func (e *Exposure) runDiscoveryLoop(ctx context.Context) { case ctx.Err() != nil: return default: - log.Warn(). - Err(err). - Int("relay_count", len(peers)). - Msg("discover relay urls failed") + if !discoveryFailed { + log.Debug(). + Err(err). + Int("relay_count", len(peers)). + Msg("discover relay urls failed") + } + discoveryFailed = true } } diff --git a/sdk/listener.go b/sdk/listener.go index 43758319..cb0987be 100644 --- a/sdk/listener.go +++ b/sdk/listener.go @@ -113,11 +113,11 @@ func NewListener(ctx context.Context, relayURL string, cfg ListenerConfig) (*Lis l.stream = transport.NewClientStream(readyTarget, handshakeTimeout) if cfg.UDPEnabled { l.datagram = transport.NewClientDatagram(func(err error) { - log.Warn(). + log.Info(). Err(err). Str("component", "sdk-datagram-plane"). Str("lease_id", l.LeaseID()). - Msg("quic receive loop ended") + Msg("quic datagram plane disconnected; waiting to reconnect") }) } diff --git a/utils/registry.go b/utils/registry.go new file mode 100644 index 00000000..3a00d827 --- /dev/null +++ b/utils/registry.go @@ -0,0 +1,52 @@ +package utils + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "github.com/gosuda/portal/v2/types" +) + +func ResolvePortalRelayURLs(ctx context.Context, explicit []string, includeDefaults bool) ([]string, error) { + explicit, err := NormalizeRelayURLs(explicit...) + if err != nil { + return nil, err + } + if !includeDefaults { + return explicit, nil + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, types.PortalRelayRegistryURL, nil) + if err != nil { + return explicit, nil + } + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + return explicit, nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return explicit, nil + } + + var registry struct { + Relays []string `json:"relays"` + } + if err := json.NewDecoder(resp.Body).Decode(®istry); err != nil { + return explicit, nil + } + + defaults, err := NormalizeRelayURLs(registry.Relays...) + if err != nil { + return explicit, nil + } + if len(defaults) == 0 { + return explicit, nil + } + return MergeRelayURLs(defaults, nil, explicit) +}