DPVS SNAT实现
DPVS的数据流量从以流入流出划分为:inbond outbond;
- outbond:指数据包从内网服务器 -> DPVS -> 外网服务器(如 qq.com )
- inbond:指数据包从外网服务器(如qq.com) -> DPVS -> 内网服务器
outbond 方向数据包处理流程
内网服务器需要访问外网某个功能接口,请求数据包到达了 DPVS 服务器。数据包从网卡某个队列 queuex 进入后,被 cpux 接收并开始相关的逻辑处理。收包路径大概包括如下:
- netif_loop -> lcore_job_recv_fwd -> lcore_process_packets -> netif_deliver_mbuf
- netif_deliver_mbuf 函数中根据 packet_type 分别调用相应的 func 函数,func 类型主要包括:ipv4、ipv6、arp 三种类型。这里主要以 IPv4 分析 SNAT 整个过程。
- IPv4 类型的包处理函数是 ipv4_rcv,经过 IP 层相关检查和校验处理后,调用 INET_HOOK 来执行相关的钩子函数,这里调用的是 INET_HOOK_PRE_ROUTING 位置钩子,包括:dp_vs_pre_routing & dp_vs_in 函数,核心代码入口在 dp_vs_in 里边
在 dp_vs_in
中其实调用的是 __dp_vs_in
函数,主逻辑入口在这个函数中体现:
- 首先调用协议查找函数 dp_vs_proto_lookup,确认此数据包是哪种协议类型,如 TCP 或 UDP 等,每种协议的相关处理逻辑不同,后续的相关操作会根据对应协议注册的回调函数来执行。
- 假设本次是请求是首包,需要调用 proto 的 conn_sched 函数进行连接的初始化和调度,TCP 的连接调度函数是 tcp_conn_sched。
- 调度时,只有 TCP syn 包才能触发正常调度流程。首先,根据数据包的相关数据确认该包是否属于我们配置的服务,调用 dp_vs_service_lookup 函数进行查找,不过这里查找到的 service 类型是 match 方式。如果找不到响应的 svc 就跳出 SNAT 主流程。
- 找到 svc 后,调用 dp_vs_schedule 进行主调度流程,并创建初始化新的连接 Entry。
- 调用 svc->scheduler->schedule 函数,从 RS 列表中挑选出一个 RS 作为 dest
- 对于 SNAT 模式,调用 dp_vs_snat_schedule() 函数
- 此时已确认 dest 作为源 IP 地址,调用 sa_fetch 根据源地址选择相对应的源 port
- 调用 dp_vs_conn_fill_param 将相关数据赋值到 param 变量中去。
- 确定相关参数后,调用 dp_vs_conn_new 建立初始化新的连接条目。
- 申请 new conn 连接内存资源,调用 dp_vs_conn_alloc
- 初始化 inbond 和 outbond 连接表关联的 ntuple 哈希
- 初始化 new conn 的相关字段
- 为 conn 绑定对应的 dest,并设置对应的 xmit 函数
- 添加 new conn 到连接表中 dp_vs_conn_hash
- 初始化相关的计数器和定时器
- 到此为止,一个新的 SNAT 连接已经完成
- 连接调度成功后,根据模式,SNAT 初始化方向为 DPVS_CONN_DIR_OUTBOUND 不同于其他模式。
- 调用 proto 的 state_trans 函数,进行相关协议状态机的转换。
- 调用 xmit_outbound 将数据包发送出去。
- 调用相关接口,确认 output 路由,并修改三层和四层信息
- 确认下一跳等相关信息,调用 neigh_output 发出数据包
到此从 SNAT 数据包的接收、处理、转发等过程,完成 SNAT outbond 方向的整个流程。
inbond 方向数据包处理流程
内网服务器发送请求数据包后,外网服务器执行相关操作,发出响应数据包,此数据包到达了 DPVS 服务器。此时 DPDK 驱动程序会根据关键字段进行 FDIR 规则匹配,数据包从同样的网卡度列 queuex 进入后,被 cpux 接收并开始相关的逻辑处理,这样整个连接都是被同一个 cpux 来处理的。收包路径大概包括如下:
在 dp_vs_in 前的收包处理流程和 outbond 基本一致,这里就不分析了。
- 根据数据包字段,确认协议类型,调用 dp_vs_proto_lookup 确认本次是 TCP 协议
- 根据响应数据包的 4 元组来查找连接表(根据元组字段和 tuple hash 字段),正常情况能够找到对应的连接表。
- 进行协议的状态机转换
- 调用 xmit_inbound 将数据包发送出去,具体细节与 outbond 很类似。
不断的重复 outbond 和 inbond 流程,就能够通过多次数据包的交互完成连接过程中业务的传输,从而实现 SNAT 功能
存在问题:
- ICMP 处理问题。内网服务器 ping 外网是一个很常见的需求,从实验测试和代码分析来看,响应 ICMP 包可能命中到其它网卡队列,导致连接表 miss 的问题,这个问题可以通过 FDIR 功能将 ICMP 包都定位到固定的网卡 queue 上,这样可以简单的方式来解决这个问题。
NAT模式原理
对于inbound方向的流量,实际上做的是dnat,将目标ip由lb vip转换成真正的rs ip,此时后端rs是能拿到client ip的。outbond的流量做snat,将源地址换成lb vip
/* return verdict INET_XXX * af from mbuf->l3_type? No! The field is rewritten by netif and conflicts with * m.packet_type(an union), so using a wrapper to get af. * */ static int __dp_vs_in(void *priv, struct rte_mbuf *mbuf, const struct inet_hook_state *state, int af) { struct dp_vs_iphdr iph; struct dp_vs_proto *prot; struct dp_vs_conn *conn; int dir, verdict, err, related; bool drop = false; lcoreid_t cid, peer_cid; eth_type_t etype = mbuf->packet_type; /* FIXME: use other field ? */ assert(mbuf && state); //获取当前运行的lcore id cid = peer_cid = rte_lcore_id(); //数据包不是发往本机的,直接返回ACCEPT,之后执行ipv4_rcv_fin if (unlikely(etype != ETH_PKT_HOST)) return INET_ACCEPT; //填充内部dp_vs_iphdr,如果出错,主要是协议族不正确,直接返回ACCEPT if (dp_vs_fill_iphdr(af, mbuf, &iph) != EDPVS_OK) return INET_ACCEPT; //处理ICMP消息,类似于linux内核中icmp_error相关的处理逻辑 if (unlikely(iph.proto == IPPROTO_ICMP || iph.proto == IPPROTO_ICMPV6)) { /* handle related ICMP error to existing conn */ verdict = dp_vs_in_icmp(af, mbuf, &related); if (related || verdict != INET_ACCEPT) return verdict; /* let unrelated and valid ICMP goes down, * may implement ICMP fwd in the futher. */ } //查找四层处理协议,目前实现了tcp,udp和icmp prot = dp_vs_proto_lookup(iph.proto); if (unlikely(!prot)) return INET_ACCEPT; /* * Defrag ipvs-forwarding TCP/UDP is not supported for some reasons, * * - RSS/flow-director do not support TCP/UDP fragments, means it's * not able to direct frags to same lcore as original TCP/UDP packets. * - per-lcore conn table will miss if frags reachs wrong lcore. * * If we redirect frags to "correct" lcore, it may cause performance * issue. Also it need to understand RSS algorithm. Moreover, for the * case frags in same flow are not occur in same lcore, a global lock is * needed, which is not a good idea. */ //目前不支持ip分片,此处与flow director相关 if (af == AF_INET && ip4_is_frag(ip4_hdr(mbuf))) { RTE_LOG(DEBUG, IPVS, "%s: frag not support.\n", __func__); return INET_DROP; } //调用proto相关conn_lookup函数查找会话,tcp中为 tcp_conn_lookup 。有可能会 drop 掉。dir 是设置数据流方向,从client到LB, //还是从real server到LB,peer_cid为查找时决定处理该连接的lcore id /* packet belongs to existing connection ? */ conn = prot->conn_lookup(prot, &iph, mbuf, &dir, false, &drop, &peer_cid); if (unlikely(drop)) { RTE_LOG(DEBUG, IPVS, "%s: deny ip try to visit.\n", __func__); return INET_DROP; } /* * The connection is not locally found, however the redirect is found so * forward the packet to the remote redirect owner core. */ //如果不在当前lcore上处理,则恢复mbuf->data_off指向L2 header后转发给其他lcore处理,此处ring enqueue成功后返回INET_STOLEN, //否则返回DROP丢弃数据包 if (cid != peer_cid) { /* recover mbuf.data_off to outer Ether header */ rte_pktmbuf_prepend(mbuf, (uint16_t)sizeof(struct rte_ether_hdr)); return dp_vs_redirect_pkt(mbuf, peer_cid); } //对于新建的连接,肯定是没有会话的,conn_sched根据请求选择一个后端real server建立连接 if (unlikely(!conn)) { /* try schedule RS and create new connection */ //调用proto中conn_sched接口选择一个后端rs建立连接,如果创建连接失败,返回verdict if (prot->conn_sched(prot, &iph, mbuf, &conn, &verdict) != EDPVS_OK) { /* RTE_LOG(DEBUG, IPVS, "%s: fail to schedule.\n", __func__); */ return verdict; } //snat模式,则是内部服务器访问外部服务,内网服务器--->dpvs--->外网服务器(baidu), //所以设置dir=DPVS_CONN_DIR_OUTBOUND /* only SNAT triggers connection by inside-outside traffic. */ if (conn->dest->fwdmode == DPVS_FWD_MODE_SNAT) dir = DPVS_CONN_DIR_OUTBOUND; else//其余模式设置dir=DPVS_CONN_DIR_INBOUND dir = DPVS_CONN_DIR_INBOUND; } else { /* assert(conn->dest->svc != NULL); */ if (conn->dest && conn->dest->svc && prot->conn_expire_quiescent && (conn->dest->svc->flags & DPVS_CONN_F_EXPIRE_QUIESCENT)) { if (rte_atomic16_read(&conn->dest->weight) == 0) { RTE_LOG(INFO, IPVS, "%s: the conn is quiescent, expire it right now," " and drop the packet!\n", __func__); prot->conn_expire_quiescent(conn); dp_vs_conn_put(conn); return INET_DROP; } } } //特殊处理,syn_proxy if (conn->flags & DPVS_CONN_F_SYNPROXY) { if (dir == DPVS_CONN_DIR_INBOUND) { /* Filter out-in ack packet when cp is at SYN_SENT state. * Drop it if not a valid packet, store it otherwise */ if (0 == dp_vs_synproxy_filter_ack(mbuf, conn, prot, &iph, &verdict)) { dp_vs_stats_in(conn, mbuf); dp_vs_conn_put(conn); return verdict; } /* "Reuse" synproxy sessions. * "Reuse" means update syn_proxy_seq struct * and clean ack_mbuf etc. */ if (0 != dp_vs_synproxy_ctrl_conn_reuse) { if (0 == dp_vs_synproxy_reuse_conn(af, mbuf, conn, prot, &iph, &verdict)) { dp_vs_stats_in(conn, mbuf); dp_vs_conn_put(conn); return verdict; } } } else { /* Syn-proxy 3 logic: receive syn-ack from rs */ if (dp_vs_synproxy_synack_rcv(mbuf, conn, prot, iph.len, &verdict) == 0) { dp_vs_stats_out(conn, mbuf); dp_vs_conn_put(conn); return verdict; } } } //状态迁移,tcp中为tcp_state_trans if (prot->state_trans) { err = prot->state_trans(prot, conn, mbuf, dir); if (err != EDPVS_OK) RTE_LOG(WARNING, IPVS, "%s: fail to trans state.", __func__); } conn->old_state = conn->state; /* holding the conn, need a "put" later. */ //根据流量方向dir,来选择如何发送数据 if (dir == DPVS_CONN_DIR_INBOUND) return xmit_inbound(mbuf, prot, conn); else return xmit_outbound(mbuf, prot, conn); }
http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!!
但行好事 莫问前程
--身高体重180的胖子
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南