TCP 与 UDP 如何互通

Posted dog250

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP 与 UDP 如何互通相关的知识,希望对你有一定的参考价值。

今天再来个花式玩法。

TCP 连接的报文,结果却送到了 UDP socket,有趣…

既然以太帧既可以在铜线上传输,也可以在光纤上,甚至空气里传输,那么 SOCK_STREAM socket 也就可以在 UDP 上传输,反之,TCP 报文也可以被 SOCK_DGRAM 接收。

上周实现了 TCP 的不可靠传输:不可靠不重传的假 TCP。但还可以更花式,将 TCP 数据送到 SOCK_DGRAM socket 如何?很简单,协议转换一下而已。

先展示实验。

服务端不启动 iperf -s,反而启动 nc -u -l -p 12345.

客户端发起 iperf -c 192.168.1.248 -i 1 -p 12345 -t 5.

下面是服务端 nc -u -l -p 12345 的输出一角:

iperf -c 端的输出:

是不是很神奇?TCP 竟然可将报文送到 UDP。

实现并不难,关键是想到这种玩法。如此一来,socket 和传输协议真解耦:

  • 用 TCP 传输 socket(AF_INET, SOCK_DGRAM, 0)。
  • 用 UDP 传输 socket(AF_INET, SOCK_STREAM, 0)。
  • 一端为 socket(AF_INET, SOCK_STREAM, 0),另一端为 socket(AF_INET, SOCK_DGRAM, 0)。

由此引申出一种和传统封装型隧道不同的新隧道,协议转换型隧道。这类隧道解决了封装型隧道载荷率变低问题:vxlan 封装 TCP,wg 封装 vxlan,又是个 IPv6 环境,还能留给 payload 多少空间?

为保持原始 payload 的连接性(即五元组),协议转换型隧道需在隧道两端维护识别原始五元组的虚电路,比如当 tupleX 1.1.1.1:123 tcp 2.2.2.2:321 第一次通过隧道,隧道要建立一个虚电路,tupleX 便可脱去整个 inner TCP/IP 头,用 UDP 携带一个超精简仅携带 seq,ack,rwnd 等字段的小头重新封装通过隧道,在隧道对端由虚电路重组成 tupleX。

借 NAT64 的可行性,IPv4 也可用来做传输 IPv6 的隧道,IPv6 报文转成 IPv4 报文通过网络,从而提高载荷率。

MPLS 大致也是类似,但还是不同,没这么狠。

总之就是用一个相对短的协议封装 payload 通过可控的网络,这也是 overlay 的思路,只是着眼点不同:

  • 封装型 overlay 无状态,每包仅封装。
  • 转换型 overlay 有状态,事先建立虚电路,保存不变元数据。

为了不让协议头越封装越长,就要消耗点时间建立虚电路,这也是时间换空间。

回到最初,TCP 换 UDP 怎么做到的?代码如下:

// 又一个不可靠,不重传的实现,POC 只能单流玩,不能重入!
#include <linux/module.h>
#include <linux/netfilter_ipv4.h>
#include <net/tcp.h>

static int max_seq = 0;
static bool tcp_reply(struct tcphdr *tcph, const struct tcphdr *oth, uint16_t payload, bool *retrans)


	/* SYN > SYN-ACK */
	if (oth->syn && !oth->ack) 
		tcph->syn = true;
		tcph->ack = true;
		tcph->window = 65000;
		tcph->seq = htonl(prandom_u32() & ~oth->seq);
		tcph->ack_seq = htonl(ntohl(oth->seq) + oth->syn);
		max_seq = ntohl(oth->seq);
	
        // 忽略重传数据
	if (ntohl(oth->seq) < max_seq) 
		*retrans = true;
		return false;
	
	/* ACK > ACK */
        // 来了就 ACK,不重传
	if (oth->ack && (!(oth->fin || oth->syn))) 
		tcph->syn = false;
		tcph->ack = true;
		tcph->window = 65000;
		tcph->ack_seq = htonl(ntohl(oth->seq) + payload);
		tcph->seq = oth->ack_seq;
		max_seq = ntohl(oth->seq) + payload;
		return false;
	

	/* FIN > RST */
	else if (oth->fin) 
		tcph->window  = 0;
		tcph->seq = oth->ack_seq;
		tcph->ack_seq = oth->ack_seq;
		tcph->fin = false;
		tcph->ack = false;
		tcph->rst = true;
	
	return true;


static unsigned int ipv4_pseudotcp_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)

	struct iphdr *niph, ihdr, *iphu, *iph = ip_hdr(skb);
	struct tcphdr _otcph, *oth, thdr, *tcph;
	struct udphdr *udph;
	struct sk_buff *nskb;
	unsigned int delta = sizeof(struct tcphdr) - sizeof(struct udphdr);
	uint16_t tmp, payload;
	bool reply = false, retrans = false;

	if (iph->protocol != IPPROTO_TCP)
		goto out;
	if (skb->len < ip_hdrlen(skb) + sizeof(struct tcphdr))
		goto out;
	oth = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_otcph), &_otcph);
	if (oth == NULL)
		goto out;
	if (nf_ip_checksum(skb, NF_INET_LOCAL_IN, ip_hdrlen(skb), IPPROTO_TCP))
		goto out;
	nskb = skb_copy_expand(skb, LL_MAX_HEADER, skb_tailroom(skb), GFP_ATOMIC);
	if (nskb == NULL)
		goto out;

	nf_reset_ct(nskb);
	skb_init_secmark(nskb);
	skb_shinfo(nskb)->gso_size = 0;
	skb_shinfo(nskb)->gso_segs = 0;
	skb_shinfo(nskb)->gso_type = 0;
	ihdr = *iph;
	tcph = (struct tcphdr *)(skb_network_header(nskb) + ip_hdrlen(nskb));
	thdr = *tcph;
	if (htons(tcph->dest) != 12345)
		goto out;

	niph = ip_hdr(nskb);
	niph->daddr = xchg(&niph->saddr, niph->daddr);
	tmp = tcph->source;
	tcph->source = tcph->dest;
	tcph->dest = tmp;
	payload = nskb->len - ip_hdrlen(nskb) - sizeof(struct tcphdr);
	tcph->doff = sizeof(struct tcphdr) / 4;
	skb_trim(nskb, ip_hdrlen(nskb) + sizeof(struct tcphdr));
	niph->tot_len = htons(nskb->len);
	tcph->urg_ptr = 0;
	((u_int8_t *)tcph)[13] = 0;

	reply = tcp_reply(tcph, oth, payload, &retrans);
	if ((reply == false && payload == 0) || retrans)
		goto free_nskb;

	tcph->check = 0;
	tcph->check = tcp_v4_check(sizeof(struct tcphdr), niph->saddr, niph->daddr, csum_partial((char *)tcph, sizeof(struct tcphdr), 0));
	niph->frag_off = htons(IP_DF);
	niph->id = ~ihdr.id + 1;

	if (ip_route_me_harder(&init_net, nskb->sk, nskb, RTN_LOCAL))
		goto free_nskb;
	else
		niph = ip_hdr(nskb);
	nskb->ip_summed = CHECKSUM_NONE;
	niph->ttl = 64;
	niph->check = 0;
	niph->check = ip_fast_csum(skb_network_header(nskb), niph->ihl);
	nf_ct_attach(nskb, skb);

	NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT, &init_net, nskb->sk, nskb, NULL, skb_dst(nskb)->dev, dst_output);
	if (reply == true || payload == 0)
		goto drop;

	ihdr.protocol = IPPROTO_UDP;
	ihdr.tot_len = htons(ntohs(ihdr.tot_len) - delta);
	iphu = (struct iphdr *)skb_pull(skb, delta);
	*iphu = ihdr;
	skb_reset_network_header(skb);
	skb_set_transport_header(skb,  iphu->ihl*4);
	ip_send_check(iphu);
	udph = (struct udphdr *)skb_transport_header(skb);
	udph->source = thdr.source;
	udph->dest = thdr.dest;
	udph->len = htons(ntohs(iphu->tot_len) - sizeof(struct iphdr));
	udph->check = 0;
out:
	return NF_ACCEPT;
free_nskb:
	kfree_skb(nskb);
drop:
	return NF_DROP;


static const struct nf_hook_ops ipv4_pseudotcp_ops[] = 
	
		.hook = ipv4_pseudotcp_hook,
		.pf = NFPROTO_IPV4,
		.hooknum = NF_INET_LOCAL_IN,
		.priority = NF_IP_PRI_LAST,
	,
;

static int __init pseudotcp_init(void)

	return nf_register_net_hooks(&init_net, ipv4_pseudotcp_ops, ARRAY_SIZE(ipv4_pseudotcp_ops));


static void __exit pseudotcp_exit(void)

	nf_unregister_net_hooks(&init_net, ipv4_pseudotcp_ops, ARRAY_SIZE(ipv4_pseudotcp_ops));


module_init(pseudotcp_init);
module_exit(pseudotcp_exit);
MODULE_LICENSE("GPL");

这个算法依然不重传,完全是 UDP 的语义。既然接收端是 UDP,为什么大费周章用假 TCP 传输呢?
为了欺骗运营商呗。

还有一个意思,从此以后,TCP 发送端可以和 UDP 接收端对接,这对应用程序而言,意味着可分别改造即可完成适配,甚至故意这么玩,都可。

NAT64 可以将 IPv6 报文转换为 IPv4 报文,同样,TCP 报文也能转换成 UDP 报文。简单试试,有点意思。

浙江温州皮鞋湿,下雨进水不会胖。

以上是关于TCP 与 UDP 如何互通的主要内容,如果未能解决你的问题,请参考以下文章

Socket 通信原理(Android客户端和服务器以TCP&&UDP方式互通)

UDP 丢包的艺术

TCP与UDP,可靠UDP如何实现

网络协议趣谈UDP协议

网络协议趣谈UDP协议

UDP 协议格式及应用