给30天自制操作系统编写网卡驱动[7]: 在收到的数据中筛选DHCP信息

如何在自制操作系统写网卡驱动程序(1) 在自制操作系统上写网卡驱动(2): 网卡的I/O配置 在30天自制操作系统上编写网卡驱动「3」:读取网卡的MAC地址 在30天自制操作系统上编写网卡驱动[4]:寄存器控制网卡收发数据 在30天自制操作系统上编写网卡驱动[5]:寄存器控制网卡收发数据 在30天自制操作系统上编写网卡驱动[6]:用缓冲环和中断来接收数据

前几天的教程中,我们完成了对数据的接收,可以把网线上的数据读入 CPU所管理的内存中,即我们声明的数组中。

今天,我们会先发送一个DHCP discover信息到网络上,然后对接收到的数据进行解析,从接收到的众多数据中,筛选出路由器上的DHCP服务器反馈给我们的DHCP offer信息。

注意到:网线上,所传输的任何信息,我们的网卡是都可以收到的,只要我们从网卡的内存中读,都是可以读取到的。

不过,一般我们会在网卡的寄存器中设置:只接收哪些可能发送给自己的信息:信息中携带的目的地mac地址与我们网卡匹配,网卡才会接收。

当然,如果我们要对网络上的数据进行监控,就可以设置网卡的相关寄存器。这样网卡就可以接收到所有的信心了,即使不是发送给自己的信息,都是能够接收到的。这个寄存器就是:RSR

给30天自制操作系统编写网卡驱动[7]:在收到的数据中筛选DHCP信息

RCR中的bit4,PRO,如果将其设置为1,那么只要设置了网卡目的MAC地址的数据,都会被接收。如果将其设置为0,就只能接收发送给我们网卡的MAC地址数据。

所以说,网络信息监控的还是很容易的,做一个sniffer嗅探器的成本不高。

但是,注意到,这里的数据必须有MAC地址字段,如果没有这个字段,网卡还是不接收的。

也就意味着,网卡监控不到没有设置MAC地址字段的那些信息。只要有这个字段就行,至于这个字段具体值是多少,没有关系。

那么下面,我们就开始写代码去发送DHCP discover信息,然后看看我们接收的众多消息中,能不能筛选出DHCP offer类型的信息。这个信息,可能就是路由器上的DHCP服务器对我们discover信息的反馈。并且,我们还要继续给DHCP服务器反馈一个request类型的消息。

其实,我们可以用wireshark去抓取一个DHCP的全过程,这要就能到相关细节了。

抓包查看DHCP全过程

给30天自制操作系统编写网卡驱动[7]:在收到的数据中筛选DHCP信息

如图所示,当我们在命令行输入ipconfig/release,然后再输入ipconfig/renew,就看到wireshark抓取到的4条信息,分别是DHCP Discover,DHCP Offer,DHCP Request,DHCP ACK类型的数据,刚好完成了整个DHCP分配IP地址的全过程。

也就是说,我们今天也要完成这个一个全过程:

  1. 我们控制发送DHCP discover 信息给路由器上的DHCP服务器。
  2. 路由器上的DHCP服务给我们反馈DHCP offer,这个offer里含有IP地址
  3. 我们收到DHCP offer后,再发送一个DHCP request消息给路由器:申请offer里面的IP地址。
  4. 路由器上的DHCP服务收到request后,给我们发送一个DHCP ACK信息,同意我们所申请的IP地址,还会返回 子网掩码,域名服务器等局域网的相关信息。

我们先看看,抓回来的DHCP offer消息。在上图中双击DHCP offer消息,弹出如下所示画面:

给30天自制操作系统编写网卡驱动[7]:在收到的数据中筛选DHCP信息

DHCP offer消息前加了3个协议头:Ethernet II 以太网协议头,Internet Protocol version 4,IPV4协议头,User Datagram Protocal,UDP协议头。这与我们自己写的DHCP discover消息一样。

我们直接去看Dynamic Host Configuration Protocal(Offer),即DHCP Offer消息:

它的Message type 为2,即boot replay,回应消息。DHCP discover的这个值为1。

后面的字段,Hardware type,Hardware address length等,都与我们的discover消息一样。凡事discover有的字段,DHCP Offer都有。只不过,这些字段的值可能不一样。这里的Your (client) IP address字段:10.211.55.6就是服务器发送过来,想要给我们使用的IP地址了。

Offer消息也是有Option字段的,如下所示:

给30天自制操作系统编写网卡驱动[7]:在收到的数据中筛选DHCP信息

Option 字段中DHCP Message Type为2,表示Offer类型。我们等下就需要把Message Type这个字段解析出来,看看是否为2,如果是2,说明这条信息就是我们要寻找的信息,就可以去提取它的Your IP address字段的内容,作为我们想的IP地址向路由器发送DHCP request消息去申请了。

Option的其他字段如下:

给30天自制操作系统编写网卡驱动[7]:在收到的数据中筛选DHCP信息

IP address Lease Time 是1800秒,30分钟

Router,即路由器的IP地址为10.211.55.1

了解了DHCP Offer消息的细节,我们就去写代码去解析他,然后过滤出 Message type为2的信息就行了

对接收到的DHCP offer数据进行解读,解读出IP地址

如果我们把接收的数据都存储在了数组recv中,那么我们可以这样去解析它:

void analysis_struct(FORSHOWINFO *forshow,uchar *recv,uchar *s2,int start_index){  
    // 获取MAC_HEADER在数组recv中的地址
    MAC_HEADER * mac_header = (MAC_HEADER *)(recv+start_index);
    buffshow(mac_header->dest_mac,6);//查看目的MAC地址是否为我们网卡的
    buffshow(&(mac_header->protocal),2);// 获取到mac_header->protocal,看它的协议号是否为0x0800,IPv4
    // 获取IP_HEADER在数组recv中的地址  
    IP_HEADER * ip_header = (IP_HEADER *)(mac_header+1);
    // 获取协议号,看是不是0x11,UDP,因为DHCP offer消息是添加了UDP头的
    buffshow(&(ip_header->protocal),1);
    
    // 获取UPD_HEADER在数组recv中的地址
    UDP_HEADER * udp_header= (UDP_HEADER *)(ip_header +1);
    // 获取端口号,看端口是是否为67,和 68,只有这两个端口号才是DHCP offer消息里的
    buffshow(&(udp_header->src_port),2);
    buffshow(&(udp_header->dest_port),2);
    
    // 获取DHCP_MESSAGE在recv中的地址:
    DHCP_MESSAGE * dhcp_message = (DHCP_MESSAGE *)(udp_header+1);
    // 将dhcp_message中含有的your ip address, message type等信息存入DHCP_INFO结构体中,留着我们做判断
    DHCP_INFO dhcp_info_recv={0,0,0,0,0,0,0,0,0};
    dhcp_options_ana(dhcp_message->options,&dhcp_info_recv);
    // 显示出消息类型:
    uchar s3[20];
    switch(dhcp_info_recv.message_type){
        case 1:
            sprintf(s3,"[DISCOVER]");
            break;
        case 2:
            sprintf(s3,"[OFFER]");
            break;
        case 3:
            sprintf(s3,"[REQUEST]");
            break;
        case 4://DHCP客户端收到DHCP服务器ACK应答报⽂后,通过地址冲突检测发现服务器分配的地址冲突或者由于其他原因导致不能使⽤,则
               //Decline(0x04)
               //会向DHCP服务器发送Decline请求报⽂,通知服务器所分配的IP地址不可⽤,以期获得新的IP地址。
            sprintf(s3,"[DECLINE]");
            break;
        case 5:
            sprintf(s3,"[ACK]");
            break;
        case 6:
            sprintf(s3,"[NAK]");
            break;
        case 7:
            sprintf(s3,"[RELEASSE]");
            break;
        case 8://DHCP客户端如果需要从DHCP服务器端获取更为详细的配置信息,则向DHCP服务器发送Inform请求报⽂;DHCP服务器在收到该
               //Inform(0x08)
               //报⽂后,将根据租约进⾏查找到相应的配置信息后,向DHCP客户端发送ACK应答报⽂。⽬前基本上不⽤了。
            sprintf(s3,"[INFORM]");
            break;

        default:
            sprintf(s3,"[OTHER]");
    }
    //显示DHCP消息中所带的IP地址:
    buffshow(&(dhcp_message->ciaddr),4);
    buffshow(&(dhcp_message->yiaddr),4);
    buffshow(&(dhcp_message->siaddr),4);
    buffshow(&(dhcp_message->giaddr),4);
    //显示DHCP消息中Option字段所含有的信息
    buffshow(&(dhcp_info_recv.server_identifier),4);
    buffshow(&(dhcp_info_recv.router),4);
    buffshow(&(dhcp_info_recv.subnet_mask),4);
    buffshow(&(dhcp_info_recv.domain_name_server),4);
    buffshow(&(dhcp_info_recv.lease_time),4);
    buffshow(&(dhcp_info_recv.length),4);


}

可以看到,有了结构体的帮助,去找到数组中的相关字段,就方便多了。

上面解析函数中所涉及的结构体为:

typedef struct{
	uchar message_type;
	long your_ip_addr;
	int length;
	long server_identifier;
	long lease_time;
	long subnet_mask;
	long router;
	long domain_name_server;
	uchar *domain_name;
} DHCP_INFO;


typedef struct{
    unsigned char op;
    unsigned char htype;
    unsigned char hlen;
    unsigned char hops;
    long xid;
    short secs;
    short flag;
    long ciaddr;
    long yiaddr;
    long siaddr;
    long giaddr;
    unsigned char chaddr[16];
    unsigned char sname[64];
    unsigned char file[128];
    unsigned char magic_cookie[4];
    unsigned char options[44];
    unsigned char end[10];
} DHCP_MESSAGE;

typedef struct{
	short src_port;
	short dest_port;
	short length;
	short check_sum;
} UDP_HEADER;


typedef struct {
	unsigned char ip_and_headlength;
	unsigned char service_level;
	short length;
	short identify;
	short label_and_offset;
	unsigned char    ttl;
	unsigned char protocal;
	short head_checksum;
	long src_ip;
	long dest_ip;
} IP_HEADER;

typedef struct {
	unsigned short pre;
	unsigned char dest_mac[6];
	unsigned char src_mac[6];
	//unsigned char type[5];
	unsigned short protocal;
} MAC_HEADER;

所以的字段的解析中,对option字段的解析是最复杂的,这个解析在函数dhcp_options_ana里:

int dhcp_options_ana(uchar *options,DHCP_INFO * dhcp_info){
    /*
    uchar message_type;
    long client_ip_addr;
    int length;
    long server_identifier;
    long lease_time;
    long subnet_mask;
    long router;
    long domain_name_server;
    uchar *domain_name;
    */
    int i=0;
    int length=1;
    int tmp_len=1;

    for(i=0;i<100;i++){

        switch(*(options+length-1)){
            case 1:// subnet mask
                dhcp_info->subnet_mask=*((long *)(options+length+1));
                break;
            case 2:
                break;
            case 3:// router
                dhcp_info->router=*((long *)(options+length+1));
                break;
            case 4:
                break;
            case 5:
                break;
            case 6:// domain name server
                dhcp_info->domain_name_server=*((long *)(options+length+1));
                break;
            case 15: // domain name
                break;
            case 51: // ip addr lease time
                dhcp_info->lease_time=*((long *)(options+length+1));
                break;
            
            case 53:
                dhcp_info->message_type=*(options+length+1);
                //return dhcp_info->message_type;
                break;
            case 54: // server identifier
                dhcp_info->server_identifier=*((long *)(options+length+1));
                break;
            
            default:
                break;

        }
        tmp_len=*(options+length);
        length=length+tmp_len+2;
        if((*(options+length))==0 && (*(options+length-1))==0xff){
            length=length-2;
            break;
        }

    }
    dhcp_info->length=length+1;
   
    return 0;
}

由于Options字段里是一个可选表格,是一个不完整的表格,其实就是

在30天自制操作系统上编写网卡驱动[4]:寄存器控制网卡收发数据

文章里的不完整表格。

所以,解析它的时候,就只能循环的去用当前字段的长度去判断下一个字段在数组中的位置,从而把所有的可选字段解读出来。

在主程序中,我们只用判断DHCP类型是否为Offer,即message type 是否为2,以及目的MAC地址是否与我们网卡地址匹配,就可以放心的把your ip address里的ip 地址拿去申请了。

代码如下:

                        DHCP_INFO dhcp_info_tmp={3,dhcp_message->yiaddr};
                        MAC_PACKAGE *mac_p_tmp=NULL;
                        int number_tmp=0;
                        switch(dhcp_message.message_type){
                        	case 1:
                        	    break;
                        	
                        	case 2:// 如果message_type的值为2,说明是offer信息,我们需要发送一个DHCP requet类型的消息
                                mac_p_tmp = create_dhcp_pacakge(memman,&(dhcp_info_tmp));
                                number_tmp=send_dhcp_message(((unsigned char * )mac_p_tmp )+2,sizeof(*mac_p_tmp)-2);
                                sprintf(s2,"discove length=%d,package length=%d,send_length=%d",sizeof(DHCP_MESSAGE),sizeof(*mac_p_tmp)-2,number_tmp);
                                strshow(s2);
                                buffshow(((unsigned char * )mac_p_tmp )+2,sizeof(*mac_p_tmp)-2);
                                structana(((unsigned char * )mac_p_tmp ),0);
                                memman_free(memman,&(mac_p_tmp),sizeof(*(mac_p_tmp)));
                                register_win(0,5,0);
                        	    
                        	    break;
                        	
                        	default:
                        	    break;
                        }

代码的第8行,判断了消息类型是2,DHCP OFFER

代码的第9行,构造了一个DHCP REQUEST消息,并给它加上UDP HEADER,IP HEADER,ether II Header.代码的第10行,将新构造的DHCP REQUEST消息发送出去。

那么该如何构造出DHCP REQUEST消息呢?

构造DHCP REQUEST数组回复DHCP 服务:“申请使用这个IP”

构造DHCP消息的函数为create_dhcp_pacakge函数,代码如下:

    MAC_PACKAGE * create_dhcp_pacakge(struct MEMMAN *memman,DHCP_INFO* dhcp_info){
    
    MAC_PACKAGE *mac_p= (MAC_PACKAGE *) memman_alloc_4k(memman, sizeof(MAC_PACKAGE));

     // MAC header
    init_mac_header(mac_p,0x0800);
    // IP header
    init_ip_package(&((mac_p->ip).header));
    mac_p->ip.header.head_checksum=calculate_checksum((unsigned short *)(&(mac_p->ip.header)),sizeof(mac_p->ip.header)/2);
    
    // UDP header
    mac_p->ip.udp.header.src_port=hxl(68);
    mac_p->ip.udp.header.dest_port=hxl(67);
    mac_p->ip.udp.header.length=hxl(sizeof(mac_p->ip.udp)-4);
    //mac_p->ip.udp.lenth=sizeof(mac_p->ip.udp)-4;
    mac_p->ip.udp.header.check_sum=0;

    struct UDP_PERSDO_PACKAGE *udp_persdo=(struct UDP_PERSDO_PACKAGE *)memman_alloc_4k(memman,sizeof(struct UDP_PERSDO_PACKAGE));
    udp_persdo->src_ip = 0x00000000;
    udp_persdo->dest_ip = 0xffffffff;
    udp_persdo->zeros=0x00;
    udp_persdo->protocal = 17;
    udp_persdo->length = sizeof(struct UDP_PERSDO_PACKAGE);
    
    
    // DHCP message header
    init_message_package(&(mac_p->ip.udp.message),dhcp_info);
    //init_message_package(&(mac_p->ip.udp.message));
    udp_persdo->message=mac_p->ip.udp.message;
    mac_p->ip.udp.header.check_sum = calculate_checksum((short *)udp_persdo, sizeof(*udp_persdo)/2);
    // 释放UPD伪包的内存
    memman_free(memman,&(udp_persdo),sizeof(*(udp_persdo)));
    return mac_p;
}

可以看到,整个构造过程与DHCP discover一样,不同的是第27行。

在27行的init_message_package函数里,我们将offer 信息中的your ip address信息放入了requtst信息的相关字段,如下:

void init_message_package(DHCP_MESSAGE * dd,DHCP_INFO * dhcp_info){
    dd->op=1;
    dd->htype=1;
    dd->hlen=6;
    dd->hops=0;
    dd->xid=0x83208856;
    dd->secs=0;
    dd->flag=0;
    dd->ciaddr=dhcp_info->your_ip_addr;
    dd->yiaddr=0;
    dd->siaddr=0;
    dd->giaddr=0;
    dd->chaddr[5]=GetRegisterValue(1,6);
    dd->chaddr[4]=GetRegisterValue(1,5);
    dd->chaddr[3]=GetRegisterValue(1,4);
    dd->chaddr[2]=GetRegisterValue(1,3);
    dd->chaddr[1]=GetRegisterValue(1,2);
    dd->chaddr[0]=GetRegisterValue(1,1);
    int i;
    //dd->sname=(unsigned char *)memman_alloc_4k(memman,64);
    for(i=0;i<64;i++)
        dd->sname[i]=0;
    for(i=0;i<128;i++)
        dd->file[i]=0;
    //dd->file=(unsigned char *)memman_alloc_4k(memman,128);
    dd->magic_cookie[0]=0x63;
    dd->magic_cookie[1]=0x82;
    dd->magic_cookie[2]=0x53;
    dd->magic_cookie[3]=0x63;
    //dd->options=options;
    uchar *yip = (uchar *)(&(dhcp_info->your_ip_addr));
    unsigned char options[44]={
        0x35,0x01,dhcp_info->message_type,//3
        61,7,1,GetRegisterValue(1,1),GetRegisterValue(1,2),GetRegisterValue(1,3),GetRegisterValue(1,4),GetRegisterValue(1,5),GetRegisterValue(1,6),//9
        50,4,yip[0],yip[1],yip[2],yip[3],//6
        12,3,'z','m','m',//5
        60,3,'r','l','k',//5
        55,14,1,3,6,15,31,33,43,44,46,47,119,121,249,252//16
    };
    
    for(i=0;i<44;i++){
        dd->options[i]=options[i];
    }
    for(i=0;i<10;i++){
        dd->end[i]=0x00;
    }
    dd->end[0]=0xff;


}

在9行,将your_ip_addr放在了ciaddr字段,在第35行,将your_ip_addr放在了Option的50字段,即Requested IP Address字段。这条DHCP request消息就是为了向路由器DHCP服务申请IP地址的,这个Requested IP address字段里的IP地址,就是我们要申请的IP地址。

下图展示了我们抓包抓到的DHCP Request消息。

给30天自制操作系统编写网卡驱动[7]:在收到的数据中筛选DHCP信息

可以看到,它的Option,50,Requsted IP Address为10.211,55.6.这条消息就向路由器DHCP服务申请了这个IP地址。

注意到Option中的DHCP Message Type为3,3表示Request。

好了,发送完这条消息,我们就等着接收到一个DHCP ACK消息了,

DHCP ACK消息如下:

给30天自制操作系统编写网卡驱动[7]:在收到的数据中筛选DHCP信息

只要收到DHCP ACK消息,就表示我们刚才的申请的IP地址是可用的。如上返回的DHCP ACK的DHCP Message Type 为5.注意到,给我们分配IP地址的路由器地址为:10.211.55.1,IPaddress Lease Time:1800s,30分钟,子网掩码为255.255.255.0.

不过以上抓取到的消息并不是在30天自制操作系统上发出的DHCP discover 消息,DHCP request消息。那么现在看看我们自制操作系统上申请IP地址的过程吧:

视频加载中...

对30天自制操作系统发出的DHCP消息进行抓包。

下面视频里有更多的展示

视频加载中...

后记:

使用DHCP协议获取IP地址的开发工作到一段落了。

到目前为止,我们已经实现了DHCP,UPD,IP,ethernet II等协议,

后续会继续讲解ARP,ICMP,TCP的实现。

再往后实现ssh, http,ftp等协议。

基本的主要TCP/IP协议蔟都可以按照我们实现DHCP的这个过程去实现:

  1. 抓包,分析包的内容
  2. 构造相应协议的结构体,这个结构体即可以用来产生数据,又可以用来解析收到的数据。

总的来说,现在常用的通信协议,如果我们从底层去了解,发现其实不复杂。

复杂的是,当我们要求它高并发,要求按照这个协议传输的又快又好时,就要求我们对协议细节精通,甚至去改进协议,制定下一代协议。

另外就是,开发网卡驱动时,往往一次改了很多功能,添加了很多功能后,再去编译调试,

我发现这样做很不好。因为改的地方太多,产生bug后,去找原因就不好找了。找来找去也很浪费时间的。

那么怎么做比较好呢?改动一个地方,添加一个功能后,就立即做测试,这样不仅定位bug较快,而且整体开发思路非常清晰,开发一天也不会累。

不会累就是因为找bug的时间少了,思维被打断的时间少了。

其实,我还是觉得要先把网卡的基本控制信息用系统调用开放给用户,然后我们开发相应协议的时候,在APP中开发会比较好。现在系统中带着这些代码,总感觉没啥必要。我觉得参考一下linux操作系统中的做法会比较妥:看看哪些功能的实现放在操作系统内核里,哪些放在了APP中去实现。比如申请IP地址的这个命令,完全可以在命令行里操作,在命令行里实现DHCP,实现ping命令。

下一次就在命令行里实现ARP协议吧。