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 报文。简单试试,有点意思。

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

  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值