diff --git a/easytier-core/src/config/api_input.rs b/easytier-core/src/config/api_input.rs
index 61d27e63..5935058b 100644
--- a/easytier-core/src/config/api_input.rs
+++ b/easytier-core/src/config/api_input.rs
@@ -87,6 +87,7 @@ pub(crate) fn set_network_flags(result: &mut NetworkConfig, flags: FlagsPatch) {
result.enable_udp_broadcast_relay = flags.enable_udp_broadcast_relay;
result.disable_sym_hole_punching = flags.disable_sym_hole_punching;
result.enable_magic_dns = flags.accept_dns;
+ result.tld_dns_zone = flags.tld_dns_zone;
result.mtu = flags.mtu.map(|mtu| mtu as i32);
result.data_compress_algo = flags.data_compress_algo;
result.encryption_algorithm = flags.encryption_algorithm;
@@ -513,6 +514,7 @@ impl NetworkConfigExt for NetworkConfig {
}
}),
accept_dns: self.enable_magic_dns,
+ tld_dns_zone: self.tld_dns_zone.clone(),
mtu: self
.mtu
.map(u32::try_from)
@@ -929,6 +931,21 @@ mod tests {
);
}
+ #[test]
+ fn magic_dns_zone_round_trips_missing_default_and_custom_values() {
+ for zone in [None, Some("et.net."), Some("nas.internal.")] {
+ let input = NetworkConfig {
+ tld_dns_zone: zone.map(str::to_owned),
+ ..standalone_config()
+ };
+ let config = input.gen_config().unwrap();
+ let config = TomlConfigLoader::new_from_str(&config.dump()).unwrap();
+ assert_eq!(config.get_flags().tld_dns_zone, zone.unwrap_or("et.net."));
+ let output = NetworkConfig::new_from_config(&config).unwrap();
+ assert_eq!(output.tld_dns_zone.as_deref(), zone);
+ }
+ }
+
#[test]
fn vpn_portal_api_config_round_trips_through_toml_model() {
let input = NetworkConfig {
@@ -1045,6 +1062,22 @@ mod tests {
assert!(config.get_vpn_portal_config().is_none());
}
+ #[cfg(feature = "browser-config")]
+ #[test]
+ fn browser_merge_updates_and_clears_magic_dns_zone() {
+ let original = "[flags]\ntld_dns_zone = \"old.internal.\"\n";
+ for zone in [None, Some("et.net."), Some("nas.internal.")] {
+ let input = NetworkConfig {
+ tld_dns_zone: zone.map(str::to_owned),
+ ..standalone_config()
+ };
+ let merged = merge_network_config_toml(original, &input).unwrap();
+ let output = TomlConfigLoader::new_from_str(&merged).unwrap();
+ assert_eq!(output.get_flags().tld_dns_zone, zone.unwrap_or("et.net."));
+ assert_eq!(output.get_flags_patch().tld_dns_zone.as_deref(), zone);
+ }
+ }
+
#[cfg(feature = "browser-config")]
#[test]
fn browser_merge_reconciles_renamed_flags() {
diff --git a/easytier-proto/proto/api_manage.proto b/easytier-proto/proto/api_manage.proto
index 44c73db2..00e9afad 100644
--- a/easytier-proto/proto/api_manage.proto
+++ b/easytier-proto/proto/api_manage.proto
@@ -50,6 +50,7 @@ message NetworkConfig {
optional bool use_smoltcp = 21;
optional bool disable_ipv6 = 47;
+ optional string tld_dns_zone = 73;
optional bool enable_kcp_proxy = 22;
optional bool disable_kcp_input = 23;
optional bool disable_p2p = 24;
diff --git a/easytier-proto/proto/common.proto b/easytier-proto/proto/common.proto
index 90ffa119..5cbeb9fa 100644
--- a/easytier-proto/proto/common.proto
+++ b/easytier-proto/proto/common.proto
@@ -66,7 +66,7 @@ message Flags {
bool disable_sym_hole_punching = 30 [(easytier.flag) = { default: "false" }];
// tld dns zone for magic dns
- string tld_dns_zone = 31 [(easytier.flag) = { default: "et.net." }];
+ string tld_dns_zone = 31 [(easytier.flag) = { default: "et.net.", form: true }];
bool p2p_only = 32 [(easytier.flag) = { default: "false" }];
diff --git a/easytier-web/frontend-lib/scripts/test-network-config.mjs b/easytier-web/frontend-lib/scripts/test-network-config.mjs
index 08223c22..71622462 100644
--- a/easytier-web/frontend-lib/scripts/test-network-config.mjs
+++ b/easytier-web/frontend-lib/scripts/test-network-config.mjs
@@ -168,6 +168,7 @@ function allFieldFixture() {
mtu: 1280,
mapped_listeners: ['tcp://127.0.0.1:13010'],
enable_magic_dns: true,
+ tld_dns_zone: 'nas.internal.',
enable_private_mode: true,
enable_quic_proxy: true,
disable_quic_input: true,
diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue
index ea89fef8..ab21a562 100644
--- a/easytier-web/frontend-lib/src/components/Config.vue
+++ b/easytier-web/frontend-lib/src/components/Config.vue
@@ -387,6 +387,18 @@ function removeVpnPortalClient(index: number) {
+
diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml
index e8f50722..351fb6b3 100644
--- a/easytier-web/frontend-lib/src/locales/cn.yaml
+++ b/easytier-web/frontend-lib/src/locales/cn.yaml
@@ -255,7 +255,11 @@ disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将
enable_magic_dns: 启用魔法DNS
enable_magic_dns_help: |
启用魔法DNS,允许通过EasyTier的DNS服务器访问其他节点的虚拟IPv4地址, 如 node1.et.net。
-
+tld_dns_zone: DNS域名后缀
+tld_dns_zone_help: |
+ 自定义魔法DNS的顶级域名(TLD)后缀。默认为 "et.net."。
+ 如需使用其他域名后缀,可改为 "et.internal." 或 "easytier.lan" 等。
+tld_dns_zone_placeholder: 例如 et.net.
enable_private_mode: 启用私有模式
enable_private_mode_help: |
启用私有模式,则不允许使用了与本网络不相同的网络名称和密码的节点通过本节点进行握手或中转。
diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml
index 82830b49..f37f5285 100644
--- a/easytier-web/frontend-lib/src/locales/en.yaml
+++ b/easytier-web/frontend-lib/src/locales/en.yaml
@@ -254,7 +254,11 @@ disable_sym_hole_punching_help: Disable special hole punching handling for symme
enable_magic_dns: Enable Magic DNS
enable_magic_dns_help: |
Enable magic dns, all nodes in the network can access each other by domain name, e.g.: node1.et.net.
-
+tld_dns_zone: DNS Zone TLD
+tld_dns_zone_help: |
+ Custom top-level domain (TLD) zone for Magic DNS hostnames. Default is "et.net.".
+ Change this if you need a different domain suffix, e.g. "et.internal." or "easytier.lan".
+tld_dns_zone_placeholder: e.g. et.net.
enable_private_mode: Enable Private Mode
enable_private_mode_help: |
Enable private mode, nodes with different network names or passwords from this network are not allowed to perform handshake or relay through this node.
diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts
index 812d51cb..cec8fadd 100644
--- a/easytier-web/frontend-lib/src/types/network.ts
+++ b/easytier-web/frontend-lib/src/types/network.ts
@@ -207,6 +207,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
mtu: null,
instance_recv_bps_limit: null,
mapped_listeners: [],
+ tld_dns_zone: 'et.net.',
port_forwards: [],
acl: emptyAcl(),
}
diff --git a/easytier-web/frontend-lib/tests/config-ui.spec.ts b/easytier-web/frontend-lib/tests/config-ui.spec.ts
index 7318af4f..d936c35d 100644
--- a/easytier-web/frontend-lib/tests/config-ui.spec.ts
+++ b/easytier-web/frontend-lib/tests/config-ui.spec.ts
@@ -416,6 +416,7 @@ describe('Config.vue network config projection', () => {
expect(input(wrapper, '#no_tun').checked).toBe(true)
expect(input(wrapper, '#hostname').value).toBe('host-a')
+ expect(input(wrapper, '#tld_dns_zone').value).toBe('et.net.')
expect(input(wrapper, '#subnet-proxy').value).toBe('10.10.0.0/16,172.16.1.0/24')
expect(input(wrapper, '#vpn_portal_wireguard_listen').value).toBe('0.0.0.0:22023')
expect(input(wrapper, '#dev_name').value).toBe('tun-test')
@@ -446,6 +447,7 @@ describe('Config.vue network config projection', () => {
await wrapper.find('#no_tun').setValue(false)
await wrapper.find('#disable_ipv6').setValue(false)
await setInput(wrapper, '#hostname', 'host-edited')
+ await setInput(wrapper, '#tld_dns_zone', 'nas.internal.')
await setInput(wrapper, '#subnet-proxy', '10.7.0.0/16,172.17.0.0/16')
await setInput(wrapper, '#vpn_portal_wireguard_listen', '[::]:23000')
await setInput(wrapper, 'input[data-add-label="add_listener_url"]', 'tcp://0.0.0.0:13010')
@@ -474,6 +476,7 @@ describe('Config.vue network config projection', () => {
no_tun: false,
disable_ipv6: false,
hostname: 'host-edited',
+ tld_dns_zone: 'nas.internal.',
proxy_cidrs: ['10.7.0.0/16', '172.17.0.0/16'],
vpn_portal_config: {
wireguard_listen: '[::]:23000',
@@ -509,6 +512,7 @@ describe('Config.vue network config projection', () => {
network_secret: 'secret-edited',
peer_urls: ['tcp://peer-x:11010', 'udp://peer-y:11010'],
listener_urls: ['tcp://0.0.0.0:13010'],
+ tld_dns_zone: 'nas.internal.',
mtu: 1260,
instance_recv_bps_limit: '9007199254740993',
vpn_portal_config: {
@@ -528,6 +532,9 @@ describe('Config.vue network config projection', () => {
dst_port: 9090,
}],
})
+
+ await setInput(wrapper, '#tld_dns_zone', '')
+ expect(toBackendNetworkConfig(curNetwork).tld_dns_zone).toBeUndefined()
})
it('shows a flag the configuration does not state as the value the core resolves', async () => {
diff --git a/easytier-web/src/client_manager/mod.rs b/easytier-web/src/client_manager/mod.rs
index 6c7c4c3e..2422ba75 100644
--- a/easytier-web/src/client_manager/mod.rs
+++ b/easytier-web/src/client_manager/mod.rs
@@ -1086,6 +1086,7 @@ mod tests {
assert_eq!(updated.disable_upnp, Some(true));
assert_eq!(updated.disable_relay_data, Some(true));
assert_eq!(updated.enable_magic_dns, Some(true));
+ assert_eq!(updated.tld_dns_zone.as_deref(), Some("nas.internal."));
assert_eq!(updated.enable_private_mode, Some(true));
assert_eq!(updated.mtu, Some(1360));
assert_eq!(
@@ -1112,6 +1113,7 @@ mod tests {
"relay_all_peer_rpc": false,
"multi_thread": false,
"disable_relay_data": false,
+ "tld_dns_zone": "initial.internal.",
"mtu": 1380
})
}
@@ -1162,6 +1164,7 @@ mod tests {
disable_upnp: Some(true),
disable_relay_data: Some(true),
enable_magic_dns: Some(true),
+ tld_dns_zone: Some("nas.internal.".to_owned()),
enable_private_mode: Some(true),
mtu: Some(1360),
data_compress_algo: Some(CompressionAlgoPb::Zstd as i32),
@@ -1651,10 +1654,11 @@ mod tests {
.await
.unwrap();
- wait_for_runtime_config(&core_manager, instance_id, |config| {
+ let initial = wait_for_runtime_config(&core_manager, instance_id, |config| {
config.network_name.as_deref() == Some("managed-initial")
})
.await;
+ assert_eq!(initial.tld_dns_zone.as_deref(), Some("initial.internal."));
wait_for_applied_revision(&mgr, user_id, machine_id, "rev-initial").await;
// Runtime-only mutations do not change SQLite. Invalidate the Session
@@ -1771,6 +1775,7 @@ mod tests {
Some("managed-redelivered")
);
assert_eq!(redelivered.enable_kcp_proxy, Some(true));
+ assert_eq!(redelivered.tld_dns_zone, None);
assert_eq!(redelivered.instance_recv_bps_limit, Some(654321));
assert_eq!(
core_manager.config_source(instance_id),
@@ -1806,6 +1811,7 @@ mod tests {
Some("managed-redelivered")
);
assert_eq!(replayed.enable_kcp_proxy, Some(true));
+ assert_eq!(replayed.tld_dns_zone, None);
assert_eq!(replayed.instance_recv_bps_limit, Some(654321));
webhook_server.abort();
diff --git a/easytier-web/src/client_manager/runtime_reconcile.rs b/easytier-web/src/client_manager/runtime_reconcile.rs
index 0873050f..fd1baeb1 100644
--- a/easytier-web/src/client_manager/runtime_reconcile.rs
+++ b/easytier-web/src/client_manager/runtime_reconcile.rs
@@ -730,6 +730,26 @@ mod tests {
.collect()
}
+ #[test]
+ fn magic_dns_zone_change_requires_recreate() {
+ let current = config_with_port_forwards(Vec::new());
+ let mut desired = current.clone();
+ desired.tld_dns_zone = Some("et.net.".to_owned());
+ let action =
+ prepare_web_source_runtime_reconcile_from_current(¤t, desired.clone(), false)
+ .unwrap();
+ assert!(matches!(action, RuntimeReconcileAction::Unchanged(_)));
+
+ desired.tld_dns_zone = Some("nas.internal.".to_owned());
+ let action =
+ prepare_web_source_runtime_reconcile_from_current(¤t, desired, false).unwrap();
+ let RuntimeReconcileAction::Run { config, overwrite } = action else {
+ panic!("changing the DNS zone must recreate the instance");
+ };
+ assert!(overwrite);
+ assert_eq!(config.tld_dns_zone.as_deref(), Some("nas.internal."));
+ }
+
#[test]
fn vpn_portal_client_changes_produce_hot_patches() {
let current = config_with_vpn_portal(