Settings → IP/DNS and Hostname: editable DNS + per-host /etc/hosts editor
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/superpowers/specs/2026-06-29-settings-dns-hosts-design.md. Edit at the source, not here.
Settings → IP/DNS and Hostname: editable DNS + per-host /etc/hosts editor
Section titled “Settings → IP/DNS and Hostname: editable DNS + per-host /etc/hosts editor”- Date: 2026-06-29
- Status: Approved (design) — pending implementation plan
- Owner crate(s):
synvirt-network(data plane),synvirt-daemon(API),web-ux-v2(UI) - Scope: per-host (no fleet fan-out)
1. Problem
Section titled “1. Problem”While wiring an external migrator source (a vCenter at vcenter.synnet.mx) on
synvirt-02 (10.10.26.12), discovery failed with a transport error
(could not connect) because the name does not resolve: .12’s
/etc/resolv.conf carries only 8.8.8.8 (the hardcoded default), and there is
no internal record for vcenter.synnet.mx on any reachable resolver. Trying to
fix this from the dashboard surfaced two real gaps:
-
DNS is not editable in Settings. The Settings section is literally named “IP/DNS and Hostname” (
web-ux-v2/src/views/SettingsView.vue:81, described as managing “DNS servers, and search domains”), and the plumbing already carries DNS — the draft and the apply payload both includedns(NetworkPanel.vue:67,124;ServicePortConfig.dnsinapi/network.ts:44). But the DNS<input>is missing from the form: the management-interface editor only renders Address / Gateway / VLAN / MTU (NetworkPanel.vue:265-296). The panel reads the current DNS and re-applies it unchanged, with no way to edit it. -
/etc/hostshas no surface anywhere in the running product — no API, no UI. Only the installer writes the127.0.1.1line once at install time (daemon/src/api/install.rs:1225,installer/src/apply/hostname.rs). There is no way to add a host entry likevcenter.synnet.mx → <IP>from the dashboard.
2. Goals / Non-goals
Section titled “2. Goals / Non-goals”Goals
- Make the per-management-interface DNS servers editable from Settings → IP/DNS and Hostname (frontend-only; backend already supports it).
- Add a per-host
/etc/hostsentries editor: model + materializer insynvirt-network, REST surface on the daemon, and a card in the same Settings panel.
Non-goals (explicitly out of scope)
- Search domains (the
VmKernelmodel has no field for them today; deferred as a separate extension —Domains=in the resolved drop-in + a new model field). - Fleet-wide fan-out of host entries (entries are per-host by decision).
- Editing the system-owned lines of
/etc/hosts(loopback etc.). We only own a delimited managed block.
3. Design
Section titled “3. Design”3.1 Piece 1 — DNS field (frontend only)
Section titled “3.1 Piece 1 — DNS field (frontend only)”VmKernel.dns: Vec<String> already exists
(synvirt-network/src/spec/mod.rs:406) and the apply path
(applyRow → ServicePortConfig → vSwitch service-port apply, commit-confirm +
rollback) already round-trips dns. The only change is UI: add a DNS input
to the management-interface grid in
web-ux-v2/src/views/settings/NetworkPanel.vue (next to Address/Gateway/VLAN/
MTU), bound to draftOf(m.vswitch).dns with comma/space-separated text ↔
string[] conversion (the exact parse the ServicePortModal/VSwitchModal
already use: value.split(/[,\s]+/).map(s=>s.trim()).filter(Boolean)). Dirty
detection already compares d.dns.join(',') !== m.dns.join(',')
(NetworkPanel.vue:102).
No backend, no API change.
3.2 Piece 2 — /etc/hosts editor
Section titled “3.2 Piece 2 — /etc/hosts editor”Data model — new top-level field on NetworkYaml
(synvirt-network/src/spec/mod.rs:27):
/// One managed entry in `/etc/hosts`: an address mapped to one or more names.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]pub struct HostEntry { /// IPv4 or IPv6 literal. pub ip: String, /// One or more hostnames/aliases for this address (RFC 1123 each). pub hostnames: Vec<String>, /// Optional operator note, rendered as a trailing `# comment`. #[serde(default, skip_serializing_if = "Option::is_none")] pub comment: Option<String>,}
// NetworkYaml gains:// #[serde(default)] pub host_entries: Vec<HostEntry>,#[serde(default)] keeps existing network.yaml files (no host_entries key)
parsing unchanged.
Materializer — new module synvirt-network/src/hosts.rs, mirroring
dns.rs:
pub fn apply_hosts(yaml: &NetworkYaml) -> Result<HostsApplyReport, HostsApplyError>;-
Target file:
/etc/hosts. -
Rewrites only a delimited managed block; everything outside is preserved byte-for-byte:
# >>> SYNVIRT MANAGED — do not edit by hand172.16.11.50 vcenter.synnet.mx vcenter # added via dashboard# <<< SYNVIRT MANAGED -
Idempotent: regenerating with the same entries produces an identical file.
-
If the block is absent, append it at EOF; if
host_entriesis empty, remove the block (and its surrounding blank line) entirely. -
Never touches loopback/system lines. Refuses to emit an entry whose hostname collides with
localhost/the loopback names or the host’s own canonical name (validation, below). -
Writes atomically (temp file in
/etc+ rename) to avoid a torn/etc/hosts.
Apply timing — two invocation points, same as DNS:
- Boot reconcile: call
hosts::apply_hosts(&yaml)insynvirt-network/src/main.rsimmediately after the existingdns::apply_dnscall (main.rs:334), insidespawn_blocking. This makes the block boot-persistent. - On-demand: when the operator saves from the UI, the daemon persists YAML via the controller and the controller materializes the block immediately, so resolution works without a reboot. This is a local file write only — it does not run OVS reconcile or touch L2/L3, so the nested-host bond-break risk does not apply.
Controller surface — synvirt-network/src/controller.rs, following the
hostname() / set_hostname() pair (controller.rs:966,974):
pub async fn host_entries(&self) -> Vec<HostEntry>;pub async fn set_host_entries(&self, entries: Vec<HostEntry>) -> Result<(), NetworkError>;set_host_entries validates, writes network.yaml via yaml_store, then calls
hosts::apply_hosts on the new YAML.
Daemon API — extend the existing Settings → Network surface
(daemon/src/settings/api/network.rs, mounted in settings/api/mod.rs):
GET /api/v1/settings/network— addhost_entries: Vec<HostEntryView>toNetworkSettingsView(the view that already returns hostname + ipMGMT).PUT /api/v1/settings/network/hosts— new route insettings/api/mod.rs; body{ "entries": [ { "ip", "hostnames": [...], "comment"? } ] }. Handler validates and delegates toNetworkController::set_host_entries. Audited like the hostname PUT.operation_id = "put_settings_network_hosts".
Validation (in synvirt-network, surfaced as E_* codes):
ipparses asIpAddr(v4 or v6) — elseE_HOST_ENTRY_BAD_IP.- each hostname passes
is_rfc1123_hostname(spec/mod.rs:574) — elseE_HOST_ENTRY_BAD_NAME. - at least one hostname per entry — else
E_HOST_ENTRY_NO_NAME. - no entry may map a name to
127.0.0.1/::1aliases reserved for loopback, or shadow the host’s own canonical hostname — elseE_HOST_ENTRY_RESERVED_NAME. - duplicate
(ip, hostname)pairs are de-duplicated (warning, not error).
Frontend — web-ux-v2:
- New card “Host file entries” inside
views/settings/NetworkPanel.vue(same panel as DNS/hostname — it is a name-resolution concern). An editable table: IP, hostnames (comma/space-separated), comment; add-row / delete-row; an Apply button with commit semantics + outcome banner. - Extend
useHostNetworkcomposable +api/settings/network.tswith thehost_entriesread and thePUT .../hostscall. Map the newE_*codes inERROR_MAP. - Regenerate
openapi.snapshot.jsonafter the endpoint lands and re-rungenerate:api:from-file(frozen-contract rule).
4. Testing
Section titled “4. Testing”synvirt-network(hosts.rs) — primary risk, TDD here:- generates the managed block with correct formatting for v4 + v6 + multiple hostnames + comment;
- preserves all lines outside the block byte-for-byte;
- idempotent re-apply produces an identical file;
- empty
host_entriesremoves the block cleanly; - rejects invalid IP / invalid hostname / reserved-name collisions;
- atomic write (no torn file on simulated failure).
controller:set_host_entriesrejects invalid input and persists + materializes valid input (mirrorset_hostname_rejects_invalid_and_persists_valid,controller/tests.rs:2190).- daemon:
PUT /api/v1/settings/network/hostsvalidation + persistence;GETreturns the entries. - web-ux-v2 (vitest): the new card parses comma/space input, computes
dirty/apply state, and renders mapped errors; the DNS input round-trips
string[]↔ text.
5. File-change map
Section titled “5. File-change map”| Layer | File | Change |
|---|---|---|
| model | synvirt-network/src/spec/mod.rs |
HostEntry struct + NetworkYaml.host_entries |
| materializer | synvirt-network/src/hosts.rs (new) |
apply_hosts + managed-block render + validation |
| boot | synvirt-network/src/main.rs |
call hosts::apply_hosts after dns::apply_dns (~:334) |
| controller | synvirt-network/src/controller.rs |
host_entries() / set_host_entries() |
| lib | synvirt-network/src/lib.rs |
pub mod hosts; |
| API | daemon/src/settings/api/network.rs |
host_entries in view + put_hosts handler |
| API | daemon/src/settings/api/mod.rs |
route PUT /v1/settings/network/hosts |
| UI | web-ux-v2/.../settings/NetworkPanel.vue |
DNS input + Host-file-entries card |
| UI | web-ux-v2/src/composables/useHostNetwork.ts |
host_entries read + PUT |
| UI | web-ux-v2/src/api/settings/network.ts |
types + client call |
| contract | daemon openapi + openapi.snapshot.json |
re-dump + regenerate TS |
6. Deployment note
Section titled “6. Deployment note”Build + commit lands autonomously. Fleet deploy (.11/.12/.13) is gated on an
explicit go per the standing SEV-0 deploy discipline; this spec does not
deploy. The first real use is adding vcenter.synnet.mx → <IP> on .12 once
the editor ships (still pending the vCenter’s IP).
7. Open questions
Section titled “7. Open questions”None blocking. Search domains intentionally deferred (§2). Reserved-name policy
(loopback + own-hostname) is the one judgment call; implemented as a hard reject
with a clear E_* message.