2024-03-01-Linux高级网络编程(6-原始套接字)
6. 原始套接字
6.1 TCP UDP回顾
数据报式套接字(SOCK_DGRAM)
- 无连接的 socket,针对无连接的 UDP 服务
- 可通过邮件模型来进行对比
流式套接字(SOCK_STREAM)
- 面向连接的 socket,针对面向连接的 TCP 服务
- 可通过电话模型来进行对比
这两类套接字似乎涵盖了 TCP/IP 应用的全部
TCP 与 UDP 各自有独立的 port ,互不影响。
一个进程可同时拥有多个 port不必关心TCP/IP协议实现的过程。
6.1.1 UDP编程回顾
client
- 创建 socket 接口
- 定义 sockaddr_in 变量,其中ip、port 为目的主机的信息
- 可发送0长度的数据包
server
- bind 本地主机的 ip、port 等信息
- 接收到的数据包中包含来源主机的ip、port 信息
6.1.2 TCP编程回顾
client
1. connect来建立连接 2. send、recv收发数据 3. 不可发送0长度的数据
server
- bind 本地主机的 IP、PORT 等信息
- listen 把主动套接字变为被动
- accept 会有新的返回值
- 多进程、线程完成并发
6.2 原始套接字概述、创建
6.2.1 原始套接字概述
原始套接宇(SOCK RAW):
- 一种不同于 SOCK STREAM、SOCK DGRAM 的套接字,它实现于系统核心
- 可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用
- 开发人员可发送自己组装的数据包到网络上
- 广泛应用于高级网络编程
- 网络专家、黑客通常会用此来编写奇特的网络程序
流式套接字只能收发TCP 协议的数据,数据报套接字只能收发UDP 协议的数据
原始套接字可以收发任意数据。
6.2.2 创建原始套接字
#include <sys/types.n> #include <sys/socket.h> int socket(int domain, int type, int protocol); 功能:创建套接字,返回文件描述符; 参数: domain:通信域,地址族 AF PACKET type:套接字类型 SOCK_RAW protocol:附加协议 #include <netinet/ether.h> ETH_P_ALL 所有协议对应的数据包 ETH_P_IP ip数据包 ETH_P_ARP arp数据包 返回值: 成功:文件描述符 失败:-1
6.2.3 创建原始套接字
#include <sys/socket.h> #include <sys/types.h> //socket #include <netinet/ether.h> //ETH P_ALL #include <unistd.h> //close #include <stdlib.h> //exit #include <stdio.h> //printf int main(int argc, char const *argv[]) { int socketfd; if ((socketfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1) // 需要转换字节序 htons { perror("fail to sockerfd"); exit(1); } printf("socketfd = %d\n", socketfd); close(socketfd); return 0; }
// 因为原始套接字调用底层的内容,所以需要调用root权限 root@ubuntu:/home/spider/C/12原始套接字/output# ./01_socket_raw socketfd = 3
注意:原始套接字的代码运行时需要管理员权限
6.3 数据包详解
6.3.1 UDP封包格式

UDP报文格式
- 源端口号: 发送方端口号
- 目的端口号: 接收方端口号
- 长度: UDP用户数据报的长度,最小值是 8 字节 (仅有首部 )
- 校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃
6.3.2 IP报文格式
-
版本:IPV4协议的版本
-
首部长度:单位32位(4字节),首部总长度是20字节,所以这里取5
-
服务类型:一般不用,取0;前3位:优先级;第4~7位:延时、吞吐量、可靠性和花费;第8位保留
-
总长度:指首部加上数据的总长度,单位为字节。最大长度为65535字节
-
标识(identification):用来标识主机发送的每一份数据报。IP软件在存储器中维持一个计数器,每产生一个数据报,计数器就加1,并将此值赋给标识字段
-
标志(flag):目前只有两位有意义。
标志字段中的最低位记为MF。MF=1即表示后面“还有分片”的数据报。MF=0表示这已是若干数据报片中的最后一个。
标志字段中间的一位记为DF,意思是“不能分片”,只有当DF=0时才允许分片
-
片偏移:指出较长的分组在分片后,某片在源分组中的相对位置,也就是说,相对于用户数据段的起点,该片从何处开始。片偏移以8字节为偏移单位。
-
生存时间:TTL,表明是数据报在网络中的寿命,即为“跳数限制”,由发出数据服的源点设置这个字段。路由器在转发数据之前就把TTL值减一,当TTL值减为零时,就丢弃这个数据报。通常设置为
32、64、128
。 -
协议:指出此数据报携带的数据时使用何种协议,以便使目的主机的IP层知道应将数据部分上交给哪个处理过程,常用的
ICMP(1) IGMP(2),TCP(6),UDP(17),IPv6(41)
-
首部校验和:只校验数据报的首部,不包括数据部分
-
源地址:发送方P地址
-
目的地址:接收方IP地址
-
选项:用来定义一些任选项;如记录路径、时间戳等。这些选项很少被使用,同时并不是所有主机和路由器都支持这些选项。一般忽略不计。
6.3.3 Ethernet报文格式

- 目的地址: 目的mac地址
- 源地址: 源mac地址
- 类型: IP数据报(0x0800)、ARP数据报(0x0806)、RARP(0x8035)
- 数据:数据根据类型来确定
- CRC、PAD 在组包时可以忽略
- FCS
CRC即循环冗余校验码:是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定。循环冗余检査是一种数据传输检错功能,对数据进行h多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。
6.3.4 TCP报文格式
- 源端口号:发送方端口号
- 目的端口号:接收方端口号
- 序列号:本报文段的数据的第一个字节的序号
- 确认序号:期望收到对方下一个报文段的第一个数据字节的序号
- 首部长度(数据偏移):TCP报文段的数据起始处距离TCP报文段的起始处有多远,即首部长度。单位:32位,即以4字节为计算单位。
- 保留:占6位,保留为今后使用,目前应置为0
- 紧急URG:此位置1,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送
- 确认ACK:仅当ACK=1时确认号字段才有效,TCP规定,在连接建立后所有传达的报文段都必须把ACK置1
- 推送PSH:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作,这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去,接收方收到PSH=1的报文段,就尽快地(即“推送”向前)交付给接收应用进程,而不再等到整个缓存都填满后再向上交付
- 复位RST:用于复位相应的TCP连接11.同步SYN:仅在三次握手建立TCP连接时有效。当SYN=1而ACK=0时,表明这是一个连接请求报文段对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1.因此,SYN置1就表示这是一个连接请求或连接接受报文
- 同步SYN:仅在三次握手建立TCP连接时有效。当SYN=1而ACK=0时,表明这是一个连接请求报文段,对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1.因此,SYN置1就表示这是一个连接请求或连接接受报文
- 终止FIN:用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接。
- 窗口:指发送本报文段的一方的接收窗口(而不是自己的发送窗口)14.校验和:校验和字段检验的范围包括首部和数据两部分,在计算校验和时需要加上12字节的伪头部
- 校验和:校验和字段检验的范围包括首部和数据两部分,在计算校验和时需要加上12字节的伪头部
- 紧急指针:仅在URC=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据),即指出了紧急数据的末尾在报文中的位置,注意:即使窗口为零时也可发送紧急数据
- 选项:长度可变,最长可达40字节,当没有使用选项时,TCP首部长度是20字节
6.3.5 ICMP报文格式
注意:不同类型值以及代码值,代表不同的功能
#include <sys/socket.h> #include <sys/types.h> //socket #include <netinet/ether.h> //ETH P ALL #include <unistd.h> //close #include <stdlib.h> //exit #include <stdio.h> //printf #include <arpa/inet.h> //htons int main(int argc, char const *argv[]) { int sockfd; if ((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) { perror("fail to sockfd"); exit(1); } unsigned char msg[1600] = ""; while (1) { if (recvfrom(sockfd, msg, sizeof(msg), 0, NULL, NULL) < 0) { perror("fail to recvfrom"); exit(1); } // 分析接收到的数据包 unsigned char dst_mac[18] = ""; unsigned char src_mac[18] = ""; unsigned short type; // 接收数据存放到dst_mac中 sprintf(dst_mac, "%x:%x:%x:%x:%x:%x", msg[0], msg[1], msg[2], msg[3], msg[4], msg[5]); sprintf(dst_mac, "%x:%x:%x:%x:%x:%x", msg[6], msg[7], msg[8], msg[9], msg[10], msg[11]); type = ntohs(*(unsigned short *)(msg + 12)); printf("源mac%s ---> 目的mac:%s \n", src_mac, dst_mac); printf("type = %#x\n", type); } return 0; }
输出结果
root@ubuntu:/home/spider/C/12原始套接字/output# ./02_raw_recv_mac 源mac ---> 目的mac:0:50:56:c0:0:8 type = 0x806 源mac ---> 目的mac:0:50:56:c0:0:8 type = 0x806 源mac ---> 目的mac:0:50:56:c0:0:8 源mac ---> 目的mac:0:c:29:76:6d:89 type = 0x800 源mac ---> 目的mac:0:50:56:ff:4f:d type = 0x800 源mac ---> 目的mac:0:50:56:ff:4f:d type = 0x800
6.3.6 ARP报文格式
- 目标MAC:目的MAC地址
- 源MAC:源MAC地址
- 帧类型: 0x0806
- 硬件类型: 1 (以太网)
- 协议类型:0x0800(IP地址)
- 硬件地址长度:6
- 协议地址长度:4
- 操作代码: 1(ARP请求),2(ARP应答),3(RARP请求),4(RARP应答)
6.3.7 混杂模式
混杂模式
-
指一台机器的网卡能够接收所有经过它的数据包,而不论其目的地址是否是它。
-
一般计算机网卡都工作在非混杂模式下,如果设置网卡为混杂模式需要root权限
linux 下设置
// 设置混杂模式 ifconfig eth0 promisc // 取消混杂模式 ifconfig eth0 -promisc
6.4 sendto发送数据
6.4.1 sendto发送原始套接字数据
sendto(sock raw fd, msg, msg_len, 0,(struct sockaddr*)&sll,sizeof(sll)); 注意: 1. sock_raw_fd:原始套接字 2. msg:发送的消息(封装好的协议数据) 3. sll:本机网络接口,指发送的数据应该从本机的哪个网卡出去,而不是以前的目的地址
6.4.2 本机网络接口
#include <netpacket/packet.h> struct sockaddr_ll{ unsigned short int sll family; //一般为PF_PACKET unsigned short int s1l_protocol; // 上层协议 int sll_ifindex; // 接口类型 unsigned short int sll_hatype; // 报头类型 unsigned char sll_pkttype; // 包类型 unsigned char sll_halen; // 地址长度 unsigned char sll_addr[8];// MAc地址 }; // 只需要对sll.sll_ifindex赋值就可以使用
struct sockaddr_ll sll; bzero(&s11,sizeof(s11)); s1l.sll ifindex = ???; // 赋值获取到当前要出去的网络接口地址 if(sendto(sockfd,msg,sizeof(msg),0,(struct sockaddr *)&sl1, sizeof(s11)==-1){ perror("fail to sendto"); exit(1); }
6.4.3 获取网络接口地址 - ioctl
Linux中的ioctl函数是与内核交互的一种方法, 在驱动和网络中的使用都非常的广泛。
#include <sys/ioctl.h> int ioctl(int d, int request, ...); 参数: d: 表示要操作的文件描述符 request: 表示ioctl函数的操作选项,不同的选项具有不同的功能。 第三参数,是一个void*的指针类型,要根据request参数来决定。
int main(int argc, const char *argv[]) { int sockfd; struct ifreq req; char buf[32] = {0}; int i = 0; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("fail to create socket ..!"); exit(1); } strcpy(req.ifr_ifrn.ifrn_name, "eth0"); /* *获得eth0的MAC地址 */ if (ioctl(sockfd, SIOCGIFHWADDR, &req) < 0) { perror("fail to ioctl "); close(sockfd); exit(1); } close(sockfd); memcpy(buf, req.ifr_ifru.ifru_hwaddr.sa_data, 6); //strncpy(buf, req.ifr_ifru.ifru_hwaddr.sa_data, 6); /* *按照xx:xx:xx:xx:xx:xx的格式显示 */ for (i = 0; i < 6; i++){ printf("%02x:", buf[i] & 0xff); puts("\b ");///后输出不带":" } return 0; }
6.5 练习 - MAC地址扫描器
如果A(192.168.1.1
)向B(192.168.1.2
)发送一个数据包,那么需要的条件有ip、port
、使用的协议(TCP/UDP)之外还需要MAC地址。因为在以太网数据包中MAC地址是必须要有的。问:怎样才能知道对方的MAC地址?使用什么协议呢?
ARP(Address Resolution Protocol,地址解析协议)
- 是TCP/IP协议族中的一个
- 主要用于查询指定ip所对应的的MAC
- 请求方使用广播来发送请求
- 应答方使用单播来回送数据
- 为了在发送数据的时候提高效率在计算中会有一个ARP缓存表,用来暂时存放ip所对应的MAC,在linux中使用
ARP
即可查看
注意:
当主机A和主机B通信时,会先查看arp表中有没有对方的mac地址;如果有,则直接通信即可如果没有再调用arp协议获取对方mac地址并将其保存在arp表中。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <netinet/if_ether.h> #include <netinet/ip.h> #include <netinet/ether.h> #include <netpacket/packet.h> #define BUFFER_SIZE 1024 int main() { int sockfd; struct sockaddr_ll sa; struct ifreq ifr; char interface[] = "ens33"; // 替换为你的网络接口名称 // 创建原始套接字 sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (sockfd == -1) { perror("Failed to create socket"); exit(EXIT_FAILURE); } // 获取接口索引 memset(&ifr, 0, sizeof(struct ifreq)); strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name) - 1); if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1) { perror("Failed to get interface index"); close(sockfd); exit(EXIT_FAILURE); } // 设置套接字地址 memset(&sa, 0, sizeof(struct sockaddr_ll)); sa.sll_family = AF_PACKET; sa.sll_protocol = htons(ETH_P_ALL); sa.sll_ifindex = ifr.ifr_ifindex; if (bind(sockfd, (struct sockaddr *)&sa, sizeof(struct sockaddr_ll)) == -1) { perror("Failed to bind socket"); close(sockfd); exit(EXIT_FAILURE); } // 构建自定义报文 unsigned char src_mac[6] = {0x00, 0x0c, 0x29, 0x76, 0x6d, 0x89}; // 源MAC地址 unsigned char dst_mac[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; // 广播MAC地址 unsigned char target_ip[4] = {192, 168, 146, 1}; // 目标IP地址 unsigned char source_ip[4] = {192, 168, 146, 128}; // 本地IP地址 unsigned char packet[BUFFER_SIZE]; memset(packet, 0, BUFFER_SIZE); // 以太网头部 struct ether_header *eth_header = (struct ether_header *)packet; memcpy(eth_header->ether_shost, src_mac, 6); memcpy(eth_header->ether_dhost, dst_mac, 6); eth_header->ether_type = htons(ETHERTYPE_ARP); // ARP头部 struct ether_arp *arp_header = (struct ether_arp *)(packet + sizeof(struct ether_header)); arp_header->ea_hdr.ar_hrd = htons(ARPHRD_ETHER); arp_header->ea_hdr.ar_pro = htons(ETH_P_IP); arp_header->ea_hdr.ar_hln = 6; arp_header->ea_hdr.ar_pln = 4; arp_header->ea_hdr.ar_op = htons(ARPOP_REQUEST); memcpy(arp_header->arp_sha, src_mac, 6); memset(arp_header->arp_tha, 0, 6); memcpy(arp_header->arp_spa, source_ip, 4); memcpy(arp_header->arp_tpa, target_ip, 4); // 发送报文 if (sendto(sockfd, packet, sizeof(struct ether_header) + sizeof(struct ether_arp), 0, (struct sockaddr *)&sa, sizeof(struct sockaddr_ll)) == -1) { perror("Failed to send packet"); close(sockfd); exit(EXIT_FAILURE); } // 接收响应报文 unsigned char recv_buffer[BUFFER_SIZE]; int num_bytes = recv(sockfd, recv_buffer, BUFFER_SIZE, 0); if (num_bytes == -1) { perror("Failed to receive packet"); close(sockfd); exit(EXIT_FAILURE); } // 解析响应报文 struct ether_header *recv_eth_header = (struct ether_header *)recv_buffer; struct ether_arp *recv_arp_header = (struct ether_arp *)(recv_buffer + sizeof(struct ether_header)); // 提取目标主机的MAC地址 unsigned char *target_mac = recv_arp_header->arp_sha; // 打印目标主机的MAC地址 printf("Target MAC Address: %02x:%02x:%02x:%02x:%02x:%02x\n", target_mac[0], target_mac[1], target_mac[2], target_mac[3], target_mac[4], target_mac[5]); // 关闭套接字 close(sockfd); return 0; }
输出结果
本文来自博客园,作者:Yasuo_Hasaki,转载请注明原文链接:https://www.cnblogs.com/hasaki-yasuo/p/18047417
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步