Compare commits

...
273 Commits
Author SHA1 Message Date
fanyang 09e30aeff6 fix(dns): adapt resolver chain to dns policy branch 2026-06-04 23:14:38 +08:00
fanyang e21046a5fa feat(dns): support configurable resolver chain 2026-06-04 23:14:38 +08:00
Luna Yao b207b8a7bb Merge branch 'main' into feat/dns-policy
# Conflicts:
#	Cargo.lock
#	easytier/build/main.rs
2026-05-15 14:56:13 +02:00
Luna Yao 5e8a79a747 Cargo.lock 2026-05-14 02:37:34 +02:00
Luna Yao 8485091614 add RepeatedSerialize & RepeatedDeserialize 2026-05-14 02:37:16 +02:00
Luna Yao e31fee1a6d merge
Cargo.lock
2026-05-14 02:37:16 +02:00
Luna Yao 743ec3c95e Merge branch 'main' into feat/dns-policy 2026-05-13 21:17:59 +02:00
Luna Yao 4c2859ae7c upgrade optionize to 0.2 2026-05-04 03:51:47 +02:00
Luna Yao c9e610a175 fmt 2026-05-04 02:06:50 +02:00
Luna Yao 22c458163a merge 2026-05-04 01:46:37 +02:00
Luna Yao ae83de91ab Merge branch 'main' into feat/dns-policy
# Conflicts:
#	easytier/build.rs
#	easytier/src/common/dns.rs
#	easytier/src/web_client/mod.rs
2026-05-04 01:44:54 +02:00
Luna Yao 348b83d768 use optionize 2026-05-04 01:33:57 +02:00
Luna Yao 4b95f44a91 replace derive_builder with bon
c'est meilleur

tauri
2026-05-01 21:53:17 +02:00
Luna Yao 96e2a6c775 config: use getset 2026-04-30 18:56:00 +02:00
Luna Yao 480677f085 node: increase heartbeat test wait time
heartbeat test
2026-04-30 18:56:00 +02:00
Luna Yao b7677031cb zone: fallthrough policy
heartbeat test

try_from
2026-04-30 18:56:00 +02:00
Luna Yao ee1f656d6b config: set name to optional 2026-04-29 12:25:18 +02:00
Luna Yao c866ed4da3 merge 2026-04-29 11:55:45 +02:00
Luna Yao 2fd8333ebb Merge branch 'main' into feat/dns-policy 2026-04-29 11:53:32 +02:00
Luna Yao 29ff4283d7 Cargo.lock 2026-04-29 11:44:43 +02:00
Luna Yao 8c0652a2e2 config: add disabled flag 2026-04-29 11:44:43 +02:00
Luna Yao 92af568d8b config: rewrite
test

test

origin

test

test
2026-04-29 11:44:22 +02:00
Luna Yao 0a23546e3f three_node 2026-04-28 21:54:05 +02:00
Luna Yao b85d5bc515 tests: ignore refresh error in wait_peer_zone_visibility
fmt
2026-04-28 21:23:17 +02:00
Luna Yao 30b88bf626 peer_mgr: invalidate unreachable peer
fmt
2026-04-28 21:23:16 +02:00
Luna Yao 486cc43059 node: remove serial_test
fmt
2026-04-28 21:23:16 +02:00
Luna Yao 46d1c2aef3 node & peer_mgr: fix rpc register/unregister
mut node
2026-04-28 21:23:16 +02:00
Luna Yao 8cf9d493b8 node: replace rr interval with heartbeat interval
interval
2026-04-28 21:23:16 +02:00
Luna Yao e527717eef node: add reconcile
logs

reconcile

reconcile
2026-04-28 21:23:16 +02:00
Luna Yao 80041086de peer_mgr(refresh): add backoff retry
fmt

refresh
2026-04-28 15:03:53 +02:00
Luna Yao 40d3e48cc4 remove redundant heartbeat log 2026-04-28 14:52:02 +02:00
Luna Yao 88297868c3 node_mgr: append system zone handler to all origin
system
2026-04-28 05:12:27 +02:00
Luna Yao d59257c8b7 zone_handler: skip negative response to forward to upstream
catalog_lookup_returns_nodata_on_nameexists

fmt
2026-04-28 05:12:27 +02:00
Luna Yao fb4479a99f unit tests 2026-04-28 05:12:27 +02:00
Luna Yao 185777b741 server: add addresses to nic and bind to them
fmt

add address

save bindings
2026-04-28 05:12:27 +02:00
Luna Yao 7d536d3353 remove address hijack 2026-04-28 05:12:27 +02:00
Luna Yao 87f2905360 server: use guarden to protect system dns config
server: guarden

server guarden
2026-04-28 05:12:27 +02:00
Luna Yao 1338ab343c node(election): use guard to protect nic_packet_process_pipeline
node: guarden
2026-04-28 04:52:51 +02:00
Luna Yao 38e7366c15 merge 2026-04-27 20:45:20 +02:00
Luna Yao 0f4003245b Merge branch 'main' into feat/dns-policy
# Conflicts:
#	easytier/src/common/config.rs
#	easytier/src/common/dns.rs
#	easytier/src/common/mod.rs
#	easytier/src/core.rs
#	easytier/src/instance/instance.rs
#	easytier/src/launcher.rs
#	easytier/src/peers/peer_ospf_route.rs
#	easytier/src/proto/peer_rpc.proto
#	easytier/src/utils/task.rs
2026-04-27 17:16:43 +02:00
Luna Yao 524c8d9987 move hostname function to utils 2026-04-20 00:51:59 +02:00
Luna Yao 11626d8082 remove id from zone 2026-04-19 02:17:50 +02:00
Luna Yao 362547eaab use localhost as default hostname 2026-04-19 01:48:08 +02:00
Luna Yao 5df7913043 plan: update 2026-04-19 01:38:30 +02:00
Luna Yao 9feb548bc2 remove fqdn from DnsConfig
l

l

plan: update

plan: update
2026-04-19 01:38:29 +02:00
Luna Yao cbb966d57c remove fqdn from DnsExportConfig 2026-04-19 00:34:57 +02:00
Luna Yao 6db0be00ad use sanitize in get_hostname
fmt

config
2026-04-19 00:34:57 +02:00
Luna Yao 185eb1e4c2 move sanitize and parse
fmt dns

dns utils

fmt

unit test

unit test
2026-04-19 00:34:55 +02:00
Luna Yao ae4e9b3513 rewrite resolver_conf, socket_addrs
comment

dns utils

res
2026-04-19 00:34:44 +02:00
Luna Yao 7ae7725fbf rewrite txt_lookup and srv_lookup
rewrite

rewrite

lookup

l
2026-04-19 00:34:41 +02:00
Luna Yao 5a516a195c move common::dns to utils::dns
move dns

move
2026-04-18 23:14:25 +02:00
Luna Yao c71ab7543f remove accept_dns 2026-04-18 21:26:30 +02:00
Luna Yao 4f6f8f3d93 use CancellableTask in DnsNode
remove AsyncRuntime from DnsNode

node

n

n

clippy
2026-04-18 21:26:30 +02:00
Luna Yao d5995ea1cb use CancellableTask in DnsServer
server
2026-04-18 17:18:36 +02:00
Luna Yao b0aae4f1fa replace AsyncRuntime with simpler CancellableTask
add docstring for AsyncRuntime

task

task
2026-04-18 17:18:36 +02:00
Luna Yao 4512e03d5f plan: update 2026-04-18 11:29:11 +02:00
Luna Yao 49e33f182b Merge branch 'main' into feat/dns-policy
# Conflicts:
#	easytier/Cargo.toml
2026-04-18 11:23:51 +02:00
Luna Yao bc86917dea add fallthrough flag
zone

test log
2026-04-18 04:18:31 +02:00
Luna Yao 9332baf6f9 rewrite NameServerAddr
fix Url to NameServerAddr conversion

fmt
2026-04-18 03:46:43 +02:00
Luna Yao 84984e1641 move dirty to crate::utils 2026-04-18 03:46:43 +02:00
Luna Yao 7345acfe7c upgrade hickory-dns to 0.26.0
fix zone test
2026-04-18 03:46:43 +02:00
Luna Yao 6001eef736 RoutePeerInfo 2026-04-17 19:11:31 +02:00
Luna Yao b070cff8e2 fmt 2026-04-17 18:59:11 +02:00
Luna Yao cbdfdfacc1 use cfg_select in dns::system::get 2026-04-17 18:55:13 +02:00
Luna Yao b94ab414a0 replace parking_lot with std 2026-04-17 18:50:12 +02:00
Luna Yao 3795800975 replace gethostname with hostname 2026-04-17 18:43:16 +02:00
Luna Yao ba7fc1098b merge 2026-04-17 18:26:21 +02:00
Luna Yao 38cb4a22fd Merge branch 'main' into feat/dns-policy 2026-04-17 17:46:20 +02:00
Luna Yao a60f101bda merge 2026-04-17 17:45:43 +02:00
Luna Yao 6b01554c17 Merge branch 'main' into feat/dns-policy
# Conflicts:
#	easytier/src/common/log.rs
#	easytier/src/connector/dns_connector.rs
#	easytier/src/dns/system/windows.rs
#	easytier/src/gateway/socks5.rs
#	easytier/src/instance/dns_server/server.rs
#	easytier/src/instance/dns_server/server_instance.rs
#	easytier/src/instance/instance.rs
#	easytier/src/peers/peer_manager.rs
#	easytier/src/peers/peer_ospf_route.rs
#	easytier/src/proto/utils.rs
#	easytier/src/tunnel/common.rs
#	easytier/src/utils/panic.rs
2026-04-17 17:16:55 +02:00
Luna Yao 5a8a1d3e6b disallow some methods from itertools 2026-04-06 18:28:30 +02:00
Luna Yao 6af62e939e constants
clippy
2026-04-06 17:38:17 +02:00
Luna Yao 012b35acd6 arrange tests
comment
2026-04-06 17:36:44 +02:00
Luna Yao 34dab9c739 node: comments 2026-04-06 17:04:30 +02:00
Luna Yao 09b26fd4fe zone: comments 2026-04-06 17:04:30 +02:00
Luna Yao 50ecd3679b peer_mgr: remove peer from cache when lost route 2026-04-06 17:04:30 +02:00
Luna Yao ddbfe76932 log: test log 2026-04-06 17:04:30 +02:00
Luna Yao 847e82ff8c config: filter out disabled export 2026-04-06 17:04:29 +02:00
Luna Yao 501c2bdb73 test: integration test
test: integration tests

test: integration
2026-04-06 17:04:29 +02:00
Luna Yao 8c5851e2ee server: unit tests 2026-04-06 16:19:25 +02:00
Luna Yao 7d47cc837f zone: unit tests 2026-04-06 16:19:25 +02:00
Luna Yao b422e603c8 node_mgr: unit tests 2026-04-06 16:19:25 +02:00
Luna Yao ee217c51f6 peer_mgr: unit tests 2026-04-06 16:19:25 +02:00
Luna Yao 4f180ab309 move new_request to tests 2026-04-06 11:55:25 +02:00
Luna Yao 248117c119 node: unit tests 2026-04-06 11:55:25 +02:00
Luna Yao 4dad60a94d replace ttl with tti 2026-04-06 11:55:25 +02:00
Luna Yao 67108d14b0 test: new_request 2026-04-06 11:55:25 +02:00
Luna Yao 9e8a4c94cb structured log 2026-04-06 11:55:25 +02:00
Luna Yao 8610234ee2 server: clean up on stop
server
2026-04-06 11:55:25 +02:00
Luna Yao b51b17e229 plan: update 2026-04-06 11:55:25 +02:00
Luna Yao f17ee9244c node: join_joinset_background
fmt

join
2026-04-06 11:55:25 +02:00
Luna Yao 0cd92ff215 zone: move iter_records to test 2026-04-06 11:55:25 +02:00
Luna Yao 4ed111dd04 zone: remove unused PartialEq 2026-04-06 11:55:25 +02:00
Luna Yao 2b3417d703 remove unneeded pub 2026-04-06 11:55:25 +02:00
Luna Yao b7edbad4b9 utils(authority): delegate 2026-04-06 11:55:25 +02:00
Luna Yao a6988e55f6 zone: some tests 2026-04-06 11:55:25 +02:00
Luna Yao 1562092afe build: remove redundant protoc arg 2026-04-06 11:55:25 +02:00
Luna Yao d5c68e356a utils(response): clear inner before writing 2026-04-06 11:55:25 +02:00
Luna Yao a9facedf57 peer_mgr: remove redundant * when comp Vec 2026-04-06 11:55:25 +02:00
Luna Yao faa252fc39 utils(dirty): replace Notify with watch 2026-04-06 11:55:25 +02:00
Luna Yao a031b6f701 server: rearrange method ordering 2026-04-06 11:55:25 +02:00
Luna Yao 41d7335bd8 server: save listeners after reloading 2026-04-06 11:55:25 +02:00
Luna Yao 2a2f85397c test(relay_peer_e2e_encryption): wait for key before ping test 2026-04-06 11:55:25 +02:00
Luna Yao 18dc9e4ca3 instance: drop dns in Drop 2026-04-06 11:55:25 +02:00
Luna Yao 39c595b5e9 instance: drop peer_mgr in time 2026-04-06 11:55:22 +02:00
Luna Yao 604386f7e2 peer_mgr: unregister rpc in Drop 2026-04-06 11:54:51 +02:00
Luna Yao 0d91aaf974 clippy 2026-04-06 11:54:51 +02:00
Luna Yao 1b220dd825 peer_mgr: add register() for rpc 2026-04-06 11:54:51 +02:00
Luna Yao e4f2078ff1 change heartbeat log level to trace 2026-04-06 11:54:50 +02:00
Luna Yao 62b512fbae stop dns in Drop of Instance 2026-04-06 11:54:50 +02:00
Luna Yao 5f8548d272 make dns Option 2026-04-06 11:54:50 +02:00
Luna Yao 9b18f820ad clippy 2026-04-06 11:54:50 +02:00
Luna Yao 56bd8491d1 fix tun feature gate
fmt
2026-04-06 11:54:50 +02:00
Luna Yao 7cb4345e4d fmt 2026-04-06 11:54:50 +02:00
Luna Yao ea5e34b6af Revert "remove hostname from config" 2026-04-06 11:54:50 +02:00
Luna Yao c8697e76b4 remove magic-dns proto
rm magic dns

rm magic dns
2026-04-06 11:54:50 +02:00
Luna Yao 385ff54b3e fix magic-dns feature gate 2026-04-06 11:54:50 +02:00
Luna Yao d94cb3a96d peer_mgr: ignore unsupported peer 2026-04-06 11:54:50 +02:00
Luna Yao b6afe6305a format 2026-04-06 11:54:50 +02:00
Luna Yao 7c895c401b fix: mobile, macos
macos
2026-04-06 11:54:50 +02:00
Luna Yao 8da5dc5f6e add readme
readme: intro

readme: part 1

readme: part 2
2026-04-06 11:54:50 +02:00
Luna Yao 561bbd2d62 node: remove DnsNodeRuntime 2026-04-06 11:54:50 +02:00
Luna Yao 23f63f1e4a log & clippy 2026-04-06 11:54:50 +02:00
Luna Yao cddf6abfa7 cargo: update indexmap 2026-04-06 11:54:50 +02:00
Luna Yao 0733a1029c system: update windows test 2026-04-06 11:54:50 +02:00
Luna Yao cb1e5ca273 utils: From<&NameServerConfig> for NameServerAddr 2026-04-06 11:54:50 +02:00
Luna Yao 7286a42d58 proxy_cidrs_monitor: remove get_route_peer_info_last_update_time check 2026-04-06 11:54:50 +02:00
Luna Yao 28f26a1750 node_mgr: simplify collect_zones 2026-04-06 11:54:50 +02:00
Luna Yao 96b9d89191 node_mgr: use into_groups and iter_authorities 2026-04-06 11:54:50 +02:00
Luna Yao 58c7828c85 authority: add ArcAuthority, add into_groups & iter_authorities 2026-04-06 11:54:50 +02:00
Luna Yao c9ce17d25e zone::tests: catalog test 2026-04-06 11:54:50 +02:00
Luna Yao 231b7d8300 node_mgr: fix catalog creation, step 1 2026-04-06 11:54:50 +02:00
Luna Yao ffa7ca3850 node_mgr: test 2026-04-06 11:54:50 +02:00
Luna Yao 5b4ec2f56b utils: move ResponseHandle 2026-04-06 11:54:50 +02:00
Luna Yao 723fc90691 utils: move ResponseHandle 2026-04-06 11:54:50 +02:00
Luna Yao dcd0c7cc2f utils: move ResponseHandle 2026-04-06 11:54:50 +02:00
Luna Yao 2b94df8320 node_mgr: zone ordering 2026-04-06 11:54:50 +02:00
Luna Yao 447e15f478 zone 2026-04-06 11:54:50 +02:00
Luna Yao d5d600a524 remove hostname from config 2026-04-06 11:54:50 +02:00
Luna Yao 56f430bf10 add ttl to ZoneData 2026-04-06 11:54:50 +02:00
Luna Yao 09ba9a1203 node_mgr: heartbeat log
node_mgr log
2026-04-06 11:54:50 +02:00
Luna Yao 4ff9e0f8d7 dns: tests mod 2026-04-06 11:54:50 +02:00
Luna Yao 5bc0c1b2ff zone: add empty check for forward config
zone
2026-04-06 11:54:50 +02:00
Luna Yao 3fb1da6c2e node & peer_mgr: refactor refresh logic, add my_peer_id check
node
2026-04-06 11:54:50 +02:00
Luna Yao 725cdf7d06 server: skip reload if unchanged 2026-04-06 11:54:50 +02:00
Luna Yao 484c264444 server: remove port of address for system config 2026-04-06 11:54:50 +02:00
Luna Yao fe43b5706f dirty: fix Default, remove unused DirtyState 2026-04-06 11:54:50 +02:00
Luna Yao 0ca5e38dba log: span
log: format
2026-04-06 11:54:50 +02:00
Luna Yao b0e4808d00 log: add test log init
log
2026-04-06 11:54:50 +02:00
Luna Yao 2a34032221 log: refactor 2026-04-06 11:54:50 +02:00
Luna Yao 15cc86c8bc log: use test writer 2026-04-06 11:54:50 +02:00
Luna Yao 0e5d2cbf08 node & server: add log
server & node: log

node & server: instrument
2026-04-06 11:54:50 +02:00
Luna Yao 8cb9856f09 server: add test 2026-04-06 11:54:50 +02:00
Luna Yao 97d534a8c0 utils: add state to AsyncRuntime
utils: format
2026-04-06 11:54:50 +02:00
Luna Yao 093cd7c197 server: add register method
node: format

node & server: fix rpc ownership
2026-04-06 11:54:50 +02:00
Luna Yao 6c47b0d86c plan: update 2026-04-06 11:54:50 +02:00
Luna Yao 4f3244636d instance: add dns 2026-04-06 11:54:50 +02:00
Luna Yao 2f04de9420 server: rename; hide ResponseHandle inner 2026-04-06 11:54:50 +02:00
Luna Yao e0173a52da server: use AsyncRuntime 2026-04-06 11:54:50 +02:00
Luna Yao 08a1640e30 server: lifecycle 2026-04-06 11:54:50 +02:00
Luna Yao 70fba64e17 node: lifecycle 2026-04-06 11:54:50 +02:00
Luna Yao 95e460dbf5 utils: add AsyncRuntime
utils: fix AsyncRuntime lock
2026-04-06 11:54:50 +02:00
Luna Yao a73a029c50 server: update system dns settings on address reload 2026-04-06 11:54:50 +02:00
Luna Yao d9d211c5a4 remove old magic-dns 2026-04-06 11:54:47 +02:00
Luna Yao 8559de1857 system: add get; rename 2026-04-06 11:54:02 +02:00
Luna Yao 18e28197b8 node: adjust field ordering 2026-04-06 11:54:02 +02:00
Luna Yao e01f2c22a8 plan: update 2026-04-06 11:54:02 +02:00
Luna Yao 918c9b9174 system: rename SystemConfig to SystemConfigurator, add Clone to SystemConfigurator 2026-04-06 11:54:02 +02:00
Luna Yao 4ed9cbc4a1 move system_config to dns::system 2026-04-06 11:54:02 +02:00
Luna Yao 202d8ec121 server: Drop 2026-04-06 11:54:02 +02:00
Luna Yao 833ed8d8b8 node: save global_ctx as a field 2026-04-06 11:54:02 +02:00
Luna Yao a44a98d7c4 plan: update 2026-04-06 11:54:02 +02:00
Luna Yao 86871b14d5 store DnsServer in GlobalCtx, load routes from DnsServer in ProxyCidrsMonitor 2026-04-06 11:54:02 +02:00
Luna Yao 3fc030de48 server: use parking_lot::RwLock for addresses 2026-04-06 11:54:02 +02:00
Luna Yao b5a33a192b node: use join in election 2026-04-06 11:54:02 +02:00
Luna Yao 9d1f134f9e plan: update 2026-04-06 11:54:02 +02:00
Luna Yao 57a2428f21 node: add server election
peer_mgr

server: add peer_mgr

peer_mgr
2026-04-06 11:54:02 +02:00
Luna Yao 238858ebab server: use select! for run 2026-04-06 11:54:02 +02:00
Luna Yao 0c8da66a74 node: add Notify for server election 2026-04-06 11:54:02 +02:00
Luna Yao 14155d706c server: cleanup resources in run and drop 2026-04-06 11:54:02 +02:00
Luna Yao c968fb6d06 server: impl Drop for DnsServerRuntime 2026-04-06 11:54:02 +02:00
Luna Yao 6618635622 server: impl NicPacketFilter 2026-04-06 11:54:02 +02:00
Luna Yao 30cfe7a74a server: use bind_socket 2026-04-06 11:54:02 +02:00
Luna Yao 13275aae99 move bind_tcp_socket and bind_udp_socket to common 2026-04-06 11:54:02 +02:00
Luna Yao 2e1f9bc9cc node & server: separate dirty flag, remove DirtyState 2026-04-06 11:54:02 +02:00
Luna Yao fcfb0ad6bc plan: update 2026-04-06 11:54:02 +02:00
Luna Yao e967434ef9 node: refresh on PeerInfoUpdated 2026-04-06 11:54:02 +02:00
Luna Yao f75c35d4a0 add GlobalCtxEvent::PeerInfoUpdated, issue this event in do_sync_route_info
fix use in peer_ospf_route.rs
2026-04-06 11:54:02 +02:00
Luna Yao 088628c1b6 plan: update 2026-04-06 11:54:02 +02:00
Luna Yao 9b372d3087 node: watch config/ip change 2026-04-06 11:54:02 +02:00
Luna Yao 5de87cd494 plan: update 2026-04-06 11:54:02 +02:00
Luna Yao f5d3443928 client: rename to node 2026-04-06 11:54:01 +02:00
Luna Yao 5372596432 client: replace spawn_local with spawn 2026-04-06 11:54:01 +02:00
Luna Yao 5457adb533 utils: move to a mod 2026-04-06 11:54:01 +02:00
Luna Yao 474f1a3636 client: remove DnsPeerMgrDirtyFlags 2026-04-06 11:54:01 +02:00
Luna Yao efcdf4c456 utils: add DirtyState 2026-04-06 11:54:01 +02:00
Luna Yao e9a0e4f042 server: add new 2026-04-06 11:54:01 +02:00
Luna Yao d54eb97201 server: rename zones dirty flag to catalog 2026-04-06 11:54:01 +02:00
Luna Yao dc1e1929c8 client_mgr: add
format

client_mgr
2026-04-06 11:54:01 +02:00
Luna Yao b7073504d8 peer_mgr: use flat_map 2026-04-06 11:54:01 +02:00
Luna Yao f83816ac16 server: remove comments 2026-04-06 11:54:01 +02:00
Luna Yao d3ca03e20e use notify in dirty states 2026-04-06 11:54:01 +02:00
Luna Yao 3d3dba28e8 utils: add DirtyFlag 2026-04-06 11:54:01 +02:00
Luna Yao e3ed91aced peer_mgr: format 2026-04-06 11:54:01 +02:00
Luna Yao 8f7fc6e07b server: remove Notify of dirty 2026-04-06 11:54:01 +02:00
Luna Yao 5e69a26a53 server: add DnsServerRuntime and event loop 2026-04-06 11:54:01 +02:00
Luna Yao 488dfaf233 client: fix atomic ordering 2026-04-06 11:54:01 +02:00
Luna Yao 4a18a86ed6 proto: rename DnsPeerMgrRpc 2026-04-06 11:54:01 +02:00
Luna Yao a8fa7be4ce server: use ArcGlobalCtx 2026-04-06 11:54:01 +02:00
Luna Yao b469a28e19 server: use HashSet in DnsClientInfo 2026-04-06 11:54:01 +02:00
Luna Yao a5fdebe778 chore: move constants to mod.rs, format 2026-04-06 11:54:01 +02:00
Luna Yao 87713e96b7 chore: move all config to a dedicated mod 2026-04-06 11:54:01 +02:00
Luna Yao f9108672e6 server: add DnsClientInfo 2026-04-06 11:54:01 +02:00
Luna Yao dbb2f86a25 zone: add PartialEq 2026-04-06 11:54:01 +02:00
Luna Yao 0fdfbc2636 utils: use itertools
utils
2026-04-06 11:54:01 +02:00
Luna Yao 2cd208fad4 peer_mgr: remove DnsSnapshot, store zones directly in DnsPeerInfo 2026-04-06 11:54:01 +02:00
Luna Yao e0897dcf99 proto: add update method to HeartbeatRequest 2026-04-06 11:54:01 +02:00
Luna Yao 13aaa437b2 config: use itertools 2026-04-06 11:54:01 +02:00
Luna Yao 6856649705 client: remove Heartbeat 2026-04-06 11:54:01 +02:00
Luna Yao 35b27b73a2 zone: make Zone a model of ZoneData
zone: rename ZoneData back to Zone
2026-04-06 11:54:01 +02:00
Luna Yao 97edee20f9 config: add a wrapper for ZoneConfig, use Derivative to impl Default for DnsConfig
zone: make Zone a model of ZoneData

zone: rename ZoneData back to Zone
2026-04-06 11:54:01 +02:00
Luna Yao 743d8e4810 client: remove Derivative from DnsClient 2026-04-06 11:54:01 +02:00
Luna Yao 129f696a65 proto: remove ttl from ZoneData 2026-04-06 11:54:01 +02:00
Luna Yao 90409bfcaa utils: use itertools 2026-04-06 11:54:01 +02:00
Luna Yao f53ced6281 utils: update MapTryInto to match behaviour of itertools 2026-04-06 11:54:01 +02:00
Luna Yao e4bf2ca959 utils: add NameServerConfigGroup conversion impls 2026-04-06 11:54:01 +02:00
Luna Yao fd39961567 peer_mgr: remove Derivative 2026-04-06 11:54:01 +02:00
Luna Yao 9502a83143 dns: change mod to pub 2026-04-06 11:54:01 +02:00
Luna Yao 822e742ee2 server: add 2026-04-06 11:54:01 +02:00
Luna Yao 85c06b0758 proto: rename ZoneConfigPb to ZoneData 2026-04-06 11:54:01 +02:00
Luna Yao 7041214321 client: add Heartbeat 2026-04-06 11:54:01 +02:00
Luna Yao 9caeeafbf9 config: use NameServerAddr for addresses 2026-04-06 11:54:01 +02:00
Luna Yao 9a2ba9cce8 peer_mgr: add DnsSnapshot Model 2026-04-06 11:54:01 +02:00
Luna Yao 2caf492551 zone: add system, remove add_forwarders 2026-04-06 11:54:01 +02:00
Luna Yao 4f3afc8ee8 utils: update NameServerAddr conversion 2026-04-06 11:54:01 +02:00
Luna Yao 863fc6f4cc utils: add MapTryInto 2026-04-06 11:54:01 +02:00
Luna Yao de84adb3a6 zone: add id to display 2026-04-06 11:54:01 +02:00
Luna Yao a9dc19ce76 proto: update Url conversion
url

url

fix url conversion
2026-04-06 11:54:01 +02:00
Luna Yao 8076734cd4 utils: add RepeatedMessageModel 2026-04-06 11:54:01 +02:00
Luna Yao 888bec6eac utils: move DeterministicDigest to utils 2026-04-06 11:54:01 +02:00
Luna Yao 7b1b651a46 peer_mgr: rename cache to peers, extract ttl as a const 2026-04-06 11:54:01 +02:00
Luna Yao 070332b9a7 plan: rename to ChainedAuthority 2026-04-06 11:54:01 +02:00
Luna Yao a6bec0fa6f zone: move chained authority to utils, rewrite authority creation 2026-04-06 11:54:01 +02:00
Luna Yao 77ccd7dbc9 zone: remove unused function 2026-04-06 11:54:01 +02:00
Luna Yao a6d030494c client: fix heartbeat initialization 2026-04-06 11:54:01 +02:00
Luna Yao 2ca8b5f8a2 server: add mod 2026-04-06 11:54:01 +02:00
Luna Yao 0e3474f049 peer_mgr: moved to a standalone file 2026-04-06 11:54:01 +02:00
Luna Yao 28c4c7b4a0 plan: remove delta, add DnsRunner, refactor heartbeat 2026-04-06 11:54:01 +02:00
Luna Yao 2e68a8db91 client: rewrite heartbeat, add resync 2026-04-06 11:54:01 +02:00
Luna Yao 7f8f036596 client: add DnsPeerManager and rpc 2026-04-06 11:54:01 +02:00
Luna Yao c4cf8cf1d8 rpc: let peers generate their own dedicated zones 2026-04-06 11:54:01 +02:00
Luna Yao cff0264caf chore: format 2026-04-06 11:54:01 +02:00
Luna Yao 2b5b80e179 client: framework 2026-04-06 11:54:01 +02:00
Luna Yao 9d48cefd05 config: let get_name return LowerName 2026-04-06 11:54:01 +02:00
Luna Yao 6836d61127 utils: add a parse function for DNS name 2026-04-06 11:54:01 +02:00
Luna Yao 75ad15c324 utils: add From<&NameServerAddr> for Url 2026-04-06 11:54:01 +02:00
Luna Yao 83bdcf0d2b rpc: add digest 2026-04-06 11:54:01 +02:00
Luna Yao ee3b7f2335 client: add mod 2026-04-06 11:54:01 +02:00
Luna Yao 7e16b7f69f plan: store only hash of DNS in RoutePeerInfo 2026-04-06 11:54:01 +02:00
Luna Yao 9376d3959b config: update test for policies 2026-04-06 11:54:01 +02:00
Luna Yao 78b2afb785 config: add DNS import/export policies
config: move ID generation to ZoneConfig

config: serde fixes
2026-04-06 11:54:01 +02:00
Luna Yao 8a93bb311b add FallbackAuthority and related test 2026-04-06 11:54:01 +02:00
Luna Yao b24bb42faa add dns to RoutePeerInfo 2026-04-06 11:54:01 +02:00
Luna Yao 2eaa5c215d add zone module with Zone struct and related functionality 2026-04-06 11:54:01 +02:00
Luna Yao 3f3aec8edd add DnsConfig and ZoneConfig with proto definitions 2026-04-06 11:54:01 +02:00
Luna Yao 7abdfd35e8 add dns mod 2026-04-06 11:54:01 +02:00
Luna Yao d3454febb7 plan: store only hash in RoutePeerInfo 2026-04-06 11:54:01 +02:00
Luna Yao 63dd133f0b plan: add alternative plan 2026-04-06 11:54:01 +02:00
Luna Yao 32f86e421a plan: remove delta update 2026-04-06 11:54:01 +02:00
Luna Yao 1c4303f5b7 plan: simplify RPC 2026-04-06 11:54:01 +02:00
Luna Yao 07376eb690 plan: add zone-wise acl control
plan: fix config example
2026-04-06 11:54:01 +02:00
Luna Yao 0b6ff1ecd7 plan: replace timestamp by id & seq 2026-04-06 11:54:01 +02:00
Luna Yao 46b53c601e add dev plan 2026-04-06 11:54:01 +02:00
69 changed files with 7307 additions and 2504 deletions
Generated
+232 -139
View File
@@ -915,7 +915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
dependencies = [
"once_cell",
"proc-macro-crate 3.2.0",
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -1981,37 +1981,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling 0.20.10",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.117",
]
[[package]]
name = "derive_more"
version = "0.99.20"
@@ -2262,28 +2231,29 @@ dependencies = [
"defguard_wireguard_rs",
"delegate",
"derivative",
"derive_builder",
"derive_more 2.1.1",
"encoding",
"flume 0.12.0",
"forwarded-header-value",
"futures",
"futures-util",
"gethostname 0.5.0",
"getset",
"git-version",
"globwalk",
"guarden",
"hickory-client",
"hickory-net",
"hickory-proto",
"hickory-resolver",
"hickory-server",
"hmac",
"hostname",
"http",
"http_req",
"humansize",
"humantime-serde",
"idna 1.0.3",
"igd-next",
"indexmap 2.14.0",
"indoc",
"itertools 0.14.0",
"kcp-sys",
@@ -2301,6 +2271,7 @@ dependencies = [
"nix 0.29.0",
"once_cell",
"openssl",
"optionize",
"ordered_hash_map",
"parking_lot",
"paste",
@@ -2308,7 +2279,7 @@ dependencies = [
"petgraph 0.8.1",
"pin-project-lite",
"pnet",
"prefix-trie",
"prefix-trie 0.7.0",
"proc-macro2",
"prost",
"prost-build",
@@ -2332,6 +2303,7 @@ dependencies = [
"rustls",
"serde",
"serde_json",
"serde_with",
"serial_test",
"service-manager",
"sha2",
@@ -2385,7 +2357,7 @@ version = "0.1.0"
dependencies = [
"android_logger",
"easytier",
"jni",
"jni 0.21.1",
"log",
"once_cell",
"serde",
@@ -2414,7 +2386,7 @@ dependencies = [
"dashmap",
"dunce",
"easytier",
"gethostname 1.1.0",
"gethostname",
"libc",
"once_cell",
"security-framework-sys",
@@ -2701,24 +2673,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "enumflags2"
version = "0.7.12"
@@ -3300,16 +3254,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "gethostname"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
dependencies = [
"rustix 0.38.34",
"windows-targets 0.52.6",
]
[[package]]
name = "gethostname"
version = "1.1.0"
@@ -3372,6 +3316,18 @@ dependencies = [
"wasip3",
]
[[package]]
name = "getset"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "ghash"
version = "0.5.1"
@@ -3735,88 +3691,103 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hickory-client"
version = "0.25.2"
name = "hickory-net"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c466cd63a4217d5b2b8e32f23f58312741ce96e3c84bf7438677d2baff0fc555"
dependencies = [
"cfg-if",
"data-encoding",
"futures-channel",
"futures-util",
"hickory-proto",
"once_cell",
"radix_trie",
"rand 0.9.1",
"thiserror 2.0.11",
"tokio",
"tracing",
]
[[package]]
name = "hickory-proto"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
dependencies = [
"async-trait",
"bytes",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"h2",
"hickory-proto",
"http",
"idna 1.0.3",
"ipnet",
"once_cell",
"rand 0.9.1",
"ring",
"jni 0.22.4",
"rand 0.10.1",
"rustls",
"serde",
"thiserror 2.0.11",
"tinyvec",
"tokio",
"tokio-rustls",
"tracing",
"url",
"webpki-roots 1.0.6",
]
[[package]]
name = "hickory-proto"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
dependencies = [
"data-encoding",
"idna 1.0.3",
"ipnet",
"jni 0.22.4",
"once_cell",
"prefix-trie 0.8.4",
"rand 0.10.1",
"ring",
"serde",
"thiserror 2.0.11",
"tinyvec",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.25.2"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
dependencies = [
"cfg-if",
"futures-util",
"hickory-net",
"hickory-proto",
"ipconfig",
"ipnet",
"jni 0.22.4",
"moka",
"ndk-context",
"once_cell",
"parking_lot",
"rand 0.9.1",
"rand 0.10.1",
"resolv-conf",
"rustls",
"serde",
"smallvec",
"system-configuration 0.7.0",
"thiserror 2.0.11",
"tokio",
"tokio-rustls",
"tracing",
"webpki-roots 1.0.6",
]
[[package]]
name = "hickory-server"
version = "0.25.2"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53e5fe811b941c74ee46b8818228bfd2bc2688ba276a0eaeb0f2c95ea3b2585"
checksum = "130236ba6abba90da6a7acf7a87b27d862b592c3145dc74bc47bf86d8ff198ec"
dependencies = [
"async-trait",
"bytes",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-util",
"hickory-net",
"hickory-proto",
"hickory-resolver",
"ipnet",
"prefix-trie",
"prefix-trie 0.8.4",
"serde",
"thiserror 2.0.11",
"time",
@@ -3852,6 +3823,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "hostname"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
dependencies = [
"cfg-if",
"libc",
"windows-link 0.2.1",
]
[[package]]
name = "html5ever"
version = "0.29.1"
@@ -4037,7 +4019,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.6.3",
"tokio",
"tower-service",
"tracing",
@@ -4549,19 +4531,68 @@ dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"jni-sys 0.3.0",
"log",
"thiserror 1.0.63",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.11",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.117",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn 2.0.117",
]
[[package]]
name = "jobserver"
version = "0.1.32"
@@ -5203,7 +5234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 2.8.0",
"jni-sys",
"jni-sys 0.3.0",
"log",
"ndk-sys",
"num_enum",
@@ -5223,7 +5254,7 @@ version = "0.6.0+11769913"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
dependencies = [
"jni-sys",
"jni-sys 0.3.0",
]
[[package]]
@@ -5239,7 +5270,7 @@ dependencies = [
"netlink-packet-route 0.17.1",
"netlink-sys",
"once_cell",
"system-configuration",
"system-configuration 0.6.1",
"windows-sys 0.52.0",
]
@@ -5350,15 +5381,6 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nibble_vec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.25.1"
@@ -5577,7 +5599,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro-crate 2.0.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -5928,6 +5950,30 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "optionize"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f15b218321873a7919c8b2a188ad488bf18c067478274be4c69c3d402b2be3ea"
dependencies = [
"delegate",
"derive_more 2.1.1",
"optionize-macros",
]
[[package]]
name = "optionize-macros"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1af5a2aba73a1808cbb39b451aabb5f8dc5c9c73d68ce5e61b79f8892f775a0f"
dependencies = [
"darling 0.23.0",
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "ordered-float"
version = "2.10.1"
@@ -6657,6 +6703,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "prefix-trie"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
dependencies = [
"either",
"ipnet",
"num-traits",
]
[[package]]
name = "prettyplease"
version = "0.2.20"
@@ -6697,11 +6754,11 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit 0.22.20",
"toml_edit 0.25.12+spec-1.1.0",
]
[[package]]
@@ -7054,16 +7111,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]]
name = "rand"
version = "0.7.3"
@@ -7390,7 +7437,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"system-configuration 0.6.1",
"tokio",
"tokio-native-tls",
"tokio-rustls",
@@ -7565,7 +7612,7 @@ checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate 3.2.0",
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"regex",
@@ -7728,6 +7775,7 @@ version = "0.23.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
@@ -7774,7 +7822,7 @@ checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
dependencies = [
"core-foundation 0.10.0",
"core-foundation-sys",
"jni",
"jni 0.21.1",
"log",
"once_cell",
"rustls",
@@ -8573,6 +8621,16 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
@@ -9132,6 +9190,17 @@ dependencies = [
"system-configuration-sys",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags 2.8.0",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
@@ -9201,7 +9270,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
"jni",
"jni 0.21.1",
"libc",
"log",
"ndk",
@@ -9263,7 +9332,7 @@ dependencies = [
"heck 0.5.0",
"http",
"image 0.25.10",
"jni",
"jni 0.21.1",
"libc",
"log",
"mime",
@@ -9398,7 +9467,7 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997"
dependencies = [
"gethostname 1.1.0",
"gethostname",
"log",
"os_info",
"serde",
@@ -9491,7 +9560,7 @@ dependencies = [
"dpi",
"gtk",
"http",
"jni",
"jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@@ -9514,7 +9583,7 @@ checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e"
dependencies = [
"gtk",
"http",
"jni",
"jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@@ -9965,6 +10034,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.19.15"
@@ -10002,6 +10080,18 @@ dependencies = [
"winnow 0.6.18",
]
[[package]]
name = "toml_edit"
version = "0.25.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
dependencies = [
"indexmap 2.14.0",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"winnow 1.0.1",
]
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
@@ -11849,6 +11939,9 @@ name = "winnow"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
@@ -12050,7 +12143,7 @@ dependencies = [
"html5ever",
"http",
"javascriptcore-rs",
"jni",
"jni 0.21.1",
"kuchikiki",
"libc",
"ndk",
@@ -12113,7 +12206,7 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname 1.1.0",
"gethostname",
"rustix 1.0.7",
"x11rb-protocol",
]
@@ -12230,7 +12323,7 @@ version = "5.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -12467,7 +12560,7 @@ version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
+3 -6
View File
@@ -15,9 +15,7 @@ use easytier::rpc_service::remote_client::{
use easytier::web_client::{self, WebClient};
use easytier::{
common::{
config::{
ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader,
},
config::{ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfig, TomlConfigLoader},
log,
},
instance_manager::NetworkInstanceManager,
@@ -1326,7 +1324,7 @@ pub fn run_gui() -> std::process::ExitCode {
let Ok(log_dir) = get_log_dir(app.app_handle()) else {
return Ok(());
};
let config = LoggingConfigBuilder::default()
let config = LoggingConfig::builder()
.file_logger(FileLoggerConfig {
dir: Some(log_dir.to_string_lossy().to_string()),
level: None,
@@ -1334,8 +1332,7 @@ pub fn run_gui() -> std::process::ExitCode {
size_mb: None,
count: None,
})
.build()
.map_err(|e| e.to_string())?;
.build();
let Ok(_) = log::init(&config, true) else {
return Ok(());
};
+14 -11
View File
@@ -51,6 +51,9 @@ time = "0.3"
toml = "0.8.12"
chrono = { version = "0.4.37", features = ["serde"] }
getset = "0.1.6"
optionize = "0.2"
guarden = "0.1"
delegate = "0.13.5"
@@ -59,7 +62,7 @@ itertools = "0.14.0"
strum = { version = "0.27.2", features = ["derive"] }
gethostname = "0.5.0"
hostname = "0.4.2"
futures = { version = "0.3", features = ["bilock", "unstable"] }
@@ -70,10 +73,12 @@ tokio-util = { version = "0.7.9", features = ["codec", "net", "io", "rt"] }
async-stream = "0.3.5"
async-trait = "0.1.74"
maplit = "1.0.2"
dashmap = "6.0"
moka = { version = "0.12", features = ["future"] }
timedmap = "=1.0.1"
moka = { version = "0.12", features = ["future"] }
# for full-path zero-copy
zerocopy = { version = "0.7.32", features = ["derive", "simd"] }
bytes = "1.5.0"
@@ -152,6 +157,7 @@ rand = "0.8.5"
serde = { version = "1.0", features = ["derive"] }
pnet = { version = "0.35.0", features = ["serde"] }
serde_json = "1"
serde_with = "3"
clap = { version = "4.5.30", features = [
"string",
@@ -170,6 +176,7 @@ network-interface = "2.0"
# for ospf route
petgraph = "0.8.1"
ordered_hash_map = "0.5.0"
indexmap = "2.13.1"
# for wireguard
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
@@ -236,17 +243,14 @@ http_req = { git = "https://github.com/EasyTier/http_req.git", default-features
] }
# for dns connector
hickory-resolver = "0.25.2"
hickory-proto = "0.25.2"
hickory-proto = "0.26.0"
hickory-net = { version = "0.26.0", features = ["serde"] }
hickory-resolver = { version = "0.26.0", features = ["https-ring", "webpki-roots"] }
# for magic dns
hickory-client = { version = "0.25.2", optional = true }
hickory-server = { version = "0.25.2", features = [
"resolver",
], optional = true }
hickory-server = { version = "0.26.0", features = ["resolver"], optional = true }
bon = "3.9.1"
derive_builder = "0.20.2"
humantime-serde = "1.1.1"
multimap = "0.10.1"
version-compare = "0.2.0"
@@ -338,7 +342,6 @@ zip = "4.0.0"
serial_test = "3.0.0"
rstest = "0.25.0"
futures-util = "0.3.31"
maplit = "1.0.2"
tempfile = "3.22.0"
ctor = "0.8.0"
@@ -400,7 +403,7 @@ jemalloc-prof = [
"jemalloc-sys/stats",
]
tracing = ["tokio/tracing", "dep:console-subscriber"]
magic-dns = ["dep:hickory-client", "dep:hickory-server"]
magic-dns = ["dep:hickory-server"]
faketcp = ["dep:flume"]
zstd = ["dep:zstd"]
# For Network Extension on macOS
+21 -16
View File
@@ -166,7 +166,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"src/proto/api_config.proto",
"src/proto/api_manage.proto",
"src/proto/web.proto",
"src/proto/magic_dns.proto",
"src/proto/dns.proto",
"src/proto/acl.proto",
];
@@ -174,17 +174,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed={proto_file}");
}
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
let descriptor_file = out.join("descriptors.bin");
let mut config = prost_build::Config::new();
config
.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]")
.extern_path(".google.protobuf.Any", "::prost_wkt_types::Any")
.extern_path(".google.protobuf.Timestamp", "::prost_wkt_types::Timestamp")
.extern_path(".google.protobuf.Value", "::prost_wkt_types::Value")
.file_descriptor_set_path(&descriptor_file)
.protoc_arg("--experimental_allow_proto3_optional")
.extern_path(".google.protobuf.Value", "::prost_wkt_types::Value");
config
.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]")
.type_attribute("peer_rpc.DirectConnectedPeerInfo", "#[derive(Hash)]")
.type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]")
.type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]")
@@ -198,20 +195,28 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.type_attribute("acl.AclV1", "#[serde(default)]")
.type_attribute("acl.Chain", "#[serde(default)]")
.type_attribute("acl.Rule", "#[serde(default)]")
.type_attribute("acl.GroupInfo", "#[serde(default)]")
.field_attribute(".api.manage.NetworkConfig", "#[serde(default)]")
.service_generator(Box::new(ServiceGenerator::default()))
.btree_map(["."])
.skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
.type_attribute("acl.GroupInfo", "#[serde(default)]");
config.compile_protos(&proto_files, &["src/proto/"])?;
config.field_attribute("api.manage.NetworkConfig", "#[serde(default)]");
config.skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
let out = PathBuf::from(env::var("OUT_DIR")?);
let descriptor_file = out.join("descriptors.bin");
config
.btree_map(["."])
.service_generator(Box::new(ServiceGenerator::default()))
.protoc_arg("--experimental_allow_proto3_optional")
.file_descriptor_set_path(&descriptor_file)
.compile_protos(&proto_files, &["src/proto/"])?;
prost_reflect_build::Builder::new()
.file_descriptor_set_bytes("crate::proto::DESCRIPTOR_POOL_BYTES")
.compile_protos_with_config(config, &proto_files_reflect, &["src/proto/"])?;
let descriptor_bytes = std::fs::read(descriptor_file).unwrap();
let descriptor = FileDescriptorSet::decode(&descriptor_bytes[..]).unwrap();
let descriptor_bytes = std::fs::read(descriptor_file)?;
let descriptor = FileDescriptorSet::decode(&descriptor_bytes[..])?;
prost_wkt_build::add_serde(out, descriptor);
check_locale();
-6
View File
@@ -220,12 +220,6 @@ core_clap:
port_forward:
en: "forward local port to remote port in virtual network. e.g.: udp://0.0.0.0:12345/10.126.126.1:23456, means forward local udp port 12345 to 10.126.126.1:23456 in the virtual network. can specify multiple."
zh-CN: "将本地端口转发到虚拟网络中的远程端口。例如:udp://0.0.0.0:12345/10.126.126.1:23456,表示将本地UDP端口12345转发到虚拟网络中的10.126.126.1:23456。可以指定多个。"
accept_dns:
en: "if true, enable magic dns. with magic dns, you can access other nodes with a domain name, e.g.: <hostname>.et.net. magic dns will modify your system dns settings, enable it carefully."
zh-CN: "如果为true,则启用魔法DNS。使用魔法DNS,您可以使用域名访问其他节点,例如:<hostname>.et.net。魔法DNS将修改您的系统DNS设置,请谨慎启用。"
tld_dns_zone:
en: "specify the top-level domain zone for magic DNS. if not provided, defaults to the value from dns_server module (et.net.). only used when accept_dns is true."
zh-CN: "指定魔法DNS的顶级域名区域。如果未提供,默认使用dns_server模块中的值(et.net.)。仅在accept_dns为true时使用。"
private_mode:
en: "if true, foreign networks are only allowed when this node can verify they use the same network secret, or when a foreign credential node is already trusted via admin-issued credential propagation; different or missing secrets are otherwise rejected."
zh-CN: "如果为true,则仅允许两类 foreign network 接入:本节点能验证其使用相同 network secret 的节点,或已通过 foreign network 管理节点传播而被信任的 credential 节点;否则 secret 不同或缺失时会被拒绝。"
+218 -45
View File
@@ -1,36 +1,114 @@
use std::{
hash::Hasher,
net::{IpAddr, SocketAddr},
path::PathBuf,
sync::{Arc, Mutex},
};
use anyhow::Context;
use base64::{Engine as _, prelude::BASE64_STANDARD};
use clap::ValueEnum;
use clap::builder::PossibleValue;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, VariantArray};
use tokio::io::AsyncReadExt as _;
use super::env_parser;
use crate::utils::dns;
use crate::{
common::stun::StunInfoCollector,
instance::dns_server::DEFAULT_ET_DNS_ZONE,
proto::{
acl::Acl,
api::manage::ConfigSource as RpcConfigSource,
common::{CompressionAlgoPb, PortForwardConfigPb, SecureModeConfig, SocketType},
},
tunnel::{IpScheme, TunnelScheme, generate_digest_from_str},
utils,
};
use anyhow::Context;
use base64::{Engine as _, prelude::BASE64_STANDARD};
use bon::Builder;
use clap::ValueEnum;
use clap::builder::PossibleValue;
use derivative::Derivative;
use derive_more::{Constructor, Deref};
use getset::Getters;
use optionize::Optionized;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::{
hash::Hasher,
net::{IpAddr, SocketAddr},
path::PathBuf,
sync::{Arc, Mutex},
};
use strum::{Display, EnumString, VariantArray};
use tokio::io::AsyncReadExt as _;
use super::env_parser;
#[derive(Derivative, Debug, Clone, Constructor, Getters, Deref, Deserialize)]
#[derivative(PartialEq(bound = "Parsed: PartialEq"))]
#[serde(try_from = "Raw")]
#[serde(
bound = "Raw: Deserialize<'de>, <ConfigBase<Raw, Parsed, Data> as TryFrom<Raw>>::Error: Display"
)]
pub struct ConfigBase<Raw, Parsed, Data = ()>
where
Raw: Optionized<Subject = Parsed>,
ConfigBase<Raw, Parsed, Data>: TryFrom<Raw>,
{
#[deref]
parsed: Parsed,
#[getset(get)]
#[derivative(PartialEq = "ignore")]
raw: Raw,
#[getset(get)]
#[derivative(PartialEq = "ignore")]
data: Data,
}
impl<Raw, Parsed, Data> Serialize for ConfigBase<Raw, Parsed, Data>
where
Raw: Optionized<Subject = Parsed> + Serialize,
ConfigBase<Raw, Parsed, Data>: TryFrom<Raw, Error: Debug>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.raw.serialize(serializer)
}
}
impl<Raw, Parsed, Data> Default for ConfigBase<Raw, Parsed, Data>
where
Raw: Optionized<Subject = Parsed> + Default,
ConfigBase<Raw, Parsed, Data>: TryFrom<Raw, Error: Debug>,
{
fn default() -> Self {
Raw::default().try_into().unwrap()
}
}
impl<Raw, Parsed, Data> ConfigBase<Raw, Parsed, Data>
where
Raw: Optionized<Subject = Parsed>,
ConfigBase<Raw, Parsed, Data>: TryFrom<Raw, Error: Debug>,
{
pub fn into_parsed(self) -> Parsed {
self.parsed
}
pub fn into_raw(self) -> Raw {
self.raw
}
pub fn into_data(self) -> Data {
self.data
}
pub fn update(self, config: Raw) -> Result<Self, <Self as TryFrom<Raw>>::Error> {
let mut raw = self.into_raw();
raw.merge(config);
raw.try_into()
}
}
pub type Flags = crate::proto::common::FlagsInConfig;
pub fn gen_default_flags() -> Flags {
#[allow(deprecated)]
Flags {
#[allow(deprecated)]
quic_listen_port: u32::MAX,
#[allow(deprecated)]
accept_dns: false,
#[allow(deprecated)]
tld_dns_zone: "".to_string(),
default_protocol: "tcp".to_string(),
dev_name: "".to_string(),
enable_encryption: true,
@@ -55,7 +133,6 @@ pub fn gen_default_flags() -> Flags {
disable_kcp_input: false,
disable_relay_kcp: false,
enable_relay_foreign_network_kcp: false,
accept_dns: false,
private_mode: false,
enable_quic_proxy: false,
disable_quic_input: false,
@@ -65,9 +142,6 @@ pub fn gen_default_flags() -> Flags {
multi_thread_count: 2,
encryption_algorithm: EncryptionAlgorithm::default().to_string(),
disable_sym_hole_punching: false,
tld_dns_zone: DEFAULT_ET_DNS_ZONE.to_string(),
quic_listen_port: u32::MAX,
need_p2p: false,
instance_recv_bps_limit: u64::MAX,
disable_upnp: false,
@@ -152,8 +226,19 @@ impl Default for EncryptionAlgorithm {
}
}
cfg_select! {
feature = "magic-dns" => {
use crate::dns::config::{DnsConfig, DnsConfigLoaderExt};
}
_ => {
#[auto_impl::auto_impl(Box, &)]
pub trait DnsConfigLoaderExt {}
}
}
#[auto_impl::auto_impl(Box, &)]
pub trait ConfigLoader: Send + Sync {
pub trait ConfigLoader: Send + Sync + DnsConfigLoaderExt {
fn get_id(&self) -> uuid::Uuid;
fn set_id(&self, id: uuid::Uuid);
@@ -240,6 +325,14 @@ pub trait ConfigLoader: Send + Sync {
fn get_stun_servers_v6(&self) -> Option<Vec<String>>;
fn set_stun_servers_v6(&self, servers: Option<Vec<String>>);
fn get_dns_resolvers(&self) -> Vec<String> {
dns::get_default_dns_resolvers()
}
fn get_dns_resolvers_config(&self) -> Option<Vec<String>> {
None
}
fn set_dns_resolvers(&self, _resolvers: Option<Vec<String>>) {}
fn get_secure_mode(&self) -> Option<SecureModeConfig>;
fn set_secure_mode(&self, secure_mode: Option<SecureModeConfig>);
@@ -427,11 +520,11 @@ pub struct ConsoleLoggerConfig {
pub level: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, derive_builder::Builder)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Builder)]
pub struct LoggingConfig {
#[builder(setter(into, strip_option), default = None)]
#[builder(into)]
pub file_logger: Option<FileLoggerConfig>,
#[builder(setter(into, strip_option), default = None)]
#[builder(into)]
pub console_logger: Option<ConsoleLoggerConfig>,
}
@@ -542,6 +635,10 @@ struct Config {
peer: Option<Vec<PeerConfig>>,
proxy_network: Option<Vec<ProxyNetworkConfig>>,
#[cfg(feature = "magic-dns")]
#[serde(default)]
dns: DnsConfig,
vpn_portal_config: Option<VpnPortalConfig>,
routes: Option<Vec<cidr::Ipv4Cidr>>,
@@ -563,6 +660,7 @@ struct Config {
udp_whitelist: Option<Vec<String>>,
stun_servers: Option<Vec<String>>,
stun_servers_v6: Option<Vec<String>>,
dns_resolvers: Option<Vec<String>>,
credential_file: Option<PathBuf>,
source: Option<ConfigSourceConfig>,
@@ -596,6 +694,10 @@ impl TomlConfigLoader {
Self::normalize_config_source(&mut config);
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
if let Some(dns_resolvers) = &config.dns_resolvers {
dns::validate_dns_resolvers(dns_resolvers)
.with_context(|| "invalid dns_resolvers config")?;
}
let config = TomlConfigLoader {
config: Arc::new(Mutex::new(config)),
@@ -637,6 +739,21 @@ impl TomlConfigLoader {
}
}
impl DnsConfigLoaderExt for TomlConfigLoader {
cfg_select! {
feature = "magic-dns" => {
fn get_dns(&self) -> DnsConfig {
self.config.lock().unwrap().dns.clone()
}
fn set_dns(&self, config: DnsConfig) {
self.config.lock().unwrap().dns = config;
}
}
_ => {}
}
}
impl ConfigLoader for TomlConfigLoader {
fn get_inst_name(&self) -> String {
self.config
@@ -652,26 +769,17 @@ impl ConfigLoader for TomlConfigLoader {
}
fn get_hostname(&self) -> String {
let hostname = self.config.lock().unwrap().hostname.clone();
let hostname = self
.config
.lock()
.unwrap()
.hostname
.as_ref()
.map(|hostname| dns::sanitize(hostname))
.filter(|h| !h.is_empty());
match hostname {
Some(hostname) => {
let hostname = hostname
.chars()
.filter(|c| !c.is_control())
.take(32)
.collect::<String>();
if !hostname.is_empty() {
self.set_hostname(Some(hostname.clone()));
hostname
} else {
self.set_hostname(None);
gethostname::gethostname().to_string_lossy().to_string()
}
}
None => gethostname::gethostname().to_string_lossy().to_string(),
}
self.set_hostname(hostname.clone());
hostname.unwrap_or_else(|| utils::dns::sanitize(utils::hostname()))
}
fn set_hostname(&self, name: Option<String>) {
@@ -999,6 +1107,23 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().stun_servers_v6 = servers;
}
fn get_dns_resolvers(&self) -> Vec<String> {
self.config
.lock()
.unwrap()
.dns_resolvers
.clone()
.unwrap_or_else(dns::get_default_dns_resolvers)
}
fn get_dns_resolvers_config(&self) -> Option<Vec<String>> {
self.config.lock().unwrap().dns_resolvers.clone()
}
fn set_dns_resolvers(&self, resolvers: Option<Vec<String>>) {
self.config.lock().unwrap().dns_resolvers = resolvers;
}
fn get_secure_mode(&self) -> Option<SecureModeConfig> {
self.config.lock().unwrap().secure_mode.clone()
}
@@ -1061,6 +1186,9 @@ impl ConfigLoader for TomlConfigLoader {
if config.stun_servers_v6 == Some(StunInfoCollector::get_default_servers_v6()) {
config.stun_servers_v6 = None;
}
if config.dns_resolvers == Some(dns::get_default_dns_resolvers()) {
config.dns_resolvers = None;
}
toml::to_string_pretty(&config).unwrap()
}
}
@@ -1292,6 +1420,51 @@ stun_servers = [
assert_eq!(stun_servers[2], "txt:stun.easytier.cn");
}
#[test]
fn test_dns_resolvers_default_and_roundtrip() {
let config = TomlConfigLoader::default();
assert_eq!(config.get_dns_resolvers_config(), None);
assert_eq!(config.get_dns_resolvers(), vec!["system".to_string()]);
assert!(!config.dump().contains("dns_resolvers"));
let config = TomlConfigLoader::new_from_str(
r#"
dns_resolvers = ["system", "https://dns.alidns.com/dns-query"]
"#,
)
.unwrap();
assert_eq!(
config.get_dns_resolvers_config().unwrap(),
vec![
"system".to_string(),
"https://dns.alidns.com/dns-query".to_string()
]
);
assert_eq!(
config.get_dns_resolvers(),
vec![
"system".to_string(),
"https://dns.alidns.com/dns-query".to_string()
]
);
let dumped = config.dump();
assert!(dumped.contains("dns_resolvers"));
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
assert_eq!(loaded.get_dns_resolvers(), config.get_dns_resolvers());
}
#[test]
fn test_dns_resolvers_reject_unknown_doh_without_bootstrap() {
let err = TomlConfigLoader::new_from_str(
r#"
dns_resolvers = ["https://example.com/dns-query"]
"#,
)
.unwrap_err();
assert!(err.to_string().contains("invalid dns_resolvers"));
}
#[test]
fn test_network_config_source_toml_roundtrip() {
let config = TomlConfigLoader::default();
-151
View File
@@ -1,151 +0,0 @@
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Context;
use hickory_proto::runtime::TokioRuntimeProvider;
use hickory_proto::xfer::Protocol;
use hickory_resolver::config::{LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts};
use hickory_resolver::name_server::{GenericConnector, TokioConnectionProvider};
use hickory_resolver::system_conf::read_system_conf;
use hickory_resolver::{Resolver, TokioResolver};
use once_cell::sync::Lazy;
use tokio::net::lookup_host;
use super::error::Error;
pub fn get_default_resolver_config() -> ResolverConfig {
let mut default_resolve_config = ResolverConfig::new();
default_resolve_config.add_name_server(NameServerConfig::new(
"223.5.5.5:53".parse().unwrap(),
Protocol::Udp,
));
default_resolve_config.add_name_server(NameServerConfig::new(
"180.184.1.1:53".parse().unwrap(),
Protocol::Udp,
));
default_resolve_config
}
pub static ALLOW_USE_SYSTEM_DNS_RESOLVER: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(true));
pub static RESOLVER: Lazy<Arc<Resolver<GenericConnector<TokioRuntimeProvider>>>> =
Lazy::new(|| {
let system_cfg = read_system_conf();
let mut cfg = get_default_resolver_config();
let mut opt = ResolverOpts::default();
if let Ok(s) = system_cfg {
for ns in s.0.name_servers() {
cfg.add_name_server(ns.clone());
}
opt = s.1;
}
opt.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
let builder = TokioResolver::builder_with_config(cfg, TokioConnectionProvider::default())
.with_options(opt);
Arc::new(builder.build())
});
pub async fn resolve_txt_record(domain_name: &str) -> Result<String, Error> {
let r = RESOLVER.clone();
let response = r
.txt_lookup(domain_name)
.await
.with_context(|| format!("txt_lookup failed, domain_name: {}", domain_name))?;
let txt_record = response
.iter()
.next()
.with_context(|| format!("no txt record found, domain_name: {}", domain_name))?;
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
tracing::info!(?txt_data, ?domain_name, "get txt record");
Ok(txt_data.to_string())
}
pub async fn socket_addrs(
url: &url::Url,
default_port_number: impl Fn() -> Option<u16>,
) -> Result<Vec<SocketAddr>, Error> {
let host = url.host().ok_or(Error::InvalidUrl(url.to_string()))?;
let port = url
.port()
.or_else(default_port_number)
.ok_or(Error::InvalidUrl(url.to_string()))?;
// if host is an ip address, return it directly
match host {
url::Host::Ipv4(ip) => return Ok(vec![SocketAddr::new(std::net::IpAddr::V4(ip), port)]),
url::Host::Ipv6(ip) => return Ok(vec![SocketAddr::new(std::net::IpAddr::V6(ip), port)]),
_ => {}
}
let host = host.to_string();
if ALLOW_USE_SYSTEM_DNS_RESOLVER.load(std::sync::atomic::Ordering::Relaxed) {
let socket_addr = format!("{}:{}", host, port);
match lookup_host(socket_addr).await {
Ok(a) => {
let a = a.collect();
tracing::debug!(?a, "system dns lookup done");
return Ok(a);
}
Err(e) => {
tracing::error!(?e, "system dns lookup failed");
}
}
}
// use hickory_resolver
let ret = RESOLVER.lookup_ip(&host).await.with_context(|| {
format!(
"hickory dns lookup_ip failed, host: {}, port: {}",
host, port
)
})?;
Ok(ret
.iter()
.map(|ip| SocketAddr::new(ip, port))
.collect::<Vec<_>>())
}
#[cfg(test)]
mod tests {
use super::*;
use guarden::defer;
#[tokio::test]
async fn test_socket_addrs() {
let url = url::Url::parse("tcp://github-ci-test.easytier.cn:80").unwrap();
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
println!("addrs: {:?}", addrs);
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(false, std::sync::atomic::Ordering::Relaxed);
defer!(
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(true, std::sync::atomic::Ordering::Relaxed);
);
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
println!("addrs2: {:?}", addrs);
}
#[tokio::test]
async fn socket_addrs_preserves_explicit_zero_port() {
let cases = [
("ws://127.0.0.1:0", 80, 0),
("wss://127.0.0.1:0", 443, 0),
("ws://127.0.0.1", 80, 80),
("wss://127.0.0.1", 443, 443),
];
for (raw_url, default_port, expected_port) in cases {
let url = url::Url::parse(raw_url).unwrap();
let addrs = socket_addrs(&url, || Some(default_port)).await.unwrap();
assert_eq!(
addrs,
vec![SocketAddr::from(([127, 0, 0, 1], expected_port))]
);
}
}
}
+55 -9
View File
@@ -1,14 +1,18 @@
use arc_swap::ArcSwap;
use crossbeam::atomic::AtomicCell;
use dashmap::DashMap;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use socket2::Protocol;
use std::{
collections::{BTreeSet, HashMap, hash_map::DefaultHasher},
hash::Hasher,
iter,
net::{IpAddr, SocketAddr},
sync::{Arc, Mutex},
time::{SystemTime, UNIX_EPOCH},
};
use arc_swap::ArcSwap;
use dashmap::DashMap;
use super::{
PeerId,
config::{ConfigLoader, Flags},
@@ -31,10 +35,11 @@ use crate::{
rpc_service::protected_port,
tunnel::matches_protocol,
};
use crossbeam::atomic::AtomicCell;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use socket2::Protocol;
#[cfg(feature = "magic-dns")]
use crate::{
dns::config::{DnsConfigLoaderExt, DnsExportConfig, DnsGlobalCtxExt, zone::ZoneConfig},
utils::dns,
};
pub type NetworkIdentity = crate::common::config::NetworkIdentity;
@@ -48,6 +53,8 @@ pub enum GlobalCtxEvent {
PeerConnAdded(PeerConnInfo),
PeerConnRemoved(PeerConnInfo),
PeerInfoUpdated(Vec<PeerId>),
ListenerAdded(url::Url),
ListenerAddFailed(url::Url, String), // (url, error message)
ListenerAcceptFailed(url::Url, String), // (url, error message)
@@ -255,7 +262,7 @@ impl std::fmt::Debug for GlobalCtx {
}
}
pub type ArcGlobalCtx = std::sync::Arc<GlobalCtx>;
pub type ArcGlobalCtx = Arc<GlobalCtx>;
impl GlobalCtx {
fn apply_disable_relay_data_flag(
@@ -287,6 +294,12 @@ impl GlobalCtx {
let (event_bus, _) = tokio::sync::broadcast::channel(16);
if let Some(dns_resolvers) = config_fs.get_dns_resolvers_config()
&& let Err(e) = crate::utils::dns::set_dns_resolvers(dns_resolvers)
{
crate::common::log::warn!("failed to set dns resolvers: {:?}", e);
}
let stun_info_collector = StunInfoCollector::new_with_default_servers();
if let Some(stun_servers) = config_fs.get_stun_servers() {
@@ -491,7 +504,7 @@ impl GlobalCtx {
}
pub fn get_hostname(&self) -> String {
return self.hostname.lock().unwrap().clone();
self.hostname.lock().unwrap().clone()
}
pub fn set_hostname(&self, hostname: String) {
@@ -786,6 +799,39 @@ impl GlobalCtx {
}
}
#[cfg(feature = "magic-dns")]
impl DnsGlobalCtxExt for GlobalCtx {
fn dns_self_zone(&self) -> ZoneConfig {
use hickory_proto::rr::Name;
let dns = self.config.get_dns();
let name: Name = dns
.name
.clone()
.unwrap_or_else(|| dns::parse(self.get_hostname()))
.into();
let fqdn = name.append_domain(&dns.domain).unwrap_or_default().into();
let ipv4 = self.get_ipv4().map(|ip| ip.address());
let ipv6 = self.get_ipv6().map(|ip| ip.address());
let ipv6 = ipv6.map(|a| vec![a]).unwrap_or_default();
ZoneConfig::dedicated(fqdn, ipv4, ipv6)
}
fn dns_export_config(&self) -> DnsExportConfig {
DnsExportConfig {
zones: self
.dns_iter_zones()
.filter(|z| z.policy.export.as_ref().is_some_and(|f| !f.disabled)) // TODO: check policies of parent zones
.map(ZoneConfig::into_data)
.collect(),
}
}
fn dns_iter_zones(&self) -> impl Iterator<Item = ZoneConfig> {
iter::once(self.dns_self_zone()).chain(self.config.get_dns().into_parsed().zones)
}
}
#[cfg(test)]
pub mod tests {
use crate::{
+2 -4
View File
@@ -6,6 +6,7 @@ use std::{
time::{Duration, Instant},
};
use crate::utils::hostname;
use anyhow::Context as _;
#[cfg(unix)]
use nix::{
@@ -233,10 +234,7 @@ fn machine_uid_seed() -> Option<String> {
fn linux_machine_id_seed(machine_uid: &str) -> String {
let mut seed = format!("machine_uid={machine_uid}");
let hostname = gethostname::gethostname()
.to_string_lossy()
.trim()
.to_string();
let hostname = hostname();
if !hostname.is_empty() {
seed.push_str("\nhostname=");
seed.push_str(&hostname);
-1
View File
@@ -11,7 +11,6 @@ pub mod acl_processor;
pub mod compressor;
pub mod config;
pub mod constants;
pub mod dns;
pub mod env_parser;
pub mod error;
pub mod global_ctx;
+79 -27
View File
@@ -11,7 +11,7 @@ use crossbeam::atomic::AtomicCell;
use rand::seq::IteratorRandom;
use socket2::{SockAddr, SockRef};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UdpSocket, lookup_host};
use tokio::net::UdpSocket;
use tokio::sync::{Mutex, broadcast};
use tokio::task::JoinSet;
use tracing::{Instrument, Level};
@@ -20,10 +20,9 @@ use bytecodec::{DecodeExt, EncodeExt};
use stun_codec::rfc5389::methods::BINDING;
use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder};
use crate::common::error::Error;
use super::dns::resolve_txt_record;
use super::stun_codec_ext::*;
use crate::common::error::Error;
use crate::utils::dns::{resolve_host, txt_resolve};
const DEFAULT_UDP_STUN_SERVERS: &[&str] = &[
"txt:stun.easytier.cn",
@@ -61,9 +60,16 @@ impl HostResolverIter {
}
}
async fn get_txt_record(domain_name: &str) -> Result<Vec<String>, Error> {
let txt_data = resolve_txt_record(domain_name).await?;
Ok(txt_data.split(" ").map(|x| x.to_string()).collect())
fn parse_ipv6_socket_addr_without_brackets(host: &str) -> Option<SocketAddr> {
if host.parse::<IpAddr>().is_ok() {
return None;
}
let (ip, port) = host.rsplit_once(':')?;
Some(SocketAddr::new(
IpAddr::V6(ip.parse().ok()?),
port.parse().ok()?,
))
}
#[async_recursion::async_recursion]
@@ -74,15 +80,10 @@ impl HostResolverIter {
}
let host = self.hostnames.remove(0);
let host = if host.contains(':') {
host
} else {
format!("{}:3478", host)
};
if host.starts_with("txt:") {
let domain_name = host.trim_start_matches("txt:");
match Self::get_txt_record(domain_name).await {
match txt_resolve(domain_name).await {
Ok(hosts) => {
tracing::info!(
?domain_name,
@@ -104,22 +105,53 @@ impl HostResolverIter {
}
let use_ipv6 = self.use_ipv6;
match lookup_host(&host).await {
Ok(ips) => {
self.ips = ips
.filter(|x| if use_ipv6 { x.is_ipv6() } else { x.is_ipv4() })
.choose_multiple(&mut rand::thread_rng(), self.max_ip_per_domain as usize);
if self.ips.is_empty() {
return self.next().await;
}
if let Ok(addr) = host.parse::<SocketAddr>() {
if (use_ipv6 && addr.is_ipv6()) || (!use_ipv6 && addr.is_ipv4()) {
self.ips = vec![addr];
}
Err(e) => {
tracing::warn!(?host, ?e, "lookup host for stun failed");
if self.ips.is_empty() {
return self.next().await;
}
};
} else if let Some(addr) = Self::parse_ipv6_socket_addr_without_brackets(&host) {
if use_ipv6 {
self.ips = vec![addr];
}
if self.ips.is_empty() {
return self.next().await;
}
} else {
let (host, port) = if let Ok(ip) = host.parse::<IpAddr>() {
(ip.to_string(), 3478)
} else if let Ok(url) = url::Url::parse(&format!("stun://{}", host)) {
let Some(parsed_host) = url.host_str() else {
tracing::warn!(?host, "parse stun host failed");
return self.next().await;
};
(parsed_host.to_string(), url.port().unwrap_or(3478))
} else {
(host, 3478)
};
match resolve_host(&host, port).await {
Ok(ips) => {
self.ips = ips
.into_iter()
.filter(|x| if use_ipv6 { x.is_ipv6() } else { x.is_ipv4() })
.choose_multiple(
&mut rand::thread_rng(),
self.max_ip_per_domain as usize,
);
if self.ips.is_empty() {
return self.next().await;
}
}
Err(e) => {
tracing::warn!(?host, ?e, "resolve host for stun failed");
return self.next().await;
}
};
}
}
Some(self.ips.remove(0))
@@ -1349,6 +1381,26 @@ mod tests {
use super::*;
#[test]
fn parse_ipv6_socket_addr_without_brackets_rejects_plain_ipv6_literals() {
assert_eq!(
HostResolverIter::parse_ipv6_socket_addr_without_brackets("2001:db8::1"),
None
);
assert_eq!(
HostResolverIter::parse_ipv6_socket_addr_without_brackets("2001:db8:0:0:0:0:0:1"),
None
);
}
#[test]
fn parse_ipv6_socket_addr_without_brackets_accepts_unambiguous_port() {
assert_eq!(
HostResolverIter::parse_ipv6_socket_addr_without_brackets("::1:55355"),
Some("[::1]:55355".parse().unwrap())
);
}
#[tokio::test]
async fn test_udp_nat_type_detector() {
let collector = StunInfoCollector::new(
@@ -1563,6 +1615,6 @@ mod tests {
});
let stun_servers = vec!["::1:55355".to_string()];
let ret = StunInfoCollector::get_public_ipv6(&stun_servers).await;
println!("{:#?}", ret);
assert_eq!(ret, Some(Ipv6Addr::LOCALHOST));
}
}
+2 -4
View File
@@ -12,10 +12,7 @@ use std::{
};
use crate::{
common::{
PeerId, dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx,
stun::StunInfoCollectorTrait,
},
common::{PeerId, error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait},
connector::udp_hole_punch::handle_rpc_result,
peers::{
peer_conn::PeerConnId,
@@ -40,6 +37,7 @@ use super::{
udp_hole_punch,
};
use crate::tunnel::{FromUrl, IpScheme, TunnelScheme, matches_scheme};
use crate::utils::dns::socket_addrs;
use anyhow::Context;
use rand::Rng;
use socket2::Protocol;
+14 -27
View File
@@ -1,19 +1,15 @@
use std::{net::SocketAddr, sync::Arc};
use super::{create_connector_by_url, http_connector::TunnelWithInfo};
use crate::utils::dns::{srv_lookup, txt_resolve};
use crate::{
common::{
dns::{RESOLVER, resolve_txt_record},
error::Error,
global_ctx::ArcGlobalCtx,
log,
},
common::{error::Error, global_ctx::ArcGlobalCtx, log},
proto::common::TunnelInfo,
tunnel::{IpScheme, IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelScheme},
};
use anyhow::Context;
use dashmap::DashSet;
use hickory_resolver::proto::rr::rdata::SRV;
use hickory_proto::rr::rdata::SRV;
use rand::{Rng as _, seq::SliceRandom};
use strum::VariantArray;
@@ -58,14 +54,13 @@ impl DnsTunnelConnector {
&self,
domain_name: &str,
) -> Result<Box<dyn TunnelConnector>, Error> {
let txt_data = resolve_txt_record(domain_name)
let txt_data = txt_resolve(domain_name)
.await
.with_context(|| format!("resolve txt record failed, domain_name: {}", domain_name))?;
let candidate_urls = txt_data
.split(" ")
.map(|s| s.to_string())
.filter_map(|s| url::Url::parse(s.as_str()).ok())
.iter()
.filter_map(|s| url::Url::parse(s).ok())
.collect::<Vec<_>>();
// shuffle candidate_urls and get the first one
@@ -73,7 +68,7 @@ impl DnsTunnelConnector {
.choose(&mut rand::thread_rng())
.with_context(|| {
format!(
"no valid url found, txt_data: {}, expecting an url list splitted by space",
"no valid url found, txt_data: {:?}, expecting an url list split by space",
txt_data
)
})?;
@@ -83,26 +78,23 @@ impl DnsTunnelConnector {
Ok(connector)
}
fn handle_one_srv_record(record: &SRV, protocol: IpScheme) -> Result<(url::Url, u64), Error> {
fn handle_one_srv_record(record: SRV, protocol: IpScheme) -> Result<(url::Url, u64), Error> {
// port must be non-zero
if record.port() == 0 {
if record.port == 0 {
return Err(anyhow::anyhow!("port must be non-zero").into());
}
let connector_dst = record.target().to_utf8();
let dst_url = format!("{}://{}:{}", protocol, connector_dst, record.port());
let connector_dst = record.target.to_utf8();
let dst_url = format!("{}://{}:{}", protocol, connector_dst, record.port);
Ok((
dst_url.parse().with_context(|| {
format!(
"parse dst_url failed, protocol: {}, connector_dst: {}, port: {}, dst_url: {}",
protocol,
connector_dst,
record.port(),
dst_url
protocol, connector_dst, record.port, dst_url
)
})?,
record.priority() as _,
record.priority as _,
))
}
@@ -122,14 +114,9 @@ impl DnsTunnelConnector {
let srv_lookup_tasks = srv_domains
.iter()
.map(|(protocol, srv_domain)| {
let resolver = RESOLVER.clone();
let responses = responses.clone();
async move {
let response = resolver.srv_lookup(srv_domain).await.with_context(|| {
format!("srv_lookup failed, srv_domain: {}", srv_domain)
})?;
tracing::info!(?response, ?srv_domain, "srv_lookup response");
for record in response.iter() {
for record in srv_lookup(srv_domain).await? {
let parsed_record = Self::handle_one_srv_record(record, **protocol);
tracing::info!(?parsed_record, ?srv_domain, "parsed_record");
if let Err(e) = &parsed_record {
+3 -3
View File
@@ -9,7 +9,7 @@ use dashmap::DashSet;
use tokio::{sync::mpsc, task::JoinSet, time::timeout};
use crate::{
common::{PeerId, dns::socket_addrs, join_joinset_background},
common::{PeerId, join_joinset_background},
peers::peer_conn::PeerConnId,
proto::{
api::instance::{
@@ -22,6 +22,8 @@ use crate::{
utils::weak_upgrade,
};
use super::create_connector_by_url;
use crate::utils::dns::socket_addrs;
use crate::{
common::{
error::Error,
@@ -32,8 +34,6 @@ use crate::{
use_global_var,
};
use super::create_connector_by_url;
type ConnectorMap = Arc<DashSet<url::Url>>;
#[derive(Debug, Clone)]
+2 -2
View File
@@ -1,14 +1,14 @@
use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
use crate::{
common::{dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx, idn},
common::{error::Error, global_ctx::ArcGlobalCtx, idn},
connector::dns_connector::DnsTunnelConnector,
proto::common::PeerFeatureFlag,
tunnel::{
self, IpScheme, IpVersion, TunnelConnector, TunnelError, TunnelScheme,
ring::RingTunnelConnector, tcp::TcpTunnelConnector, udp::UdpTunnelConnector,
},
utils::BoxExt,
utils::{BoxExt, dns::socket_addrs},
};
use http_connector::HttpTunnelConnector;
use rand::seq::SliceRandom;
-18
View File
@@ -578,19 +578,6 @@ struct NetworkOptions {
)]
port_forward: Vec<url::Url>,
#[arg(
long,
env = "ET_ACCEPT_DNS",
help = t!("core_clap.accept_dns").to_string(),
)]
accept_dns: Option<bool>,
#[arg(
long = "tld-dns-zone",
env = "ET_TLD_DNS_ZONE",
help = t!("core_clap.tld_dns_zone").to_string())]
tld_dns_zone: Option<String>,
#[arg(
long,
env = "ET_PRIVATE_MODE",
@@ -1130,7 +1117,6 @@ impl NetworkOptions {
f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input);
f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy);
f.disable_quic_input = self.disable_quic_input.unwrap_or(f.disable_quic_input);
f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns);
f.private_mode = self.private_mode.unwrap_or(f.private_mode);
f.foreign_relay_bps_limit = self
.foreign_relay_bps_limit
@@ -1154,10 +1140,6 @@ impl NetworkOptions {
f.enable_udp_broadcast_relay = self
.enable_udp_broadcast_relay
.unwrap_or(f.enable_udp_broadcast_relay);
// Configure tld_dns_zone: use provided value if set
if let Some(tld_dns_zone) = &self.tld_dns_zone {
f.tld_dns_zone = tld_dns_zone.clone();
}
cfg.set_flags(f);
if !self.exit_nodes.is_empty() {
+55
View File
@@ -0,0 +1,55 @@
use crate::common::config::ConfigBase;
use crate::dns::config::policy::DnsPolicyConfig;
use crate::dns::config::zone::ZoneConfig;
use crate::dns::config::{DNS_DEFAULT_ADDRESSES, DNS_DEFAULT_DOMAIN};
use crate::dns::utils::addr::NameServerAddrGroup;
use crate::proto::dns::GetExportConfigResponse;
use hickory_proto::rr::LowerName;
use optionize::{Optionizable, optionized};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[optionized]
#[optionize(name = "DnsConfigRaw")]
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct DnsConfigParsed {
pub disabled: bool,
#[serde(rename = "zone")]
pub zones: Vec<ZoneConfig>,
#[optionize(flatten)]
#[serde(flatten)]
pub policies: HashMap<LowerName, DnsPolicyConfig>,
#[optionize(flatten)]
pub name: Option<LowerName>,
pub domain: LowerName,
pub addresses: NameServerAddrGroup,
pub listeners: NameServerAddrGroup,
}
pub type DnsConfig = ConfigBase<DnsConfigRaw, DnsConfigParsed, ()>;
impl From<DnsConfigRaw> for DnsConfig {
fn from(raw: DnsConfigRaw) -> Self {
let mut parsed = DnsConfigParsed {
domain: DNS_DEFAULT_DOMAIN.clone(),
addresses: DNS_DEFAULT_ADDRESSES.clone(),
..Default::default()
};
parsed.load(raw.clone());
Self::new(parsed, raw, ())
}
}
#[auto_impl::auto_impl(Box, &)]
pub trait DnsConfigLoaderExt {
fn get_dns(&self) -> DnsConfig;
fn set_dns(&self, dns: DnsConfig);
}
pub type DnsExportConfig = GetExportConfigResponse;
pub trait DnsGlobalCtxExt {
fn dns_self_zone(&self) -> ZoneConfig;
fn dns_export_config(&self) -> DnsExportConfig;
fn dns_iter_zones(&self) -> impl Iterator<Item = ZoneConfig>;
}
+29
View File
@@ -0,0 +1,29 @@
use crate::dns::utils::addr::NameServerAddrGroup;
use hickory_proto::rr::LowerName;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::LazyLock;
use std::time::Duration;
use url::Url;
mod dns;
pub use dns::*;
mod policy;
pub mod zone;
pub static DNS_DEFAULT_DOMAIN: LazyLock<LowerName> =
LazyLock::new(|| LowerName::from_str("et.net.").unwrap());
pub static DNS_DEFAULT_ADDRESSES: LazyLock<NameServerAddrGroup> =
LazyLock::new(|| IpAddr::from_str("100.100.100.101").unwrap().into());
pub static DNS_SERVER_RPC_ADDR: LazyLock<Url> =
LazyLock::new(|| Url::parse("tcp://127.0.0.1:49813").unwrap());
pub const DNS_NODE_TTI: Duration = Duration::from_secs(5);
pub const DNS_NODE_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(2);
pub const DNS_NODE_RECONCILE_INTERVAL: Duration = Duration::from_secs(10);
pub const DNS_SERVER_ELECTION_INTERVAL: Duration = Duration::from_secs(5);
pub const DNS_PEER_TTI: Duration = Duration::from_secs(3);
pub const DNS_PEER_REFRESH_ATTEMPTS: usize = 3;
pub const DNS_PEER_REFRESH_BACKOFF: Duration = Duration::from_secs(1);
+46
View File
@@ -0,0 +1,46 @@
use derive_more::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct AclPolicy {
pub whitelist: Option<Vec<String>>,
pub blacklist: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, Deref, DerefMut)]
#[serde(default)]
pub struct FunctionalityPolicy {
#[serde(flatten)]
#[deref]
#[deref_mut]
acl: AclPolicy, // TODO
pub disabled: bool,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, Deref, DerefMut)]
#[serde(default)]
pub struct DnsPolicy<P = FunctionalityPolicy> {
#[serde(flatten)]
#[deref]
#[deref_mut]
policy: P,
pub recursive: bool, // TODO
}
pub type ZoneExportPolicy = FunctionalityPolicy;
pub type DnsExportPolicy = DnsPolicy<ZoneExportPolicy>;
pub type DnsImportPolicy = DnsPolicy<FunctionalityPolicy>;
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct DnsPolicyConfig {
pub import: DnsImportPolicy,
pub export: Option<DnsExportPolicy>,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct ZonePolicyConfig {
pub export: Option<DnsExportPolicy>,
}
+111
View File
@@ -0,0 +1,111 @@
use crate::common::config::ConfigBase;
use crate::dns::config::policy::{DnsExportPolicy, ZonePolicyConfig};
use crate::dns::utils::addr::NameServerAddrGroup;
use crate::dns::zone::Zone;
use crate::proto::dns::ZoneData;
use derive_more::From;
use hickory_proto::op::ResponseCode;
use hickory_proto::rr::LowerName;
use maplit::hashset;
use optionize::{Optionizable, optionized};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::convert::TryFrom;
use std::net::{Ipv4Addr, Ipv6Addr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, From, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Fallthrough {
Any,
ResponseCode(ResponseCode),
}
impl From<Fallthrough> for i32 {
fn from(value: Fallthrough) -> Self {
match value {
Fallthrough::ResponseCode(code) => u16::from(code).into(),
Fallthrough::Any => -1,
}
}
}
impl From<i32> for Fallthrough {
fn from(value: i32) -> Self {
match u16::try_from(value) {
Ok(value) => Self::ResponseCode(value.into()),
Err(_) => Self::Any,
}
}
}
#[optionized]
#[optionize(name = "ZoneConfigRaw")]
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ZoneConfigParsed {
#[optionize(flatten)]
pub origin: LowerName,
pub ttl: u32,
pub records: Vec<String>,
pub forwarders: NameServerAddrGroup,
#[optionize(flatten)]
#[serde(flatten)]
pub policy: ZonePolicyConfig,
pub fallthrough: HashSet<Fallthrough>,
}
impl From<&ZoneConfigParsed> for ZoneData {
fn from(value: &ZoneConfigParsed) -> Self {
Self::new(
&value.origin,
value.ttl,
&value.records,
value.forwarders.iter().map(Into::into),
value.fallthrough.iter().copied(),
)
}
}
pub type ZoneConfig = ConfigBase<ZoneConfigRaw, ZoneConfigParsed, ZoneData>;
impl TryFrom<ZoneConfigRaw> for ZoneConfig {
type Error = anyhow::Error;
fn try_from(raw: ZoneConfigRaw) -> Result<Self, Self::Error> {
let mut parsed = ZoneConfigParsed {
fallthrough: hashset! {Fallthrough::Any},
..Default::default()
};
parsed.load(raw.clone());
let data = (&parsed).into();
let _ = Zone::try_from(&data)?; // validation
Ok(Self::new(parsed, raw, data))
}
}
impl ZoneConfig {
pub fn dedicated(origin: LowerName, ipv4: Option<Ipv4Addr>, ipv6: Vec<Ipv6Addr>) -> Self {
let mut records = Vec::new();
if let Some(ipv4) = ipv4 {
records.push(format!("@ IN A {}", ipv4));
}
for ipv6 in ipv6 {
records.push(format!("@ IN AAAA {}", ipv6));
}
let policy = ZonePolicyConfig {
export: Some(DnsExportPolicy::default()),
};
let parsed = ZoneConfigParsed {
origin,
records,
policy,
..Default::default()
};
let data = (&parsed).into();
Self::new(parsed, Default::default(), data)
}
}
+11
View File
@@ -0,0 +1,11 @@
pub mod config;
pub mod node;
mod node_mgr;
mod peer_mgr;
pub mod server;
mod system;
mod utils;
mod zone;
#[cfg(test)]
mod tests;
+560
View File
@@ -0,0 +1,560 @@
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent};
use crate::dns::config::{
DNS_NODE_HEARTBEAT_INTERVAL, DNS_NODE_RECONCILE_INTERVAL, DNS_PEER_REFRESH_ATTEMPTS,
DNS_PEER_REFRESH_BACKOFF, DNS_SERVER_ELECTION_INTERVAL, DNS_SERVER_RPC_ADDR,
};
use crate::dns::peer_mgr::DnsPeerMgr;
use crate::dns::server::DnsServer;
#[cfg(feature = "tun")]
use crate::instance::instance::ArcNicCtx;
use crate::peers::peer_manager::PeerManager;
use crate::proto::dns::{DnsNodeMgrRpcClientFactory, HeartbeatRequest};
use crate::proto::rpc_impl::standalone::{StandAloneClient, StandAloneServer};
use crate::proto::rpc_types::controller::BaseController;
use crate::tunnel::tcp::{TcpTunnelConnector, TcpTunnelListener};
use crate::utils::task::CancellableTask;
use std::io;
use std::sync::Arc;
use tokio::sync::{Notify, broadcast};
use tokio::task::JoinSet;
use tokio::time::{MissedTickBehavior, interval};
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use uuid::Uuid;
#[derive(Debug, Clone)]
struct DnsNodeRuntime {
mgr: DnsPeerMgr,
#[cfg(feature = "tun")]
nic_ctx: ArcNicCtx, // TODO: REMOVE THIS
peer_mgr: Arc<PeerManager>,
global_ctx: ArcGlobalCtx,
elect: Arc<Notify>,
}
impl DnsNodeRuntime {
fn id(&self) -> Uuid {
self.global_ctx.get_id()
}
#[instrument(skip_all, name = "DnsNode election loop")]
async fn run_election(&self, token: CancellationToken) {
let mut election_interval = interval(DNS_SERVER_ELECTION_INTERVAL);
election_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
loop {
tokio::select! {
biased;
_ = token.cancelled() => {
tracing::info!("DnsNode received shutdown signal, exiting election loop");
break;
}
_ = self.elect.notified() => {}
_ = election_interval.tick() => {}
}
tracing::info!("trying to become DNS server");
let mut rpc =
StandAloneServer::new(TcpTunnelListener::new(DNS_SERVER_RPC_ADDR.clone()));
if rpc.serve().await.is_err() {
// Another node already owns the address — that's fine.
tracing::info!(
"failed to bind RPC server, another node might have won the election"
);
continue;
}
tracing::info!("won DNS server election, starting DnsServer");
let server = Arc::new(DnsServer::new(
self.peer_mgr.clone(),
self.global_ctx.clone(),
#[cfg(feature = "tun")]
self.nic_ctx.clone(),
));
server.register(&rpc);
server.run(token.child_token()).await;
tracing::warn!("DnsServer exited, will retry election");
}
}
#[instrument(skip_all, name = "DnsNode main loop")]
async fn run(&self, token: CancellationToken) {
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(DNS_SERVER_RPC_ADDR.clone()));
let mut heartbeat = HeartbeatRequest {
id: Some(self.id().into()),
..Default::default()
};
let mut heartbeat_interval = interval(DNS_NODE_HEARTBEAT_INTERVAL);
heartbeat_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut reconcile_interval = interval(DNS_NODE_RECONCILE_INTERVAL);
reconcile_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut subscriber = self.global_ctx.subscribe();
let mut tasks = JoinSet::new();
loop {
tokio::select! {
biased;
_ = token.cancelled() => {
tracing::info!("DnsNode received shutdown signal, exiting main loop");
break;
}
_ = heartbeat_interval.tick() => {
if let Err(error) = self.heartbeat(&mut rpc, &mut heartbeat).await {
tracing::error!(?error, "heartbeat failed");
self.elect.notify_one();
}
}
_ = reconcile_interval.tick() => {
let mgr = self.mgr.clone();
tasks.spawn(async move {
mgr.reconcile().await;
});
}
_ = self.mgr.dirty.wait() => {}
event = subscriber.recv() => {
match event {
Ok(GlobalCtxEvent::PeerInfoUpdated(peer_ids)) => {
for peer_id in peer_ids {
let mgr = self.mgr.clone();
tasks.spawn(async move {
if let Err(error) = mgr.refresh(peer_id, DNS_PEER_REFRESH_ATTEMPTS, DNS_PEER_REFRESH_BACKOFF).await {
tracing::error!(?error, ?peer_id, "failed to refresh peer");
}
});
}
continue;
}
Ok(
GlobalCtxEvent::DhcpIpv4Changed(..)
| GlobalCtxEvent::DhcpIpv4Conflicted(..),
) => {
tracing::info!(?event, "ip change detected, rebuilding snapshot");
}
Ok(GlobalCtxEvent::ConfigPatched(patch)) => {
// TODO: inspect patch
tracing::info!(?patch, "config change detected, rebuilding snapshot");
}
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!("event listener lagged, skipped {n} events, rebuilding snapshot");
}
Err(broadcast::error::RecvError::Closed) => {
tracing::info!("event bus closed");
break;
}
_ => continue,
}
self.mgr.dirty.mark();
}
result = tasks.join_next(), if !tasks.is_empty() => {
if let Some(Err(error)) = result {
tracing::error!(?error, "refresh task panicked");
}
}
}
}
}
async fn heartbeat(
&self,
rpc: &mut StandAloneClient<TcpTunnelConnector>,
heartbeat: &mut HeartbeatRequest,
) -> anyhow::Result<()> {
let request = if heartbeat.snapshot.is_none() || self.mgr.dirty.reset() {
heartbeat.update(self.mgr.snapshot());
heartbeat.clone()
} else {
let snapshot = heartbeat.snapshot.take();
let request = heartbeat.clone();
heartbeat.snapshot = snapshot;
request
};
let client = rpc
.scoped_client::<DnsNodeMgrRpcClientFactory<BaseController>>("".to_string())
.await?;
let response = client.heartbeat(BaseController::default(), request).await?;
if response.resync {
tracing::trace!("resync requested by server, sending full snapshot");
client
.heartbeat(BaseController::default(), heartbeat.clone())
.await?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct DnsNode {
runtime: DnsNodeRuntime,
task: Option<CancellableTask<()>>,
}
impl DnsNode {
pub fn new(
peer_mgr: Arc<PeerManager>,
global_ctx: ArcGlobalCtx,
#[cfg(feature = "tun")] nic_ctx: ArcNicCtx, // TODO: REMOVE THIS
) -> Self {
let runtime = DnsNodeRuntime {
mgr: DnsPeerMgr::new(peer_mgr.clone(), global_ctx.clone()),
#[cfg(feature = "tun")]
nic_ctx,
peer_mgr,
global_ctx,
elect: Default::default(),
};
Self {
runtime,
task: None,
}
}
pub fn start(&mut self) {
let runtime = self.runtime.clone();
self.task
.replace(CancellableTask::spawn(|token| async move {
runtime.elect.notify_one();
tokio::join!(runtime.run_election(token.clone()), runtime.run(token));
}));
self.runtime.mgr.register();
}
pub async fn stop(&mut self) -> io::Result<()> {
self.runtime.mgr.unregister();
let Some(task) = self.task.take() else {
return Ok(());
};
task.stop(None).await
}
}
impl Drop for DnsNode {
fn drop(&mut self) {
self.runtime.mgr.unregister();
}
}
#[cfg(all(test, feature = "tun"))]
mod tests {
use super::*;
use crate::common::global_ctx::GlobalCtxEvent;
use crate::peers::tests::create_mock_peer_manager;
use crate::proto::api::config::InstanceConfigPatch;
use crate::proto::dns::{DnsNodeMgrRpc, DnsNodeMgrRpcServer, HeartbeatResponse};
use crate::proto::rpc_impl::standalone::StandAloneServer;
use crate::proto::rpc_types;
use crate::tunnel::common::tests::wait_for_condition;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::sleep;
use url::Url;
#[derive(Debug)]
struct RecordingDnsNodeMgr {
requests: Mutex<Vec<HeartbeatRequest>>,
resync_on_first: AtomicBool,
}
impl RecordingDnsNodeMgr {
fn new(resync_on_first: bool) -> Self {
Self {
requests: Mutex::new(Vec::new()),
resync_on_first: AtomicBool::new(resync_on_first),
}
}
async fn recorded_requests(&self) -> Vec<HeartbeatRequest> {
self.requests.lock().await.clone()
}
}
#[async_trait::async_trait]
impl DnsNodeMgrRpc for RecordingDnsNodeMgr {
type Controller = BaseController;
async fn heartbeat(
&self,
_: Self::Controller,
input: HeartbeatRequest,
) -> rpc_types::error::Result<HeartbeatResponse> {
let mut requests = self.requests.lock().await;
requests.push(input);
let is_first = requests.len() == 1;
let resync = is_first && self.resync_on_first.load(Ordering::Relaxed);
if is_first {
self.resync_on_first.store(false, Ordering::Relaxed);
}
Ok(HeartbeatResponse { resync })
}
}
async fn build_test_runtime() -> DnsNodeRuntime {
let peer_mgr = create_mock_peer_manager().await;
let global_ctx = peer_mgr.get_global_ctx();
let nic_ctx: ArcNicCtx = Arc::new(Mutex::new(None));
DnsNodeRuntime {
mgr: DnsPeerMgr::new(peer_mgr.clone(), global_ctx.clone()),
nic_ctx,
peer_mgr,
global_ctx,
elect: Default::default(),
}
}
async fn start_recording_rpc_server(
rpc_addr: Url,
resync_on_first: bool,
) -> anyhow::Result<(
Arc<RecordingDnsNodeMgr>,
StandAloneServer<TcpTunnelListener>,
)> {
let mgr = Arc::new(RecordingDnsNodeMgr::new(resync_on_first));
let mut server = StandAloneServer::new(TcpTunnelListener::new(rpc_addr));
server
.registry()
.register(DnsNodeMgrRpcServer::new_arc(mgr.clone()), "");
server.serve().await?;
sleep(Duration::from_millis(50)).await;
Ok((mgr, server))
}
async fn occupy_dns_rpc_addr(rpc_addr: Url) -> StandAloneServer<TcpTunnelListener> {
let mut server = StandAloneServer::new(TcpTunnelListener::new(rpc_addr));
server.serve().await.unwrap();
server
}
#[tokio::test]
async fn heartbeat_first_send_includes_snapshot() {
let rpc_addr = Url::parse(&format!("tcp://127.0.0.1:{}", 49851)).unwrap();
let (_mgr, server) = start_recording_rpc_server(rpc_addr.clone(), false)
.await
.unwrap();
let node = build_test_runtime().await;
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(rpc_addr));
let mut heartbeat = HeartbeatRequest {
id: Some(node.id().into()),
..Default::default()
};
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
drop(server);
sleep(Duration::from_millis(50)).await;
assert!(heartbeat.snapshot.is_some());
assert!(!heartbeat.digest.is_empty());
}
#[tokio::test]
async fn heartbeat_clean_send_digest_only() {
let rpc_addr = Url::parse(&format!("tcp://127.0.0.1:{}", 49852)).unwrap();
let (mgr, server) = start_recording_rpc_server(rpc_addr.clone(), false)
.await
.unwrap();
let node = build_test_runtime().await;
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(rpc_addr));
let mut heartbeat = HeartbeatRequest {
id: Some(node.id().into()),
..Default::default()
};
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
let _ = node.mgr.dirty.reset();
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
let requests = mgr.recorded_requests().await;
drop(server);
sleep(Duration::from_millis(50)).await;
assert_eq!(requests.len(), 2);
assert!(requests[0].snapshot.is_some());
assert!(requests[1].snapshot.is_none());
assert_eq!(requests[0].digest, requests[1].digest);
}
#[tokio::test]
async fn heartbeat_dirty_forces_full_snapshot() {
let rpc_addr = Url::parse(&format!("tcp://127.0.0.1:{}", 49853)).unwrap();
let (mgr, server) = start_recording_rpc_server(rpc_addr.clone(), false)
.await
.unwrap();
let node = build_test_runtime().await;
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(rpc_addr));
let mut heartbeat = HeartbeatRequest {
id: Some(node.id().into()),
..Default::default()
};
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
node.mgr.dirty.mark();
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
let requests = mgr.recorded_requests().await;
drop(server);
sleep(Duration::from_millis(50)).await;
assert_eq!(requests.len(), 2);
assert!(requests[0].snapshot.is_some());
assert!(requests[1].snapshot.is_some());
}
#[tokio::test]
async fn heartbeat_resync_triggers_second_send() {
let rpc_addr = Url::parse(&format!("tcp://127.0.0.1:{}", 49854)).unwrap();
let (mgr, server) = start_recording_rpc_server(rpc_addr.clone(), true)
.await
.unwrap();
let node = build_test_runtime().await;
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(rpc_addr));
let mut heartbeat = HeartbeatRequest {
id: Some(node.id().into()),
..Default::default()
};
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
let requests = mgr.recorded_requests().await;
drop(server);
sleep(Duration::from_millis(50)).await;
assert_eq!(requests.len(), 2);
assert!(requests[0].snapshot.is_some());
assert!(requests[1].snapshot.is_some());
}
#[tokio::test]
#[serial_test::serial(dns_node_rpc_addr)]
async fn run_marks_dirty_on_dhcp_event() {
let node = build_test_runtime().await;
let _ = node.mgr.dirty.reset();
assert!(!node.mgr.dirty.peek());
let token = CancellationToken::new();
let handle = tokio::spawn({
let node = node.clone();
let token = token.clone();
async move { node.run(token).await }
});
sleep(Duration::from_millis(50)).await;
node.global_ctx
.issue_event(GlobalCtxEvent::DhcpIpv4Changed(None, None));
wait_for_condition(async || node.mgr.dirty.peek(), Duration::from_secs(2)).await;
token.cancel();
tokio::time::timeout(Duration::from_secs(2), handle)
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn run_marks_dirty_on_config_patched_event() {
let node = build_test_runtime().await;
let _ = node.mgr.dirty.reset();
assert!(!node.mgr.dirty.peek());
let token = CancellationToken::new();
let handle = tokio::spawn({
let node = node.clone();
let token = token.clone();
async move { node.run(token).await }
});
sleep(Duration::from_millis(50)).await;
node.global_ctx
.issue_event(GlobalCtxEvent::ConfigPatched(InstanceConfigPatch::default()));
wait_for_condition(async || node.mgr.dirty.peek(), Duration::from_secs(2)).await;
token.cancel();
tokio::time::timeout(Duration::from_secs(2), handle)
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn run_peer_info_updated_non_self_does_not_mark_dirty() {
let node = build_test_runtime().await;
let _ = node.mgr.dirty.reset();
assert!(!node.mgr.dirty.peek());
let token = CancellationToken::new();
let handle = tokio::spawn({
let node = node.clone();
let token = token.clone();
async move { node.run(token).await }
});
sleep(Duration::from_millis(50)).await;
node.global_ctx
.issue_event(GlobalCtxEvent::PeerInfoUpdated(vec![u32::MAX]));
sleep(Duration::from_millis(200)).await;
assert!(!node.mgr.dirty.peek());
token.cancel();
tokio::time::timeout(Duration::from_secs(2), handle)
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn run_heartbeat_error_notifies_election() {
let node = build_test_runtime().await;
let _ = node.mgr.dirty.reset();
let token = CancellationToken::new();
let notified = node.elect.notified();
let handle = tokio::spawn({
let node = node.clone();
let token = token.clone();
async move { node.run(token).await }
});
tokio::time::timeout(2 * DNS_NODE_HEARTBEAT_INTERVAL, notified)
.await
.expect("heartbeat failure should notify election");
token.cancel();
tokio::time::timeout(Duration::from_secs(5), handle)
.await
.unwrap()
.unwrap();
}
}
+561
View File
@@ -0,0 +1,561 @@
use crate::dns::config::DNS_NODE_TTI;
use crate::dns::utils::addr::NameServerAddr;
use crate::dns::zone::{Zone, ZoneGroup};
use crate::proto::dns::DnsNodeMgrRpc;
use crate::proto::dns::{DnsSnapshot, HeartbeatRequest, HeartbeatResponse};
use crate::proto::rpc_types;
use crate::proto::rpc_types::controller::BaseController;
use crate::proto::utils::TransientDigest;
use crate::utils::dirty::DirtyFlag;
use anyhow::Error;
use hickory_server::zone_handler::Catalog;
use itertools::Itertools;
use moka::future::Cache;
use std::collections::HashSet;
use uuid::Uuid;
#[derive(Debug, Clone, Default)]
struct DnsNodeInfo {
digest: [u8; 32],
zones: ZoneGroup,
addresses: HashSet<NameServerAddr>,
listeners: HashSet<NameServerAddr>,
}
impl TryFrom<&DnsSnapshot> for DnsNodeInfo {
type Error = Error;
fn try_from(value: &DnsSnapshot) -> Result<Self, Self::Error> {
Ok(Self {
digest: value.digest(),
zones: value.zones.as_slice().try_into()?,
addresses: value
.addresses
.iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
listeners: value
.listeners
.iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
})
}
}
#[derive(Debug, Default)]
pub struct DnsNodeMgrDirtyFlags {
pub catalog: DirtyFlag,
pub addresses: DirtyFlag,
pub listeners: DirtyFlag,
}
#[derive(Debug)]
pub struct DnsNodeMgr {
nodes: Cache<Uuid, DnsNodeInfo>,
pub dirty: DnsNodeMgrDirtyFlags,
}
impl DnsNodeMgr {
pub fn new() -> Self {
Self {
nodes: Cache::builder().time_to_idle(DNS_NODE_TTI).build(),
dirty: Default::default(),
}
}
pub fn catalog(&self) -> Catalog {
let groups = self.collect_zones().into_groups();
tracing::trace!("building catalog with zones: {:?}", groups);
let system = Zone::system().create_forward_zone_handler();
groups
.into_iter()
.fold(Catalog::new(), |mut catalog, (origin, zones)| {
catalog.upsert(
origin.clone(),
zones
.iter_zone_handlers()
.chain(system.iter().cloned())
.collect(),
);
catalog
})
}
pub fn collect_zones(&self) -> ZoneGroup {
let mut zones = Vec::new();
let mut local = HashSet::new();
for (_, info) in self.nodes.iter() {
zones.extend(info.zones);
local.extend(info.addresses);
local.extend(info.listeners);
}
zones.push(Zone::system());
for forward in zones.iter_mut().flat_map(|z| &mut z.forward) {
forward.name_servers.retain_mut(|ns| {
ns.connections
.retain(|c| !local.contains(&(ns.ip, c).into()));
!ns.connections.is_empty()
});
}
zones.into()
}
pub fn iter_addresses(&self) -> impl Iterator<Item = NameServerAddr> + use<'_> {
self.nodes
.iter()
.flat_map(|(_, info)| info.addresses)
.unique()
}
pub fn iter_listeners(&self) -> impl Iterator<Item = NameServerAddr> + use<'_> {
self.nodes
.iter()
.flat_map(|(_, info)| info.listeners)
.unique()
}
}
#[async_trait::async_trait]
impl DnsNodeMgrRpc for DnsNodeMgr {
type Controller = BaseController;
async fn heartbeat(
&self,
_: BaseController,
input: HeartbeatRequest,
) -> rpc_types::error::Result<HeartbeatResponse> {
let id = input
.id
.ok_or(anyhow::anyhow!(
"missing id in heartbeat request: {:?}",
input
))?
.into();
let resync = if let Some(snapshot) = input.snapshot.as_ref() {
let new = DnsNodeInfo::try_from(snapshot)?;
let old = self.nodes.get(&id).await.unwrap_or_default();
if new.digest != old.digest {
self.dirty.catalog.mark();
if new.addresses != old.addresses {
self.dirty.addresses.mark();
}
if new.listeners != old.listeners {
self.dirty.listeners.mark();
}
self.nodes.insert(id, new).await;
}
false
} else {
self.nodes
.get(&id)
.await
.is_none_or(|info| input.digest != info.digest)
};
Ok(HeartbeatResponse { resync })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dns::tests::{
dns_snapshot_with as snapshot_with, heartbeat_with_snapshot, new_request,
zone_data_a_with_forwarders as valid_zone_data,
};
use crate::dns::utils::response::ResponseHandle;
use hickory_proto::op::{Message, ResponseCode};
use hickory_proto::rr::{RData, RecordType};
use std::net::Ipv4Addr;
use tokio::time::{Duration, sleep};
fn heartbeat_digest_only(id: Uuid, digest: Vec<u8>) -> HeartbeatRequest {
HeartbeatRequest {
id: Some(id.into()),
digest,
snapshot: None,
}
}
fn reset_all_dirty(mgr: &DnsNodeMgr) {
let _ = mgr.dirty.catalog.reset();
let _ = mgr.dirty.addresses.reset();
let _ = mgr.dirty.listeners.reset();
}
async fn send_heartbeat(mgr: &DnsNodeMgr, input: HeartbeatRequest) -> HeartbeatResponse {
DnsNodeMgrRpc::heartbeat(mgr, BaseController::default(), input)
.await
.expect("heartbeat should succeed")
}
fn ns(s: &str) -> NameServerAddr {
s.parse().expect("invalid nameserver")
}
async fn lookup_a_record(mgr: &DnsNodeMgr, name: &str) -> anyhow::Result<Message> {
let request = new_request(name, RecordType::A)?;
let response = ResponseHandle::new(512);
let info = mgr
.catalog()
.lookup(&request, None, 0, response.clone())
.await;
assert_eq!(info.response_code, ResponseCode::NoError);
let response = response.into_inner().expect("response should exist");
Message::from_vec(&response).map_err(Into::into)
}
#[tokio::test]
async fn catalog_lookup_returns_record_after_snapshot_heartbeat() -> anyhow::Result<()> {
let mgr = DnsNodeMgr::new();
let id = Uuid::new_v4();
let snapshot = snapshot_with(
vec![valid_zone_data("catalog.test", "10.20.30.40", vec![])],
vec![],
vec![],
);
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot)).await;
let message = lookup_a_record(&mgr, "catalog.test.").await?;
assert!(message.answers.iter().any(|record| {
matches!(
record.data,
RData::A(addr) if *addr == Ipv4Addr::new(10, 20, 30, 40)
)
}));
Ok(())
}
#[tokio::test]
async fn catalog_lookup_aggregates_records_from_multiple_nodes() -> anyhow::Result<()> {
let mgr = DnsNodeMgr::new();
let snap_a = snapshot_with(
vec![valid_zone_data("node-a.test", "10.11.12.13", vec![])],
vec!["udp://10.0.1.1:53"],
vec![],
);
let snap_b = snapshot_with(
vec![valid_zone_data("node-b.test", "10.21.22.23", vec![])],
vec!["udp://10.0.2.1:53"],
vec![],
);
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(Uuid::new_v4(), snap_a)).await;
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(Uuid::new_v4(), snap_b)).await;
let message_a = lookup_a_record(&mgr, "node-a.test.").await?;
let message_b = lookup_a_record(&mgr, "node-b.test.").await?;
assert!(message_a.answers.iter().any(|record| {
matches!(
record.data,
RData::A(addr) if *addr == Ipv4Addr::new(10, 11, 12, 13)
)
}));
assert!(message_b.answers.iter().any(|record| {
matches!(
record.data,
RData::A(addr) if *addr == Ipv4Addr::new(10, 21, 22, 23)
)
}));
Ok(())
}
#[tokio::test]
async fn heartbeat_digest_only_resync_behavior() {
let mgr = DnsNodeMgr::new();
let id = Uuid::new_v4();
let first = send_heartbeat(&mgr, heartbeat_digest_only(id, vec![1, 2, 3])).await;
assert!(first.resync);
let snapshot = snapshot_with(
vec![valid_zone_data("resync.test", "10.0.0.10", vec![])],
vec!["udp://10.0.0.1:53"],
vec!["udp://10.0.0.2:53"],
);
let digest = snapshot.digest();
let full = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot)).await;
assert!(!full.resync);
let same = send_heartbeat(&mgr, heartbeat_digest_only(id, digest.into())).await;
assert!(!same.resync);
let different = send_heartbeat(&mgr, heartbeat_digest_only(id, vec![9, 9, 9])).await;
assert!(different.resync);
}
#[tokio::test]
async fn heartbeat_with_snapshot_marks_dirty_flags_by_field_changes() {
let mgr = DnsNodeMgr::new();
let id = Uuid::new_v4();
reset_all_dirty(&mgr);
let first = snapshot_with(
vec![valid_zone_data("dirty.test", "10.0.0.1", vec![])],
vec!["udp://10.10.10.1:53"],
vec!["udp://10.10.10.2:53"],
);
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, first)).await;
assert!(mgr.dirty.catalog.peek());
assert!(mgr.dirty.addresses.peek());
assert!(mgr.dirty.listeners.peek());
reset_all_dirty(&mgr);
let record_changed = snapshot_with(
vec![valid_zone_data("dirty.test", "10.0.0.2", vec![])],
vec!["udp://10.10.10.1:53"],
vec!["udp://10.10.10.2:53"],
);
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, record_changed)).await;
assert!(mgr.dirty.catalog.peek());
assert!(!mgr.dirty.addresses.peek());
assert!(!mgr.dirty.listeners.peek());
reset_all_dirty(&mgr);
let addr_listener_changed = snapshot_with(
vec![valid_zone_data("dirty.test", "10.0.0.2", vec![])],
vec!["udp://10.10.10.10:53"],
vec!["udp://10.10.10.20:53"],
);
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, addr_listener_changed)).await;
assert!(mgr.dirty.catalog.peek());
assert!(mgr.dirty.addresses.peek());
assert!(mgr.dirty.listeners.peek());
}
#[tokio::test]
async fn heartbeat_with_same_snapshot_digest_is_noop_for_dirty() {
let mgr = DnsNodeMgr::new();
let id = Uuid::new_v4();
let snapshot = snapshot_with(
vec![valid_zone_data("stable.test", "10.30.40.50", vec![])],
vec!["udp://10.3.0.1:53"],
vec!["udp://10.3.0.2:53"],
);
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot.clone())).await;
reset_all_dirty(&mgr);
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot)).await;
assert!(!mgr.dirty.catalog.peek());
assert!(!mgr.dirty.addresses.peek());
assert!(!mgr.dirty.listeners.peek());
}
#[tokio::test]
async fn heartbeat_missing_id_returns_error() {
let mgr = DnsNodeMgr::new();
let err =
DnsNodeMgrRpc::heartbeat(&mgr, BaseController::default(), HeartbeatRequest::default())
.await
.expect_err("missing id should error");
assert!(err.to_string().contains("missing id"));
}
#[tokio::test]
async fn iter_addresses_and_listeners_deduplicate_across_multiple_nodes() -> anyhow::Result<()>
{
let mgr = DnsNodeMgr::new();
let zone_a = Zone::try_from(&valid_zone_data("iter-a.test", "10.1.1.1", vec![]))?;
let zone_b = Zone::try_from(&valid_zone_data("iter-b.test", "10.1.1.2", vec![]))?;
mgr.nodes
.insert(
Uuid::new_v4(),
DnsNodeInfo {
digest: [1; 32],
zones: vec![zone_a].into(),
addresses: [ns("udp://10.100.0.1:53"), ns("udp://10.100.0.2:53")]
.into_iter()
.collect(),
listeners: [ns("udp://10.200.0.1:53")].into_iter().collect(),
},
)
.await;
mgr.nodes
.insert(
Uuid::new_v4(),
DnsNodeInfo {
digest: [2; 32],
zones: vec![zone_b].into(),
addresses: [ns("udp://10.100.0.2:53"), ns("udp://10.100.0.3:53")]
.into_iter()
.collect(),
listeners: [ns("udp://10.200.0.1:53"), ns("udp://10.200.0.2:53")]
.into_iter()
.collect(),
},
)
.await;
let addresses: HashSet<_> = mgr.iter_addresses().collect();
let listeners: HashSet<_> = mgr.iter_listeners().collect();
assert_eq!(addresses.len(), 3);
assert!(addresses.contains(&ns("udp://10.100.0.1:53")));
assert!(addresses.contains(&ns("udp://10.100.0.2:53")));
assert!(addresses.contains(&ns("udp://10.100.0.3:53")));
assert_eq!(listeners.len(), 2);
assert!(listeners.contains(&ns("udp://10.200.0.1:53")));
assert!(listeners.contains(&ns("udp://10.200.0.2:53")));
Ok(())
}
#[tokio::test]
async fn collect_zones_filters_out_local_forwarders() -> anyhow::Result<()> {
let mgr = DnsNodeMgr::new();
let zone = Zone::try_from(&valid_zone_data(
"filter-loop.test",
"10.2.3.4",
vec![
"udp://10.0.0.10:53",
"tcp://10.0.0.11:53",
"udp://1.1.1.1:53",
],
))?;
mgr.nodes
.insert(
Uuid::new_v4(),
DnsNodeInfo {
digest: [1; 32],
zones: vec![zone].into(),
addresses: [ns("udp://10.0.0.10:53")].into_iter().collect(),
listeners: [ns("tcp://10.0.0.11:53")].into_iter().collect(),
},
)
.await;
let zones: Vec<_> = mgr.collect_zones().into_iter().map(Into::into).collect();
let loop_zone = zones
.into_iter()
.find(|z: &crate::proto::dns::ZoneData| z.content.contains("$ORIGIN filter-loop.test"))
.expect("test zone should exist");
let forwarders: HashSet<NameServerAddr> = loop_zone
.forwarders
.iter()
.map(|u| NameServerAddr::try_from(u).expect("forwarder should be valid"))
.collect();
assert_eq!(forwarders.len(), 1);
assert!(forwarders.contains(&ns("udp://1.1.1.1:53")));
Ok(())
}
#[tokio::test]
async fn collect_zones_filters_cross_node_local_forwarders() -> anyhow::Result<()> {
let mgr = DnsNodeMgr::new();
let node_a = snapshot_with(
vec![valid_zone_data(
"cross-node-filter.test",
"10.8.8.8",
vec![
"udp://10.50.0.1:53",
"udp://10.50.0.2:53",
"udp://8.8.8.8:53",
],
)],
vec!["udp://10.50.0.1:53"],
vec![],
);
let node_b = snapshot_with(
vec![valid_zone_data(
"cross-node-helper.test",
"10.9.9.9",
vec![],
)],
vec![],
vec!["udp://10.50.0.2:53"],
);
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(Uuid::new_v4(), node_a)).await;
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(Uuid::new_v4(), node_b)).await;
let zones: Vec<_> = mgr.collect_zones().into_iter().map(Into::into).collect();
let zone = zones
.into_iter()
.find(|z: &crate::proto::dns::ZoneData| {
z.content.contains("$ORIGIN cross-node-filter.test")
})
.expect("test zone should exist");
let forwarders: HashSet<NameServerAddr> = zone
.forwarders
.iter()
.map(|u| NameServerAddr::try_from(u).expect("forwarder should be valid"))
.collect();
assert_eq!(forwarders.len(), 1);
assert!(forwarders.contains(&ns("udp://8.8.8.8:53")));
Ok(())
}
#[tokio::test]
async fn heartbeat_digest_resync_is_node_scoped() {
let mgr = DnsNodeMgr::new();
let node_a = Uuid::new_v4();
let node_b = Uuid::new_v4();
let snap_a = snapshot_with(
vec![valid_zone_data("scope-a.test", "10.60.0.1", vec![])],
vec!["udp://10.60.0.2:53"],
vec![],
);
let digest_a = snap_a.digest();
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(node_a, snap_a)).await;
let a_same = send_heartbeat(&mgr, heartbeat_digest_only(node_a, digest_a.into())).await;
assert!(!a_same.resync);
let b_unknown = send_heartbeat(&mgr, heartbeat_digest_only(node_b, vec![1, 2, 3])).await;
assert!(b_unknown.resync);
}
#[tokio::test]
async fn heartbeat_resync_after_node_idle_ttl_expiry() {
let mgr = DnsNodeMgr::new();
let id = Uuid::new_v4();
let snapshot = snapshot_with(
vec![valid_zone_data("ttl.test", "10.9.9.9", vec![])],
vec!["udp://10.9.0.1:53"],
vec![],
);
let digest = snapshot.digest();
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot)).await;
let before_expiry = send_heartbeat(&mgr, heartbeat_digest_only(id, digest.to_vec())).await;
assert!(!before_expiry.resync);
sleep(DNS_NODE_TTI + Duration::from_millis(300)).await;
let after_expiry = send_heartbeat(&mgr, heartbeat_digest_only(id, digest.into())).await;
assert!(after_expiry.resync);
}
}
+873
View File
@@ -0,0 +1,873 @@
use crate::common::PeerId;
use crate::common::global_ctx::ArcGlobalCtx;
use crate::dns::config::zone::ZoneConfig;
use crate::dns::config::{
DNS_PEER_REFRESH_ATTEMPTS, DNS_PEER_REFRESH_BACKOFF, DNS_PEER_TTI, DnsExportConfig,
DnsGlobalCtxExt,
};
use crate::dns::zone::ZoneGroup;
use crate::peer_center::instance::PeerCenterPeerManagerTrait;
use crate::peers::peer_manager::PeerManager;
use crate::peers::route_trait::Route;
use crate::proto::dns::{
DnsPeerMgrRpc, DnsPeerMgrRpcClientFactory, DnsPeerMgrRpcServer, DnsSnapshot,
GetExportConfigRequest, GetExportConfigResponse, ZoneData,
};
use crate::proto::rpc_types;
use crate::proto::rpc_types::controller::BaseController;
use crate::proto::utils::TransientDigest;
use crate::utils::dirty::DirtyFlag;
use anyhow::Context;
use futures::StreamExt;
use futures::stream;
use moka::future::Cache;
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
use tracing::instrument;
#[derive(Debug, Clone)]
struct DnsPeerInfo {
digest: [u8; 32],
zones: Vec<ZoneData>,
}
impl TryFrom<DnsExportConfig> for DnsPeerInfo {
type Error = anyhow::Error;
fn try_from(value: DnsExportConfig) -> Result<Self, Self::Error> {
let _ = ZoneGroup::try_from(value.zones.as_slice())?;
Ok(Self {
digest: value.digest(),
zones: value.zones,
})
}
}
#[derive(Debug)]
pub struct DnsPeerMgrInner {
peers: Cache<PeerId, DnsPeerInfo>,
pub dirty: DirtyFlag,
peer_mgr: Arc<PeerManager>,
global_ctx: ArcGlobalCtx,
}
impl DnsPeerMgrInner {
pub fn snapshot(&self) -> DnsSnapshot {
let global_ctx = &self.global_ctx;
let zones = global_ctx
.dns_iter_zones()
.map(ZoneConfig::into_data)
.chain(
self.peers
.iter()
.flat_map(|(_, info)| info.zones.into_iter()),
)
.collect();
let config = global_ctx.config.get_dns().into_parsed();
DnsSnapshot {
zones,
addresses: config.addresses.into(),
listeners: config.listeners.into(),
}
}
#[instrument(skip(self), level = "trace", ret)]
pub async fn refresh(
&self,
peer_id: PeerId,
mut attempts: usize,
mut backoff: Duration,
) -> anyhow::Result<bool> {
loop {
attempts = attempts.saturating_sub(1);
let result = self.try_refresh(peer_id).await;
match &result {
Ok(_) => {
tracing::trace!(?peer_id, "peer info refreshed");
return result;
}
Err(_) if attempts == 0 => {
self.peers.invalidate(&peer_id).await;
self.dirty.mark();
tracing::error!(
?peer_id,
"exhausted all attempts to refresh peer info, invalidating cache"
);
return result;
}
Err(error) => {
tracing::error!(
?error,
?peer_id,
"failed to refresh peer info, retrying in {:?}",
backoff
);
sleep(backoff).await;
backoff *= 2;
}
}
}
}
async fn try_refresh(&self, peer_id: PeerId) -> anyhow::Result<bool> {
if peer_id == self.peer_mgr.my_peer_id() {
self.dirty.mark();
return Ok(true);
}
let Some(route) = self.peer_mgr.get_route().get_peer_info(peer_id).await else {
if self.peers.remove(&peer_id).await.is_some() {
tracing::debug!(?peer_id, "peer route disappeared, removing from cache");
self.dirty.mark();
}
return Ok(true);
};
if self
.peers
.get(&peer_id)
.await
.is_some_and(|info| route.dns == info.digest)
{
return Ok(false);
}
if !route.dns.is_empty() {
let info = self.fetch(peer_id).await.with_context(|| {
format!("failed to fetch dns export config from peer {}", peer_id)
})?;
self.peers.insert(peer_id, info).await;
} else {
self.peers.invalidate(&peer_id).await;
}
self.dirty.mark();
Ok(true)
}
#[instrument(skip(self), level = "trace", ret)]
async fn fetch(&self, peer_id: PeerId) -> anyhow::Result<DnsPeerInfo> {
self.peer_mgr
.get_peer_rpc_mgr()
.rpc_client()
.scoped_client::<DnsPeerMgrRpcClientFactory<BaseController>>(
self.peer_mgr.my_peer_id(),
peer_id,
self.global_ctx.get_network_name(),
)
.get_export_config(BaseController::default(), GetExportConfigRequest {})
.await
.context("rpc call failed")?
.try_into()
}
}
#[async_trait::async_trait]
impl DnsPeerMgrRpc for DnsPeerMgrInner {
type Controller = BaseController;
async fn get_export_config(
&self,
_: Self::Controller,
_: GetExportConfigRequest,
) -> rpc_types::error::Result<GetExportConfigResponse> {
Ok(self.global_ctx.dns_export_config())
}
}
#[derive(Debug, Clone)]
pub struct DnsPeerMgr(Arc<DnsPeerMgrInner>);
impl DnsPeerMgr {
pub fn new(peer_mgr: Arc<PeerManager>, global_ctx: ArcGlobalCtx) -> Self {
Self(Arc::new(DnsPeerMgrInner {
peers: Cache::builder().time_to_idle(DNS_PEER_TTI).build(),
dirty: Default::default(),
peer_mgr,
global_ctx,
}))
}
pub fn register(&self) {
self.peer_mgr
.get_peer_rpc_mgr()
.rpc_server()
.registry()
.register(
DnsPeerMgrRpcServer::new_arc(self.0.clone()),
&self.global_ctx.get_network_name(),
);
}
pub fn unregister(&self) -> Option<()> {
self.peer_mgr
.get_peer_rpc_mgr()
.rpc_server()
.registry()
.unregister(
DnsPeerMgrRpcServer::new_arc(self.0.clone()),
&self.global_ctx.get_network_name(),
)
}
#[instrument(skip(self), level = "trace")]
pub async fn reconcile(&self) {
stream::iter(self.peer_mgr.list_routes().await.into_iter())
.map(|route| {
let peer_id = route.peer_id;
let this = self.clone();
async move {
if let Err(error) = this
.refresh(peer_id, DNS_PEER_REFRESH_ATTEMPTS, DNS_PEER_REFRESH_BACKOFF)
.await
{
tracing::error!(?error, ?peer_id, "failed to refresh peer info");
}
}
})
.buffer_unordered(32)
.collect::<Vec<_>>()
.await;
}
}
impl Deref for DnsPeerMgr {
type Target = DnsPeerMgrInner;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::global_ctx::tests::get_mock_global_ctx;
use crate::dns::config::zone::ZoneConfig;
use crate::dns::tests::zone_data_a as valid_zone_data;
use crate::peers::create_packet_recv_chan;
use crate::peers::peer_manager::RouteAlgoType;
use crate::peers::tests::{connect_peer_manager, wait_route_appear};
use crate::proto::dns::GetExportConfigRequest;
use std::collections::HashSet;
use std::net::Ipv4Addr;
use tokio::time::{Duration, sleep};
async fn create_peer_manager_with_zone(
host: &str,
origin: &str,
record_ip: Ipv4Addr,
) -> Arc<PeerManager> {
let ctx = get_mock_global_ctx();
let mut dns = ctx.config.get_dns().into_raw();
dns.name = Some(host.parse().unwrap());
dns.zones
.get_or_insert_default()
.push(ZoneConfig::dedicated(
origin.parse().expect("invalid zone origin"),
Some(record_ip),
vec![],
));
ctx.config.set_dns(dns.into());
let (s, _r) = create_packet_recv_chan();
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, ctx, s));
peer_mgr.run().await.unwrap();
peer_mgr
}
#[test]
fn dns_peer_info_try_from_valid_config() {
let cfg = DnsExportConfig {
zones: vec![valid_zone_data("valid.peer.test", "10.0.0.10")],
};
let info = DnsPeerInfo::try_from(cfg).expect("valid export config should pass");
assert_eq!(info.zones.len(), 1);
assert!(!info.digest.is_empty());
}
#[test]
fn dns_peer_info_try_from_invalid_zone_rejected() {
let cfg = DnsExportConfig {
zones: vec![ZoneData::new(&".".parse().unwrap(), 60, ["?"], [], [])],
};
assert!(DnsPeerInfo::try_from(cfg).is_err());
}
#[tokio::test]
async fn snapshot_merges_local_and_cached_peer_zones() {
let peer_mgr = create_peer_manager_with_zone(
"local-peer",
"local-custom.test",
Ipv4Addr::new(10, 10, 10, 10),
)
.await;
let global_ctx = peer_mgr.get_global_ctx();
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx);
mgr.peers
.insert(
999_999,
DnsPeerInfo {
digest: [9; 32],
zones: vec![valid_zone_data("peer-cache.test", "10.20.30.40")],
},
)
.await;
let snapshot = mgr.snapshot();
assert!(
snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN peer-cache.test"))
);
assert!(
snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN local-custom.test"))
);
}
#[tokio::test]
async fn snapshot_includes_local_addresses_and_listeners() {
let peer_mgr = create_peer_manager_with_zone(
"local-addr-listener",
"local-addr-zone.test",
Ipv4Addr::new(10, 10, 11, 11),
)
.await;
let global_ctx = peer_mgr.get_global_ctx();
let expected = global_ctx.config.get_dns().into_parsed();
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx);
let snapshot = mgr.snapshot();
let mut expected_addresses = expected
.addresses
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<_>>();
let mut expected_listeners = expected
.listeners
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<_>>();
let mut got_addresses = snapshot
.addresses
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<_>>();
let mut got_listeners = snapshot
.listeners
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<_>>();
expected_addresses.sort();
expected_listeners.sort();
got_addresses.sort();
got_listeners.sort();
assert_eq!(got_addresses, expected_addresses);
assert_eq!(got_listeners, expected_listeners);
}
#[tokio::test]
async fn snapshot_aggregates_zones_from_multiple_cached_peers() {
let peer_mgr = create_peer_manager_with_zone(
"local-multi",
"local-multi.test",
Ipv4Addr::new(10, 10, 12, 1),
)
.await;
let global_ctx = peer_mgr.get_global_ctx();
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx);
mgr.peers
.insert(
11,
DnsPeerInfo {
digest: [11; 32],
zones: vec![valid_zone_data("peer-a.test", "10.20.30.41")],
},
)
.await;
mgr.peers
.insert(
12,
DnsPeerInfo {
digest: [12; 32],
zones: vec![valid_zone_data("peer-b.test", "10.20.30.42")],
},
)
.await;
let snapshot = mgr.snapshot();
let contents: HashSet<_> = snapshot.zones.into_iter().map(|z| z.content).collect();
assert!(contents.iter().any(|z| z.contains("$ORIGIN peer-a.test")));
assert!(contents.iter().any(|z| z.contains("$ORIGIN peer-b.test")));
assert!(
contents
.iter()
.any(|z| z.contains("$ORIGIN local-multi.test"))
);
}
#[tokio::test]
async fn snapshot_with_peer_without_zones_keeps_local_snapshot() {
let peer_mgr = create_peer_manager_with_zone(
"local-empty-peer-zone",
"local-empty-zone.test",
Ipv4Addr::new(10, 10, 13, 1),
)
.await;
let mgr = DnsPeerMgr::new(peer_mgr, get_mock_global_ctx());
let before = mgr.snapshot();
mgr.peers
.insert(
13,
DnsPeerInfo {
digest: [13; 32],
zones: vec![],
},
)
.await;
let after = mgr.snapshot();
assert_eq!(before.zones.len(), after.zones.len());
assert_eq!(before.addresses, after.addresses);
assert_eq!(before.listeners, after.listeners);
}
#[tokio::test]
async fn get_export_config_returns_global_ctx_export() {
let peer_mgr = create_peer_manager_with_zone(
"export-peer",
"exported-zone.test",
Ipv4Addr::new(10, 10, 20, 20),
)
.await;
let global_ctx = peer_mgr.get_global_ctx();
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx.clone());
let got = DnsPeerMgrRpc::get_export_config(
mgr.0.as_ref(),
BaseController::default(),
GetExportConfigRequest {},
)
.await
.expect("get_export_config should succeed");
assert_eq!(got, global_ctx.dns_export_config());
}
#[tokio::test]
async fn refresh_self_peer_marks_dirty_only() {
let peer_mgr = create_peer_manager_with_zone(
"self-peer",
"self-zone.test",
Ipv4Addr::new(10, 0, 0, 1),
)
.await;
let mgr = DnsPeerMgr::new(peer_mgr.clone(), peer_mgr.get_global_ctx());
mgr.dirty.reset();
mgr.try_refresh(peer_mgr.my_peer_id()).await.unwrap();
assert!(mgr.dirty.peek());
}
#[tokio::test]
async fn refresh_missing_route_noop_and_not_dirty() {
let peer_mgr = create_peer_manager_with_zone(
"solo-peer",
"solo-zone.test",
Ipv4Addr::new(10, 0, 0, 2),
)
.await;
let mgr = DnsPeerMgr::new(peer_mgr, get_mock_global_ctx());
mgr.dirty.reset();
mgr.try_refresh(987_654).await.unwrap();
assert!(!mgr.dirty.peek());
}
#[tokio::test]
async fn refresh_same_digest_skips_fetch_and_not_mark_dirty() {
let local = create_peer_manager_with_zone(
"local-same",
"local-same.test",
Ipv4Addr::new(10, 0, 1, 1),
)
.await;
let remote = create_peer_manager_with_zone(
"remote-same",
"remote-same.test",
Ipv4Addr::new(10, 0, 1, 2),
)
.await;
connect_peer_manager(local.clone(), remote.clone()).await;
wait_route_appear(local.clone(), remote.clone())
.await
.expect("route should appear");
let remote_id = remote.my_peer_id();
let remote_route_dns = local
.get_route()
.get_peer_info(remote_id)
.await
.expect("remote route should exist")
.dns;
let mgr = DnsPeerMgr::new(local, get_mock_global_ctx());
mgr.peers
.insert(
remote_id,
DnsPeerInfo {
digest: remote_route_dns
.try_into()
.expect("route dns digest should be 32 bytes"),
zones: vec![valid_zone_data("cached-same.test", "10.0.1.9")],
},
)
.await;
mgr.dirty.reset();
mgr.try_refresh(remote_id).await.unwrap();
sleep(Duration::from_millis(50)).await;
assert!(!mgr.dirty.peek());
}
#[tokio::test]
async fn refresh_remote_peer_fetches_and_updates_snapshot() {
let local = create_peer_manager_with_zone(
"local-refresh",
"local-refresh.test",
Ipv4Addr::new(10, 0, 2, 1),
)
.await;
let remote = create_peer_manager_with_zone(
"remote-refresh",
"remote-export.test",
Ipv4Addr::new(10, 0, 2, 2),
)
.await;
let local_dns = DnsPeerMgr::new(local.clone(), local.get_global_ctx());
let remote_dns = DnsPeerMgr::new(remote.clone(), remote.get_global_ctx());
remote_dns.register();
connect_peer_manager(local.clone(), remote.clone()).await;
wait_route_appear(local.clone(), remote.clone())
.await
.expect("route should appear");
local_dns.dirty.reset();
local_dns.try_refresh(remote.my_peer_id()).await.unwrap();
assert!(local_dns.dirty.peek());
let snapshot = local_dns.snapshot();
assert!(
snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN remote-export.test"))
);
}
#[tokio::test]
async fn multi_peer_refresh_updates_only_target_peer_snapshot_data() {
let local = create_peer_manager_with_zone(
"local-multi-refresh",
"local-multi-refresh.test",
Ipv4Addr::new(10, 2, 0, 1),
)
.await;
let peer_a =
create_peer_manager_with_zone("peer-a", "remote-a.test", Ipv4Addr::new(10, 2, 0, 2))
.await;
let peer_b =
create_peer_manager_with_zone("peer-b", "remote-b.test", Ipv4Addr::new(10, 2, 0, 3))
.await;
let local_dns = DnsPeerMgr::new(local.clone(), local.get_global_ctx());
let peer_a_dns = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
peer_a_dns.register();
connect_peer_manager(local.clone(), peer_a.clone()).await;
connect_peer_manager(local.clone(), peer_b.clone()).await;
wait_route_appear(local.clone(), peer_a.clone())
.await
.expect("route to peer_a should appear");
wait_route_appear(local.clone(), peer_b.clone())
.await
.expect("route to peer_b should appear");
local_dns.try_refresh(peer_a.my_peer_id()).await.unwrap();
let snapshot = local_dns.snapshot();
assert!(
snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN remote-a.test"))
);
assert!(
!snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN remote-b.test"))
);
}
#[tokio::test]
async fn multi_peer_refresh_failure_invalidates_only_target_peer_cache() {
let local = create_peer_manager_with_zone(
"local-invalidate",
"local-invalidate.test",
Ipv4Addr::new(10, 2, 1, 1),
)
.await;
let fail_peer = create_peer_manager_with_zone(
"peer-fail",
"peer-fail.test",
Ipv4Addr::new(10, 2, 1, 2),
)
.await;
let keep_peer = create_peer_manager_with_zone(
"peer-keep",
"peer-keep.test",
Ipv4Addr::new(10, 2, 1, 3),
)
.await;
let local_dns = DnsPeerMgr::new(local.clone(), local.get_global_ctx());
local_dns.register();
let keep_dns = DnsPeerMgr::new(keep_peer.clone(), keep_peer.get_global_ctx());
keep_dns.register();
let fail_id = fail_peer.my_peer_id();
let keep_id = keep_peer.my_peer_id();
local_dns
.peers
.insert(
fail_id,
DnsPeerInfo {
digest: [1; 32],
zones: vec![valid_zone_data("cached-fail.test", "10.2.1.20")],
},
)
.await;
local_dns
.peers
.insert(
keep_id,
DnsPeerInfo {
digest: [2; 32],
zones: vec![valid_zone_data("cached-keep.test", "10.2.1.21")],
},
)
.await;
connect_peer_manager(local.clone(), fail_peer.clone()).await;
connect_peer_manager(local.clone(), keep_peer.clone()).await;
wait_route_appear(local.clone(), fail_peer.clone())
.await
.expect("route to fail_peer should appear");
wait_route_appear(local.clone(), keep_peer.clone())
.await
.expect("route to keep_peer should appear");
local_dns.dirty.reset();
local_dns
.refresh(fail_id, Default::default(), Default::default())
.await
.unwrap_err();
assert!(local_dns.dirty.peek());
assert!(local_dns.peers.get(&fail_id).await.is_none());
assert!(local_dns.peers.get(&keep_id).await.is_some());
}
#[tokio::test]
async fn multi_peer_mixed_digest_changes_only_mark_for_changed_peer() {
let local = create_peer_manager_with_zone(
"local-mixed",
"local-mixed.test",
Ipv4Addr::new(10, 2, 2, 1),
)
.await;
let changed_peer = create_peer_manager_with_zone(
"peer-changed",
"peer-changed.test",
Ipv4Addr::new(10, 2, 2, 2),
)
.await;
let unchanged_peer = create_peer_manager_with_zone(
"peer-unchanged",
"peer-unchanged.test",
Ipv4Addr::new(10, 2, 2, 3),
)
.await;
let local_dns = DnsPeerMgr::new(local.clone(), local.get_global_ctx());
let changed_dns = DnsPeerMgr::new(changed_peer.clone(), changed_peer.get_global_ctx());
let unchanged_dns =
DnsPeerMgr::new(unchanged_peer.clone(), unchanged_peer.get_global_ctx());
changed_dns.register();
unchanged_dns.register();
connect_peer_manager(local.clone(), changed_peer.clone()).await;
connect_peer_manager(local.clone(), unchanged_peer.clone()).await;
wait_route_appear(local.clone(), changed_peer.clone())
.await
.expect("route to changed_peer should appear");
wait_route_appear(local.clone(), unchanged_peer.clone())
.await
.expect("route to unchanged_peer should appear");
let unchanged_id = unchanged_peer.my_peer_id();
let unchanged_digest = local
.get_route()
.get_peer_info(unchanged_id)
.await
.expect("unchanged route should exist")
.dns;
local_dns
.peers
.insert(
changed_peer.my_peer_id(),
DnsPeerInfo {
digest: [0; 32],
zones: vec![valid_zone_data("stale-changed.test", "10.2.2.20")],
},
)
.await;
local_dns
.peers
.insert(
unchanged_id,
DnsPeerInfo {
digest: unchanged_digest
.try_into()
.expect("route dns digest should be 32 bytes"),
zones: vec![valid_zone_data("cached-unchanged.test", "10.2.2.21")],
},
)
.await;
local_dns.dirty.reset();
local_dns
.try_refresh(changed_peer.my_peer_id())
.await
.unwrap();
assert!(local_dns.dirty.peek());
local_dns.dirty.reset();
local_dns.try_refresh(unchanged_id).await.unwrap();
assert!(!local_dns.dirty.peek());
let unchanged_cache = local_dns
.peers
.get(&unchanged_id)
.await
.expect("unchanged peer cache should stay");
assert!(
unchanged_cache
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN cached-unchanged.test"))
);
}
#[tokio::test]
async fn snapshot_removes_cached_peer_zone_after_tti_expire() {
let peer_mgr = create_peer_manager_with_zone(
"local-tti",
"local-tti.test",
Ipv4Addr::new(10, 3, 0, 1),
)
.await;
let global_ctx = peer_mgr.get_global_ctx();
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx);
let cached_peer_id = 66_666;
mgr.peers
.insert(
cached_peer_id,
DnsPeerInfo {
digest: [6; 32],
zones: vec![valid_zone_data("cached-expire.test", "10.3.0.2")],
},
)
.await;
let before = mgr.snapshot();
assert!(
before
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN cached-expire.test"))
);
assert!(
before
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN local-tti.test"))
);
let deadline = tokio::time::Instant::now() + DNS_PEER_TTI + Duration::from_secs(3);
loop {
let now_snapshot = mgr.snapshot();
let expired = !now_snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN cached-expire.test"));
if expired {
assert!(
now_snapshot
.zones
.iter()
.any(|z| z.content.contains("local-tti.test"))
);
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"cached peer zone did not expire within expected TTI window"
);
sleep(Duration::from_millis(100)).await;
}
}
#[tokio::test]
async fn register_then_unregister_returns_some() {
let peer_mgr = create_peer_manager_with_zone(
"register-peer",
"register-zone.test",
Ipv4Addr::new(10, 1, 0, 1),
)
.await;
let mgr = DnsPeerMgr::new(peer_mgr.clone(), peer_mgr.get_global_ctx());
mgr.register();
assert!(mgr.unregister().is_some());
}
}
+163
View File
@@ -0,0 +1,163 @@
## 目标
`instance/dns_server` 重写为单独的 `dns` 模块,为如下的配置项提供支持:
```toml
[dns]
name = "localhost" # optional, replaces hostname, default to system hostname
domain = "localdomain" # optional, replaces tld_dns_zone, default to et.net
addresses = [
"100.100.100.101:53",
] # optional, default to [ "100.100.100.101:53" ]
# any UDP packet or ICMP packet to these addresses will be hijacked by the dns server
# the server does *not* bind to/listen on these addresses!
listeners = [
] # optional, default to empty
# let the dns server bind to these addresses
# could be useful when no_tun = true
# these two options supersede accept_dns
# setting both of them to empty is equivalent to set accept_dns = false, but zones are still broadcasted
# this policy applies to all zones with origin "example.com"
[dns."example.com".import]
whitelist = ["*"]
blacklist = []
disabled = true # optional, whether to reject zones with this origin from connected peers, default to false
recursive = true # optional, apply this policy to all subzones, default to false
[[dns.zone]]
origin = "example.com" # required, name of the zone
ttl = 3600 # optional, default to 0
records = [
"www 60 IN A 123.123.123.123",
"app IN CNAME www",
] # optional, custom DNS records
forwarders = [
"1.1.1.1",
] # optional, forward DNS requests to these servers
fallthrough = false # optional, whether to fall back to next zone (with same origin) if request doesn't match any record in this zone, default to true
# this policy applies to the current zone block
[dns.zone.export] # if present, export this zone to connected peers
whitelist = ["*"] # optional
blacklist = [] # optional
# same zone, but not exported
[[dns.zone]]
origin = "example.com"
forwarders = [
"tcp://192.168.0.53:5353",
]
```
<details>
<summary><h2>计划和进展</h2></summary>
每个 peer 会默认拥有一个专用 zone,它的 origin 是这个 peer 的 fqdn,唯一的记录是指向该 peer 的 ip 的 A、AAAA 记录
## protobuf
- `ZoneData`:包含 Zone 配置,以及一个 ID,该 ID 在读取 TOML 时生成
- `GetExportConfigResponse` (`DnsExportConfig`):包含全部 export 的 `ZoneData`(特别地,包含专用 zone)、该 peer 的 fqdn
- `HeartbeatRequest`: DnsNode 发送的心跳,包含:id、digest、`Option<Snapshot>`
- `DnsSnapshot`: 所有 DnsServer 需要的配置
## RoutePeerInfo
为预防用户提交大量自定义 DNS 记录导致 RoutePeerInfo 泛洪造成带宽压力:
-`RoutePeerInfo` 中只保存本地 DNS 配置的 hash
- 收到 `RoutePeerInfo` 后读取其中 DNS 的 hash,若与本地不同,通过 RPC 拉取 Peer 的 DNS 配置
## DnsNode
1. - [x] 监听配置更新/IP 地址变化,重建快照
2. - [x] (`GlobalCtx` 的扩展 trait) 使用自己的 name 和 domain 创建一个专用 zone,让 name 指向自身 IP(为 DNS 一致性避免使用 127.0.0.1 作为 IP,若没有 IP 则不创建这个 zone)
3. - [x] 每次获得 RoutePeerInfo 时,读取其中的 dns 字段(和一些别的身份标记字段),这是远程 Peer 的 dns 配置(不含 addresses 和 listeners)的 digest
- [x] 接收后检查 digest 和本地配置是否一致,如果一致,不做修改,否则标记 dirty,下一次心跳时将重建快照
4. - [x] 每隔一小段时间向 DnsServer 发送心跳和当前 digest
1. 如果没有 dirty 标记,心跳不含 snapshot
2. 如果有 dirty 标记,重建 snapshot 并在心跳中包含;
3. 如果 DnsServer 返回 resync,立刻重新发送带有 Snapshot 的心跳
5. - [x] 一个 RPC 接口,供 Peer 拉取 DNS 配置
6. - [x] 一个独立循环,用于选举 DnsServer,每次循环尝试绑定 DNS_SERVER_RPC_ADDR 监听 RPC 请求
1. 一台机器上所有 EasyTier 实例一起尝试绑定 DNS_SERVER_RPC_ADDR,绑定成功的那个就启动 DnsServer(当然也启动 DnsNode),失败的那些就只有 DnsNode
2. 每隔一小段时间或者 DnsNode 心跳失败(notify)后立刻尝试 bind,如果 bind 成功就说明 DnsServer 真挂了,那就自己在这个已有的 SocketAddr 上启动 DnsServer(忽略 bind 失败或启动失败,启动失败就直接释放 socket),这样才能保证服务不断
## DnsServer
1. - [x] 提供一个 RPC 接口接受 DnsNode 的心跳,如果心跳 digest 和本地不符则返回 resync
2. - [x] 收到含有 snapshot 的心跳时替换本地配置;如果 snapshot 中的 listeners 或者 addresses 不同则 rebind
3. - [x] (`moka::Cache`) 持续检查是否有过期(丢失心跳)的 DnsNode,需要把这些 DnsNode 提供的所有配置清除
4. - [x] 每次更新 zone 时自动添加 root zone
- [x] (`Zone::system`) 并把它的 forwarder 设置为系统 DNS
5. - [x] 使用 snapshot 更新 zone。不用合并同名 zone,直接用 Zone 结构体提供的 ChainedZoneHandler 按顺序插入 Catalog 就行,不过注意要先插入 MemoryZoneHandler,这些都是 records,后插入 ForwardZoneHandler,这都是 forwarders
6. - [x] 更新 zone 的时候自动去掉 forwarder 中导致回环的那些,就是把 addresses 和 listeners 去掉(root zone 也需要这个逻辑)
7. - [x] 内部接口,控制 DnsServer 是否 bind 到某些 socket(也就是配置中的 listeners
8. - [x] Listeners 绑定失败打印日志(失败一个打印一次然后就跳过),即便这时 addresses 为空也不要停机。(否则释放 socket 绑定后会有 instance 抢占 socket 试图启动 server,然后就死循环)
9. - [x] 内部接口,更新 addresses。目前这些用来 hijack 的 addresses 都是只支持 udp 简单查询,就是一个 UDP 包查询,tcp 完全不管。但是可以支持除了 53 之外的端口,这个不难。
- [x] 并且给 tun 添加删除这些 addresses 的路由
10. - [x] 启动时,往 packet pipeline 上挂一个 filter,和目前 magic dns 的操作一样,给 addresses 添加路由并劫持所有目的为配置中 addresses 的 UDP 包,直接作为 DNS request 读取并交给 DnsServer 解析
- [x] 这个 addresses 可能还得 append 到 resolv.conf 之类的地方
11. - [x] Addresses 和 Listeners 更新时~~需要检查所有 zone 的 forwarder~~直接更新所有 zone,之前为了避免回环可能去掉了一些 forwarder,或者有新的 forwarder 要去除
此外,还有以下几个设计要点:
- Zone 允许只有 forwarder,这时候就是纯转发器
- Zone 允许没有 forwarder,这时候要检查是不是有 SOA 和 NS 记录,如果没有可能需要添加?
- 另一种方案是 DnsNode 挂 filter,自己处理 UDP 劫持,用某种方式(如 RPC)把 DNS 请求代理给 DnsServer,该方案的优势在于完全解耦 DnsServer 的实现,特别是解决了 DnsServer 所在实例可能 no_tun 的问题,缺点是:
- 性能更差
- 操作路由表或 /etc/resolv.conf 时会有多个 instance 同时修改,修改结果没有确定性
- DnsServer 仍然需要得知 addresses 以进行回环检测
- debug 更麻烦
- 难以实现策略 DNS,比如不同来源的 DNS 请求走不同的 zone
另外任何关于系统 DNS 的操作,清理都参考现有的 magic dns。
## 已知但无需/无计划解决的问题
- the ttl option isn't working because of https://github.com/hickory-dns/hickory-dns/pull/3450
- [minor] address 路由绑定必须在有 tun 的实例上做;listener 绑定则与 tun 无关,现有竞选机制无法保证有 tun 的实例能优先启动 DnsServer
- 不妨假设大多数情况下一台机器上所有实例的 no_tun 设置相同,这时候这个问题实际上不存在
- [minor] DnsServer 更新 zone 的时候需要更精细的合并/去重控制,如延迟低者/本地优先
- [minor] 更新 forwarder 时还需要检查间接回环,如 DNS 请求发送给某个 Peer,这个 Peer 又把请求转发回自己了
- [minor] 防止死锁/挂起的 DnsServer 占用 socket
- ~~[minor] RoutePeerInfo 可能不能过大~~
- [minor] 增量 Zone 更新
- DNS 策略
</details>
<details>
<summary><h2>Related Issues</h2></summary>
- closes https://github.com/EasyTier/EasyTier/issues/742
- closes https://github.com/EasyTier/EasyTier/issues/771
- closes https://github.com/EasyTier/EasyTier/issues/927
- closes https://github.com/EasyTier/EasyTier/issues/1071
- closes https://github.com/EasyTier/EasyTier/issues/1142
- closes https://github.com/EasyTier/EasyTier/issues/1322
- closes https://github.com/EasyTier/EasyTier/issues/1381
- closes https://github.com/EasyTier/EasyTier/issues/1488
- closes https://github.com/EasyTier/EasyTier/issues/1597
- closes https://github.com/EasyTier/EasyTier/issues/1645
- closes https://github.com/EasyTier/EasyTier/issues/1764
- closes https://github.com/EasyTier/EasyTier/issues/1814
- closes https://github.com/EasyTier/EasyTier/issues/1826
- closes https://github.com/EasyTier/EasyTier/issues/2004
---
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/937
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1016
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1348
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1699
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1873
</details>
+562
View File
@@ -0,0 +1,562 @@
# EasyTier DNS 模块设计说明(重构版)
> 本文档基于 `easytier/src/dns` 当前代码实现与 `plan.md`。
> 当前先给出第一部分:**整体架构与基础逻辑**。
## 1. 模块目标与定位
`dns` 模块是对旧 `instance/dns_server` 方案的重构,目标是把 DNS 能力从“单点功能”升级为“可同步、可扩展、可自治”的子系统。它同时承担三类职责:
1. **本机 DNS 服务能力**
- 能监听配置中的 `listeners`UDP/TCP)作为标准 DNS server。
- 能对配置中的 `addresses` 做流量劫持(UDP DNS + ICMP echo)。
2. **多 Peer DNS 配置同步能力**
- 每个实例作为 `DnsNode` 生成快照并定期心跳。
- 机器上被选举出的 `DnsServer` 聚合所有 Node 快照并动态重建 Catalog。
3. **系统 DNS 接入能力**tun 场景):
- 把 DNS nameserver/search/match domain 写入系统配置(当前主要是 Windows/macOSLinux 仍在演进中)。
---
## 2. 顶层架构(角色分层)
从职责上看,模块分成 4 层:
- **配置层**`config/*`
- 解析 TOML 的 `[dns]``[[dns.zone]]`、策略字段。
- 产出 `DnsConfig``ZoneConfig`,并提供默认值(如默认域名 `et.net`、默认地址 `100.100.100.101:53`)。
- **节点层(控制面)**`node.rs` + `peer_mgr.rs`
- `DnsNode`:本实例的 DNS 控制器,负责选举、心跳、事件监听、重建 snapshot。
- `DnsPeerMgr`:维护远端 peer 的 DNS 摘要与配置拉取,拼装 `DnsSnapshot`
- **服务层(数据面)**`server.rs` + `node_mgr.rs` + `zone.rs`
- `DnsServer`:真正处理 DNS 请求、维护监听 socket、管理 hijack addresses。
- `DnsNodeMgr`:服务端的快照管理器,接收 Node 心跳,维护节点 TTL 与 dirty 状态。
- `Zone`/`ZoneGroup`:把 records + forwarders 变成 Hickory `ZoneHandler` 并装配 `Catalog`
- **系统集成层**`system/*`
- 将当前 DNS 配置下发到 OS`SystemConfigurator` 抽象)。
- 服务退出/变更时负责清理或覆盖。
---
## 3. 关键对象与数据模型
- **`DnsConfig`**`config/dns.rs`
- 核心字段:`zones``policies``name``domain``addresses``listeners`
- `get_fqdn()``name + domain` 生成本机 FQDN。
- **`ZoneData` / `Zone`**`proto/dns.proto` + `zone.rs`
- `ZoneData` 是网络传输模型(protobuf),含 `id/origin/ttl/records/forwarders`
- `Zone` 是运行期模型:
- `records -> InMemoryZoneHandler`
- `forwarders -> ForwardZoneHandler`
- 同 origin 可链式共存(ChainedZoneHandler 语义)。
- **`DnsSnapshot`**`proto/dns.proto`
- Node 发给 Server 的完整状态:`zones + addresses + listeners`
- **`HeartbeatRequest`**
- 发送 `id + digest + optional snapshot`
- digest 一致时可只发轻量心跳,不带 snapshot。
- **`DirtyFlag`**`utils/dirty.rs`
- 全模块统一的“脏标记 + 通知器”,用于节流和增量触发(不是每次事件都全量重建)。
---
## 4. 基础运行逻辑(主链路)
### 4.1 本地节点启动
`Instance``magic-dns` feature 下创建并启动 `DnsNode``DnsNode` 启动后并行跑两个循环:
1. **选举循环**`run_election`
- 周期尝试绑定固定 RPC 地址 `tcp://127.0.0.1:49813`
- 绑定成功者成为本机 `DnsServer` 持有者;失败者继续只做 `DnsNode`
2. **主循环**`run`
- 监听配置变更/IP 变化/PeerInfo 更新。
- 维护 dirty 状态并按节奏发送 heartbeat。
### 4.2 快照构建与同步
`DnsPeerMgr::snapshot()` 组装快照:
- 本机 zones`dns_iter_zones()`(包含“自有专用 zone” + 用户配置 zone)。
- 远端 zones:从 peer RPC 拉取并缓存的 export zones。
- 本机 `addresses/listeners`:来自 `DnsConfig`
Node 发送 heartbeat 时:
- dirty 或首包 -> 带 `snapshot` 全量发送。
- 未 dirty -> 只发 `digest`(轻量心跳)。
- Server 返回 `resync=true` -> 立刻补发全量 snapshot。
### 4.3 服务端聚合与生效
`DnsNodeMgr` 收到 heartbeat 后:
- 若 snapshot digest 改变:更新节点缓存并标记 dirtycatalog/addresses/listeners 分开标记)。
- 若仅 digest 且本地无该节点或不一致:返回 `resync=true`
`DnsServer::run()` 有三个独立 reload 循环:
- `reload_catalog`:替换 `DynamicCatalog`
- `reload_addresses`:更新 hijack 地址,并尝试下发系统 DNS。
- `reload_listeners`:重绑 DNS listener socket。
这三个循环彼此解耦,避免单一失败阻塞全部 DNS 功能。
---
## 5. 数据面请求路径(DNS/ICMP 劫持)
`DnsServer` 作为 `NicPacketFilter` 挂入 packet pipeline
1. 检查目的 IP 是否命中 `addresses`
2. UDP
- 解析 DNS 请求 -> 投递给 `catalog.handle_request()`
- 用响应覆盖原 UDP payload,修正长度与校验和。
3. ICMP
- 对 EchoRequest 直接改写为 EchoReply。
4. 最后交换源/目的 IP,并把包回注到本机 peer pipeline。
这使得 `addresses` 不要求真实 bind/listen,也能作为“虚拟 DNS 入口地址”。
---
## 6. 可靠性与收敛机制
- **服务高可用(单机维度)**
- 任何实例都可竞选 Server;现任退出后其余实例会重试接管。
- **配置高效同步(全网维度)**
- `RoutePeerInfo` 只传播 DNS digest,不直接携带全量记录。
- digest 变化后才通过 RPC 拉取详情,降低路由泛洪压力。
- **自动过期清理**
- `DnsNodeMgr` 通过 `moka::Cache` TTL 自动淘汰失联节点配置(心跳过期)。
- **回环防护**
- 重建 zones 时会从 forwarders 中剔除本地 `addresses/listeners`,避免显式自环。
---
## 7. 当前实现状态(对应 plan.md)
从代码可见,以下主干能力已经落地:
- Node/Server 双角色、选举、心跳与 resync。
- 快照机制(zone/addresses/listeners)与 digest 驱动同步。
- 自有专用 zone 自动生成与 export。
- Catalog 动态替换、listener/address 分离热更新。
- UDP DNS 劫持 + ICMP 响应。
- forwarder 的本地回环剔除。
仍在计划中的重点:
- 系统 DNS 配置改造(尤其 Linux 路径统一与清理语义完善)。
- 更完整的单元测试覆盖与 CLI 状态输出。
---
## 8. 配置层详解(`config/*`
这一层负责把 TOML 配置映射成可校验、可传播、可计算 digest 的运行模型。
### 8.1 常量与默认值(`config/mod.rs`
- `DNS_DEFAULT_TLD = et.net.``domain` 缺省值。
- `DNS_DEFAULT_ADDRESS = udp://100.100.100.101:53``addresses` 缺省值。
- `DNS_SERVER_RPC_ADDR = tcp://127.0.0.1:49813`:本机 DNS Server 选举地址。
- `DNS_SERVER_ELECTION_INTERVAL = 5s`:选举重试周期。
- `DNS_SUPPORTED_PROTOCOLS = [Udp, Tcp]`:地址/转发器协议白名单。
### 8.2 `DnsConfig``config/dns.rs`
`DnsConfig``[dns]` 根配置,关键点如下:
- `zones: Vec<ZoneConfig>` 对应 `[[dns.zone]]`
- `policies: HashMap<LowerName, DnsPolicyConfig>``#[serde(flatten)]` 承接 `[dns."origin".import]` 形式策略。
- `name/domain` 组合 FQDN。
- `addresses/listeners` 使用 `NameServerAddrGroup`(支持 `ip``ip:port``udp://``tcp://` 解析)。
约束与语义:
- `deserialize_addresses()` 强制 `addresses` 只能是 UDP(与当前 hijack 数据面能力一致)。
- `get_name()`:若 `name` 为空,回退系统 hostname。
- `get_fqdn()`:将 `name` 拼接 `domain` 得到完整域名。
- `set_fqdn()`:反向拆分 FQDN 到 `name``domain`
### 8.3 `ZoneConfig` 与专用 Zone`config/zone.rs`
`ZoneConfig` 由两部分构成:
- `ZoneData`:用于 protobuf 传输(`id/origin/ttl/records/forwarders`)。
- `ZoneConfigInner`:配置层字段(含 policy)。
关键设计:
- `TryFrom<ZoneConfigInner> for ZoneConfig` 会立即调用 `Zone::try_from(&ZoneData)` 做语法校验,确保“能进配置就能进运行时”。
- `ZoneConfig::dedicated(...)` 用于自动生成“本节点专用 zone”:
- `origin = 节点 fqdn`
- records 自动填充 `@ IN A/AAAA ...`
- `policy.export = Some(default)`,默认可导出给 peers。
### 8.4 策略结构体现状(`config/policy.rs`
策略模型已就位,但功能并未完全落实到执行路径:
- `AclPolicy { whitelist, blacklist }`
- `FunctionalityPolicy { disabled }`
- `DnsPolicy { recursive }`
目前代码中的直接使用点主要是:
- `dns_export_config()` 只检查 `zone.policy.export.is_some()` 决定是否导出。
- `import/recursive/acl` 仍处于待完整落地状态(与 `plan.md` 的 TODO 对齐)。
### 8.5 `DnsGlobalCtxExt`:配置到发布面的桥(`config/dns.rs`
`GlobalCtx` 被扩展出 3 个关键方法:
- `dns_self_zone()`:基于当前 IP 与 FQDN 生成专用 zone。
- `dns_iter_zones()``self_zone + 用户配置 zones`
- `dns_export_config()`:从 `dns_iter_zones()` 中筛选可导出的 zones,并附加本机 `fqdn`
这三个方法是后续 `RoutePeerInfo.dns` digest 与 RPC 拉取的源头。
---
## 9. 节点控制面详解(`node.rs` + `peer_mgr.rs`
### 9.1 `DnsNode` 初始化与 RPC 注册
`DnsNode::new(...)` 会创建 `DnsPeerMgr`,并把 `DnsPeerMgrRpcServer` 注册到 peer RPC registry。
这使“我给别人提供 DNS 导出配置”与“我向别人拉取导出配置”在同一组件闭环。
### 9.2 选举循环(`DnsNode::run_election`
选举逻辑是“抢占固定地址”的单机 leader 机制:
1. 周期或被 `elect.notify_one()` 触发。
2. 尝试 `StandAloneServer(TcpTunnelListener(DNS_SERVER_RPC_ADDR)).serve()`
3. 绑定成功 -> 启动 `DnsServer`,注册 `DnsNodeMgrRpc`,并挂载 NIC packet pipeline。
4. `DnsServer` 退出后清理 pipeline,回到选举循环。
要点:
- 不依赖外部分布式锁,仅利用本机 socket 独占。
- 失败不是错误态,意味着“已有实例担任 Server”。
### 9.3 主循环(`DnsNode::run`
主循环负责“何时重建、何时发全量、何时触发重选举”:
- 维护 `HeartbeatRequest { id, digest, snapshot? }`
- 基于 `DirtyFlag` 动态调整心跳节奏:
- dirty 时更积极(`rr_interval`
- clean 时更快短轮询(`rr_interval / 8`
- 监听 `GlobalCtxEvent`
- `PeerInfoUpdated` -> 并发调用 `mgr.refresh(peer_id)`
- IP 变化、配置变化、事件丢失(lagged)-> `dirty.mark()`
- 心跳失败 -> 触发一次选举通知(可能是 Server 挂了)。
### 9.4 心跳协议(`DnsNode::heartbeat`
发送策略:
- 首次或 dirty -> `heartbeat.update(self.mgr.snapshot())`,发送全量 snapshot。
- 非 dirty -> 尽量只发 digest(轻量包)。
服务端响应:
- `resync = true` 时,客户端立刻再发一次带 snapshot 的心跳。
这实现了“正常轻量保活 + 状态漂移时快速自愈”。
### 9.5 `DnsPeerMgr`:远端配置拉取与去抖
`DnsPeerMgr` 核心职责:
- 本地缓存:`Cache<PeerId, DnsPeerInfo>`TTL = 3s)。
- `refresh(peer_id)`
- 先读路由里的 `route.dns` digest。
- 若与本地缓存一致则跳过 RPC。
- 不一致才调用 `fetch(peer_id)` 拉取 `GetExportConfigResponse`
- `snapshot()`:拼接
- 本机 zones`dns_iter_zones()`
- 所有远端缓存 zones
- 本机 addresses/listeners
这正是 `plan.md` 中“RoutePeerInfo 仅携带 hash,详情按需拉取”的落地实现。
---
## 10. 服务聚合与数据面详解(`node_mgr.rs` + `server.rs`
### 10.1 `DnsNodeMgr`:服务器侧状态机
`DnsNodeMgr` 保存每个 Node 的最新状态:
- `nodes: Cache<Uuid, DnsNodeInfo>`(TTL = 5s,心跳过期即自动淘汰)。
- `DnsNodeInfo = digest + zones + addresses + listeners`
- `dirty` 分三类:`catalog``addresses``listeners`
`heartbeat()` 判定逻辑:
- 请求带 snapshot
- 反序列化为 `DnsNodeInfo`
- digest 变化才更新缓存并打脏标记。
- 此分支返回 `resync = false`
- 请求不带 snapshot
- 若本地没有该 node 或 digest 不一致 -> `resync = true`
### 10.2 Catalog 构建(`DnsNodeMgr::catalog/collect_zones`
构建步骤:
1. 聚合全部节点 zones。
2. 追加 `Zone::system()` 作为 root zone。
3. 收集本地所有 `addresses + listeners` 形成 `local` 集合。
4. 遍历每个 zone 的 forwarders,剔除命中 `local` 的 nameserver(避免显式回环)。
5.`origin -> zone_handlers[]` 方式 `upsert` 到 Hickory `Catalog`
### 10.3 `DnsServer::run`:三路热重载
`DnsServer` 使用 3 个异步循环处理不同脏标记:
- `reload_catalog``DynamicCatalog::replace(...)` 原子替换目录。
- `reload_addresses`:更新劫持地址集合,并尝试下发系统 DNS。
- `reload_listeners`:重建 `ServerFuture` 的 UDP/TCP 监听 socket。
每路失败都会重新 `mark()` 自己,避免瞬时错误导致永久失效。
### 10.4 listener/address 的行为边界
- `listeners`:真正 bind 的服务地址;单个地址 bind 失败会打印错误并跳过,不导致整体停机。
- `addresses`:仅用于劫持匹配,不需要 bind;可用于 `no_tun=false` 下的虚拟 DNS 入口。
- `addresses``listeners` 分离,符合 `plan.md` 中“hijack 地址不等于监听地址”的设计。
### 10.5 NIC 数据面处理(`NicPacketFilter`
处理链:
1. `handle_ip_packet()` 解析 IPv4 头并检查目标 IP 是否属于 hijack 地址集合。
2. UDP 分支:
- `MessageRequest::from_bytes` 解包 DNS 查询。
- 交给 `catalog.handle_request(...)` 获取响应。
- 回填 payload,修正 UDP/IP 长度与 checksum。
3. ICMP 分支:
- EchoRequest 改写为 EchoReply。
4. 统一收尾:交换 src/dst IP,并把包路由回本机 `peer_id`
该路径让 DNS 响应无需经过用户态 socket recv/send,直接在 packet pipeline 内完成。
---
## 11. Zone 组装与权威链详解(`zone.rs`
### 11.1 `Zone` 运行时模型
`Zone` 包含:
- `id: Uuid`(来源于配置/网络数据)
- `origin: LowerName`
- `records: BTreeMap<RrKey, RecordSet>`
- `forward: Option<ForwardConfig>`
`PartialEq``forward` 使用自定义比较(只比较 nameserver 序列),避免与无关字段耦合。
### 11.2 反序列化与校验(`TryFrom<&ZoneData>`
转换过程:
1. 必须有 `id`,否则报错。
2. 用 Hickory `Parser` 解析 zone 文本(origin + RR)。
3.`forwarders` URL 转成 `NameServerAddr`,为空则 `forward=None`
这确保网络收到的 `ZoneData` 能直接映射成可执行 zone_handler。
### 11.3 ZoneHandler 构建策略
- `create_memory_zone_handler()`:仅当 records 非空时创建 `InMemoryZoneHandler`
- `create_forward_zone_handler()`:仅当 forward 非空时创建 `ForwardZoneHandler`
因此允许 3 种 zone 形态:
1. 纯记录(权威回答)
2. 纯转发(forward-only
3. 记录 + 转发(链式)
### 11.4 `ZoneGroup` 与同源链式行为
- `ZoneGroup::into_groups()``origin` 分组。
- `iter_zone_handlers()` 对每个 zone 按顺序产出:先 memory,再 forward。
- `DnsNodeMgr::catalog()` 把同 origin 的多个 zone zone_handler 以数组形式 `upsert`
结果是同 origin 下可自然形成 ChainedZoneHandler,不做“硬合并单 Zone”,与 `plan.md` 一致。
### 11.5 `Zone::system()` 的作用边界
`Zone::system()` 读取系统 resolver 作为 root zone forwarders。
在当前文档范围内仅关注它在 catalog 聚合中的语义:**兜底递归出口**。
---
## 12. 文档后续范围
后续若继续扩写,将集中在以下主题(不再展开 `system/*`):
1. 策略执行链路补齐:`import/recursive/acl` 如何从配置走到查询路径。
2. 测试矩阵梳理:单元测试、集成测试与故障注入测试的覆盖面。
3. CLI 状态输出:如何观测 node/server 角色、snapshot digest、zone 来源与健康状态。
---
## 13. 策略执行链路现状与缺口
本节专门回答一个容易误解的问题:**配置里有策略字段,不等于运行时已经完全执行**。
### 13.1 已生效的策略相关行为
当前代码中,和策略直接相关且已生效的路径主要有一条:
- `GlobalCtx::dns_export_config()` 在导出 zones 时仅检查:
- `zone.policy.export.is_some()`
也就是说,当前“导出/不导出”是可工作的,但粒度仍偏粗。
### 13.2 已建模但尚未完整落地的策略字段
以下字段在 `config/policy.rs` 已定义,但执行链路尚未完全打通:
- `import.whitelist / import.blacklist`
- `import.disabled`
- `import.recursive`
- `export` 内更细粒度 ACL
从调用路径看:
- `DnsPeerMgr::snapshot()` 只做本地 + 远端 zones 拼接,不做 import/export ACL 过滤。
- `DnsNodeMgr::collect_zones()` 只做聚合与回环剔除,不做来源级策略裁剪。
- `DnsServer::handle_ip_packet()` 是纯查询执行,不做请求来源与策略绑定。
### 13.3 代码中的明确信号(TODO)
当前有两个关键 TODO 信号:
- `dns_export_config()` 里标注了 `TODO: check policies of parent zones`
- `policy.rs``AclPolicy``recursive` 旁边保留了 TODO 注释。
这说明作者已经把策略模型前置到配置层,但执行面仍属于“进行中”。
### 13.4 文档使用建议(给维护者)
在策略彻底落地前,建议把语义按两层理解:
1. **已可依赖**`zone.policy.export.is_some()` 控制是否导出。
2. **暂不可依赖**import/export ACL、recursive、disabled 的全链路行为。
---
## 14. 测试体系与覆盖面
本模块测试不是集中在一个文件,而是“按组件就地内嵌”。
### 14.1 测试分布
- `dns/tests.rs`:测试基建与辅助函数(构造环境、启动 `DnsNode`、DNS 查询断言工具)。
- `dns/server.rs`:数据面与 server 行为主测试集。
- `dns/node_mgr.rs`:聚合 catalog 的基本可用性测试。
- `dns/zone.rs`:配置解析、记录转换、zone_handler 装配测试。
> 说明:`system/*` 也有测试,但本轮文档按约定不展开。
### 14.2 `server.rs` 覆盖要点
`server.rs` 的测试集中验证了以下核心行为:
- `DynamicCatalog::replace()` 可安全替换。
- hijack 判定:`is_hijacked_ip` / `is_hijacked_addr`
- ICMP 改写:EchoRequest -> EchoReply。
- UDP DNS 包内联处理:解析请求、生成应答、回填 payload。
- 一个基础端到端路径:真实 UDP listener + Hickory client 查询。
这些测试对应模块里最复杂、最容易回归的包处理逻辑。
### 14.3 `node_mgr.rs` 覆盖要点
`node_mgr.rs` 的测试重点是:
- 人工插入节点 zone 后,`catalog()` 能查到预期记录。
它验证了“快照聚合 -> Catalog 查询可用”的最小闭环,但尚未覆盖复杂心跳时序、TTL 过期后的清理行为。
### 14.4 `zone.rs` 覆盖要点
`zone.rs` 的测试覆盖了:
- TOML `DnsConfig` 解析。
- `ZoneConfig -> ZoneData -> Zone` 转换链。
- record 解析/TTL 基本行为。
- memory/forward zone_handler 构建,以及通过 server 查询验证。
该测试更多是“模型与解析正确性”,不是策略执行链路完整验证。
### 14.5 当前测试缺口
结合 `plan.md` 与现有测试,仍建议补充:
- `DnsNode` 心跳 + resync + 重选举的并发时序测试。
- `DnsNodeMgr` TTL 过期淘汰与脏标记联动测试。
- 策略字段(import/export ACL、recursive)的行为测试。
- 多 peer、同 origin 多 zone 的优先级/去重回归测试。
---
## 15. CLI 与可观测性现状
### 15.1 CLI 现状
从当前代码看,`easytier/src/easytier-cli.rs` 没有 DNS 专用子命令。
因此“查看 DNS 子系统状态”主要依赖日志与通用状态接口,而非专门 CLI 面板。
### 15.2 日志观测点(已存在)
`dns` 子系统已经布置了较多 `tracing` 埋点,关键入口包括:
- `DnsNode election loop`
- `DnsNode main loop`
- `DnsServer main loop`
- `DnsNodeMgr::heartbeat`(含来源 id 与 snapshot 信息)
可用于定位:
- 当前实例是否赢得选举。
- 心跳是否失败、是否触发 `resync`
- catalog/addresses/listeners 是否持续重载失败。
### 15.3 当前可观测性短板
- 缺少 DNS 专项 CLI 展示:
- 本机角色(Node/Server
- 当前 snapshot digest
- zone 来源与数量
- 监听地址与 hijack 地址状态
- 缺少结构化指标(metrics),目前偏日志驱动排障。
### 15.4 建议的最小可观测面
后续若补 CLI,可先实现一个最小 DNS 状态视图:
1. 角色与选举状态(是否持有 `DNS_SERVER_RPC_ADDR`)。
2. 最近心跳时间、`resync` 次数。
3. 已装载 zone 数量(按本地/远端分组)。
4. listeners 与 addresses 当前集合。
该视图不改变数据面行为,但能显著降低线上排障成本。
+570
View File
@@ -0,0 +1,570 @@
use crate::common::global_ctx::ArcGlobalCtx;
use crate::dns::node_mgr::DnsNodeMgr;
use crate::dns::system;
use crate::dns::utils::addr::NameServerAddr;
#[cfg(feature = "tun")]
use crate::instance::instance::{ArcNicCtx, NicCtx};
use crate::peers::peer_manager::PeerManager;
use crate::proto::dns::DnsNodeMgrRpcServer;
use crate::proto::rpc_impl::standalone::StandAloneServer;
use crate::tunnel::common::bind;
use crate::tunnel::tcp::TcpTunnelListener;
use crate::utils::task::CancellableTask;
use anyhow::Context;
use derivative::Derivative;
use guarden::guarded;
use hickory_net::runtime::Time;
use hickory_net::xfer::Protocol;
use hickory_server::{
Server,
server::{Request, RequestHandler, ResponseHandler, ResponseInfo},
zone_handler::Catalog,
};
use itertools::chain;
use parking_lot::RwLock;
use std::collections::HashSet;
use std::net::IpAddr;
use std::{sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use tracing::{Instrument, instrument};
#[derive(Clone)]
struct DynamicCatalog {
inner: Arc<tokio::sync::RwLock<Catalog>>,
}
impl DynamicCatalog {
fn new() -> Self {
Self {
inner: Arc::new(tokio::sync::RwLock::new(Catalog::new())),
}
}
async fn replace(&self, new: Catalog) {
*self.inner.write().await = new;
}
}
#[async_trait::async_trait]
impl RequestHandler for DynamicCatalog {
async fn handle_request<R: ResponseHandler, T: Time>(
&self,
request: &Request,
response_handle: R,
) -> ResponseInfo {
self.inner
.read()
.await
.handle_request::<_, T>(request, response_handle)
.await
}
}
#[derive(Derivative)]
#[derivative(Debug)]
pub struct DnsServer {
mgr: Arc<DnsNodeMgr>,
#[cfg(feature = "tun")]
nic_ctx: ArcNicCtx, // TODO: REMOVE THIS
peer_mgr: Arc<PeerManager>,
global_ctx: ArcGlobalCtx,
#[derivative(Debug = "ignore")]
catalog: DynamicCatalog,
runtime: Mutex<Option<CancellableTask<()>>>,
bindings: RwLock<HashSet<NameServerAddr>>,
addresses: RwLock<HashSet<NameServerAddr>>,
listeners: RwLock<HashSet<NameServerAddr>>,
}
const DNS_SERVER_TCP_TIMEOUT: Duration = Duration::from_secs(5);
const DNS_SERVER_TCP_BUFFER_SIZE: usize = 32;
impl DnsServer {
pub fn new(
peer_mgr: Arc<PeerManager>,
global_ctx: ArcGlobalCtx,
#[cfg(feature = "tun")] nic_ctx: ArcNicCtx, // TODO: REMOVE THIS
) -> Self {
Self {
mgr: Arc::new(DnsNodeMgr::new()),
#[cfg(feature = "tun")]
nic_ctx,
peer_mgr,
global_ctx,
catalog: DynamicCatalog::new(),
runtime: Default::default(),
bindings: Default::default(),
listeners: Default::default(),
addresses: Default::default(),
}
}
pub fn register(&self, rpc: &StandAloneServer<TcpTunnelListener>) {
rpc.registry()
.register(DnsNodeMgrRpcServer::new_arc(self.mgr.clone()), "");
}
#[cfg(feature = "tun")]
async fn update_system(&self, nameservers: &HashSet<NameServerAddr>) -> anyhow::Result<()> {
let nic_ctx = self.nic_ctx.lock().await;
let nic_ctx = nic_ctx
.as_ref()
.and_then(|nic_ctx| nic_ctx.downcast_ref::<NicCtx>())
.with_context(|| "failed to get NicCtx")?;
let ifname = nic_ctx
.ifname()
.await
.with_context(|| "failed to get interface name from NicCtx")?;
let system = system::get(&ifname)?.with_context(|| "failed to get system configurator")?;
let config = self.global_ctx.config.get_dns();
let domain = vec![config.domain.to_string()];
system.set_dns(&system::SystemConfig {
nameservers: nameservers
.iter()
.filter_map(|a| {
(a.protocol == Protocol::Udp && a.addr.port() == 53)
.then_some(a.addr.ip().to_string())
})
.collect(),
search_domains: domain.clone(),
match_domains: domain
.into_iter()
.chain(config.zones.iter().map(|z| z.origin.to_string()))
.collect(),
})?;
Ok(())
}
#[instrument(skip_all)]
async fn rebind(&self) -> anyhow::Result<bool> {
let Ok(mut runtime) = self.runtime.try_lock() else {
return Ok(false);
};
let mut bindings = {
let current = self.bindings.read();
let bindings = chain(
self.addresses.read().iter().cloned(),
self.listeners.read().iter().cloned(),
)
.collect();
if *current == bindings {
tracing::info!("bindings unchanged, no need to rebind");
return Ok(false);
}
bindings
};
if let Some(runtime) = runtime.take() {
runtime.stop(None).await?;
}
let mut server = Server::new(self.catalog.clone());
bindings.retain(|binding| {
let addr = binding.addr;
tracing::info!(?addr, "binding");
match binding.protocol {
Protocol::Tcp => bind().addr(addr).call().map(|s| {
server.register_listener(s, DNS_SERVER_TCP_TIMEOUT, DNS_SERVER_TCP_BUFFER_SIZE)
}),
Protocol::Udp => bind().addr(addr).call().map(|s| server.register_socket(s)),
_ => unimplemented!(),
}
.inspect_err(|error| tracing::error!(?addr, ?error, "failed to bind"))
.is_ok()
});
let token = server.shutdown_token().clone();
let handle = tokio::spawn(
async move {
server
.block_until_done()
.await
.unwrap_or_else(|e| tracing::error!("DNS server exited with error: {:?}", e));
}
.instrument(tracing::info_span!("DNS server backend runtime")),
);
*runtime = Some(CancellableTask::with_handle(token, handle));
#[cfg(feature = "tun")]
if let Err(error) = self.update_system(&bindings).await {
tracing::error!(?error, "failed to update system DNS settings");
}
*self.bindings.write() = bindings;
Ok(true)
}
#[instrument(skip_all)]
async fn reload_addresses(&self) -> anyhow::Result<()> {
let addresses = self.mgr.iter_addresses().collect();
let removed = {
let current = self.addresses.read();
if *current == addresses {
tracing::info!("addresses unchanged, no need to reload");
return Ok(());
}
current
.difference(&addresses)
.cloned()
.collect::<HashSet<_>>()
};
tracing::info!(?addresses, "reloading");
#[cfg(feature = "tun")]
{
let nic_ctx = self.nic_ctx.lock().await;
if let Some(nic_ctx) = nic_ctx
.as_ref()
.and_then(|nic_ctx| nic_ctx.downcast_ref::<NicCtx>())
{
for addr in &addresses {
let ip = addr.addr.ip();
if let Err(error) = match ip {
IpAddr::V4(ipv4) => nic_ctx.add_ipv4_to_tun_device(ipv4.into()).await,
IpAddr::V6(ipv6) => nic_ctx.add_ipv6_to_tun_device(ipv6.into()).await,
} {
tracing::error!(?addr, ?error, "failed to add address to tun device");
}
}
for addr in removed {
let ip = addr.addr.ip();
if let Err(error) = match ip {
IpAddr::V4(ipv4) => nic_ctx.remove_ipv4_from_tun_device(ipv4.into()).await,
IpAddr::V6(ipv6) => nic_ctx.remove_ipv6_from_tun_device(ipv6.into()).await,
} {
tracing::error!(?addr, ?error, "failed to remove address from tun device");
}
}
}
}
*self.addresses.write() = addresses;
self.rebind().await?;
Ok(())
}
#[instrument(skip_all)]
async fn reload_listeners(&self) -> anyhow::Result<()> {
let listeners = self.mgr.iter_listeners().collect();
if *self.listeners.read() == listeners {
tracing::info!("listeners unchanged, no need to reload");
return Ok(());
}
tracing::info!(?listeners, "reloading");
*self.listeners.write() = listeners;
self.rebind().await?;
Ok(())
}
#[instrument(skip_all, name = "DnsServer main loop")]
pub async fn run(&self, token: CancellationToken) {
let dirty = &self.mgr.dirty;
let runtime = None::<CancellableTask<()>>;
#[cfg(feature = "tun")]
guarded! {
system_guard => [
nic_ctx = self.nic_ctx.clone(),
]
async move {
if let Some(nic_ctx) = nic_ctx
.lock()
.await
.as_ref()
.and_then(|nic_ctx| nic_ctx.downcast_ref::<NicCtx>())
&& let Some(system) = nic_ctx
.ifname()
.await
.and_then(|ifname| system::get(&ifname).ok())
.flatten()
{
let _ = system.clean();
}
}
}
guarded! {
runtime_guard => [
mut runtime,
]
async move {
if let Some(runtime) = runtime.take() {
let _ = runtime.stop(Some(Duration::from_secs(1))).await;
}
}
}
let reload_catalog = async {
loop {
dirty.catalog.wait().await;
if dirty.catalog.reset() {
self.catalog.replace(self.mgr.catalog()).await;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
};
let reload_addresses = async {
loop {
dirty.addresses.wait().await;
if dirty.addresses.reset()
&& let Err(error) = self.reload_addresses().await
{
tracing::error!(?error, "failed to reload addresses");
dirty.addresses.mark();
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
};
let reload_listeners = async {
loop {
dirty.listeners.wait().await;
if dirty.listeners.reset()
&& let Err(error) = self.reload_listeners().await
{
tracing::error!(?error, "failed to reload listeners");
dirty.listeners.mark();
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
};
tokio::select!(
_ = token.cancelled() => {
tracing::info!("DnsServer received shutdown signal, exiting server loop");
}
_ = reload_catalog => {},
_ = reload_addresses => {},
_ = reload_listeners => {},
);
#[cfg(feature = "tun")]
system_guard.trigger().await;
runtime_guard.trigger().await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::peers::tests::create_mock_peer_manager;
use hickory_net::client::{Client, ClientHandle};
use hickory_net::runtime::TokioRuntimeProvider;
use hickory_net::udp::UdpClientStream;
use hickory_proto::op::{Message, MessageType, OpCode, Query};
use hickory_proto::rr::{DNSClass, Name, RData, Record, RecordType, rdata};
use hickory_proto::serialize::binary::BinEncodable;
use hickory_server::store::in_memory::InMemoryZoneHandler;
use hickory_server::zone_handler::ZoneType;
use hickory_server::zone_handler::{AxfrPolicy, Catalog};
use pnet::packet::icmp::{IcmpTypes, MutableIcmpPacket};
use pnet::packet::ipv4::MutableIpv4Packet;
use pnet::packet::udp::MutableUdpPacket;
use pnet::packet::{MutablePacket, icmp, ipv4, udp};
use std::net::Ipv4Addr;
use std::str::FromStr;
use std::time::Duration;
use tokio::time::sleep;
/// Build a `Catalog` containing a single A record: `test.example.com -> 1.2.3.4`.
fn build_test_catalog() -> Catalog {
let origin = Name::from_str("example.com.").unwrap();
let mut zone_handler = InMemoryZoneHandler::<TokioRuntimeProvider>::empty(
origin.clone(),
ZoneType::Primary,
AxfrPolicy::default(),
);
let record = Record::from_rdata(
Name::from_str("test.example.com.").unwrap(),
60,
RData::A(rdata::a::A(Ipv4Addr::new(1, 2, 3, 4))),
);
let rr_key =
hickory_proto::rr::RrKey::new(record.name.clone().into(), record.record_type());
let mut rr_set =
hickory_proto::rr::RecordSet::new(record.name.clone(), record.record_type(), 0);
rr_set.insert(record, 0);
zone_handler
.records_get_mut()
.insert(rr_key, Arc::new(rr_set));
let mut catalog = Catalog::new();
catalog.upsert(
origin.into(),
vec![Arc::new(zone_handler) as Arc<dyn hickory_server::zone_handler::ZoneHandler>],
);
catalog
}
/// Create a test `DnsServer` with `create_mock_peer_manager()`.
async fn create_test_server() -> Arc<DnsServer> {
let peer_mgr = create_mock_peer_manager().await;
let global_ctx = peer_mgr.get_global_ctx();
Arc::new(DnsServer::new(
peer_mgr,
global_ctx,
#[cfg(feature = "tun")]
ArcNicCtx::default(),
))
}
/// Build a raw IPv4 packet (as `Vec<u8>`) carrying the given L4 payload bytes.
/// `protocol` selects ICMP / UDP etc.
fn build_ipv4_packet(
src: Ipv4Addr,
dst: Ipv4Addr,
protocol: pnet::packet::ip::IpNextHeaderProtocol,
l4_payload: &[u8],
) -> Vec<u8> {
let ip_header_len = 20usize;
let total_len = ip_header_len + l4_payload.len();
let mut buf = vec![0u8; total_len];
{
let mut ip = MutableIpv4Packet::new(&mut buf).unwrap();
ip.set_version(4);
ip.set_header_length(5); // 20 bytes
ip.set_total_length(total_len as u16);
ip.set_ttl(64);
ip.set_next_level_protocol(protocol);
ip.set_source(src);
ip.set_destination(dst);
ip.payload_mut().copy_from_slice(l4_payload);
ip.set_checksum(ipv4::checksum(&ip.to_immutable()));
}
buf
}
/// Build ICMP Echo Request payload (8 bytes minimum).
fn build_icmp_echo_request() -> Vec<u8> {
let mut buf = vec![0u8; 8];
{
let mut icmp_pkt = MutableIcmpPacket::new(&mut buf).unwrap();
icmp_pkt.set_icmp_type(IcmpTypes::EchoRequest);
icmp_pkt.set_icmp_code(icmp::IcmpCode::new(0));
icmp_pkt.set_checksum(icmp::checksum(&icmp_pkt.to_immutable()));
}
buf
}
/// Build a minimal DNS query message for `name` and encode it to bytes.
fn build_dns_query_bytes(name: &str) -> Vec<u8> {
let mut msg = Message::new(0x1234, MessageType::Query, OpCode::Query);
msg.metadata.recursion_desired = true;
let mut query = Query::new();
query.set_name(Name::from_str(name).unwrap());
query.set_query_type(RecordType::A);
query.set_query_class(DNSClass::IN);
msg.add_query(query);
msg.to_bytes().unwrap().to_vec()
}
/// Build a UDP packet carrying `payload`, with given src/dst ports.
fn build_udp_packet(
src_port: u16,
dst_port: u16,
payload: &[u8],
src_ip: Ipv4Addr,
dst_ip: Ipv4Addr,
) -> Vec<u8> {
let udp_len = 8 + payload.len();
let mut buf = vec![0u8; udp_len];
{
let mut udp_pkt = MutableUdpPacket::new(&mut buf).unwrap();
udp_pkt.set_source(src_port);
udp_pkt.set_destination(dst_port);
udp_pkt.set_length(udp_len as u16);
udp_pkt.payload_mut().copy_from_slice(payload);
udp_pkt.set_checksum(udp::ipv4_checksum(
&udp_pkt.to_immutable(),
&src_ip,
&dst_ip,
));
}
buf
}
async fn wait_until(mut f: impl FnMut() -> bool) {
for _ in 0..80 {
if f() {
return;
}
sleep(Duration::from_millis(50)).await;
}
panic!("condition not met in time");
}
// ─── Tests ───────────────────────────────────────────────────────────
/// Full end-to-end test: start a real DNS UDP listener via `ServerFuture`,
/// send a query with a `hickory_client`, and verify the response.
#[tokio::test]
async fn should_resolve_record_via_real_udp_listener() {
use hickory_server::Server;
use tokio::net::UdpSocket;
use tokio::time::timeout;
// Build a catalog with test.example.com -> 1.2.3.4.
let catalog = build_test_catalog();
// Bind to a random port.
let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let addr = socket.local_addr().unwrap();
let mut server = Server::new(catalog);
server.register_socket(socket);
let shutdown_token = server.shutdown_token().clone();
tokio::spawn(async move {
server.block_until_done().await.ok();
});
// Send a real DNS query using hickory_client.
let stream = UdpClientStream::builder(addr, TokioRuntimeProvider::default()).build();
let (mut client, bg) = Client::<TokioRuntimeProvider>::from_sender(stream);
tokio::spawn(bg);
let response = timeout(
Duration::from_secs(2),
client.query(
Name::from_str("test.example.com.").unwrap(),
DNSClass::IN,
RecordType::A,
),
)
.await
.expect("query timeout")
.expect("query failed");
assert!(!response.answers.is_empty(), "should get answers");
let a_record = &response.answers[0];
if let RData::A(a) = a_record.data {
assert_eq!(a.0, Ipv4Addr::new(1, 2, 3, 4));
} else {
panic!("expected A record, got {:?}", a_record.data);
}
// Shutdown the server.
shutdown_token.cancel();
}
}
@@ -6,13 +6,13 @@ use std::{
path::Path,
};
use super::{OSConfig, SystemConfig};
use super::{SystemConfig, SystemConfigurator};
const MAC_RESOLVER_FILE_HEADER: &str = "# Added by easytier\n";
const ETC_RESOLVER: &str = "/etc/resolver";
const ETC_RESOLV_CONF: &str = "/etc/resolv.conf";
#[derive(Default)]
#[derive(Clone, Default)]
pub struct DarwinConfigurator {}
impl DarwinConfigurator {
pub fn new() -> Self {
@@ -27,7 +27,7 @@ impl DarwinConfigurator {
true
}
pub fn do_set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
pub fn do_set_dns(&self, cfg: &SystemConfig) -> io::Result<()> {
fs::create_dir_all(ETC_RESOLVER)?;
let mut keep = HashSet::new();
@@ -104,12 +104,12 @@ impl DarwinConfigurator {
}
}
impl SystemConfig for DarwinConfigurator {
fn set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
impl SystemConfigurator for DarwinConfigurator {
fn set_dns(&self, cfg: &SystemConfig) -> io::Result<()> {
self.do_set_dns(cfg)
}
fn close(&self) -> io::Result<()> {
fn clean(&self) -> io::Result<()> {
self.do_close()
}
}
+42
View File
@@ -0,0 +1,42 @@
#[allow(unused_imports)]
use crate::utils::BoxExt;
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(all(target_os = "macos", not(feature = "macos-ne")))]
pub mod macos;
#[derive(Default, Debug)]
pub struct SystemConfig {
pub nameservers: Vec<String>,
pub search_domains: Vec<String>,
pub match_domains: Vec<String>,
}
pub trait SystemConfigurator: Send + Sync {
fn set_dns(&self, cfg: &SystemConfig) -> std::io::Result<()>;
fn clean(&self) -> std::io::Result<()>;
}
// TODO: move this to nic mod
pub fn get(
#[allow(unused_variables)] interface: &str,
) -> Result<Option<Box<dyn SystemConfigurator>>, anyhow::Error> {
cfg_select! {
target_os = "windows" => {
use crate::dns::system::windows::WindowsDNSManager;
Ok(Some(WindowsDNSManager::new(interface)?.boxed()))
}
all(target_os = "macos", not(feature = "macos-ne")) => {
use crate::dns::system::macos::DarwinConfigurator;
Ok(Some(DarwinConfigurator::new().boxed()))
}
_ => Ok(None)
}
}
@@ -6,7 +6,7 @@ use winreg::RegKey;
use crate::common::ifcfg::RegistryManager;
use super::{OSConfig, SystemConfig};
use super::{SystemConfig, SystemConfigurator};
pub fn is_windows_10_or_better() -> io::Result<bool> {
let hklm = winreg::enums::HKEY_LOCAL_MACHINE;
@@ -19,6 +19,7 @@ pub fn is_windows_10_or_better() -> io::Result<bool> {
}
// 假设 interface_guid 是你的网络接口 GUID
#[derive(Clone)]
pub struct InterfaceControl {
interface_guid: String,
}
@@ -125,6 +126,7 @@ impl InterfaceControl {
}
}
#[derive(Clone)]
pub struct WindowsDNSManager {
tun_dev_name: String,
interface_control: InterfaceControl,
@@ -146,8 +148,8 @@ impl WindowsDNSManager {
}
}
impl SystemConfig for WindowsDNSManager {
fn set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
impl SystemConfigurator for WindowsDNSManager {
fn set_dns(&self, cfg: &SystemConfig) -> io::Result<()> {
self.set_primary_dns(
&cfg.nameservers
.iter()
@@ -158,44 +160,48 @@ impl SystemConfig for WindowsDNSManager {
Ok(())
}
fn close(&self) -> io::Result<()> {
fn clean(&self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)]
#[cfg(all(test, target_os = "windows", feature = "magic-dns", feature = "tun"))]
mod tests {
use cidr::Ipv4Inet;
use std::net::IpAddr;
#[cfg(target_os = "windows")]
#[tokio::test]
async fn test_windows_set_primary_server() {
use std::{net::Ipv4Addr, str::FromStr as _, time::Duration};
use std::{str::FromStr as _, sync::Arc, time::Duration};
use tokio_util::sync::CancellationToken;
use crate::instance::dns_server::{
runner::DnsRunner,
tests::{check_dns_record, prepare_env},
use crate::dns::{
config::DNS_DEFAULT_ADDRESSES,
tests::{prepare_env, start_dns_node},
};
use crate::instance::proxy_cidrs_monitor::ProxyCidrsMonitor;
use crate::instance::virtual_nic::NicCtx;
use crate::peers::peer_manager::PeerManager;
let tun_ip = Ipv4Inet::from_str("10.144.144.10/24").unwrap();
let (peer_mgr, virtual_nic) = prepare_env("test1", tun_ip).await;
let (peer_mgr, virtual_nic): (Arc<PeerManager>, NicCtx) =
prepare_env("test1", tun_ip).await;
let tun_name = virtual_nic.ifname().await.unwrap();
// prepare_env does not run full Instance::run, so start the monitor explicitly in test.
let _monitor = ProxyCidrsMonitor::new(peer_mgr.clone(), peer_mgr.get_global_ctx()).start();
let mut dns_node = start_dns_node(peer_mgr, virtual_nic);
println!("dev_name: {}", tun_name);
let fake_ip = Ipv4Addr::from_str("100.100.100.101").unwrap();
let mut dns_runner = DnsRunner::new(peer_mgr, Some(tun_name.clone()), tun_ip, fake_ip);
let fake_ip = match DNS_DEFAULT_ADDRESSES[0].addr.ip() {
IpAddr::V4(ip) => ip,
IpAddr::V6(ip) => panic!("unexpected ipv6 default dns address in test: {ip}"),
};
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
let t = tokio::spawn(async move {
dns_runner.run(cancel_token_clone).await;
});
// windows is slow to add a ip address, wait for a longer time for dns server ready ,with ping
// Windows may take a while to attach the test IP; wait until ping succeeds.
let now = std::time::Instant::now();
while now.elapsed() < Duration::from_secs(15) {
let mut ping_ready = false;
while now.elapsed() < Duration::from_secs(5) {
tokio::time::sleep(Duration::from_secs(1)).await;
if let Ok(o) = tokio::process::Command::new("ping")
.arg("-n")
@@ -207,11 +213,49 @@ mod tests {
.await
&& o.status.success()
{
ping_ready = true;
break;
}
}
if !ping_ready {
tracing::warn!(
"dns test endpoint {} did not respond to ping in time; continue with dns checks",
fake_ip
);
}
check_dns_record(&fake_ip, "test1.et.net", "10.144.144.10").await;
// First verify the DNS node can answer queries when explicitly targeting it.
let direct_lookup_deadline = std::time::Instant::now() + Duration::from_secs(3);
let mut direct_lookup_ok = false;
let mut last_direct_output = String::new();
while std::time::Instant::now() < direct_lookup_deadline {
let result = tokio::time::timeout(
Duration::from_secs(3),
tokio::process::Command::new("nslookup")
.arg("test1.et.net")
.arg(fake_ip.to_string())
.output(),
)
.await;
if let Ok(Ok(ret)) = result {
let output = String::from_utf8_lossy(&ret.stdout).to_string();
println!("direct nslookup output: {}", output);
if ret.status.success() && output.contains("10.144.144.10") {
direct_lookup_ok = true;
break;
}
last_direct_output = output;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
assert!(
direct_lookup_ok,
"direct nslookup against dns server did not return expected IP in time; last output: {last_direct_output}"
);
let dns_mgr = super::WindowsDNSManager::new(&tun_name).unwrap();
println!("dev_name: {}", tun_name);
@@ -219,27 +263,44 @@ mod tests {
dns_mgr
.interface_control
.set_primary_dns(
&["100.100.100.101".parse().unwrap()],
&[".et.net.".to_string()],
)
.set_primary_dns(&[fake_ip.into()], &[".et.net.".to_string()])
.unwrap();
dns_mgr.interface_control.flush_dns().unwrap();
tracing::info!("check dns record with nslookup");
// nslookup should return 10.144.144.10
let ret = tokio::process::Command::new("nslookup")
.arg("test1.et.net")
.output()
.await
.expect("failed to execute process");
assert!(ret.status.success());
let output = String::from_utf8_lossy(&ret.stdout);
println!("nslookup output: {}", output);
assert!(output.contains("10.144.144.10"));
// nslookup should eventually return 10.144.144.10 after system DNS setting propagation.
let lookup_deadline = std::time::Instant::now() + Duration::from_secs(30);
let mut lookup_ok = false;
let mut last_output = String::new();
cancel_token.cancel();
let _ = t.await;
while std::time::Instant::now() < lookup_deadline {
let result = tokio::time::timeout(
Duration::from_secs(3),
tokio::process::Command::new("nslookup")
.arg("test1.et.net")
.output(),
)
.await;
if let Ok(Ok(ret)) = result {
let output = String::from_utf8_lossy(&ret.stdout).to_string();
println!("nslookup output: {}", output);
if ret.status.success() && output.contains("10.144.144.10") {
lookup_ok = true;
break;
}
last_output = output;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
assert!(
lookup_ok,
"nslookup did not return expected IP in time; last output: {last_output}"
);
dns_node.stop().await.unwrap();
}
}
+984
View File
@@ -0,0 +1,984 @@
#![cfg(all(feature = "magic-dns", feature = "tun"))]
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::str::FromStr as _;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::common::config::TomlConfigLoader;
use crate::common::global_ctx::GlobalCtx;
use crate::common::global_ctx::tests::get_mock_global_ctx;
use crate::connector::udp_hole_punch::tests::replace_stun_info_collector;
use crate::dns::node::DnsNode;
use crate::dns::peer_mgr::DnsPeerMgr;
use crate::instance::instance::ArcNicCtx;
use crate::instance::virtual_nic::NicCtx;
use crate::peers::create_packet_recv_chan;
use crate::peers::peer_manager::{PeerManager, RouteAlgoType};
use crate::peers::tests::{connect_peer_manager, wait_route_appear};
use crate::proto::common::{NatType, Url};
use crate::proto::dns::{DnsSnapshot, HeartbeatRequest, ZoneData};
use cidr::Ipv4Inet;
use hickory_net::client::{Client, ClientHandle};
use hickory_net::runtime::TokioRuntimeProvider;
use hickory_net::udp::UdpClientStream;
use hickory_net::xfer::Protocol;
use hickory_proto::op::{Message, MessageType, OpCode, Query};
use hickory_proto::rr;
use hickory_proto::rr::{DNSClass, Name, RData, RecordType};
use hickory_proto::serialize::binary::{BinEncodable, BinEncoder};
use hickory_server::server::Request;
use maplit::hashset;
use tokio::sync::Notify;
use uuid::Uuid;
// TODO: move to system::tests
pub async fn prepare_env(dns_name: &str, tun_ip: Ipv4Inet) -> (Arc<PeerManager>, NicCtx) {
prepare_env_with_tld_dns_zone(dns_name, tun_ip, None).await
}
pub async fn prepare_env_with_tld_dns_zone(
dns_name: &str,
tun_ip: Ipv4Inet,
tld_dns_zone: Option<&str>,
) -> (Arc<PeerManager>, NicCtx) {
let ctx = get_mock_global_ctx();
ctx.set_hostname(dns_name.to_owned());
ctx.set_ipv4(Some(tun_ip));
let mut dns_config = ctx.config.get_dns().into_raw();
dns_config.name = Some(dns_name.parse().unwrap());
if let Some(zone) = tld_dns_zone {
dns_config.domain = Some(zone.parse().expect("invalid test dns zone"));
}
ctx.config.set_dns(dns_config.into());
let (s, r) = create_packet_recv_chan();
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, ctx, s));
peer_mgr.run().await.unwrap();
replace_stun_info_collector(peer_mgr.clone(), NatType::PortRestricted);
let r = Arc::new(tokio::sync::Mutex::new(r));
let mut virtual_nic = NicCtx::new(
peer_mgr.get_global_ctx(),
&peer_mgr,
r,
Arc::new(Notify::new()),
);
virtual_nic.run(Some(tun_ip), None).await.unwrap();
(peer_mgr, virtual_nic)
}
pub fn start_dns_node(peer_mgr: Arc<PeerManager>, virtual_nic: NicCtx) -> DnsNode {
let global_ctx = peer_mgr.get_global_ctx();
let nic_ctx: ArcNicCtx = Arc::new(tokio::sync::Mutex::new(Some(Box::new(virtual_nic))));
let mut node = DnsNode::new(peer_mgr, global_ctx, nic_ctx);
node.start();
node
}
pub fn start_dns_node_without_nic(peer_mgr: Arc<PeerManager>) -> DnsNode {
let global_ctx = peer_mgr.get_global_ctx();
let nic_ctx: ArcNicCtx = Arc::new(tokio::sync::Mutex::new(None));
let mut node = DnsNode::new(peer_mgr, global_ctx, nic_ctx);
node.start();
node
}
pub async fn prepare_env_from_config_str(config_str: &str) -> Arc<PeerManager> {
let config = TomlConfigLoader::new_from_str(config_str).expect("invalid test config");
let ctx = Arc::new(GlobalCtx::new(config));
let (s, _r) = create_packet_recv_chan();
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, ctx, s));
peer_mgr.run().await.unwrap();
replace_stun_info_collector(peer_mgr.clone(), NatType::PortRestricted);
peer_mgr
}
pub fn zone_data_a(origin: &str, record: &str) -> ZoneData {
zone_data_a_with_forwarders(origin, record, vec![])
}
pub fn zone_data_a_with_forwarders(origin: &str, record: &str, forwarders: Vec<&str>) -> ZoneData {
ZoneData::new(
&origin.parse().unwrap(),
60,
[format!("@ IN A {record}")],
forwarders.into_iter().map(|f| Url::from_str(f).unwrap()),
hashset! {},
)
}
pub fn dns_snapshot_with(
zones: Vec<ZoneData>,
addresses: Vec<&str>,
listeners: Vec<&str>,
) -> DnsSnapshot {
DnsSnapshot {
zones,
addresses: addresses
.into_iter()
.map(|a| Url::from_str(a).expect("invalid address"))
.collect(),
listeners: listeners
.into_iter()
.map(|l| Url::from_str(l).expect("invalid listener"))
.collect(),
}
}
pub fn heartbeat_with_snapshot(id: Uuid, snapshot: DnsSnapshot) -> HeartbeatRequest {
let mut hb = HeartbeatRequest {
id: Some(id.into()),
..Default::default()
};
hb.update(snapshot);
hb
}
fn find_free_udp_port() -> u16 {
std::net::UdpSocket::bind("127.0.0.1:0")
.expect("failed to bind temp udp socket")
.local_addr()
.expect("failed to get local addr")
.port()
}
pub async fn check_dns_record(fake_ip: &Ipv4Addr, domain: &str, expected_ip: &str) {
check_dns_record_at(SocketAddr::new((*fake_ip).into(), 53), domain, expected_ip).await;
}
pub async fn check_dns_record_at(server_addr: SocketAddr, domain: &str, expected_ip: &str) {
let expected = expected_ip.parse::<Ipv4Addr>().unwrap();
let name = Name::from_str(domain).unwrap();
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
let (mut client, background) = Client::<TokioRuntimeProvider>::from_sender(stream);
let background_task = tokio::spawn(background);
let query_result = tokio::time::timeout(
Duration::from_secs(2),
client.query(name.clone(), DNSClass::IN, RecordType::A),
)
.await;
background_task.abort();
let _ = background_task.await;
let attempt_err = match query_result {
Ok(Ok(response)) => {
if response.answers.len() == 1
&& let Some(resp) = response.answers.first()
&& matches!(resp.clone().data, RData::A(a) if a.0 == expected)
{
return;
}
format!("unexpected response: {:?}", response.answers)
}
Ok(Err(e)) => {
format!("DNS query failed for domain '{domain}': {e}")
}
Err(_) => {
format!("DNS query timed out for domain '{domain}'")
}
};
if Instant::now() >= deadline {
panic!(
"DNS query failed unexpectedly for domain '{domain}' after retries: {attempt_err}"
);
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
pub async fn check_dns_record_missing(fake_ip: &Ipv4Addr, domain: &str) {
check_dns_record_missing_at(SocketAddr::new((*fake_ip).into(), 53), domain).await;
}
pub async fn check_dns_record_missing_at(server_addr: SocketAddr, domain: &str) {
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
let (mut client, background) = Client::<TokioRuntimeProvider>::from_sender(stream);
let background_task = tokio::spawn(background);
let query_result = tokio::time::timeout(
Duration::from_secs(2),
client.query(Name::from_str(domain).unwrap(), DNSClass::IN, RecordType::A),
)
.await;
background_task.abort();
let _ = background_task.await;
let attempt_err = match query_result {
Ok(Ok(response)) => {
if response.answers.is_empty() {
return;
}
format!("unexpected non-empty response: {:?}", response.answers)
}
Ok(Err(e)) => {
format!("DNS query for missing record failed for domain '{domain}': {e}")
}
Err(_) => {
format!("DNS query for missing record timed out for domain '{domain}'")
}
};
if Instant::now() >= deadline {
panic!(
"missing-record query failed unexpectedly for domain '{domain}' after retries: {attempt_err}"
);
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
pub fn new_request(name: &str, rtype: RecordType) -> anyhow::Result<Request> {
let mut msg = Message::new(0, MessageType::Query, OpCode::Query);
msg.metadata.recursion_desired = true;
msg.add_query(Query::query(Name::from_ascii(name)?, rtype));
let mut request = Vec::new();
let mut encoder = BinEncoder::new(&mut request);
msg.emit(&mut encoder)?;
Ok(Request::from_bytes(
request,
SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0).into(),
Protocol::Udp,
)?)
}
async fn wait_route_disappear(peer_mgr: Arc<PeerManager>, target_peer_id: u32) {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let has_route = peer_mgr
.list_routes()
.await
.iter()
.any(|r| r.peer_id == target_peer_id);
if !has_route {
return;
}
assert!(
Instant::now() < deadline,
"route to peer {} did not disappear in time",
target_peer_id
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
async fn disconnect_all_peer_conns(a: Arc<PeerManager>, b: Arc<PeerManager>) {
if let Some(conns) = a.get_peer_map().list_peer_conns(b.my_peer_id()).await {
for conn in conns {
let conn_id = conn.conn_id.parse().expect("invalid conn id");
let _ = a.close_peer_conn(b.my_peer_id(), &conn_id).await;
}
}
if let Some(conns) = b.get_peer_map().list_peer_conns(a.my_peer_id()).await {
for conn in conns {
let conn_id = conn.conn_id.parse().expect("invalid conn id");
let _ = b.close_peer_conn(a.my_peer_id(), &conn_id).await;
}
}
}
async fn check_dns_unavailable_at(server_addr: SocketAddr, domain: &str) {
let deadline = Instant::now() + Duration::from_secs(15);
let name = rr::Name::from_str(domain).unwrap();
loop {
let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
let (mut client, background) = Client::<TokioRuntimeProvider>::from_sender(stream);
let background_task = tokio::spawn(background);
let query_result = tokio::time::timeout(
Duration::from_secs(1),
client.query(name.clone(), DNSClass::IN, RecordType::A),
)
.await;
background_task.abort();
let _ = background_task.await;
match query_result {
Ok(Ok(response)) if !response.answers.is_empty() => {
if Instant::now() >= deadline {
panic!(
"DNS endpoint {server_addr} still answered for '{domain}': {:?}",
response.answers
);
}
}
_ => return,
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
async fn wait_peer_zone_visibility(
peer_mgr: Arc<PeerManager>,
target_peer_id: u32,
zone_origin_substr: &str,
expected_visible: bool,
) {
let dns = DnsPeerMgr::new(peer_mgr.clone(), peer_mgr.get_global_ctx());
let deadline = Instant::now() + Duration::from_secs(20);
loop {
let _ = dns
.refresh(target_peer_id, Default::default(), Default::default())
.await;
let snapshot = dns.snapshot();
let visible = snapshot.zones.iter().any(|z| {
z.content
.contains(&format!("$ORIGIN {}", zone_origin_substr))
});
if visible == expected_visible {
return;
}
assert!(
Instant::now() < deadline,
"zone visibility mismatch for '{}': expected {}, got {}",
zone_origin_substr,
expected_visible,
visible,
);
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
fn cfg_with_listener(name: &str, ipv4: &str, domain: &str, listener_port: u16) -> String {
format!(
r#"
instance_name = "dns-int-{name}"
ipv4 = "{ipv4}"
[dns]
name = "{name}"
domain = "{domain}"
listeners = ["udp://127.0.0.1:{listener_port}"]
"#
)
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_single_node_resolves_self_and_custom_zone() {
let listener_port = find_free_udp_port();
let config = format!(
r#"
{}
[[dns.zone]]
origin = "services.alpha.et-test"
records = ["api IN A 10.77.0.7"]
[dns.zone.export]
whitelist = ["*"]
"#,
cfg_with_listener("alpha", "10.144.144.11/24", "et-test", listener_port)
);
let peer_mgr = prepare_env_from_config_str(&config).await;
let mut dns_node = start_dns_node_without_nic(peer_mgr);
let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_port);
check_dns_record_at(server_addr, "alpha.et-test.", "10.144.144.11").await;
check_dns_record_at(server_addr, "api.services.alpha.et-test.", "10.77.0.7").await;
dns_node.stop().await.unwrap();
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_two_nodes_sync_self_zone_and_exported_zone() {
let listener_a = find_free_udp_port();
let listener_b = find_free_udp_port();
let config_a = format!(
r#"
{}
[[dns.zone]]
origin = "shared.mesh-test"
records = ["app IN A 10.66.0.8"]
[dns.zone.export]
whitelist = ["*"]
"#,
cfg_with_listener("node-a", "10.144.144.21/24", "mesh-test", listener_a)
);
let config_b = cfg_with_listener("node-b", "10.144.144.22/24", "mesh-test", listener_b);
let peer_a = prepare_env_from_config_str(&config_a).await;
let peer_b = prepare_env_from_config_str(&config_b).await;
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
wait_route_appear(peer_a.clone(), peer_b.clone())
.await
.expect("route should appear");
let mut node_a = start_dns_node_without_nic(peer_a);
let mut node_b = start_dns_node_without_nic(peer_b);
let addr_a = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_a);
let addr_b = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_b);
check_dns_record_at(addr_a, "node-b.mesh-test.", "10.144.144.22").await;
check_dns_record_at(addr_b, "node-b.mesh-test.", "10.144.144.22").await;
check_dns_record_at(addr_a, "app.shared.mesh-test.", "10.66.0.8").await;
check_dns_record_at(addr_b, "app.shared.mesh-test.", "10.66.0.8").await;
node_a.stop().await.unwrap();
node_b.stop().await.unwrap();
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_zone_without_export_section_is_not_synced() {
let listener_a = find_free_udp_port();
let listener_b = find_free_udp_port();
let config_a = format!(
r#"
{}
[[dns.zone]]
origin = "private.mesh-test"
records = ["secret IN A 10.99.0.9"]
"#,
cfg_with_listener("local-a", "10.144.144.31/24", "mesh-test", listener_a)
);
let config_b = cfg_with_listener("local-b", "10.144.144.32/24", "mesh-test", listener_b);
let peer_a = prepare_env_from_config_str(&config_a).await;
let peer_b = prepare_env_from_config_str(&config_b).await;
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
wait_route_appear(peer_a.clone(), peer_b.clone())
.await
.expect("route should appear");
// Export behavior is determined by whether `[dns.zone.export]` exists.
// Verify from peer-sync view to avoid host-wide DNS-server election side effects.
let dns_a = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
dns_a.register();
let dns_b = DnsPeerMgr::new(peer_b.clone(), peer_b.get_global_ctx());
dns_b.register();
dns_b
.refresh(peer_a.my_peer_id(), Default::default(), Default::default())
.await
.unwrap();
let snapshot = dns_b.snapshot();
assert!(
!snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN private.mesh-test")),
"zone without [dns.zone.export] should not be exported to peer snapshot"
);
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_export_section_disabled_is_not_synced() {
let listener_a = find_free_udp_port();
let listener_b = find_free_udp_port();
let config_a = format!(
r#"
{}
[[dns.zone]]
origin = "disabled.mesh-test"
records = ["secret IN A 10.99.1.9"]
[dns.zone.export]
disabled = true
"#,
cfg_with_listener("local-da", "10.144.144.41/24", "mesh-test", listener_a)
);
let config_b = cfg_with_listener("local-db", "10.144.144.42/24", "mesh-test", listener_b);
let peer_a = prepare_env_from_config_str(&config_a).await;
let peer_b = prepare_env_from_config_str(&config_b).await;
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
wait_route_appear(peer_a.clone(), peer_b.clone())
.await
.expect("route should appear");
let dns_a = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
dns_a.register();
let dns_b = DnsPeerMgr::new(peer_b.clone(), peer_b.get_global_ctx());
dns_b.register();
dns_b
.refresh(peer_a.my_peer_id(), Default::default(), Default::default())
.await
.unwrap();
let snapshot = dns_b.snapshot();
assert!(
!snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN disabled.mesh-test")),
"zone with [dns.zone.export] disabled=true should not be exported"
);
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_patch_updates_zone_record_visible_on_query() {
let listener = find_free_udp_port();
let config = format!(
r#"
{}
[[dns.zone]]
origin = "patch.mesh-test"
records = ["api IN A 10.80.0.1"]
[dns.zone.export]
"#,
cfg_with_listener("patch-node", "10.144.149.11/24", "mesh-test", listener)
);
let peer = prepare_env_from_config_str(&config).await;
let mut dns_node = start_dns_node_without_nic(peer.clone());
let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener);
check_dns_record_at(server_addr, "api.patch.mesh-test.", "10.80.0.1").await;
let mut dns = peer.get_global_ctx().config.get_dns().into_raw();
let mut zones = dns.zones.unwrap();
let zone_idx = zones
.iter()
.position(|z| z.origin.to_string().contains("patch.mesh-test"))
.expect("patch zone should exist");
let mut zone = zones[zone_idx].clone().into_raw();
zone.records = Some(vec!["api IN A 10.80.0.2".to_string()]);
zones[zone_idx] = zone.try_into().expect("patch zone update should be valid");
dns.zones = Some(zones);
peer.get_global_ctx().config.set_dns(dns.into());
peer.get_global_ctx()
.issue_event(crate::common::global_ctx::GlobalCtxEvent::ConfigPatched(
crate::proto::api::config::InstanceConfigPatch::default(),
));
check_dns_record_at(server_addr, "api.patch.mesh-test.", "10.80.0.2").await;
dns_node.stop().await.unwrap();
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_patch_reloads_listener_binding() {
let listener_old = find_free_udp_port();
let listener_new = find_free_udp_port();
let config = cfg_with_listener(
"listener-patch",
"10.144.150.11/24",
"mesh-test",
listener_old,
);
let peer = prepare_env_from_config_str(&config).await;
let mut dns_node = start_dns_node_without_nic(peer.clone());
let old_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_old);
let new_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_new);
check_dns_record_at(old_addr, "listener-patch.mesh-test.", "10.144.150.11").await;
let mut dns = peer.get_global_ctx().config.get_dns().into_raw();
dns.listeners = Some(
vec![
format!("udp://127.0.0.1:{listener_new}")
.parse()
.expect("invalid listener"),
]
.into(),
);
peer.get_global_ctx().config.set_dns(dns.into());
peer.get_global_ctx()
.issue_event(crate::common::global_ctx::GlobalCtxEvent::ConfigPatched(
crate::proto::api::config::InstanceConfigPatch::default(),
));
check_dns_record_at(new_addr, "listener-patch.mesh-test.", "10.144.150.11").await;
check_dns_unavailable_at(old_addr, "listener-patch.mesh-test.").await;
dns_node.stop().await.unwrap();
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_three_nodes_partition_and_recover_dns_propagation() {
let listener_a = find_free_udp_port();
let listener_b = find_free_udp_port();
let listener_c = find_free_udp_port();
let config_a = cfg_with_listener("node-a7", "10.144.151.11/24", "mesh7-test", listener_a);
let config_b = cfg_with_listener("node-b7", "10.144.151.12/24", "mesh7-test", listener_b);
let config_c = format!(
r#"
{}
[[dns.zone]]
origin = "shared-c7.mesh7-test"
records = ["svc IN A 10.77.7.7"]
[dns.zone.export]
"#,
cfg_with_listener("node-c7", "10.144.151.13/24", "mesh7-test", listener_c)
);
let peer_a = prepare_env_from_config_str(&config_a).await;
let peer_b = prepare_env_from_config_str(&config_b).await;
let peer_c = prepare_env_from_config_str(&config_c).await;
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
wait_route_appear(peer_a.clone(), peer_c.clone())
.await
.expect("route a-c should appear via b");
let mut node_a = start_dns_node_without_nic(peer_a.clone());
let mut node_b = start_dns_node_without_nic(peer_b.clone());
let mut node_c = start_dns_node_without_nic(peer_c.clone());
let addr_a = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_a);
check_dns_record_at(addr_a, "node-c7.mesh7-test.", "10.144.151.13").await;
check_dns_record_at(addr_a, "svc.shared-c7.mesh7-test.", "10.77.7.7").await;
wait_peer_zone_visibility(
peer_a.clone(),
peer_c.my_peer_id(),
"node-c7.mesh7-test",
true,
)
.await;
wait_peer_zone_visibility(
peer_a.clone(),
peer_c.my_peer_id(),
"shared-c7.mesh7-test",
true,
)
.await;
disconnect_all_peer_conns(peer_b.clone(), peer_c.clone()).await;
wait_route_disappear(peer_a.clone(), peer_c.my_peer_id()).await;
// Validate via peer-sync snapshot to avoid process-wide DNS-server election side effects.
wait_peer_zone_visibility(
peer_a.clone(),
peer_c.my_peer_id(),
"node-c7.mesh7-test",
false,
)
.await;
wait_peer_zone_visibility(
peer_a.clone(),
peer_c.my_peer_id(),
"shared-c7.mesh7-test",
false,
)
.await;
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
wait_route_appear(peer_a.clone(), peer_c.clone())
.await
.expect("route a-c should recover via b");
wait_peer_zone_visibility(
peer_a.clone(),
peer_c.my_peer_id(),
"node-c7.mesh7-test",
true,
)
.await;
wait_peer_zone_visibility(
peer_a.clone(),
peer_c.my_peer_id(),
"shared-c7.mesh7-test",
true,
)
.await;
check_dns_record_at(addr_a, "node-c7.mesh7-test.", "10.144.151.13").await;
check_dns_record_at(addr_a, "svc.shared-c7.mesh7-test.", "10.77.7.7").await;
node_a.stop().await.unwrap();
node_b.stop().await.unwrap();
node_c.stop().await.unwrap();
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_three_nodes_chain_sync_self_and_exported_zone() {
let listener_a = find_free_udp_port();
let listener_b = find_free_udp_port();
let listener_c = find_free_udp_port();
let config_a = cfg_with_listener("node-a3", "10.144.145.11/24", "mesh3-test", listener_a);
let config_b = cfg_with_listener("node-b3", "10.144.145.12/24", "mesh3-test", listener_b);
let config_c = format!(
r#"
{}
[[dns.zone]]
origin = "shared-c.mesh3-test"
records = ["api IN A 10.66.1.8"]
[dns.zone.export]
"#,
cfg_with_listener("node-c3", "10.144.145.13/24", "mesh3-test", listener_c)
);
let peer_a = prepare_env_from_config_str(&config_a).await;
let peer_b = prepare_env_from_config_str(&config_b).await;
let peer_c = prepare_env_from_config_str(&config_c).await;
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
wait_route_appear(peer_a.clone(), peer_b.clone())
.await
.expect("route a-b should appear");
wait_route_appear(peer_b.clone(), peer_c.clone())
.await
.expect("route b-c should appear");
wait_route_appear(peer_a.clone(), peer_c.clone())
.await
.expect("route a-c should appear via b");
let mut node_a = start_dns_node_without_nic(peer_a);
let mut node_b = start_dns_node_without_nic(peer_b);
let mut node_c = start_dns_node_without_nic(peer_c);
let addr_a = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_a);
let addr_b = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_b);
let addr_c = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_c);
check_dns_record_at(addr_a, "node-c3.mesh3-test.", "10.144.145.13").await;
check_dns_record_at(addr_b, "node-c3.mesh3-test.", "10.144.145.13").await;
check_dns_record_at(addr_c, "node-a3.mesh3-test.", "10.144.145.11").await;
check_dns_record_at(addr_a, "api.shared-c.mesh3-test.", "10.66.1.8").await;
check_dns_record_at(addr_c, "api.shared-c.mesh3-test.", "10.66.1.8").await;
node_a.stop().await.unwrap();
node_b.stop().await.unwrap();
node_c.stop().await.unwrap();
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_three_nodes_late_join_propagates_dns() {
let listener_a = find_free_udp_port();
let listener_b = find_free_udp_port();
let listener_c = find_free_udp_port();
let config_a = cfg_with_listener("node-a4", "10.144.146.11/24", "mesh4-test", listener_a);
let config_b = cfg_with_listener("node-b4", "10.144.146.12/24", "mesh4-test", listener_b);
let config_c = format!(
r#"
{}
[[dns.zone]]
origin = "joined.mesh4-test"
records = ["svc IN A 10.66.2.8"]
[dns.zone.export]
"#,
cfg_with_listener("node-c4", "10.144.146.13/24", "mesh4-test", listener_c)
);
let peer_a = prepare_env_from_config_str(&config_a).await;
let peer_b = prepare_env_from_config_str(&config_b).await;
let peer_c = prepare_env_from_config_str(&config_c).await;
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
wait_route_appear(peer_a.clone(), peer_b.clone())
.await
.expect("route a-b should appear");
let mut node_a = start_dns_node_without_nic(peer_a.clone());
let mut node_b = start_dns_node_without_nic(peer_b.clone());
let addr_a = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_a);
check_dns_record_missing_at(addr_a, "node-c4.mesh4-test.").await;
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
wait_route_appear(peer_b.clone(), peer_c.clone())
.await
.expect("route b-c should appear");
wait_route_appear(peer_a.clone(), peer_c.clone())
.await
.expect("route a-c should appear after c joins");
let mut node_c = start_dns_node_without_nic(peer_c);
check_dns_record_at(addr_a, "node-c4.mesh4-test.", "10.144.146.13").await;
check_dns_record_at(addr_a, "svc.joined.mesh4-test.", "10.66.2.8").await;
node_a.stop().await.unwrap();
node_b.stop().await.unwrap();
node_c.stop().await.unwrap();
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_three_nodes_zone_without_export_not_synced_across_hop() {
let listener_a = find_free_udp_port();
let listener_b = find_free_udp_port();
let listener_c = find_free_udp_port();
let config_a = cfg_with_listener("node-a5", "10.144.147.11/24", "mesh5-test", listener_a);
let config_b = cfg_with_listener("node-b5", "10.144.147.12/24", "mesh5-test", listener_b);
let config_c = format!(
r#"
{}
[[dns.zone]]
origin = "private-c.mesh5-test"
records = ["secret IN A 10.66.3.8"]
"#,
cfg_with_listener("node-c5", "10.144.147.13/24", "mesh5-test", listener_c)
);
let peer_a = prepare_env_from_config_str(&config_a).await;
let peer_b = prepare_env_from_config_str(&config_b).await;
let peer_c = prepare_env_from_config_str(&config_c).await;
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
wait_route_appear(peer_a.clone(), peer_c.clone())
.await
.expect("route a-c should appear via b");
let dns_c = DnsPeerMgr::new(peer_c.clone(), peer_c.get_global_ctx());
dns_c.register();
let dns_a = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
dns_a.register();
dns_a
.refresh(peer_c.my_peer_id(), Default::default(), Default::default())
.await
.unwrap();
let snapshot = dns_a.snapshot();
assert!(
!snapshot
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN private-c.mesh5-test")),
"zone without [dns.zone.export] should not sync over multi-hop"
);
}
#[tokio::test]
#[serial_test::serial(dns_integration_rpc)]
async fn config_string_two_nodes_peer_dns_offline_then_rejoin() {
let listener_a = find_free_udp_port();
let listener_b = find_free_udp_port();
let config_a = cfg_with_listener("node-a6", "10.144.148.11/24", "mesh6-test", listener_a);
let config_b = cfg_with_listener("node-b6", "10.144.148.12/24", "mesh6-test", listener_b);
let peer_a = prepare_env_from_config_str(&config_a).await;
let peer_b = prepare_env_from_config_str(&config_b).await;
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
wait_route_appear(peer_a.clone(), peer_b.clone())
.await
.expect("route should appear");
let dns_b = DnsPeerMgr::new(peer_b.clone(), peer_b.get_global_ctx());
dns_b.register();
let dns_a_online = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
dns_a_online.register();
dns_a_online
.refresh(peer_b.my_peer_id(), Default::default(), Default::default())
.await
.unwrap();
assert!(
dns_a_online
.snapshot()
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN node-b6.mesh6-test")),
"peer B self zone should be visible after initial refresh"
);
// Simulate peer offline by closing all direct connections and waiting route withdrawal.
if let Some(conns) = peer_a
.get_peer_map()
.list_peer_conns(peer_b.my_peer_id())
.await
{
for conn in conns {
let conn_id = conn.conn_id.parse().expect("invalid conn id");
let _ = peer_a.close_peer_conn(peer_b.my_peer_id(), &conn_id).await;
}
}
if let Some(conns) = peer_b
.get_peer_map()
.list_peer_conns(peer_a.my_peer_id())
.await
{
for conn in conns {
let conn_id = conn.conn_id.parse().expect("invalid conn id");
let _ = peer_b.close_peer_conn(peer_a.my_peer_id(), &conn_id).await;
}
}
wait_route_disappear(peer_a.clone(), peer_b.my_peer_id()).await;
wait_route_disappear(peer_b.clone(), peer_a.my_peer_id()).await;
// Cached remote zones should be purged after peer cache idle timeout.
tokio::time::sleep(Duration::from_secs(4)).await;
let dns_a_offline = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
assert!(
!dns_a_offline
.snapshot()
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN node-b6.mesh6-test")),
"peer B self zone should disappear after route withdrawal and cache expiry"
);
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
wait_route_appear(peer_a.clone(), peer_b.clone())
.await
.expect("route should re-appear");
let dns_a_rejoin = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
dns_a_rejoin
.refresh(peer_b.my_peer_id(), Default::default(), Default::default())
.await
.unwrap();
assert!(
dns_a_rejoin
.snapshot()
.zones
.iter()
.any(|z| z.content.contains("$ORIGIN node-b6.mesh6-test")),
"peer B self zone should be restored after DNS RPC rejoins"
);
}
+183
View File
@@ -0,0 +1,183 @@
use crate::proto;
use crate::proto::utils::{RepeatedDeserialize, RepeatedMessageModel, RepeatedSerialize};
use anyhow::{Error, anyhow};
use hickory_net::xfer::Protocol;
use hickory_resolver::config::{ConnectionConfig, NameServerConfig, ProtocolConfig};
use serde::de::IntoDeserializer;
use serde::{Deserialize, Deserializer, de};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::fmt::{Display, Formatter};
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use url::Url;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
pub struct NameServerAddr {
pub protocol: Protocol,
pub addr: SocketAddr,
}
impl From<NameServerAddr> for NameServerConfig {
fn from(value: NameServerAddr) -> Self {
let mut config = match value.protocol {
Protocol::Udp => ConnectionConfig::udp(),
Protocol::Tcp => ConnectionConfig::tcp(),
_ => unimplemented!(),
};
config.port = value.addr.port();
Self::new(value.addr.ip(), true, vec![config])
}
}
impl From<(IpAddr, &ConnectionConfig)> for NameServerAddr {
fn from(value: (IpAddr, &ConnectionConfig)) -> Self {
let (ip, config) = value;
Self {
protocol: config.protocol.to_protocol(),
addr: SocketAddr::new(ip, config.port),
}
}
}
impl TryFrom<&Url> for NameServerAddr {
type Error = Error;
fn try_from(url: &Url) -> Result<Self, Self::Error> {
let protocol = match Protocol::deserialize(url.scheme().into_deserializer())
.map_err(|e: de::value::Error| anyhow!("invalid protocol '{}': {}", url.scheme(), e))?
{
Protocol::Udp => ProtocolConfig::Udp,
Protocol::Tcp => ProtocolConfig::Tcp,
p => return Err(anyhow!("unsupported protocol: {}", p)),
};
let host = url.host_str().ok_or(anyhow!("host not found"))?;
let port = url.port().unwrap_or(protocol.default_port());
let addr = if let Ok(addr) = IpAddr::from_str(host) {
SocketAddr::new(addr, port)
} else {
return Err(anyhow!("invalid address: {}", host));
};
Ok(Self {
protocol: protocol.to_protocol(),
addr,
})
}
}
impl TryFrom<&proto::common::Url> for NameServerAddr {
type Error = Error;
fn try_from(value: &proto::common::Url) -> Result<Self, Self::Error> {
(&Url::try_from(value)?).try_into()
}
}
impl From<&NameServerAddr> for Url {
fn from(value: &NameServerAddr) -> Self {
Url::parse(&format!("{}://{}", value.protocol, value.addr)).unwrap()
}
}
impl From<&NameServerAddr> for proto::common::Url {
fn from(value: &NameServerAddr) -> Self {
Url::from(value).into()
}
}
impl From<NameServerAddr> for Url {
fn from(value: NameServerAddr) -> Self {
(&value).into()
}
}
impl From<NameServerAddr> for proto::common::Url {
fn from(value: NameServerAddr) -> Self {
(&value).into()
}
}
impl FromStr for NameServerAddr {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
(&Url::parse(s)?).try_into()
}
}
impl Display for NameServerAddr {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(Url::from(*self).as_str())
}
}
pub type NameServerAddrGroup = RepeatedMessageModel<NameServerAddr>;
impl From<&NameServerConfig> for NameServerAddrGroup {
fn from(value: &NameServerConfig) -> Self {
value
.connections
.iter()
.map(|c| (value.ip, c).into())
.collect()
}
}
impl From<SocketAddr> for NameServerAddrGroup {
fn from(value: SocketAddr) -> Self {
vec![
NameServerAddr {
protocol: Protocol::Udp,
addr: value,
},
NameServerAddr {
protocol: Protocol::Tcp,
addr: value,
},
]
.into()
}
}
impl From<IpAddr> for NameServerAddrGroup {
fn from(value: IpAddr) -> Self {
SocketAddr::new(value, 53).into()
}
}
impl From<u16> for NameServerAddrGroup {
fn from(value: u16) -> Self {
SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), value).into()
}
}
impl RepeatedSerialize for NameServerAddr {}
impl<'de> RepeatedDeserialize<'de> for NameServerAddr {
fn deserialize<D>(deserializer: D) -> Result<NameServerAddrGroup, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Candidate {
NameServerAddr(NameServerAddr),
U16(u16),
IpAddr(IpAddr),
SocketAddr(SocketAddr),
}
let items = Vec::<Candidate>::deserialize(deserializer)?;
let items = items
.into_iter()
.flat_map(|item| -> NameServerAddrGroup {
match item {
Candidate::NameServerAddr(addr) => vec![addr].into(),
Candidate::U16(port) => port.into(),
Candidate::IpAddr(ip) => ip.into(),
Candidate::SocketAddr(addr) => addr.into(),
}
})
.collect();
Ok(items)
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod addr;
pub mod response;
pub mod zone_handler;
+59
View File
@@ -0,0 +1,59 @@
use hickory_net::NetError;
use hickory_proto::rr::Record;
use hickory_proto::serialize::binary::BinEncoder;
use hickory_server::server::{ResponseHandler, ResponseInfo};
use hickory_server::zone_handler::MessageResponse;
use parking_lot::Mutex;
use std::sync::Arc;
// ResponseWrapper for serializing DNS responses into a byte buffer.
// Used by the address hijacking NIC packet filter to produce DNS replies in-place.
#[derive(Debug, Clone)]
pub struct ResponseHandle {
inner: Arc<Mutex<Vec<u8>>>,
}
impl ResponseHandle {
pub fn new(capacity: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(Vec::with_capacity(capacity))),
}
}
pub fn into_inner(self) -> Option<Vec<u8>> {
Arc::into_inner(self.inner).map(Mutex::into_inner)
}
}
pub trait RecordIter<'r>: Iterator<Item = &'r Record> + Send + 'r {}
impl<'r, T> RecordIter<'r> for T where T: Iterator<Item = &'r Record> + Send + 'r {}
#[async_trait::async_trait]
impl ResponseHandler for ResponseHandle {
async fn send_response<'r>(
&mut self,
response: MessageResponse<
'_,
'r,
impl RecordIter<'r>,
impl RecordIter<'r>,
impl RecordIter<'r>,
impl RecordIter<'r>,
>,
) -> Result<ResponseInfo, NetError> {
let max_size = if let Some(edns) = response.edns() {
edns.max_payload()
} else {
hickory_net::udp::MAX_RECEIVE_BUFFER_SIZE as u16
};
let mut inner = self.inner.lock();
inner.clear();
let mut encoder = BinEncoder::new(inner.as_mut());
encoder.set_max_size(max_size);
response
.destructive_emit(&mut encoder)
.map_err(NetError::Proto)
}
}
+85
View File
@@ -0,0 +1,85 @@
use crate::dns::config::zone::Fallthrough;
use delegate::delegate;
use derive_more::{Constructor, Deref, DerefMut};
use hickory_proto::op::ResponseCode;
use hickory_proto::rr::{LowerName, RecordType, TSigResponseContext};
use hickory_server::server::{Request, RequestInfo};
use hickory_server::zone_handler::{
AuthLookup, AxfrPolicy, LookupControlFlow, LookupError, LookupOptions, ZoneHandler, ZoneType,
};
use std::collections::HashSet;
use std::sync::Arc;
pub type ArcZoneHandler = Arc<dyn ZoneHandler>;
#[derive(Constructor, Deref, DerefMut)]
pub struct ChainedZoneHandler<H>
where
H: ZoneHandler,
{
#[deref]
#[deref_mut]
handler: H,
fallthrough: HashSet<Fallthrough>,
}
#[async_trait::async_trait]
impl<H> ZoneHandler for ChainedZoneHandler<H>
where
H: ZoneHandler,
{
delegate! {
to self.handler {
fn zone_type(&self) -> ZoneType;
fn axfr_policy(&self) -> AxfrPolicy;
fn origin(&self) -> &LowerName;
}
}
#[inline]
async fn update(
&self,
update: &Request,
now: u64,
) -> (Result<bool, ResponseCode>, Option<TSigResponseContext>) {
self.handler.update(update, now).await
}
#[inline]
async fn lookup(
&self,
name: &LowerName,
rtype: RecordType,
request_info: Option<&RequestInfo<'_>>,
lookup_options: LookupOptions,
) -> LookupControlFlow<AuthLookup> {
self.handler
.lookup(name, rtype, request_info, lookup_options)
.await
}
#[inline]
async fn search(
&self,
request: &Request,
lookup_options: LookupOptions,
) -> (LookupControlFlow<AuthLookup>, Option<TSigResponseContext>) {
let (result, tsig) = self.handler.search(request, lookup_options).await;
match &result {
LookupControlFlow::Continue(Err(e)) | LookupControlFlow::Break(Err(e))
if self.fallthrough.contains(&Fallthrough::Any)
|| matches!(e, LookupError::ResponseCode(c) if self.fallthrough.contains(&(*c).into())) =>
{
(LookupControlFlow::Skip, None)
}
_ => (result, tsig),
}
}
#[inline]
async fn nsec_records(
&self,
name: &LowerName,
lookup_options: LookupOptions,
) -> LookupControlFlow<AuthLookup> {
self.handler.nsec_records(name, lookup_options).await
}
}
+572
View File
@@ -0,0 +1,572 @@
use crate::dns::config::zone::Fallthrough;
use crate::dns::utils::addr::{NameServerAddr, NameServerAddrGroup};
use crate::dns::utils::zone_handler::{ArcZoneHandler, ChainedZoneHandler};
use crate::proto::dns::ZoneData;
use crate::proto::utils::RepeatedMessageModel;
use crate::utils::dns::resolver_conf;
use hickory_net::runtime::TokioRuntimeProvider;
use hickory_proto::rr::{LowerName, RecordSet, RrKey};
use hickory_proto::serialize::txt::Parser;
use hickory_server::store::forwarder::{ForwardConfig, ForwardZoneHandler};
use hickory_server::store::in_memory::InMemoryZoneHandler;
use hickory_server::zone_handler::{AxfrPolicy, ZoneType};
use indexmap::IndexMap;
use itertools::chain;
use maplit::hashset;
use std::collections::{BTreeMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct Zone {
origin: LowerName,
records: BTreeMap<RrKey, RecordSet>,
pub forward: Option<ForwardConfig>,
fallthrough: HashSet<Fallthrough>,
}
impl Zone {
pub fn system() -> Self {
let (config, opts) = resolver_conf();
let forward = ForwardConfig {
name_servers: config.name_servers().to_vec(),
options: Some(opts),
};
Self {
origin: ".".parse().unwrap(),
forward: Some(forward),
fallthrough: hashset! {},
..Default::default()
}
}
}
impl Zone {
pub fn create_memory_zone_handler(&self) -> Option<ArcZoneHandler> {
(!self.records.is_empty()).then(|| {
let mut memory = InMemoryZoneHandler::<TokioRuntimeProvider>::empty(
self.origin.clone().into(),
ZoneType::External,
AxfrPolicy::default(),
);
memory.records_get_mut().extend(
self.records
.clone()
.into_iter()
.map(|(k, v)| (k, Arc::new(v))),
);
Arc::new(ChainedZoneHandler::new(memory, self.fallthrough.clone())) as _
})
}
pub fn create_forward_zone_handler(&self) -> Option<ArcZoneHandler> {
self.forward.as_ref().and_then(|forward| {
ForwardZoneHandler::builder_with_config(
forward.clone(),
TokioRuntimeProvider::default(),
)
.build()
.inspect_err(|error| tracing::error!(?error, "failed to create forward zone_handler"))
.ok()
.map(|handler| {
Arc::new(ChainedZoneHandler::new(handler, self.fallthrough.clone())) as _
})
})
}
}
impl TryFrom<&ZoneData> for Zone {
type Error = anyhow::Error;
fn try_from(value: &ZoneData) -> Result<Self, Self::Error> {
let (origin, records) = Parser::new(&value.content, None, None)
.parse()
.map_err(|e| anyhow::anyhow!("failed to parse zone data: {e}"))?;
let name_servers = value
.forwarders
.iter()
.map(NameServerAddr::try_from)
.map(|a| a.map(Into::into))
.collect::<Result<Vec<_>, _>>()?;
let forward = (!name_servers.is_empty()).then_some(ForwardConfig {
name_servers,
options: None,
});
let fallthrough = value.fallthrough.iter().copied().map(Into::into).collect();
Ok(Self {
origin: origin.into(),
records,
forward,
fallthrough,
})
}
}
impl From<Zone> for ZoneData {
fn from(value: Zone) -> Self {
let records = value
.records
.values()
.flat_map(RecordSet::records_without_rrsigs)
.map(ToString::to_string);
let forwarders = value
.forward
.into_iter()
.flat_map(|f| f.name_servers.into_iter())
.map(|ns| (&ns).into())
.flat_map(NameServerAddrGroup::into_iter)
.map(Into::into);
Self::new(&value.origin, 0, records, forwarders, value.fallthrough)
}
}
pub type ZoneGroup = RepeatedMessageModel<Zone>;
impl ZoneGroup {
pub fn into_groups(self) -> IndexMap<LowerName, ZoneGroup> {
self.into_iter().fold(IndexMap::new(), |mut map, zone| {
map.entry(zone.origin.clone()).or_default().push(zone);
map
})
}
pub fn iter_zone_handlers(&self) -> impl Iterator<Item = ArcZoneHandler> + use<'_> {
self.iter().flat_map(|zone| {
chain(
zone.create_memory_zone_handler(),
zone.create_forward_zone_handler(),
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dns::tests::new_request;
use crate::dns::utils::response::ResponseHandle;
use crate::proto::common::Url;
use crate::proto::dns::ZoneData;
use hickory_proto::op::{Message, ResponseCode};
use hickory_proto::rr::{RData, Record, RecordType, RrsetRecords};
use hickory_server::Server;
use hickory_server::zone_handler::Catalog;
use maplit::hashset;
use std::collections::HashSet;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use tokio::net::UdpSocket;
use tokio::task::JoinHandle;
impl Zone {
// Test-only record iterator for precise assertions.
pub fn iter_records(&self) -> impl Iterator<Item = &Record> {
self.records
.values()
.filter(|set| !set.is_empty())
.flat_map(|set| {
let RrsetRecords::RecordsOnly(records) = set.records_without_rrsigs() else {
unreachable!()
};
records
})
}
}
fn zone_data_with_fallthrough(
origin: &str,
records: Vec<&str>,
forwarders: Vec<&str>,
fallthrough: HashSet<Fallthrough>,
) -> ZoneData {
ZoneData::new(
&origin.parse().unwrap(),
60,
records,
forwarders
.into_iter()
.map(|url| Url::from_str(url).unwrap()),
fallthrough,
)
}
fn zone_data(origin: &str, records: Vec<&str>, forwarders: Vec<&str>) -> ZoneData {
zone_data_with_fallthrough(origin, records, forwarders, hashset! {Fallthrough::Any})
}
fn build_catalog(zones: ZoneGroup) -> Catalog {
zones
.into_groups()
.into_iter()
.fold(Catalog::new(), |mut catalog, (origin, group)| {
catalog.upsert(origin, group.iter_zone_handlers().collect());
catalog
})
}
async fn lookup_message(
catalog: &Catalog,
name: &str,
record_type: RecordType,
) -> anyhow::Result<(ResponseCode, Option<Message>)> {
let request = new_request(name, record_type)?;
let response = ResponseHandle::new(1024);
let info = catalog.lookup(&request, None, 0, response.clone()).await;
let message = response
.into_inner()
.map(|raw| Message::from_vec(&raw))
.transpose()?;
Ok((info.response_code, message))
}
fn has_a_answer(message: &Message, expected: Ipv4Addr) -> bool {
message
.answers
.iter()
.any(|record| matches!(record.data, RData::A(addr) if *addr == expected))
}
fn has_aaaa_answer(message: &Message, expected: Ipv6Addr) -> bool {
message
.answers
.iter()
.any(|record| matches!(record.data, RData::AAAA(addr) if *addr == expected))
}
async fn start_upstream_server() -> anyhow::Result<(SocketAddr, JoinHandle<()>)> {
let upstream = Zone::try_from(&zone_data(
"upstream.test",
vec!["from-forward 60 IN A 203.0.113.9"],
vec![],
))?;
let mut catalog = Catalog::new();
catalog.upsert(
upstream.origin.clone(),
vec![upstream.create_memory_zone_handler().unwrap()],
);
let socket = UdpSocket::bind("127.0.0.1:0").await?;
let addr = socket.local_addr()?;
let mut server = Server::new(catalog);
server.register_socket(socket);
let handle = tokio::spawn(async move {
let _ = server.block_until_done().await;
});
Ok((addr, handle))
}
#[test]
fn zone_try_from_rejects_invalid_record() {
let data = zone_data("invalid-record.test", vec!["this is not a record"], vec![]);
let err = Zone::try_from(&data).expect_err("invalid record should fail");
assert!(err.to_string().contains("failed to parse zone data"));
}
#[test]
fn zone_try_from_rejects_invalid_forwarder_protocol() {
let data = zone_data("invalid-forwarder.test", vec![], vec!["http://1.1.1.1:53"]);
let err = Zone::try_from(&data).expect_err("unsupported forwarder should fail");
assert!(err.to_string().contains("unsupported") || err.to_string().contains("protocol"));
}
#[test]
fn empty_zone_creates_no_zone_handler() -> anyhow::Result<()> {
let zone = Zone::try_from(&zone_data("empty.test", vec![], vec![]))?;
assert!(zone.create_memory_zone_handler().is_none());
assert!(zone.create_forward_zone_handler().is_none());
Ok(())
}
#[test]
fn zone_roundtrip_preserves_records_and_forwarders() -> anyhow::Result<()> {
let zone = Zone::try_from(&zone_data(
"roundtrip.test",
vec!["www 0 IN A 123.123.123.123", "app IN CNAME www"],
vec!["udp://1.1.1.1:53", "tcp://8.8.8.8:53"],
))?;
assert_eq!(zone.iter_records().count(), 2);
assert_eq!(zone.forward.as_ref().unwrap().name_servers.len(), 2);
let serialized = ZoneData::from(zone.clone());
let reparsed = Zone::try_from(&serialized)?;
assert_eq!(reparsed.origin.to_string(), "roundtrip.test.");
assert_eq!(reparsed.iter_records().count(), 2);
assert_eq!(reparsed.forward.as_ref().unwrap().name_servers.len(), 2);
Ok(())
}
#[test]
fn zone_group_into_groups_merges_same_origin() -> anyhow::Result<()> {
let zones: ZoneGroup = vec![
Zone::try_from(&zone_data("same.test", vec!["@ IN A 10.0.0.1"], vec![]))?,
Zone::try_from(&zone_data("other.test", vec!["@ IN A 10.0.0.2"], vec![]))?,
Zone::try_from(&zone_data("same.test", vec![], vec!["udp://1.1.1.1:53"]))?,
]
.into();
let groups = zones.into_groups();
assert_eq!(groups.len(), 2);
assert_eq!(
groups
.get(&LowerName::from_str("same.test.")?)
.unwrap()
.len(),
2
);
assert_eq!(
groups
.get(&LowerName::from_str("other.test.")?)
.unwrap()
.len(),
1
);
Ok(())
}
#[test]
fn zone_group_iter_zone_handlers_returns_memory_and_forward() -> anyhow::Result<()> {
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
"zone-handler.test",
vec!["@ IN A 10.0.0.10"],
vec!["udp://1.1.1.1:53"],
))?]
.into();
let zone_handlers = zones.iter_zone_handlers().collect::<Vec<_>>();
assert_eq!(zone_handlers.len(), 2);
Ok(())
}
#[test]
fn zone_system_builds_root_forwarder() {
let zone = Zone::system();
assert_eq!(zone.origin.to_string(), ".");
assert!(zone.forward.is_some());
assert!(zone.create_forward_zone_handler().is_some());
}
#[tokio::test]
async fn catalog_lookup_returns_a_record_from_memory_zone_handler() -> anyhow::Result<()> {
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
"memory.test",
vec!["@ IN A 10.20.30.40"],
vec![],
))?]
.into();
let catalog = build_catalog(zones);
let (rcode, message) = lookup_message(&catalog, "memory.test.", RecordType::A).await?;
assert_eq!(rcode, ResponseCode::NoError);
let message = message.expect("response should exist");
assert!(has_a_answer(&message, Ipv4Addr::new(10, 20, 30, 40)));
Ok(())
}
#[tokio::test]
async fn catalog_lookup_returns_refused_when_zone_is_missing() -> anyhow::Result<()> {
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
"present.test",
vec!["@ IN A 10.20.30.41"],
vec![],
))?]
.into();
let catalog = build_catalog(zones);
let (rcode, _message) = lookup_message(&catalog, "absent.test.", RecordType::A).await?;
assert_eq!(rcode, ResponseCode::Refused);
Ok(())
}
#[tokio::test]
async fn catalog_lookup_forwards_on_nameexists() -> anyhow::Result<()> {
let upstream = Zone::try_from(&zone_data(
"forward-aaaa.test",
vec!["host 60 IN AAAA 2001:db8::1"],
vec![],
))?;
let mut upstream_catalog = Catalog::new();
upstream_catalog.upsert(
upstream.origin.clone(),
vec![upstream.create_memory_zone_handler().unwrap()],
);
let socket = UdpSocket::bind("127.0.0.1:0").await?;
let upstream_addr = socket.local_addr()?;
let mut server = Server::new(upstream_catalog);
server.register_socket(socket);
let upstream_handle = tokio::spawn(async move {
let _ = server.block_until_done().await;
});
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
"forward-aaaa.test",
vec!["host IN A 10.20.30.40"],
vec![&format!("udp://{}", upstream_addr)],
))?]
.into();
let catalog = build_catalog(zones);
let (rcode, message) =
lookup_message(&catalog, "host.forward-aaaa.test.", RecordType::AAAA).await?;
assert_eq!(rcode, ResponseCode::NoError);
let message = message.expect("response should exist");
assert!(has_aaaa_answer(&message, "2001:db8::1".parse()?));
upstream_handle.abort();
let _ = upstream_handle.await;
Ok(())
}
#[tokio::test]
async fn catalog_lookup_forwards_on_nxdomain() -> anyhow::Result<()> {
let upstream = Zone::try_from(&zone_data(
"forward-nxdomain.test",
vec!["missing 60 IN A 203.0.113.55"],
vec![],
))?;
let mut upstream_catalog = Catalog::new();
upstream_catalog.upsert(
upstream.origin.clone(),
vec![upstream.create_memory_zone_handler().unwrap()],
);
let socket = UdpSocket::bind("127.0.0.1:0").await?;
let upstream_addr = socket.local_addr()?;
let mut server = Server::new(upstream_catalog);
server.register_socket(socket);
let upstream_handle = tokio::spawn(async move {
let _ = server.block_until_done().await;
});
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
"forward-nxdomain.test",
vec!["present IN A 10.20.30.41"],
vec![&format!("udp://{}", upstream_addr)],
))?]
.into();
let catalog = build_catalog(zones);
let (rcode, message) =
lookup_message(&catalog, "missing.forward-nxdomain.test.", RecordType::A).await?;
assert_eq!(rcode, ResponseCode::NoError);
let message = message.expect("response should exist");
assert!(has_a_answer(&message, Ipv4Addr::new(203, 0, 113, 55)));
upstream_handle.abort();
let _ = upstream_handle.await;
Ok(())
}
#[tokio::test]
async fn catalog_lookup_falls_back_to_later_zone_handler_with_same_origin() -> anyhow::Result<()>
{
let zones: ZoneGroup = vec![
// First matching zone exists but does not contain the queried name.
Zone::try_from(&zone_data(
"fallback.test",
vec!["first IN A 10.20.30.1"],
vec![],
))?,
// Second matching zone should be queried as fallback and answer.
Zone::try_from(&zone_data(
"fallback.test",
vec!["target IN A 10.20.30.2"],
vec![],
))?,
]
.into();
let catalog = build_catalog(zones);
let (rcode, message) =
lookup_message(&catalog, "target.fallback.test.", RecordType::A).await?;
assert_eq!(rcode, ResponseCode::NoError);
assert!(has_a_answer(
&message.expect("response should exist"),
Ipv4Addr::new(10, 20, 30, 2)
));
Ok(())
}
#[tokio::test]
async fn catalog_lookup_does_not_fall_back_when_fallthrough_disabled() -> anyhow::Result<()> {
let zones: ZoneGroup = vec![
Zone::try_from(&zone_data_with_fallthrough(
"fallback-disabled.test",
vec!["first IN A 10.20.31.1"],
vec![],
hashset! {},
))?,
Zone::try_from(&zone_data_with_fallthrough(
"fallback-disabled.test",
vec!["target IN A 10.20.31.2"],
vec![],
hashset! {},
))?,
]
.into();
let catalog = build_catalog(zones);
let (rcode, message) =
lookup_message(&catalog, "target.fallback-disabled.test.", RecordType::A).await?;
assert_ne!(rcode, ResponseCode::NoError);
if let Some(message) = message.as_ref() {
assert!(!has_a_answer(message, Ipv4Addr::new(10, 20, 31, 2)));
}
Ok(())
}
#[tokio::test]
async fn catalog_forward_only_zone_queries_upstream() -> anyhow::Result<()> {
let (upstream_addr, upstream_handle) = start_upstream_server().await?;
let forward_zone = Zone::try_from(&zone_data(
"upstream.test",
vec![],
vec![&format!("udp://{}", upstream_addr)],
))?;
let catalog = build_catalog(vec![forward_zone].into());
let (rcode, message) =
lookup_message(&catalog, "from-forward.upstream.test.", RecordType::A).await?;
assert_eq!(rcode, ResponseCode::NoError);
assert!(has_a_answer(
&message.expect("response should exist"),
Ipv4Addr::new(203, 0, 113, 9)
));
upstream_handle.abort();
let _ = upstream_handle.await;
Ok(())
}
}
@@ -1,111 +0,0 @@
use std::{sync::Arc, time::Duration};
use tokio::task::JoinSet;
use crate::{
peers::peer_manager::PeerManager,
proto::{
api::instance::Route,
common::Void,
magic_dns::{
HandshakeRequest, MagicDnsServerRpc, MagicDnsServerRpcClientFactory,
UpdateDnsRecordRequest,
},
rpc_impl::standalone::StandAloneClient,
rpc_types::controller::BaseController,
},
tunnel::tcp::TcpTunnelConnector,
};
use super::MAGIC_DNS_INSTANCE_ADDR;
pub struct MagicDnsClientInstance {
rpc_client: StandAloneClient<TcpTunnelConnector>,
rpc_stub: Option<Box<dyn MagicDnsServerRpc<Controller = BaseController> + Send>>,
peer_mgr: Arc<PeerManager>,
tasks: JoinSet<()>,
}
impl MagicDnsClientInstance {
pub async fn new(peer_mgr: Arc<PeerManager>) -> Result<Self, anyhow::Error> {
let tcp_connector = TcpTunnelConnector::new(MAGIC_DNS_INSTANCE_ADDR.parse().unwrap());
let mut rpc_client = StandAloneClient::new(tcp_connector);
let rpc_stub = rpc_client
.scoped_client::<MagicDnsServerRpcClientFactory<BaseController>>("".to_string())
.await?;
Ok(MagicDnsClientInstance {
rpc_client,
rpc_stub: Some(rpc_stub),
peer_mgr,
tasks: JoinSet::new(),
})
}
async fn update_dns_task(
peer_mgr: Arc<PeerManager>,
rpc_stub: Box<dyn MagicDnsServerRpc<Controller = BaseController> + Send>,
) -> Result<(), anyhow::Error> {
let mut prev_last_update = None;
rpc_stub
.handshake(BaseController::default(), HandshakeRequest::default())
.await?;
loop {
rpc_stub
.heartbeat(BaseController::default(), Void::default())
.await?;
let last_update = peer_mgr.get_route_peer_info_last_update_time().await;
if Some(last_update) == prev_last_update {
tokio::time::sleep(Duration::from_millis(500)).await;
continue;
}
let mut routes = peer_mgr.list_routes().await;
// add self as a route
let ctx = peer_mgr.get_global_ctx();
routes.push(Route {
hostname: ctx.get_hostname(),
ipv4_addr: ctx.get_ipv4().map(Into::into),
..Default::default()
});
// Use configured tld_dns_zone (always set by default)
let flags = ctx.config.get_flags();
let req = UpdateDnsRecordRequest {
routes,
zone: flags.tld_dns_zone.clone(),
};
tracing::debug!(
"MagicDnsClientInstance::update_dns_task: update dns records: {:?}",
req
);
rpc_stub
.update_dns_record(BaseController::default(), req)
.await?;
let last_update_after_rpc = peer_mgr.get_route_peer_info_last_update_time().await;
if last_update_after_rpc == last_update {
prev_last_update = Some(last_update);
}
}
}
pub async fn run_and_wait(&mut self) {
let rpc_stub = self.rpc_stub.take().unwrap();
let peer_mgr = self.peer_mgr.clone();
self.tasks.spawn(async move {
let ret = Self::update_dns_task(peer_mgr, rpc_stub).await;
if let Err(e) = ret {
tracing::error!("MagicDnsServerInstanceData::run_and_wait: {:?}", e);
}
});
tokio::select! {
_ = self.tasks.join_next() => {
tracing::warn!("MagicDnsServerInstanceData::run_and_wait: dns record update task exited");
}
_ = self.rpc_client.wait() => {
tracing::warn!("MagicDnsServerInstanceData::run_and_wait: rpc client exited");
}
}
}
}
-193
View File
@@ -1,193 +0,0 @@
use hickory_proto::rr;
use hickory_proto::rr::RData;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use std::str::FromStr;
use std::time::Duration;
#[derive(Serialize, Deserialize, Debug, Clone, derive_builder::Builder)]
pub struct RunConfig {
general: GeneralConfig,
#[builder(default = HashMap::new())]
zones: Zone,
#[builder(default = Vec::new())]
#[serde(default)]
excluded_forward_nameservers: Vec<IpAddr>,
}
impl RunConfig {
pub fn general(&self) -> &GeneralConfig {
&self.general
}
pub fn zones(&self) -> &Zone {
&self.zones
}
pub fn excluded_forward_nameservers(&self) -> &Vec<IpAddr> {
&self.excluded_forward_nameservers
}
}
#[derive(Serialize, Deserialize, Debug, Clone, derive_builder::Builder)]
pub struct GeneralConfig {
#[builder(setter(into, strip_option), default = None)]
listen_tcp: Option<String>,
#[builder(setter(into, strip_option), default = None)]
listen_udp: Option<String>,
}
impl GeneralConfig {
pub fn listen_tcp(&self) -> &Option<String> {
&self.listen_tcp
}
pub fn listen_udp(&self) -> &Option<String> {
&self.listen_udp
}
}
pub type Zone = HashMap<String, Vec<Record>>; // domain -> records
pub type RecordType = rr::RecordType;
#[derive(Serialize, Deserialize, Debug, Clone, derive_builder::Builder)]
pub struct Record {
#[serde(rename = "type")]
rr_type: RecordType,
name: String,
value: String,
#[serde(with = "humantime_serde")]
ttl: Duration,
}
impl Record {
pub fn name(&self) -> anyhow::Result<rr::Name> {
let name = rr::Name::from_str(self.name.as_str())?;
Ok(name)
}
fn rr_type(&self) -> rr::RecordType {
self.rr_type
}
}
impl TryFrom<Record> for rr::Record {
type Error = anyhow::Error;
fn try_from(value: Record) -> Result<Self, Self::Error> {
let r: rr::Record = (&value).try_into()?;
Ok(r)
}
}
impl TryFrom<&Record> for rr::Record {
type Error = anyhow::Error;
fn try_from(value: &Record) -> Result<Self, Self::Error> {
let name = value.name()?;
let mut record = Self::update0(name, value.ttl.as_secs() as u32, value.rr_type());
record.set_dns_class(rr::DNSClass::IN);
match value.rr_type {
RecordType::A => {
let addr: Ipv4Addr = value.value.parse()?;
record.set_data(RData::A(rr::rdata::a::A(addr)));
}
RecordType::SOA => {
let soa = value.value.split_whitespace().collect::<Vec<_>>();
if soa.len() != 7 {
return Err(anyhow::anyhow!("invalid SOA record"));
}
let mname = rr::Name::from_str(soa[0])?;
let rname = rr::Name::from_str(soa[1])?;
let serial: u32 = soa[2].parse()?;
let refresh: u32 = soa[3].parse()?;
let retry: u32 = soa[4].parse()?;
let expire: u32 = soa[5].parse()?;
let minimum: u32 = soa[6].parse()?;
record.set_data(RData::SOA(rr::rdata::soa::SOA::new(
mname,
rname,
serial,
refresh.try_into().unwrap(),
retry.try_into().unwrap(),
expire.try_into().unwrap(),
minimum,
)));
}
_ => todo!(),
}
Ok(record)
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
#[tokio::test]
async fn it_works() -> anyhow::Result<()> {
let text = r#"
[general]
listen_tcp = "127.0.0.1:5300"
listen_udp = "127.0.0.1:5353"
[[zones."et.internal"]]
type = "A"
name = "www"
value = "123.123.123.123"
ttl = "60s"
[[zones."et.top"]]
type = "A"
name = "@"
value = "100.100.100.100"
ttl = "61s"
"#;
let config = toml::from_str::<RunConfig>(text)?;
assert_eq!(
config.general.listen_tcp().clone().unwrap(),
"127.0.0.1:5300"
);
assert_eq!(
config.general.listen_udp().clone().unwrap(),
"127.0.0.1:5353"
);
assert_eq!(config.zones.len(), 2);
let (domain, records) = config
.zones
.get_key_value("et.internal")
.ok_or(anyhow!("et.internal not found"))?;
assert_eq!(domain, "et.internal");
assert_eq!(records.len(), 1);
let record = &records[0];
assert_eq!(record.rr_type, RecordType::A);
assert_eq!(record.name, "www");
assert_eq!(record.value, "123.123.123.123");
assert_eq!(record.ttl.as_secs(), 60);
let (domain, records) = config
.zones
.get_key_value("et.top")
.ok_or(anyhow!("et.top not found"))?;
assert_eq!(domain, "et.top");
assert_eq!(records.len(), 1);
let record = &records[0];
assert_eq!(record.rr_type, RecordType::A);
assert_eq!(record.name, "@");
assert_eq!(record.value, "100.100.100.100");
assert_eq!(record.ttl.as_secs(), 61);
Ok(())
}
}
-21
View File
@@ -1,21 +0,0 @@
// This module is copy and modified from https://github.com/fanyang89/libdns
#[cfg(feature = "magic-dns")]
pub(crate) mod config;
#[cfg(feature = "magic-dns")]
pub(crate) mod server;
#[cfg(feature = "magic-dns")]
pub mod client_instance;
#[cfg(feature = "magic-dns")]
pub mod runner;
#[cfg(feature = "magic-dns")]
pub mod server_instance;
#[cfg(feature = "magic-dns")]
pub mod system_config;
#[cfg(all(test, feature = "tun", feature = "magic-dns"))]
mod tests;
pub static MAGIC_DNS_INSTANCE_ADDR: &str = "tcp://127.0.0.1:49813";
pub static MAGIC_DNS_FAKE_IP: &str = "100.100.100.101";
pub static DEFAULT_ET_DNS_ZONE: &str = "et.net.";
@@ -1,93 +0,0 @@
use cidr::Ipv4Inet;
use tokio_util::sync::CancellationToken;
use crate::peers::peer_manager::PeerManager;
use std::{net::Ipv4Addr, sync::Arc, time::Duration};
use super::{client_instance::MagicDnsClientInstance, server_instance::MagicDnsServerInstance};
static DEFAULT_ET_DNS_ZONE: &str = "et.net.";
pub struct DnsRunner {
client: Option<MagicDnsClientInstance>,
server: Option<MagicDnsServerInstance>,
peer_mgr: Arc<PeerManager>,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
}
impl DnsRunner {
pub fn new(
peer_mgr: Arc<PeerManager>,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
) -> Self {
Self {
client: None,
server: None,
peer_mgr,
tun_dev,
tun_inet,
fake_ip,
}
}
async fn clean_env(&mut self) {
if let Some(server) = self.server.take() {
server.clean_env().await;
}
self.client.take();
}
async fn run_once(&mut self) -> anyhow::Result<()> {
// try server first
match MagicDnsServerInstance::new(
self.peer_mgr.clone(),
self.tun_dev.clone(),
self.tun_inet,
self.fake_ip,
)
.await
{
Ok(server) => {
self.server = Some(server);
tracing::info!("DnsRunner::run_once: server started");
}
Err(e) => {
tracing::error!("DnsRunner::run_once: {:?}", e);
}
}
// every runner must run a client
let client = MagicDnsClientInstance::new(self.peer_mgr.clone()).await?;
self.client = Some(client);
self.client.as_mut().unwrap().run_and_wait().await;
Err(anyhow::anyhow!("Client instance exit"))
}
pub async fn run(&mut self, canel_token: CancellationToken) {
loop {
tracing::info!("DnsRunner::run: start");
tokio::select! {
_ = canel_token.cancelled() => {
self.clean_env().await;
tracing::info!("DnsRunner::run: cancelled");
return;
}
ret = self.run_once() => {
self.clean_env().await;
if let Err(e) = ret {
tracing::error!("DnsRunner::run: {:?}", e);
} else {
tracing::info!("DnsRunner::run: unexpected exit, server may be down");
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
}
}
-338
View File
@@ -1,338 +0,0 @@
use anyhow::{Context, Result};
use hickory_proto::op::Edns;
use hickory_proto::rr;
use hickory_proto::rr::LowerName;
use hickory_resolver::config::ResolverOpts;
use hickory_resolver::name_server::TokioConnectionProvider;
use hickory_resolver::system_conf::read_system_conf;
use hickory_server::ServerFuture;
use hickory_server::authority::{AuthorityObject, Catalog, ZoneType};
use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
use hickory_server::store::forwarder::ForwardConfig;
use hickory_server::store::{forwarder::ForwardAuthority, in_memory::InMemoryAuthority};
use std::io;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::{TcpListener, UdpSocket};
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinSet;
use crate::common::dns::get_default_resolver_config;
use super::config::{GeneralConfig, Record, RunConfig};
pub struct Server {
server: ServerFuture<CatalogRequestHandler>,
catalog: Arc<RwLock<Catalog>>,
general_config: GeneralConfig,
udp_local_addr: Option<SocketAddr>,
tcp_local_addr: Option<SocketAddr>,
tasks: JoinSet<()>,
}
struct CatalogRequestHandler {
catalog: Arc<RwLock<Catalog>>,
}
impl CatalogRequestHandler {
fn new(catalog: Arc<RwLock<Catalog>>) -> CatalogRequestHandler {
// let system_conf = read_system_conf();
// let recursor = match system_conf {
// Ok((conf, _)) => RecursorBuilder::default().build(conf),
// Err(_) => RecursorBuilder::default().build(get_default_resolver_config()),
// }
// // policy is security unware, this will never return an error
// .unwrap();
Self { catalog }
}
}
#[async_trait::async_trait]
impl RequestHandler for CatalogRequestHandler {
async fn handle_request<R: ResponseHandler>(
&self,
request: &Request,
response_handle: R,
) -> ResponseInfo {
self.catalog
.read()
.await
.handle_request(request, response_handle)
.await
}
}
pub fn build_authority(domain: &str, records: &[Record]) -> Result<InMemoryAuthority> {
let zone = rr::Name::from_str(domain)?;
let mut authority = InMemoryAuthority::empty(zone, ZoneType::Primary, false);
for record in records.iter() {
let r = record.try_into()?;
authority.upsert_mut(r, 0);
}
Ok(authority)
}
impl Server {
pub fn new(config: RunConfig) -> Self {
Self::try_new(config).unwrap()
}
fn try_new(config: RunConfig) -> Result<Self> {
let mut catalog = Catalog::new();
for (domain, records) in config.zones().iter() {
let zone = rr::Name::from_str(domain.as_str())?;
let authroty = build_authority(domain, records)?;
catalog.upsert(zone.clone().into(), vec![Arc::new(authroty)]);
}
// use forwarder authority for the root zone
let system_conf =
read_system_conf().unwrap_or((get_default_resolver_config(), ResolverOpts::default()));
let forward_config = ForwardConfig {
name_servers: system_conf
.0
.name_servers()
.iter()
.filter(|&x| {
!config
.excluded_forward_nameservers()
.contains(&x.socket_addr.ip())
})
.cloned()
.collect::<Vec<_>>()
.into(),
options: Some(system_conf.1),
};
let auth = ForwardAuthority::builder_with_config(
forward_config,
TokioConnectionProvider::default(),
)
.build()
.unwrap();
catalog.upsert(rr::Name::from_str(".")?.into(), vec![Arc::new(auth)]);
let catalog = Arc::new(RwLock::new(catalog));
let handler = CatalogRequestHandler::new(catalog.clone());
let server = ServerFuture::new(handler);
Ok(Self {
server,
catalog,
general_config: config.general().clone(),
udp_local_addr: None,
tcp_local_addr: None,
tasks: JoinSet::new(),
})
}
pub fn udp_local_addr(&self) -> Option<SocketAddr> {
self.udp_local_addr
}
pub fn tcp_local_addr(&self) -> Option<SocketAddr> {
self.tcp_local_addr
}
pub async fn register_udp_socket(&mut self, address: String) -> Result<SocketAddr> {
let bind_addr = SocketAddr::from_str(&address)
.with_context(|| format!("DNS Server failed to parse address {}", address))?;
let socket = socket2::Socket::new(
socket2::Domain::IPV4,
socket2::Type::DGRAM,
Some(socket2::Protocol::UDP),
)
.with_context(|| {
format!(
"DNS Server failed to create UDP socket for address {}",
address
)
})?;
socket2::SockRef::from(&socket)
.set_reuse_address(true)
.with_context(|| {
format!(
"DNS Server failed to set reuse address on socket {}",
address
)
})?;
socket.bind(&bind_addr.into()).with_context(|| {
format!("DNS Server failed to bind socket to address {}", bind_addr)
})?;
socket
.set_nonblocking(true)
.with_context(|| "DNS Server failed to set socket to non-blocking".to_string())?;
let socket = UdpSocket::from_std(socket.into()).with_context(|| {
format!(
"DNS Server failed to convert socket to UdpSocket for address {}",
address
)
})?;
let local_addr = socket
.local_addr()
.with_context(|| "DNS Server failed to get local address".to_string())?;
self.server.register_socket(socket);
Ok(local_addr)
}
pub async fn run(&mut self) -> Result<()> {
if let Some(address) = self.general_config.listen_tcp() {
let tcp_listener = TcpListener::bind(address.clone())
.await
.with_context(|| format!("DNS Server failed to bind TCP address {}", address))?;
self.tcp_local_addr = Some(tcp_listener.local_addr()?);
self.server
.register_listener(tcp_listener, Duration::from_secs(5));
}
if let Some(address) = self.general_config.listen_udp() {
let local_addr = self.register_udp_socket(address.clone()).await?;
self.udp_local_addr = Some(local_addr);
};
Ok(())
}
pub async fn shutdown(&mut self) -> Result<()> {
self.server.shutdown_gracefully().await?;
Ok(())
}
pub async fn upsert(&self, name: LowerName, authority: Arc<dyn AuthorityObject>) {
self.catalog.write().await.upsert(name, vec![authority]);
}
pub async fn remove(&self, name: &LowerName) -> Option<Vec<Arc<dyn AuthorityObject>>> {
self.catalog.write().await.remove(name)
}
pub async fn update<R: ResponseHandler>(
&self,
update: &Request,
response_edns: Option<Edns>,
response_handle: R,
) -> io::Result<ResponseInfo> {
self.catalog
.write()
.await
.update(update, response_edns, response_handle)
.await
}
pub async fn contains(&self, name: &LowerName) -> bool {
self.catalog.read().await.contains(name)
}
pub async fn lookup<R: ResponseHandler>(
&self,
request: &Request,
response_edns: Option<Edns>,
response_handle: R,
) -> ResponseInfo {
self.catalog
.read()
.await
.lookup(request, response_edns, response_handle)
.await
}
pub async fn read_catalog(&self) -> RwLockReadGuard<'_, Catalog> {
self.catalog.read().await
}
pub async fn write_catalog(&self) -> RwLockWriteGuard<'_, Catalog> {
self.catalog.write().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instance::dns_server::config::{
GeneralConfigBuilder, RecordBuilder, RecordType, RunConfigBuilder,
};
use anyhow::Result;
use hickory_client::client::{Client, ClientHandle};
use hickory_proto::rr;
use hickory_proto::runtime::TokioRuntimeProvider;
use hickory_proto::udp::UdpClientStream;
use maplit::hashmap;
use std::time::Duration;
#[tokio::test]
async fn it_works() -> Result<()> {
let mut server = Server::new(
RunConfigBuilder::default()
.general(GeneralConfigBuilder::default().build()?)
.build()?,
);
server.run().await?;
server.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn can_resolve_records() -> Result<()> {
let configured_record = RecordBuilder::default()
.rr_type(RecordType::A)
.name("www.et.internal.".to_string())
.value("123.123.123.123".to_string())
.ttl(Duration::from_secs(60))
.build()?;
let configured_record2 = RecordBuilder::default()
.rr_type(RecordType::A)
.name("中文.et.internal.".to_string())
.value("123.123.123.123".to_string())
.ttl(Duration::from_secs(60))
.build()?;
let soa_record = RecordBuilder::default()
.rr_type(RecordType::SOA)
.name("et.internal.".to_string())
.value(
"ns.et.internal. hostmaster.et.internal. 2023101001 7200 3600 1209600 86400"
.to_string(),
)
.ttl(Duration::from_secs(60))
.build()?;
let config = RunConfigBuilder::default()
.general(
GeneralConfigBuilder::default()
.listen_udp("127.0.0.1:0")
.build()?,
)
.zones(hashmap! {
"et.internal.".to_string() => vec![configured_record.clone(), soa_record.clone(), configured_record2.clone()],
})
.build()?;
let mut server = Server::new(config);
server.run().await?;
let local_addr = server.udp_local_addr().unwrap();
let stream = UdpClientStream::builder(local_addr, TokioRuntimeProvider::default()).build();
let (mut client, background) = Client::connect(stream).await?;
let background_task = tokio::spawn(background);
let response = client
.query(
rr::Name::from_str("www.et.internal")?,
rr::DNSClass::IN,
rr::RecordType::A,
)
.await?;
drop(background_task);
println!("Response: {:?}", response);
assert_eq!(response.answers().len(), 1);
let expected_record: rr::Record = configured_record.try_into()?;
assert_eq!(response.answers().first().unwrap(), &expected_record);
server.shutdown().await?;
Ok(())
}
}
@@ -1,610 +0,0 @@
// single-instance server in one machine, every easytier instance that has ip address and tun device will try to create a server instance.
// magic dns client will connect to this server to update the dns records.
// magic dns server will add the dns server ip address to the tun device, and forward the dns request to the dns server
// magic dns client will establish a long live tcp connection to the magic dns server, and when the server stops or crashes,
// all the clients will exit and let the easytier instance to launch a new server instance.
use super::{
MAGIC_DNS_INSTANCE_ADDR,
config::{GeneralConfigBuilder, RunConfigBuilder},
server::Server,
system_config::{OSConfig, SystemConfig},
};
use crate::{
common::{
PeerId,
ifcfg::{IfConfiger, IfConfiguerTrait},
},
instance::dns_server::{
config::{Record, RecordBuilder, RecordType},
server::build_authority,
},
peers::{NicPacketFilter, peer_manager::PeerManager},
proto::{
api::instance::Route,
common::{TunnelInfo, Void},
magic_dns::{
DnsRecord, DnsRecordA, DnsRecordList, GetDnsRecordResponse, HandshakeRequest,
HandshakeResponse, MagicDnsServerRpc, MagicDnsServerRpcServer, UpdateDnsRecordRequest,
dns_record::{self},
},
rpc_impl::standalone::{RpcServerHook, StandAloneServer},
rpc_types::controller::{BaseController, Controller},
},
tunnel::{packet_def::ZCPacket, tcp::TcpTunnelListener},
};
use anyhow::Context;
use cidr::Ipv4Inet;
use dashmap::DashMap;
use hickory_proto::rr::LowerName;
use hickory_proto::serialize::binary::{BinDecodable, BinEncoder};
use hickory_server::authority::{MessageRequest, MessageResponse};
use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
use multimap::MultiMap;
use pnet::packet::icmp::{IcmpTypes, MutableIcmpPacket};
use pnet::packet::ipv4::Ipv4Packet;
use pnet::packet::udp::UdpPacket;
use pnet::packet::{
MutablePacket, Packet, icmp,
ip::IpNextHeaderProtocols,
ipv4::{self, MutableIpv4Packet},
udp::{self, MutableUdpPacket},
};
use std::net::{SocketAddr, SocketAddrV4};
use std::sync::Mutex;
use std::{collections::BTreeMap, io, net::Ipv4Addr, str::FromStr, sync::Arc, time::Duration};
static NIC_PIPELINE_NAME: &str = "magic_dns_server";
pub(super) struct MagicDnsServerInstanceData {
dns_server: Server,
tun_dev: Option<String>,
tun_ip: Ipv4Addr,
fake_ip: Ipv4Addr,
my_peer_id: PeerId,
// zone -> (tunnel remote addr -> route)
route_infos: DashMap<String, MultiMap<url::Url, Route>>,
system_config: Option<Box<dyn SystemConfig>>,
}
impl MagicDnsServerInstanceData {
pub async fn update_dns_records<'a, T: Iterator<Item = &'a Route>>(
&self,
routes: T,
zone: &str,
) -> Result<(), anyhow::Error> {
let mut records: Vec<Record> = vec![];
for route in routes {
if route.hostname.is_empty() {
continue;
}
let Some(ipv4_addr) = route.ipv4_addr.unwrap_or_default().address else {
continue;
};
let record = RecordBuilder::default()
.rr_type(RecordType::A)
.name(format!("{}.{}", route.hostname, zone))
.value(ipv4_addr.to_string())
.ttl(Duration::from_secs(1))
.build()?;
// check record name valid for dns
if let Err(e) = record.name() {
tracing::error!("Invalid subdomain label: {}", e);
continue;
}
records.push(record);
}
let soa_record = RecordBuilder::default()
.rr_type(RecordType::SOA)
.name(zone.to_string())
.value(format!(
"ns.{} hostmaster.{} 2023101001 7200 3600 1209600 86400",
zone, zone
))
.ttl(Duration::from_secs(60))
.build()?;
records.push(soa_record);
let authority = build_authority(zone, &records)?;
self.dns_server
.upsert(
LowerName::from_str(zone)
.with_context(|| "Invalid zone name, expect format like \"et.net.\"")?,
Arc::new(authority),
)
.await;
tracing::debug!("Updated DNS records for zone {}: {:?}", zone, records);
Ok(())
}
pub async fn update(&self) {
for item in self.route_infos.iter() {
let zone = item.key();
let route_iter = item.value().flat_iter().map(|x| x.1);
if let Err(e) = self.update_dns_records(route_iter, zone).await {
tracing::error!("Failed to update DNS records for zone {}: {:?}", zone, e);
}
}
}
async fn keep_zone_authoritative(&self, zone: &str) {
if let Err(e) = self
.update_dns_records(std::iter::empty::<&Route>(), zone)
.await
{
tracing::error!(
"Failed to keep DNS zone {} authoritative after route prune: {:?}",
zone,
e
);
}
}
fn do_system_config(&self, zone: &str) -> Result<(), anyhow::Error> {
if let Some(c) = &self.system_config {
c.set_dns(&OSConfig {
nameservers: vec![self.fake_ip.to_string()],
search_domains: vec![zone.to_string()],
match_domains: vec![zone.to_string()],
})?;
}
Ok(())
}
}
#[async_trait::async_trait]
impl MagicDnsServerRpc for MagicDnsServerInstanceData {
type Controller = BaseController;
async fn handshake(
&self,
_ctrl: Self::Controller,
_input: HandshakeRequest,
) -> crate::proto::rpc_types::error::Result<HandshakeResponse> {
Ok(Default::default())
}
async fn heartbeat(
&self,
_ctrl: Self::Controller,
_input: Void,
) -> crate::proto::rpc_types::error::Result<Void> {
Ok(Default::default())
}
async fn update_dns_record(
&self,
ctrl: Self::Controller,
input: UpdateDnsRecordRequest,
) -> crate::proto::rpc_types::error::Result<Void> {
let Some(tunnel_info) = ctrl.get_tunnel_info() else {
return Err(anyhow::anyhow!("No tunnel info").into());
};
let Some(remote_addr) = &tunnel_info.remote_addr else {
return Err(anyhow::anyhow!("No remote addr").into());
};
let zone = input.zone.clone();
let remote_addr: url::Url = remote_addr.clone().into();
let mut zone_removed = false;
if let Some(mut routes_by_addr) = self.route_infos.get_mut(&zone) {
routes_by_addr.remove(&remote_addr);
if !input.routes.is_empty() {
routes_by_addr.insert_many(remote_addr, input.routes);
}
zone_removed = routes_by_addr.is_empty();
} else if !input.routes.is_empty() {
let mut routes_by_addr = MultiMap::new();
routes_by_addr.insert_many(remote_addr, input.routes);
self.route_infos.insert(zone.clone(), routes_by_addr);
}
if zone_removed {
self.route_infos.remove(&zone);
self.keep_zone_authoritative(&zone).await;
}
self.update().await;
Ok(Default::default())
}
async fn get_dns_record(
&self,
_ctrl: Self::Controller,
_input: Void,
) -> crate::proto::rpc_types::error::Result<GetDnsRecordResponse> {
let mut ret = BTreeMap::new();
for item in self.route_infos.iter() {
let zone = item.key();
let routes = item.value();
let mut dns_records = DnsRecordList::default();
for route in routes.iter().map(|x| x.1) {
dns_records.records.push(DnsRecord {
record: Some(dns_record::Record::A(DnsRecordA {
name: format!("{}.{}", route.hostname, zone),
value: route.ipv4_addr.unwrap_or_default().address,
ttl: 1,
})),
});
}
ret.insert(zone.clone(), dns_records);
}
Ok(GetDnsRecordResponse { records: ret })
}
}
// This should only be used for UDP response.
// For other protocols, the variable `max_size` in `send_response` should be u16::MAX.
#[derive(Clone)]
struct ResponseWrapper {
response: Arc<Mutex<Vec<u8>>>,
}
trait RecordIter<'a>: Iterator<Item = &'a hickory_proto::rr::Record> + Send + 'a {}
impl<'a, T> RecordIter<'a> for T where T: Iterator<Item = &'a hickory_proto::rr::Record> + Send + 'a {}
#[async_trait::async_trait]
impl ResponseHandler for ResponseWrapper {
async fn send_response<'a>(
&mut self,
response: MessageResponse<
'_,
'a,
impl RecordIter<'a>,
impl RecordIter<'a>,
impl RecordIter<'a>,
impl RecordIter<'a>,
>,
) -> io::Result<ResponseInfo> {
let mut buffer = self
.response
.lock()
.map_err(|_| io::Error::other("lock poisoned"))?;
let mut encoder = BinEncoder::new(&mut buffer);
// `max_size` should be u16::MAX for protocol other than UDP.
let max_size = if let Some(edns) = response.get_edns() {
edns.max_payload()
} else {
hickory_proto::udp::MAX_RECEIVE_BUFFER_SIZE as u16
};
encoder.set_max_size(max_size);
response
.destructive_emit(&mut encoder)
.map_err(io::Error::other)
}
}
impl MagicDnsServerInstanceData {
/// Replace content of incoming UDP DNS request and ICMP echo request packet with reply data,
/// and swap source and destination IP addresses to send it back.
async fn handle_ip_packet(&self, zc_packet: &mut ZCPacket) -> Option<()> {
let (ip_header_length, ip_protocol, src_ip, dst_ip) = {
let ip_packet = Ipv4Packet::new(zc_packet.payload())?;
if ip_packet.get_version() != 4 {
return None;
}
(
ip_packet.get_header_length() as usize * 4,
ip_packet.get_next_level_protocol(),
ip_packet.get_source(),
ip_packet.get_destination(),
)
};
if dst_ip != self.fake_ip {
return None;
}
match ip_protocol {
IpNextHeaderProtocols::Udp => {
self.handle_udp_packet(zc_packet, ip_header_length, src_ip, dst_ip)
.await?;
}
IpNextHeaderProtocols::Icmp => {
self.handle_icmp_packet(zc_packet, ip_header_length)?;
}
_ => {
return None;
}
}
let mut ip_packet = MutableIpv4Packet::new(zc_packet.mut_payload())?;
ip_packet.set_source(dst_ip);
ip_packet.set_destination(src_ip);
ip_packet.set_checksum(ipv4::checksum(&ip_packet.to_immutable()));
zc_packet.mut_peer_manager_header().unwrap().to_peer_id = self.my_peer_id.into();
Some(())
}
/// Extract the DNS request message and send it to the hickory-dns server instance.
/// Replace the content of the UDP packet with the response message.
async fn handle_udp_packet(
&self,
zc_packet: &mut ZCPacket,
ip_header_length: usize,
src_ip: Ipv4Addr,
dst_ip: Ipv4Addr,
) -> Option<()> {
let (src_port, dst_port, request, request_length) = {
let udp_packet = UdpPacket::new(&zc_packet.payload()[ip_header_length..])?;
let src_port = udp_packet.get_source();
let dst_port = udp_packet.get_destination();
// Remove this to support any UDP port
if dst_port != 53 {
return None;
}
let request_payload = udp_packet.payload();
(
src_port,
dst_port,
Request::new(
MessageRequest::from_bytes(request_payload).ok()?,
SocketAddr::from(SocketAddrV4::new(src_ip, src_port)),
hickory_proto::xfer::Protocol::Udp,
),
request_payload.len(),
)
};
let response_payload = {
let response_payload_arc = Arc::new(Mutex::new(Vec::with_capacity(512)));
self.dns_server
.read_catalog()
.await
.handle_request(
&request,
ResponseWrapper {
response: response_payload_arc.clone(),
},
)
.await;
Arc::into_inner(response_payload_arc)?.into_inner().ok()?
};
let response_length = response_payload.len();
let delta_length = response_length as isize - request_length as isize;
let inner_length = (zc_packet.buf_len() as isize + delta_length) as usize;
if zc_packet.mut_inner().capacity() < inner_length {
let header_length = inner_length - response_length;
zc_packet.mut_inner().truncate(header_length);
}
zc_packet.mut_inner().resize(inner_length, 0);
let mut ip_packet = MutableIpv4Packet::new(zc_packet.mut_payload())?;
let ip_length = (ip_packet.get_total_length() as isize + delta_length) as u16;
ip_packet.set_total_length(ip_length);
let mut udp_packet = MutableUdpPacket::new(ip_packet.payload_mut())?;
let udp_length = (udp_packet.get_length() as isize + delta_length) as u16;
udp_packet.set_length(udp_length);
udp_packet.set_source(dst_port);
udp_packet.set_destination(src_port);
udp_packet.payload_mut().copy_from_slice(&response_payload);
udp_packet.set_checksum(udp::ipv4_checksum(
&udp_packet.to_immutable(),
&dst_ip,
&src_ip,
));
Some(())
}
fn handle_icmp_packet(&self, zc_packet: &mut ZCPacket, ip_header_length: usize) -> Option<()> {
let mut icmp_packet =
MutableIcmpPacket::new(&mut zc_packet.mut_payload()[ip_header_length..])?;
if icmp_packet.get_icmp_type() != IcmpTypes::EchoRequest {
return None;
}
icmp_packet.set_icmp_type(IcmpTypes::EchoReply);
icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable()));
Some(())
}
}
#[async_trait::async_trait]
impl NicPacketFilter for MagicDnsServerInstanceData {
async fn try_process_packet_from_nic(&self, zc_packet: &mut ZCPacket) -> bool {
self.handle_ip_packet(zc_packet).await.is_some()
}
fn id(&self) -> String {
NIC_PIPELINE_NAME.to_string()
}
}
#[async_trait::async_trait]
impl RpcServerHook for MagicDnsServerInstanceData {
async fn on_new_client(
&self,
tunnel_info: Option<TunnelInfo>,
) -> Result<Option<TunnelInfo>, anyhow::Error> {
tracing::info!(?tunnel_info, "New client connected");
Ok(tunnel_info)
}
async fn on_client_disconnected(&self, tunnel_info: Option<TunnelInfo>) {
tracing::info!(?tunnel_info, "Client disconnected");
let Some(tunnel_info) = tunnel_info else {
return;
};
let Some(remote_addr) = tunnel_info.remote_addr else {
return;
};
let remote_addr = remote_addr.into();
let mut removed_zones = vec![];
for mut item in self.route_infos.iter_mut() {
item.value_mut().remove(&remote_addr);
if item.value().is_empty() {
removed_zones.push(item.key().clone());
}
}
for zone in &removed_zones {
self.route_infos.remove(zone);
}
for zone in removed_zones {
self.keep_zone_authoritative(&zone).await;
}
self.update().await;
}
}
pub struct MagicDnsServerInstance {
rpc_server: StandAloneServer<TcpTunnelListener>,
pub(super) data: Arc<MagicDnsServerInstanceData>,
peer_mgr: Arc<PeerManager>,
tun_inet: Ipv4Inet,
}
fn get_system_config(
_tun_name: Option<&str>,
) -> Result<Option<Box<dyn SystemConfig>>, anyhow::Error> {
#[cfg(target_os = "windows")]
{
use super::system_config::windows::WindowsDNSManager;
let tun_name = _tun_name.ok_or_else(|| anyhow::anyhow!("No tun name"))?;
return Ok(Some(Box::new(WindowsDNSManager::new(tun_name)?)));
}
#[cfg(all(target_os = "macos", not(feature = "macos-ne")))]
{
use super::system_config::darwin::DarwinConfigurator;
return Ok(Some(Box::new(DarwinConfigurator::new())));
}
#[allow(unreachable_code)]
Ok(None)
}
impl MagicDnsServerInstance {
pub async fn new(
peer_mgr: Arc<PeerManager>,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
) -> Result<Self, anyhow::Error> {
let tcp_listener = TcpTunnelListener::new(MAGIC_DNS_INSTANCE_ADDR.parse()?);
let mut rpc_server = StandAloneServer::new(tcp_listener);
rpc_server.serve().await?;
let dns_config = RunConfigBuilder::default()
.general(GeneralConfigBuilder::default().build()?)
.excluded_forward_nameservers(vec![fake_ip.into()])
.build()?;
let mut dns_server = Server::new(dns_config);
dns_server.run().await?;
if !tun_inet.contains(&fake_ip)
&& let Some(tun_dev_name) = &tun_dev
{
let cost = if cfg!(target_os = "windows") {
Some(4)
} else {
None
};
let ifcfg = IfConfiger {};
ifcfg
.add_ipv4_route(tun_dev_name, fake_ip, 32, cost)
.await?;
}
let data = Arc::new(MagicDnsServerInstanceData {
dns_server,
tun_dev: tun_dev.clone(),
tun_ip: tun_inet.address(),
fake_ip,
my_peer_id: peer_mgr.my_peer_id(),
route_infos: DashMap::new(),
system_config: get_system_config(tun_dev.as_deref())?,
});
rpc_server
.registry()
.register(MagicDnsServerRpcServer::new_arc(data.clone()), "");
rpc_server.set_hook(data.clone());
peer_mgr
.add_nic_packet_process_pipeline(Box::new(data.clone()))
.await;
// Use configured tld_dns_zone or fall back to DEFAULT_ET_DNS_ZONE if empty
let flags = peer_mgr.get_global_ctx().config.get_flags();
let tld_dns_zone_clone = flags.tld_dns_zone.clone();
data.update_dns_records(std::iter::empty(), &tld_dns_zone_clone)
.await
.context("Failed to initialize DNS zone")?;
let data_clone = data.clone();
tokio::task::spawn_blocking(move || data_clone.do_system_config(&tld_dns_zone_clone))
.await
.context("Failed to configure system")??;
Ok(Self {
rpc_server,
data,
peer_mgr,
tun_inet,
})
}
pub async fn clean_env(&self) {
if let Some(configer) = &self.data.system_config {
let ret = configer.close();
if let Err(e) = ret {
tracing::error!("Failed to close system config: {:?}", e);
}
if !self.tun_inet.contains(&self.data.fake_ip)
&& let Some(tun_dev_name) = &self.data.tun_dev
{
let ifcfg = IfConfiger {};
let _ = ifcfg
.remove_ipv4_route(tun_dev_name, self.data.fake_ip, 32)
.await;
}
}
let _ = self
.peer_mgr
.remove_nic_packet_process_pipeline(NIC_PIPELINE_NAME.to_string())
.await;
}
}
impl Drop for MagicDnsServerInstance {
fn drop(&mut self) {
println!("MagicDnsServerInstance dropped");
}
}
@@ -1,20 +0,0 @@
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(all(target_os = "macos", not(feature = "macos-ne")))]
pub mod darwin;
#[derive(Default, Debug)]
pub struct OSConfig {
pub nameservers: Vec<String>,
pub search_domains: Vec<String>,
pub match_domains: Vec<String>,
}
pub trait SystemConfig: Send + Sync {
fn set_dns(&self, cfg: &OSConfig) -> std::io::Result<()>;
fn close(&self) -> std::io::Result<()>;
}
-327
View File
@@ -1,327 +0,0 @@
use std::net::{Ipv4Addr, SocketAddr};
use std::str::FromStr as _;
use std::sync::Arc;
use std::time::Duration;
use cidr::Ipv4Inet;
use hickory_client::client::{Client, ClientHandle as _};
use hickory_proto::rr;
use hickory_proto::runtime::TokioRuntimeProvider;
use hickory_proto::udp::UdpClientStream;
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
use crate::common::global_ctx::tests::get_mock_global_ctx;
use crate::connector::udp_hole_punch::tests::replace_stun_info_collector;
use crate::instance::dns_server::runner::DnsRunner;
use crate::instance::dns_server::server_instance::MagicDnsServerInstance;
use crate::instance::dns_server::{DEFAULT_ET_DNS_ZONE, MAGIC_DNS_FAKE_IP};
use crate::instance::virtual_nic::NicCtx;
use crate::peers::peer_manager::{PeerManager, RouteAlgoType};
use crate::peers::create_packet_recv_chan;
use crate::proto::api::instance::Route;
use crate::proto::common::NatType;
use crate::proto::magic_dns::{MagicDnsServerRpc as _, UpdateDnsRecordRequest};
use crate::proto::rpc_types::controller::{BaseController, Controller as _};
pub async fn prepare_env(dns_name: &str, tun_ip: Ipv4Inet) -> (Arc<PeerManager>, NicCtx) {
prepare_env_with_tld_dns_zone(dns_name, tun_ip, None).await
}
pub async fn prepare_env_with_tld_dns_zone(
dns_name: &str,
tun_ip: Ipv4Inet,
tld_dns_zone: Option<&str>,
) -> (Arc<PeerManager>, NicCtx) {
let ctx = get_mock_global_ctx();
ctx.set_hostname(dns_name.to_owned());
ctx.set_ipv4(Some(tun_ip));
if tld_dns_zone.is_some() {
let mut flags = ctx.config.get_flags();
flags.accept_dns = true; // Enable DNS
if let Some(zone) = tld_dns_zone {
flags.tld_dns_zone = zone.to_string();
}
ctx.set_flags(flags);
}
let (s, r) = create_packet_recv_chan();
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, ctx, s));
peer_mgr.run().await.unwrap();
replace_stun_info_collector(peer_mgr.clone(), NatType::PortRestricted);
let r = Arc::new(tokio::sync::Mutex::new(r));
let mut virtual_nic = NicCtx::new(
peer_mgr.get_global_ctx(),
&peer_mgr,
r,
Arc::new(Notify::new()),
);
virtual_nic.run(Some(tun_ip), None).await.unwrap();
(peer_mgr, virtual_nic)
}
pub async fn check_dns_record(fake_ip: &Ipv4Addr, domain: &str, expected_ip: &str) {
let stream = UdpClientStream::builder(
SocketAddr::new((*fake_ip).into(), 53),
TokioRuntimeProvider::default(),
)
.build();
let (mut client, background) = Client::connect(stream).await.unwrap();
let background_task = tokio::spawn(background);
let response = client
.query(
rr::Name::from_str(domain).unwrap(),
rr::DNSClass::IN,
rr::RecordType::A,
)
.await
.unwrap_or_else(|e| panic!("DNS query failed unexpectedly for domain '{domain}': {e}"));
background_task.abort();
let _ = background_task.await;
println!("Response: {:?}", response);
assert_eq!(response.answers().len(), 1, "{:?}", response.answers());
let resp = response.answers().first().unwrap();
assert_eq!(
resp.clone().into_parts().rdata.into_a().unwrap().0,
expected_ip.parse::<Ipv4Addr>().unwrap()
);
}
pub async fn check_dns_record_missing(fake_ip: &Ipv4Addr, domain: &str) {
let stream = UdpClientStream::builder(
SocketAddr::new((*fake_ip).into(), 53),
TokioRuntimeProvider::default(),
)
.build();
let (mut client, background) = Client::connect(stream).await.unwrap();
let background_task = tokio::spawn(background);
let response = client
.query(
rr::Name::from_str(domain).unwrap(),
rr::DNSClass::IN,
rr::RecordType::A,
)
.await
.unwrap_or_else(|e| {
panic!("DNS query for missing record failed unexpectedly for domain '{domain}': {e}")
});
background_task.abort();
let _ = background_task.await;
assert!(response.answers().is_empty(), "{:?}", response.answers());
}
#[tokio::test]
async fn test_magic_dns_server_instance() {
let tun_ip = Ipv4Inet::from_str("10.144.144.10/24").unwrap();
let (peer_mgr, virtual_nic) = prepare_env("test1", tun_ip).await;
let tun_name = virtual_nic.ifname().await.unwrap();
let fake_ip = Ipv4Addr::from_str("100.100.100.101").unwrap();
let dns_server_inst =
MagicDnsServerInstance::new(peer_mgr.clone(), Some(tun_name), tun_ip, fake_ip)
.await
.unwrap();
let routes = [
Route {
hostname: "test1".to_string(),
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/24").unwrap().into()),
..Default::default()
},
Route {
hostname: "中文".to_string(),
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/24").unwrap().into()),
..Default::default()
},
Route {
hostname: ".invalid".to_string(),
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/24").unwrap().into()),
..Default::default()
},
];
dns_server_inst
.data
.update_dns_records(routes.iter(), DEFAULT_ET_DNS_ZONE)
.await
.unwrap();
check_dns_record(&fake_ip, "test1.et.net", "8.8.8.8").await;
check_dns_record(&fake_ip, "中文.et.net", "8.8.8.8").await;
}
#[tokio::test]
async fn test_magic_dns_runner() {
// Test first runner with default DNS settings
{
let tun_ip = Ipv4Inet::from_str("10.144.144.10/24").unwrap();
let (peer_mgr, virtual_nic) = prepare_env("test1", tun_ip).await;
let tun_name = virtual_nic.ifname().await.unwrap();
let fake_ip = Ipv4Addr::from_str(MAGIC_DNS_FAKE_IP).unwrap();
let mut dns_runner = DnsRunner::new(peer_mgr, Some(tun_name), tun_ip, fake_ip);
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
let t = tokio::spawn(async move {
dns_runner.run(cancel_token_clone).await;
});
tokio::time::sleep(Duration::from_secs(3)).await;
// Test default settings: query should resolve test1.et.net to tunnel IP via default fake IP
check_dns_record(&fake_ip, "test1.et.net", "10.144.144.10").await;
cancel_token.cancel();
t.await.unwrap();
// Wait a bit for cleanup
tokio::time::sleep(Duration::from_secs(1)).await;
}
// Test second runner with different TLD zone
{
let tun_ip = Ipv4Inet::from_str("10.144.144.20/24").unwrap();
// NOTE: Using same fake IP to avoid system DNS configuration conflicts
let custom_tld_zone = "custom.local."; // Different TLD zone is safer
let (peer_mgr, virtual_nic) =
prepare_env_with_tld_dns_zone("test2", tun_ip, Some(custom_tld_zone)).await;
let tun_name = virtual_nic.ifname().await.unwrap();
let fake_ip = Ipv4Addr::from_str(MAGIC_DNS_FAKE_IP).unwrap();
let mut dns_runner = DnsRunner::new(peer_mgr, Some(tun_name), tun_ip, fake_ip);
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
let t = tokio::spawn(async move {
dns_runner.run(cancel_token_clone).await;
});
tokio::time::sleep(Duration::from_secs(3)).await;
// Test with same fake IP but different TLD zone
check_dns_record(&fake_ip, "test2.custom.local", "10.144.144.20").await;
cancel_token.cancel();
t.await.unwrap();
}
}
#[tokio::test]
async fn test_magic_dns_update_replaces_records_for_same_client() {
let tun_ip = Ipv4Inet::from_str("100.100.100.0/24").unwrap();
let ctx = get_mock_global_ctx();
ctx.set_hostname("test1".to_string());
ctx.set_ipv4(Some(tun_ip));
let (s, _r) = create_packet_recv_chan();
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, ctx, s));
peer_mgr.run().await.unwrap();
replace_stun_info_collector(peer_mgr.clone(), NatType::PortRestricted);
let fake_ip = Ipv4Addr::from_str(MAGIC_DNS_FAKE_IP).unwrap();
let dns_server_inst = MagicDnsServerInstance::new(peer_mgr.clone(), None, tun_ip, fake_ip)
.await
.unwrap();
let mut ctrl = BaseController::default();
ctrl.set_tunnel_info(Some(crate::proto::common::TunnelInfo {
tunnel_type: "tcp".to_string(),
local_addr: None,
remote_addr: Some(crate::proto::common::Url {
url: "tcp://127.0.0.1:54321".to_string(),
}),
resolved_remote_addr: None,
}));
dns_server_inst
.data
.update_dns_record(
ctrl.clone(),
UpdateDnsRecordRequest {
zone: DEFAULT_ET_DNS_ZONE.to_string(),
routes: vec![Route {
hostname: "test1".to_string(),
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/32").unwrap().into()),
..Default::default()
}],
},
)
.await
.unwrap();
dns_server_inst
.data
.update_dns_record(
ctrl,
UpdateDnsRecordRequest {
zone: DEFAULT_ET_DNS_ZONE.to_string(),
routes: vec![Route {
hostname: "test1".to_string(),
ipv4_addr: Some(Ipv4Inet::from_str("1.1.1.1/32").unwrap().into()),
..Default::default()
}],
},
)
.await
.unwrap();
let dns_records = dns_server_inst
.data
.get_dns_record(
BaseController::default(),
crate::proto::common::Void::default(),
)
.await
.unwrap();
let zone_records = dns_records.records.get(DEFAULT_ET_DNS_ZONE).unwrap();
let a_records = zone_records
.records
.iter()
.filter_map(|record| match record.record.as_ref() {
Some(crate::proto::magic_dns::dns_record::Record::A(a))
if a.name == "test1.et.net." =>
{
Some(a)
}
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(a_records.len(), 1, "{a_records:?}");
let resolved_ip = Ipv4Addr::from(a_records[0].value.unwrap_or_default());
assert_eq!(resolved_ip, Ipv4Addr::new(1, 1, 1, 1));
let mut ctrl = BaseController::default();
ctrl.set_tunnel_info(Some(crate::proto::common::TunnelInfo {
tunnel_type: "tcp".to_string(),
local_addr: None,
remote_addr: Some(crate::proto::common::Url {
url: "tcp://127.0.0.1:54321".to_string(),
}),
resolved_remote_addr: None,
}));
dns_server_inst
.data
.update_dns_record(
ctrl,
UpdateDnsRecordRequest {
zone: DEFAULT_ET_DNS_ZONE.to_string(),
routes: vec![],
},
)
.await
.unwrap();
let dns_records = dns_server_inst
.data
.get_dns_record(
BaseController::default(),
crate::proto::common::Void::default(),
)
.await
.unwrap();
assert!(!dns_records.records.contains_key(DEFAULT_ET_DNS_ZONE));
}
+67 -164
View File
@@ -7,16 +7,12 @@ use std::sync::{Arc, Weak};
#[cfg(feature = "tun")]
use std::time::Duration;
use anyhow::Context;
use cidr::{IpCidr, Ipv4Inet};
use futures::FutureExt;
use tokio::sync::{Mutex, Notify};
#[cfg(feature = "tun")]
use tokio::{sync::oneshot, task::JoinSet};
#[cfg(feature = "magic-dns")]
use tokio_util::sync::CancellationToken;
use tokio_util::task::AbortOnDropHandle;
use super::listeners::ListenerManager;
use super::public_ipv6_provider::{
reconcile_public_ipv6_provider_runtime, run_public_ipv6_provider_reconcile_task,
should_run_public_ipv6_provider_reconcile, validate_public_ipv6_config,
validate_public_ipv6_config_values,
};
use crate::common::PeerId;
use crate::common::acl_processor::AclRuleBuilder;
use crate::common::config::ConfigLoader;
@@ -26,6 +22,8 @@ use crate::connector::direct::DirectConnectorManager;
use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManager};
use crate::connector::tcp_hole_punch::TcpHolePunchConnector;
use crate::connector::udp_hole_punch::UdpHolePunchConnector;
#[cfg(feature = "magic-dns")]
use crate::dns::{config::DnsConfigLoaderExt, node::DnsNode};
use crate::gateway::icmp_proxy::IcmpProxy;
#[cfg(feature = "kcp")]
use crate::gateway::kcp_proxy::{KcpProxyDst, KcpProxyDstRpcService, KcpProxySrc};
@@ -60,15 +58,13 @@ use crate::proto::rpc_types::controller::BaseController;
use crate::rpc_service::InstanceRpcService;
use crate::utils::weak_upgrade;
use crate::vpn_portal::{self, VpnPortal};
#[cfg(feature = "magic-dns")]
use super::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
use super::listeners::ListenerManager;
use super::public_ipv6_provider::{
reconcile_public_ipv6_provider_runtime, run_public_ipv6_provider_reconcile_task,
should_run_public_ipv6_provider_reconcile, validate_public_ipv6_config,
validate_public_ipv6_config_values,
};
use anyhow::Context;
use cidr::{IpCidr, Ipv4Inet};
use futures::FutureExt;
use tokio::sync::{Mutex, Notify};
#[cfg(feature = "tun")]
use tokio::{sync::oneshot, task::JoinSet};
use tokio_util::task::AbortOnDropHandle;
#[cfg(feature = "socks5")]
use crate::gateway::socks5::Socks5Server;
@@ -135,65 +131,10 @@ impl IpProxy {
}
#[cfg(feature = "tun")]
type NicCtx = super::virtual_nic::NicCtx;
#[cfg(feature = "magic-dns")]
struct MagicDnsContainer {
dns_runner_task: AbortOnDropHandle<()>,
dns_runner_cancel_token: CancellationToken,
}
// nic container will be cleared when dhcp ip changed
#[cfg(feature = "tun")]
pub struct NicCtxContainer {
nic_ctx: Option<Box<dyn Any + 'static + Send>>,
#[cfg(feature = "magic-dns")]
magic_dns: Option<MagicDnsContainer>,
}
pub type NicCtx = super::virtual_nic::NicCtx;
#[cfg(feature = "tun")]
impl NicCtxContainer {
#[cfg(not(feature = "magic-dns"))]
fn new(nic_ctx: NicCtx) -> Self {
Self {
nic_ctx: Some(Box::new(nic_ctx)),
}
}
#[cfg(feature = "magic-dns")]
fn new(nic_ctx: NicCtx, dns_runner: Option<DnsRunner>) -> Self {
if let Some(mut dns_runner) = dns_runner {
let token = CancellationToken::new();
let token_clone = token.clone();
let task = tokio::spawn(async move {
let _ = dns_runner.run(token_clone).await;
});
Self {
nic_ctx: Some(Box::new(nic_ctx)),
magic_dns: Some(MagicDnsContainer {
dns_runner_task: AbortOnDropHandle::new(task),
dns_runner_cancel_token: token,
}),
}
} else {
Self {
nic_ctx: Some(Box::new(nic_ctx)),
magic_dns: None,
}
}
}
fn new_with_any<T: 'static + Send>(ctx: T) -> Self {
Self {
nic_ctx: Some(Box::new(ctx)),
#[cfg(feature = "magic-dns")]
magic_dns: None,
}
}
}
#[cfg(feature = "tun")]
type ArcNicCtx = Arc<Mutex<Option<NicCtxContainer>>>;
pub type ArcNicCtx = Arc<Mutex<Option<Box<dyn Any + 'static + Send>>>>;
pub struct InstanceRpcServerHook {
rpc_portal_whitelist: Vec<IpCidr>,
@@ -616,6 +557,8 @@ pub struct Instance {
#[cfg(feature = "tun")]
nic_ctx: ArcNicCtx,
#[cfg(feature = "magic-dns")]
dns: Option<DnsNode>,
peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
peer_manager: Arc<PeerManager>,
@@ -698,13 +641,19 @@ impl Instance {
#[cfg(feature = "socks5")]
let socks5_server = Socks5Server::new(global_ctx.clone(), peer_manager.clone(), None);
#[cfg(feature = "tun")]
let nic_ctx = Arc::new(Mutex::new(None));
Instance {
inst_name: global_ctx.inst_name.clone(),
id,
peer_packet_receiver: Arc::new(Mutex::new(peer_packet_receiver)),
#[cfg(feature = "tun")]
nic_ctx: Arc::new(Mutex::new(None)),
nic_ctx,
#[cfg(feature = "magic-dns")]
dns: None,
peer_manager,
listener_manager,
@@ -760,16 +709,6 @@ impl Instance {
arc_nic_ctx: ArcNicCtx,
packet_recv: Arc<Mutex<PacketRecvChanReceiver>>,
) {
#[cfg(feature = "magic-dns")]
if let Some(old_ctx) = arc_nic_ctx.lock().await.take()
&& let Some(dns_runner) = old_ctx.magic_dns
{
dns_runner.dns_runner_cancel_token.cancel();
tracing::debug!("cancelling dns runner task");
let ret = dns_runner.dns_runner_task.await;
tracing::debug!("dns runner task cancelled, ret: {:?}", ret);
};
let mut tasks = JoinSet::new();
tasks.spawn(async move {
let mut packet_recv = packet_recv.lock().await;
@@ -777,46 +716,15 @@ impl Instance {
tracing::trace!("packet consumed by mock nic ctx: {:?}", packet);
}
});
arc_nic_ctx
.lock()
.await
.replace(NicCtxContainer::new_with_any(tasks));
arc_nic_ctx.lock().await.replace(Box::new(tasks));
tracing::debug!("nic ctx cleared.");
}
#[cfg(feature = "magic-dns")]
fn create_magic_dns_runner(
peer_mgr: Arc<PeerManager>,
tun_dev: Option<String>,
tun_ip: Ipv4Inet,
) -> Option<DnsRunner> {
let ctx = peer_mgr.get_global_ctx();
if !ctx.config.get_flags().accept_dns {
return None;
}
let runner = DnsRunner::new(
peer_mgr,
tun_dev,
tun_ip,
MAGIC_DNS_FAKE_IP.parse().unwrap(),
);
Some(runner)
}
#[cfg(feature = "tun")]
async fn use_new_nic_ctx(
arc_nic_ctx: ArcNicCtx,
nic_ctx: NicCtx,
#[cfg(feature = "magic-dns")] magic_dns: Option<DnsRunner>,
) {
async fn use_new_nic_ctx(arc_nic_ctx: ArcNicCtx, nic_ctx: NicCtx) {
let mut g = arc_nic_ctx.lock().await;
*g = Some(NicCtxContainer::new(
nic_ctx,
#[cfg(feature = "magic-dns")]
magic_dns,
));
*g = Some(Box::new(nic_ctx));
tracing::debug!("nic ctx updated.");
}
@@ -921,15 +829,7 @@ impl Instance {
global_ctx_c.set_ipv4(None);
continue;
}
#[cfg(feature = "magic-dns")]
let ifname = new_nic_ctx.ifname().await;
Self::use_new_nic_ctx(
nic_ctx.clone(),
new_nic_ctx,
#[cfg(feature = "magic-dns")]
Self::create_magic_dns_runner(peer_manager_c.clone(), ifname, ip),
)
.await;
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
}
current_dhcp_ip = Some(ip);
@@ -961,10 +861,11 @@ impl Instance {
tokio::spawn(async move {
let mut output_tx = Some(first_round_output);
loop {
let close_notifier = Arc::new(Notify::new());
{
let Some(peer_mgr) = peer_mgr.upgrade() else {
let mut new_nic_ctx = {
let Some(peer_manager) = peer_mgr.upgrade() else {
tracing::warn!("peer manager is dropped, stop static ip check.");
if let Some(output_tx) = output_tx.take() {
let _ = output_tx.send(Err(Error::Unknown));
@@ -973,38 +874,26 @@ impl Instance {
return;
};
let mut new_nic_ctx = NicCtx::new(
peer_mgr.get_global_ctx(),
&peer_mgr,
NicCtx::new(
peer_manager.get_global_ctx(),
&peer_manager,
peer_packet_receiver.clone(),
close_notifier.clone(),
);
)
};
if let Err(e) = new_nic_ctx.run(ipv4_addr, ipv6_addr).await {
if let Some(output_tx) = output_tx.take() {
let _ = output_tx.send(Err(e));
return;
}
tracing::error!("failed to create new nic ctx, err: {:?}", e);
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
if let Err(e) = new_nic_ctx.run(ipv4_addr, ipv6_addr).await {
if let Some(output_tx) = output_tx.take() {
let _ = output_tx.send(Err(e));
return;
}
// Create Magic DNS runner only if we have IPv4
#[cfg(feature = "magic-dns")]
{
let ifname = new_nic_ctx.ifname().await;
let dns_runner = if let Some(ipv4) = ipv4_addr {
Self::create_magic_dns_runner(peer_mgr, ifname, ipv4)
} else {
None
};
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx, dns_runner).await;
}
#[cfg(not(feature = "magic-dns"))]
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
tracing::error!("failed to create new nic ctx, err: {:?}", e);
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
if let Some(output_tx) = output_tx.take() {
let _ = output_tx.send(Ok(()));
}
@@ -1044,6 +933,18 @@ impl Instance {
}
}
#[cfg(feature = "magic-dns")]
if !self.global_ctx.config.get_dns().disabled {
let mut node = DnsNode::new(
self.get_peer_manager(),
self.get_global_ctx(),
#[cfg(feature = "tun")]
self.get_nic_ctx(),
);
node.start();
self.dns = Some(node);
}
if self.global_ctx.config.get_dhcp() {
self.check_dhcp_ip_conflict();
}
@@ -1583,26 +1484,28 @@ impl Instance {
.await
.with_context(|| "add ip failed")?;
let magic_dns_runner = if let Some(ipv4) = global_ctx.get_ipv4() {
Self::create_magic_dns_runner(peer_manager.clone(), None, ipv4)
} else {
None
};
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx, magic_dns_runner).await;
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
Ok(())
}
pub async fn clear_resources(&mut self) {
self.peer_manager.clear_resources().await;
#[cfg(feature = "magic-dns")]
if let Some(mut node) = self.dns.take() {
let _ = node.stop().await;
}
#[cfg(feature = "tun")]
let _ = self.nic_ctx.lock().await.take();
}
}
// TODO: duplicated with clear_resources?
impl Drop for Instance {
fn drop(&mut self) {
let my_peer_id = self.peer_manager.my_peer_id();
let pm = Arc::downgrade(&self.peer_manager);
#[cfg(feature = "magic-dns")]
let _ = self.dns.take(); // force abort
#[cfg(feature = "tun")]
let nic_ctx = self.nic_ctx.clone();
tokio::spawn(async move {
-1
View File
@@ -1,4 +1,3 @@
pub mod dns_server;
#[allow(clippy::module_inception)]
pub mod instance;
+2 -3
View File
@@ -1,9 +1,8 @@
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent};
use crate::peers::peer_manager::PeerManager;
use std::collections::BTreeSet;
use std::sync::{Arc, Weak};
use std::time::Instant;
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent};
use crate::peers::peer_manager::PeerManager;
use tokio_util::task::AbortOnDropHandle;
/// ProxyCidrsMonitor monitors changes in proxy CIDRs from peer routes
+28
View File
@@ -835,6 +835,34 @@ impl NicCtx {
nic.ifname.as_ref().map(|s| s.to_owned())
}
pub async fn add_ipv4_to_tun_device(&self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> {
let nic = self.nic.lock().await;
nic.add_ip(ipv4_addr.address(), ipv4_addr.network_length() as i32)
.await
}
pub async fn add_ipv6_to_tun_device(&self, ipv6_addr: cidr::Ipv6Inet) -> Result<(), Error> {
let nic = self.nic.lock().await;
nic.add_ipv6(ipv6_addr.address(), ipv6_addr.network_length() as i32)
.await
}
pub async fn remove_ipv4_from_tun_device(
&self,
ipv4_addr: cidr::Ipv4Inet,
) -> Result<(), Error> {
let nic = self.nic.lock().await;
nic.remove_ip(Some(ipv4_addr)).await
}
pub async fn remove_ipv6_from_tun_device(
&self,
ipv6_addr: cidr::Ipv6Inet,
) -> Result<(), Error> {
let nic = self.nic.lock().await;
nic.remove_ipv6(Some(ipv6_addr)).await
}
pub async fn assign_ipv4_to_tun_device(&self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> {
let nic = self.nic.lock().await;
nic.link_up().await?;
+2
View File
@@ -316,6 +316,8 @@ fn handle_event(
event!(info, peer_id, "[{}] peer removed", instance_id);
}
GlobalCtxEvent::PeerInfoUpdated(_) => {}
GlobalCtxEvent::PeerConnAdded(conn_info) => {
event!(
info,
-6
View File
@@ -828,10 +828,6 @@ impl NetworkConfig {
flags.disable_sym_hole_punching = disable_sym_hole_punching;
}
if let Some(enable_magic_dns) = self.enable_magic_dns {
flags.accept_dns = enable_magic_dns;
}
if let Some(mtu) = self.mtu {
flags.mtu = mtu as u32;
}
@@ -1001,7 +997,6 @@ impl NetworkConfig {
result.disable_relay_data = Some(flags.disable_relay_data);
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
result.enable_magic_dns = Some(flags.accept_dns);
result.mtu = Some(flags.mtu as i32);
result.instance_recv_bps_limit =
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
@@ -1269,7 +1264,6 @@ mod tests {
flags.disable_udp_hole_punching = rng.gen_bool(0.2);
flags.disable_upnp = rng.gen_bool(0.2);
flags.enable_udp_broadcast_relay = rng.gen_bool(0.2);
flags.accept_dns = rng.gen_bool(0.6);
flags.mtu = rng.gen_range(1200..1500);
flags.private_mode = rng.gen_bool(0.3);
+2
View File
@@ -6,6 +6,8 @@ use clap::Command;
use clap_complete::{Generator, Shell};
mod arch;
#[cfg(feature = "magic-dns")]
mod dns;
mod gateway;
pub mod instance;
mod peer_center;
+34 -12
View File
@@ -30,6 +30,8 @@ use tokio::{
task::{JoinHandle, JoinSet},
};
use crate::common::config::ConfigLoader;
use crate::proto::utils::TransientDigest;
use crate::{
common::{
PeerId,
@@ -188,9 +190,11 @@ fn is_foreign_network_info_newer(
}
impl RoutePeerInfo {
#[allow(deprecated)]
pub fn new() -> Self {
Self {
#[allow(deprecated)]
quic_port: None,
peer_id: 0,
inst_id: Some(uuid::Uuid::nil().into()),
cost: 0,
@@ -209,8 +213,8 @@ impl RoutePeerInfo {
network_length: 24,
ipv6_addr: None,
groups: Vec::new(),
dns: Default::default(),
quic_port: None,
noise_static_pubkey: Vec::new(),
trusted_credential_pubkeys: Vec::new(),
ipv6_public_addr_prefix: None,
@@ -240,7 +244,19 @@ impl RoutePeerInfo {
.and_then(|cfg| cfg.public_key().ok())
.map(|pk| pk.as_bytes().to_vec())
.unwrap_or_default();
let dns = cfg_select! {
feature = "magic-dns" => {{
use crate::dns::config::DnsGlobalCtxExt;
global_ctx.dns_export_config().digest().into()
}}
_ => Default::default(),
};
Self {
#[allow(deprecated)]
quic_port: None,
peer_id: my_peer_id,
inst_id: Some(global_ctx.get_id().into()),
cost: 0,
@@ -280,6 +296,7 @@ impl RoutePeerInfo {
ipv6_public_addr_lease: public_ipv6_addr_lease.map(Into::into),
groups: global_ctx.get_acl_groups(my_peer_id),
dns,
noise_static_pubkey,
@@ -293,8 +310,6 @@ impl RoutePeerInfo {
} else {
Vec::new()
},
..Default::default()
}
}
@@ -347,7 +362,7 @@ impl From<RoutePeerInfo> for crate::proto::api::instance::Route {
next_hop_peer_id: 0, // next_hop_peer_id is calculated in RouteTable.
cost: 0, // cost is calculated in RouteTable.
path_latency: 0, // path_latency is calculated in RouteTable.
proxy_cidrs: val.proxy_cidrs.clone(),
proxy_cidrs: val.proxy_cidrs,
hostname: val.hostname.unwrap_or_default(),
stun_info: {
let mut stun_info = StunInfo::default();
@@ -837,8 +852,8 @@ impl SyncedRouteInfo {
dst_peer_id: PeerId,
peer_infos: &[RoutePeerInfo],
raw_peer_infos: &[DynamicMessage],
) -> Result<(), Error> {
let mut need_inc_version = false;
) -> Result<Vec<PeerId>, Error> {
let mut updated_peer_ids = Vec::new();
for (idx, route_info) in peer_infos.iter().enumerate() {
let mut route_info = route_info.clone();
let raw_route_info = &raw_peer_infos[idx];
@@ -874,14 +889,15 @@ impl SyncedRouteInfo {
{
self.raw_peer_infos
.insert(route_info.peer_id, raw_route_info.clone());
guard.insert(route_info.peer_id, route_info);
need_inc_version = true;
let peer_id = route_info.peer_id;
guard.insert(peer_id, route_info);
updated_peer_ids.push(peer_id);
}
}
if need_inc_version {
if !updated_peer_ids.is_empty() {
self.version.inc();
}
Ok(())
Ok(updated_peer_ids)
}
fn update_conn_info_one_peer(
@@ -3541,7 +3557,7 @@ impl RouteSessionManager {
.get_network_identity()
.network_secret
.is_none();
service_impl.synced_route_info.update_peer_infos(
let updated_peer_ids = service_impl.synced_route_info.update_peer_infos(
my_peer_id,
service_impl.my_peer_route_id,
from_peer_id,
@@ -3557,6 +3573,12 @@ impl RouteSessionManager {
);
session.update_dst_saved_peer_info_version(pi, from_peer_id);
need_update_route_table = true;
if !updated_peer_ids.is_empty() {
service_impl
.global_ctx
.issue_event(GlobalCtxEvent::PeerInfoUpdated(updated_peer_ids));
}
}
}
+1 -1
View File
@@ -72,7 +72,7 @@ message NetworkConfig {
optional int32 mtu = 40;
repeated string mapped_listeners = 41;
optional bool enable_magic_dns = 42;
optional bool enable_magic_dns = 42 [deprecated = true];
optional bool enable_private_mode = 43;
// repeated string rpc_portal_whitelists = 44;
+2 -2
View File
@@ -33,7 +33,7 @@ message FlagsInConfig {
bool proxy_forward_by_system = 21;
// enable magic dns or not
bool accept_dns = 22;
bool accept_dns = 22 [deprecated = true];
// enable private mode
bool private_mode = 23;
@@ -65,7 +65,7 @@ message FlagsInConfig {
bool disable_sym_hole_punching = 30;
// tld dns zone for magic dns
string tld_dns_zone = 31;
string tld_dns_zone = 31 [deprecated = true];
bool p2p_only = 32;
+41
View File
@@ -0,0 +1,41 @@
syntax = "proto3";
import "common.proto";
package dns;
message ZoneData {
string content = 1;
repeated common.Url forwarders = 2;
repeated int32 fallthrough = 3;
}
message GetExportConfigRequest {}
message GetExportConfigResponse {
repeated ZoneData zones = 1;
}
service DnsPeerMgrRpc {
rpc GetExportConfig(GetExportConfigRequest) returns (GetExportConfigResponse) {}
}
message DnsSnapshot {
repeated ZoneData zones = 1;
repeated common.Url addresses = 2;
repeated common.Url listeners = 3;
}
message HeartbeatRequest {
common.UUID id = 1;
bytes digest = 2;
optional DnsSnapshot snapshot = 3;
}
message HeartbeatResponse {
bool resync = 1;
}
service DnsNodeMgrRpc {
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse) {}
}
+51
View File
@@ -0,0 +1,51 @@
use crate::dns::config::zone::Fallthrough;
use crate::proto::common::Url;
use crate::proto::utils::TransientDigest;
use hickory_proto::rr::LowerName;
use std::fmt::Write;
include!(concat!(env!("OUT_DIR"), "/dns.rs"));
impl HeartbeatRequest {
pub fn update(&mut self, snapshot: DnsSnapshot) {
self.digest = snapshot.digest().into();
self.snapshot = Some(snapshot);
}
}
impl ZoneData {
pub fn new<Record: AsRef<str>>(
origin: &LowerName,
ttl: u32,
records: impl IntoIterator<Item = Record>,
forwarders: impl IntoIterator<Item = Url>,
fallthrough: impl IntoIterator<Item = Fallthrough>,
) -> Self {
let mut content = String::new();
content.push_str("; EasyTier Magic DNS zone data\n");
content.push_str("; https://github.com/easytier/easytier\n");
let mut origin = origin.to_string();
if !origin.ends_with('.') {
origin.push('.');
}
writeln!(content, "$ORIGIN {}", origin).unwrap();
writeln!(content, "$TTL {}", ttl).unwrap();
for record in records {
content.push_str(record.as_ref());
content.push('\n');
}
let forwarders = forwarders.into_iter().collect();
let fallthrough = fallthrough.into_iter().map(Into::into).collect();
Self {
content,
forwarders,
fallthrough,
}
}
}
-49
View File
@@ -1,49 +0,0 @@
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "common.proto";
import "api_instance.proto";
package magic_dns;
message DnsRecordA {
string name = 1;
common.Ipv4Addr value = 2;
int32 ttl = 3;
}
message DnsRecordSOA {
string name = 1;
string value = 2;
}
message DnsRecord {
oneof record {
DnsRecordA a = 1;
DnsRecordSOA soa = 2;
}
}
message DnsRecordList {
repeated DnsRecord records = 1;
}
message UpdateDnsRecordRequest {
string zone = 1;
repeated api.instance.Route routes = 2;
}
message GetDnsRecordResponse {
map<string, DnsRecordList> records = 1;
}
message HandshakeRequest {}
message HandshakeResponse {}
service MagicDnsServerRpc {
rpc Handshake(HandshakeRequest) returns (HandshakeResponse) {}
rpc Heartbeat(common.Void) returns (common.Void) {}
rpc UpdateDnsRecord(UpdateDnsRecordRequest) returns (common.Void) {}
rpc GetDnsRecord(common.Void) returns (GetDnsRecordResponse) {}
}
-1
View File
@@ -1 +0,0 @@
include!(concat!(env!("OUT_DIR"), "/magic_dns.rs"));
+2 -2
View File
@@ -4,9 +4,9 @@ pub mod rpc_types;
pub mod acl;
pub mod api;
pub mod common;
pub mod error;
#[cfg(feature = "magic-dns")]
pub mod magic_dns;
pub mod dns;
pub mod error;
pub mod peer_rpc;
pub mod web;
+3
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "common.proto";
import "dns.proto";
package peer_rpc;
@@ -48,6 +49,8 @@ message RoutePeerInfo {
// Trusted credential public keys published by admin nodes (holding network_secret)
repeated TrustedCredentialPubkeyProof trusted_credential_pubkeys = 19;
bytes dns = 20;
optional common.Ipv6Inet ipv6_public_addr_prefix = 22;
optional common.Ipv6Inet ipv6_public_addr_lease = 24;
}
+63 -24
View File
@@ -2,7 +2,7 @@ use delegate::delegate;
use derivative::Derivative;
use derive_more::{AsMut, AsRef, Deref, DerefMut, From, IntoIterator};
use prost::Message;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};
/// Generates a stable digest strictly within the lifecycle of the current process.
@@ -36,43 +36,27 @@ where
}
#[derive(
Derivative,
Debug,
Clone,
PartialEq,
Eq,
Hash,
From,
Deref,
DerefMut,
AsRef,
AsMut,
Serialize,
Deserialize,
IntoIterator,
Derivative, Debug, Clone, PartialEq, Eq, Hash, From, Deref, DerefMut, AsRef, AsMut, IntoIterator,
)]
#[derivative(Default(bound = ""))]
#[as_ref(forward)]
#[as_mut(forward)]
#[serde(transparent)]
#[into_iterator(owned, ref, ref_mut)]
pub struct RepeatedMessageModel<Model>(Vec<Model>);
impl<Model> RepeatedMessageModel<Model> {
pub fn into_inner(self) -> Vec<Model> {
self.0
}
pub struct RepeatedMessageModel<Model> {
pub models: Vec<Model>,
}
impl<Model> FromIterator<Model> for RepeatedMessageModel<Model> {
fn from_iter<I: IntoIterator<Item = Model>>(iter: I) -> Self {
Self(iter.into_iter().collect())
Self {
models: iter.into_iter().collect(),
}
}
}
impl<Model> Extend<Model> for RepeatedMessageModel<Model> {
delegate! {
to self.0 {
to self.models {
fn extend<T: IntoIterator<Item = Model>>(&mut self, iter: T);
}
}
@@ -99,3 +83,58 @@ where
value.into_iter().map(Into::into).collect()
}
}
pub trait RepeatedSerialize: Serialize + Sized {
fn serialize<S>(models: &RepeatedMessageModel<Self>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Serialize::serialize(&**models, serializer)
}
}
pub trait RepeatedDeserialize<'de>: Deserialize<'de> {
fn deserialize<D>(deserializer: D) -> Result<RepeatedMessageModel<Self>, D::Error>
where
D: Deserializer<'de>,
{
Vec::deserialize(deserializer).map(Into::into)
}
#[doc(hidden)]
fn deserialize_in_place<D>(
deserializer: D,
place: &mut RepeatedMessageModel<Self>,
) -> Result<(), D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize_in_place(deserializer, &mut place.models)
}
}
impl<Model: RepeatedSerialize> Serialize for RepeatedMessageModel<Model> {
delegate! {
#[through(RepeatedSerialize)]
to self {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer;
}
}
}
impl<'de, Model: RepeatedDeserialize<'de>> Deserialize<'de> for RepeatedMessageModel<Model> {
delegate! {
to RepeatedDeserialize {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>;
#[doc(hidden)]
fn deserialize_in_place<D>(deserializer: D, place: &mut Self) -> Result<(), D::Error>
where
D: Deserializer<'de>;
}
}
}
+260
View File
@@ -4213,3 +4213,263 @@ pub async fn relay_peer_session_cleanup() {
drop_insts(insts).await;
}
#[cfg(feature = "magic-dns")]
async fn check_dns_record_at(server_addr: SocketAddr, domain: &str, expected_ip: &str) {
use hickory_net::client::{Client, ClientHandle};
use hickory_net::runtime::TokioRuntimeProvider;
use hickory_net::udp::UdpClientStream;
use hickory_proto::rr::{DNSClass, Name, RData, RecordType};
use std::str::FromStr;
use std::time::{Duration, Instant};
let expected = expected_ip.parse::<std::net::Ipv4Addr>().unwrap();
let name = Name::from_str(domain).unwrap();
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
let (mut client, background) = Client::<TokioRuntimeProvider>::from_sender(stream);
let background_task = tokio::spawn(background);
let query_result = tokio::time::timeout(
Duration::from_secs(2),
client.query(name.clone(), DNSClass::IN, RecordType::A),
)
.await;
background_task.abort();
let _ = background_task.await;
let attempt_err = match query_result {
Ok(Ok(response)) => {
if response.answers.len() == 1
&& let Some(resp) = response.answers.first()
&& let RData::A(a) = &resp.data
&& a.0 == expected
{
return;
}
format!("unexpected response: {:?}", response.answers)
}
Ok(Err(e)) => format!("DNS query failed for domain '{domain}': {e}"),
Err(_) => format!("DNS query timed out for domain '{domain}'"),
};
if Instant::now() >= deadline {
panic!(
"DNS query failed unexpectedly for domain '{domain}' after retries: {attempt_err}"
);
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
#[cfg(feature = "magic-dns")]
#[tokio::test]
#[serial_test::serial]
pub async fn three_node_dns_export() {
let insts = init_three_node_ex(
"tcp",
|cfg| {
use crate::dns::config::zone::ZoneConfig;
use crate::dns::config::{DnsConfigLoaderExt, DnsConfigRaw};
use hickory_proto::rr::LowerName;
use std::str::FromStr;
let inst_name = cfg.get_inst_name();
let origin = LowerName::from_str(&format!("{}.com.", inst_name)).unwrap();
let mut dns_config = DnsConfigRaw {
name: Some(LowerName::from_str(&inst_name).unwrap()),
..Default::default()
};
let ipv4 = match inst_name.as_str() {
"inst1" => "10.144.144.1".parse().ok(),
"inst2" => "10.144.144.2".parse().ok(),
"inst3" => "10.144.144.3".parse().ok(),
_ => None,
};
dns_config
.zones
.get_or_insert_default()
.push(ZoneConfig::dedicated(origin, ipv4, vec![]));
let listener_port = match inst_name.as_str() {
"inst1" => 5351,
"inst2" => 5352,
"inst3" => 5353,
_ => 5350,
};
dns_config.listeners = Some(
vec![
format!("udp://127.0.0.1:{}", listener_port)
.parse()
.unwrap(),
]
.into(),
);
cfg.set_dns(dns_config.into());
cfg
},
false,
)
.await;
use std::net::{Ipv4Addr, SocketAddr};
let addr1 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5351);
let addr2 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5352);
let addr3 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5353);
check_dns_record_at(addr1, "inst2.com.", "10.144.144.2").await;
check_dns_record_at(addr1, "inst3.com.", "10.144.144.3").await;
check_dns_record_at(addr2, "inst1.com.", "10.144.144.1").await;
check_dns_record_at(addr2, "inst3.com.", "10.144.144.3").await;
check_dns_record_at(addr3, "inst1.com.", "10.144.144.1").await;
check_dns_record_at(addr3, "inst2.com.", "10.144.144.2").await;
drop_insts(insts).await;
}
#[cfg(feature = "magic-dns")]
#[tokio::test]
#[serial_test::serial]
pub async fn three_node_dns_export_chain() {
prepare_linux_namespaces();
let cfg_cb = |cfg: TomlConfigLoader| {
use crate::dns::config::zone::ZoneConfig;
use crate::dns::config::{DnsConfigLoaderExt, DnsConfigRaw};
use hickory_proto::rr::LowerName;
use std::str::FromStr;
let inst_name = cfg.get_inst_name();
let origin = LowerName::from_str(&format!("{}.com.", inst_name)).unwrap();
let mut dns_config = DnsConfigRaw {
name: Some(LowerName::from_str(&inst_name).unwrap()),
..Default::default()
};
let ipv4 = match inst_name.as_str() {
"inst1" => "10.144.144.1".parse().ok(),
"inst2" => "10.144.144.2".parse().ok(),
"inst3" => "10.144.144.3".parse().ok(),
_ => None,
};
dns_config
.zones
.get_or_insert_default()
.push(ZoneConfig::dedicated(origin, ipv4, vec![]));
let listener_port = match inst_name.as_str() {
"inst1" => 5351,
"inst2" => 5352,
"inst3" => 5353,
_ => 5350,
};
dns_config.listeners = Some(
vec![
format!("udp://127.0.0.1:{}", listener_port)
.parse()
.unwrap(),
]
.into(),
);
cfg.set_dns(dns_config.into());
let mut flags = cfg.get_flags();
flags.disable_p2p = true;
cfg.set_flags(flags);
cfg
};
let mut inst1 = Instance::new(cfg_cb(get_inst_config(
"inst1",
Some("net_a"),
"10.144.144.1",
"fd00::1/64",
)));
let mut inst2 = Instance::new(cfg_cb(get_inst_config(
"inst2",
Some("net_b"),
"10.144.144.2",
"fd00::2/64",
)));
let mut inst3 = Instance::new(cfg_cb(get_inst_config(
"inst3",
Some("net_c"),
"10.144.144.3",
"fd00::3/64",
)));
inst1.run().await.unwrap();
inst2.run().await.unwrap();
inst3.run().await.unwrap();
inst1
.get_conn_manager()
.add_connector(crate::tunnel::tcp::TcpTunnelConnector::new(
"tcp://10.1.1.2:11010".parse().unwrap(),
));
inst2
.get_conn_manager()
.add_connector(crate::tunnel::ring::RingTunnelConnector::new(
format!("ring://{}", inst3.id()).parse().unwrap(),
));
wait_for_condition(
|| async {
let routes = inst2.get_peer_manager().list_routes().await;
routes.len() == 2
},
Duration::from_secs(5),
)
.await;
wait_for_condition(
|| async {
let routes = inst1.get_peer_manager().list_routes().await;
routes.len() == 2
},
Duration::from_secs(5),
)
.await;
wait_for_condition(
|| async {
let routes = inst3.get_peer_manager().list_routes().await;
routes.len() == 2
},
Duration::from_secs(5),
)
.await;
let insts = vec![inst1, inst2, inst3];
use std::net::{Ipv4Addr, SocketAddr};
let addr1 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5351);
let addr2 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5352);
let addr3 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5353);
check_dns_record_at(addr1, "inst2.com.", "10.144.144.2").await;
check_dns_record_at(addr1, "inst3.com.", "10.144.144.3").await;
check_dns_record_at(addr2, "inst1.com.", "10.144.144.1").await;
check_dns_record_at(addr2, "inst3.com.", "10.144.144.3").await;
check_dns_record_at(addr3, "inst1.com.", "10.144.144.1").await;
check_dns_record_at(addr3, "inst2.com.", "10.144.144.2").await;
drop_insts(insts).await;
}
+2 -2
View File
@@ -541,14 +541,14 @@ pub fn bind<B: Bindable>(
B::finalize(socket)
}
// endregion
pub fn reserve_buf(buf: &mut BytesMut, min_size: usize, max_size: usize) {
if buf.capacity() < min_size {
buf.reserve(max_size);
}
}
// endregion
pub mod tests {
use atomic_shim::AtomicU64;
use std::{sync::Arc, time::Instant};
+2 -4
View File
@@ -2,10 +2,7 @@ use std::{
collections::hash_map::DefaultHasher, hash::Hasher, net::SocketAddr, pin::Pin, sync::Arc,
};
use crate::{
common::{dns::socket_addrs, error::Error},
proto::common::TunnelInfo,
};
use crate::{common::error::Error, proto::common::TunnelInfo};
use async_trait::async_trait;
use derive_more::{From, TryInto};
use futures::{Sink, Stream};
@@ -397,6 +394,7 @@ macro_rules! __matches_protocol__ {
};
}
use crate::utils::dns::socket_addrs;
pub(crate) use __matches_protocol__ as matches_protocol;
#[cfg(test)]
+37
View File
@@ -0,0 +1,37 @@
use tokio::sync::watch;
#[derive(Debug)]
pub struct DirtyFlag {
tx: watch::Sender<bool>,
rx: watch::Receiver<bool>,
}
impl DirtyFlag {
pub fn new(value: bool) -> Self {
let (tx, rx) = watch::channel(value);
Self { tx, rx }
}
pub fn mark(&self) {
self.tx.send(true).ok();
}
pub fn peek(&self) -> bool {
*self.tx.borrow()
}
pub fn reset(&self) -> bool {
self.tx.send_replace(false)
}
pub async fn wait(&self) {
let mut rx = self.rx.clone();
let _ = rx.wait_for(|v| *v).await;
}
}
impl Default for DirtyFlag {
fn default() -> Self {
Self::new(true)
}
}
+476
View File
@@ -0,0 +1,476 @@
use crate::common::error::Error;
use anyhow::Context;
use hickory_net::runtime::TokioRuntimeProvider;
use hickory_proto::rr::rdata::SRV;
use hickory_proto::rr::{IntoName, LowerName, RData};
use hickory_resolver::config::{
ConnectionConfig, LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts,
};
use hickory_resolver::system_conf::read_system_conf;
use hickory_resolver::{Resolver, TokioResolver};
use idna::AsciiDenyList;
use once_cell::sync::Lazy;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::AtomicBool;
use tokio::net::lookup_host;
const SYSTEM_DNS_RESOLVER: &str = "system";
#[derive(Clone)]
enum DnsResolver {
System,
Hickory {
uri: String,
resolver: Arc<Resolver<TokioRuntimeProvider>>,
},
}
pub fn get_default_dns_resolvers() -> Vec<String> {
vec![SYSTEM_DNS_RESOLVER.to_string()]
}
fn bootstrap_ips_for_doh_host(host: &str) -> Option<Vec<IpAddr>> {
if let Ok(ip) = host.parse::<IpAddr>() {
return Some(vec![ip]);
}
match host {
"dns.alidns.com" => Some(vec![
IpAddr::V4(Ipv4Addr::new(223, 5, 5, 5)),
IpAddr::V4(Ipv4Addr::new(223, 6, 6, 6)),
IpAddr::V6(Ipv6Addr::new(0x2400, 0x3200, 0, 0, 0, 0, 0, 1)),
IpAddr::V6(Ipv6Addr::new(0x2400, 0x3200, 0xbaba, 0, 0, 0, 0, 1)),
]),
_ => None,
}
}
fn build_hickory_resolver(
config: ResolverConfig,
) -> Result<Arc<Resolver<TokioRuntimeProvider>>, Error> {
let mut opts = ResolverOpts::default();
opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
Ok(Arc::new(
TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
.with_options(opts)
.build()
.context("failed to build DNS resolver")?,
))
}
fn build_doh_resolver(raw: &str) -> Result<DnsResolver, Error> {
let url = url::Url::parse(raw).map_err(|e| Error::InvalidUrl(e.to_string()))?;
if url.scheme() != "https" {
return Err(anyhow::anyhow!("unsupported dns resolver scheme: {}", url.scheme()).into());
}
let host = url
.host_str()
.with_context(|| format!("DoH resolver host is empty: {}", raw))?;
let ips = bootstrap_ips_for_doh_host(host).with_context(|| {
format!(
"DoH resolver {} requires a known bootstrap IP; currently only dns.alidns.com or IP literals are supported",
raw
)
})?;
let port = url.port().unwrap_or(443);
let server_name: Arc<str> = Arc::from(host.to_string());
let http_endpoint: Option<Arc<str>> = match url.path() {
"" | "/" | "/dns-query" => None,
path => Some(Arc::from(path.to_string())),
};
let name_servers = ips
.into_iter()
.map(|ip| {
let mut connection =
ConnectionConfig::https(server_name.clone(), http_endpoint.clone());
connection.port = port;
NameServerConfig::new(ip, true, vec![connection])
})
.collect::<Vec<_>>();
Ok(DnsResolver::Hickory {
uri: raw.to_string(),
resolver: build_hickory_resolver(ResolverConfig::from_parts(
None,
Vec::new(),
name_servers,
))?,
})
}
fn build_dns_resolver(raw: &str) -> Result<DnsResolver, Error> {
if raw.eq_ignore_ascii_case(SYSTEM_DNS_RESOLVER) {
return Ok(DnsResolver::System);
}
build_doh_resolver(raw)
}
fn build_dns_resolvers(raw_resolvers: &[String]) -> Result<Vec<DnsResolver>, Error> {
let raw_resolvers = if raw_resolvers.is_empty() {
get_default_dns_resolvers()
} else {
raw_resolvers.to_vec()
};
raw_resolvers
.iter()
.map(|raw| build_dns_resolver(raw))
.collect()
}
pub fn validate_dns_resolvers(raw_resolvers: &[String]) -> Result<(), Error> {
build_dns_resolvers(raw_resolvers).map(|_| ())
}
pub fn set_dns_resolvers(raw_resolvers: Vec<String>) -> Result<(), Error> {
let resolvers = build_dns_resolvers(&raw_resolvers)?;
*DNS_RESOLVERS.write().unwrap() = resolvers;
Ok(())
}
static DNS_RESOLVERS: Lazy<RwLock<Vec<DnsResolver>>> =
Lazy::new(|| RwLock::new(build_dns_resolvers(&get_default_dns_resolvers()).unwrap()));
fn configured_dns_resolvers() -> Vec<DnsResolver> {
DNS_RESOLVERS.read().unwrap().clone()
}
pub fn sanitize(name: impl AsRef<str>) -> String {
let name = name.as_ref();
let dot = name.ends_with('.');
let mut name = idna::domain_to_ascii_cow(name.as_ref(), AsciiDenyList::EMPTY)
.unwrap_or_default()
.into_owned()
.to_lowercase()
.split('.')
.map(|label| {
label
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.take(63)
.collect::<String>()
.trim_matches('-')
.to_string()
})
.filter(|label| !label.is_empty())
.collect::<Vec<_>>()
.join(".");
name.truncate(253);
if dot {
name.push('.');
}
name
}
pub fn parse(name: impl AsRef<str>) -> LowerName {
let name = name.as_ref();
if let Ok(name) = name.parse() {
name
} else {
let sanitized = sanitize(name);
tracing::debug!("invalid name: {}, sanitized to: {}", name, sanitized);
sanitized.parse().unwrap_or_default()
}
}
pub fn resolver_conf() -> (ResolverConfig, ResolverOpts) {
let mut config = ResolverConfig::default();
for server in ["223.5.5.5", "180.184.1.1"] {
config.add_name_server(NameServerConfig::new(
server.parse().unwrap(),
true,
vec![ConnectionConfig::udp()],
));
}
let mut opts = ResolverOpts::default();
if let Ok((system_config, system_opts)) = read_system_conf() {
for ns in system_config.name_servers() {
config.add_name_server(ns.clone());
}
opts = system_opts;
}
opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
(config, opts)
}
static ALLOW_USE_SYSTEM_DNS_RESOLVER: AtomicBool = AtomicBool::new(true);
static RESOLVER: Lazy<Arc<Resolver<TokioRuntimeProvider>>> = Lazy::new(|| {
let (config, opts) = resolver_conf();
let builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
.with_options(opts);
Arc::new(
builder
.build()
.expect("failed to initialize global DNS resolver"),
)
});
pub async fn txt_lookup(name: impl IntoName) -> Result<Vec<String>, Error> {
let name = name.into_name().context("invalid txt record name")?;
let mut last_err = None;
for resolver in configured_dns_resolvers() {
let response = match resolver {
DnsResolver::System => RESOLVER.txt_lookup(name.clone()).await,
DnsResolver::Hickory { uri, resolver } => {
let response = resolver.txt_lookup(name.clone()).await;
if response.is_err() {
tracing::debug!(?uri, ?name, "txt lookup failed with resolver");
}
response
}
};
let Ok(response) = response else {
last_err = Some(anyhow::anyhow!("failed to lookup txt record").into());
continue;
};
let data = response
.answers()
.iter()
.filter_map(|record| match record.data {
RData::TXT(ref txt) => Some(txt.to_string()),
_ => None,
})
.collect::<Vec<_>>();
if data.is_empty() {
last_err = Some(Error::NotFound);
continue;
}
tracing::info!(?data, "got txt record(s)");
return Ok(data);
}
Err(last_err.unwrap_or(Error::NotFound))
}
pub async fn txt_resolve(name: impl IntoName) -> Result<Vec<String>, Error> {
Ok(txt_lookup(name)
.await?
.iter()
.flat_map(|s| s.split_whitespace())
.map(String::from)
.collect())
}
pub async fn srv_lookup(name: impl IntoName) -> Result<Vec<SRV>, Error> {
let name = name.into_name().context("invalid srv record name")?;
let mut last_err = None;
for resolver in configured_dns_resolvers() {
let response = match resolver {
DnsResolver::System => RESOLVER.srv_lookup(name.clone()).await,
DnsResolver::Hickory { uri, resolver } => {
let response = resolver.srv_lookup(name.clone()).await;
if response.is_err() {
tracing::debug!(?uri, ?name, "srv lookup failed with resolver");
}
response
}
};
let Ok(response) = response else {
last_err = Some(anyhow::anyhow!("failed to lookup srv record").into());
continue;
};
let data = response
.answers()
.iter()
.filter_map(|record| match record.data {
RData::SRV(ref srv) => Some(srv.clone()),
_ => None,
})
.collect::<Vec<_>>();
if data.is_empty() {
last_err = Some(Error::NotFound);
continue;
}
tracing::info!(?data, "got srv record(s)");
return Ok(data);
}
Err(last_err.unwrap_or(Error::NotFound))
}
pub async fn resolve_host(host: &str, port: u16) -> Result<Vec<SocketAddr>, Error> {
if let Ok(ip) = host.parse::<IpAddr>() {
return Ok(vec![SocketAddr::new(ip, port)]);
}
let mut last_err = None;
for resolver in configured_dns_resolvers() {
match resolver {
DnsResolver::System => {
if !ALLOW_USE_SYSTEM_DNS_RESOLVER.load(std::sync::atomic::Ordering::Relaxed) {
continue;
}
match lookup_host(format!("{}:{}", host, port)).await {
Ok(addrs) => {
let addrs = addrs.collect::<Vec<_>>();
if !addrs.is_empty() {
tracing::debug!(?addrs, "system dns lookup done");
return Ok(addrs);
}
}
Err(error) => {
tracing::debug!(?error, "system dns lookup failed");
last_err = Some(Error::from(error));
}
}
}
DnsResolver::Hickory { uri, resolver } => match resolver.lookup_ip(host).await {
Ok(lookup) => {
let addrs = lookup
.iter()
.map(|ip| SocketAddr::new(ip, port))
.collect::<Vec<_>>();
if !addrs.is_empty() {
return Ok(addrs);
}
}
Err(error) => {
tracing::debug!(?uri, ?host, ?error, "hickory dns lookup failed");
last_err = Some(
anyhow::anyhow!(
"hickory dns lookup_ip failed, host: {}, port: {}",
host,
port
)
.into(),
);
}
},
}
}
Err(last_err.unwrap_or(Error::NotFound))
}
pub async fn socket_addrs(
url: &url::Url,
default_port_number: impl Fn() -> Option<u16>,
) -> Result<Vec<SocketAddr>, Error> {
let host = url.host().ok_or(Error::InvalidUrl(url.to_string()))?;
// see https://github.com/EasyTier/EasyTier/pull/947, https://github.com/EasyTier/EasyTier/pull/1700
let port = url
.port()
.or_else(default_port_number)
.ok_or(Error::InvalidUrl(url.to_string()))?;
// if host is an ip address, return it directly
if let Some(ip) = match host {
url::Host::Ipv4(ip) => Some(ip.into()),
url::Host::Ipv6(ip) => Some(ip.into()),
_ => None,
} {
return Ok(vec![SocketAddr::new(ip, port)]);
}
resolve_host(&host.to_string(), port).await
}
#[cfg(test)]
mod tests {
use super::*;
use guarden::defer;
#[test]
fn parse_matrix_cases() {
let cases = [
["Example.COM.", "example.com."],
["a_b!.et.net.", "a-b.et.net."],
["foo..bar.com.", "foo.bar.com."],
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com.",
],
["___", "___"],
["!", ""],
["", ""],
];
for [input, expected] in cases {
let parsed = parse(input);
let expected: LowerName = expected.parse().unwrap();
assert_eq!(
parsed, expected,
"parse({input:?}) should equal {expected:?}, got {parsed:?}"
);
}
}
#[test]
fn default_dns_resolver_is_system() {
assert_eq!(get_default_dns_resolvers(), vec!["system".to_string()]);
assert!(matches!(
build_dns_resolvers(&get_default_dns_resolvers()).unwrap()[0],
DnsResolver::System
));
}
#[test]
fn alidns_doh_resolver_is_supported() {
validate_dns_resolvers(&["https://dns.alidns.com/dns-query".to_string()]).unwrap();
}
#[test]
fn unknown_doh_resolver_requires_bootstrap_ip() {
assert!(validate_dns_resolvers(&["https://example.com/dns-query".to_string()]).is_err());
}
#[tokio::test]
async fn test_socket_addrs() {
let url = url::Url::parse("tcp://github-ci-test.easytier.cn:80").unwrap();
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
println!("addrs: {:?}", addrs);
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(false, std::sync::atomic::Ordering::Relaxed);
defer!(
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(true, std::sync::atomic::Ordering::Relaxed);
);
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
println!("addrs2: {:?}", addrs);
}
#[tokio::test]
async fn socket_addrs_preserves_explicit_zero_port() {
let cases = [
("ws://127.0.0.1:0", 80, 0),
("wss://127.0.0.1:0", 443, 0),
("ws://127.0.0.1", 80, 80),
("wss://127.0.0.1", 443, 443),
];
for (raw_url, default_port, expected_port) in cases {
let url = url::Url::parse(raw_url).unwrap();
let addrs = socket_addrs(&url, || Some(default_port)).await.unwrap();
assert_eq!(
addrs,
vec![SocketAddr::from(([127, 0, 0, 1], expected_port))]
);
}
}
}
+9
View File
@@ -1,3 +1,5 @@
pub mod dirty;
pub mod dns;
pub mod error;
pub mod panic;
pub mod string;
@@ -22,6 +24,13 @@ pub fn weak_upgrade<T>(weak: &Weak<T>) -> anyhow::Result<Arc<T>> {
.ok_or_else(|| anyhow::anyhow!("{} not available", std::any::type_name::<T>()))
}
pub fn hostname() -> String {
hostname::get()
.unwrap_or_else(|_| "localhost".into())
.to_string_lossy()
.into_owned()
}
pub trait BoxExt: Sized {
fn boxed(self) -> Box<Self> {
Box::new(self)
+2 -5
View File
@@ -14,6 +14,7 @@ use crate::{
instance_manager::{DaemonGuard, NetworkInstanceManager},
proto::common::NatType,
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelScheme},
utils,
};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
@@ -282,11 +283,7 @@ pub async fn run_web_client(
let mut flags = global_ctx.get_flags();
flags.bind_device = false;
global_ctx.set_flags(flags);
let hostname = match hostname {
None => gethostname::gethostname().to_string_lossy().to_string(),
Some(hostname) => hostname,
};
let hostname = hostname.unwrap_or_else(utils::hostname);
Ok(WebClient::new(
ConfigServerConnector {
url: c_url,