feat: use embedded bootstrap relays instead of registry.json fetch

Replace the runtime HTTP fetch of registry.json with the embedded bootstrap.relays list from config.toml, eliminating the GitHub round trip and making the relay seed list a build-time invariant. ResolvePortalRelayURLs moves from utils/network.go to utils/utils.go alongside the other relay URL helpers, and drops its now-unused ctx argument. The CLI flag stays --default-relays for backward compat, but the help text is updated to drop the stale "public registry" wording. registry.json and types.PortalRelayRegistryURL are removed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

oesni committed May 6, 2026 at 00:19 UTC e2dadc4548372e6959c98d9952dd14beeaac3800
9 files changed +35 -57
cmd/demo-app/main.go
+2 -2
@@ -53,8 +53,8 @@ type demoConfig struct {
53 // registerConnectivityFlags registers the relay, discovery, identity, and
54 // owner flags that are shared across TCP and UDP demo commands.
55 func registerConnectivityFlags(fs *flag.FlagSet, cfg *demoConfig, defaultRelays string) {
56 - utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", defaultRelays, "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays when discovery is enabled)", "RELAYS")
57 - utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY")
56 + utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", defaultRelays, "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with bootstrap relays when discovery is enabled)", "RELAYS")
57 + utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include bootstrap relays and enable discovery", "DISCOVERY")
58 utils.BoolFlagEnv(fs, &cfg.banMITM, "ban-mitm", false, "ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
59 utils.StringFlagEnv(fs, &cfg.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
60 utils.StringFlagEnv(fs, &cfg.identityJSON, "identity-json", "", "identity json payload; overrides --identity-path contents and is persisted there when both are set", "IDENTITY_JSON")
cmd/portal-tunnel/main.go
+3 -3
@@ -77,7 +77,7 @@ func runExposeCommand(args []string) error {
77
78 utils.StringFlag(fs, &flags.relayCSV, "relays", "", "Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https)")
79 utils.StringFlagEnv(fs, &flags.multiHopCSV, "multi-hop", "", "Ordered multi-hop relay API URLs, comma-separated", "MULTI_HOP")
80 - utils.BoolFlag(fs, &flags.discovery, "discovery", true, "Include public registry relays and discover additional relay bootstraps")
80 + utils.BoolFlag(fs, &flags.discovery, "discovery", true, "Include bootstrap relays and discover additional relays")
81 utils.BoolFlagEnv(fs, &flags.banMITM, "ban-mitm", true, "Ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
82 utils.StringFlagEnv(fs, &flags.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
83 utils.StringFlagEnv(fs, &flags.identityJSON, "identity-json", "", "identity json payload; overrides --identity-path contents and is persisted there when both are set", "IDENTITY_JSON")
@@ -219,7 +219,7 @@ func runListCommand(args []string) error {
219 fs := utils.NewFlagSet("list", printListUsage)
220
221 utils.StringFlag(fs, &flags.relayCSV, "relays", "", "Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https)")
222 - utils.BoolFlag(fs, &flags.defaultRelays, "default-relays", true, "Include public registry relays")
222 + utils.BoolFlag(fs, &flags.defaultRelays, "default-relays", true, "Include bootstrap relays")
223
224 if err := utils.ParseFlagSet(fs, args, printListUsage); err != nil {
225 if errors.Is(err, flag.ErrHelp) {
@@ -237,7 +237,7 @@ func runListCommand(args []string) error {
237
238 relayInputs := utils.SplitCSV(flags.relayCSV)
239
240 - relayURLs, err := utils.ResolvePortalRelayURLs(ctx, relayInputs, flags.defaultRelays)
240 + relayURLs, err := utils.ResolvePortalRelayURLs(relayInputs, flags.defaultRelays)
241 if err != nil {
242 return fmt.Errorf("resolve relay urls: %w", err)
243 }
cmd/relay-server/main.go
+1 -1
@@ -69,7 +69,7 @@ func runServeCommand(args []string) error {
69
70 utils.StringFlagEnv(fs, &cfg.PortalURL, "portal-url", "https://localhost:4017", "portal base URL", "PORTAL_URL")
71 utils.StringFlagEnv(fs, &cfg.IdentityPath, "identity-path", "./.portal-certs", "directory path for relay identity, admin state, and keyless materials", "IDENTITY_PATH")
72 - utils.StringFlagEnv(fs, &cfg.Bootstraps, "bootstraps", "", "bootstrap relay API URLs; merged with public registry relays when discovery is enabled", "BOOTSTRAPS")
72 + utils.StringFlagEnv(fs, &cfg.Bootstraps, "bootstraps", "", "bootstrap relay API URLs; merged with bootstrap relays when discovery is enabled", "BOOTSTRAPS")
73 utils.BoolFlagEnv(fs, &cfg.DiscoveryEnabled, "discovery", false, "serve relay discovery endpoints and poll discovery peers", "DISCOVERY")
74 utils.IntFlagEnv(fs, &cfg.WireGuardPort, "wireguard-port", overlay.DefaultListenPort, utils.ParsePortNumber, "public and listen UDP port for relay overlay", "WIREGUARD_PORT")
75
portal/server.go
+1 -1
@@ -148,7 +148,7 @@ func NewServer(cfg ServerConfig) (*Server, error) {
148 }
149 var relaySet *discovery.RelaySet
150 if cfg.DiscoveryEnabled {
151 - cfg.Bootstraps, err = utils.ResolvePortalRelayURLs(context.Background(), cfg.Bootstraps, true)
151 + cfg.Bootstraps, err = utils.ResolvePortalRelayURLs(cfg.Bootstraps, true)
152 if err != nil {
153 return nil, fmt.Errorf("resolve discovery bootstraps: %w", err)
154 }
registry.json deleted
-11
@@ -1,11 +0,0 @@
1 -{
2 - "relays": [
3 - "https://portal.thumbgo.kr/",
4 - "https://portal.rabbitson87.dev/",
5 - "https://s-h.day/",
6 - "https://portal.1ncursio.dev/",
7 - "https://portal.dawnfullstack.com/",
8 - "https://portal.damn.it.com/",
9 - "https://kakashit.org"
10 - ]
11 -}
sdk/expose.go
+2 -2
@@ -109,12 +109,12 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
109 listenerRelayURLs = []string{multiHop[len(multiHop)-1]}
110 relaySetURLs = append([]string(nil), multiHop...)
111 } else if cfg.MultiHopDepth > 1 {
112 - relaySetURLs, err = utils.ResolvePortalRelayURLs(ctx, explicitRelayURLs, cfg.Discovery)
112 + relaySetURLs, err = utils.ResolvePortalRelayURLs(explicitRelayURLs, cfg.Discovery)
113 if err != nil {
114 return nil, err
115 }
116 } else {
117 - relaySetURLs, err = utils.ResolvePortalRelayURLs(ctx, explicitRelayURLs, cfg.Discovery)
117 + relaySetURLs, err = utils.ResolvePortalRelayURLs(explicitRelayURLs, cfg.Discovery)
118 if err != nil {
119 return nil, err
120 }
types/types.go
+5 -2
@@ -9,8 +9,6 @@ import (
9 )
10
11 const (
12 - PortalRelayRegistryURL = "https://raw.githubusercontent.com/gosuda/portal-tunnel/main/registry.json"
13 -
12 HeaderAccessToken = "X-Portal-Access-Token"
13 MarkerKeepalive = byte(0x00)
14 MarkerRawStart = byte(0x01)
@@ -22,6 +20,7 @@ var (
20 SDKVersion string
21 DiscoveryVersion string
22 OfficialReleaseBaseURL string
23 + BootstrapRelays []string
24 )
25
26 func init() {
@@ -34,6 +33,9 @@ func init() {
33 Tunnel string `toml:"tunnel"`
34 Discovery string `toml:"discovery"`
35 } `toml:"protocol"`
36 + Bootstrap struct {
37 + Relays []string `toml:"relays"`
38 + } `toml:"bootstrap"`
39 }
40 if err := toml.Unmarshal(portaltunnel.ConfigTOML, &m); err != nil {
41 panic(fmt.Errorf("unmarshal config TOML: %w", err))
@@ -42,4 +44,5 @@ func init() {
44 OfficialReleaseBaseURL = m.Release.BaseURL
45 SDKVersion = m.Protocol.Tunnel
46 DiscoveryVersion = m.Protocol.Discovery
47 + BootstrapRelays = m.Bootstrap.Relays
48 }
utils/network.go
-35
@@ -140,38 +140,3 @@ func FetchRelayVersion(ctx context.Context, relayURL string) string {
140 }
141 return envelope.Data.ReleaseVersion
142 }
143 -
144 -func ResolvePortalRelayURLs(ctx context.Context, explicit []string, includeDefaults bool) ([]string, error) {
145 - explicit, err := NormalizeRelayURLs(explicit...)
146 - if err != nil {
147 - return nil, err
148 - }
149 - if !includeDefaults {
150 - return explicit, nil
151 - }
152 -
153 - client := NewHTTPClient(WithHTTPTimeout(3 * time.Second))
154 - var registry struct {
155 - Relays []string `json:"relays"`
156 - }
157 - resp, err := httpDo(ctx, client, http.MethodGet, types.PortalRelayRegistryURL, nil, nil)
158 - if err != nil {
159 - return explicit, nil
160 - }
161 - defer resp.Body.Close()
162 - if resp.StatusCode != http.StatusOK {
163 - return explicit, nil
164 - }
165 - if err := json.NewDecoder(resp.Body).Decode(&registry); err != nil {
166 - return explicit, nil
167 - }
168 -
169 - defaults, err := NormalizeRelayURLs(registry.Relays...)
170 - if err != nil {
171 - return explicit, nil
172 - }
173 - if len(defaults) == 0 {
174 - return explicit, nil
175 - }
176 - return MergeRelayURLs(defaults, nil, explicit)
177 -}
utils/utils.go
+21
@@ -16,6 +16,8 @@ import (
16 "unicode"
17
18 "golang.org/x/net/idna"
19 +
20 + "github.com/gosuda/portal-tunnel/v2/types"
21 )
22
23 func SplitCSV(raw string) []string {
@@ -360,6 +362,25 @@ func MergeRelayURLs(current, excluded, inputs []string) ([]string, error) {
362 return FilterRelayURLs(merged, excluded), nil
363 }
364
365 +func ResolvePortalRelayURLs(explicit []string, includeBootstrap bool) ([]string, error) {
366 + explicit, err := NormalizeRelayURLs(explicit...)
367 + if err != nil {
368 + return nil, err
369 + }
370 + if !includeBootstrap {
371 + return explicit, nil
372 + }
373 +
374 + defaults, err := NormalizeRelayURLs(types.BootstrapRelays...)
375 + if err != nil {
376 + return explicit, nil
377 + }
378 + if len(defaults) == 0 {
379 + return explicit, nil
380 + }
381 + return MergeRelayURLs(defaults, nil, explicit)
382 +}
383 +
384 func ExcludeLocalRelayURLs(inputs ...string) ([]string, error) {
385 normalized, err := NormalizeRelayURLs(inputs...)
386 if err != nil {