Add free domain DNS onboarding hints

This commit is contained in:
jwinterm
2026-07-06 18:52:57 -04:00
parent 1dd601b0e4
commit b2bf06e050
7 changed files with 182 additions and 1 deletions
+8
View File
@@ -52,6 +52,7 @@ python3 -m ampg --config gateway.toml init site wownero \
python3 -m ampg --config gateway.toml deploy plan --profile vps-full python3 -m ampg --config gateway.toml deploy plan --profile vps-full
python3 -m ampg --config gateway.toml dns plan --mode dynamic --behind-router python3 -m ampg --config gateway.toml dns plan --mode dynamic --behind-router
python3 -m ampg --config gateway.toml dns plan --free-domain-hints
python3 -m ampg --config gateway.toml build python3 -m ampg --config gateway.toml build
python3 -m ampg --config gateway.toml doctor python3 -m ampg --config gateway.toml doctor
python3 -m ampg --config gateway.toml install-plan --profile vps-full --write-artifacts python3 -m ampg --config gateway.toml install-plan --profile vps-full --write-artifacts
@@ -91,6 +92,7 @@ API remains internal even when the public site is available on Tor or I2P.
- [Architecture](docs/architecture.md) - [Architecture](docs/architecture.md)
- [Configuration](docs/configuration.md) - [Configuration](docs/configuration.md)
- [Daemon management](docs/daemon-management.md) - [Daemon management](docs/daemon-management.md)
- [Domain onboarding](docs/domain-onboarding.md)
- [Fixture manifests](docs/fixture-manifests.md) - [Fixture manifests](docs/fixture-manifests.md)
- [State contract](docs/state-contract.md) - [State contract](docs/state-contract.md)
- [Interaction capabilities](docs/interaction-capabilities.md) - [Interaction capabilities](docs/interaction-capabilities.md)
@@ -113,6 +115,7 @@ The current implementation is dependency-free Python:
python3 -m ampg --config gateway.toml init site wownero --domain wownero.org --source ../wownero.org-website --preset full python3 -m ampg --config gateway.toml init site wownero --domain wownero.org --source ../wownero.org-website --preset full
python3 -m ampg --config examples/wownero.gateway.toml deploy plan --profile vps-full python3 -m ampg --config examples/wownero.gateway.toml deploy plan --profile vps-full
python3 -m ampg --config examples/wownero.gateway.toml dns plan --profile vps-full python3 -m ampg --config examples/wownero.gateway.toml dns plan --profile vps-full
python3 -m ampg --config examples/wownero.gateway.toml dns plan --profile vps-full --free-domain-hints
python3 -m ampg --config examples/wownero.gateway.toml dns plan --profile vps-full --mode dynamic --behind-router python3 -m ampg --config examples/wownero.gateway.toml dns plan --profile vps-full --mode dynamic --behind-router
python3 -m ampg --config examples/wownero.gateway.toml dns check --profile vps-full python3 -m ampg --config examples/wownero.gateway.toml dns check --profile vps-full
python3 -m ampg --config examples/wownero.gateway.toml plan python3 -m ampg --config examples/wownero.gateway.toml plan
@@ -183,6 +186,11 @@ ready to copy into `gateway.state_dir`. The live form requires `--yes`; it creat
AMPG-owned state directories and copies approved artifact contents. It does not install AMPG-owned state directories and copies approved artifact contents. It does not install
packages, change adopted daemons, start services, or remove keys. packages, change adopted daemons, start services, or remove keys.
`dns plan --free-domain-hints` prints optional community subdomain services that may help
new users get a clearnet name without buying a domain. These are third-party registries;
operators still need to review current terms, availability, content rules, and DNS
record support before relying on one.
Use `--protocol` to scope operational commands to one or more enabled protocols. This Use `--protocol` to scope operational commands to one or more enabled protocols. This
lets a full site config build or activate only Tor, only I2P, or a selected subset without lets a full site config build or activate only Tor, only I2P, or a selected subset without
unselected transports blocking the run. unselected transports blocking the run.
+24
View File
@@ -43,6 +43,7 @@ from .dns import (
DNS_MODES, DNS_MODES,
DNSPlan, DNSPlan,
DNSRecordPlan, DNSRecordPlan,
FreeDomainHint,
dns_check, dns_check,
dns_plan, dns_plan,
) )
@@ -197,6 +198,11 @@ def main(argv: list[str] | None = None) -> int:
action="store_true", action="store_true",
help="Include NAT/router/tunnel reachability hints.", help="Include NAT/router/tunnel reachability hints.",
) )
dns_plan_parser.add_argument(
"--free-domain-hints",
action="store_true",
help="Include optional free subdomain registries to consider.",
)
dns_check_parser = dns_subcommands.add_parser( dns_check_parser = dns_subcommands.add_parser(
"check", "check",
help="Resolve clearnet domains and compare optional expected addresses.", help="Resolve clearnet domains and compare optional expected addresses.",
@@ -598,11 +604,14 @@ def _cmd_dns(config, args) -> int:
ipv6=args.ipv6, ipv6=args.ipv6,
dynamic_hostname=args.dynamic_hostname, dynamic_hostname=args.dynamic_hostname,
behind_router=args.behind_router, behind_router=args.behind_router,
free_domain_hints=args.free_domain_hints,
) )
for record in plan.records: for record in plan.records:
_print_dns_record(record) _print_dns_record(record)
for hint in plan.hints: for hint in plan.hints:
_print_connectivity_hint(hint) _print_connectivity_hint(hint)
for hint in plan.free_domains:
_print_free_domain_hint(hint)
_print_dns_summary(plan) _print_dns_summary(plan)
return 1 if plan.status == "blocked" else 0 return 1 if plan.status == "blocked" else 0
@@ -1094,6 +1103,20 @@ def _print_connectivity_hint(hint: ConnectivityHint) -> None:
) )
def _print_free_domain_hint(hint: FreeDomainHint) -> None:
print(
"AMPG_FREE_DOMAIN_HINT "
f"provider=\"{_quote(hint.provider)}\" "
f"suffixes=\"{_quote(','.join(hint.suffixes))}\" "
f"fit=\"{_quote(hint.fit)}\" "
f"records=\"{_quote(hint.records)}\" "
f"workflow=\"{_quote(hint.workflow)}\" "
f"status={hint.status} "
f"url=\"{_quote(hint.url)}\" "
f"message=\"{_quote(hint.message)}\""
)
def _print_dns_summary(plan: DNSPlan) -> None: def _print_dns_summary(plan: DNSPlan) -> None:
statuses = [record.status for record in plan.records] statuses = [record.status for record in plan.records]
statuses.extend(hint.status for hint in plan.hints) statuses.extend(hint.status for hint in plan.hints)
@@ -1102,6 +1125,7 @@ def _print_dns_summary(plan: DNSPlan) -> None:
f"status={plan.status} " f"status={plan.status} "
f"records={len(plan.records)} " f"records={len(plan.records)} "
f"hints={len(plan.hints)} " f"hints={len(plan.hints)} "
f"free_domain_hints={len(plan.free_domains)} "
f"todo={statuses.count('todo')} " f"todo={statuses.count('todo')} "
f"review={statuses.count('review')} " f"review={statuses.count('review')} "
f"blocked={statuses.count('blocked')} " f"blocked={statuses.count('blocked')} "
+72
View File
@@ -28,11 +28,24 @@ class ConnectivityHint:
message: str message: str
@dataclass(frozen=True)
class FreeDomainHint:
provider: str
suffixes: tuple[str, ...]
fit: str
records: str
workflow: str
status: str
url: str
message: str
@dataclass(frozen=True) @dataclass(frozen=True)
class DNSPlan: class DNSPlan:
status: str status: str
records: tuple[DNSRecordPlan, ...] records: tuple[DNSRecordPlan, ...]
hints: tuple[ConnectivityHint, ...] hints: tuple[ConnectivityHint, ...]
free_domains: tuple[FreeDomainHint, ...]
message: str message: str
@@ -55,6 +68,7 @@ def dns_plan(
ipv6: str | None = None, ipv6: str | None = None,
dynamic_hostname: str | None = None, dynamic_hostname: str | None = None,
behind_router: bool = False, behind_router: bool = False,
free_domain_hints: bool = False,
) -> DNSPlan: ) -> DNSPlan:
if mode not in DNS_MODES: if mode not in DNS_MODES:
raise ValueError(f"unsupported DNS mode {mode!r}") raise ValueError(f"unsupported DNS mode {mode!r}")
@@ -64,6 +78,7 @@ def dns_plan(
status="skipped", status="skipped",
records=(), records=(),
hints=(), hints=(),
free_domains=(),
message="clearnet is not selected", message="clearnet is not selected",
) )
@@ -74,15 +89,72 @@ def dns_plan(
else: else:
records.extend(_dynamic_records(site_id, domain, dynamic_hostname=dynamic_hostname)) records.extend(_dynamic_records(site_id, domain, dynamic_hostname=dynamic_hostname))
hints = _connectivity_hints(behind_router=behind_router) hints = _connectivity_hints(behind_router=behind_router)
free_domains = FREE_DOMAIN_HINTS if free_domain_hints else ()
status = _plan_status([record.status for record in records] + [hint.status for hint in hints]) status = _plan_status([record.status for record in records] + [hint.status for hint in hints])
return DNSPlan( return DNSPlan(
status=status, status=status,
records=tuple(records), records=tuple(records),
hints=tuple(hints), hints=tuple(hints),
free_domains=free_domains,
message=_plan_message(status), message=_plan_message(status),
) )
FREE_DOMAIN_HINTS: tuple[FreeDomainHint, ...] = (
FreeDomainHint(
provider="is-a.dev",
suffixes=("is-a.dev",),
fit="developer personal sites and projects",
records="DNS records through reviewed repository changes",
workflow="fork repository, add domain config, open pull request",
status="community-reviewed",
url="https://github.com/is-a-dev/register",
message="good beginner option when a developer-branded subdomain is acceptable",
),
FreeDomainHint(
provider="JS.ORG",
suffixes=("js.org",),
fit="JavaScript ecosystem projects",
records="CNAME",
workflow="configure hosting custom domain, then open pull request",
status="strict-scope",
url="https://github.com/js-org/js.org",
message="only use for sites directly related to the JavaScript community",
),
FreeDomainHint(
provider="Open Domains",
suffixes=("is-cool.dev", "is-local.org", "is-not-a.dev", "localplayer.dev"),
fit="students and open-source projects",
records="A, AAAA, CNAME, NS, TXT, and related DNS records",
workflow="register through the current Open Domains web app",
status="verify-current-domains",
url="https://opendomains.andrewstech.me/",
message="GitHub registry moved to a web app; verify the current domain list before use",
),
FreeDomainHint(
provider="Community GitHub registries",
suffixes=(
"is-an.app",
"cluster.ws",
"wip.la",
"thedev.id",
"io.day",
"jsid.dev",
"is-a.co",
"is-a-good.dev",
"is-really.cool",
"js.cool",
),
fit="personal sites, hobby apps, and open-source projects",
records="varies by registry",
workflow="review registry terms, then open the requested issue or pull request",
status="verify-before-use",
url="https://github.com/tarampampam/domains",
message="treat as optional naming ideas; availability and rules can change",
),
)
def dns_check( def dns_check(
config: GatewayConfig, config: GatewayConfig,
*, *,
+4 -1
View File
@@ -39,6 +39,7 @@ config artifacts.
```sh ```sh
python3 -m ampg --config gateway.toml deploy plan --profile mobile-i2p python3 -m ampg --config gateway.toml deploy plan --profile mobile-i2p
python3 -m ampg --config gateway.toml dns plan --profile vps-full python3 -m ampg --config gateway.toml dns plan --profile vps-full
python3 -m ampg --config gateway.toml dns plan --profile vps-full --free-domain-hints
python3 -m ampg --config gateway.toml dns plan --profile vps-full --mode dynamic --behind-router python3 -m ampg --config gateway.toml dns plan --profile vps-full --mode dynamic --behind-router
python3 -m ampg --config gateway.toml dns check --profile vps-full python3 -m ampg --config gateway.toml dns check --profile vps-full
python3 -m ampg --config gateway.toml status python3 -m ampg --config gateway.toml status
@@ -68,7 +69,9 @@ to run and does not change files or services.
and `www` records. Dynamic mode supports a Dynamic DNS hostname and warns when apex and `www` records. Dynamic mode supports a Dynamic DNS hostname and warns when apex
records require provider-specific ALIAS/ANAME or API-updated A/AAAA support. With records require provider-specific ALIAS/ANAME or API-updated A/AAAA support. With
`--behind-router`, it also lists port forwarding, IPv6, explicit router mapping, `--behind-router`, it also lists port forwarding, IPv6, explicit router mapping,
reverse-tunnel, and DNS-01 certificate options. reverse-tunnel, and DNS-01 certificate options. With `--free-domain-hints`, it also
prints optional third-party community subdomain registries for operators who do not yet
own a domain.
`status` prints one row per enabled protocol with the selected platform provider, `status` prints one row per enabled protocol with the selected platform provider,
adapter, installed/running daemon probe, action, and policy result. adapter, installed/running daemon probe, action, and policy result.
+52
View File
@@ -0,0 +1,52 @@
# Domain Onboarding
> Status: draft | Updated 2026-07-06 | Applies to: clearnet DNS setup
AMPG treats clearnet naming as an operator choice. A site can use a normal owned domain,
a community free subdomain, a Dynamic DNS name, or no clearnet name at all when only
private transports are selected.
## Command
```sh
python3 -m ampg --config gateway.toml dns plan
python3 -m ampg --config gateway.toml dns plan --free-domain-hints
python3 -m ampg --config gateway.toml dns plan --mode dynamic --behind-router
python3 -m ampg --config gateway.toml dns check
```
`dns plan --free-domain-hints` prints `AMPG_FREE_DOMAIN_HINT` rows for optional
third-party services that may provide free subdomains for personal sites, hobby apps, or
open-source projects. AMPG does not register these names automatically because each
provider has its own review process, terms, availability, and DNS record support.
## Choices
Owned domain:
: Best for production sites, long-lived projects, storefronts, and anything that needs
strong brand control.
Free community subdomain:
: Useful for experiments, personal projects, demos, and users who cannot buy a domain
yet. Review the provider's current rules before relying on it.
Dynamic DNS:
: Useful for laptops, home servers, old phones, or other hosts whose public IP can
change. Apex-domain support depends on the DNS provider.
Behind-router hosting:
: Use port forwarding, public IPv6, an explicit router mapping, a reverse tunnel, or
DNS-01 certificate validation when inbound port 80 is unavailable.
## Included Hints
AMPG includes conservative hints for:
- `is-a.dev`: developer personal sites and projects.
- `js.org`: JavaScript ecosystem projects; narrower content rules.
- Open Domains: student and open-source project subdomains through the current web app.
- Other community GitHub registries such as `is-an.app`, `wip.la`, `thedev.id`,
`io.day`, `jsid.dev`, `is-a.co`, `is-a-good.dev`, `is-really.cool`, and `js.cool`.
These hints are naming ideas, not guarantees. Verify current status before adding a
domain to a public deployment.
+1
View File
@@ -154,6 +154,7 @@ daemon_policy = "auto"
python3 -m ampg --config gateway.toml init site wownero --domain wownero.org --source ../wownero.org-website --preset full python3 -m ampg --config gateway.toml init site wownero --domain wownero.org --source ../wownero.org-website --preset full
python3 -m ampg --config gateway.toml deploy plan --profile vps-full python3 -m ampg --config gateway.toml deploy plan --profile vps-full
python3 -m ampg --config gateway.toml dns plan --profile vps-full python3 -m ampg --config gateway.toml dns plan --profile vps-full
python3 -m ampg --config gateway.toml dns plan --profile vps-full --free-domain-hints
python3 -m ampg --config examples/wownero.gateway.toml plan python3 -m ampg --config examples/wownero.gateway.toml plan
python3 -m ampg --config examples/wownero.gateway.toml build python3 -m ampg --config examples/wownero.gateway.toml build
python3 -m ampg --config examples/wownero.gateway.toml manifest python3 -m ampg --config examples/wownero.gateway.toml manifest
+21
View File
@@ -93,6 +93,27 @@ class DNSTest(unittest.TestCase):
self.assertIn("AMPG_CONNECTIVITY_HINT method=port-forward", output) self.assertIn("AMPG_CONNECTIVITY_HINT method=port-forward", output)
self.assertIn("AMPG_CONNECTIVITY_HINT method=reverse-tunnel", output) self.assertIn("AMPG_CONNECTIVITY_HINT method=reverse-tunnel", output)
def test_dns_plan_can_include_free_domain_hints(self):
with tempfile.TemporaryDirectory() as tmp:
config_path = _init_config(Path(tmp).resolve())
status, output, error = _run_cli(
[
"--config",
str(config_path),
"dns",
"plan",
"--free-domain-hints",
]
)
self.assertEqual(0, status, error)
self.assertIn("AMPG_FREE_DOMAIN_HINT", output)
self.assertIn('provider="is-a.dev"', output)
self.assertIn('suffixes="js.org"', output)
self.assertIn("status=verify-before-use", output)
self.assertIn("free_domain_hints=4", output)
def test_dns_plan_skips_when_clearnet_is_not_selected(self): def test_dns_plan_skips_when_clearnet_is_not_selected(self):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
config_path = _init_config(Path(tmp).resolve(), preset="i2p-only") config_path = _init_config(Path(tmp).resolve(), preset="i2p-only")