Skip to content

Commit 1367b4b

Browse files
committed
fix bootstrap logics
1 parent f79839d commit 1367b4b

14 files changed

Lines changed: 167 additions & 129 deletions

File tree

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
# Public routing
1+
# Public routing and discovery
22
PORTAL_URL=https://localhost:4017
3+
BOOTSTRAPS=https://localhost:4017
4+
DISCOVERY=true
35

46
# Listener ports
57
API_PORT=4017

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,22 @@ For deployment to a public domain, see [docs/deployment.md](docs/deployment.md).
4646

4747
### Expose Local Service via Tunnel
4848

49-
1. Run your local service.
50-
2. Open the Portal relay site.
51-
3. Click `Add your server` button.
52-
4. Use the generated command to connect your local service.
49+
For a local relay started with `docker compose up`:
50+
51+
```bash
52+
curl -ksSL https://localhost:4017/install.sh | bash
53+
portal expose 3000 --relays https://localhost:4017
54+
```
55+
56+
```powershell
57+
$ProgressPreference = 'SilentlyContinue'
58+
irm https://localhost:4017/install.ps1 | iex
59+
portal expose 3000 --relays https://localhost:4017
60+
```
61+
62+
Replace `https://localhost:4017` with your relay URL when using a hosted relay.
63+
The relay landing page also generates the exact install command for the current relay.
64+
For CLI usage and install details, see [cmd/portal-tunnel/README.md](cmd/portal-tunnel/README.md).
5365

5466
### Use the Go SDK (Advanced)
5567

cmd/demo-app/main.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,23 @@ func main() {
3131
}
3232

3333
type demoConfig struct {
34-
relayURLs string
35-
defaultRelays bool
36-
addr string
37-
name string
38-
desc string
39-
tags string
40-
owner string
41-
hide bool
42-
thumbnail string
34+
relayURLs string
35+
discovery bool
36+
addr string
37+
name string
38+
desc string
39+
tags string
40+
owner string
41+
hide bool
42+
thumbnail string
4343
}
4444

4545
func runTCPCommand(args []string) error {
4646
cfg := demoConfig{}
4747

4848
fs := utils.NewFlagSet("demo-app", printTCPUsage)
49-
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")
50-
utils.BoolFlagEnv(fs, &cfg.defaultRelays, "default-relays", true, "include public registry relays", "DEFAULT_RELAYS")
49+
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")
50+
utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY")
5151
utils.StringFlag(fs, &cfg.addr, "addr", "127.0.0.1:8092", "local demo HTTP listen address (host:port or URL; disable if empty)")
5252
utils.StringFlag(fs, &cfg.name, "name", "demo-app", "public hostname prefix (single DNS label)")
5353
utils.StringFlag(fs, &cfg.desc, "description", "Portal demo connectivity app", "lease description")
@@ -77,8 +77,8 @@ func runUDPCommand(args []string) error {
7777
cfg := demoConfig{}
7878
fs := utils.NewFlagSet("demo-app-udp", printUDPUsage)
7979

80-
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")
81-
utils.BoolFlagEnv(fs, &cfg.defaultRelays, "default-relays", false, "include public registry relays", "DEFAULT_RELAYS")
80+
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")
81+
utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY")
8282
utils.StringFlag(fs, &cfg.name, "name", "demo-udp", "public hostname prefix (single DNS label)")
8383
utils.StringFlag(fs, &cfg.desc, "description", "Portal demo UDP echo service", "lease description")
8484
utils.StringFlag(fs, &cfg.tags, "tags", "demo,udp,echo", "comma-separated lease tags")
@@ -131,9 +131,9 @@ func runHelpCommand(args []string) error {
131131

132132
func runTCPDemo(ctx context.Context, cfg demoConfig) error {
133133
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
134-
RelayURLs: utils.SplitCSV(cfg.relayURLs),
135-
DefaultRelayEnabled: cfg.defaultRelays,
136-
Name: cfg.name,
134+
RelayURLs: utils.SplitCSV(cfg.relayURLs),
135+
Name: cfg.name,
136+
Discovery: cfg.discovery,
137137
Metadata: types.LeaseMetadata{
138138
Description: cfg.desc,
139139
Tags: utils.SplitCSV(cfg.tags),
@@ -170,10 +170,10 @@ func runTCPDemo(ctx context.Context, cfg demoConfig) error {
170170

171171
func runUDPDemo(ctx context.Context, cfg demoConfig) error {
172172
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
173-
RelayURLs: utils.SplitCSV(cfg.relayURLs),
174-
DefaultRelayEnabled: cfg.defaultRelays,
175-
Name: cfg.name,
176-
UDPEnabled: true,
173+
RelayURLs: utils.SplitCSV(cfg.relayURLs),
174+
Name: cfg.name,
175+
UDPEnabled: true,
176+
Discovery: cfg.discovery,
177177
Metadata: types.LeaseMetadata{
178178
Description: cfg.desc,
179179
Tags: utils.SplitCSV(cfg.tags),
@@ -273,7 +273,7 @@ func printUDPUsage(w io.Writer) {
273273
[]string{
274274
"demo-app udp",
275275
"demo-app udp --name my-udp-demo",
276-
"demo-app udp --default-relays=true",
276+
"demo-app udp --discovery=true",
277277
},
278278
)
279279
}

cmd/portal-tunnel/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ portal expose localhost:8080 \
3636
- Bare ports resolve to `127.0.0.1:<port>`.
3737
- `--name` is optional. When omitted, the CLI generates a name for that run.
3838
- `--relays` sets the relay API URLs for that run.
39-
- `--default-relays=false` disables the public registry list for that run.
39+
- `--discovery=false` disables the public registry seed list and the discovery expansion loop for that run.
4040

4141
Flags:
4242

4343
```text
4444
--relays Portal relay API URLs (comma-separated, https only)
45-
--default-relays Include public registry relays
45+
--discovery Include public registry relays and discover additional relay bootstraps
4646
--name Public hostname prefix (single DNS label); auto-generated when omitted
4747
--description Service description metadata
4848
--tags Service tags metadata (comma-separated)
@@ -54,7 +54,7 @@ Flags:
5454
### `portal list [flags]`
5555

5656
- Prints the relay URLs that the CLI will use for the current invocation.
57-
- `--relays` and `--default-relays=false` follow the same semantics as `portal expose`.
57+
- `--relays` adds explicit relay URLs, and `--default-relays=false` disables the public registry list for the current listing run.
5858

5959
Legacy execution compatibility has been removed:
6060

@@ -67,7 +67,7 @@ Legacy execution compatibility has been removed:
6767
- `install.sh` installs the downloaded binary as `portal`.
6868
- `install.ps1` installs `portal.exe` for the current Windows user and updates the user `PATH`.
6969
- The installer does not write a config file.
70-
- `portal expose 3000` still works after install because default relays are enabled.
70+
- `portal expose 3000` still works after install because discovery is enabled by default.
7171
- Use `--relays https://portal.example.com` only when you want to target a specific relay explicitly.
7272

7373
## Notes
@@ -77,7 +77,7 @@ Legacy execution compatibility has been removed:
7777
- The tunnel consumes one aggregate SDK listener, so the CLI no longer manages per-relay listener loops itself.
7878
- 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.
7979
- 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.
80-
- 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.
80+
- 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.
8181
- SDK callers that do not set `ListenerConfig.RetryCount` use infinite retry semantics for each relay.
8282
- Tenant TLS is provisioned automatically through the relay keyless signer. The SDK fetches the relay certificate chain and uses `/v1/sign` for remote signing.
8383
- When the local service is unreachable, the tunnel returns an HTTP 503 page.

cmd/portal-tunnel/main.go

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,26 @@ func main() {
3232
}
3333

3434
type exposeFlags struct {
35-
relayCSV string
36-
defaultRelays bool
37-
discoveryEnabled bool
38-
privateKey string
39-
name string
40-
desc string
41-
tags string
42-
owner string
43-
thumbnail string
44-
hide bool
45-
targetAddr string
46-
udp bool
47-
udpAddr string
35+
relayCSV string
36+
discovery bool
37+
privateKey string
38+
name string
39+
desc string
40+
tags string
41+
owner string
42+
thumbnail string
43+
hide bool
44+
targetAddr string
45+
udp bool
46+
udpAddr string
4847
}
4948

5049
func runExposeCommand(args []string) error {
5150
flags := exposeFlags{}
5251
fs := utils.NewFlagSet("expose", printExposeUsage)
5352

5453
utils.StringFlag(fs, &flags.relayCSV, "relays", "", "Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https)")
55-
utils.BoolFlag(fs, &flags.defaultRelays, "default-relays", true, "Include public registry relays")
56-
utils.BoolFlag(fs, &flags.discoveryEnabled, "discovery", false, "Advertise known relay URLs and discover additional relay bootstraps")
54+
utils.BoolFlag(fs, &flags.discovery, "discovery", true, "Include public registry relays and discover additional relay bootstraps")
5755
utils.StringFlag(fs, &flags.privateKey, "private-key", "", "Owner private key used to derive a discovery address")
5856
utils.StringFlag(fs, &flags.name, "name", "", "Public hostname prefix (single DNS label); auto-generated when omitted")
5957
utils.StringFlag(fs, &flags.desc, "description", "", "Service description metadata")
@@ -92,13 +90,12 @@ func runExposeCommand(args []string) error {
9290
defer stop()
9391

9492
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
95-
RelayURLs: utils.SplitCSV(flags.relayCSV),
96-
DefaultRelayEnabled: flags.defaultRelays,
97-
Name: flags.name,
98-
TargetAddr: flags.targetAddr,
99-
UDPAddr: flags.udpAddr,
100-
UDPEnabled: flags.udp,
101-
Discovery: flags.discoveryEnabled,
93+
RelayURLs: utils.SplitCSV(flags.relayCSV),
94+
Name: flags.name,
95+
TargetAddr: flags.targetAddr,
96+
UDPAddr: flags.udpAddr,
97+
UDPEnabled: flags.udp,
98+
Discovery: flags.discovery,
10299
Metadata: types.LeaseMetadata{
103100
Description: flags.desc,
104101
Tags: utils.SplitCSV(flags.tags),
@@ -142,7 +139,7 @@ func runListCommand(args []string) error {
142139

143140
relayInputs := utils.SplitCSV(flags.relayCSV)
144141

145-
relayURLs, err := sdk.ResolveRelayURLs(ctx, relayInputs, flags.defaultRelays)
142+
relayURLs, err := utils.ResolvePortalRelayURLs(ctx, relayInputs, flags.defaultRelays)
146143
if err != nil {
147144
return fmt.Errorf("resolve relay urls: %w", err)
148145
}
@@ -255,7 +252,7 @@ func printExposeUsage(w io.Writer) {
255252
"portal expose 3000",
256253
"portal expose localhost:8080 --name my-app",
257254
"portal expose 3000 --udp --udp-addr 127.0.0.1:5353",
258-
"portal expose 3000 --relays https://portal.example.com --default-relays=false",
255+
"portal expose 3000 --relays https://portal.example.com --discovery=false",
259256
},
260257
)
261258
}

cmd/portal-tunnel/relays.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func proxyExposure(ctx context.Context, exposure *sdk.Exposure, serviceName stri
7979
log.Error().Err(udpErr).Msg("udp proxy exited with error")
8080
}
8181
if closeErr != nil {
82-
log.Error().Err(closeErr).Msg("relay shutdown failed")
82+
log.Warn().Err(closeErr).Msg("relay shutdown completed with cleanup errors")
8383
}
8484

8585
if ctx.Err() != nil {
@@ -126,7 +126,7 @@ func proxyRelayConnections(ctx context.Context, exposure *sdk.Exposure, localAdd
126126
go func(connID int64, relayConn net.Conn) {
127127
defer connWG.Done()
128128
if err := proxyConnection(ctx, localAddr, relayConn); err != nil {
129-
log.Error().Err(err).Int64("conn_id", connID).Msg("proxy connection failed")
129+
log.Debug().Err(err).Int64("conn_id", connID).Msg("proxy connection closed with an I/O error")
130130
}
131131
log.Info().Int64("conn_id", connID).Msg("proxy connection closed")
132132
}(connID, relayConn)

cmd/relay-server/main.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func runServeCommand(args []string) error {
6363
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")
6464
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")
6565
utils.StringFlagEnv(fs, &cfg.Bootstraps, "bootstraps", "", "additional bootstrap relay API URLs used for discovery expansion", "BOOTSTRAPS")
66-
utils.BoolFlagEnv(fs, &cfg.DiscoveryEnabled, "discovery", false, "serve relay discovery endpoints and poll discovery peers", "DISCOVERY_ENABLED")
66+
utils.BoolFlagEnv(fs, &cfg.DiscoveryEnabled, "discovery", false, "serve relay discovery endpoints and poll discovery peers", "DISCOVERY")
6767
utils.StringFlagEnv(fs, &cfg.OwnerPrivateKey, "owner-private-key", "", "relay owner private key used to derive a discovery address", "OWNER_PRIVATE_KEY")
6868
utils.StringFlagEnv(fs, &cfg.AdminSecretKey, "admin-secret-key", "", "admin auth secret", "ADMIN_SECRET_KEY")
6969
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 {
106106
}
107107

108108
func runServer(ctx context.Context, cfg relayServerConfig) error {
109+
bootstraps, err := utils.ResolvePortalRelayURLs(ctx, utils.SplitCSV(cfg.Bootstraps), cfg.DiscoveryEnabled)
110+
if err != nil {
111+
return fmt.Errorf("resolve discovery bootstraps: %w", err)
112+
}
113+
109114
server, err := portal.NewServer(portal.ServerConfig{
110115
PortalURL: cfg.PortalURL,
111116
OwnerPrivateKey: cfg.OwnerPrivateKey,
112-
Bootstraps: []string{cfg.Bootstraps},
117+
Bootstraps: bootstraps,
113118
ACME: acme.Config{
114119
KeyDir: cfg.KeylessDir,
115120
DNSProvider: cfg.ACMEDNSProvider,

docker-compose.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ services:
1212
# - "${SNI_PORT:-443}:${SNI_PORT:-443}/udp"
1313
# - "50000-50009:50000-50009/udp" # adjust range to match UDP_PORT_COUNT
1414
environment:
15-
# Public routing
15+
# Public routing and discovery
1616
PORTAL_URL: ${PORTAL_URL:-https://localhost:${API_PORT:-4017}}
17+
BOOTSTRAPS: ${BOOTSTRAPS:-}
18+
DISCOVERY: ${DISCOVERY:-true}
1719

1820
# Listener ports (published to the host below)
1921
API_PORT: ${API_PORT:-4017}

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
115115

116116
### SDK (`sdk/`)
117117

118-
- `ExposeConfig.DefaultRelayEnabled`: when true, `Expose` fetches the default Portal relay registry, merges it with explicit relay inputs, and normalizes the result
118+
- `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
119119
- Entry points can opt out of registry defaults and call `utils.NormalizeRelayURLs` directly when they need explicit relay inputs only
120120
- `Listener`: validates one relay URL locally, then starts relay compatibility checks, lease registration, reverse session maintenance, and lease renewal in the background until ready
121121
- `api_client.go`: internal relay client for control-plane requests, reverse session dialing, and internal QUIC tunnel setup
122122
- `ListenerConfig.RetryCount <= 0` means retry forever; positive values close the listener after the retry budget is exhausted
123123
- `NewListener` callers provide explicit normalized relay URLs
124-
- Default exposure flow is `Expose{DefaultRelayEnabled: true} -> PublicURLs -> http.Server.Serve(exposure)`, with an opt-out path for explicit relay inputs only
124+
- Default exposure flow is `Expose{Discovery: true} -> PublicURLs -> http.Server.Serve(exposure)`, with an opt-out path for explicit relay inputs only
125125
- `expose.go`: optional `RunHTTP` helper for serving one handler on both a local HTTP port and the relay listener
126126
- `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
127127
- `Exposure.RelayURLs()` returns the configured normalized relay URLs, while `Exposure.PublicURLs()` returns only relays that are currently registered and ready

docs/deployment.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ Navigate to `/admin`, toggle UDP transport to "Enabled", and optionally set a ma
178178

179179
```bash
180180
PORTAL_URL=https://example.com
181+
BOOTSTRAPS=
182+
DISCOVERY=true
181183
SNI_PORT=443
182184
ADMIN_SECRET_KEY=your-admin-secret
183185
KEYLESS_DIR=./.portal-certs

0 commit comments

Comments
 (0)