Fake TCP tunneling with scapy to improve transmission performance

Does TCP have to be TCP?It might be a trick!

It's raining so hard that you can write a joke.

I wrote two essays about this article the other day:

It is very complex to want packets to be transmitted and processed as TCP.

However, TCP is an end-to-end stateful protocol, which means that intermediate forwarding devices cannot handle the details of TCP. If the end system does not need to handle the details of TCP, a stream just needs to make the intermediate forwarding devices look like TCP!

When does the end system not need to handle TCP details, and why should a stream that is not TCP look like TCP?

We need to understand some behavior of intermediate forwarding devices.Intermediate forwarding devices take actions against TCP, such as:

  • Packet dropout limits for protocols other than TCP during peak traffic periods (excessive TCP packet dropouts can cause extreme reactions to blind CC algorithms on the end system and exacerbate network congestion).
  • Limit the speed of a single stream according to the TCP quaternion.
  • Single stream is reshaped according to TCP quaternion.
  • ...

These actions are based on the following facts:

  • Behavior eventually acts on end-to-end systems, which correctly handle TCP details and converge the network.

Instead of limiting, we can benefit from these actions by ignoring end-to-end processing:

  • Encapsulate a TCP header for UDP to avoid being speed-limited during peak times.
  • Encapsulate different quaternion TCP headers for the same stream without being speed-limited.
  • Encapsulates TCP headers of different quaternions for the same stream without being shaped.

This paper presents a POC that disguises any traffic as TCP traffic.I call this a dummy TCP tunnel.

I wanted to do it with Netfilter, but I was tired of writing kernel modules. I wanted to do it with eBPF, but I thought it was a bit of an uproar, so I did it with scapy+tun because it was simple!

Here is the dummy TCP tunnel python code (data-side only):

# dtun.py
from scapy.all import *
import socket
import fcntl

IFF_TUN = 0x0001
IFF_NO_PI = 0x1000
TUNSETIFF = 0x400454ca

src = ''
peer = ''
sport = 0
dport = 0
seq = 12345 # Should have been random, but for simplicity's sake
ack_seq = 12345 # Should have been random, but for simplicity's sake

# Receive dummy TCP tunnel encapsulated message from network, directly remove TCP header, send to tun network card
def net2tun(packet):
	global ack_seq, src, peer, sport, dport
	flags = packet[TCP].flags
	# Processing if branch of analog SYNACK
	if flags & 0x02 != 0 and flags & 0x10 == 0:
		ip = IP(src = src, dst = peer)
		# Receive SYN packet, get ack, reply directly to SYNACK
		tcp = ip/TCP(sport = sport, dport = dport, flags = "SA", seq = seq, ack = ack_seq)
		ack_seq = packet[TCP].seq
	# if branch handling normal communication
	elif flags & 0x02 == 0:
		os.write(tun.fileno(), str(packet[TCP].payload))
		ack_seq = packet[TCP].seq + len(packet[TCP].payload)

def recv():
	filter = "src " + peer + " and tcp and tcp port 12345"
	sniff(filter = filter,iface="enp0s8", prn = net2tun)

# Simulates a TCP handshake, which mainly negotiates serial numbers and allows intermediate state devices to initialize stream tables.
def handshake(src, dst):
	global seq, ack_seq, sport, dport
	ip = IP(src = src, dst = dst)
	tcp = ip/TCP(sport = sport, dport = dport, flags = "S", seq = seq)
	pkt = sr1(tcp, iface = "enp0s8")
	ack_seq = pkt[TCP].ack
	print pkt[TCP].seq
	print pkt[TCP].ack

# Send to network after encapsulating TCP headers directly 
def tun2net(src, dst, payload):
	global seq, ack_seq

	ip = IP(src = src, dst = dst)
	tcp = ip/TCP(sport = sport, dport = dport, flags = "A", seq = seq, ack = ack_seq)/payload
	seq += len(payload)

if __name__ == "__main__":
	global peer
	tunip = sys.argv[1]
	peer = sys.argv[2]
	src = sys.argv[3]
	syn = sys.argv[4]

	dport = int("12345")
	sport = int("12345")

	tun = open('/dev/net/tun', 'r+w')
	iface = "tun0"

	# Open and set tun device
	ifr = struct.pack('16sH', iface, IFF_TUN|IFF_NO_PI)
	fcntl.ioctl(tun, TUNSETIFF, ifr)
	ip = socket.inet_aton(tunip)
	sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0);
	ifr = struct.pack('16sH2s4s8s', iface, socket.AF_INET, '\x00'*2, ip, '\x00'*8)
	fcntl.ioctl(sockfd, 0x8916, ifr)
	ifr = struct.pack('16sH2s4s8s', iface, socket.AF_INET, '\x00'*2, socket.inet_aton(""), '\x00' * 8)
	fcntl.ioctl(sockfd, 0x891C, ifr)
	ifr = struct.pack('16sH', iface, IFF_UP)
	fcntl.ioctl(sockfd, SIOCSIFFLAGS, ifr)

		threading.Thread(target = recv).start()
		if syn == "1":
			handshake(src, peer)
		# Receives bare packets from tun devices, encapsulates TCP headers, and sends directly to dummy TCP tunnels
		while True:
			packet = os.read(tun.fileno(), 2048)
			tun2net(src, peer, packet)

	except KeyboardInterrupt:

See the effect.

Prepare two virtual machines, hostA and hostB, connect directly, and configure as follows:

# hostA
# hostB

The following scripts are launched on hostA and hostB respectively:

# hostB (Start B first because it waits to receive SYN)
root@zhaoya-VirtualBox:/home/zhaoya/tun# ./dtun.py 0
# hostA
[root@localhost tun]# ./dtun.py 1

The tunnels for both machines are now set up. Since there is no real TCP connection, two additional filtering rules need to be added on hostA and hostB to avoid end-to-end RST:

iptables -t mangle -A POSTROUTING -p tcp -m tcp --tcp-flags RST RST -j DROP

OK, now you can experiment.

Address of tun network card pinging hostB on hostA:

[root@localhost ~]# ping -c 2
PING ( 56(84) bytes of data.
64 bytes from icmp_seq=1 ttl=64 time=42.1 ms
64 bytes from icmp_seq=2 ttl=64 time=50.2 ms

--- ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 42.196/46.243/50.290/4.047 ms

tcpdump captures the following:

[root@localhost tun]# tcpdump -i enp0s8 tcp port 12345 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s8, link-type EN10MB (Ethernet), capture size 262144 bytes
# Looks like a TCP handshake package
06:24:40.483759 IP > Flags [S], seq 12345, win 8192, length 0
06:24:40.522107 IP > Flags [S.], seq 12889, ack 12345, win 8192, length 0
# Oh, the 3rd ACK is missing, well, it needs to be improved...
06:24:46.525279 IP > Flags [.], seq 0:48, ack 1, win 8192, length 48
06:24:46.989491 IP > Flags [.], seq 1:85, ack 48, win 8192, length 84
06:24:47.027375 IP > Flags [.], seq 48:132, ack 85, win 8192, length 84
06:24:47.989780 IP > Flags [.], seq 85:169, ack 132, win 8192, length 84
06:24:48.034897 IP > Flags [.], seq 132:216, ack 169, win 8192, length 84

TCP streaming, as it is...

In fact, we have not established any TCP connection, which is false.

When an intermediate device sees such a packet, it thinks it comes from a real TCP connection. When it wants to restrict the flow, it performs queuing, dropping packets, and so on, in order to expect end-to-end TCP cc algorithms to sense this repression by measuring RTT, detecting packet dropouts, and so on, and make gentle concessions.However, there is no end-to-end TCP processing at all and they are all installed.

Now see what you can do further.

If I set up 10 such dummy TCP tunnels with 10 different source addresses, then the transit package of the tunnel only needs to pick one of the 10 tunnels at a time.In theory, this scheme can increase throughput 10 times if the intermediate device limits TCP single-stream speed!

Well, you might say that an intermediate device can limit the speed of an IP segment. That's not impossible either. As long as one end of the tunnel has a loudspeaker opening, you can use that end to simulate a SYN to build multiple tunnels:

Many years ago, when I played OpenU++Q-N, I kept shouting that TCP tunnels would cause connection crashes, and I kept shouting that UDP tunnels should be used. Well, at that time, I should actually play like this. I didn't think of these techniques, perhaps because the financial network is a quasi-internal network, all aspects of QoS are guaranteed, and there are not too many restrictions on UDP, ifAt that time, the production environment was switched to the public Internet, and trick was probably already on the line by me.

Left and right hands fight, one foot tall, the Devil is very tall, hit his face, then grab the manager's lead and push him into the ditch.

Zhejiang Wenzhou leather shoes are wet, so you won't get fat when it rains.

Tags: socket network Python VirtualBox

Posted on Mon, 29 Jun 2020 20:22:34 -0400 by albynas