Skip to content

feat(relay-server): report configuration state instead of failing silently - #294

Open
rabbitson87 wants to merge 8 commits into
mainfrom
feat/config-observability
Open

feat(relay-server): report configuration state instead of failing silently#294
rabbitson87 wants to merge 8 commits into
mainfrom
feat/config-observability

Conversation

@rabbitson87

Copy link
Copy Markdown
Member

Problem

Configuration problems in a relay deployment are hard to see.

A renamed or mistyped variable looks set but is never read. A value that cannot
be parsed falls back to its default without a word — DISCOVERY=yes silently
becomes false. And a feature that was switched off looks exactly like one that
was switched on but cannot run: both log as false.

All three failures are indistinguishable from a working deployment until
something turns out to be missing much later.

What this adds

A config subcommand and a startup report that make the effective
configuration visible, plus removal of the silent fallbacks that hid it.

relay-server config

Runs without starting the server, so a deployment can be checked before it is
applied:

$ relay-server config --env-file .env

OK   PORTAL_URL           https://portal.example.com  [.env]     relay --portal-url
       portal base URL
--   CLOUDFLARE_TOKEN     <unset>                     [default]  relay --cloudflare-token
       Cloudflare DNS API token. Used only when ACME_DNS_PROVIDER=cloudflare.

UNKNOWN  2 key(s) are not read by any component and are silently ignored:
  ADMIN_WALLETS             did you mean ADMIN_TOKEN?
  BOOTSTRAP_URIS            did you mean BOOTSTRAPS?

Features
 ! acme             blocked      ACME_DNS_PROVIDER=cloudflare
         missing: CLOUDFLARE_TOKEN is empty

Every line carries the value, where it came from, which component reads it, and
the flag's own usage text. Keys nothing reads are named with a nearest-match
suggestion.

Startup report

The same report is logged at startup. Each feature is enabled, disabled or
blocked, and only the last two are worth attention:

INF feature=tcp-transport state=disabled by=TCP_ENABLED=false
WRN feature=udp-transport state=blocked  by=UDP_ENABLED=true
    missing="MIN_PORT=0 MAX_PORT=0 is not a usable range; set both and publish the range"
WRN feature=admin-api     state=UNPROTECTED
    missing="ADMIN_TOKEN is empty; the admin and policy APIs accept unauthenticated requests"

disabled means you turned it off and nothing is wrong. blocked means you
turned it on and it cannot run. Those were previously the same line.

The blocked conditions are not new rules. They are failure modes already
described in flag usage strings and the docs, moved to startup where they are
seen.

Values that cannot be parsed now fail

DISCOVERY=yes is a startup error instead of a silent false, and an
out-of-range port is reported rather than clamped.

How it stays correct

The descriptions are not a second list. utils.StringFlagEnv and friends
already received the env name, flag name, usage text and default, and discarded
all of it; they now record it. The report, the generated reference, and the docs
check all read from the flag definitions.

make check-env-example fails when a flag is added without documenting it in
.env.example and the configuration reference. Keys the bundled topology pins
(API_PORT, SNI_PORT, TZ) are marked in envcatalog.go and excluded on
purpose, so documenting them cannot invite an override that breaks the wiring.

Keys read by Compose or the Google Cloud SDK rather than the relay are
catalogued so they are never reported as unknown.

Verification

  • make vet, make lint, make test, gofmt clean
  • make check-env-example passes, and fails as intended when a key is removed
    from either file
  • False-positive regression: the full .env.example reports 0 unknown keys
  • Detection: ADMIN_WALLETS → suggests ADMIN_TOKEN, BOOTSTRAP_URIS
    suggests BOOTSTRAPS
  • DISCOVERY=yes refuses to start
  • Verified inside the built image (docker compose build portal), including the
    frontend and landing-page features new to the single-container topology

Not included

An earlier draft also reworked the bundled nginx edge and the multi-container
Compose topology. That work targets files this branch's base has removed, and
argues against the direction the deployment guide now documents, so it is left
out.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added relay-server config to review effective settings, environment sources, feature status, unused keys, and configuration issues.
    • Added diagnostics for invalid values, missing settings, unknown keys, and deployment topology warnings.
    • Added generated configuration references and environment completeness checks.
  • Documentation

    • Expanded configuration guidance, including diagnostics usage and pprof port setup.
    • Reworked the environment template with clearer deployment, transport, DNS, proxy, and diagnostic settings.
    • Documented startup errors for invalid environment values.
  • Chores

    • Added Makefile commands for validating and generating configuration references.

Walkthrough

The relay now provides configuration inspection, feature-state reporting, environment metadata, invalid-value detection, generated references, startup validation, and documented deployment settings.

Changes

Relay configuration tooling

Layer / File(s) Summary
Environment metadata and parsing
utils/cmd.go
Environment-backed flags now register metadata, record source variables, and report invalid boolean and integer values.
Environment catalog and template
cmd/relay-server/envcatalog.go, .env.example
The catalog and example file define relay, external, pinned, transport, storage, proxy, diagnostics, and DNS provider settings.
Configuration inspection command
cmd/relay-server/config.go
The config command loads environment files, resolves settings, evaluates feature states, redacts secrets, reports unknown keys, and generates references and key lists.
Startup wiring and validation
cmd/relay-server/main.go, Makefile, cmd/relay-server/config_test.go, docs/src/routes/configuration/+page.md
The serve command reuses shared configuration resolution and rejects recorded environment errors. Make targets validate documented keys and print references. Tests cover environment isolation and feature states. Documentation covers inspection, validation, startup errors, and PPROF_PORT.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant relay_server
  participant resolveRelayServerConfig
  participant EnvVars
  participant evaluateFeatures
  Operator->>relay_server: Run config with format and env-file
  relay_server->>resolveRelayServerConfig: Resolve relay settings
  resolveRelayServerConfig->>EnvVars: Read metadata and parse issues
  relay_server->>evaluateFeatures: Evaluate relay capabilities
  evaluateFeatures-->>relay_server: Return feature states and diagnostics
  relay_server-->>Operator: Print configuration report or generated reference
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits format and accurately describes the configuration observability changes.
Description check ✅ Passed The description clearly explains the configuration observability features, validation changes, implementation approach, and verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/config-observability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/relay-server/config.go`:
- Around line 153-166: Update ensGaslessFeature to reuse the DNS provider
validation performed by acmeFeature, rather than only checking whether
ACMEDNSProvider is non-empty. When that provider is blocked, propagate the
blocked state and missing requirement; only report ENS gasless as enabled when
the provider validation succeeds.
- Around line 362-405: Update writeConfigReport to report resolved effective
values rather than raw envFileEntry.Value: derive values and sources from cfg
and the resolver metadata, including utils.EnvVar.SetBy, so ignored aliases are
not displayed as active. Always render a Keys section for every registered relay
key, including when entries is empty and values come from the process
environment, while preserving unknown-key reporting for supplied entries.

In `@cmd/relay-server/main.go`:
- Around line 79-80: Update resolveRelayServerConfig to clear the process-global
utils.EnvIssues registry at the start of each resolution pass, before any
environment validation or issue registration occurs. Preserve accumulation of
all issues generated during that pass so envIssueError() evaluates only the
current configuration attempt.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b146079f-a361-4050-8c4b-e2ed71c09adb

📥 Commits

Reviewing files that changed from the base of the PR and between 79ccc6f and 83ffabc.

📒 Files selected for processing (7)
  • .env.example
  • Makefile
  • cmd/relay-server/config.go
  • cmd/relay-server/envcatalog.go
  • cmd/relay-server/main.go
  • docs/src/routes/configuration/+page.md
  • utils/cmd.go
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
cmd/relay-server/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

cmd/relay-server/**/*.go: All JSON control-plane responses must use the APIEnvelope wrapper: { ok: bool, data?: any, error?: { code, message } }. Write responses through writeAPIData(), writeAPIOK(), or writeAPIError(). Exceptions: admin HTML pages, tunnel script/binary responses, and other non-JSON endpoints.
Relay-local frontend asset paths stay local to cmd/relay-server. Filenames like favicon.svg or portal.jpg are frontend serving details, not cross-package API contract.

Files:

  • cmd/relay-server/envcatalog.go
  • cmd/relay-server/main.go
  • cmd/relay-server/config.go
{cmd,sdk}/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Do not import portal from cmd/* or sdk just to reach shared DTOs or constants. Shared public shapes belong in types/.

Files:

  • cmd/relay-server/envcatalog.go
  • cmd/relay-server/main.go
  • cmd/relay-server/config.go
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Imports follow the order: stdlib -> external -> internal (blank-line separated), local prefix gosuda.org/portal/v2.
Use errgroup.Group over WaitGroup. Use errgroup.SetLimit for bounded work. Use context.WithTimeout over time.After in loops. No bare go func() without owned lifecycle.
Prefer directory-scoped operations when touching filesystem trees in Go code.

Files:

  • cmd/relay-server/envcatalog.go
  • cmd/relay-server/main.go
  • utils/cmd.go
  • cmd/relay-server/config.go
🪛 checkmake (0.3.2)
Makefile

[warning] 66-66: Target body for "check-env-example" exceeds allowed length of 5 lines (25).

(maxbodylength)

🪛 dotenv-linter (4.0.0)
.env.example

[warning] 16-16: [ExtraBlankLine] Extra blank line detected

(ExtraBlankLine)


[warning] 30-30: [ExtraBlankLine] Extra blank line detected

(ExtraBlankLine)


[warning] 47-47: [UnorderedKey] The MAX_PORT key should go before the MIN_PORT key

(UnorderedKey)


[warning] 49-49: [ExtraBlankLine] Extra blank line detected

(ExtraBlankLine)


[warning] 63-63: [UnorderedKey] The TCP_ENABLED key should go before the UDP_ENABLED key

(UnorderedKey)


[warning] 78-78: [UnorderedKey] The TRUSTED_PROXY_CIDRS key should go before the TRUST_PROXY_HEADERS key

(UnorderedKey)


[warning] 84-84: [UnorderedKey] The X402_PAY_TO key should go before the X402_TESTNET key

(UnorderedKey)


[warning] 90-90: [UnorderedKey] The PPROF_ADDR key should go before the PPROF_ENABLED key

(UnorderedKey)


[warning] 93-93: [ExtraBlankLine] Extra blank line detected

(ExtraBlankLine)

🔇 Additional comments (7)
utils/cmd.go (1)

12-94: LGTM!

Also applies to: 100-158, 210-228

cmd/relay-server/envcatalog.go (1)

1-74: LGTM!

.env.example (1)

1-145: LGTM!

cmd/relay-server/config.go (1)

22-151: LGTM!

Also applies to: 169-285, 287-360, 430-593, 595-681

cmd/relay-server/main.go (1)

27-30: LGTM!

Makefile (1)

1-1: LGTM!

Also applies to: 19-20, 57-95

docs/src/routes/configuration/+page.md (1)

29-88: LGTM!

Comment thread cmd/relay-server/config.go
Comment thread cmd/relay-server/config.go
Comment thread cmd/relay-server/main.go

@gosunuts gosunuts left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration report can currently diverge from the deployment it is intended to validate in two cases. Please address the inline findings before merging.

Comment thread cmd/relay-server/config.go Outdated
return fmt.Errorf("read env file: %w", err)
}
for _, entry := range loaded {
if err := os.Setenv(entry.Name, entry.Value); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--env-file is documented as being inspected instead of the process environment, but this only overwrites keys present in the file. Any relay variable absent from the file remains inherited, and a higher-priority alias from the shell can override a value that is present (for example, process AWS_REGION overrides file AWS_DEFAULT_REGION). This can make the report validate a different configuration from the one Compose will deploy. Please resolve against an isolated environment—e.g. clear/restore all registered names around this pass, or pass an explicit environment map to the resolver.

return f
}

f.State, f.By = stateEnabled, "ACME_DNS_PROVIDER="+provider

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reports ACME as enabled whenever the provider/credentials look valid, even when PORTAL_URL has a local-only host. The runtime does not enable managed ACME in that case: acme.NewManager returns before creating a DNS provider for local hosts, and ENS status is disabled as well. With PORTAL_URL=https://localhost, the report therefore says ACME (and potentially ENS gasless) is enabled while neither automation path runs. Please include the local-host condition in both feature evaluations so the report reflects runtime state.

@rabbitson87

Copy link
Copy Markdown
Member Author

Both findings were reproducible and are fixed in 1ecc3b0. Thanks — they were
the mismatch this report exists to surface, so the report having it was the
worst place for it.

--env-file isolation. Confirmed: it only set the file's own keys, so a
variable absent from the file stayed inherited, and process AWS_REGION did
beat file AWS_DEFAULT_REGION. The file is now the whole environment for that
pass — every name the deployment understands (registry names, their aliases,
and the externally-owned keys) is cleared, the file applied, and the previous
environment restored on the way out.

I took the clear/restore option rather than threading an environment map
through the resolver. The map is cleaner in principle, but it changes the
utils.*FlagEnv signatures and touches ~60 call sites across four commands;
config prints and exits, so the contained version buys the same guarantee.
Happy to switch if you would rather have the map.

Inspecting the process environment (no --env-file) is unchanged, since
isolation is not the intent there.

Local-only host. Confirmed against acme.NewManager — it returns before
building a DNS provider when IsLocalRelayHost(cfg.BaseDomain), so managed
issuance cannot run regardless of the credential. A configured provider on a
local host is now blocked:

! acme        blocked  ACME_DNS_PROVIDER=cloudflare
    missing: PORTAL_URL host "localhost" is local-only; managed issuance is
    skipped for local hosts and a development certificate is used instead
! ens-gasless blocked  ENS_GASLESS_ENABLED=true
    missing: the DNS provider it shares with ACME is blocked: ...

ens-gasless picks this up for free: an earlier commit in this branch made it
defer to acmeFeature instead of repeating half the check, so it inherits both
the state and the reason.

Tests. Added cmd/relay-server/config_test.go covering both, including the
alias precedence case and environment restoration. I checked each test fails
when its fix is reverted, so they pin the behaviour rather than just passing.

gosunuts
gosunuts previously approved these changes Aug 11, 2026

@gosunuts gosunuts left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@gosunuts gosunuts left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

…ently

Configuration problems in a relay deployment are hard to see. A renamed or
mistyped variable looks set but is never read, and a value that cannot be
parsed falls back to its default without a word. Both failures look identical
to a working deployment until something is missing much later.

Add a config subcommand and a startup report that make the effective
configuration visible, and stop the silent fallbacks that hid it.

- utils: record the env name, flag, usage text and default behind every
  Flag*Env call. The helpers already received all of it and discarded it, so
  the descriptions keep a single owner in the flag definitions.

- utils: a value that is present but unusable is recorded as an issue rather
  than swallowed. DISCOVERY=yes is now a startup error instead of a silent
  false, and an out-of-range port is reported rather than clamped.

- relay-server config: prints every key with its effective value, its source,
  the component that reads it, and its usage text; names keys nothing reads
  with a nearest-match suggestion; and evaluates each feature. Runs without
  starting the server, so a deployment can be checked before it is applied.

- Startup logs the same report. Each feature is enabled, disabled or blocked:
  "you switched this off" and "you switched this on but it cannot run" were
  previously indistinguishable, and only the second is a misconfiguration.
  Blocked and unprotected states log at warning level.

- envcatalog: keys read by Compose, the Google Cloud SDK or the image are
  catalogued so they are not reported as unknown, and keys the bundled
  topology pins are marked so documenting them cannot invite an override.

- .env.example is grouped by what an operator actually has to decide, and
  make check-env-example fails when a flag is added without documenting it in
  .env.example and the configuration reference.

The blocked conditions are not new rules; they are failure modes already
described in flag usage strings and the docs, moved to startup where they are
seen.
… in sync

Three problems found in review.

The report printed the raw text of each env-file line rather than the value the
flag resolved to. A key overridden by a higher-priority name, or an alias that
was never consulted, read as though it were in effect — the exact confusion the
report exists to remove. It also skipped the key listing entirely when no env
file was given, so a process-environment deployment got no listing at all.

The registry now carries the resolved value, and every relay key is listed with
it plus where it came from: the env file, the process environment, or the
default. When an alias supplied the value the alias is named, so "I set
AWS_REGION, why is it different?" is answered by seeing AWS_DEFAULT_REGION was
consulted first. Keys owned by another component are still only shown when
supplied, since the relay has no effective value for them.

ens-gasless repeated only the "is a provider set" half of the ACME check, so it
reported enabled while acme was blocked on a missing credential. It now defers
to acmeFeature and propagates the reason.

The registry is process-global, and the config subcommand resolves a second time
after loading an env file. Issues accumulated across passes, so a stale one
could fail a configuration that no longer contained it. Resolution now starts
from an empty registry.
…checks

Two cases where the report could validate a configuration different from the
one that would actually run.

`--env-file` is documented as inspecting a file instead of the process
environment, but it only set the file's own keys. A relay variable absent from
the file stayed inherited from the shell, and a higher-priority alias in the
shell beat a value the file did supply — process AWS_REGION over file
AWS_DEFAULT_REGION. Compose passes only the file, so the report could describe
a different deployment than the one being checked. The file is now the whole
environment for that pass: every name the deployment understands is cleared
first, the file applied, and the previous environment restored afterwards.
Inspecting the process environment, where isolation is not the intent, is
unchanged.

ACME was reported as enabled whenever the provider and credential looked valid,
including when PORTAL_URL had a local-only host. acme.NewManager returns before
it builds a DNS provider for a local base domain, so managed issuance never
runs in that case and a development certificate is used instead. The report now
treats a configured provider on a local host as blocked, which is what asking
for automation that cannot start should look like. ens-gasless already defers
to acmeFeature, so it inherits the state and the reason.

Both are the mismatch this report exists to surface, so both are covered by
tests that fail when either fix is reverted.
The Keys section runs every value through displayValue, which prints <set>
for a name that looks like a credential. The Invalid values section printed
the raw text instead.

Nothing leaks today: an issue is only recorded when a boolean or integer
fails to parse, and no credential is either. But the two lists are read the
same way, and a report that masks a secret in one place and prints it in
another is one numeric credential away from pasting it into a bug thread.
The documented check reads the file from stdin, but `docker compose run`
asks for a TTY unless told not to. Older Compose versions then fail with
"the input device is not a TTY" and the operator never sees the report --
which is the one command this feature exists to offer.

Newer versions detect the redirect and work either way, so the omission
survives a local test and only shows up on the server.
@rabbitson87
rabbitson87 force-pushed the feat/config-observability branch from 0e9e8ae to e78c232 Compare August 13, 2026 04:30

@gosunuts gosunuts left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag-derived EnvVar{Value, SetBy, Usage, Default} metadata and strict bool/int parsing are worth keeping, and the earlier shell-environment leakage and local-host ACME mismatches are fixed at this head. I still do not think this is mergeable yet: the command can report a different configuration from both Compose and the runtime, and the env parser introduces another silent typo path. Please resolve the first two inline blockers and rebase onto current main (mergeable: false) before merging. During that rebase, preserve the newer relay/tunnel ECH wording that main added to .env.example; this branch rewrites that section extensively.

Architecturally, config should display validation owned by the runtime rather than grow into a competing configuration model. Reusing pure normalization/validation from portal and portal/acme should also reduce much of the new config.go surface.

// the file would stay inherited from the shell, and a higher-priority alias in
// the shell would beat a value the file does supply — process AWS_REGION over
// file AWS_DEFAULT_REGION, for instance. Either way the report would describe a
// configuration different from the one Compose is going to deploy, which is the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not claim this pass reproduces the Compose deployment

Isolation fixes shell leakage, but after clearing the container environment this code calls resolveRelayServerConfig(nil), so every key omitted from the file gets the relay binary default. Compose applies a different layer first: for example, it injects MIN_PORT=40000, MAX_PORT=40009, and IDENTITY_PATH=/portal-certs, while the binary defaults are 0, 0, and ./.portal-certs. Consequently an env file containing only PORTAL_URL=https://relay.example.com and UDP_ENABLED=true is reported as udp-transport blocked, although docker compose up supplies 40000-40009 and enables it. Either scope --env-file explicitly to the relay binary's environment semantics and remove the Compose/deployment claims, or inspect Compose's fully interpolated container environment. Reimplementing Compose defaults here would create another configuration engine.

f.State, f.By = stateDisabled, "DISCOVERY=false"
return f
}
host := portalURLHost(cfg.PortalURL)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reuse runtime normalization before reporting this feature as enabled

This checks only the parsed hostname. PORTAL_URL=http://relay.example.com therefore reports discovery as enabled, but portal.NewServer calls utils.NormalizeRelayURL and rejects the non-HTTPS URL. BOOTSTRAPS has the same split: this report only SplitCSVs/counts entries while the runtime requires NormalizeRelayURLs to succeed. Similar provider and port rules are duplicated in this file and dnsProviderCredential separately duplicates the provider set owned by portal/acme. Please have the report consume shared pure runtime normalization/validation results instead of reconstructing feature validity here; otherwise startup can log enabled immediately before rejecting the same configuration.

Comment thread cmd/relay-server/config.go Outdated
line = strings.TrimPrefix(line, "export ")
name, value, found := strings.Cut(line, "=")
if !found {
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reject malformed non-comment lines instead of silently dropping them

A typo such as DISCOVERY true reaches this branch and disappears, causing discovery to fall back to its default without any warning. An empty assignment name is discarded the same way below. That recreates the silent-misconfiguration failure this PR is intended to eliminate. Track the scanner line number and return a filename:line parse error for every non-empty, non-comment line that is not a valid assignment.

…ot asked

Three findings from review, all cases where the report was confident about
something it had not actually checked.

`--env-file` was documented as showing what a deployment will run, but it
resolves the file against the relay binary's defaults. Compose supplies its
own first: a file carrying only PORTAL_URL and UDP_ENABLED=true reported
`udp-transport blocked` while `docker compose up` would give it
MIN_PORT=40000 and enable it. Reimplementing Compose's defaults here would
build a second configuration engine, so the claim is narrowed instead. The
accurate check needs no new code — running the subcommand inside the
container lets Compose build the environment first:

    docker compose run --rm -T portal config

That is now what .env.example, the configuration page and the command's own
usage recommend, and a file-scoped report says in its header which of the
two questions it answered.

`discoveryFeature` inspected only the parsed hostname, so
PORTAL_URL=http://relay.example.com reported enabled and portal.NewServer
rejected the same value seconds later — the exact divergence this feature
exists to remove. It now calls utils.NormalizeRelayURL and
utils.NormalizeRelayURLs, the same normalization the server applies, rather
than holding a second opinion about it. Bootstraps are counted after
normalization for the same reason.

loadEnvFile silently skipped any line without an `=`. `DISCOVERY true`
vanished and discovery reported its default with nothing to explain why,
which is the silent misconfiguration this command was written to expose.
Malformed lines now fail with file:line.

Also keys dnsProviderCredential by acme's exported Type* constants rather
than repeating the provider names, so acme stays the one place that decides
what is supported and this map only adds the credential each one needs.
@rabbitson87

Copy link
Copy Markdown
Member Author

All three addressed in f16b6225.

--env-file claiming to reproduce the Compose deployment. You're right, and the example you gave reproduces exactly: a file with only PORTAL_URL and UDP_ENABLED=true reports udp-transport blocked while docker compose up supplies 40000-40009 and enables it.

I took the first option. Reimplementing Compose's defaults here would build a second configuration engine, and the accurate check turns out to need no new code at all — running the subcommand inside the container lets Compose build the environment first:

docker compose run --rm -T portal config

That is now what .env.example, the configuration page and the command's own usage recommend. --env-file is scoped to what it really does — one file against relay defaults — and a file-scoped report now says so in its header, so the two questions cannot be confused.

discoveryFeature reconstructing validity. Fixed by consuming the runtime's own normalization rather than holding a second opinion about it: utils.NormalizeRelayURL for PORTAL_URL and utils.NormalizeRelayURLs for BOOTSTRAPS, which is what portal.NewServer calls. PORTAL_URL=http://… now reports blocked with the https requirement named, instead of enabled immediately before the server rejects it. Bootstraps are counted after normalization too.

On dnsProviderCredential: it now keys off acme.TypeCloudflare and friends rather than repeating the strings, so acme stays the single place that decides what is supported and the map only adds the credential each provider needs — knowledge the report legitimately owns. I stopped short of deriving requirements from acme.NewDNSProvider, because construction succeeds with an empty token and the error only appears at challenge time, so it would not tell the report what it needs to know. Happy to go further if you'd prefer that inverted.

Malformed env-file lines. Fixed with file:line:

read env file: /tmp/bad.env:2: not an assignment: "DISCOVERY true"

Empty assignment names are rejected the same way. Comments, blanks and export prefixes are unchanged.

Five tests pin the three: malformed line and empty name both rejected with the line number, comments/blanks still accepted, non-https PORTAL_URL blocked, unusable BOOTSTRAPS blocked, and normalized bootstraps counted correctly.

main gained an embedded authoritative DNS server that is now the default
DNS provider, which changes what this report has to say.

An unset ACME_DNS_PROVIDER no longer means "manual certificates". It
selects the embedded server, so reporting that as disabled would describe
a relay that is in fact serving its own zone and answering ACME challenges
from it. acme now reports embedded as enabled and says what it needs: the
NS delegation and 53/tcp+udp, with manual certificate files still taking
over when present.

ENS gasless gains a blocked state for the same reason. The embedded server
cannot manage zone DNSSEC yet, and it is exactly what an operator gets by
turning ENS on and changing nothing else, so that combination now says so
instead of claiming to work.

.env.example keeps this branch's four-tier layout with the new keys folded
in: ACME_DNS_PROVIDER=embedded as the documented default, EMBEDDED_DNS_PORT
in the provider section beside the credentials it replaces, and the ENS
entry corrected to name the DNSSEC requirement rather than "provider must
be set".

The startup log keeps the feature report rather than main's flat field
dump; that substitution is the point of the branch.
The flag that binds CLOUDFLARE_TOKEN to relayServerConfig.CloudflareToken
is gone from main. The struct field is still there and is still passed to
acme.Config, so nothing fails to compile — it is simply always empty, and
a relay configured with ACME_DNS_PROVIDER=cloudflare starts, reports
itself configured, and then fails DNS-01 with "cloudflare token is
required". The token appears in .env.example, docker-compose.yml and three
documentation pages; it appears in no Go file.

Every other provider credential is still wired, so this is Cloudflare
alone.

This branch's own report is what surfaced it, from two directions at once:

    UNKNOWN  1 key(s) are not read by any component and are silently ignored:
      CLOUDFLARE_TOKEN               did you mean HCLOUD_TOKEN?

     ! acme             blocked      ACME_DNS_PROVIDER=cloudflare
             missing: CLOUDFLARE_TOKEN is empty

Kept as its own commit rather than folded into the merge, so it can be
taken separately or ahead of the rest.
@rabbitson87
rabbitson87 requested a review from gosunuts August 20, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants