Skip to content

Commit 24f720d

Browse files
authored
Merge pull request #311 from gosuda/feature/embed-dns-provider
feat(acme): add embedded authoritative DNS provider
2 parents be34972 + eb28435 commit 24f720d

21 files changed

Lines changed: 1138 additions & 108 deletions

File tree

.agents/skills/relay-server-config/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Entrypoint `cmd/relay-server/main.go`, dispatched by `utils.RunCommands` (`""`/`
1616
| `API_PORT` | `4017` | HTTPS API + frontend (`--api-port`) |
1717
| `SNI_PORT` | `443` | public SNI ingress (`--sni-port`) — cannot bind unprivileged; override locally |
1818
| `WIREGUARD_PORT` | `51820` | (`--wireguard-port`) |
19+
| `EMBEDDED_DNS_PORT` | `53` | embedded authoritative DNS (`--embedded-dns-port`) — default provider; needs 53/tcp+udp and `CAP_NET_BIND_SERVICE` in containers |
1920
| `ADMIN_TOKEN` | empty | bearer token for admin/policy APIs (`--admin-token`) |
2021
| `LANDING_PAGE_ENABLED` | `false` | (`--landing-page-enabled`) |
2122
| `DISCOVERY` | `false` | enable gossip; needs `BOOTSTRAPS` (`--discovery`) |
@@ -27,7 +28,7 @@ Entrypoint `cmd/relay-server/main.go`, dispatched by `utils.RunCommands` (`""`/`
2728
| `PPROF_ENABLED` / `PPROF_ADDR` | `false` / `127.0.0.1:6060` | pprof |
2829
| `X402_ENABLED` / `X402_TESTNET` / `X402_PAY_TO` | `false` / `false` / empty | mounts facilitator under `/api/x402` only when enabled |
2930

30-
DNS/ACME provider creds are also env-bound: `ACME_DNS_PROVIDER`, `CLOUDFLARE_TOKEN`, GCP (`GCP_PROJECT_ID`+aliases, `GCP_MANAGED_ZONE`+aliases), Hetzner (`HETZNER_API_TOKEN`/`HCLOUD_TOKEN`), AWS Route53 (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN`/`AWS_REGION`→defaults us-east-1 downstream/`AWS_HOSTED_ZONE_ID`/`AWS_DNSSEC_KMS_KEY_ARN`), `VULTR_API_KEY`, `NJALLA_TOKEN`, `ENS_GASLESS_ENABLED`. Never hardcode these — reference as env vars / secrets.
31+
Embedded DNS is the default `ACME_DNS_PROVIDER` (empty value): `EMBEDDED_DNS_PORT` (default 53). External provider creds remain env-bound: `ACME_DNS_PROVIDER`, `CLOUDFLARE_TOKEN`, GCP (`GCP_PROJECT_ID`+aliases, `GCP_MANAGED_ZONE`+aliases), Hetzner (`HETZNER_API_TOKEN`/`HCLOUD_TOKEN`), AWS Route53 (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN`/`AWS_REGION`→defaults us-east-1 downstream/`AWS_HOSTED_ZONE_ID`/`AWS_DNSSEC_KMS_KEY_ARN`), `VULTR_API_KEY`, `NJALLA_TOKEN`, `ENS_GASLESS_ENABLED` (unsupported with embedded). Never hardcode these — reference as env vars / secrets.
3132

3233
## Reserved API surface (types/paths.go)
3334
Root-host trees that must never fall through to the SPA: `types.ReservedRootPrefixes` = `/api`, `/sdk`, `/discovery`, `/v1`. x402 public paths `/x402/prepare`, `/x402/client.js`; relay facilitator `/api/x402/{supported,verify,settle}`. Discovery: `/discovery`, `/discovery/announce` (only when `DISCOVERY=true`).
@@ -37,5 +38,5 @@ Root-host trees that must never fall through to the SPA: `types.ReservedRootPref
3738

3839
## Config embedding & Docker
3940
- `config.toml` (version/protocol) and `registry.json` (default relays) are embedded via `manifest.go` and parsed in `types/types.go`.
40-
- `docker-compose.yml` runs `ghcr.io/gosuda/portal:2`, publishes TCP 443 + UDP WireGuard port, and passes the above env vars.
41+
- `docker-compose.yml` runs `ghcr.io/gosuda/portal:2`, publishes TCP 443 + TCP/UDP 53 (embedded DNS, `cap_add: NET_BIND_SERVICE`) + UDP WireGuard port, and passes the above env vars.
4142
- `Dockerfile`: Node 22 stage builds the frontend into `cmd/relay-server/dist/app`, then `make build-server-bin` compiles the Go relay.

.env.example

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@ MAX_PORT=0
1414
UDP_ENABLED=false
1515
TCP_ENABLED=false
1616

17-
# Supported managed values: cloudflare, gcloud, hetzner, njalla, route53, vultr.
17+
# Supported managed values: embedded (default), cloudflare, gcloud, hetzner, njalla, route53, vultr.
1818
# Reused for ACME DNS-01, managed A records, the relay ECH record,
19-
# opt-in tunnel ECH records, and optional ENS DNS automation.
20-
ACME_DNS_PROVIDER=
19+
# and opt-in tunnel ECH records.
20+
ACME_DNS_PROVIDER=embedded
21+
22+
# Embedded authoritative DNS server, the default DNS provider.
23+
# Requires a one-time NS delegation of the base domain at the parent zone
24+
# (NS portal.example.com -> ns.portal.example.com + glue A to the relay IP)
25+
# and open 53/tcp + 53/udp.
26+
# ENS gasless automation (zone DNSSEC) is not supported with embedded yet.
27+
EMBEDDED_DNS_PORT=53
2128

2229
# Cloudflare API token (required when ACME_DNS_PROVIDER=cloudflare)
2330
CLOUDFLARE_TOKEN=
@@ -48,6 +55,7 @@ NJALLA_TOKEN=
4855

4956
# ENS gasless DNS import automation. When enabled, Portal uses ACME_DNS_PROVIDER
5057
# for DNSSEC and ENS TXT automation, even when certificate files are managed manually.
58+
# Not supported with ACME_DNS_PROVIDER=embedded yet.
5159
ENS_GASLESS_ENABLED=false
5260

5361
# Admin/auth configuration. Use a long random value for production relays.

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,9 @@ ENV TZ=UTC
3939

4040
EXPOSE 443/tcp
4141
EXPOSE 51820/udp
42+
# Embedded authoritative DNS (ACME_DNS_PROVIDER=embedded). The runtime is
43+
# nonroot, so binding 53 requires CAP_NET_BIND_SERVICE:
44+
# docker run --cap-add NET_BIND_SERVICE -p 53:53/tcp -p 53:53/udp ...
45+
EXPOSE 53/tcp
46+
EXPOSE 53/udp
4247
ENTRYPOINT ["/usr/bin/relay-server"]

cmd/relay-server/main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type relayServerConfig struct {
5858

5959
ACMEDNSProvider string
6060
ENSGaslessEnabled bool
61+
EmbeddedDNSPort int
6162
CloudflareToken string
6263
GCPProjectID string
6364
GCPManagedZone string
@@ -101,9 +102,9 @@ func runServeCommand(args []string) error {
101102
utils.BoolFlagEnv(fs, &cfg.X402Testnet, "x402-testnet", false, "use Sui testnet for relay-owned x402 facilitator payments", "X402_TESTNET")
102103
utils.StringFlagEnv(fs, &cfg.X402PayTo, "x402-pay-to", "", "Sui payment recipient address for relay-owned control-plane x402 resources", "X402_PAY_TO")
103104

104-
utils.StringFlagEnv(fs, &cfg.ACMEDNSProvider, "acme-dns-provider", "", "DNS provider for managed DNS-01/A-record sync, ECH HTTPS records, and ENS gasless DNSSEC/TXT automation (cloudflare|gcloud|hetzner|njalla|route53|vultr); leave empty to use manual fullchain.pem/privatekey.pem from IDENTITY_PATH", "ACME_DNS_PROVIDER")
105+
utils.StringFlagEnv(fs, &cfg.ACMEDNSProvider, "acme-dns-provider", "", "DNS provider for managed DNS-01/A-record sync, ECH HTTPS records, and ENS gasless DNSSEC/TXT automation (embedded|cloudflare|gcloud|hetzner|njalla|route53|vultr); defaults to embedded when unset", "ACME_DNS_PROVIDER")
105106
utils.BoolFlagEnv(fs, &cfg.ENSGaslessEnabled, "ens-gasless-enabled", false, "enable ENS gasless DNS import automation for the managed DNS zone and lease hostnames", "ENS_GASLESS_ENABLED")
106-
utils.StringFlagEnv(fs, &cfg.CloudflareToken, "cloudflare-token", "", "Cloudflare DNS API token (required when acme-dns-provider=cloudflare)", "CLOUDFLARE_TOKEN")
107+
utils.IntFlagEnv(fs, &cfg.EmbeddedDNSPort, "embedded-dns-port", 53, utils.ParsePortNumber, "listen port for the embedded authoritative DNS server (the default DNS provider); requires a one-time NS delegation of the base domain and open 53/tcp+udp", "EMBEDDED_DNS_PORT")
107108
utils.StringFlagEnv(fs, &cfg.GCPProjectID, "gcp-project-id", "", "Google Cloud project id for Cloud DNS automation; auto-detected from ADC or GCE metadata when omitted", "GCP_PROJECT_ID", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT", "GCE_PROJECT")
108109
utils.StringFlagEnv(fs, &cfg.GCPManagedZone, "gcp-managed-zone", "", "explicit Google Cloud DNS managed zone name or numeric ID override", "GCP_MANAGED_ZONE", "GCP_ZONE", "GCE_ZONE_ID")
109110
utils.StringFlagEnv(fs, &cfg.HetznerAPIToken, "hetzner-api-token", "", "Hetzner Cloud API token for DNS automation (required when acme-dns-provider=hetzner)", "HETZNER_API_TOKEN", "HCLOUD_TOKEN")
@@ -152,6 +153,7 @@ func runServeCommand(args []string) error {
152153
Bool("x402_testnet", cfg.X402Testnet).
153154
Bool("x402_pay_to_configured", strings.TrimSpace(cfg.X402PayTo) != "").
154155
Str("acme_dns_provider", cfg.ACMEDNSProvider).
156+
Int("embedded_dns_port", cfg.EmbeddedDNSPort).
155157
Bool("ens_gasless_enabled", cfg.ENSGaslessEnabled).
156158
Msg("configured relay server")
157159

@@ -185,7 +187,7 @@ func runServer(ctx context.Context, cfg relayServerConfig) error {
185187
KeyDir: cfg.IdentityPath,
186188
DNSProvider: cfg.ACMEDNSProvider,
187189
ENSGaslessEnabled: cfg.ENSGaslessEnabled,
188-
CloudflareToken: cfg.CloudflareToken,
190+
EmbeddedDNSPort: cfg.EmbeddedDNSPort,
189191
GCPProjectID: cfg.GCPProjectID,
190192
GCPManagedZone: cfg.GCPManagedZone,
191193
HetznerAPIToken: cfg.HetznerAPIToken,

docker-compose.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ services:
55
context: .
66
dockerfile: Dockerfile
77
stop_grace_period: 30s
8+
# Binding the default embedded DNS port 53 as a nonroot container.
9+
cap_add:
10+
- NET_BIND_SERVICE
811
ports:
912
- "443:443"
1013
- "${WIREGUARD_PORT:-51820}:${WIREGUARD_PORT:-51820}/udp"
14+
# Embedded authoritative DNS (the default DNS provider).
15+
- "${EMBEDDED_DNS_PORT:-53}:${EMBEDDED_DNS_PORT:-53}/tcp"
16+
- "${EMBEDDED_DNS_PORT:-53}:${EMBEDDED_DNS_PORT:-53}/udp"
1117
# Uncomment for UDP backhaul, public UDP lease ports, and raw TCP lease ports as needed.
1218
# - "443:443/udp"
1319
# - "${MIN_PORT:-40000}-${MAX_PORT:-40009}:${MIN_PORT:-40000}-${MAX_PORT:-40009}/udp"
@@ -44,6 +50,7 @@ services:
4450

4551
# TLS/ACME materials and DNS automation
4652
ACME_DNS_PROVIDER: ${ACME_DNS_PROVIDER:-}
53+
EMBEDDED_DNS_PORT: ${EMBEDDED_DNS_PORT:-53}
4754
ENS_GASLESS_ENABLED: ${ENS_GASLESS_ENABLED:-false}
4855
CLOUDFLARE_TOKEN: ${CLOUDFLARE_TOKEN:-}
4956
GCP_PROJECT_ID: ${GCP_PROJECT_ID:-}

docs/src/routes/configuration/+page.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,16 @@ The relay server (`relay-server`) reads configuration from environment variables
5858

5959
| Variable | Default | Type | Description |
6060
|----------|---------|------|-------------|
61-
| `ACME_DNS_PROVIDER` | `""` | string | DNS provider for managed DNS-01/A-record sync, the relay ECH record, opt-in tunnel ECH records, and ENS gasless DNSSEC/TXT automation (`cloudflare` \| `gcloud` \| `hetzner` \| `njalla` \| `route53` \| `vultr`); leave empty to use manual `fullchain.pem`/`privatekey.pem` from `IDENTITY_PATH` |
62-
| `ENS_GASLESS_ENABLED` | `false` | bool | Enable ENS gasless DNS import automation for the managed DNS zone and lease hostnames |
61+
| `ACME_DNS_PROVIDER` | `""` | string | DNS provider for managed DNS-01/A-record sync, the relay ECH record, opt-in tunnel ECH records, and ENS gasless DNSSEC/TXT automation (`embedded` \| `cloudflare` \| `gcloud` \| `hetzner` \| `njalla` \| `route53` \| `vultr`); unset defaults to `embedded`; manual `fullchain.pem`/`privatekey.pem` in `IDENTITY_PATH` is used when present |
62+
| `ENS_GASLESS_ENABLED` | `false` | bool | Enable ENS gasless DNS import automation for the managed DNS zone and lease hostnames; not supported with `ACME_DNS_PROVIDER=embedded` yet |
63+
64+
### Embedded DNS
65+
66+
Serves the relay base domain from an authoritative DNS server embedded in the relay process, so no DNS provider API credentials are required. It is the default provider when `ACME_DNS_PROVIDER` is unset. Delegate the base domain once at the parent zone (`NS portal.example.com -> ns.portal.example.com` with glue `A` pointing at the relay public IP) and open `53/tcp` + `53/udp`. Containers running without root need `CAP_NET_BIND_SERVICE` to bind the default port. A answers for the apex and every covered name are synthesized from the relay public IPv4; ACME DNS-01 TXT and tunnel ECH HTTPS records are served directly. ENS gasless automation (zone DNSSEC) is not supported yet.
67+
68+
| Variable | Default | Type | Description |
69+
|----------|---------|------|-------------|
70+
| `EMBEDDED_DNS_PORT` | `53` | int | Listen port for the embedded authoritative DNS server (UDP and TCP) |
6371

6472
### Diagnostics
6573

docs/src/routes/deployment/+page.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,9 @@ SNI router.
3131

3232
- A public Linux server with Docker and Docker Compose.
3333
- A public hostname such as `portal.example.com`.
34-
- DNS `A` records for `portal.example.com` and `*.portal.example.com`.
35-
- Inbound `443/tcp` and `51820/udp` when the overlay is enabled.
36-
- A certificate covering the root and wildcard names, or a configured Portal
37-
DNS provider that can issue it.
34+
- A one-time NS delegation at the parent zone: `NS portal.example.com -> ns.portal.example.com` with a glue `A` record pointing at the relay public IP.
35+
- Inbound `443/tcp`, `53/tcp` + `53/udp` for the embedded authoritative DNS, and `51820/udp` when the overlay is enabled.
36+
- Certificates for the root and wildcard names are issued automatically via ACME DNS-01 against the embedded authoritative DNS.
3837

3938
## Configuration
4039

@@ -48,8 +47,12 @@ LANDING_PAGE_ENABLED=false
4847
DISCOVERY=false
4948
BOOTSTRAPS=
5049
51-
ACME_DNS_PROVIDER=cloudflare
52-
CLOUDFLARE_TOKEN=replace-with-an-api-token
50+
# Embedded authoritative DNS is the default provider and needs no API
51+
# credentials once the NS delegation above is in place. External providers
52+
# (cloudflare, gcloud, hetzner, njalla, route53, vultr) remain available by
53+
# setting ACME_DNS_PROVIDER explicitly.
54+
ACME_DNS_PROVIDER=
55+
EMBEDDED_DNS_PORT=53
5356
```
5457

5558
`LANDING_PAGE_ENABLED` supplies the initial value. Changes made from the admin
@@ -80,7 +83,8 @@ Frameworks that require a live SSR server cannot be mounted as static files.
8083
Run those applications separately and call the Portal API over HTTPS; the API
8184
allows cross-origin requests. Static-export modes can use the mount directly.
8285

83-
When `ACME_DNS_PROVIDER` is empty, place these files in `./.portal-certs`:
86+
To override ACME with a manually managed certificate, place these files in
87+
`./.portal-certs`:
8488

8589
```text
8690
fullchain.pem
@@ -109,6 +113,7 @@ The Compose stack publishes:
109113
| Port | Purpose |
110114
|---|---|
111115
| `443/tcp` | Portal HTTPS, SPA, APIs, and SNI tunnel ingress |
116+
| `53/tcp` + `53/udp` | Embedded authoritative DNS for the delegated relay zone |
112117
| `51820/udp` | Relay discovery overlay |
113118
| configured lease range | Optional UDP and raw TCP leases |
114119

@@ -186,8 +191,10 @@ non-reserved client routes fall back to the selected SPA's `index.html`.
186191
### Certificate Errors
187192

188193
Confirm `PORTAL_URL` matches the certificate root hostname and inspect the
189-
certificate files under `IDENTITY_PATH`. With managed ACME, verify the DNS API
190-
token has permission to update the selected zone.
194+
certificate files under `IDENTITY_PATH`. With the embedded DNS provider,
195+
confirm the delegation is visible (`dig @<relay public IP> portal.example.com NS`)
196+
and that `53/tcp` + `53/udp` are reachable. With an external provider, verify
197+
the DNS API token has permission to update the selected zone.
191198

192199
### API Port 4017
193200

docs/src/routes/self-hosting/+page.md

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,23 @@ You should have a relay running and accepting tunnel connections in about 10 min
1717
- A Linux server with a static public IP
1818
- A domain name you control (e.g. `relay.example.com`)
1919
- Inbound `443/tcp` open for the dashboard, relay APIs, and SNI tunnel traffic
20+
- Inbound `53/tcp` + `53/udp` open for the embedded authoritative DNS
2021

2122
## Quick Start
2223

2324
Run the relay with a single Docker command:
2425

2526
```bash
2627
mkdir -p ./relay-data
27-
# Put fullchain.pem and privatekey.pem in ./relay-data first, or configure ACME below.
28+
# Optional: place fullchain.pem/privatekey.pem in ./relay-data to use a manual
29+
# certificate instead of ACME.
2830
docker run -d \
2931
--name portal-relay \
3032
--restart unless-stopped \
33+
--cap-add NET_BIND_SERVICE \
3134
-p 443:443 \
35+
-p 53:53/tcp \
36+
-p 53:53/udp \
3237
-e PORTAL_URL=https://relay.example.com \
3338
-e IDENTITY_PATH=/portal-certs \
3439
-e ADMIN_TOKEN="$(openssl rand -hex 32)" \
@@ -49,8 +54,13 @@ services:
4954
relay:
5055
image: ghcr.io/gosuda/portal:2
5156
restart: unless-stopped
57+
# Binding the default embedded DNS port 53 as a nonroot container.
58+
cap_add:
59+
- NET_BIND_SERVICE
5260
ports:
5361
- "443:443"
62+
- "53:53/tcp"
63+
- "53:53/udp"
5464
environment:
5565
PORTAL_URL: https://relay.example.com
5666
API_PORT: "4017"
@@ -76,7 +86,7 @@ docker compose up -d
7686
| `SNI_PORT` | `443` | TCP SNI router port for tunnel traffic. |
7787
| `IDENTITY_PATH` | `./.portal-certs` | Relay state directory containing `identity.json`, `policy.json`, and TLS materials. |
7888
| `ADMIN_TOKEN` | | Bearer token source for relay admin and policy APIs. |
79-
| `LANDING_PAGE_ENABLED` | `false` | Initial dashboard landing-page visibility; admin changes persist in `policy.json`. |
89+
| `EMBEDDED_DNS_PORT` | `53` | Embedded authoritative DNS listen port; requires `53/tcp` + `53/udp` and `CAP_NET_BIND_SERVICE` in containers. |
8090

8191
## Optional: Enable Relay-Owned Sui x402 Facilitator
8292

@@ -122,18 +132,25 @@ portal-relay localhost:3000
122132

123133
## DNS Configuration
124134

125-
Tunnels are assigned subdomains under your relay domain (e.g. `abc123.relay.example.com`). You need a wildcard DNS record pointing to your server:
135+
The relay serves DNS for its own subdomains from the embedded authoritative
136+
server (`relay.example.com` and every name under it, including tunnel
137+
hostnames). Delegate the zone to the relay once from your existing DNS
138+
management UI:
126139

127140
| Type | Name | Value |
128141
|---|---|---|
129-
| `A` | `*.relay.example.com` | `<your server IP>` |
130-
| `A` | `relay.example.com` | `<your server IP>` |
142+
| `NS` | `relay.example.com` | `ns.relay.example.com` |
143+
| `A` | `ns.relay.example.com` | `<your server IP>` (glue) |
144+
No wildcard record is needed: the relay synthesizes answers for every tunnel
145+
hostname. The nameserver name is fixed to `ns.<your relay domain>`; publish the
146+
matching glue `A` record at the parent zone as shown above.
131147

132-
DNS propagation typically takes a few minutes but can take up to 48 hours depending on your provider.
148+
## TLS with ACME
133149

134-
## Optional: TLS with ACME
135-
136-
By default the relay expects you to place `fullchain.pem` and `privatekey.pem` in the `IDENTITY_PATH` directory (`.portal-certs` by default). For automatic certificate management via DNS-01 challenges, set `ACME_DNS_PROVIDER`:
150+
Certificates are issued automatically via ACME DNS-01 against the embedded
151+
authoritative DNS server — no DNS provider credentials are required once the
152+
delegation above is in place. To use an external DNS provider instead, set
153+
`ACME_DNS_PROVIDER`:
137154

138155
```yaml
139156
environment:
@@ -175,21 +192,28 @@ requires TCP `443` because Portal publishes standard HTTPS tunnel URLs.
175192

176193
**DNS not resolving**
177194

178-
Verify your wildcard record is live before connecting a tunnel:
195+
Query the relay's authoritative server directly first, then through a public
196+
resolver:
179197

180198
```bash
199+
dig +short @<your server IP> test.relay.example.com
181200
dig +short test.relay.example.com
182201
```
183202

184-
If nothing returns, check your DNS provider dashboard and allow more time for propagation.
203+
If the direct query works but the public one does not, the NS delegation at
204+
the parent zone is missing or not yet propagated. If both fail, confirm the
205+
relay is running and `53/tcp` + `53/udp` are open.
185206

186207
**Firewall blocking connections**
187208

188-
Ensure the public HTTPS port is open in your cloud provider's security group or firewall:
209+
Ensure the public HTTPS and DNS ports are open in your cloud provider's
210+
security group or firewall:
189211

190212
```bash
191213
# UFW example
192214
sudo ufw allow 443/tcp
215+
sudo ufw allow 53/tcp
216+
sudo ufw allow 53/udp
193217
```
194218

195219
**Certificate errors**

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/knadh/koanf/parsers/toml/v2 v2.2.0
2323
github.com/knadh/koanf/providers/file v1.2.1
2424
github.com/knadh/koanf/v2 v2.3.4
25+
github.com/miekg/dns v1.1.72
2526
github.com/montanaflynn/stats v0.9.0
2627
github.com/pelletier/go-toml/v2 v2.2.4
2728
github.com/prometheus/client_golang v1.23.2
@@ -118,7 +119,6 @@ require (
118119
github.com/mattn/go-isatty v0.0.21 // indirect
119120
github.com/mattn/go-localereader v0.0.1 // indirect
120121
github.com/mattn/go-runewidth v0.0.19 // indirect
121-
github.com/miekg/dns v1.1.72 // indirect
122122
github.com/mitchellh/copystructure v1.2.0 // indirect
123123
github.com/mitchellh/reflectwalk v1.0.2 // indirect
124124
github.com/mr-tron/base58 v1.2.0 // indirect

plugins/portal-deploy/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ plugins/portal-deploy/
1818
│ ├── agents/openai.yaml # Codex skill UI metadata
1919
│ └── references/
2020
│ ├── portal-cli.md
21-
│ └── safety-and-verification.md
21+
│ ├── safety-and-verification.md
22+
│ └── x402.md
2223
└── README.md
2324
```
2425

0 commit comments

Comments
 (0)