IPV6_MTU_DISCOVER 选项不探测是否支持, 强制使用 IPV6_DONTFRAG 选项,该选项目前 FreeBSD 和 Linux 都支持。
编译过程
SSL 库
此处以 OpenSSL quic 为例,可以参考以下方式编译
cd /data/
wget https://github.com/quictls/openssl/archive/refs/tags/OpenSSL_1_1_1v-quic1.tar.gz
tar xzvf OpenSSL_1_1_1v-quic1.tar.gz
cd /data/openssl-OpenSSL_1_1_1v-quic1/
./config enable-tls1_3 no-shared --prefix=/usr/local/OpenSSL_1_1_1v-quic1
make
make install_sw
【注意:】准确的说目前运营商的递归 DNS、南京信风公共 DNS(114.114.114.114)和阿里云公共 DNS(223.5.5.5)等使用过期缓存应答行为与 RFC8764 中乐观 DNS 的行为是冲突的,并不是真正的乐观 DNS 缓存,但此处也先以乐观 DNS 称呼。
RFC8767 中的对乐观 DNS 行为的是只有权威 DNS 解析失败时, 比如DDoS 攻击、网络等其他原因导致的超时、REFUSE等错误时才向客户端应答过期的缓存记录数据,而目前部分国内支持乐观 DNS 行为的递归 DNS 则不关注权威应答成功或失败,直接给客户端应答缓存中的过期记录,再去异步向权威查询。
使用过期缓存应答的客户端查询超时时间在标准 RFC 中建议为 1.8 秒,但从个人的经验看,这个值在现网环境中明显有点高了,可以考虑酌情降低,个人建议低于 400 ms
最大过期时间(A maximum stale timer)即 TTL 过期多长时间内可以继续应答过期的记录,RFC 的建议是 1 – 3 天,而目前部分国内递归 DNS 在实践中该时间是无限的。
乐观 DNS 的优点
乐观 DNS 对终端用户的使用体验以及递归 DNS 服务提供商有一些好处,主要包括以下方面:
在缓存的记录 TTL 过期后降低客户端的 DNS 解析时延,尤其是请求量不是很大的冷域名没有了冷启动过程,因为省却了同步的向各级权威 DNS (包括根 DNS, TLD DNS, zone 域名 DNS等 ) 递归(迭代) DNS 查询的过程。
此处主要是部分国内非标准实现的乐观 DNS 的优点。
运营商递归 DNS 或云厂商等公共 DNS 中普遍使用缓存和递归分离的多级架构中,此方式工程实现上更为简单,缓存层可以无需保存递归过程中的客户端连接状态信息。
当权威 DNS (包括根 DNS, TLD DNS, zone 域名 DNS等 ) 遭受 DDoS 攻击或其他故障无法响应递归 DNS 的查询时,不影响客户端获取请求域名的记录,虽然 TTL 已经过期,但是比解析失败更好一些(”stale bread is better than no bread.”)。
此处可以减少递归 DNS 提供商的收到的无法解析等情况问题反馈、咨询工单等。
降低递归 DNS 的成本,包括网络带宽和计算资源成本等,权威失败时仅间隔性的重试到权威的请求。
乐观 DNS 的缺点
如果按照标准的 RFC8767 去实现乐观 DNS,这里其实并没有什么显著的缺点,因为只有在权威 DNS 不可用或不稳定时才会触发该功能,虽然可能会有一些解析错误的情况出现,但至少不会使情况变得更糟。
乐观 DNS 更多的缺点来自目前部分递归 DNS 的非标实现,主要包括以下方面:
过期的解析记录在递归 DNS 缓存中可能长期(数小时、数天、甚至数月)无法刷新,还可能解析到旧的记录,尤其是对平时解析量不大的冷域名,此时过期的记录很可能早已无法访问。
尤其是最大过期时间(A maximum stale timer)设置为无限时,而不是标准建议的 1-3 天或自行稍微斟酌修改的时间。
此处会增加递归 DNS 提供商的收到的解析错误等情况问题反馈、咨询工单等,具体最终工单量增加还是减少需视客户业务和网络等情况单独去看。
对需要频繁切换记录的域名不友好,如全局负载均衡或 CDN 调度等。
如果域名的 DNS 查询请求量足够大,则可以减轻该影响。
个人观点
基本支持标准 RFC 的乐观 DNS 行为。
强烈反对部分递归 DNS 的乐观 DNS 的非标实现,见之前乐观 DNS 介绍中的注意事项,降低解析时延不能以解析到错误甚至不可用的 IP 为代价。
使用开源软件等自建自用递归 DNS 的场景,根据个人需求自行设置缓存规则即可。
国内乐观 DNS 测试
测试范围及方法
测试范围:境内 31 个省份三大运营商(电信、联通、移动)共 93 个测试目标,及部分公共 DNS (119.29.29.29,114.114.114.114,223.5.5.5)共 3 个测试目标。
测试的大体步骤如下所示,该测试过程会重复测试多次:
步骤1,首先设置测试域名opt.test.example.com的 A 记录为1.1.1.1,并设置 TTL 为 600 秒。
此处的测试域名和测试 IP 都非真实的测试域名和测试 IP,仅为说明测试过程。
步骤2, 通过遍布全国的测试客户端(IDC 及 LastMile)请求测试域名,其中包括直接指定目标递归 DNS 进行解析(IDC)和 http 请求中包含的 DNS 解析(LastMile)。
本过程的目的使客户端使用的递归 DNS 都解析并缓存过opt.test.example.com的 A 记录为1.1.1.1
步骤3,等待步骤2的全部测试全部结束后,修改测试域名opt.test.example.com的 A 记录为2.2.2.2,并设置 TTL 为 600 秒。
此处绝大部分测试客户端已经排除了客户端本地 DNS 缓存的影响,尽量直接向本地设置的递归 DNS 进行请求。
步骤6,过滤掉干扰 DNS(如 LastMile 的路由器的内网 IP段等),仅保留本地运营商实际的递归 DNS 和目标公共 DNS 的数据记录进行统计,如果仍然存在会解析到已经过期的旧 IP 1.1.1.1,则认为该递归 DNS 开启了乐观 DNS,如果所有记录都为新的 IP2.2.2.2,则认为该递归 DNS 未开启乐观 DNS。
为减少偶然情况的影响,以上测试步骤会执行多次。
测试数据
从目前目前国内的主流的递归 DNS 测试数据看,过半数(约60%左右)已经开启了乐观 DNS,本文仅列出测试数据,不详细点评。
如果业务域名的请求量较大,可以较快的触发递归 DNS 快速刷新到新的记录,那么乐观 DNS 可以微弱的降低解析时延,目前没有看到太明显的缺点,正常使用各递归 DNS 即可。
请求量大的热域名,则缓存命中率高,总体时延降低有限。
然而在很多实际的业务中,对切换实时性要求比较高,但是访问量又没有那么大的中小域名,目前如此高比例非标准实现的乐观 DNS肯定会对业务存在一定的影响,此时就需要一些其他技术手段来对乐观 DNS 进行规避,下面介绍一些方法,其中部分方法比较常见,外网已经有很多的公开信息可以查询参考,此处就不展开详细介绍。
提前修改解析记录/保留旧IP一段时间
如果是有规划的切换 IP 时,也可以考虑预留几天(如3天)的缓冲期,这段时间内新旧 IP 都保持可用,来降低 LocalDNS 会返回过期 IP 造成的影响。
类似于修改域名的 NS,需要留几天的缓冲期保持新旧NS的记录都可用并同步更新
不适用于临时或频繁的切换 IP
即使保留 3 天的缓存,也不能保证完全刷新掉旧的IP
感谢 @changlinli 提供
可以使用未开启乐观 DNS 的公共 DNS 来规避乐观 DNS,目前国内腾讯云 DNSPod 的公共 DNS 等是未开启乐观 DNS 可供选用。不同的使用方式有不同的接入方法,更多信息可以参考这里。
使用未开启乐观 DNS 的 公共DNS
通用的公共 DNS
需要修改 WIFI/路由器 等使用的 DNS IP,有一定使用门槛,且普通 DNS 请求容易被劫持。
DoH/DoT(DoQ)
需要较高的 PC 操作系统版本/浏览器/手机 OS(主流IOS/Android均支持)等支持,并进行配置,也有一定的使用门槛。
cd /data/f-stack/adapter/sysctall make clean;make all ls -lrt fstack libff_syscall.so helloworld_stack helloworld_stack_thread_socket helloworld_stack_epoll helloworld_stack_epoll_thread_socket helloworld_stack_epoll_kernel
export FF_PATH=/data/f-stack export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/local/lib64/pkgconfig:/usr/lib/pkgconfig cd /data/f-stack/adapter/sysctall export FF_KERNEL_EVENT=1 export FF_MULTI_SC=1 make clean;make all
ip link add link eth0 name eth0.10 type vlan id 10 ifconfig eth0.10 10.10.10.10 netmask 255.255.255.0 broadcast 10.10.10.255 up route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth0.10 ip link add link eth0 name eth0.20 type vlan id 20 ifconfig eth0.20 10.10.20.20 netmask 255.255.255.0 broadcast 10.10.20.255 up route add -net 10.10.20.0/24 gw 10.10.20.1 dev eth0.20 ip link add link eth0 name eth0.30 type vlan id 30 ifconfig eth0.30 10.10.30.30 netmask 255.255.255.0 broadcast 10.10.30.255 up route add -net 10.10.30.0/24 gw 10.10.30.1 dev eth0.30 # 设置各 vlan 的外网 VIP ifconfig lo.0 110.110.110.110 netmask 255.255.255.255 ifconfig lo.1 120.120.120.120 netmask 255.255.255.255 ifconfig lo.2 130.130.130.130 netmask 255.255.255.255
设置策略路由
echo "10 t0" >> /etc/iproute2/rt_tables echo "20 t1" >> /etc/iproute2/rt_tables echo "30 t2" >> /etc/iproute2/rt_tables # 设置各 vlan 的路由表, 并设置对应的网关地址 ip route add default dev eth0.10 via 10.10.10.1 table 10 ip route add default dev eth0.20 via 10.10.20.1 table 20 ip route add default dev eth0.30 via 10.10.30.1 table 30 #systemctl restart network # 从对应 VIP 出去的包使用对应 vlan 的路由表 ip rule add from 110.110.110.0/24 table 10 ip rule add from 120.120.120.0/24 table 20 ip rule add from 130.130.130.0/24 table 30
curl --http3 -vvv https://xxxx.cn/
* Trying x.x.x.x:443...
* Connect socket 5 over QUIC to x.x.x.x:443
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
* CApath: none
* subjectAltName does not match xxxx.cn
* SSL: no alternative certificate subject name matches target host name 'xxxx.cn'
* connect to x.x.x.x port 443 failed: SSL peer certificate or SSH remote key was not OK
* Failed to connect to xxxx.cn port 443 after 102 ms: SSL peer certificate or SSH remote key was not OK
* Closing connection 0
curl: (60) SSL: no alternative certificate subject name matches target host name 'xxxx.cn'
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
提供接口ff_zc_mbuf_get(),用于应用提前申请包含可以由内核直接使用的mbuf的结构体作为应用层数据缓存,接口声明如下。int ff_zc_mbuf_get(struct ff_zc_mbuf *m, int len);该接口输入struct ff_zc_mbuf *指针和需要申请的缓存总长度,内部将通过m_getm2()分配mbuf链,首地址保存在ff_zc_mbuf结构的bsd_mbuf变量中,后续可以传递给ff_write()接口。其中m_getm2()为标准socket接口拷贝应用层数据到协议栈时分配mbuf链的接口,所以使用该接口范围的mbuf链作为应用层缓存,可以在发送数据时完全兼容。
提供了缓存数据写入函数ff_zc_mbuf_write(),函数声明如下,
int ff_zc_mbuf_write(struct ff_zc_mbuf *m, const char *data, int len); 应用层在保存待发送的数据时,应通过接口ff_zc_mbuf_wirte()直接将数据写到ff_zc_mbuf指向的mbuf链的缓存中,ff_zc_mbuf_wirte()接口可以多次调用写入缓存数据,接口内部自动处理缓存的偏移情况,但多次总的写入长度不能超过初始申请的缓存长度。
应用调用ff_write()接口时指定传递ff_zc_mubf.bsd_mbuf为buf参数,示例如下所示,ff_write(clientfd, zc_buf.bsd_mbuf, buf_len);在m_uiotombuf()函数中,直接使用传递的mbuf链的首地址,不再额外进行mbuf链的分配和数据拷贝,如下所示,#ifdef FSTACK_ZC_SEND if (uio->uio_segflg == UIO_SYSSPACE && uio->uio_rw == UIO_WRITE) { m = (struct mbuf *)uio->uio_iov->iov_base; /* 直接使用应用层的mbuf链首地址 */ uio->uio_iov->iov_base = (char *)(uio->uio_iov->iov_base) + total; uio->uio_iov->iov_len = 0; uio->uio_resid = 0; uio->uio_offset = total; progress = total; } else { #endif m = m_getm2(NULL, max(total + align, 1), how, MT_DATA, flags); /* 拷贝模式分配mbuf链*/ if (m == NULL) return (NULL); m->m_data += align; /* Fill all mbufs with uio data and update header information. */ for (mb = m; mb != NULL; mb = mb->m_next) { length = min(M_TRAILINGSPACE(mb), total – progress); error = uiomove(mtod(mb, void *), length, uio); /* 拷贝模式拷贝应用层数据到协议栈 */ if (error) { m_freem(m); return (NULL); } mb->m_len = length; progress += length; if (flags & M_PKTHDR) m->m_pkthdr.len += length; } #ifdef FSTACK_ZC_SEND } #endif