针对网络包,我们一般的发送接收直接使用的是应用层,此时无法分辨接收为广播包还是单播包,为了能够分辨出接收到的是否为广播包,需要接收数据链路层的数据或者网络层的数据。
原理啥的就不复制了,可看参考链接,也可自行搜索。。。------ 》》》
直接上正题
我们正常的 udp 通信如下:
int fd = socket(AF_INET, SOCK_DGRAM|SOCK_NONBLOCK, 0);
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr.s_addr = INADDR_ANY;
bind(fd, (struct sockaddr *) &sin, sizeof(sin));
需要获取网络层数据,需要使用抓包过滤规则
setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
bpf 为定义在
中
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter __user *filter;
};
生成过滤规则可使用 tcpdump 命令,如下:
$ sudo tcpdump udp and dst port 8888 -d
(000) ldh [12] //接收包的第12字节处,16位
(001) jeq #0x86dd jt 2 jf 6 //判断第12字节处是否等于0x86dd,满足跳到(002),不满足跳到(006)行
(002) ldb [20]
(003) jeq #0x11 jt 4 jf 15
(004) ldh [56]
(005) jeq #0x22b8 jt 14 jf 15
(006) jeq #0x800 jt 7 jf 15 //判断第12字节处是否等于0x800,满足跳到(007),不满足跳到(015)行
(007) ldb [23] //接收包的第23字节处,8位
(008) jeq #0x11 jt 9 jf 15 //判断第23字节处是否等于0x11,满足跳到(009),不满足跳到(015)行
(009) ldh [20] //接收包的第20字节处,16位
(010) jset #0x1fff jt 15 jf 11
(011) ldxb 4*([14]&0xf) //接收包的第14字节处,8位,取低四位,然后乘4,抓包看此处一般为 0x45,计算得 20
(012) ldh [x + 16] //接收包的第x+16字节处,16位,x在上面计算得20,故为第36字节处
(013) jeq #0x22b8 jt 14 jf 15 //判断第36字节处是否等于0x22b8,满足跳到(014),不满足跳到(015)行
(014) ret #262144 //满足,显示
(015) ret #0 //不满足,过滤
上面生成的主要是数据链路层起始数据,而区分广播包,获取网络层数据即可,上面生成过于复杂,可简化判断接收数据端口号,再加上网络层第一二字节判断
{ 0x28, 0, 0, 0x00000000 },
{ 0x15, 0, 3, 0x00004500 },
{ 0x28, 0, 0, 0x0000016 },
{ 0x15, 0, 1, 0x000022B8 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
在使用的过程中,发现:若只使用 SOCK_RAW 获取网络层包,则会出现端口不可达
ICMP Destination unreachable (Port unreachable)
故而推测 socket(AF_INET, SOCK_RAW|SOCK_NONBLOCK, IPPROTO_UDP )
使用时,不会打开端口,但数据收发也都正常,就是多了一条 ICMP,故而本文做法,打开所在端口,就不会出现端口不可达了,SOCK_RAW主要用于报文的抓取,在内核获取到报文后,会复制一份给 SOCK_RAW 套接字,另一个打开所在端口也可正常接收,例子如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include // sockaddr_ll
#include
#include
#include
#include
int main(int argc, char *argv[])
{
//内核会把数据给每一个socket拷贝一份
uint8_t buf[1024] = { 0 };
uint8_t buf2[1024] = { 0 };
int len = 0,len2 = 0;
int fd, fd2;
struct sockaddr_in client_addr, client_addr2;
socklen_t addrlen = sizeof(client_addr);
//fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP));
fd = socket(AF_INET, SOCK_RAW|SOCK_NONBLOCK, IPPROTO_UDP );
struct sock_filter bpf_code[] = {
{ 0x28, 0, 0, 0x00000000 },
{ 0x15, 0, 3, 0x00004500 },
{ 0x28, 0, 0, 0x0000016 },
{ 0x15, 0, 1, 0x000022B8 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
};
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));
}
/*
如果IP_HDRINCL未开启,由进程让内核发送的数据是从IP首部之后的第一个字节开始的,内核会自动构造合适的IP
如果IP_HDRINGL开启,进行需要自行构造IP包
*/
/*
int one = 1;
const int *val = &one;
if (setsockopt(fd, IPPROTO_IP, IP_HDRINCL, val, sizeof(int))) {
perror("setsockopt() error");
exit(-1);
}
*/
fd2 = socket(AF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_UDP);
struct sockaddr_in addrServ;
addrServ.sin_addr.s_addr= htonl(0);//指定0.0.0.0地址,表示任意地址0xC0A8012e
addrServ.sin_family = AF_INET;//表示IPv4的套接字类型
addrServ.sin_port = htons(8888);
bind(fd2, (struct sockaddr*)&addrServ, sizeof(addrServ));
while (1) {
if ((len = recvfrom(fd, buf, sizeof(buf), 0,(struct sockaddr *)&client_addr, &addrlen)) == -1){
//printf("recvfrom failed ! error message : %s\n", strerror(errno));
continue;
}
if ((len2 = recvfrom(fd2, buf2, sizeof(buf2), 0, (struct sockaddr *)&client_addr2, &addrlen)) == -1){
printf("recvfrom failed ! error message : %d %s\n", errno, strerror(errno));
}
if(len){
printf("%s port: %d, len: %d, data: %x %x %x %x \n",inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port), len, buf[0], buf[28], buf[29], buf[30]);
}
if(len2){
printf("%s port: %d, len: %d, data: %x %x %x %x \n",inet_ntoa(client_addr2.sin_addr), htons(client_addr2.sin_port), len2, buf2[0], buf2[1], buf2[2], buf2[3]);
}
if(len){
buf[0]=0x22;
buf[1]=0xB8;
buf[2]=0xaf;
buf[3]=0x13;
buf[4]=0x0;
buf[5]=0x22;
sendto(fd, buf, len, 0, (struct sockaddr *)&client_addr, addrlen);
}
}
return 0;
}
上面程序的接收和发送,都是使用的 Raw Socket 的方式进行,其下为数据收发情况
其上打印,Raw Socket 的方式接收了 48 字节,首字节从0x45开始,其后包含了源 IP、目的 IP 和端口号等,正常数据位于第28字节处;
而正常的 UDP 模式下接收了 20 字节,开始即是 0x68
Raw Socket 的方式发送从端口号开始, 可自行组包,源端口,目的端口,数据长度等,其正常数据从上面的调试助手看,从第 8 字节开始
Raw Socket 比较适用于抓包分析,由于打开套接字不会打开对应端口,故使用时为去除 ICMP 包,需打开正常的端口号,发送可使用正常数据包,无需自行组包。
使用 Raw Socket 方式可解决:区分接收到的是广播数据还是单播数据
这一简单应用。
https://github.com/xgfone/snippet/blob/master/snippet/docs/linux/program/raw-socket.md
http://qiusuoge.com/17205.html
https://www.kernel.org/doc/html/latest/networking/filter.html#networking-filter