今天再来个花式玩法。
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 报文。简单试试,有点意思。
浙江温州皮鞋湿,下雨进水不会胖。