linux 下 tcpdump 详解 后篇(自己实现抓包过滤)

2023-05-16

一 概述

在了解了tcpdump的原理后,你有没有想过自己去实现抓包过滤? 可能你脑子里有个大概的思路,但是知道了理论知识,其实并不能代表你完全的理解。只要运用后,你才知道哪些点需要注意,之前没有考虑到的。

二 如何实现抓包过滤

在写代码前,先捋下思路,和相应的理论知识。

libpcap 库中实现抓包关键代码

	sock_fd = cooked ?
	socket(PF_PACKET, SOCK_DGRAM, protocol) :
	socket(PF_PACKET, SOCK_RAW, protocol);

libpcap库中pcap_open_live 函数最终会调用上面这行代码,而创建的这socket就可以接收数据链路层的数据包。而protocol 可以指定数据链路层协议帧类型,例如IPv4帧,可以传入htons(ETH_P_IP),接收到数据链路层所有协议帧,可以传入htons(ETH_P_ALL)

  • socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))
    当指定SOCK_DGRAM时,获取的数据包是去掉了数据链路层的头(link-layer header)
  • socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
    当指定SOCK_RAW时,获取的数据包是一个完整的数据链路层数据包

已经可以抓数据链路层的数据包,但是如何设置过滤规则呢?

在libpcap 设置过滤规则用到了两个接口,pcap_compile()和 pcap_setfilter ()
函数,其中pcap_compile()主要将我们输入的过滤规则表达式编译成BPF代码,然后存入bpf_program的结构中。而pcap_setfilter 就是将过滤的规则注入内核。这里不关注pcap_compile 如何编译成bpf代码,然后存入bpf_program。 bpf深入的话,其实里面的东西还有很多(例如输入的过滤的规则怎么转发对应的bpf代码,应用层是如何注入bpf规则到内核,内核又是如何不必要重新编译代码,就可以怎么根据不同的socket上的bpf规则过滤数据包的,等等)。本人技术有限,只了解了部分。有兴趣可以看这篇博客Linux bpf 1.1、BPF内核实现。言归正传,主要看pcap_setfilter 关键代码,如下

struct sock_fprog *fcode
ret = setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,
			 fcode, sizeof(*fcode));

其实在liunx上,你只需要简单的创建你的filter代码,通过SO_ATTTACH_FILTER选项发送到内核,并且你的filter代码能通过内核的检查,这样你就可以立即过滤socket上面的数据了。
而 struct sock_fprog 结构如下

struct sock_fprog {			/* Required for SO_ATTACH_FILTER. */
	unsigned short		   len;	/* Number of filter blocks */
	struct sock_filter __user *filter;
};

struct sock_filter {	/* Filter block */
	__u16	code;   /* Actual filter code */
	__u8	jt;	/* Jump true */
	__u8	jf;	/* Jump false */
	__u32	k;      /* Generic multiuse field */
};

其中code元素是一个16位宽的操作码,具有特定的指令编码。jt和jf是两个8位宽的跳转目标,一个用于条件“跳转如果真”,另一个“跳转如果假”。最后k元素包含一个可以用不同方式解析的杂项参数,依赖于code给定的指令。

那么过滤规则如何转化为sock_filter 结构体对应的规则呢?不是说不需要深入bpf,其实tcpdump 提供了一种方法,可以将过滤规则转化成对应liunx c下sock_filter 规则。

例如你需要过滤经过本机端口22所有的数据。在liunx 终端安装了tcpdump 。只需在终端输入 tcpdump port 22 -nn -dd 就可生产对应规则,如下图
tcpdump  -dd
至此你其实已经完全可以根据要过滤的包,自己实现抓包过滤了。程序如下

int create_link_raw_socket(){
	struct sock_filter bpf_code[] = {
			// tcpdump  port 22 -nn -dd
			{ 0x28, 0, 0, 0x0000000c },
			{ 0x15, 0, 8, 0x000086dd },
			{ 0x30, 0, 0, 0x00000014 },
			{ 0x15, 2, 0, 0x00000084 },
			{ 0x15, 1, 0, 0x00000006 },
			{ 0x15, 0, 17, 0x00000011 },
			{ 0x28, 0, 0, 0x00000036 },
			{ 0x15, 14, 0, 0x00000016 },
			{ 0x28, 0, 0, 0x00000038 },
			{ 0x15, 12, 13, 0x00000016 },
			{ 0x15, 0, 12, 0x00000800 },
			{ 0x30, 0, 0, 0x00000017 },
			{ 0x15, 2, 0, 0x00000084 },
			{ 0x15, 1, 0, 0x00000006 },
			{ 0x15, 0, 8, 0x00000011 },
			{ 0x28, 0, 0, 0x00000014 },
			{ 0x45, 6, 0, 0x00001fff },
			{ 0xb1, 0, 0, 0x0000000e },
			{ 0x48, 0, 0, 0x0000000e },
			{ 0x15, 2, 0, 0x00000016 },
			{ 0x48, 0, 0, 0x00000010 },
			{ 0x15, 0, 1, 0x00000016 },
			{ 0x6, 0, 0, 0x0000ffff },
			{ 0x6, 0, 0, 0x00000000 }
	};
	
	int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	struct sock_fprog bpf;
	memset(&bpf,0x00,sizeof(bpf));
	bpf.len = sizeof(bpf_code) / sizeof(struct sock_filter);
	bpf.filter = bpf_code;
	int ret = setsockopt( fd,SOL_SOCKET, SO_ATTACH_FILTER, &bpf,sizeof(bpf));
	if (ret < 0)
	{
		printf("setsockopt:SO_ATTACH_FILTER>>>>error:%s\n",strerror(errno));
	}
	return fd;
}

通过上面的代码你根据返回的 fd ,调用recvfrom 接收到的包就是经过过滤的22端口的数据包。但是你要是想抓的包是去除数据链路层的头,用socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); 然后根据tcpdump -dd生成的规则,你会发现加入的规则起不到作用。这里推测 tcpdump -dd 生成的规则只针对链路层。

同时在socket除了你可以添加你要过滤的规则,还有几个两个与bpf规则相关的系统调用

  • 在套接字socket 附加filter规则 :
    setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &val, sizeof(val));
  • 把filter从socket上移除 :
    setsockopt(sockfd, SOL_SOCKET, SO_DETACH_FILTER, &val, sizeof(val));
  • 运行中锁定附加到socket上的filter:
    setsockopt(sockfd, SOL_SOCKET, SO_LOCK_FILTER, &val, sizeof(val));

其中SO_DETACH_FILTER选项可以把filter从socket上移除。这可能不会被经常使用,因为当你关闭socket的时候如果有filter会被自动移除。另外一个不太常见的情况是在同一个socket上添加不同的filter,当你还有另一个filter正在运行:如果你的新filter代码能够通过内核检查,内核小心的把旧的filter移除把新的filter换上,如果检查失败旧的filter将继续保留在socket上。

SO_LOCK_FILTER选项运行锁定附加到socket上的filter。一旦设置,filter不能被移除或者改变。这种允许一个进程设置一个socket、附加一个filter、锁定它们并放弃特权,确保这个filter保持到socket的关闭。

三 抓包过滤应用实现

通过上面了解,貌似自己实现抓包过滤并不存在啥技术难度。相反是不是感觉很简单。其实并不见得,本章要实现抓包过滤的应用功能,本质上是类似实现nat转换的功能。大概就是经过本机指定的srcip,srcPort过滤数据包,然后修改数据包,给该数据转发到另一台设备上destip,destPort。

1. 数据链路层抓包

notes : 只给出关键代码

int create_link_raw_socket(){
	struct sock_filter bpf_code[] = {
			// tcpdump  src  10.68.22.140 and port 7777 -nn -dd
			{ 0x28, 0, 0, 0x0000000c },
			{ 0x15, 0, 14, 0x00000800 },
			{ 0x20, 0, 0, 0x0000001a },
			{ 0x15, 0, 12, 0x0a44168c },
			{ 0x30, 0, 0, 0x00000017 },
			{ 0x15, 2, 0, 0x00000084 },
			{ 0x15, 1, 0, 0x00000006 },
			{ 0x15, 0, 8, 0x00000011 },
			{ 0x28, 0, 0, 0x00000014 },
			{ 0x45, 6, 0, 0x00001fff },
			{ 0xb1, 0, 0, 0x0000000e },
			{ 0x48, 0, 0, 0x0000000e },
			{ 0x15, 2, 0, 0x00001e61 },
			{ 0x48, 0, 0, 0x00000010 },
			{ 0x15, 0, 1, 0x00001e61 },
			{ 0x6, 0, 0, 0x0000ffff },
			{ 0x6, 0, 0, 0x00000000 }
	};
	int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	struct sock_fprog bpf;
	memset(&bpf,0x00,sizeof(bpf));
	bpf.len = sizeof(bpf_code) / sizeof(struct sock_filter);
	bpf.filter = bpf_code;

	int ret = setsockopt( fd,SOL_SOCKET, SO_ATTACH_FILTER, &bpf,sizeof(bpf));
	if (ret < 0)
	{
		printf("setsockopt:SO_ATTACH_FILTER>>>>error:%s\n",strerror(errno));
	}
	return fd;
}

首先创建了socket ,从数据链路层开始过滤,只抓src 10.68.22.140 并且 7777 端口的数据包,这部分代码前面做过说明。

	while (1) {
		memset(buffer, 0, BUFFER_SIZE);
		struct sockaddr_in t_addr;
		socklen_t addr_len = sizeof(struct sockaddr_in);
		recv_ret = recvfrom(ptm->raw_fd, buffer, BUFFER_SIZE,0, (struct sockaddr *) &t_addr, &addr_len);
		if (recv_ret <= 0) {
			continue;
		}

		if (ptm->do_message_parse) {
			d_len = ptm->do_message_parse(buffer, recv_ret,ptm);
			if (d_len <= 0) {
				continue;
			}
		}
		struct sockaddr_ll addr;
		memset( &addr, 0, sizeof(addr) );
		addr.sll_family = AF_PACKET;
		struct ifreq ifstruct;
		strcpy(ifstruct.ifr_name, "eth0");
		ioctl(ptm->raw_fd, SIOCGIFINDEX, &ifstruct); 
		addr.sll_ifindex = ifstruct.ifr_ifindex;
		addr.sll_protocol = htons(ETH_P_ALL);
		send_ret= sendto(ptm->raw_fd, buffer, d_len, 0, &addr, sizeof(struct sockaddr_ll));
		if (send_ret < 0)
		{
			printf("error:%s\n",strerror(errno));
		}
		if (send_ret <= 0) {
			continue;
		}
	}
	

while循环里三个功能

  • 1 循环抓包 recvfrom
  • 2 抓包处理分析 do_message_parse
  • 3 转发包 sendto

其中抓包处理是核心,下面会具体说明,这里说下 struct sockaddr_ll 结构体

/*设备无关的物理层地址结构,数据链路层的头信息通常定义在 sockaddr_ll 的结构体中,通过setsockopt可以设置网卡的多播或混杂模式*/
struct sockaddr_ll
{
	unsigned short int sll_family; /* 一般为AF_PACKET */
	unsigned short int sll_protocol; /* 上层协议 */
	int  sll_ifindex; /* 接口类型 */
	unsigned short int sll_hatype; /* 报头类型 */
	unsigned char sll_pkttype; /* 包类型 */
	unsigned char sll_halen; /* 地址长度 */
	unsigned char sll_addr[8]; /* MAC地址 */
};
/ * ---------------------------------------------------
 sll_ifindex: interface索引,0 匹配所有的网络接口卡
 sll_pkttype: 包含了packet类型。
                 PACK_HOST                  包地址为本地主机地址。
                 PACK_BROADCAST    物理层广播包。
                 PACK_MULTICAST      发送到物理层多播地址的包。
                 PACK_OTHERHOST    发往其它在混杂模式下被设备捕获的主机的包。
                 PACK_OUTGOING       本地回环包(从本机发出的,不小心loopback到当前socket了)
当发送数据包时,指定 sll_family, sll_addr, sll_halen, sll_ifindex, sll_protocol 就足够了。其它字段设置为0; sll_hatype和 sll_pkttype是在接收数据包时使用的; 如果要bind, 只需要使用 sll_protocol和 sll_ifindex;
------------------------------------------------------------*/         

若是你想iPv4的struct sockaddr_in 套接字地址,发送数据。sendto 会报错,显示无效的地址。正如你抓的包是从链路层开始抓的,想要发送出去,套接字地址也得从链路层开始设置。

最后看关键处理部分 do_message_parse

int do_message_parse( char *packet,int lens,void * param){
	raw_chl * ptm = (raw_chl *)param;
	if(ptm == NULL){
		return lens;
	}
	// 链路层
	struct ethernet *ethernet = (struct ethernet*) (packet);
	char tempbuf[6] ={0};
	memcpy(tempbuf,ethernet->ether_dhost,6);
	memcpy(ethernet->ether_dhost,ethernet->ether_shost,6);
	memcpy(ethernet->ether_shost,tempbuf,6);

	const struct ndpi_llc_header_snap *llc;
	u_short ethernet_type = ntohs(ethernet ->ether_type);
	int offset = sizeof(struct ethernet);

	int pyld_eth_len = 0;

	if(ethernet_type <= 1500){
		pyld_eth_len = ethernet_type;
		printf("================ethernet_type:%d\n",ethernet_type);
	}

	if(pyld_eth_len != 0) {
		llc = (struct ndpi_llc_header_snap *)(&packet[offset]);
		/* check for LLC layer with SNAP extension */
		if(llc->dsap == SNAP || llc->ssap == SNAP) {
			ethernet_type = llc->snap.proto_ID;
			offset += + 8;
		}
		/* No SNAP extension - Spanning Tree pkt must be discarted */
		else if(llc->dsap == BSTP || llc->ssap == BSTP) {
			//			printf("\n\nWARNING: only IPv4/IPv6 packets are supported in this demo (vg_security supports both IPv4 and IPv6), all other packets will be discarded\n\n");
			return lens;
		}
	}

	while (ethernet_type == ETH_P_8021Q) {
		ethernet_type = (packet[offset + 2] << 8) + packet[offset + 3];
		offset += 4;
	}

	// 网络层 ip 
	struct ip * ip_header = (struct ip*) (packet + offset);

	u_int ip_len = ntohs(ip_header->ip_len);
	u_int ip_size = IP_HL(ip_header) * 4;
	u_int msg_size = 0;
	printf("msg_c2s_parse befor ===========enter sniffer-flow:src_ip%s\n",(char*)inet_ntoa(ip_header->ip_src));
	printf("msg_c2s_parse befor ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
	ip_header->ip_src.s_addr = ptm->local_addr.n_ip;
	ip_header->ip_dst.s_addr = ptm->server_addr.n_ip;
	ip_header->ip_sum = 0;
	printf("msg_c2s_parse after ===========enter sniffer-flow:src_ip%s\n",vg_sock_get_aip( &ptm->local_addr));
	printf("msg_c2s_parse after ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));

	ip_header->ip_sum = in_chksum((u_short *)ip_header,ip_size);

	tsd_hdr_t psdHeader;
	memset(&psdHeader,0,sizeof(tsd_hdr_t));

//  传输层  udp 和 tcp 
	switch (ethernet_type) {
	case ETH_P_IP:
		switch (ip_header->ip_p) {
		case IPPROTO_TCP: {
			offset += ip_size;

			struct tcp* tcp = (struct tcp*) (packet + offset);
			u_int tcp_size = TH_OFF(tcp) * 4;

			msg_size = ip_len - ip_size - tcp_size;

			g_n_port_client = tcp->th_sport;

			printf("===src_port:%d\n",ntohs(tcp->th_sport));
			printf("===dst_port:%d\n",ntohs(tcp->th_dport));

//			tcp->th_sport = ptm->out_addr.n_port;
			tcp->th_dport = ptm->server_addr.n_port;
			tcp->th_sum = 0;

			psdHeader.saddr=ip_header->ip_src.s_addr;
			psdHeader.daddr=ip_header->ip_dst.s_addr;
			psdHeader.mbz=0;
			psdHeader.ptcl=ip_header->ip_p;
			psdHeader.tcpl=htons(msg_size + tcp_size);

			char szSendBuf[65535] = {0};
			memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
			memcpy(szSendBuf+sizeof(psdHeader), packet+offset,tcp_size);
			memcpy(szSendBuf+sizeof(psdHeader)+tcp_size,packet+offset+tcp_size ,msg_size);
			tcp->th_sum=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+tcp_size +msg_size);

			//			memcpy(packet+offset,tcp,sizeof(struct tcp));
		}
		break;
		case IPPROTO_UDP:
			offset += ip_size;
			struct udphdr * udp = (struct udphdr*) (packet + offset);

			g_n_port_client = udp->source;

			printf("===src_port:%d\n",ntohs(udp->source));
			printf("===dst_port:%d\n",ntohs(udp->dest));
			udp->source = ptm->local_addr.n_port;
			udp->dest = ptm->server_addr.n_port;
			udp->check = 0;

			msg_size = ip_len - SIZE_IPNET - SIZE_UDP_HEAD;

			psdHeader.saddr=ip_header->ip_src.s_addr;
			psdHeader.daddr=ip_header->ip_dst.s_addr;
			psdHeader.mbz=0;
			psdHeader.ptcl=ip_header->ip_p;
			psdHeader.tcpl=htons(8+msg_size);

			char szSendBuf[65535] = {0};
			memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
			memcpy(szSendBuf+sizeof(psdHeader), udp, 8);
			memcpy(szSendBuf+sizeof(psdHeader)+8, packet + offset +8, msg_size);
			udp->check=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+8+msg_size);

			//			memcpy(packet+offset,udp,sizeof(struct udphdr));
			offset =offset +SIZE_UDP_HEAD;
			break;
		default:
			break;
		}
		break;

		default:
			break;
	}

	return lens;
}

对tcp/ip 协议没有了解过的,估计上面代码会看的有点吃力,建议还是先熟悉下tcp/ip协议,tcp/ip 不是本章的讨论点。而上面代码其实做了三件事

  • 获取到数据链路层数据,修改src,dst的MAC地址,(sendto 从数据链路层的数据包时,需要正确的MAC地址)
  • 获取IP 层数据,修改src,dst的IP地址,同时重新计算网络层校验和
  • 获取 udp/tcp 层数据 ,修改src,dst的port ,同时计算传输层校验和

你发现要是从链路层抓包的话,转发的时候还得需要对方的MAC地址,才能转发,而这在大多数情况是无法获取到的,因此上面这种方法是有局限性的。个人觉得也就适合对方发你,然后你组包回对方。

难道抓的包包含链路层的数据,转发发送数据的时候,就必须填充对方的MAC地址吗?当然不是,你可以通过创建两个fd,一个捕获包,另一个发送包。于是上面代码变成。

//创建了发送数据包的套接字
int create_net_raw_socket(){
	int fd = socket(AF_INET,SOCK_RAW,IPPROTO_UDP|IPPROTO_TCP);
	int flag = 1;
	int ret = setsockopt(fd,IPPROTO_IP, IP_HDRINCL,&flag,sizeof(int));
	if (ret < 0)
	{
		printf("setsockopt :%d=====%d======%s\n",ret,errno,strerror(errno));
	}
	return fd;
}

当需要编写自己的IP数据包首部时,可以在原始套接字上设置套接字选项IP_HDRINCL。我们需要修改src,dst的ip地址。因此需要设置IP_HDRINCL

如果IP_HDRINCL选项未开启,则由内核自动构造IP首部并把它置于来自进程的数据之前,进程让内核发送的数据起始地址指的是IP首部之后的第一个字节。(就是指进程不需要管IP首部)

如果IP_HDRINCL选项开启,则进程让内核发送数据的起始地址指的是IP首部的第一个字节,进程调用输出函数写出的数据量必须包括IP首部的大小,整个IP首部由进程构造,不过IPv4校验可置为0,表示进程让内核来设置该值,IPv4首部校验和字段总是由内核计算并存储。简而言之你不需要计算IP首部校验和,只需要置为0,内核会自动计算。

while (1) {
		memset(buffer, 0, BUFFER_SIZE);
		struct sockaddr_in t_addr;
		socklen_t addr_len = sizeof(struct sockaddr_in);
		recv_ret = recvfrom(ptm->raw_fd, buffer, BUFFER_SIZE,0, (struct sockaddr *) &t_addr, &addr_len);
		if (recv_ret <= 0) {
			continue;
		}

		printf("proxy_c2s  recv_lens:%d\n",recv_ret);

		if (ptm->do_message_parse) {
			d_len = ptm->do_message_parse(buffer, recv_ret,ptm);
			if (d_len <= 0) {
				continue;
			}
		}

//		struct sockaddr_ll addr;
//		memset( &addr, 0, sizeof(addr) );
//		addr.sll_family = AF_PACKET;
//		struct ifreq ifstruct;
//		strcpy(ifstruct.ifr_name, "eth0");
//		ioctl(ptm->raw_fd, SIOCGIFINDEX, &ifstruct); //??I/O??
//		addr.sll_ifindex = ifstruct.ifr_ifindex;
//		addr.sll_protocol = htons(ETH_P_ALL);

		struct sockaddr_in in;
		memset(&in,0,sizeof(struct sockaddr_in));
		in.sin_family = AF_INET;
		in.sin_addr.s_addr = ptm->server_addr.n_ip;
		in.sin_port = ptm->server_addr.n_port;
		memset(in.sin_zero, 0, sizeof(in.sin_zero));

		send_ret= sendto(ptm->send_fd, buffer, d_len, 0,(struct sockaddr *) &in, sizeof(struct sockaddr_in));
		if (send_ret <= 0) {
			continue;
		}

	}

while 循环 其实跟之前结构一样,只是套接字地址用了IPV4的sockaddr_in,同时发送的套接字是由create_net_raw_socket()创建的。而最后虽然捕获的数据包是包含链路层数据的,但是最后sendto的时候数据是将链路层的数据截除后发送的。看do_message_parse 数据处理函数。

int do_message_parse( char *packet,int lens,void * param){
	raw_chl * ptm = (raw_chl *)param;
	if(ptm == NULL){
		return lens;
	}
	struct ethernet *ethernet = (struct ethernet*) (packet);
	const struct ndpi_llc_header_snap *llc;
	u_short ethernet_type = ntohs(ethernet ->ether_type);
	int offset = sizeof(struct ethernet);

	int pyld_eth_len = 0;

	if(ethernet_type <= 1500){
		pyld_eth_len = ethernet_type;
		printf("================ethernet_type:%d\n",ethernet_type);
	}

	if(pyld_eth_len != 0) {
		llc = (struct ndpi_llc_header_snap *)(&packet[offset]);
		/* check for LLC layer with SNAP extension */
		if(llc->dsap == SNAP || llc->ssap == SNAP) {
			ethernet_type = llc->snap.proto_ID;
			offset += + 8;
		}
		/* No SNAP extension - Spanning Tree pkt must be discarted */
		else if(llc->dsap == BSTP || llc->ssap == BSTP) {
			//			printf("\n\nWARNING: only IPv4/IPv6 packets are supported in this demo (vg_security supports both IPv4 and IPv6), all other packets will be discarded\n\n");
			return lens;
		}
	}

	while (ethernet_type == ETH_P_8021Q) {
		ethernet_type = (packet[offset + 2] << 8) + packet[offset + 3];
		offset += 4;
	}
	int recvlens = lens -offset;
	int offflag = offset;
	struct ip * ip_header = (struct ip*) (packet + offset);

	u_int ip_len = ntohs(ip_header->ip_len);
	u_int ip_size = IP_HL(ip_header) * 4;
	u_int msg_size = 0;
	printf("msg_s2c_parse befor ===========enter sniffer-flow:src_ip%s\n",(char*)inet_ntoa(ip_header->ip_src));
	printf("msg_s2c_parse befor ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
	ip_header->ip_src.s_addr = ip_header->ip_dst.s_addr;
	ip_header->ip_dst.s_addr = ptm->server_addr.n_ip;
	ip_header->ip_sum = 0;
//	printf("msg_s2c_parse after ===========enter sniffer-flow:src_ip%s\n",vg_sock_get_aip( &ptm->in_addr));
	printf("msg_s2c_parse after ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));

	//	ip_header->ip_sum = in_chksum((u_short *)ip_header,ip_size);


	tsd_hdr_t psdHeader;
	memset(&psdHeader,0,sizeof(tsd_hdr_t));

	switch (ethernet_type) {
	case ETH_P_IP:
		switch (ip_header->ip_p) {
		case IPPROTO_TCP: {
			offset += ip_size;

			struct tcp* tcp = (struct tcp*) (packet + offset);
			u_int tcp_size = TH_OFF(tcp) * 4;

			msg_size = ip_len - ip_size - tcp_size;

			printf("ip_len:%d ====ip_size:%d==== tcp_size:%d\n",ip_len,ip_size,tcp_size);
//			tcp->th_sport = ptm->in_addr.n_port;
			tcp->th_dport = ptm->server_addr.n_port;

			printf("msg_s2c_parse===src_port:%d\n",ntohs(tcp->th_sport));
			printf("msg_s2c_parse===dst_port:%d\n",ntohs(tcp->th_dport));
			tcp->th_sum = 0;

			psdHeader.saddr=ip_header->ip_src.s_addr;
			psdHeader.daddr=ip_header->ip_dst.s_addr;
			psdHeader.mbz=0;
			psdHeader.ptcl=ip_header->ip_p;
			psdHeader.tcpl=htons(msg_size + tcp_size);

			char szSendBuf[65535] = {0};
			memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
			printf("tcp_size:%d\n",tcp_size);
			memcpy(szSendBuf+sizeof(psdHeader), packet+offset,tcp_size);
			memcpy(szSendBuf+sizeof(psdHeader)+tcp_size,packet+offset+tcp_size ,msg_size);
			tcp->th_sum=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+tcp_size +msg_size);

			//			memcpy(packet+offset,tcp,sizeof(struct tcp));
		}
		break;
		case IPPROTO_UDP:
			offset += ip_size;
			struct udphdr * udp = (struct udphdr*) (packet + offset);

			udp->source = udp->dest;
			udp->dest = ptm->server_addr.n_port;
			udp->check = 0;

			msg_size = ip_len - SIZE_IPNET - SIZE_UDP_HEAD;

			psdHeader.saddr=ip_header->ip_src.s_addr;
			psdHeader.daddr=ip_header->ip_dst.s_addr;
			psdHeader.mbz=0;
			psdHeader.ptcl=ip_header->ip_p;
			psdHeader.tcpl=htons(8+msg_size);

			char szSendBuf[65535] = {0};
			memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
			memcpy(szSendBuf+sizeof(psdHeader), udp, 8);
			memcpy(szSendBuf+sizeof(psdHeader)+8, packet + offset +8, msg_size);
			udp->check=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+8+msg_size);

			//			memcpy(packet+offset,udp,sizeof(struct udphdr));
			offset =offset +SIZE_UDP_HEAD;
			break;
		default:
			break;
		}
		break;

		default:
			break;
	}
	// 去除链路层的数据
	memcpy(packet,packet+offflag,recvlens);
	return recvlens;
}

处理函数相比较之前链路层的处理函数:

  • 不需要修改链路层的Mac地址
  • ip层首部校验和置0即可,内核内部会计算
  • 最后返回的数据不包含链路层的数据。
2. 网络层抓包

既然可以链路层抓包,当然也可以抛开数据链路层,直接网络层抓包。而方法前面已经提到 socket(PF_PACKET, SOCK_DGRAM,htons(ETH_P_ALL)); 若是你想起来,自然也应该记得这有个很致命的问题,在不深入了解bpf规则的前提下,用tcpdump -dd生产的规则只适应链路层抓包。 那这问题如何解决呢?你想通过网络层抓包,但又不知道bpf规则如何设置?这问题困扰好久,最终不得不借助libpcap生产规则的接口。

int create_raw_socket(){
	static const char filter[] = "port 7777 and host 10.68.22.140";
	int sock = socket(PF_PACKET, SOCK_DGRAM,htons(ETH_P_ALL));
	pcap_t *pcap = pcap_open_dead(DLT_RAW, 65535);
	struct bpf_program bpf_prog;
	pcap_compile(pcap, &bpf_prog, filter, 0, PCAP_NETMASK_UNKNOWN);
	printf("==========bpf_prog.bf_len:%d\n",bpf_prog.bf_len);
	struct sock_fprog linux_bpf = {
	    .len = bpf_prog.bf_len,
	    .filter = (struct sock_filter *) bpf_prog.bf_insns,
	};
	int ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &linux_bpf, sizeof(linux_bpf));
	if (ret < 0)
	{
		printf("setsockopt:SO_ATTACH_FILTER>>>>error:%s\n",strerror(errno));
	}
	return sock;
}

pcap_open_dead()官方文档提到

It is typically used when just using libpcap for compiling BPF code;

其实就是创建了一个pcap_t 结构体,用于创建bpf代码。该接口用处不大。
pcap_compile() 接口 创建bpf代码的

while (1) {
		memset(buffer, 0, BUFFER_SIZE);
		struct sockaddr_in t_addr;
		socklen_t addr_len = sizeof(struct sockaddr_in);
		recv_ret = recvfrom(ptm->raw_fd, buffer, BUFFER_SIZE,0, (struct sockaddr *) &t_addr, &addr_len);
		if (recv_ret <= 0) {
			continue;
		}
		printf("proxy_c2s  recv_lens:%d\n",recv_ret);
		if (ptm->do_message_parse) {
			d_len = ptm->do_message_parse(buffer, recv_ret,ptm);
			if (d_len <= 0) {
				continue;
			}
		}
		struct sockaddr_in in;
		memset(&in,0,sizeof(struct sockaddr_in));
		in.sin_family = AF_INET;
		in.sin_addr.s_addr = ptm->server_addr.n_ip;
		in.sin_port = ptm->server_addr.n_port;
		memset(in.sin_zero, 0, sizeof(in.sin_zero));

		send_ret= sendto(ptm->send_fd, buffer, d_len, 0,(struct sockaddr *) &in, sizeof(struct sockaddr_in));
		if (send_ret <= 0) {
			continue;
		}
	}

再看while循环体,本质还是接收包,处理包,发送包3个流程。不过因为抓的是网络层数据包,获取到的包无需再处理数据链路。

再看do_message_parse 函数 你会发现对链路层的处理部分全都不需要了。

int do_message_parse( char *packet,int lens,void * param){
	raw_chl * ptm = (raw_chl *)param;
	if(ptm == NULL){
		return lens;
	}
	int offset = 0;
	struct ip * ip_header = (struct ip*) (packet + offset);

	u_int ip_len = ntohs(ip_header->ip_len);
	u_int ip_size = IP_HL(ip_header) * 4;
	u_int msg_size = 0;
	printf("msg_c2s_parse befor ===========enter sniffer-flow:src_ip%s\n",(char*)inet_ntoa(ip_header->ip_src));
	printf("msg_c2s_parse befor ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
	ip_header->ip_src.s_addr = ptm->local_addr.n_ip;
	ip_header->ip_dst.s_addr = ptm->server_addr.n_ip;
	ip_header->ip_sum = 0;
	printf("msg_c2s_parse after ===========enter sniffer-flow:src_ip%s\n",vg_sock_get_aip( &ptm->local_addr));
	printf("msg_c2s_parse after ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));

	ip_header->ip_sum = in_chksum((u_short *)ip_header,ip_size);

	tsd_hdr_t psdHeader;
	memset(&psdHeader,0,sizeof(tsd_hdr_t));
	
		switch (ip_header->ip_p) {
		case IPPROTO_TCP: {
			offset += ip_size;

			struct tcp* tcp = (struct tcp*) (packet + offset);
			u_int tcp_size = TH_OFF(tcp) * 4;

			msg_size = ip_len - ip_size - tcp_size;

			g_n_port_client = tcp->th_sport;

			printf("===src_port:%d\n",ntohs(tcp->th_sport));
			printf("===dst_port:%d\n",ntohs(tcp->th_dport));

//			tcp->th_sport = ptm->out_addr.n_port;
			tcp->th_dport = ptm->server_addr.n_port;
			tcp->th_sum = 0;

			psdHeader.saddr=ip_header->ip_src.s_addr;
			psdHeader.daddr=ip_header->ip_dst.s_addr;
			psdHeader.mbz=0;
			psdHeader.ptcl=ip_header->ip_p;
			psdHeader.tcpl=htons(msg_size + tcp_size);

			char szSendBuf[65535] = {0};
			memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
			memcpy(szSendBuf+sizeof(psdHeader), packet+offset,tcp_size);
			memcpy(szSendBuf+sizeof(psdHeader)+tcp_size,packet+offset+tcp_size ,msg_size);
			tcp->th_sum=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+tcp_size +msg_size);

			//			memcpy(packet+offset,tcp,sizeof(struct tcp));
		}
		break;
		case IPPROTO_UDP:
			offset += ip_size;
			struct udphdr * udp = (struct udphdr*) (packet + offset);

			g_n_port_client = udp->source;

			printf("===src_port:%d\n",ntohs(udp->source));
			printf("===dst_port:%d\n",ntohs(udp->dest));
			udp->source = ptm->local_addr.n_port;
			udp->dest = ptm->server_addr.n_port;
			udp->check = 0;

			msg_size = ip_len - SIZE_IPNET - SIZE_UDP_HEAD;

			psdHeader.saddr=ip_header->ip_src.s_addr;
			psdHeader.daddr=ip_header->ip_dst.s_addr;
			psdHeader.mbz=0;
			psdHeader.ptcl=ip_header->ip_p;
			psdHeader.tcpl=htons(8+msg_size);

			char szSendBuf[65535] = {0};
			memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
			memcpy(szSendBuf+sizeof(psdHeader), udp, 8);
			memcpy(szSendBuf+sizeof(psdHeader)+8, packet + offset +8, msg_size);
			udp->check=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+8+msg_size);
			
			offset =offset +SIZE_UDP_HEAD;
			break;
		default:
			break;
		}

	return lens;
}

3. 运行结果

测试中本机地址22.189 转发地址是22.140

程序运行在22.189 设备上
22.140:7777 端口 发送给 22.189:8888端口,如下图
发送数据
程序处理后,会将发往 22.189:8888 的数据包转发给 22.140:9000 端口,如下图
在这里插入图片描述

四. lo口抓包发包

为啥要把lo口的抓发包单独提出来呢? 带着这个问题往下看
lo口的有条重要性质:

  • 1.ip addr add 127.2.0.1/16 dev lo 添加了127.2.0.1/16的地址 代表127.2.0.1/16网段所有地址都设置好了,即ping 127.2.128.222 也是可以通的 不需要单独再配置 127.2.128.222 地址

这对某些项目部署工程的时候,要是可以用lo口地址配置一个网段地址,所有该网段的地址都起来了。因此项目中可能会用到lo口地址。例如本人之前有个通过lo口代理转发项目。

udp协议lo还发现一个特性,例如我eth0 地址配置了10.68.22.189 ,然后 lo口配置了127.2.0.1/16 网段的地址。当我用10.68.22.189 地址往 127.3.0.11 这个地址发送数据的时候。虽然127.3.0.11 地址是不存在的,但是数据包是能发送出去的。
程序demo执行如下图所示
在这里插入图片描述
tcpdump 抓包显示如下
在这里插入图片描述
但是要是我以 127.2.0.11 发往 127.3.0.11。 你会发现数据包压根发送不出去。
程序直接报错误,显示无效参数,如下图
在这里插入图片描述
后面发现udp要是想lo发送数据包,首先你发送本机设置的127地址必须是存在的,同时发送的服务端地址不管是发往的是127的还是其他地址。必须是本机存在的。lo口地址发送只能发往本机。

这条特性还是要多注意下,之前发现莫名其妙lo口发送的数据,怎么抓包都抓不到,后面才发现这个问题。

lo口相关知识点已经铺垫好了,言归正传,之前不是介绍链路层的两种抓包,还有网络层的一种抓包。后面发现用lo口发送数据,数据发送出来一条,但是抓包确认抓到两条。
test程序 127.2.0.11 往 127.2.0.44 发送了一次数据包,如下图
在这里插入图片描述
抓包程序发现接收到了两次相同的包。 而导致lo口发送一次包,转发了两条数据
在这里插入图片描述
tcpdump 抓包如下
在这里插入图片描述
从tcpdump 你发现他只收到了一次lo口的包,后面两条是我程序转发的两次数据包。那么为啥tcpdump只收到了一次数据包,tcpdump 做了啥处理了吗?

回顾libpcap pcap_read_packet() 接口 里面接收到套接字地址后,会调用
linux_check_direction()函数,里面有一段关键代码如下

struct pcap_linux	*handlep = handle->priv;
if (sll->sll_pkttype == PACKET_OUTGOING) {
	/*
	 * Outgoing packet.
	 * If this is from the loopback device, reject it;
	 * we'll see the packet as an incoming packet as well,
	 * and we don't want to see it twice.
	 */
	if (sll->sll_ifindex == handlep->lo_ifindex)
		return 0;
}

看注释的这段话,lo口的设备抓到的包,在incoming ,Outgoing 都会收到一次,我们不需要抓包两次,因此outgoing的包忽略掉

到这里我们已经看到解决方法,下面给出代码关键部分

	while (1) {
		memset(buffer, 0, BUFFER_SIZE);
		struct sockaddr_ll  t_addr;
		socklen_t addr_len = sizeof(struct sockaddr_ll);
		recv_ret = recvfrom(ptm->raw_fd, buffer, BUFFER_SIZE,0, (struct sockaddr *) &t_addr, &addr_len);
		if (recv_ret <= 0) {
			printf("recvfrom  error:%s\n",strerror(errno));
			continue;
		}

		if (t_addr.sll_pkttype == PACKET_OUTGOING) {
			continue;
		}
		if (ptm->do_message_parse) {
			d_len = ptm->do_message_parse(buffer, recv_ret,ptm);
			if (d_len <= 0) {
				continue;
			}
		}
		printf("do_message_parse:d_len:%d\n",d_len);

		struct sockaddr_in in;
		memset(&in,0,sizeof(struct sockaddr_in));
		in.sin_family = AF_INET;
		in.sin_addr.s_addr = ptm->server_addr.n_ip;
		in.sin_port = ptm->server_addr.n_port;
		memset(in.sin_zero, 0, sizeof(in.sin_zero));

		send_ret= sendto(ptm->send_fd, buffer, d_len, 0,(struct sockaddr *) &in, sizeof(struct sockaddr_in));
		if (send_ret <= 0) {
			continue;
		}

	}

测试结果,tcpdump 抓包如下
在这里插入图片描述
上图可以看到127.2.0.11 往 127.2.0.44 发送了一条udp数据
然后后面 10.68.22.189:9999 将这条数据转发给了 10.68.22.140。转发的部分就程序做的,只转发了一条,说明抓的数据已经做了过滤。

五. 总结

注意点:

  • 链路层抓包,提供了两种处理方式,一种是创建raw_fd抓包,同时还是用raw_fd发包,另一种是raw_fd抓包,send_fd 发包。
  • 网络层抓包,需要注意bpf规则的生成。同时只提供了raw_fd抓包,send_fd发包一种情况,试过用raw_fd 抓包发包,会报无效的参数错误,未找到问题。
  • lo口发送udp数据包,要注意发送的lo地址必须存在,同时发往的地址也必须本机真实存在。lo口发送包,正常情况会抓到两次数据包,要做过滤。

问题:

  • 本篇示例只处理请求转发,并没有处理响应转发。了解了请求转发过程,响应其实是一个道理
  • 提供的示例是走的udp协议,相对简单。tcp协议面向连接,必须要三次握手成功,才能互相发送数据。也就是必须处理转发和响应

tcp 协议篇幅有限,这里没有讨论tcp,若是你实践了tcp转发。这里啰嗦几句, 即使你转发响应都处理了,你会发现当客户端tcp握手一个没有开放监听的端口,syn 包请求包发送后,数据包到了本机应用层,它会发现tcp请求的端口,本机没有监听,开放。会直接返回Rst包重置掉这个请求。导致转发的三次握手一直连接不上。你只需要在iptables filter链 drop 掉 这个syn请求包即可。纳尼? 说的不简洁,是不是云里雾里。总而言之,你得明白你作为转发的本机其实就是捕获数据,并不会去在本机监听开放端口,而当客户端发起tcp连接,连接的端口并不存在,所以本机会发一个rst重置包,关闭这个连接。

断断续续花了两周多的时间整理,把遇到的坑跟大伙分析。希望对大伙有用。至此本篇完。。。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

linux 下 tcpdump 详解 后篇(自己实现抓包过滤) 的相关文章

  • Linux shell 标题大小写

    我正在编写一个 shell 脚本并有一个如下所示的变量 something that is hyphenated 我需要在脚本中的各个点使用它 如下所示 something that is hyphenated somethingthati
  • C/C++ with GCC:静态地将资源文件添加到可执行文件/库

    有人知道如何使用 GCC 将任何资源文件静态编译为可执行文件或共享库文件吗 例如 我想添加永远不会改变的图像文件 如果它们改变了 我无论如何都必须替换该文件 并且不希望它们位于文件系统中 如果这是可能的 我认为这是因为 Visual C f
  • 尝试编译 git 但在 linux 中找不到 libcurl

    我想编译支持 http https 的 git 我有 ls usr include curl curlbuild h curl h curlrules h curlver h easy h mprintf h multi h stdchea
  • Docker 容器可以访问 DNS,但无法解析主机

    我在运行 docker 容器时遇到一个有趣的问题 突然间 我无法从容器内解析 DNS 这是一个概要 一切都没有解决 apt get pip 一次性 ping 容器等正在运行docker run it dns 8 8 8 8 ubuntu p
  • 如何从 Linux 命令行打开 Sublime Text 2 文件到选项卡,而不是新窗口

    我有 ST2 设置 这样我就可以执行 sublime file txt 它将在 ST2 窗口中打开 但是我怎样才能让它在当前打开的窗口的新选项卡中打开呢 尝试 Sublime 命令行帮助 subl 帮助 Sublime Text 2 内部版
  • 终止 ssh 会话会终止正在运行的进程

    我正在使用 ssh 连接到我的 ubuntu 服务器 我使用命令启动编码程序 然而 似乎当我的 ssh 会话关闭时 因为我在进入睡眠状态的笔记本电脑上启动它 有没有办法避免这种情况 当然 阻止我的笔记本电脑休眠并不是永久的解决方案 运行你的
  • gnutls_handshake() 失败:握手失败 GIT

    一切都工作正常 但突然我收到错误 致命 无法访问 https 电子邮件受保护 cdn cgi l email protection name repo name git gnutls handshake 失败 握手失败 我在我的计算机和 E
  • 如何从 swagger 文档生成静态 html 文件?

    我创建了一个 Swagger 文档yaml文件位于 api swagger swagger yaml 现在我想分享一个静态 HTML 文档及其定义 但它已在招摇项目 https github com swagger api swagger
  • 第一次如何配置postgresql?

    我刚刚安装了 postgresql 并在安装过程中指定了密码 x 当我尝试做的时候createdb并指定我收到消息的任何密码 createdb 无法连接到数据库 postgres 致命 用户密码身份验证失败 同样适用于createuser
  • 在 Windows 下使用 linux 实用程序的最佳方法是什么? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Linux 实用程序 如 sed awk 和其他 shell 脚本功能 非常棒 但当我在 Windows 上进行开发并且无法使用其中任何一
  • 串口读取未完成

    下面的函数用于在Linux下从串口读取数据 我在调试时可以读取完整的数据 但是当我启动程序时 读缓冲区似乎并不完整 我正确接收了一小部分数据 但缓冲区的其余部分完全正确zero 可能是什么问题呢 int8 t serial port ope
  • 如何像C99一样使用make和编译?

    我正在尝试使用 Makefile 编译 Linux 内核模块 obj m main o all make C lib modules shell uname r build M PWD modules clean make C lib mo
  • 如何在 Linux 中制作一个将文件转换为大写的 x86 汇编程序?

    我找到了一个名为 ProgrammingGroundUp 1 0 booksize pdf 的 pdf 文件 其中一个项目是制作一个汇编程序 该程序接收文件并将其转换为大写 section data CONSTANTS system cal
  • 如何在 .zip 文件中使用 grep

    有 3 个文件 a csv b csv c csv 压缩为 abh zip 现在可以在 abh zip 上执行 grep 命令 是否有任何通配符 仅对里面的 c csv 文件运行 grep压缩 如果你有zipgrep 据我所知 它是随zip
  • 是否可以为我的 Linux 函数复制命令的制表符补全?

    假设我有一个名为的 bash shell 函数magic 我想定义一个制表符补全功能 magic这将允许magic搭载任何给定命令的选项卡完成功能 如果可用 换句话说 我想要magic能够做这样的事情 magic git
  • 为什么 ls -l 中的“总计”加起来不等于列出的总文件大小? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 为什么是total在输出中ls l打印为64并不是26078列出的所有文件的总数是多少 ls l test ls total 64 rw
  • 在linux中将包含word的行从一个文件复制到另一个文件

    我想复制包含某些单词的行file1 to file2 Suppose file1 ram 100 ct 50 gopal 200 bc 40 ravi 50 ct 40 krishna 200 ct 100 file2应该只有包含 ct 的
  • 检测后台操作

    在C中 检测程序在 后台模式 下调用的方法是什么 我有一个程序想要以交互方式或在后台启动 我怎样才能检测到我不应该从标准输入读取并以 已停止 tty 输入 状态结束 我应该测试 stdin 是否已关闭 我怎样才能做到这一点 编辑 isatt
  • 在 Windows 下对 Unix 下创建的文件使用 fstream::seekg

    我有一个C 跨平台程序 在Linux下用g 编译 在PC下用Visual Studio编译 该程序将行写入文本文件 使用 lt lt 运算符和std endl 但也可以从生成的文本文件中读回数据 使用std getline 为了优化数据访问
  • Linux 从设备本身运行的脚本卸载设备

    我在路径中安装了一个 iso 映像 mnt iso 在这个 iso 中我有一个安装脚本 install sh 我从 iso 运行安装脚本 最后脚本询问用户是否要卸载 iso 本身 如果用户按 y 脚本将执行以下代码 cd umount mn

随机推荐

  • linux-can编程(一)

    建立can的socket int can create socket char name int fd struct sockaddr can addr struct ifreq ifr socklen t len 61 sizeof ad
  • RTKLIB软件介绍

    转载自南京信息工程大学遥感与测绘工程学院的赵乐文老师的网课 xff08 来源 xff1a B站 xff09 开源GNSS数据处理软件介绍 RTKLIB下载 xff1a http www rtklib comCSDN里的资源 xff1a 快速
  • 将rtklib移植到树莓派

    在Raspberry Pi上构建RTKLIB CUI 材料 xff1a 树莓派4b xff0c NEO M8T 配置参考 这次实验主要相对RTKLIB进行尝试 xff0c 将rtkrcv移植到树莓派上 构建RTKRCV xff08 生成RT
  • 开发中常用的快捷键

    前言 常见的快捷键可以帮助程序员脱离鼠标的束缚 xff0c 提升效率 当然 xff0c 最重要的是可以装逼 Keyboard shortcuts I often use Windows Ctrl 43 Number ALT 43 Esc 使
  • 顶级程序员都在用哪些网站?

    前言 在这里给大家分享一点非常高质量的学习资料和工具网站 欢迎大家留下精彩的网站 xff0c 也可以是生活的 商业的等等 目录 高质量免费在线书籍高质量算法网站开源社区网站在线工具源码查找容器化技术 还有很多 将不定期更新 高质量免费在线书
  • ❤️Python是一个业余的编程语言吗?❤️

    x1f47e 前言 作为一个在python国内还很小众的时候就开始玩Python了 我知道有大佬是2013年就玩的很溜了 xff0c 麻烦让我装个逼 不过Python正渐渐退出了我的主力语言 xff0c golang c rust可能是未来
  • 一文讲清SSL协议

    OSI七层模型 计算机网络的OSI七层模型和TCP IP四层模型想必大家都知道 其中SSL TLS是一种介与于传输层 xff08 比如TCP IP xff09 和应用层 xff08 比如HTTP xff09 的协议 它通过 34 握手协议
  • 闭包详解(Python为例)

    不能简单讲 xff0c 这就要看一些底层的东西 堆栈结构等等 xff0c 估计还和编译原理有关 xff0c 我觉得重点在于延迟绑定怎么知道绑定的外层函数的局部变量 python的闭包是延迟绑定 什么是闭包 出现函数嵌套 即外层函数嵌套内层函
  • c++中函数只声明,不定义(不调用该函数)可以通过编译并运行

    c 43 43 中函数只声明 xff0c 不定义 xff0c 代码中也没有该函数的调用 xff0c 可以编译并运行 xff08 vs2015 xff09 Author gtkiller Date 2018 03 19 include lt
  • RMI的基础原理

    背景 上世纪90年代 焦点转移到跨平台通信 一台计算机可以通过某种类型网络在另一台计算机上发起一个动作 CORBA DCOM Java RMI 技术等等 xff0c 到现在的grpc等 Hello span class token keyw
  • 一次因修改Python编码规范而引起的“血案“

    简单来说就是因为如下代码 导致了ZipOutputPath的一个函数异常 导致zip包不能正确下载 有如下报错 UnboundLocalError local variable 39 val 39 referenced before ass
  • Python之禅(The Zen of Python)源码分析

    简介 非常高兴大家能够订阅这个专栏 在这里我将会给大家分享一些Python相关源码的剖析 在接下来的这段日子里 我会一同带各位pythonista探索Python的奥秘 该从什么开始 思来想去 我觉得作为一个Python初学者还是Pytho
  • Linux常用命令速查表

    目录即文件 dev null 等价于只写文件 所有写入它的内容都会永远丢失 而尝试从它那儿读取内容则什么也读不到 禁止标准输出 cat filename gt dev null 文件内容丢失 xff0c 而不会输出到标准输出 禁止标准错误
  • git常用命令速查表

    这是一些比较常用的命令 大家可以复制后用typora做成pdf格式 xff0c 方便快速查询 后续不定期更新 官方文档 官方文档 https git scm com docs 本地文档 file D cmder vendor git for
  • python开发中的常用命令

    这是一些比较常用的命令 大家可以复制后用typora做成pdf格式 xff0c 方便快速查询 后续不定期更新 pip升级 pip package installer for python use pip to install package
  • Python手写一个Base64编解码工具

    这里我们只实现标准的base64 补充位用 61 填充 编码 下面是base64字符的对照表 因为base64编码是将6bit表示成8bit 所以在原来的基础上会增长1 3 另外2 6 61 64 xff0c 这也是为什么这个表会有64个索
  • Python彩蛋源码分析(二)

    简介 非常高兴大家能够订阅这个专栏 在这里我将会给大家分享一些Python相关源码的剖析 在接下来的这段日子里 我会一同带各位pythonista探索Python的奥秘 hello world hello world hello pytho
  • Python源码剖析专栏总览

    简介 非常高兴大家能够订阅这个专栏 在这里我将会给大家分享一些Python相关源码的剖析 在接下来的这段日子里 我会一同带各位pythonista探索Python的奥秘 将会分析一些源码的构思设计以及这些工具的使用方法 彩蛋篇 Easter
  • maven笔记小抄

    settings xml settings xml位置 标签指定本地下载的依赖在本地的保存位置 user home m2 repository表示C Users username m2 settings xml idea中File Sett
  • linux 下 tcpdump 详解 后篇(自己实现抓包过滤)

    一 概述 在了解了tcpdump的原理后 xff0c 你有没有想过自己去实现抓包过滤 xff1f 可能你脑子里有个大概的思路 xff0c 但是知道了理论知识 xff0c 其实并不能代表你完全的理解 只要运用后 xff0c 你才知道哪些点需要