io.systemd.Resolve 请求全链路深度分析

io.systemd.Resolve 请求全链路深度分析

基于fedora44 的 systemd 259.6 源码分析(下载链接在文章结尾)。本文跟踪一个 DNS 查询请求从应用程序 gethostbyname() 调用,到经过 NSS 模块、Varlink IPC、systemd-resolved、DNS 事务、再到回复返回的完整旅程。

免责声明:源码的分析我大概看过一些,然后AI帮我分析了中间没必要看的部分,串起来成了这个文章。


第 0 章 背景故事

之所以捣鼓这个是因为我发现主机上突然很多遥测的域名(proj-xtrace-7e235817c9b9381c22d8b743908d469f-cn-beijing.cn-beijing.log.aliyuncs.com)出现,有点恶心人,但是很多工具都没法把域名和进程绑定到一起,只能深入了解一下getaddrinfo是怎么拿到一个域名的ip的,这样反推写一些hook再对应一下pid就能得知是什么程序。我的机器是fedora44,基于systemd-resolved,所以是走的varlink。

我粗略的epbf实现:https://github.com/scriptk1d/varlink_snoop

大概的思路就是:

  1. hook connect()​ → 检查 sockaddr_un.sun_path == /run/systemd/resolve/io.systemd.Resolve → 把 fd 加入白名单
  2. hook 发送族(write​/writev​/sendto​/sendmsg)→ 只对白名单 fd dump payload(Varlink 明文 JSON)
  3. 记录 PID / TID / comm / 时间戳,ring buffer 上报用户态

相比 uprobe getaddrinfo​:不依赖 libc、不挑语言运行时、静态链接也照抓(因为抓的是所有客户端共同收口的那个 Varlink 连接)。

在 systemd 的世界里,/run/systemd/resolve/io.systemd.Resolve​ 是一个特殊的 Unix domain socket。它不是一个普通的服务监听 socket,而是一个 Varlink 协议端点——一个用 JSON 消息、NUL 分隔、单连接请求/回复模型构建的轻量级 RPC 协议。

为什么 systemd 不复用已有的 D-Bus,而要新造一个 Varlink?核心动机写在 varlink-internal.h:43-114​ 的注释里:D-Bus 太重,不适合作为 NSS 模块这种”每进程一次调用”的客户端。NSS 模块被加载到每个进程里,不能依赖一个长期运行的 D-Bus 客户端连接。Varlink 的设计目标是:协议极简(单 JSON 文件即可描述)、连接开销极低(一个 socket)、无类型注册和信号订阅开销、可直接被静态链接进 NSS 模块。

整个 DNS 解析链路全景如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 应用进程 (curl, ssh, ...)              systemd-resolved 守护进程
┌────────────────────────────┐ ┌──────────────────────────┐
│ getaddrinfo("example.com")│ │ │
│ │ │ │ │
│ ▼ │ │ │
│ libc NSS dispatcher │ │ │
│ │ │ │ │
│ 根据行nsswitch.conf: │ │ │
│ "hosts: files resolve dns"│ │ │
│ │ │ │ │
│ ▼ │ │ │
│ libnss_resolve.so.2 │ var- │ /run/systemd/resolve/ │
│ (_nss_resolve_gethost- │ link │ io.systemd.Resolve │
│ byname4_r 入口) │ JSON │ ↑ AF_UNIX listen socket │
│ │ │ over │ │ │
│ ▼ │ AF_UNIX│ ▼ │
│ sd_varlink_connect_addr() │ ◀────▶ │ accept4() → sd_varlink │
│ sd_varlink_call( │ │ │
│ "io.systemd.Resolve. │ │ dispatch_method() │
│ ResolveHostname", │ │ │ │
│ {name:"example.com"}) │ │ ▼ │
│ │ │ │ vl_method_resolve_ │
│ ── 同步阻塞等待 ── │ │ hostname() │
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ dns_query_go() ── 发往上游 DNS
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ vl_method_resolve_ │
│ │ │ │ hostname_complete() │
│ │ │ │ │ │
│ │ │ │ sd_varlink_replybo( │
│ │ │ │ addresses:[...]) │
│ ▼ │ │ │ │
│ sd_json_dispatch(回复) │ ◀────── │ ───────┘ │
│ │ │ │ │
│ 填到 struct hostent │ │ │
│ 返回 NSS_STATUS_SUCCESS │ │ │
└────────────────────────────┘ └──────────────────────────┘

本文将逐站拆解这条链路。涉及的核心源文件:

文件 角色
src/nss-resolve/nss-resolve.c 客户端 NSS 模块,把 gethostbyname 翻译成 Varlink 调用
src/libsystemd/sd-varlink/sd-varlink.c Varlink 协议库(客户端+服务端共用)
src/libsystemd/sd-varlink/varlink-internal.h sd_varlink 结构与状态机定义
src/resolve/resolved.c systemd-resolved 主进程入口
src/resolve/resolved-manager.c 守护进程 Manager 管理,含 manager_start()
src/resolve/resolved-varlink.c Varlink 服务端 method handler 实现
src/resolve/resolved-dns-query.c DNS 查询的协议无关抽象
src/shared/varlink-io.systemd.Resolve.c io.systemd.Resolve 接口的 IDL 定义

第 2 章 整体架构:三套状态机的协作

这条链路最容易被忽略的设计点是:全程有三个独立状态机在协作。理解它们的边界,才能理解为什么代码这样写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
客户端 sd_varlink                Varlink 协议              服务端 sd_varlink
(调用线程私有) (跨进程,跨 socket) (event loop 内)
┌──────────────────┐ ┌─────────────┐ ┌──────────────────┐
│ IDLE_CLIENT │ │ │ │ IDLE_SERVER │
│ │ │ │ │ │ │ │
│ │ call() │ 发起方法 │ │ accept + │ │ 收到 JSON │
│ ▼ │ ─────────▶ │ │ ◀────── │ ▼ │
│ CALLING │ │ │ 读 socket│ PROCESSING_METHOD │
│ │ │ │ │ │ │ 回调用户函数 │
│ │ 收到回复 │ │ │ │ │ (vl_method_*) │
│ ▼ │ ◀───────── │ │ ──────▶ │ ▼ │
│ CALLED │ 返回 JSON │ │ 写 socket│ PROCESSED_METHOD │
│ │ │ │ │ │ │ │
│ │ return │ │ │ │ ▼ │
│ ▼ │ │ │ │ IDLE_SERVER │
│ IDLE_CLIENT │ │ │ │ │
└──────────────────┘ └─────────────┘ └──────────────────┘
(21 状态中的 (21 状态中的
4 个客户端状态) 服务端状态)

│ 异步 dns_query_go()

┌────────────────────┐
│ DnsQuery 状态机 │
│ (DNS_TRANSACTION_*) │
│ 独立!与 Varlink 解耦│
└────────────────────┘

这三个状态机分别定义在:

  • 客户端 / 服务端 Varlink 状态机: varlink-internal.h:11-41​ 的 VarlinkState 枚举,共 21 个状态(8 个客户端、7 个服务端、6 个关闭期共享)
  • DNS 查询状态机: resolved-dns-transaction.h​ 的 DnsTransactionState,跟踪单个 DNS 数据包在网络层的旅程
  • DnsQuery 状态机: resolved-dns-query.c​ 中通过 q->state 字段管理,是 DNS 事务之上的”用户意图”层

注意第三个状态机 (DnsQuery​) 是故意与 Varlink 解耦的。一个 Varlink method 调用可能触发多个 DNS transaction(因为 CNAME 链、A+AAAA 双查询、多 DNS 服务器轮询),DnsQuery 把这些聚合起来,完成后回调到 Varlink 层。这种解耦使得 systemd-resolved 可以同时服务于 Varlink、D-Bus、内置 DNS stub listener 三种客户端,共用同一套 DNS 查询逻辑。


第 3 章 服务端:socket 的诞生

跟踪一个 DNS 查询从服务端 socket 的诞生开始。systemd-resolved 的进程入口在 resolved.c:68:

1
2
3
r = manager_new(&m);          // resolved.c:68
...
r = manager_start(m); // resolved.c:72

manager_start()​ 在 resolved-manager.c:822-836 极简:

1
2
3
4
5
6
7
8
9
int manager_start(Manager *m) {
int r;
assert(m);
r = manager_dns_stub_start(m); // 启动 127.0.0.53:53 DNS stub
if (r < 0) return r;
r = manager_varlink_init(m); // 启动 Varlink 服务端 ← 我们关心的
if (r < 0) return r;
return 0;
}

manager_varlink_init()​ 在 resolved-varlink.c:1547-1559​ 把工作分给两个独立 server:主查询服务监控服务(后者用于 resolvectl monitor)。本报告只跟踪前者。

3.1 创建 server、绑定方法、绑定地址

varlink_main_server_init()​ 在 resolved-varlink.c:1492-1545 完成三件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 1. 创建 server,启用 ACCOUNT_UID(按 UID 计费) */
r = varlink_server_new(&s, SD_VARLINK_SERVER_ACCOUNT_UID, m);
/* ↑ 注意这里没有用 SD_VARLINK_SERVER_ROOT_ONLY,意味着所有本地用户都能查询 DNS。
防滥用靠 ACCOUNT_UID 的 per-UID 连接数上限(默认 128,见 sd-varlink.c:43)。 */

/* 2. 注册 io.systemd.Resolve 和 io.systemd.service 两个接口 */
r = sd_varlink_server_add_interface_many(s,
&vl_interface_io_systemd_Resolve, // IDL 元数据
&vl_interface_io_systemd_service);

/* 3. 把方法名 → C 回调函数的映射表登记进去 */
r = sd_varlink_server_bind_method_many(s,
"io.systemd.Resolve.ResolveHostname", vl_method_resolve_hostname,
"io.systemd.Resolve.ResolveAddress", vl_method_resolve_address,
"io.systemd.Resolve.ResolveService", vl_method_resolve_service,
"io.systemd.Resolve.ResolveRecord", vl_method_resolve_record,
...);

这里有一个 systemd-resolved 复用 io.systemd.service 接口 的设计:同一个 server 上同时挂着 io.systemd.service.Ping​、SetLogLevel​、GetEnvironment​,这些是所有 systemd 守护进程通用的”内省”方法,定义在 src/libsystemd/sd-varlink/varlink-io.systemd.c

接下来绑定地址(resolved-varlink.c:1530-1537):

1
2
3
4
5
r = sd_varlink_server_listen_auto(s);   /* 先尝试 socket activation */
if (r == 0) { /* 没拿到 systemd 传来的 fd */
r = sd_varlink_server_listen_address(s,
"/run/systemd/resolve/io.systemd.Resolve", 0666);
}

listen_auto​ 是 systemd 服务进程的标配:启动时 systemd 可能已经通过 sd_listen_fds()​ 把监听 fd 直接传给 resolved(socket activation),这时不再 bind()​。如果没有,就回退到 listen_address​ 自己创建 socket。两条路径最终都调用 varlink_server_create_listen_fd_socket()​(sd-varlink.c:3606-3634​),把 fd 注册到 sd-event 上,绑定 connect_callback

3.2 IDL:接口的”自描述”

io.systemd.Resolve.ResolveHostname​ 的 IDL 定义在 src/shared/varlink-io.systemd.Resolve.c:124-139:

1
2
3
4
5
6
7
8
9
static SD_VARLINK_DEFINE_METHOD(
ResolveHostname,
SD_VARLINK_DEFINE_INPUT(ifindex, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0),
SD_VARLINK_DEFINE_INPUT(family, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_DEFINE_INPUT(flags, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(addresses, ResolvedAddress, SD_VARLINK_ARRAY),
SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0),
SD_VARLINK_DEFINE_OUTPUT(flags, SD_VARLINK_INT, 0));

这份 IDL 在编译期变成静态 sd_varlink_symbol​ 结构,运行时被 sd_varlink_server_add_interface()​ 注册到 server 的 symbols​ hashmap。此后当客户端的请求 JSON 进来,varlink_dispatch_method()​ 会把请求参数交给 varlink_idl_validate_method_call()​(sd-varlink-idl.c​)做类型校验,校验不过直接回 InvalidParameter​ 错误,不让用户回调见到非法数据。这是 IDL 作为协议契约 的关键作用。


第 4 章 客户端:NSS 模块发起连接

现在切换视角。一个普通程序 curl https://example.com​,libc 内部会调用 getaddrinfo()​,后者依据 /etc/nsswitch.conf​ 的 hosts:​ 行依次尝试各个 NSS 模块。配置为 hosts: files resolve dns myhostname​ 时,libnss_resolve.so.2 是第二个被尝试的模块。

4.1 NSS 入口被 libc 调起

libnss_resolve.so.2​ 的代码全部在 nss-resolve.c​。导出符号形如 _nss_resolve_gethostbyname4_r​(glibc 的 NSS ABI 要求这个奇怪的命名)。函数原型在 nss-resolve.c:191-347,核心是这三段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum nss_status _nss_resolve_gethostbyname4_r(
const char *name, struct gaih_addrtuple **pat,
char *buffer, size_t buflen,
int *errnop, int *h_errnop, int32_t *ttlp) {

_cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *cparams = NULL;
...

/* 第 1 步:建立连接 */
r = connect_to_resolved(&link); /* L213 */
if (r < 0) goto fail;

/* 第 2 步:构造请求参数 JSON */
r = sd_json_buildo(&cparams,
SD_JSON_BUILD_PAIR_STRING("name", name),
SD_JSON_BUILD_PAIR_UNSIGNED("flags", query_flags())); /* L217 */

/* 第 3 步:同步发起 RPC */
const char *error_id;
r = sd_varlink_call(link, "io.systemd.Resolve.ResolveHostname",
cparams, &rparams, &error_id); /* L230 */
if (r < 0) goto fail;
if (!isempty(error_id)) { /* 进入错误分支 */ }
...
}

注意 _cleanup_(sd_varlink_unrefp)​ —— 整个 NSS 调用期间持有的 sd_varlink 对象用 cleanup 属性绑定了析构函数,函数任何 return 路径都会自动关闭连接。这是 systemd 代码里无处不在的 RAII 模式。

4.2 connect_to_resolved:打开 socket

connect_to_resolved()​ 在 nss-resolve.c:48-62:

1
2
3
4
5
6
7
8
9
10
11
12
static int connect_to_resolved(sd_varlink **ret) {
_cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
int r;
r = sd_varlink_connect_address(&link,
"/run/systemd/resolve/io.systemd.Resolve"); /* L52 */
if (r < 0) return r;
r = sd_varlink_set_relative_timeout(link,
SD_RESOLVED_QUERY_TIMEOUT_USEC); /* L56 */
if (r < 0) return r;
*ret = TAKE_PTR(link);
return 0;
}

调用 sd_varlink_connect_address()​(sd-varlink.c:158-208)展开:

1
2
3
4
5
6
7
8
9
10
11
v->input_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);  /* L170 */
v->output_fd = v->input_fd = fd_move_above_stdio(v->input_fd); /* L174 */
r = sockaddr_un_set_path(&sockaddr.un, address);
...
r = RET_NERRNO(connect(v->input_fd, &sockaddr.sa, r)); /* L187 */
if (r < 0) {
if (!IN_SET(r, -EAGAIN, -EINPROGRESS))
return log_debug_errno(r, "Failed to connect to %s: %m", address);
v->connecting = true; /* ← 异步连接标志 */
}
varlink_set_state(v, VARLINK_IDLE_CLIENT); /* L204 */

三个关键细节:

  1. SOCK_CLOEXEC|SOCK_NONBLOCK​:exec 时关闭(防止 fd 泄漏给子进程)、非阻塞(connect() 对 Unix socket 可能立即返回 EINPROGRESS)
  2. fd_move_above_stdio() ​:把 fd 移到 ≥3 的位置,避免占用 stdin/stdout/stderr 槽位——后续 exec 子进程时标准流不被干扰
  3. v->connecting标志:这是一个独立于 VarlinkState​ 的”传输层子状态”。varlink-internal.h:90-114​ 长篇注释解释了为什么需要它:状态机是”应用层”概念,而 connecting​ 是”传输层”细节。即使 connect() 未完成,也允许把消息入队,等连接就绪后自动发出。

4.3 NSS 模块的特殊考量

NSS 模块是被加载进 每个进程 的共享库,设计上有几个约束在普通 Varlink 客户端里看不到:

约束 nss-resolve 的应对 源码位置
进程可能 setuid,不能信任环境 secure_getenv_bool​ 读 SYSTEMD_NSS_RESOLVE_* 旗标 nss-resolve.c:172-178
libc 调用线程的 errno 必须保护 PROTECT_ERRNO​ / UNPROTECT_ERRNO 宏对 nss-resolve.c:204, 273
调用结束必须关连接,不能持有 _cleanup_(sd_varlink_unrefp) RAII nss-resolve.c:198
h_errno 是历史遗留全局变量,需显式重置 *h_errnop = NETDB_SUCCESS; h_errno = 0; nss-resolve.c:323-324
必须支持 buffer 不足时返回 TRYAGAIN 检查 buflen < ms nss-resolve.c:272-277

NSS 模块调用 sd_varlink_call(link, "io.systemd.Resolve.ResolveHostname", ...)​,这是一个同步阻塞 API。但 sd-varlink 内部完全是异步的,sd_varlink_call 实际是把同步语义”叠”在异步状态机之上的包装。展开 sd-varlink.c:2183-2271​ 的 sd_varlink_call_full():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_public_ int sd_varlink_call_full(sd_varlink *v, const char *method, ...) {
...
/* 把请求 JSON 入队 */
r = sd_json_buildo(&m,
SD_JSON_BUILD_PAIR_STRING("method", method),
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters));
r = varlink_enqueue_json(v, m);

/* 切到 CALLING 状态,挂起等待回复 */
varlink_set_state(v, VARLINK_CALLING); /* L2219 */
v->n_pending++;
v->timestamp = now(CLOCK_MONOTONIC);

/* ★ 核心:用 polling 模拟事件循环 ★ */
while (v->state == VARLINK_CALLING) {
r = sd_varlink_process(v); /* L2224 */
if (r < 0) return r;
if (r > 0) continue; /* 还有未处理的工作 */

r = sd_varlink_wait(v, USEC_INFINITY); /* L2230, 阻塞等 IO */
if (r < 0) return r;
}
...
}

这是一个有趣的模式:NSS 模块没有事件循环,所以 sd_varlink_call_full()自己现造了一个最小的循环。循环里反复调用 sd_varlink_process()​(把所有未完成的工作推进一次),如果没工作可做就 sd_varlink_wait()​ 阻塞在 poll() 上等 socket 可读或可写。

请求 JSON 构造完之后,varlink_enqueue_json()​(sd-varlink.c:1963-1988)决定怎么把它放进输出队列:

1
2
3
4
5
6
7
8
9
10
11
12
static int varlink_enqueue_json(sd_varlink *v, sd_json_variant *m) {
/* 快速路径:没有 fd 要传、队列为空,直接写入 output_buffer */
if (v->n_pushed_fds == 0 && !v->output_queue)
return varlink_format_json(v, m);

if (v->n_output_queue >= VARLINK_QUEUE_MAX) /* 防 DoS */
return -ENOBUFS;

/* 慢速路径:把消息和它关联的 fd 打包成一个 queue 节点 */
q = varlink_json_queue_item_new(m, v->pushed_fds, v->n_pushed_fds);
LIST_INSERT_AFTER(queue, v->output_queue, v->output_queue_tail, q);
}

这里有一个 fd 边界 问题:Varlink 协议规定 SCM_RIGHTS 传递的 fd 必须与某条消息绑定。如果两条消息都各自带 fd,不能简单地拼接到一个连续 buffer(否则 write 部分成功时 fd 会跨消息关联)。VarlinkJsonQueueItem​(varlink-internal.h:77-82)的存在就是为了绑定”一条 JSON 消息 + 它自己的 fd 数组”:

1
2
3
4
5
6
struct VarlinkJsonQueueItem {
LIST_FIELDS(VarlinkJsonQueueItem, queue);
sd_json_variant *data;
size_t n_fds;
int fds[]; /* 柔性数组,与 data 一一绑定 */
};

sd_varlink_process()​(sd-varlink.c:1457-1538)是整个协议的”心脏”,它的执行顺序设计得很讲究:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_public_ int sd_varlink_process(sd_varlink *v) {
...
r = varlink_write(v); /* 1. 先把待发数据写出去 */
if (r != 0) goto finish;

r = varlink_dispatch_reply(v); /* 2. 处理已收到的回复(client) */
if (r != 0) goto finish;

r = varlink_dispatch_method(v); /* 3. 处理已收到的方法调用(server) */
if (r != 0) goto finish;

r = varlink_parse_message(v); /* 4. 从 input buffer 解析出一条 JSON */
if (r != 0) goto finish;

r = varlink_read(v); /* 5. 从 socket 读新数据 */
if (r != 0) goto finish;

r = varlink_test_disconnect(v); /* 6. 检查半关闭 */
...
}

为什么是 “write → dispatch → parse → read” 这个顺序?先 write 是为了让回复尽快出去;dispatch 已解析的 JSON 优先于 read 新数据,是为了避免 buffer 堆积;read 放在最后,这样新数据进入 buffer 后,下一轮 sd_varlink_process() 才会被 parse_message 处理。整个序列保证每一轮 process() 最多完成一次”完整事务”。


第 6 章 服务端:accept 与 dispatch

现在跟踪请求 JSON 抵达服务端之后的旅程。socket 可读时,sd-event 触发 io_callback​(sd-varlink.c:2941-2950​),它调用 handle_revents()​ + sd_varlink_process()​。但这是已建立连接的回调。新连接 走的是另一个回调:connect_callback​(sd-varlink.c:3567-3604):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int connect_callback(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
VarlinkServerSocket *ss = ASSERT_PTR(userdata);
_cleanup_close_ int cfd = -EBADF;
sd_varlink *v = NULL;

cfd = accept4(fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC); /* L3577 */
if (cfd < 0) {
if (ERRNO_IS_ACCEPT_AGAIN(errno)) return 0;
return varlink_server_log_errno(ss->server, errno, ...);
}

r = sd_varlink_server_add_connection(ss->server, cfd, &v); /* L3585 */
if (r < 0) return 0;
TAKE_FD(cfd);

if (ss->server->connect_callback)
r = ss->server->connect_callback(ss->server, v, ss->server->userdata);
...
}

注意 systemd-resolved 没有设置 connect_callback(查看 resolved-varlink.c:1492-1545,只设置了 disconnect 回调),所以新连接进入后立即开始接受请求。这是合理的:DNS 查询是无状态请求,无需在连接建立时做认证。

6.1 安全检查:per-UID 配额

虽然没设 connect 回调,服务端创建时启用了 SD_VARLINK_SERVER_ACCOUNT_UID​(resolved-varlink.c:1501​)。sd_varlink_server_add_connection_pair()​(sd-varlink.c:3466-3553)在 accept 之后会做:

1
2
3
4
5
r = getpeercred(input_fd, &ucred);          /* L3490,内核保证可信 */
...
r = validate_connection(server, &ucred); /* L3497 */
...
r = count_connection(server, &ucred); /* L3509 */

validate_connection()​(sd-varlink.c:3395-3435)执行三道闸:

1
2
3
4
5
6
7
8
9
if (server->n_connections >= server->connections_max) {       /* 默认 4096 */
varlink_server_log(server, "Connection limit ..."); return 0;
}
if (FLAGS_SET(server->flags, SD_VARLINK_SERVER_ACCOUNT_UID)) {
unsigned c = PTR_TO_UINT(hashmap_get(server->by_uid, UID_TO_PTR(ucred->uid)));
if (c >= server->connections_per_uid_max) { /* 默认 128 */
varlink_server_log(server, "Per-UID limit ..."); return 0;
}
}

这两层防护防的是本地恶意进程通过打开海量连接耗尽 resolved 资源。getpeercred()​ 利用内核的 SO_PEERCRED(Linux)在 connect 时刻固化对端 PID/UID/GID,之后即使对端 fork 也无法伪造。

6.2 dispatch_method:JSON 落到 C 回调

请求字节通过 varlink_read()​ → varlink_parse_message()​ 进入 v->current​(一个 sd_json_variant *​),随后 varlink_dispatch_method()​(sd-varlink.c:1277-1455)把它翻译成 C 调用。这个函数是整个 sd-varlink 里最复杂的一个(179 行,圈复杂度 45),核心逻辑可拆为四步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int varlink_dispatch_method(sd_varlink *v) {
/* 步骤 1:解析 JSON 顶层字段,只允许 method/parameters/oneway/more */
JSON_VARIANT_OBJECT_FOREACH(k, e, v->current) {
if (streq(k, "method")) method = sd_json_variant_string(e);
else if (streq(k, "parameters")) parameters = sd_json_variant_ref(e);
else if (streq(k, "oneway")) flags |= SD_VARLINK_METHOD_ONEWAY;
else if (streq(k, "more")) flags |= SD_VARLINK_METHOD_MORE;
else goto invalid; /* 未知字段 → 协议错误 */
}

/* 步骤 2:根据 more/oneway 切到对应处理状态 */
varlink_set_state(v,
(flags & SD_VARLINK_METHOD_MORE) ? VARLINK_PROCESSING_METHOD_MORE :
(flags & SD_VARLINK_METHOD_ONEWAY) ? VARLINK_PROCESSING_METHOD_ONEWAY :
VARLINK_PROCESSING_METHOD);

/* 步骤 3:查方法表,先查用户注册的,再查两个通用 method */
callback = hashmap_get(v->server->methods, method);
if (!callback) {
if (streq(method, "org.varlink.service.GetInfo"))
callback = generic_method_get_info;
else if (streq(method, "org.varlink.service.GetInterfaceDescription"))
callback = generic_method_get_interface_description;
}

/* 步骤 4:IDL 校验参数,然后调用用户回调 */
if (callback) {
v->current_method = hashmap_get(v->server->symbols, method);
if (v->current_method)
r = varlink_idl_validate_method_call(v->current_method,
parameters, flags, &bad_field);
...
r = callback(v, parameters, flags, v->userdata); /* ← 真正干活 */
}
}

对我们跟踪的 io.systemd.Resolve.ResolveHostname​ 来说,callback 就是 vl_method_resolve_hostname


第 7 章 DNS 查询的异步触发

vl_method_resolve_hostname()​(resolved-varlink.c:342-410)是一个典型的”参数校验 + 启动查询 + 返回 1(表示稍后回复)”模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int vl_method_resolve_hostname(sd_varlink *link, sd_json_variant *parameters, ...) {
static const sd_json_dispatch_field dispatch_table[] = {
{ "ifindex", ..., json_dispatch_ifindex, offsetof(LookupParameters, ifindex), SD_JSON_RELAX },
{ "name", ..., sd_json_dispatch_const_string, offsetof(LookupParameters, name), SD_JSON_MANDATORY },
{ "family", ..., sd_json_dispatch_int, offsetof(LookupParameters, family), 0 },
{ "flags", ..., sd_json_dispatch_uint64, offsetof(LookupParameters, flags), 0 },
{}
};
_cleanup_(dns_query_freep) DnsQuery *q = NULL;

/* 1. 用 sd_varlink_dispatch 把 JSON 字段填进 LookupParameters 结构 */
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);

/* 2. 快速路径:如果是字面量 IP 直接合成答案,完全不查网络 */
r = parse_as_address(link, &p);
if (r != 0) return r; /* 已回复 */

/* 3. 构造 DNS question(A/AAAA + IDNA 编码) */
r = dns_question_new_address(&question_utf8, p.family, p.name, false);

/* 4. 创建 DnsQuery 对象 */
r = dns_query_new_for_varlink(m, &q, question_utf8, ...);

/* ★ 5. 关键:把 varlink link 关联到 query,设好完成回调 */
q->varlink_request = sd_varlink_ref(link); /* L399 */
sd_varlink_set_userdata(link, q); /* L400 */
q->complete = vl_method_resolve_hostname_complete; /* L402 */

/* 6. 启动 DNS 查询(可能发多个 DNS 包:多服务器、A+AAAA) */
r = dns_query_go(q);
if (r < 0) return r;

TAKE_PTR(q); /* query 的所有权移交给 manager */
return 1; /* ★ 告诉 sd_varlink_dispatch_method:不要现在回 */
}

最后那个 return 1​ 是关键。varlink_dispatch_method()​(sd-varlink.c:1421-1442)根据当前状态决定下一步:

1
2
3
4
5
6
7
8
9
10
switch (v->state) {
case VARLINK_PROCESSED_METHOD: /* 已被 reply/error */
varlink_clear_current(v);
varlink_set_state(v, VARLINK_IDLE_SERVER);
break;
case VARLINK_PROCESSING_METHOD: /* 未立即回复,稍后回 */
varlink_set_state(v, VARLINK_PENDING_METHOD); /* ← 走这条 */
break;
...
}

VARLINK_PENDING_METHOD​ 状态意味着:这条连接的 method 调用正在等外部事件(DNS 服务器响应),Varlink 状态机释放对它的占用,sd-event 转去处理其他连接。连接和查询通过 sd_varlink_ref(link)互相锚定:link 持有 query 引用,query 也持有 link 引用。这种双向引用靠 vl_on_disconnect 回调清理(见第 9 章)。

7.1 parse_as_address:绕过网络的优化

值得专门讲一下 parse_as_address​(resolved-varlink.c:304-340),它是 systemd-resolved 的一个小而美的优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int parse_as_address(sd_varlink *link, LookupParameters *p) {
/* 检查 name 是否其实是字面量 IP 地址 */
r = in_addr_ifindex_from_string_auto(p->name, &ff, &parsed, &parsed_ifindex);
if (r < 0) return 0; /* 不是字面量 IP,走正常 DNS 路径 */

/* 是字面量 IP,直接合成答案,不查网络、不查缓存 */
return sd_varlink_replybo(link,
SD_JSON_BUILD_PAIR("addresses",
SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_INTEGER("family", ff),
SD_JSON_BUILD_PAIR_BYTE_ARRAY("address", &parsed, ...)))),
SD_JSON_BUILD_PAIR_STRING("name", canonical),
SD_JSON_BUILD_PAIR_INTEGER("flags", ...|SD_RESOLVED_SYNTHETIC));
}

当用户请求 gethostbyname("127.0.0.1")​ 时,根本不需要发 DNS 查询,直接合成回复。这避免了不必要的网络流量和延迟。SD_RESOLVED_SYNTHETIC 标志告诉客户端这个答案是合成的。


DNS 查询完成时(无论成功、超时、DNSSEC 失败),DNS 子系统回调 q->complete​,即 vl_method_resolve_hostname_complete()​(resolved-varlink.c:254-302):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void vl_method_resolve_hostname_complete(DnsQuery *query) {
_cleanup_(dns_query_freep) DnsQuery *q = query;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;

if (q->state != DNS_TRANSACTION_SUCCESS)
return (void) reply_query_state(q); /* 把 DNS 错误映射成 varlink error */

/* 处理 CNAME 链 */
r = dns_query_process_cname_many(q);
if (r == DNS_QUERY_CNAME) { TAKE_PTR(q); return; } /* 查询被重启 */

/* 遍历答案 RR,把 A/AAAA 记录装进 JSON 数组 */
r = find_addr_records(&array, question, q, &canonical, ...);

if (sd_json_variant_is_blank_object(array))
return (void) sd_varlink_error(q->varlink_request,
"io.systemd.Resolve.NoSuchResourceRecord", NULL);

/* ★ 发送最终回复 */
r = sd_varlink_replybo(q->varlink_request,
SD_JSON_BUILD_PAIR_VARIANT("addresses", array),
SD_JSON_BUILD_PAIR_STRING("name", normalized),
SD_JSON_BUILD_PAIR_INTEGER("flags", dns_query_reply_flags_make(q)));
}

sd_varlink_replybo​(sd-varlink.c:2548-2563​)是 sd_varlink_reply​ 的可变参数便捷版。sd_varlink_reply​(sd-varlink.c:2502-2546)做的事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_public_ int sd_varlink_reply(sd_varlink *v, sd_json_variant *parameters) {
/* IDL 校验:回复也要符合接口契约! */
if (v->current_method) {
r = varlink_idl_validate_method_reply(v->current_method,
parameters, 0, &bad_field);
/* 校验失败仅记日志,不阻断(避免服务因 bug 卡死客户端) */
}

/* 把 {"parameters": {...}} 入队 */
r = sd_json_buildo(&m, JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters));
r = varlink_enqueue_json(v, m);

/* 因为我们处于 PENDING_METHOD 状态(异步回复),要切回 IDLE */
if (IN_SET(v->state, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE)) {
varlink_clear_current(v);
varlink_set_state(v, VARLINK_IDLE_SERVER); /* L2539 */
}
return 1;
}

注意 IDL 校验 reply 的设计:服务端发的回复也要符合接口契约。如果 vl_method_resolve_hostname_complete​ 不小心构造了不合法的 JSON,varlink_idl_validate_method_reply 会记日志但不阻断发送——这是为了一个 bug 不会让客户端永久挂起。代码注释里特意写了 “Please adjust test/units/end.sh when updating the log message” 提醒不要改这个文案,因为有测试在 grep 它。

回到 sd_varlink_process()​ 的循环,下一次轮询时 varlink_write()​(sd-varlink.c:721-809​)会真正把 output_buffer 写到 socket:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int varlink_write(sd_varlink *v) {
...
r = varlink_format_queue(v); /* 把 queue 里的 JSON 落入 buffer */
if (v->output_buffer_size == 0) return 0;

if (v->n_output_fds > 0) {
/* 带 fd:必须用 sendmsg + SCM_RIGHTS */
struct msghdr mh = { ... };
mh.msg_control = alloca0(mh.msg_controllen);
struct cmsghdr *control = CMSG_FIRSTHDR(&mh);
control->cmsg_type = SCM_RIGHTS;
memcpy(CMSG_DATA(control), v->output_fds, ...);
n = sendmsg(v->output_fd, &mh, MSG_DONTWAIT|MSG_NOSIGNAL);
} else {
/* 无 fd:优先 send() 带 MSG_NOSIGNAL,失败回退 write() */
n = send(v->output_fd, v->output_buffer + v->output_buffer_index,
v->output_buffer_size, MSG_DONTWAIT|MSG_NOSIGNAL);
}
...
close_many(v->output_fds, v->n_output_fds); /* 发出后立即关闭本地副本 */
v->n_output_fds = 0;
}

MSG_NOSIGNAL​ 是必须的:如果对端已关闭连接,write​ 会触发 SIGPIPE 把进程杀掉。NSS 模块加载在 libc 进程里,SIGPIPE 是绝对不能有的。MSG_NOSIGNAL​ 把这种情形转换成 EPIPE errno,由代码处理。


第 9 章 错误传播与 NSS 状态映射

DNS 查询失败时,reply_query_state()​(resolved-varlink.c:86-155)把 DNS 子系统的内部状态映射成结构化的 varlink error。映射表很完整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
switch (q->state) {
case DNS_TRANSACTION_NO_SERVERS:
return sd_varlink_error(q->varlink_request,
"io.systemd.Resolve.NoNameServers", NULL);
case DNS_TRANSACTION_TIMEOUT:
return sd_varlink_error(q->varlink_request,
"io.systemd.Resolve.QueryTimedOut", NULL);
case DNS_TRANSACTION_DNSSEC_FAILED:
return sd_varlink_errorbo(q->varlink_request,
"io.systemd.Resolve.DNSSECValidationFailed",
SD_JSON_BUILD_PAIR_STRING("result",
dnssec_result_to_string(q->answer_dnssec_result)),
SD_JSON_BUILD_PAIR_CONDITION(q->answer_ede_rcode >= 0,
"extendedDNSErrorCode", SD_JSON_BUILD_INTEGER(q->answer_ede_rcode)),
...);
case DNS_TRANSACTION_RCODE_FAILURE:
return sd_varlink_errorbo(q->varlink_request,
"io.systemd.Resolve.DNSError",
SD_JSON_BUILD_PAIR_INTEGER("rcode", q->answer_rcode),
...);
...
}

注意 DNSSEC 错误带了结构化字段(result​、extendedDNSErrorCode​、extendedDNSErrorMessage​),客户端可以精确知道是哪个验证环节失败。这是 Varlink 相比 D-Bus 的优势之一:错误也是结构化数据,而不是一个字符串。

回到客户端 NSS 模块。nss-resolve.c:230-241 收到 error_id 后做映射:

1
2
3
4
5
6
7
8
9
r = sd_varlink_call(link, "io.systemd.Resolve.ResolveHostname",
cparams, &rparams, &error_id);
if (r < 0) goto fail;
if (!isempty(error_id)) {
if (error_shall_try_again(error_id)) goto try_again; /* → NSS_STATUS_TRYAGAIN */
if (error_shall_fallback(error_id)) goto fail; /* → NSS_STATUS_UNAVAIL */
if (streq(error_id, "io.systemd.Resolve.NoSuchResourceRecord")) goto no_data;
goto not_found; /* → NSS_STATUS_NOTFOUND */
}

error_shall_fallback​(nss-resolve.c:25-36​)和 error_shall_try_again​(nss-resolve.c:38-46)定义了三类错误:

varlink error NSS 状态 含义
Disconnected​/Timeout​/Protocol​/InterfaceNotFound​/MethodNotFound​/MethodNotImplemented NSS_STATUS_UNAVAIL resolved 没回应或协议坏了,libc 应继续尝试 nsswitch.conf 里下一个模块
NoNameServers​/QueryTimedOut​/MaxAttemptsReached​/NetworkDown NSS_STATUS_TRYAGAIN 暂时性失败,libc 应该重试
其他所有(包括 DNSSECValidationFailed​、DNSError​、CNAMELoop 等) NSS_STATUS_NOTFOUND 名字确实查不到
NoSuchResourceRecord NSS_STATUS_NOTFOUND​ + NO_DATA NXDOMAIN-like

这个区分很重要:NSS_STATUS_UNAVAIL​ 会让 libc 继续尝试下一个模块(比如 dns​),用户最终能拿到答案;NSS_STATUS_NOTFOUND​ 则会让 libc 终止查找链(防止 dns 模块又查一次同样的失败名字)。

9.3 vl_on_disconnect:客户端消失时清理查询

最后一个常被忽略的细节。如果客户端在 DNS 查询完成前就关闭了 socket(NSS 模块加载在调用进程里,进程崩了),resolved 不应该继续浪费资源查 DNS。这就是 vl_on_disconnect​(resolved-varlink.c:157-180)的作用:

1
2
3
4
5
6
7
8
9
10
11
12
static void vl_on_disconnect(sd_varlink_server *s, sd_varlink *link, void *userdata) {
Manager *m = sd_varlink_server_get_userdata(s);
...
q = sd_varlink_get_userdata(link); /* 取出 link 上挂的 query */
if (!q) return;

if (!DNS_TRANSACTION_IS_LIVE(q->state))
return;

log_debug("Client of active query vanished, aborting query.");
dns_query_complete(q, DNS_TRANSACTION_ABORTED); /* ← 中止查询 */
}

回看第 7 章 vl_method_resolve_hostname​ 里 sd_varlink_set_userdata(link, q) 的设置——这个双向锚定的另一半就是给 disconnect 回调用的。整个生命周期管理靠两个 ref 形成”环”,环的解开发生在两个对称点:

  • query 完成 → sd_varlink_reply​ 把状态切回 IDLE_SERVER → disconnect 回调不会被触发
  • 客户端消失 → disconnect 回调 → dns_query_complete(ABORTED) → query 释放 → link 引用计数减 1

第 10 章 端到端时序:把所有章节串起来

把前面各章的步骤串起来,一次完整的 gethostbyname("example.com") 经 varlink 的旅程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
时刻   进程                操作                                    源码位置
───── ────────────────── ───────────────────────────────────── ─────────────────────
t0 应用 getaddrinfo("example.com") libc
t1 应用(NSS) _nss_resolve_gethostbyname4_r 入口 nss-resolve.c:191
t2 应用(NSS) connect_to_resolved nss-resolve.c:48
t3 应用(内核) connect() → AF_UNIX sd-varlink.c:187
t4 resolved(event) EPOLLIN on listen fd sd-varlink.c:3624
t5 resolved accept4 → new connection fd sd-varlink.c:3577
t6 resolved validate_connection (UID 检查) sd-varlink.c:3497
t7 应用(NSS) sd_json_buildo 构造请求 nss-resolve.c:217
t8 应用(NSS) sd_varlink_call → state=CALLING sd-varlink.c:2219
t9 应用(NSS) sd_varlink_process → varlink_write sd-varlink.c:1467
t10 应用(内核) send() 把 JSON 字节写入 socket
t11 resolved(event) EPOLLIN on connection fd io_callback
t12 resolved varlink_read → recv() sd-varlink.c:883
t13 resolved varlink_parse_message → sd_json_parse sd-varlink.c:978
t14 resolved varlink_dispatch_method sd-varlink.c:1277
t15 resolved IDL validate (符号校验) sd-varlink-idl.c
t16 resolved vl_method_resolve_hostname resolved-varlink.c:342
t17 resolved dns_query_new + dns_query_go resolved-varlink.c:395,404
↓ (状态切到 PENDING_METHOD,让出事件循环)
t18 resolved(event) 处理其他连接 / 等 DNS 回复
t19 resolved(内核) 收到 DNS 响应包,触发 dns_transaction
t20 resolved q->complete = vl_method_resolve_hostname_complete
resolved-varlink.c:254
t21 resolved find_addr_records → JSON 数组 resolved-varlink.c:205
t22 resolved sd_varlink_replybo resolved-varlink.c:292
↓ (状态切到 PROCESSED_METHOD → IDLE_SERVER)
t23 resolved(event) varlink_write → send() sd-varlink.c:721
t24 应用(NSS) EPOLLIN,被 sd_varlink_wait 唤醒 sd-varlink.c:1690
t25 应用(NSS) varlink_read → recv() sd-varlink.c:883
t26 应用(NSS) varlink_parse_message sd-varlink.c:978
t27 应用(NSS) varlink_dispatch_reply → state=CALLED sd-varlink.c:1189
t28 应用(NSS) sd_varlink_call_full 从循环返回 sd-varlink.c:2237
t29 应用(NSS) sd_json_dispatch 解析 reply nss-resolve.c:243
t30 应用(NSS) 填充 struct gaih_addrtuple nss-resolve.c:298
t31 应用(NSS) return NSS_STATUS_SUCCESS nss-resolve.c:326
t32 应用 getaddrinfo 返回,libc 继续业务

t17 → t20 之间是整个链路唯一真正的异步等待窗口(等待上游 DNS 服务器)。在这段窗口里,客户端 NSS 模块在 sd_varlink_wait​ 的 poll()​ 中阻塞,服务端 resolved 在 sd-event 循环里继续服务其他客户端。这是 Varlink 同步 RPC 模型最大的代价:客户端调用线程被完全占用,但好处是 NSS 模块代码极简,无需理解事件循环。


第 11 章 设计哲学与总结

11.1 三个核心设计原则

原则 1:协议极简,状态机丰富

Varlink 协议本身只有”JSON 消息 + NUL 分隔 + 可选 SCM_RIGHTS”——三个要素。但 sd_varlink​ 内部用 21 个状态来精细管理连接生命周期(varlink-internal.h:11-41)。这种”协议瘦、实现厚”的设计让客户端实现可以很简单(只需懂 JSON 即可),而把复杂性集中在一个被反复验证的库内。

原则 2:同步语义,异步实现

NSS 模块是同步 API(_nss_resolve_gethostbyname4_r​ 必须返回 enum nss_status​),但 sd-varlink 内部完全异步。sd_varlink_call_full​(sd-varlink.c:2183​)用 while + process + wait 循环把异步状态机”翻译”成同步阻塞调用,而无需动用线程。这是个很经典的设计,值得在任何”需要把异步库暴露给同步 API”的场景借鉴。

原则 3:资源生命周期用双向 ref + disconnect 回调

第 7 章和第 9 章展示了 varlink link 和 DnsQuery 的互相引用:link 引用 query(在 q->varlink_request​),query 引用 link(在 sd_varlink_set_userdata​)。这种环看似危险,实际通过 vl_on_disconnect 回调提供了单向”切断”机制:无论客户端何时消失,query 都会被 abort 并释放,环就被解开。这是 systemd 处理”跨子系统对象生命周期”的标准模式。

11.2 核心洞察

io.systemd.Resolve 这条链路最精妙的设计,在于把 Varlink 协议的”同步 RPC”语义,优雅地映射到了 DNS 查询的”异步多步事务”模型之上——通过 q->complete函数指针这一个间接层,让 Varlink 层完全不需要知道 DNS 查询要发几个包、查几条 CNAME、是否要重试。

这个间接层的具体体现就是 resolved-varlink.c:402​ 的 q->complete = vl_method_resolve_hostname_complete;​。一行赋值,把”协议层”(Varlink)和”业务层”(DNS query)彻底解耦。systemd-resolved 因此可以同时为 Varlink、D-Bus、内置 DNS stub listener 三种外部接口服务,共用同一套 dns_query_* 实现。这种”业务层不知道上层是谁”的设计,是 systemd-resolved 能在 systemd 259 演化出 Hook、Delegate、Monitor 等多个新接口而无需重写核心的根本原因。


附录:核心数据结构速查

结构 定义位置 关键字段
sd_varlink varlink-internal.h:84-191 state​(VarlinkState)、input_fd​/output_fd​、input_buffer​/output_buffer​、output_queue​(JSON+fd 绑定)、n_pending​(在飞方法数)、reply_callback​、current​(已解析的当前 JSON)、ucred​/peer_pidfd(对端凭证)
sd_varlink_server varlink-internal.h:206-236 methods​(name→callback hashmap)、interfaces​/symbols​(IDL 元数据)、by_uid​(per-UID 连接计数)、n_connections​/connections_max
VarlinkJsonQueueItem varlink-internal.h:77-82 data​(JSON)、n_fds​+fds[](绑定的 fd 数组)
LookupParameters resolved-varlink.c:36-44 ifindex​、family​、name​、flags —— ResolveHostname/Address 的入参聚合
DnsQuery resolved-dns-query.h state​(DNS_TRANSACTION_*)、varlink_request​(反向引用 link)、complete​(完成回调)、question​、answer
Manager resolved-manager.h varlink_server​(主服务)、varlink_monitor_server​(监控服务)、event(sd-event)、DNS scope/server/cache 子系统

附录:本报告涉及的全部源文件

文件 行数 在本报告中的角色
src/libsystemd/sd-varlink/sd-varlink.c 4351 Varlink 协议库主体(客户端+服务端)
src/libsystemd/sd-varlink/varlink-internal.h 261 sd_varlink​/sd_varlink_server 结构与 21 状态枚举
src/libsystemd/sd-varlink/sd-varlink-idl.c 1993 IDL 解析与请求/回复类型校验
src/nss-resolve/nss-resolve.c 729 客户端 NSS 模块,把 gethostbyname 翻译为 Varlink
src/resolve/resolved.c ~80 systemd-resolved 进程入口
src/resolve/resolved-manager.c 2364 Manager 启动、manager_varlink_init()
src/resolve/resolved-varlink.c 1566 服务端 method handler、vl_method_resolve_*
src/resolve/resolved-dns-query.c 1629 DNS 查询抽象(协议无关)
src/shared/varlink-io.systemd.Resolve.c 419 io.systemd.Resolve 接口的 IDL 定义

基于 systemd 259.6 源码分析。所有 file:line 引用均可在源码树中直接定位。

源码下载:https://kojipkgs.fedoraproject.org//packages/systemd/259.6/1.fc44/src/systemd-259.6-1.fc44.src.rpm