raw socket 实现简单 tcp 协议栈

raw socket 实现tcp三次握手

最近由于项目需要对四层以下的协议添加一些支持,因此就想到了自定义TCP协议。

需要使用的方式就是raw socket,此处我们使用python 语言。

测试客户端

此处我们先使用一个简单的c语言的tcp服务器作为被测试目标。

这个服务器的功能就是简单的 [接受数据] ---> [返回数据]

// tcpserv.c
#include <netinet/in.h>    // for sockaddr_in
#include <sys/types.h>    // for socket
#include <sys/socket.h>    // for socket
#include <stdio.h>        // for printf
#include <stdlib.h>        // for exit
#include <string.h>        // for bzero
#include <arpa/inet.h>     // inet_ntoa

#define LISTENQ 20
#define MAXLINE 10000
int SERV_PORT=6666;

void
str_echo(int sockfd)
{
	long		arg1, arg2;
	ssize_t		n;
	char		line[MAXLINE];

	for ( ; ; ) {
		if ( (n = recv(sockfd, line, MAXLINE, 0)) == 0)
			return;		/* connection closed by other end */
		n = strlen(line);
        printf("len: %d \n", n);
		printf("data: %s", line);
		send(sockfd, line, n, 0);
		memset(line, 0, MAXLINE);
	}
}

int
main(int argc, char **argv)
{
	int					listenfd, connfd;
	pid_t				childpid;
	socklen_t			clilen;
	struct sockaddr_in	cliaddr, servaddr;
  
  if (argc > 2){
    printf("usage: tcpserv [port]\n");
    exit(0);
  }
  if (argc == 2){
    printf("listening on port:%s\n", argv[1]);
    SERV_PORT = atoi(argv[1]);
  }

	listenfd = socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);

	bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr));

	listen(listenfd, LISTENQ);

	for ( ; ; ) {
		clilen = sizeof(cliaddr);
		connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &clilen);
		printf("connected from:%s, port:%d \n\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
		if ( (childpid = fork()) == 0) {	/* child process */
			close(listenfd);	/* close listening socket */
			str_echo(connfd);	/* process the request */
			exit(0);
		}
		close(connfd);			/* parent closes connected socket */
	}
	return 0;
}

运行

gcc tcpserv.c -o tcpserv
./tcpserv 6666              #监听6666端口

编写 raw socket 客户端

一些问题

此处列举了在编写代码中遇到的问题,具体代码见下一节

1. 如何创建套接字

因为我们需要自定义连接,所以我们只需要从 TCP header 开始构建就可以了(当然也可以从 IP header 开始构建,但是那样并没有什么特殊的意义,并且会增加一些工作量),构建的方式就是

s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)

2. 创建了套接字之后如何发送数据

就直接通过 pack 函数一个字节一个字节地打包数据就可以了,注意使用网络字节序

3. 如何接受数据

我们创建的套接字,无论是TCP还是UDP,都能够通过 recvfrom函数接收数据包。

它所接收的数据包是本机收到的所有 TCP数据包/UDP数据包 的一份拷贝

意思就是说开多个进程可以同时接受所有的数据包,并且需要注意的一点事,就算是我们当前进程发送的数据包也是会被接受到的,因此最后进行 wireshark 抓包的时候需要进行一些筛选,否则显示的数据量太大了

4. 如何处理内核发出的 RST 包

在发包过程中我们会发现,每当我们发出一个自定义的 SYN 包之后,接收到对端发挥的 SYN+ACK,这个时候内核就会发送一个 RST 包强行中断这个连接,以为内核需要管理所有的TCP连接,一旦受到未知连接的TCP包就会强行中断这个连接。为了解决这个问题有两种思路:

思路一:创建一个伪装TCP连接,也就是先通过 SOCK_STREAM 创建一个真正的套接字并且使用 connect 函数建立连接,然后使用 raw socket 在这个连接的端口上进行收发包。这种方式的问题在于,因为我们使用的是系统的自带的协议栈建立的连接,因此就没法获取到连接建立时所商定的 ISN(初始序列号),因此后续发包过程中就不能正确发包。

思路二:参照 stackoverflow 上的一个大佬的回答,通过修改 iptables 的 filter 表将内核发出的 RST 包给过滤掉,这样就能建立连接了。

因为我们的服务器监听的是6666端口,因此修改语句就是:

iptables -t filter -I OUTPUT -p tcp --dport 6666 --tcp-flags RST RST -j DROP

客户端代码

# -*- coding: utf-8 -*
import sys, socket
from struct import *
import str2asc


def carry_around_add(a, b):
    c = a + b
    return (c & 0xffff) + (c >> 16)

def checksum(msg):
    s = 0
    for i in range(0, len(msg), 2):
        w = (ord(msg[i]) << 8 ) + ord(msg[i+1])
        s = carry_around_add(s, w)
    return ~s & 0xffff


def raw_tcp(s, src_ip, src_port, dst_ip, dst_port, seq, ack_seq, syn_flag=0, ack_flag=1, psh_flag=0
            , payload_data=''):

    ip_source = src_ip
    ip_dest = dst_ip
    ip_protocol = socket.IPPROTO_TCP

    #  tcp header
    tcp_sport = src_port	        # source port
    tcp_dport = dst_port		# destination port
    tcp_seq = seq	    # 32-bit sequence number,这里随便指定个
    tcp_ack_seq = ack_seq		    # 32-bit ACK number。这里不准备构建ack包,故设为0
    tcp_data_offset = 5	    # 和ip header一样,没option field
    # 下面是各种tcp flags
    tcp_flag_urg = 0
    tcp_flag_ack = ack_flag
    tcp_flag_psh = psh_flag
    tcp_flag_rst = 0
    tcp_flag_syn = syn_flag
    tcp_flag_fin = 0

    tcp_window_size = 3000
    tcp_checksum = 0
    tcp_urgent_ptr = 0

    # 继续合并small fields
    tcp_offset_reserv = (tcp_data_offset << 4)
    tcp_flags = tcp_flag_fin + (tcp_flag_syn << 1) + (tcp_flag_rst << 2) + (tcp_flag_psh <<3) + (tcp_flag_ack << 4) + (tcp_flag_urg << 5)

    # 按上面描述的结构,构建tcp header。
    tcp_header = pack('!HHLLBBHHH' , tcp_sport, tcp_dport, tcp_seq, tcp_ack_seq, tcp_offset_reserv, tcp_flags, tcp_window_size, tcp_checksum, tcp_urgent_ptr)

    # 写点东西作为data部分(可选)
    # payload_data = 'wordpress.youran.me'

    # 构建pseudo ip header
    psh_saddr = socket.inet_pton(socket.AF_INET, ip_source)
    psh_daddr = socket.inet_pton(socket.AF_INET, ip_dest)
    psh_reserved = 0
    psh_protocol = ip_protocol
    psh_tcp_len = len(tcp_header) + len(payload_data)
    psh = pack('!4s4sBBH', psh_saddr, psh_daddr, psh_reserved, psh_protocol, psh_tcp_len)

    # 创建最终用于checksum的内容
    chk = psh + tcp_header + payload_data

    # 必要时追加1字节的padding
    if len(chk) % 2 != 0:
        chk += '\0'

    tcp_checksum = checksum(chk)

    # 重新构建tcp header,把checksum结果填进去
    tcp_header = pack('!HHLLBBHHH' , tcp_sport, tcp_dport, tcp_seq, tcp_ack_seq, tcp_offset_reserv, tcp_flags, tcp_window_size, tcp_checksum, tcp_urgent_ptr)

    # 最终的tcp/ip packet
    packet = tcp_header + payload_data
    # 发送出去
    s.sendto(packet, (ip_dest, 0))

if __name__ == '__main__':
    src_ip = '127.0.0.1'
    src_port = 1017
    dst_ip = '127.0.0.1'
    dst_port = 6666

    # fake_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
    # fake_sock.bind((src_ip, src_port))
    # fake_sock.connect((dst_ip, dst_port))
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
        # s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
    except socket.error , msg:
        print 'Socket could not be created. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
        sys.exit()

    seqNum = 19890602 #随便初始化的一个值
    ackNum = 0
    # syn
    raw_tcp(s, src_ip, src_port, dst_ip, dst_port, seq=seqNum, ack_seq=ackNum, syn_flag=1, ack_flag=0)

    for i in range(2):
        data, addr = s.recvfrom(1024)
        print addr
        print data
        print 'len: ', len(data)
        print 'seq: ', unpack('!I', data[24:28])
        ackNum = unpack('!I', data[24:28])[0]+1
        print 'ack: ', unpack('!I', data[28:32])
        str2asc.printBinary(data)

    # ack
    raw_tcp(s, src_ip, src_port, dst_ip, dst_port, seq=seqNum+1, ack_seq=ackNum)
    # psh + payload
    payload = "hello raw"
    raw_tcp(s, src_ip, src_port, dst_ip, dst_port, seq=seqNum+1, ack_seq=ackNum, psh_flag=1, payload_data='hello raw')
    for i in range(4):
        data, addr = s.recvfrom(1024)

    # 对于服务器返回值的ack
    raw_tcp(s, src_ip, src_port, dst_ip, dst_port, seq=seqNum+1+len(payload), ack_seq=ackNum+len(payload))

运行结果

服务器显示结果,此处的port对应代码中的 src_port

抓包结果:

可以看到成功建立了连接并且受到了服务端(6666端口的)返回数据。

但是很奇怪的就是虽然受到了服务器的返回值,但是在服务器那边的terminal中并没有显示接收到的数据,仅仅显示建立了连接。

总结

这个小程序虽说没有什么使用意义,但是在编写的过程中增进了我对于socket通信的理解,也算是实现了一个简单的tcp协议栈吧(当然没有考虑各种其他flag、窗口、option等等......)。如果有其他问题欢迎指出来。

代码地址

posted @ 2018-08-20 17:11  四度  阅读(3502)  评论(0编辑  收藏  举报