像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查询路径验收行。缓存的辅助函数仅保留用于测试和基准测试;它一次包装一条记录并重复域名验证。它和普通解析器流量都经过附加突变屏障,而普通解析器流量将每个完整响应视为一个事务。
+------------------+ +-------------------------+ | 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 驱逐:显式允许、系统规则以及所有拒绝会一直保留,直到被授权删除或完全替换。
一次完整的 SubscribedAck 或 BulkUpdate 会被规范化,并在变更前检查所有四个映射的最终占用情况。容量基于最终状态,因此在满映射中替换键是有效的;超大规模状态会被拒绝,而不会驱逐或部分接受规则。幸存者不会被移除并重新添加。如果后续映射系统调用失败,Netfence 会恢复并验证确切的调用前四个映射清单。回滚后确认的模式是旧模式或 BLOCK_ALL(通常是 BLOCK_ALL),因此守护进程仍保持挂载故障关闭,直到完整的权威重试成功。
受保护策略安全状态会与挂载一起持久化,并在心跳中导出。BLOCK_ALL 本身是一种正常、健康的已配置模式:当 policy_degraded_reason 为空时,policy_degraded 为 false。在健康的 BLOCK_ALL 状态下启动的有风险的受保护变更会首先记录 protected_policy_mutation_in_progress。这是一个临时崩溃日志,而不是稳定的故障诊断:实时操作可能会在最终日志清除保存之前发布其预期模式,成功完成会清除日志本身。如果启动时在崩溃后发现它,启动会首先强制并证明 BLOCK_ALL,然后持久化 protected_policy_mutation_interrupted。稳定的降级原因代码是:
protected_policy_mutation_interruptedauthoritative_protected_policy_failedincremental_deny_install_failedincremental_allow_removal_failedincremental_mode_change_failed这些是稳定的分类,而不是原始的系统调用/存储文本。进行中日志是一个内部持久化的崩溃边界;心跳状态与所属变更一起序列化,因此要么观察到其成功清除,要么观察到稳定的故障转换,而不是实时的中间日志。稳定的降级/中断原因会使数据包执行保持在已证明的 BLOCK_ALL 状态,并拒绝增量 CIDR 和数据包模式命令。独立的 DNS 配置更改和 DNS TTL 过期可能在该已证明的保持下继续,但不能清除稳定原因或重新激活数据包策略。从稳定原因恢复需要一次完整的 LPM 和 DNS 期望状态:通过控制平面或本地 API 应用 BulkUpdate(或者对恢复后的挂载的新 Subscribed 回复 SubscribedAck)。Netfence 暂存完整的受保护状态,应用权威 DNS 状态,激活请求的模式,并且仅在每一步都成功后清除持久化原因。建议在控制平面恢复 BulkUpdate 上使用唯一的 command_id,并要求成功的 CommandResult;本地 API 拒绝 command_id,因为其一元 RPC 结果已经报告成功或失败。
心跳会暴露四个受保护映射各自的当前物理条目、硬容量和守护进程世代高位值。引导包含在内;采用的固定映射会初始化新一代的高位值。map_full_drops 是累积的,包含受保护容量拒绝。如果占用读取失败,守护进程会保留最后证明的快照,并以最多每 30 秒一次的频率发出警告,而不是虚构新计数。要从压力中恢复,请将完整期望规则减少到每个映射容量以下,然后重试完整更新。提高 filter.max_rule_entries 仅能在加载时进行,并且需要重新创建现有的固定挂载。如果守护进程无法证明 BLOCK_ALL 或持久记录其安全标记,它将停止变更准入;请修复映射/存储故障并重启,而不是假设执行已重新打开。
运行守护进程,它会:
DaemonService),用于挂载、策略和检查ControlPlane.Connect)连接到您的控制平面启动守护进程:```bash
netfenced start
netfenced start --config /etc/netfence/config.yaml
**检查守护进程状态:**```bash
netfenced status
如果没有control_plane.url,新的附件将在禁用的数据包/DNS模式下提交,并可通过本地API或CLI立即配置。不需要控制平面进程来执行“每个连接”下记载的独立工作流。
本地gRPC API没有每个RPC的身份验证。对其Unix套接字的文件系统访问是授权边界,每个能够连接的进程都是完全受信任的主机网络管理员:它可以附加或分离主机eBPF程序、替换数据包和DNS策略、以及打开或关闭工作负载流量。限制套接字组成员范围并保护套接字的父目录。```yaml
socket: /run/netfence/netfence.sock
socket_group: netfence-admin
`NETFENCE_SOCKET` 和 `NETFENCE_SOCKET_GROUP` 是等效的环境变量。启动时,守护进程在私有暂存目录中绑定套接字,在其不可达时设置组和模式 `0660`,然后原子地发布它。守护进程会移除在配置目标处预先存在的 Unix 套接字——它不会区分过期套接字与另一个活动守护进程拥有的套接字——因此恰好一个守护进程必须拥有一个套接字路径。如果目标不是套接字,它会拒绝移除。在 Linux 上,no-replace 重命名可防止在移除后覆盖新创建的路径;关闭时仅当发布路径仍然标识守护进程自己的套接字 inode 时才会移除它。无效的组、所有权/模式失败或非套接字目标会导致启动失败,而不会发布一个允许的端点。
### Control-plane transport security (TLS / mTLS / bearer token)
控制面通道是系统中价值最高的攻击面(谁控制了它就可以向每个工作负载推送 `ALLOW` 规则),因此守护进程**默认拒绝**:如果设置了 `control_plane.url`,配置必须显式选择一种传输方式——要么是 `control_plane.tls` 块,要么是 `control_plane.insecure: true`。如果 URL 两者都没有,则在启动时被拒绝;没有隐式的明文默认值。(这是一个故意的行为变更:旧版本会静默地以未加密方式拨号控制面。)```yaml
control_plane:
url: cp.internal:443
tls:
# CA bundle used to verify the control-plane server certificate.
# Path to a PEM file or inline PEM; omit to use the system root pool.
ca: /etc/netfence/cp-ca.pem
# Client certificate + key (path or inline PEM). Setting BOTH enables
# mTLS: the daemon presents this cert to the control plane. Setting only
# one is a config error.
cert: /etc/netfence/daemon.pem
key: /etc/netfence/daemon.key
# Optional hostname override for server certificate verification (SNI),
# e.g. when dialing by IP.
server_name: cp.internal
# Optional bearer token, sent as `authorization: Bearer <token>` metadata
# on every control-plane RPC. Refused on a plaintext channel unless
# `insecure: true` was explicitly set (so a misconfiguration can't leak it).
auth_token: "..."
仅使用系统根证书的 TLS(公共 CA 颁发的服务器证书,无 mTLS)仅仅是一个空块:```yaml control_plane: url: cp.example.com:443 tls: {}
本地开发使用的明文是明确的选择(与 `tls` 互斥):```yaml
control_plane:
url: localhost:9000
insecure: true
证书和密钥在启动时只加载一次,因此错误的路径/PEM 会在启动时以清晰错误信息失败,而不是在每次重连时暴露。
守护进程会在控制面连接上发送 HTTP/2 保活 ping,从而检测并拆除静默死路径(电缆拔出、NAT 映射丢失、路由黑洞),时间大约为 keepalive_time + keepalive_timeout——而不是在 CONNECTED 状态下坐等数分钟直到内核 TCP 重传超时,同时每个代理的 DNS 查询都在耗尽完整超时。重连采用带抖动的指数退避进行节奏控制(从 1 秒开始,翻倍,±20% 抖动,上限为 reconnect_backoff_max);退避仅在连接保持健康状态 30 秒后才会重置到初始值,因此如果控制面接受连接但立即断开,则会持续退避,而非在初始值上被反复冲击。```yaml
control_plane:
keepalive_time: 30s
keepalive_timeout: 10s
reconnect_backoff_max: 30s
零/未设置值表示默认值——它们**不**会禁用 keepalive 或 backoff。您的控制平面必须在其 gRPC keepalive 强制策略中允许此 ping 频率(见下文),否则它将拒绝守护进程并返回 `ENHANCE_YOUR_CALM (too_many_pings)`。
### 守护进程重启、崩溃与升级(固定 BPF 状态)
守护进程将每个附件的 BPF 链接和规则映射固定到 bpffs(`filter.bpf_pin_dir`,默认为 `/sys/fs/bpf/netfence`,每个附件 ID 对应一个目录)。由于固定状态由内核——而非守护进程——持有,**当守护进程停止时,强制策略继续执行**:崩溃(`kill -9`)、正常停止或升级都会使最后已知的策略(模式 + 所有规则)持续生效,下一次守护进程启动时将按原样重新启用固定状态。恢复操作从不重新附加或重写活动映射,因此不会出现允许列表中的工作负载被阻止、或被阻止的目标被允许的时间窗口,也不会出现重复附加的情况。
停止行为通过显式配置(`filter.detach_on_stop`)控制:```yaml
filter:
# false (default): stopping the daemon KEEPS ENFORCING — filters stay
# attached via their bpffs pins and are re-adopted on the next start
# (fail-closed across restarts/upgrades).
# true: stopping the daemon detaches filters and removes their pins —
# traffic is unfiltered while the daemon is down (explicit fail-open).
detach_on_stop: false
# bpffs directory for pinned state. Must be on a bpffs mount; the daemon
# mounts bpffs at /sys/fs/bpf if needed (privileged). An explicit "" turns
# pinning off entirely (BPF state then dies with the process).
bpf_pin_dir: /sys/fs/bpf/netfence
# Capacity of each authoritative/system LPM map (allowed/denied per family).
# Protected entries are non-evictable; the DNS listener bootstrap consumes
# one slot in its address family. Changing pinned-map capacity requires
# recreating the attachment.
max_rule_entries: 4096
# Independent capacity of each DNS-derived exact-host HASH map (IPv4/IPv6).
# These entries can never consume or evict authoritative/deny capacity.
max_dns_rule_entries: 4096
显式的 Detach(RPC/CLI),或移除一个由一致性拥有的活跃目标,会连同附件一起销毁固定状态。重新启动时,目标缺失不授权进行猜测:未来的、未提交的、混合的或其它无法验证的持久化固定将被保留,启动中止以便检查。
固定目录是一种版本化的持久化格式。模式标记在最后才固定,仅在所有必需的地图和链接都存在之后。升级一个前精确层级的附件会使用进行中标记固定两个新的空精确地图,原子性地替换每个链接的程序,同时重用活跃的权威地图,验证程序/地图身份,并最后提交标记。崩溃或模糊的更新会使标记未提交;下一次启动将使用相同的地图重新更新每个链接。旧的和新的程序世代在那个受限的混合状态期间执行相同的权威LPM策略,因此迁移永远不会取消固定或重新创建可行的过滤器。未知、不完整或无法验证的固定集会被保留并中止启动以便检查,而不是被猜测掉。
关于重新采用状态的说明:
SyncRequest,然后为每个仍需协调的已恢复附件发送一个完整的 Subscribed 声明。回复应使用一个全新的 SubscribedAck:其模式、CIDR、TTL和DNS配置即是完整的期望状态。守护进程应用一个增量(未改变的CIDR永远不会被移除),并且仅在整个ACK应用完成之后才清除恢复标记。超时、断开连接或验证失败会保持实施不变。过滤器/地图/DNS/存储应用失败可能会留下部分增量,但协调过程不会使用整体地图清除或移除/重新添加未改变的幸存者;恢复标记保持设置,守护进程在后续连接后重试。SubscribedAck 到达;该权威ACK完全替换其生命周期,包括缩短到期时间或将临时永久规则变回有限TTL规则。已恢复的精确DNS键被清单化并由有限临时所有者(实际固定地图容量即为界限)表示;第一个权威DNS配置丢弃所有合成声明,移除无所有者的键,并且仅在某个键另有正常的活跃所有者时才保留该键。非规范/冲突的清单会中止恢复,而不会猜测或部分发布所有权元数据。netfenced apply-rules,以替换临时数据包状态并恢复其完整的期望状态和TTL。REFUSED,直到应用完整的 BulkUpdate 或 SubscribedAck。显式设置为DISABLED的DNS模式保持转发。如果已提交的UDP/TCP监听器之后意外死亡,附件将被隔离为IP BLOCK_ALL,并报告为错误取消订阅。您的编排系统调用守护进程的本地API。
RPC:``` DaemonService.Attach(interface_name: "veth123", tc_direction: TC_DIRECTION_INGRESS, metadata: {vm_id: "abc"}) // or DaemonService.Attach(cgroup_path: "/sys/fs/cgroup/...", metadata: {container_id: "xyz"})
**CLI:**```bash
# Attach to a host-side veth peer or VM tap (TC) - use ingress direction
netfenced attach --interface veth123 --direction ingress --metadata vm_id=abc
# Attach to a cgroup
netfenced attach --cgroup /sys/fs/cgroup/... --metadata container_id=xyz
# Attach to an uplink inside the workload's own netns (TC) - egress is the default
netfenced attach --interface eth0 --metadata tenant=acme,env=prod
TC 方向: tc_direction 字段(CLI --direction)选择过滤器附加到哪个 TCX 钩子,选择正确的方向取决于接口位于链路的哪一侧:
| 接口 | 正确方向 | 原因 |
|---|---|---|
| 上行链路(例如 ),或工作负载自身网络命名空间内的任何接口 |
方向仅适用于接口(TC)附加;对于 cgroup 附加则忽略。
control_plane.url 时,守护进程发送 Subscribed{id, target, type, metadata} 并等待带有初始配置(模式、CIDR、DNS 规则)的 SubscribedAck。如果控制平面在超时(默认 5 秒,可通过 control_plane.subscribe_ack_timeout 配置)内未响应,则附加操作被回滚,附加调用失败。验证和其他预提交失败遵循相同的普通回滚规则。BLOCK_ALL 状态,而不是破坏性地回滚。在有界超时内,Attach 返回一个包含保留附加 ID 的错误;调用者可以通过匹配 List 中的目标发现该 ID,控制平面必须通过完整的 BulkUpdate 恢复它。subscribe_ack_timeout: 0 时,新的 Attach 在将 Subscribed 排队后返回;稍后的 ack 仍然会被验证和应用。此零值不会禁用已恢复附加的协调:恢复尝试在后台等待最多 5 秒,如果需要,会在后续连接上重试。如果稍后的 ack 遇到 protected 压力,已经返回的附加会被保留在 BLOCK_ALL 中;SubscribedAck 既不发出 也不发出错误 ,并且守护进程在重启前不会自动重新驱动声明。在 中检测 及其原因、占用/容量以及 ,然后发送一个带有 的完整 以获得明确的恢复结果。RPC:``` DaemonService.Detach(id)
**CLI:**```bash
netfenced detach --id <attachment-id>
列出附件:```bash netfenced list netfenced list --all # fetch all pages
### 本地策略与检查
每个本地变更都是对单一`DaemonService.ApplyCommand(ControlCommand)` RPC的轻量CLI编码。提供由`attach`返回的附件ID:```bash
# Packet policy and protected CIDRs.
netfenced set-mode <id> allowlist
netfenced allow-cidr <id> 10.0.0.0/8
netfenced allow-cidr <id> 192.0.2.10/32 --ttl 5m
netfenced deny-cidr <id> 10.20.0.0/16
netfenced remove-cidr <id> 10.20.0.0/16 --list deny
# --list accepts allow, deny, or both (the default).
# DNS policy.
netfenced set-dns-mode <id> denylist
netfenced allow-domain <id> example.com --subdomains
netfenced deny-domain <id> blocked.example.com
netfenced remove-domain <id> blocked.example.com
# Deterministic current-policy inspection as protobuf JSON.
netfenced rules <id>
数据包模式包括 disabled、allowlist、denylist 和 block-all;DNS 模式
包括 disabled、allowlist、denylist 和 proxy。DNS proxy 需要
一个可达的已配置控制平面。域名匹配使用最具体的
匹配后缀;当同样具体的允许和拒绝规则同时匹配时,拒绝
优先。CIDR 和域名会被规范化为标准格式。负值、格式错误或
无效的 TTL、枚举、CIDR、域名、选择器和嵌套消息在变更之前会被拒绝,
因此无效命令将成为策略无操作。
对于完全替换,apply-rules 从文件或标准输入读取现有的 BulkUpdate
protobuf JSON 形状:```bash
cat >rules.json <<'JSON'
{
"mode": "POLICY_MODE_ALLOWLIST",
"allowCidrs": [{"cidr": "10.0.0.0/8"}],
"dns": {
"mode": "DNS_MODE_DENYLIST",
"denyDomains": [{"domain": "blocked.example.com", "includeSubdomains": true}]
}
}
JSON
netfenced apply-rules --file rules.json
netfenced apply-rules --file - < rules.json
`ApplyCommand` 仅接受 `SetMode`、`AllowCIDR`、`DenyCIDR`、`RemoveCIDR`、
`BulkUpdate`、`SetDnsMode`、`AllowDomain`、`DenyDomain` 和 `RemoveDomain`。
仅流的同步/确认变体、未知或空命令以及本地
`command_id` 值将被拒绝。`BulkUpdate` 也是唯一能够恢复稳定降级数据包策略的本地操作;
它必须包含完整的 LPM 和 DNS 期望状态。
可选的 `ControlCommand.remove_cidr_list` 选择器可以针对允许列表、
拒绝列表,或两者都针对(当命令变体为 `RemoveCIDR` 时)。其旧版未指定值
以及显式的 `BOTH` 均从两个列表中移除,保留原始协议行为。
本地和控制面突变共享一个解析器、突变屏障、TTL
注册表、故障关闭恢复路径以及策略所有者。故意没有
本地与控制面之间的所有权仲裁:对单个策略列表的冲突操作按照其提交顺序生效,
无论来源如何。特别是,后续的完整控制面 `BulkUpdate` 或
`SubscribedAck` 可以替换本地状态。
`GetRules`/`netfenced rules` 返回一个连贯、确定性的用户空间
注册表快照。每个 CIDR 报告允许/拒绝列表、本地或控制面
`policyOwned`、守护进程 `systemOwned`、绝对 `expiresAt`、已恢复的
`provisional`,以及最后提交的内核 `installed` 状态。一个已安装的条目
如果没有所有者,则是失败的移除重试,而非期望的策略。DNS 输出是
实时、规范化后的有效 `DnsConfig`。检查有意不
枚举受保护的内核映射,也不暴露动态解析的 DNS 精确主机
缓存条目;请使用心跳遥测来监测受保护映射的占用情况。
## 关于控制面(由你实现)
实现 `ControlPlane.Connect` RPC——一个双向流:
配置你的 gRPC 服务器的保活强制策略,以允许
守护进程的 ping 频率(`control_plane.keepalive_time`,默认 30 秒):将
`MinTime` 设置在该间隔或以下,并设置 `PermitWithoutStream: true`。默认的 gRPC
策略(5 分钟)会将守护进程的 ping 视为滥用行为并关闭
连接,返回 `ENHANCE_YOUR_CALM (too_many_pings)`。在 Go 中:```go
grpc.NewServer(grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 10 * time.Second,
PermitWithoutStream: true,
}))
从守护进程接收:
SyncRequest 在连接/重连时(列出当前所有附件)Subscribed 当新附件添加时,以及在 SyncRequest 之后,针对那些仍需要最新权威状态的已恢复附件Unsubscribed 当附件被移除时Heartbeat 附带统计信息CommandResult{command_id, id, success, error} — 你发送的任何带有非空 command_id 的命令的结果(ControlCommand 上的可选关联 nonce;没有 nonce 的命令不产生结果)。success 仅在命令完全应用时为 true — 部分应用的 BulkUpdate 会报告失败并附带聚合错误。结果为尽力而为:将缺失结果视为未知,而非失败。发送给守护进程:
SyncAck 在接收到 SyncRequest 后发送SubscribedAck{mode, cidrs, dns_config} 在接收到 Subscribed 后发送(必需 — 守护进程会等待此确认)SetMode{mode} — 更改 IP 过滤策略模式AllowCIDR{cidr, ttl} / DenyCIDR / RemoveCIDR(可选地选择允许、拒绝或两者;未指定则保留传统的“两者”行为)SetDnsMode{mode} — 更改 DNS 过滤模式AllowDomain{domain} / DenyDomain / RemoveDomain(最具体匹配获胜;相同具体性时拒绝获胜)BulkUpdate{mode, cidrs, dns_config} — 完整状态同步当控制平面收到 Subscribed 时,必须回复一个完整的 SubscribedAck。对于新附件,守护进程通常会等待该确认,然后才向本地调用者返回成功。对于已恢复的附件,握手在后台进行,同时固定的最后已知策略继续执行。使用元数据标识 VM/租户/容器,并返回完整的期望模式、CIDR(包括 TTL)和 DNS 状态;省略的 DNS 配置意味着禁用且域列表为空。
SyncRequest 是权威性的协调点:每次(重)连接时,它是流上的第一个事件,并列出守护进程当前完整的附件集合。对照它协调你的视图 — 添加你不知道的附件,删除列表中不存在的附件。重连时,守护进程会清除排队到先前连接的事件(同步会取代它们),因此你不会看到过时的 Heartbeat、同步中已缺失附件的 Unsubscribed,也不会重放已断开连接中的 CommandResult。设计上留下两个边缘情况,你的控制平面必须幂等地处理它们:
SyncRequest 之后,可能会跟随一个 Subscribed。这发生在新附件的确认在重连期间待处理时,并且对于每个已恢复附件,直到一次权威确认完全应用之前,会故意这样做。将其视为更新,回复一个完整的新 SubscribedAck,切勿将其作为重复丢弃。SyncRequest 协调附件清单;SubscribedAck 协调期望策略。Unsubscribed 视为无操作。AllowCIDR/DenyCIDR 命令,以及 SubscribedAck/BulkUpdate 中的 CIDR 列表)带有可选的 TTL。带有 TTL 的规则会在过期后被守护进程的清理程序移除(扫描间隔 ttl_janitor_interval,默认 1s);没有 TTL 的规则是永久的。AllowCIDR/DenyCIDR 重新添加会将 CIDR 延长到较晚的截止时间 — 它们绝不会缩短 — 而增量式重新添加不带 TTL 会使规则变为永久。相反,SubscribedAck/BulkUpdate 中的完整状态会精确替换每个控制平面生命周期,因此权威协调可以缩短 TTL 或将永久改为有限,而无需移除/重新添加实时映射条目。使用 RemoveCIDR 可以提前删除增量规则。dns.min_filter_ttl(默认 60s;零/未设置表示默认值,而非“无下限”)。因此上游 TTL 为零时,IP 会存活下限时长;缺失的 PROXY TTL 在下限应用前明确默认为 300s。当精确 DNS 所有权到期时,覆盖同一地址的永久或更长生命周期的 CIDR 规则仍独立安装在受保护的 LPM 层级中。filter.max_rule_entries 确定大小(默认每个 4096);参见上面的“受保护 CIDR 容量与故障关闭恢复”。DNS 派生的主机地址使用单独的精确匹配 HASH 映射,由 filter.max_dns_rule_entries 确定大小(默认每个 IP 族 4096),因此它们无法消耗或驱逐权威或拒绝容量。完整的 DNS 响应准入会在变异前验证/规范化每个地址,并预先检查物理和逻辑容量。DNS 精确映射内核错误会恢复其精确的快照前状态;如果无法证明该回滚成功,解析器会抑制应答并将附件隔离到持久的 IP ,然后才接受下一个变异。| 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 |
eth0egress(默认) |
| 工作负载的出站数据包通过它向外传输;其目标地址是真正目的地。 |
主机端 veth 对端或 VM 的 tap 接口(例如 fcr-*) | ingress | 工作负载的出站数据包到达主机在该接口上。此处的 egress 会看到主机→工作负载的返回流量,并根据工作负载自身的地址而不是真正目的地进行过滤。 |
CommandResultUnsubscribedHeartbeatpolicy_degradedmap_full_dropscommand_idBulkUpdateUnsubscribedBLOCK_ALL