像Envoy xDS一样,但用于eBPF过滤器。
Netfence作为一个守护进程运行在你的VM/容器主机上,自动将eBPF过滤器程序注入cgroup和网络接口,并内置DNS服务器,解析允许的域名并填充IP允许列表。
Netfence守护进程可以单独通过本地Unix socket API驱动,或者连接到你通过gRPC实现的中央控制平面,以与后端同步允许列表/拒绝列表。
你的控制平面将网络规则(如ALLOW *.pypi.org或ALLOW 10.0.0.0/16)推送到附加的接口/cgroup。当VM/容器查询DNS时,Netfence解析它,将IP添加到eBPF过滤器,并在流量离开主机之前丢弃到未知IP的流量,其热路径开销在当前基准测试中与普通socket连接几乎无法区分。
在允许列表模式下,IPv4链路本地(169.254.0.0/16)默认不再自动允许——因此云元数据服务(169.254.169.254)会被阻止,除非明确加入允许列表。这是有意的:元数据服务是凭证窃取的目标,沙箱工作负载不得隐式访问它。本地主机(127.0.0.0/8, ::1)和IPv6邻居发现(fe80::/10, ff02::/16)默认保持允许,以便基本连接和NDP正常工作。要允许工作负载访问元数据服务,请将169.254.169.254/32加入允许列表(通过控制平面进行每个附加点的排除项覆盖是计划中的后续功能)。
IPv4广播(255.255.255.255)和多播(224.0.0.0/4)没有排除项,受策略约束,因此在TC允许列表模式下,像DHCP续约广播这样的流量会被阻止,除非明确加入允许列表。排除项检查在拒绝列表之前运行,因此一个排除的范围只能通过关闭其排除项来阻止——而且由于IPv4链路本地现在默认关闭,拒绝列表模式也可以阻止元数据服务。
该解决方案的几个主要优点,其他选项通常不支持:
secretdata.someattacker.com数据外泄)据我所知,没有其他解决方案能同时提供所有这些功能。
已知限制:cgroup附加在socket层过滤(connect/sendmsg钩子),因此拥有CAP_NET_RAW的进程可以构造绕过它们的原始数据包。对于可能持有CAP_NET_RAW的工作负载,请使用TC(接口)附加,它在设备层进行过滤。
然而,这的确比httpjail之类的方案有稍多一些的开销。
这些数字是在特权Docker Linux gate上使用linux/arm64和make bench-docker测量的。数值是五个样本的中位数。
热socket基准测试使用已连接的UDP socket来将cgroup/connect4 eBPF钩子的成本与TCP握手延迟分离。在此路径中,DNS已经解析了域名,IP仍在TTL内,并且IP/CIDR已经存在于eBPF映射中。
| Path | Median latency |
|---|---|
| 普通socket连接,无eBPF | ~2.647 us |
| 热允许列表,受保护LPM命中 | ~2.691 us |
| 热允许列表,DNS精确主机命中 | ~2.741 us |
| 允许列表未命中,本地阻止 | ~1.652 us |
测量到的普通路径、受保护LPM路径和DNS精确主机连接路径之间的差异在样本噪声范围内。
目前没有“内核未命中询问父进程”的路径。cgroup允许列表未命中由eBPF本地决定并立即阻止。
这些数字测量的是DNS服务器路径,而不是热socket连接路径。
| Path | Median latency |
|---|---|
| 代理查询冷,进程内策略函数 | ~31.336 us |
| 代理查询热 | ~27.964 us |
| 带有本地上游的允许列表查询冷 | ~53.510 us |
| 带有本地上游的允许列表查询热 | ~53.432 us |
冷行通过实际的附加突变屏障同步,并在查询之间清除基准测试所有权图和模拟精确映射快照。计时器持续运行以保持UDP调度器局部性,而ns/op减去单独报告的fixture-reset-ns/op墙钟时间(包括客户端接收到数据包后先前处理器的任何尾部),从而测量当前客户端Exchange。raw-total-ns/op同时报告两者。重置保留配置的策略域和后备存储,基准测试断言每个查询进行一次物理精确映射添加。热行预先初始化所有权一次,并在整个运行中断言一次物理添加。
下面的内部所有权微基准测试是可扩展性诊断,不是端到端DNS查询路径验收行。缓存的辅助函数仅保留用于测试和基准测试;它一次包装一条记录并重复域名验证。它和普通解析器流量都经过附加突变屏障,而普通解析器流量将每个完整响应视为一个事务。
| Internal scalability diagnostic | Current median | Memory / allocations |
|---|---|---|
| 冷新密钥准入,空所有权图 | ~370.3 ns | 232 B, 5 allocs/op |
| 冷新密钥准入,4095个不相关条目 | ~451.2 ns | 232 B, 5 allocs/op |
| 物理容量压力和LRU替换 | ~3.820 ms | ~4.23 MB (4,226,243 B), 4,336 allocs/op |
| 物理预算耗尽预检查 | ~611.9 ns | 344 B, 9 allocs/op |
| 最大边缘压力,64个地址响应 | ~6.849 ms | ~7.66 MB (7,658,774 B), 2,233 allocs/op |
| 最大图工作保护,允许完整计划 | ~2.763 ms | ~4.26 MB (4,264,386 B), 3,074 allocs/op |
| 最大图工作保护,预投影耗尽拒绝 | ~10.935 us | 8.76 KB (8,760 B), 14 allocs/op |
| 接近数值上限的变动预算操作 | ~18.98 ns | 0 B, 0 allocs/op |
| 一致的所有权统计快照 | ~2.094 ns | 0 B, 0 allocs/op |
| 跨4095个条目的无操作过期扫描 | ~74.849 us/scan | 0 B, 0 allocs/op |
+------------------+ +-------------------------+ | Your Control |<------->| Daemon (per host) | | Plane (gRPC) | stream | | +------------------+ | +-------------------+ | | | DNS Server | | | | (per-attachment) | | | +-------------------+ | +-------------------------+ | +------+------+ | | TC Filter Cgroup Filter (veth, eth) (containers)
每个附加组件都会获得由守护进程分配的独特 DNS 地址(端口)。容器/虚拟机必须配置为使用分配的 DNS 地址;过滤常规工作负载的 DNS 流量不会透明地将其重定向。
### DNS 解析器拓扑与行为
`dns.listen_addr` 必须指定一个每个附加工作负载都能到达的具体 IPv4 或 IPv6 地址。通配符地址被拒绝,因为它们不能作为解析器端点进行通告。配置的主机名在守护进程启动时解析一次,得到的具体 IP 用于绑定、通告、持久化和过滤器引导。默认的 `127.0.0.1` 仅在工作负载与守护进程共享网络命名空间时适用;位于其他命名空间中的容器或虚拟机通常需要一个可达的主机/网桥地址。```yaml
dns:
listen_addr: 10.0.0.1
port_min: 11000
port_max: 11500
# Daemon-global fallback when DnsConfig.upstream_servers is empty.
upstream: 1.1.1.1:53
# Hard daemon ceilings for each attachment's bounded DNS exact ownership.
# Zero/unset uses these defaults (max_ips_per_family instead derives from
# filter.max_dns_rule_entries).
max_ips_per_family: 4096
max_ips_per_response: 64
max_ips_per_policy_domain: 1024
max_tracked_domains: 1024
max_ownership_edges: 8192
# Rolling physical-admission/LRU mutation budget and slow-planning work
# allowance. The window is daemon-global and immutable until restart;
# DnsConfig.max_churn_units may only lower the daemon ceiling.
max_churn_units: 8192
churn_window: 1m
Attach 返回具体的 dns_address;将确切的该地址配置为工作负载的解析器。Netfence 会为监听器 IP 安装一个受保护的、永不过期的 /32 或 /128 允许条目,以便白名单模式可以在没有控制平面 DNS-IP 规则的情况下启动。当前过滤器强制执行 IP 前缀,而非目标端口,因此该受保护条目允许监听器 IP 上的所有端口(而不仅仅是其 DNS 端口);这对于 cgroup 挂载威胁模型尤其重要。当这种更广泛的可达性不可接受时,请使用专用的监听器 IP。
分配到的端点同时提供 UDP 和 TCP 服务。UDP 回复会被截断至传统客户端的 512 字节限制或其所通告的 EDNS 大小,并在需要时携带 TC 标志,允许工作负载通过 TCP 重试同一端点。对于上游解析,截断的 UDP 答案会首先针对同一上游通过 TCP 重试。如果传输失败、收到 SERVFAIL 或 REFUSED,则按顺序进入下一个配置的上游。
DnsConfig.upstream_servers 会覆盖守护进程全局的 dns.upstream 设置(仅针对单个挂载)。条目使用 host:port 语法(IPv6 字面量需加方括号),会按首次出现顺序进行规范化并去重,且最多限制为八个唯一服务器。空列表表示选择全局回退。
在过滤模式下,Netfence 会从 HTTPS/SVCB 应答中剥离 ipv4hint 和 ipv6hint 参数,包括其对应的 mandatory 引用,因为提示地址未经独立过滤准入。禁用模式则保持上游应答不变。
DNS 查询计数器是互斥的:dns_queries_allowed 统计成功应答的策略允许查询(包括 NXDOMAIN),dns_queries_blocked 统计策略 REFUSED 响应,dns_queries_errors 统计解析器、代理、过滤准入、响应写入及其他错误路径。每个查询仅计入一个桶。
在过滤 DNS 模式下,每个包含地址的响应在返回任何 A/AAAA 地址之前,会作为一个事务被准入到挂载的精确 IPv4/IPv6 HASH 层级中。如果无法表示完整响应,解析器会返回 SERVFAIL 且不返回任何地址,并保留先前准入的工作集。返回地址的 PROXY 决策必须设置 add_to_filter;否则它们也会作为 SERVFAIL 失败关闭。禁用 DNS 模式是明确的透传例外。
精确条目携带从规范化查询到匹配策略所有者的 TTL 边界。删除或否认某个域名会迅速移除其最后一个仅 DNS 的精确地址,而共享地址会因另一个活跃查询所有者而存活,且重叠的控制平面 CIDR 会在受保护的 LPM 层级中独立继续存在。DNS 黑名单的默认允许和显式允许答案也会被跟踪,即使数据包黑名单忽略精确允许,这样稍后切换到白名单模式时可以使用已返回的缓存地址而无需重新查询。
所有正常/活跃的用户空间所有权状态受上述五个所有权设置约束。恢复的合成临时边界不受这些逻辑限制,因此在协调完成前不会被遗忘,但受物理 IPv4/IPv6 精确映射的约束。已配置的策略域和活跃查询域共享 max_tracked_domains,每个 (query, matched owner, IP) TTL 记录会消耗一个 max_ownership_edges 槽位。
在压力情况下,准入首先移除过期的 TTL 边界。然后,它会在最近最少使用之前,以最小物理副作用的方式回收完整的逻辑查询/所有者边界,接着是确定性的解析器观察到的 LRU(规范 IP 打破平局)。物理驱逐会移除整个 DNS 精确键及其所有 DNS 所有者。传入的物理 IP 和精确的传入 (IP, query, owner) 边界在响应事务期间受到保护。恢复的临时所有权会保护其物理键直到权威协调完成,但共享该键的不相关正常 DNS 元数据仍可能被回收。权威控制平面/系统允许以及所有拒绝仍保持在独立的 LPM 层级中,且永远不是 DNS 回收的候选对象。
每个挂载的滚动变更预算:新增一个物理精确键计为一个单位,每个被驱逐的活跃物理 DNS 键计为一个单位;因此一个完整的旧到新替换耗费两个单位。刷新、仅逻辑回收、过期和策略移除计为零。事件在年龄小于 dns.churn_window 时保持活跃,并在精确边界时过期。DnsConfig.max_churn_units 只能降低守护进程上限;零表示继承该上限。窗口不能由控制平面更改,更改守护进程上限/窗口需要重启。降低然后提高某个挂载的限制不会遗忘仍处于活跃状态的历史记录。
一个独立的滚动工作账本限制了昂贵的所有权图规划。快速刷新和普通准入从不触及它。在压力路径克隆或分析图之前,Netfence 会根据当前物理键、所有权边、跟踪域以及相对于不可变守护进程/映射上限的响应大小,收取稳定的工作单位。即使后续计划证明不可能或精确映射事务失败,该尝试收费也会保留,从而在不改变上述事务性物理变更记账的前提下,关闭零变体重试路径以应对 CPU/分配压力。在默认上限下,该配额允许每个窗口进行八个最大等效图传递;降低 DnsConfig.max_churn_units 至少保留一个。降低然后提高限制永远不会重新缩放或忘记活跃的工作历史。
当没有合格的 DNS 状态可以满足绑定时,或者任一滚动配额耗尽时,Netfence 会保留准入的工作集并返回 SERVFAIL,而不返回未准入的地址。容量故障会增加 map_full_drops;物理变更和规划工作量限制不会增加。心跳会暴露精确映射的当前值、容量和进程世代高位值,以及累积的 DNS LRU 驱逐、所有准入失败,以及覆盖两个滚动守卫的聚合预算限制计数。容量、物理预算和工作预算的压力/恢复日志独立进行速率限制。操作员可以等待 TTL/窗口恢复,减少响应/域名变更或重复的容量压力尝试,或者将 DnsConfig.max_churn_units 提高到守护进程 dns.max_churn_units 上限。提高守护进程上限需要重启;增加 filter.max_dns_rule_entries 还需要重新创建挂载,因为固定映射无法原地调整大小。
权威控制平面 CIDR 和守护进程系统规则使用四个独立的、不可驱逐的 LPM 映射:允许/拒绝 × IPv4/IPv6。每个映射有 filter.max_rule_entries 个槽位。DNS 监听器 /32 或 /128 引导是一个系统允许,并计入相应的受保护允许映射。DNS 精确主机条目保留在其独立映射中,不能消耗这些槽位。没有受保护规则会进行 LRU 驱逐:显式允许、系统规则以及所有拒绝会一直保留,直到被授权删除或完全替换。