一个小程序:
//发送方
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <time.h>
#include <sys/select.h>
struct mydata {
char data[12];
};
int main(int argc, char ** argv) {
struct mydata senddata;
strcpy(senddata.data, "hello world");
int sockfd;
struct sockaddr_in addr;
socklen_t addr_len = sizeof(addr);
bzero(&addr, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &addr.sin_addr);
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
char sendbuf[] = "hello world";
int sendbuf_len = sendto(sockfd, (void *)&senddata, sizeof(struct mydata), 0, (struct sockaddr *)&addr, addr_len);
printf("send len = %d\n", sendbuf_len);
close(sockfd);
}
//接受方
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <time.h>
#include <sys/select.h>
void proc_v4(char *);
struct mydata {
char data[12];
};
int main(int argc, char ** argv) {
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
char readbuf[100];
int ipv = atoi(argv[1]);
int len = read(sockfd, readbuf, ipv);
printf("len = %d\n", len);
proc_v4(readbuf);
close(sockfd);
}
void proc_v4(char * package) {
struct mydata * content;
struct ip * ip = (struct ip *)package;
struct in_addr from= ip->ip_src;
printf("from %s: ", inet_ntoa(from));
content = (struct mydata *)(package + sizeof(struct ip));
printf("%s\n", content->data);
}
原本网上有一些用原始套接字的例子,但我找了很久只有2个版本,一个是用原始套接字发送ICMP包,另一个则是发送TCP包(带有攻击性质)。发送ICMP包,指定的服务类型为ECHO回显服务,而这是由内核提供的,也就是说我们必须要自己来写接受方,只要发送出去即可。TCP例子则是不停的向同一台机器发送请求连接的SYN包。缺点是:本来学习的主题是原始套接口,而这2个例子还要带上对TCP和ICMP的学习。(笔者太笨了,写了很久也没有让ICMP回显,可能是ICMP头部写的有问题,包被主机忽略了)。
我这里依然采用的是ICMP协议,但我没有自己写ICMP包头,只是简单把要传递的信息放入了ICMP包中,内核依然会将其送入接受的套接口中。
1.原始套接口创建
sockfd = socket(AF_INET, SOCK_RAW, 86);
第一个参数很熟悉了,表示IPV4,SOCK_RAW表示原始套接字,最后的86是我乱写。第3个参数可选值很多IPPROTO_ICMP或者IPPROTO_TCP。如果是IPPROTO_ICMP表示通过这个原始套接字发送的IP包中带的数据将会是ICMP包,依次理解IPPROTO_TCP则表示带为TCP包。而86则是内核没有支持的协议或者说内核不认识的协议。这些将会影响接受方内核对IP数据包的处理。
2.原始套接口输出
端口对原始套接口是没有意义的,一般使用sendto或者sendmsg发送,当然可以调用connect的原始套接字,则可以直接使用write。超过MTU的会被分片。
3.原始套接口的输入
回忆一下TCP和UDP,他们都是通过端口号和地址来找到接受进程。而原始套接口则不同。它遵循以下几个规则:
a.如果发送方创建时候使用的socket(AF_INET,SOCK_RAW, IPPROTO_TCP),也就是从这个套接口出来的为TCP包,或者UDP包不会传递给任何原始套接口,他们将直接递交给TCP或者UDP协议处理。所以要想抓TCP或者UDP包,用原始套接口无法实现。
b.如果发送的是ICMP数据,除了回射,时间戳,地址掩码请求完全有内核处理,所有接受到的都将传递给某个原始套接口。(我理解是只要你创建了原始套接字,则这个套接字就会收到这个包的一个拷贝)。
c.如果是IGMP数据,所有IGMP分组将会传递某个原始套接口。
d.不能识别的协议字段,例如我上面的86,都将传递某个原始套接口。
e.被分片的包会重组再按照abcd处理。
f.ip包会进行版本,头部校验和,头部长度以及目的地址检查。检查不合格者会丢弃。
4.套接口选项IP_HDRINCL
上面程序比较简单,我没有打开这个选项,如果打开这个选项,则你写入的数据会从IP包头部开始,也就是说你需要写IP头部。
一个打开了IP_HDRINCL的原始套接字样例,接受方仍然可以采用原来的程序,这个程序修改了发送者的IP
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <time.h>
#include <sys/select.h>
//计算IP头部校验和的算法
unsigned short csum(unsigned short * buf, int nwords) {
unsigned long sum;
for(sum = 0; nwords > 0; nwords--)
sum += *buf++;
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return (unsigned short)(~sum);
}
struct mydata {
char data[12];
};
int main(int argc, char ** argv) {
char buf[400];
struct ip * ip_head = (struct ip *)buf;
struct mydata * senddata = (struct mydata *)(buf + sizeof(struct ip));
int one = 1;
const int * val = &one;
memset(buf, 0, 400);
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
bzero(&addr, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &addr.sin_addr);
//init ip head
ip_head->ip_hl = 5; //头部长度
ip_head->ip_v = 4;//ip版本号
ip_head->ip_tos = 16;
ip_head->ip_p = IPPROTO_ICMP; //服务类型
inet_pton(AF_INET, argv[1], &ip_head->ip_dst); //指定目的IP地址
inet_pton(AF_INET, "192.168.1.23", &ip_head->ip_src); //指定源IP地址,这里可以随便指定,不一定要是本机IP地址,当然如果不是本机地址,对方获得的就是错误地址,原始套接口没有确认包返回,所以不会有影响,但如果期望对方返回信息的话,就要填自己的IP地址。
strcpy(senddata->data, "hello world");
setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, val, sizeof(one));
ip_head->ip_sum = csum((unsigned short *) buf, sizeof(struct ip) + sizeof(struct mydata));//计算校验和
sendto(sockfd, buf, ip_head->ip_len, 0, (struct sockaddr *)&addr, sizeof(addr));
close(sockfd);
exit(0);
}