mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add free domain DNS onboarding hints
This commit is contained in:
@@ -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 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 doctor
|
||||
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)
|
||||
- [Configuration](docs/configuration.md)
|
||||
- [Daemon management](docs/daemon-management.md)
|
||||
- [Domain onboarding](docs/domain-onboarding.md)
|
||||
- [Fixture manifests](docs/fixture-manifests.md)
|
||||
- [State contract](docs/state-contract.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 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 --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 check --profile vps-full
|
||||
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
|
||||
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
|
||||
lets a full site config build or activate only Tor, only I2P, or a selected subset without
|
||||
unselected transports blocking the run.
|
||||
|
||||
+24
@@ -43,6 +43,7 @@ from .dns import (
|
||||
DNS_MODES,
|
||||
DNSPlan,
|
||||
DNSRecordPlan,
|
||||
FreeDomainHint,
|
||||
dns_check,
|
||||
dns_plan,
|
||||
)
|
||||
@@ -197,6 +198,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
action="store_true",
|
||||
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(
|
||||
"check",
|
||||
help="Resolve clearnet domains and compare optional expected addresses.",
|
||||
@@ -598,11 +604,14 @@ def _cmd_dns(config, args) -> int:
|
||||
ipv6=args.ipv6,
|
||||
dynamic_hostname=args.dynamic_hostname,
|
||||
behind_router=args.behind_router,
|
||||
free_domain_hints=args.free_domain_hints,
|
||||
)
|
||||
for record in plan.records:
|
||||
_print_dns_record(record)
|
||||
for hint in plan.hints:
|
||||
_print_connectivity_hint(hint)
|
||||
for hint in plan.free_domains:
|
||||
_print_free_domain_hint(hint)
|
||||
_print_dns_summary(plan)
|
||||
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:
|
||||
statuses = [record.status for record in plan.records]
|
||||
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"records={len(plan.records)} "
|
||||
f"hints={len(plan.hints)} "
|
||||
f"free_domain_hints={len(plan.free_domains)} "
|
||||
f"todo={statuses.count('todo')} "
|
||||
f"review={statuses.count('review')} "
|
||||
f"blocked={statuses.count('blocked')} "
|
||||
|
||||
+72
@@ -28,11 +28,24 @@ class ConnectivityHint:
|
||||
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)
|
||||
class DNSPlan:
|
||||
status: str
|
||||
records: tuple[DNSRecordPlan, ...]
|
||||
hints: tuple[ConnectivityHint, ...]
|
||||
free_domains: tuple[FreeDomainHint, ...]
|
||||
message: str
|
||||
|
||||
|
||||
@@ -55,6 +68,7 @@ def dns_plan(
|
||||
ipv6: str | None = None,
|
||||
dynamic_hostname: str | None = None,
|
||||
behind_router: bool = False,
|
||||
free_domain_hints: bool = False,
|
||||
) -> DNSPlan:
|
||||
if mode not in DNS_MODES:
|
||||
raise ValueError(f"unsupported DNS mode {mode!r}")
|
||||
@@ -64,6 +78,7 @@ def dns_plan(
|
||||
status="skipped",
|
||||
records=(),
|
||||
hints=(),
|
||||
free_domains=(),
|
||||
message="clearnet is not selected",
|
||||
)
|
||||
|
||||
@@ -74,15 +89,72 @@ def dns_plan(
|
||||
else:
|
||||
records.extend(_dynamic_records(site_id, domain, dynamic_hostname=dynamic_hostname))
|
||||
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])
|
||||
return DNSPlan(
|
||||
status=status,
|
||||
records=tuple(records),
|
||||
hints=tuple(hints),
|
||||
free_domains=free_domains,
|
||||
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(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
|
||||
@@ -39,6 +39,7 @@ config artifacts.
|
||||
```sh
|
||||
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 --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 check --profile vps-full
|
||||
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
|
||||
records require provider-specific ALIAS/ANAME or API-updated A/AAAA support. With
|
||||
`--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,
|
||||
adapter, installed/running daemon probe, action, and policy result.
|
||||
|
||||
@@ -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.
|
||||
@@ -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 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 --free-domain-hints
|
||||
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 manifest
|
||||
|
||||
@@ -93,6 +93,27 @@ class DNSTest(unittest.TestCase):
|
||||
self.assertIn("AMPG_CONNECTIVITY_HINT method=port-forward", 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):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config_path = _init_config(Path(tmp).resolve(), preset="i2p-only")
|
||||
|
||||
Reference in New Issue
Block a user