精华内容
下载资源
问答
  • wifi基本原理

    千次阅读 2019-10-17 09:23:01
    1.4-wifi通讯原理 废话不多说,有图有真相 由于笔者之前的wifi硬件没有备份,离职之后被公司回收,所以这里我使用笔者的MAC电脑作为服务器,笔者的iPhone6S手机作为客户端演示wifi通讯 0301.gif ...

    WIFI基本知识整理 

            这里对wifi的802.11协议中比较常见的知识做一个基本的总结和整理,便于后续的学习。因为无线网络中涉及术语很多,并且许多协议都是用英文描述,所以有些地方翻译出来会有歧义,这种情况就直接英文来描述了。

     

    主要内容:

    一、基本概述

    二、实践基础

    三、一些原理

    四、补充

    五、其它

     

     

    一、基本概述

    ============================

    1、有线和无线网络

            目前有线网络中最著名的是以太网(Ethenet),但是无线网络WLAN是一个很有前景的发展领域,虽然可能不会完全取代以太网,但是它正拥有越来越多的用户,无线网络中最有前景的是Wifi。本文介绍无线网络相关内容。

            无线网络相比有线网络,还是有许多的缺点的:

            (*)通信双方因为是通过无线进行通信,所以通信之前需要建立连接;而有线网络就直接用线缆连接,不用这个过程了。

            (*)通信双方通信方式是半双工的通信方式;而有线网络可以是全双工。

            (*)通信时在网络层以下出错的概率非常高,所以帧的重传概率很大,需要在网络层之下的协议添加重传的机制(不能只依赖上面TCP/IP的延时等待重传等开销来保证);而有线网络出错概率非常小,无需在网络层有如此复杂的机制。

            (*)数据是在无线环境下进行的,所以抓包非常容易,存在安全隐患。

            (*)因为收发无线信号,所以功耗较大,对电池来说是一个考验。

            (*)相对有线网络吞吐量低,这一点正在逐步改善,802.11n协议可以达到600Mbps的吞吐量。

     

    2、协议

            Ethenet和Wifi采用的协议都属于IEEE 802协议集。其中,Ethenet以802.3协议做为其网络层以下的协议;而Wifi以802.11做为其网络层以下的协议。无论是有线网络,还是无线网络,其网络层以上的部分,基本一样。

            这里主要关注的是Wifi网络中相关的内容。Wifi的802.11协议包含许多子部分。其中按照时间顺序发展,主要有:

            (1)802.11a,1999年9月制定,工作在5gHZ的频率范围(频段宽度325MHZ),最大传输速率54mbps,但当时不是很流行,所以使用的不多。

            (2)802.11b,1999年9月制定,时间比802.11a稍晚,工作在2.4g的频率范围(频段宽度83.5MHZ),最大传输速率11mbps。

            (3)802.11g,2003年6月制定,工作在2.4gHZ频率范围(频段宽度83.5MHZ),最大传输速率54mbps。

            (4)802.11n,2009年才被IEEE批准,在2.4gHZ和5gHZ均可工作,最大的传输速率为600mbps。

            这些协议均为无线网络的通信所需的基本协议,最新发展的,一般要比最初的有所改善。

            另外值得注意的是,802.11n在MAC层上进行了一些重要的改进,所以导致网络性能有了很大的提升例如:

            (*)因为传输速率在很大的程度上取决于Channel(信道)的ChannelWidth有多宽,而802.11n中采用了一种技术,可以在传输数据的时候将两个信道合并为一个,再进行传输,极大地提高了传输速率(这又称HT-40,high through)。

            (*)802.11n的MIMO(多输入输出)特性,使得两对天线可以在同时同Channel上传输数据,而两者却能够不相互干扰(采用了OFDM特殊的调制技术) 

     

    3、术语

            讲述之前,我们需要对无线网络中一些常用的术语有所了解。这里先列出一些,后面描述中出现的新的术语,将会在描述中解释。

            (*)LAN:即局域网,是路由和主机组成的内部局域网,一般为有线网络。

            (*)WAN:即广域网,是外部一个更大的局域网。

            (*)WLAN(Wireless LAN,即无线局域网):前面我们说过LAN是局域网,其实大多数指的是有线网络中的局域网,无线网络中的局域网,一般用WLAN。

            (*)AP(Access point的简称,即访问点,接入点):是一个无线网络中的特殊节点,通过这个节点,无线网络中的其它类型节点可以和无线网络外部以及内部进行通信。这里,AP和无线路由都在一台设备上(即Cisco E3000)。

            (*)Station(工作站):表示连接到无线网络中的设备,这些设备通过AP,可以和内部其它设备或者无线网络外部通信。

            (*)Assosiate:连接。如果一个Station想要加入到无线网络中,需要和这个无线网络中的AP关联(即Assosiate)。

            (*)SSID:用来标识一个无线网络,后面会详细介绍,我们这里只需了解,每个无线网络都有它自己的SSID。

            (*)BSSID:用来标识一个BSS,其格式和MAC地址一样,是48位的地址格式。一般来说,它就是所处的无线接入点的MAC地址。某种程度来说,它的作用和SSID类似,但是SSID是网络的名字,是给人看的,BSSID是给机器看的,BSSID类似MAC地址。

            (*)BSS(Basic Service Set):由一组相互通信的工作站组成,是802.11无线网络的基本组件。主要有两种类型的IBSS和基础结构型网络。IBSS又叫ADHOC,组网是临时的,通信方式为Station<->Station,这里不关注这种组网方式;我们关注的基础结构形网络,其通信方式是Station<->AP<->Station,也就是所有无线网络中的设备要想通信,都得经过AP。在无线网络的基础形网络中,最重要的两类设备:AP和Station。

            (*)DS(Distributed System):即分布式系统。分布式系统属于802.11逻辑组件,负责将帧转发至目的地址,802.11并未规定其技术细节,大多数商业产品以桥接引擎合分步式系统媒介共同构成分布式系统。分步式系统是接入点之间转发帧的骨干网络,一般是以太网。其实,骨干网络并不是分步系统的全部,而是其媒介。主要有三点:骨干网(例如以太网)、桥接器(具有有线无线两个网络接口的接入点包含它)、属于骨干网上的接入点所管辖的基础性网络的station通信(和外界或者BSS内部的station)必须经过DS、而外部路由只知道station的mac地址,所以也需要通过分布式系统才能知道station的具体位置并且正确送到。分步式系统中的接入点之间必须相互传递与之关联的工作站的信息,这样整个分步式系统才能知道哪个station和哪个ap关联,保证分步式系统正常工作(即转达给正确的station)。分步式系统也可以是使用无线媒介(WDS),不一定一定是以太网。总之,分步式系统骨干网络(例如以太网)做为媒介,连接各个接入点,每个接入点与其内的station可构成BSS,各个接入点中的桥接控制器有到达骨干网络和其内部BSS无线网的接口(类似两个MAC地址),station通信需要通过分布式系统。

     

     

    二、实践基础

    ============================

    1、一些参数

    (*)MAC

            MAC(即Medium/MediaAccess Control, 介质访问控制),是数据链路层的一部分。MAC地址是烧录在NetworkInterfaceCard(即网卡,简称NIC)里的,它也叫硬件地址,是由48位(即bit,一字节为8位,即1byte=8bits)16进制的数字组成。其中0-23位叫做组织唯一标志符(organizationally unique,简称OUI),是识别LAN(局域网)节点的标识(在有些抓包工具抓包的时候会将前三个字节映射成某种组织名称的字符,也可以选择不显示这种映射)。24-47位是由厂家自己分配。

    (*)SSID

            表示一个子网的名字,无线路由通过这个名字可以为其它设备标识这个无线路由的子网。设备进行扫描的时候,就会将相应SSID扫描到,然后就能够选择相应的SSID连接到相应的无线网络(当然不扫描,理论上也可以直接指定自己事先已经知道的ssid进行连接)。SSID可以和其它的重复,这样扫描的时候会看到两个同样SSID的无线网络,其实这一般用于将一个无线网络扩大的情况(毕竟无线路由器无线信号的覆盖范围是有线的):当想要扩大一个无线网络(即SSID固定)的范围的时候,可以给多个路由设置相同的SSID来达到这个目的。(这也是漫游的原理,漫游的时候,我们可以在远方或者本地都能够打电话,也就是访问移动通信网络)。

            SSID和BSSID不一定一一对应,一个BSSID在不同的Channel上面可能会对应到多个SSID,但是它们在一个Channel是一一对应的;另外,漫游的时候,虽然SSID不变,但是BSSID一定是会变化的。我们经常可以看到实际数据包中的AP的MAC地址和BSSID只差几位,其实实际设备的MAC地址可能只有一个,和BSSID没什么对应关系。在一个包含了路由功能和AP功能的无线路由器(Fat AP)上面,很可能是:路由器有两个MAC地址,一个用于外网(WAN),一个用于内网(WLAN和LAN),一般路由器上面或者配置路由器的网页上面只标注外网的MAC地址;内网的MAC地址和外网MAC地址一般只有几位不同(甚至连续,也有些相差很多的例外)。

     

    (*)Band(频率范围)

            一般ap可以支持5g或2.4g两个频率范围段的无线信号。如果两者同时可以设置,而不是互斥那么,这个路由器还能够同时支持两种频段(频段即Band),这相当于这个ap可建立两个无线网络,它们采用不同的频段(这类似收音机在长波范围内收音和短波范围内收音)。

     

    (*)Channel(信道)

            Channel是对频段的进一步划分(将5G或者2.4G的频段范围再划分为几个小的频段,每个频段称作一个Channel),有”5.18GHZ“,“Auto(DFS)”等等,处于不同传输信道上面的数据,如果信道覆盖范围没有重叠,那么不会相互干扰。对于信道的使用,在国际上有所规定。其中有些信道是无需授权即可直接使用的(究竟是那个频段的那个信道,依照各个国家而不同),无需授权使用的意思是,传输数据的时候(无论以哪种无线方式),可以让设备收发的功率导致传输时的数据进入该信道的频率并在该信道所在频段宽度内进行传输;授权的使用的意思是,不允许传输时使用授权信道进行,否则会违反规定,并且干扰该信道上其他数据的传输。另外,除了wifi,微波、红外线、蓝牙(使用802.15协议)的工作频段也都有在2.4gHZ范围内的,所以,它们传输的时候会对wifi传输造成干扰,因为两者在不同的协议下进行通信,所以互相将对方传输的信号识别为噪声。有时候配置AP的时候,Channel中有一个类似“Auto”的选项值,这表示打开AP的时候,AP自己Scan周围的环境,选择一个干扰最小的Channel来进行通信,当选择好了一个Channel的时候,一般就不会改变了。

     

    (*)Channel Width(信道宽度)

            这里的Channel Width是信道的带宽,有”20M HZ“、”40M HZ“等,它表示一个Channel片段的宽度(假设5g的频段宽度总共为100M,平均划分为互不干扰的10个Channel,那么每个Channel的Channel Width就为100M/10=10M,实际Channel并不一定是完全不重叠的)。这个参数可能依赖于一些其它的选项,例如不是802.11N的协议,就可能不会有40M HZ的Channel Width(N模式有一个特点就是可以把两个Channel合并,通过提高ChannelWidth来提高吞吐量)。例如选择了"20M HZ"这个Channel Width之后,后面再选择一个“5.18GHZ”的Channel,则表示以5.18GHZ为中心的前"10M HZ"以及其后面的"10M HZ"频带范围被占用。

            至此可知,配置无线AP的时候,如果屋子里面有很多的AP(也就是无线路由接入点)的话,仔细设置它们的Channel Width和Channel可以保证它们相互之间的干扰(类似收音机里面的串台)尽可能小。当然,如果相互干扰了,那么Net Mode所指定的协议也会有相应的处理方式让他们之间进行协调(例如让谁先通信谁等一会再通信之类的),但是这样网络的性能就不如没有干扰的时候好了。

     

    (*)Wireless Security(无线网络的安全性)

            这里主要涉及WEP、WPA、WPA2和RC4、TKIP、AES。

            IEEE 802.11 所制定的是技术性标准 ,Wi-Fi 联盟所制定的是商业化标准 , 而 Wi-Fi 所制定的商业化标准基本上也都符合 IEEE 所制定的技术性标准。WEP 是1999年9月通过的 IEEE 802.11 标准的一部分;WPA(Wi-Fi Protected Access) 事实上就是由 Wi-Fi 联盟所制定的安全性标准 , 这个商业化标准存在的目的就是为了要支持 IEEE 802.11i 这个以技术为导向的安全性标准;而 WPA2 其实就是 WPA 的第二个版本。直观点说,WEP是较老的认证方法它有好几个弱点,因此在2003年被WPA淘汰,WPA又在2004年由完整的 IEEE 802.11i 标准(又称为 WPA2)所取代。

            WEP(Wired Equivalent Privacy),采用名为RC4的RSA加密技术;WPA(Wi-Fi Protected Access) ,采用新的TKIP算法,TKIP算法保留了RC4所以也有其弱点,但是这个时候更好的CCMP还没完成,所以先在WPA上用TKIP技术;WPA2是WPA的第2个版本,采用CCMP加密协定(在有些路由器等设备上设定加密协定或者加密算法的时候,可能会用类似AES之类的字眼替代CCMP)。所以WPA2+AES是安全性最强的。

            另外,在有些无线网路设备的参数中会看到像 WPA-Enterprise / WPA2-Enterprise 以及 WPA-Personal / WPA2-Personal 的字眼 , 其实 WPA-Enterprise / WPA2-Enterprise 就是 WPA / WPA2 ; WPA-Personal / WPA2-Personal 其实就是 WPA-PSK / WPA2-PSK, 也就是以 ”pre-share key” 或 ” passphrase” 的验证 (authentication) 模式来代替 IEEE 802.1X/EAP 的验证模式 ,PSK 模式下不须使用验证服务器 ( 例如 RADIUS Server), 所以特别适合家用或 SOHO 的使用者。

            还有,wep是旧的加密方式,工作于802.11B/G模式下而802.11N草案并不支持此加密方式,所以如果802.11N的设备采用wep加密方式后,它也只会工作在802.11b/g模式下,N的性能发挥不出来。

            实际中,在有些路由器上面,设置的时候,可能不是严格按照这个规定来设置的(例如设定了采用WPA方式,还可以选择AES),但是大体一样。

     

    (*)Region(区域)

            一般在无线网络中的AP上都有一个参数,表明它是处于哪个Region(地区)。Station根据AP中设置的Region调整其相应的发射功率以遵守该地区的规定。AP的调整过程一般都是手动设定,设置好AP所处的Region之后,这些信息就会在AP发送的Beacon帧(后面会说到)中包含了;通过这个AP连接到无线网络上的Station,从Beacon帧中了解到这些Region信息,并且根据这些信息中的规定和AP进行通信。如果AP开始设置错了,那么Station和AP通信的时候,采用的将会是不符合Region规定的频段,可能会对该Region中的其它传输网络造成干扰,这应当是“非法”的。

     

    (*)Transmission Rate

            设置传输速率。这里采用不同的无线网络传输协议(802.11a,802.11b,802.11g等),那么可以设置的速率范围有所不同,这里的速度是指理论的速度,实际中,由于各种干扰因素,传输的速率可能会比设置的小。

            一般而言,在无线网络中,对于某种协议的性能进行描述时,我们需要注意的是,描述时提到的传输速率(Datarate)和吞吐量(Throughput)是不同的。Datarate是理论上面最大数据传输速率,而Throughput是数据的实际最大吞吐量。因为厂家以及传输时所使用的协议等各种因素造成的开销,会导致实际吞吐量比理论吞吐量要小,一般实际最大吞吐为理论最大的50%左右(一个不太准确但是相对直观的估计:在网络中,高清视频所需的Throughput也就30mbps左右,网络上一般的视频也就4mbps左右)。

     

    (*)Qos(质量保证)

            无线网络中的QOS是质量保证,大致的意思是,传输数据的时候,考虑各种因素(例如收费策略,所处地区等),以一定的优先级来保证传输的特定要求(一般就是速度),如果带宽足够的话,QOS反而不需要了。

     

    (*)RTS Threshold / CTS Protection Mode:

            这里的RTS是Request-To-Send的简写,CTS是Clear-To-Send的简写。设置好RTS的阈值之后,如果超过这个阈值就会在发送信息之前先发送RTS,以减少干扰,相应的CTS会回应之前的RTS。一般都是AP发送CTS数据,而Station发送RTS数据。

            这里对RTS和CTS做一个简单解释:假设在同一个AP所覆盖的无线网络范围内的两个Station A和B,它们之间可能会因为距离的原因互相不可见(例如它们在AP网络范围的两端,而这两端的距离大于两者的信号覆盖范围),但是AP却知道它们是在自己的范围内。当一个A想要在AP的网络中进行通信的时候,必定要经过AP转发它的信息,由于A不知道B的存在,所以如果同时B也通过AP进行网络通信,那么会出现AP同时收到A、B两个Station的通信请求,而这在无线网络中是不允许的(无线网络中,同一时刻不能有多个人传输数据)。在这种情况下,B和A互相干扰了对方的通信,但是却互相不可见(不可见的节点互相被称作隐藏节点)。如果在一个网络中,这样的隐藏节点很多,那么势必会影响网络的性能(因为数据一旦发送失败,就要重传,隐藏节点会导致重传的机率增大)。这个时候,可采用RTS和CTS机制。即:在A想要通信的时候,先广播发送RTS给AP,告诉AP“它想要通信”,同时接受到RTS的别的Station(它们对发送RTS的Station而言可见)会知道A将要发送数据,于是它们不会发送数据以免干扰A;AP收到RTS之后,会广播发送CTS,告诉所有在AP范围内的Station(包括对A而言的隐藏节点B)”A将要通信(同时也相当于告诉A,A可以无干扰的发送信息了)”,这样对A而言的隐藏节点B也知道有一个A的存在并且要发送信息了,于是B就不会干扰A了。 这里,A和B两者可以在不同的网络上,也就是说,不同网络的工作站之间也可以通过RTS/CTS来清除相互的干扰。

     

    (*)Beacon Interval:

            表示无线路由定期广播其SSID的时间间隔。这个一般不会特别设置,就采用默认值即可。如果不广播了,那么Station端扫描的时候可能会发现不定期广播的AP对应的SSID的网络不见了,所以可能会断开连接。这里定期广播,表示AP会定时向其范围内广播SSID的信息,以表示AP的存在,这样Station进入一个区域之后,就能够通过扫描知道这个区域是否有AP的存在。当然,除了AP广播SSID以告知其无线网络存在之外,Station也可主动广播探寻包,在其能够覆盖的范围内询问是否有AP存在(即我们通常所说的扫描寻找接入点)。

     

    (*)DTIM Interval:

            DTIM/TIM表示告诉Station,AP在为Station做package buffer(例如Station睡眠的时候)的缓存时间。为了节省电池使用时间,处于无线网络中的Station可能会在一定时间之后自动进入休眠状态。这个时候,AP会为这个Station缓存发送给它的数据,而处于休眠状态的Station只会在一定时间间隔内给AP发送一个数据帧,以确认是否有发送给自己的数据存在。例如,当我们在主机上ping另外一台睡眠的机器的时候,收到另外一台机器响应的时间,要比它不睡眠的时候响应的时间长很多。

     

    (*)Fragmentation Threshold:

            表示一个package的分片阈值。我们可以设置分片大小,当发送的数据包超过这个阈值之后,802.11协议会自动对这个数据包进行分割。如果设置的这个分片值越小,那么整个数据包越容易传输成功(因为如果出错,那么只需要传送一个片段而不是整个包,无线wifi网络中数据传输时出错的概率比有线的以太网要大的多的多),当然开销也越大(因为需要额外的信息标记每个分片,以及各个分片传输成功之后涉及到的重组问题)。

     

    2、抓包

            一般来说,我们的机器上面的软件抓取无线网卡上面的包的时候,其实这些包的目标地址都是这个机器的无线网卡,因为不是发给这个机器无线网卡的包都被网卡过滤了。所以如果我们想要抓取所处无线网络环境下所有的包的时候,需要给机器配备一种特殊的设备(sniffer就是嗅探器),然后再通过抓包工具抓取并分析。有一个硬件设备叫做AirPcap,就是做这个用的,大有几百到上千美金,它可以同时做为嗅探器或者无线网卡使用,不过做为嗅探器的时候,会抓取所有经过它的包。这个工具目前只有Windows上面的驱动,所以使用这个工具,只能在Windows上面,配合Wireshark抓包软件进行抓包。

            这里假设采用AirPcap嗅探,Wireshark软件抓包(其它抓包软件,例如linux下面的tcpdump等分析类似)。不用图形方式详细展示具体的抓包过程以及分析方法了,主要说一下抓包(这里的包实际主要指的是网络层以下的包,更常见的称呼应该是数据帧)时候需要注意的问题。

            (*)Wireshark展示包的时候,大致都是按照协议规定的字段展示,也些地方按照它自己特定的方式展示。因为这里着重讲述一些抓包时注意的基本原理上面的东西,所以不会对此进行过多阐述。大致就是:Wireshark软件中,对包展示的时候,按照协议规定的字段分别用Header和Body两个部分展示;另外,在Header之前还有两个部分是Wireshark为方便用户而展示的包的大小、时间等全局信息(例如见过表示这个包在B和G mode中的Channel 1时,用"BG1"表示)。所以,其实我们分析的时候,实际应该按照后面的Header和Body两个部分进行。 后面将基于以上所述,进行进一步的讲解。

            (*)抓包的时候,需要首先确认这个包是否是完整、正确的包。只要是校验位(checksum)不对的,就是错误的包,也无法确定接收的时候那里出了差错,所以这个包是应该忽略的,几乎没有分析的价值。另外,抓包的时候,由于干扰等原因,抓取的内容可能不是在实际传输所处的Channel上的包(例如在Channel 1上面嗅探,却嗅探到了Channel 2上的包)。

            (*)抓取授权阶段的包,需要注意实际的授权是在后面进行的。Authentication的时候,开始阶段实际是Open的(即无授权),也就是说,开始实际已经建立好了连接,所以我们在抓包的时候,开始看到的一般都是通过验证,但是在后面紧接着采用了类似802.11x等安全加强的协议,来进行再次鉴权认证,如果这里无法通过则立即将已经建立的Association断开。这样的机制,是因为原来的802.11没有充分考虑安全才会这样的,这样也兼容了以前的802.11。

            (*)抓取的包的数据,要注意这个包是否是被加过密的。根据协议标准的描述,包中如果有dataprotected字段,则表示这个数据本身是被加了密的,不知道这个数据具体是什么,当然,如果有密码,wireshark也有一个可以按照这个密码解密的工具,有时候不好用。这里所说的数据加密和网络的加密不一样,可能访问网络本身是需要密码(网络是security的),而数据本身没有crpted(加密)。对于一个加了密的数据包,我们一般看不出来这个包到底是做什么用的或者什么类型的等等。

            (*)抓包的时候,要注意包中指示的源和目的地址以及包的序号。在无线网络中通信的时候,我们抓包的时候可能会看到被抓取的包对应AP的MAC地址是不存在的,其实抓包时AP的MAC是BSSID,它和实际标注的MAC地址不一定一样(但是一般都差不多,也就是之后最后面的几位不一样)。有时候,我们看到抓取的包中的MAC地址有许多只相差几位,那么可能它们都属于一个设备(因为虽然设备可能只标注了一个网卡的MAC地址,但是它却“虚拟”出或者实际有多个MAC地址),所以当我们看到包中对应两个AP的MAC地址几乎一样的时候,一般来说,这两个MAC地址很可能就是一个设备的。还有在抓包的时候,一个地址上面的包的sequence(序号)是连续的,除非丢包了导致重复或者缺失。如果一个设备虚拟出来两个地址,那么也可能由于没有经过什么处理,导致这两个地址上面的包共同起来是连续的(如前所述,这两个地址和MAC很接近,应该是BSSID)。

            (*)抓取的数据帧如果是广播帧则不需要确认(ACK),如果是单播帧,则一般需要确认(ACK)。例如,Probe帧是广播帧,所以它无对应的ACK确认帧,对Probe的回复则叫做Probe Response;注意ACK帧本身用于确认,是单播的,但是它本身却不需要再被确认了。从包中的目的MAC地址中,可以看出这个包是广播/多播帧还是单播帧。MAC第一个字节的第一个位是1,表示组播,前两位是1表示广播,第一个字节第一个位是0表示单播。这里注意,MAC不是值,而是一个Pattern,所以没有Endian之说,也没有那个位高,那个MAC大之说。例如:“a8:27:26:....:b7”,这里第一个字节就是a8(10101000),其第一个字节的第一位就是8的最“右”位,即“0”,所以它的第一个字节的第一个位是0,是一个单播地址。其实,这里涉及到大端小端问题,后面也会讲到,总之,以太网线路上按“Big Endian”字节序传送报文(也就是最高字节先传送),而比特序是”Little Endian”(也就是字节内最低位先传送)所以,一个十六进制表示法表示的MAC地址01-80-C2-00-00-00,传送时的bit顺序就是:1000 0000 0000 0001 0100 0011 0000 0000 0000 0000 0000 0000。

            (*)使用Wire Shark在抓包或者显示包的时候,都可以设置过滤器(filter)。抓包时候设置的过滤器叫做capture filter,它是用BPF(berkerley package filter)这个比较通用的语言来描述(注意这不是Wireshark专用的filter语言,而是一个通用的语言)。但是抓包期间的过滤,有时候不准,所以我们一般先将所有的包抓取下来,然后用WireShark中显示的过滤器(即view filter)来显示我们关注的包,这里我们可以用macro来定义比较复杂的显示过滤条件。保存的时候,可以用按照显示过滤还是抓取过滤的方式保存内容。

            (*)尽量不要抓取Channel Width为40MHZ的Channel上的帧。我们还需要注意的是,使用Sniffer抓取无线网络包的时候,AirPcap无法正常抓取40MHZ Channel Width的包,或者说对抓取这个Channel Width上面的包支持不好。如果非要抓取40MHZ Channel Width的包,那么就在40或者36号Channel上面进行抓取,并在Wireshark上面设置“channel=36,offset+1”(平时offset都是0),这样能够抓取 Channel Width为40MHZ的包(但是,其他Channel上面的40mHZ的包还是无法抓取),这是由AirPcap内部的芯片固件的问题决定的(估计broad com芯片公司也不愿花过多的精力来支持这个很少有人用的抓包工具的这个功能)。

            另外,假设一个无线工作站是基于Android系统的(例如智能手机或者平板电子书)那么我们可以利用“wpa_cli status”命令来可以查看当前设备的连接的SSID,BSSID,MAC,IP等信息,(这里“cli”=“command line interface”)。 还有更“复杂”的命令“wc”和“wl”,其中wc是比较上层的命令,wl是下层的命令(是基于芯片是否支持的,例如wl在broadcom芯片上支持,但是在ti上面就没有了)。

     

     

    三、一些原理

    ============================

    1、常见的帧

            在802.11中的帧有三种类型:管理帧(Management Frame,例如Beacon帧、Association帧)、控制帧(Control Frame,例如RTS帧、CTS帧、ACK帧)、数据帧(Data Frame,承载数据的载体,其中的DS字段用来标识方向很重要)。帧头部中的类型字段中会标识出该帧属于哪个字段。

    (*)ACK帧

            单播(unicast)帧都需要用ACK来确认,ACK本身不是广播帧,ACK在MAC上是unicast的,帧中有receive地址字段(用来标识是对谁的确认),但是它却不需要再确认了。ACK只有接收地址(receive)而无源地址(src)和序号(sequence),因为发送和接受是一个整体,发送之后,其他人(除了这个发送的接受者)都不会再发送数据了(无线协议中的冲突避免机制),所以接受者会发送一个没有src的ack帧给receiver,而接收ACK的一端会根据这个知道它收到了一个ACK帧(其实根据协议,应当把发送单播帧和收到它相应的ACK看作一个原子的不可分割的整体,表示一次成功的通信)。

     

    (*)Beacon帧

            Beacon帧定时广播发送,主要用来通知网络AP的存在性。Station和AP建立Association的时候,也需要用到Beacon。Station可以通过Scan来扫描到Beacon,从而得知AP的存在,也可以在扫描的时候通过主动发送Probe来探寻AP是否存在。也就是说,建立Association的时候有主动的扫描或者被动的扫描两种方式。另外,Beacon还包含了关于Power Save、以及地区等信息。

     

    (*)Association帧

            通常Association帧都有Probe Request和相应的Probe Response。Association的Request中有其所需要的Channel以及Data Rate等状态,以便让AP决定是否让它与自己建立Association。而关联是否成功,主要是看Response中的Status code是否为Success。

     

    (*)Data帧

            Data Frame具有方向,这个方向用DS(分布式系统)字段来标识,以区分不同类型帧中关于地址的解析方式;其它的类型Frame例如Control Frame或者管理帧中,这个字段是全零。这个字段用两位表示,这两个位的含义分别表示“To Ds”和“From Ds”,大致含义如下:

            (a)To DS:表示Station->AP,一般也叫Upload。

            (b)From DS表示AP->Station,一般也叫Download。

            这里,我们可以大致将DS看做AP,To/From是从AP的角度来考虑的。To DS就是让AP干活。另外Data Frame中还有一个比较重要的字段就是Sequence,表示帧的序号。重传帧序号一样,但是多了一个Retry的字段表示该帧是重传的。

            为了便于理解,这里再次详细解释一下DS字段的含义:

            To DS=0,From DS=0:表示Station之间的AD Hoc类似的通信,或者控制侦、管理侦。

            To DS=0,From DS=1:Station接收的侦。

            To DS=1,From DS = 0:Station发送的侦。

            To DS=1,From DS = 1:无线桥接器上的数据侦。

            这里,我们主要关注To DS和From DS分别是01和10的情况,DS虽然大致等于AP但是它不是AP,它其实是一个系统,从Station的角度来看,比较容易理解。并且To DS和From DS一定是无线网络上面数据侦才有的字段。

     

    2、帧和大端小端

            Ethernet和802.11都是按照Little Endian的方式来传输数据,也就是说,而MAC层传输的时候,是采用Little Endian的方式,一个字节一个字节的传输的,前面的低位字节先传输,后面的高位字节后传输(传输单位不是按位而是字节);在协议标准上描述一个帧的时候,一般是先按照Little Endian的方式对其进行总体描述,然后具体细节说每个字段的值,这时候这个字段值是Big Endian方式表示的,这一点应当注意。

            例如,协议标准中可能能对某个帧格式做如下的描述:

            |b0|b1|b2|b3|b4|b5|b6|b7|b8|b9|...|...|

            这里,最低位b0在最前面,所以这里采用的就是小端的方式来描述帧的总体格式信息。传输的时候,就按照这里的方式,以字节为单位向物理层进行传输(先传b0~b7然后b8~b16等等)。    但是,在解释这个帧的各个域的时候却采用大端的方式进行描述。假设b3=0,b2=1,b1=0,b0=0四者共同组成一个名字为“FLAG”的域,那么会有类似如下的描述:

            FLAG=4(即FLAG为0100):表示XXX。

            所以,协议标准中具体描述某个域的时候,一般直接用大端方式表示的数值(b3b2b1b0=0100)来描述;而传输数据帧或者在协议标准中描述整体帧的时候,中给出的却是小端的方式(b0b1b2b3=0010)。 这里的每个字段都是帧的一个部分,在管理帧(后面会说)中长度不固定的部分又叫IE(information Element) 。

            另外注意,内存地址是用来标记每个字节的而不是位,所以内存里面大端小端也是以字节而不是位为单位的(前面描述“大端“、”小端”的时候却以位序而非字节序,这一点需要明辨,不要混淆)。假设奔腾的机器,CPU为32位,采用Little Endian方式,那么表示1这个int类型整数的时候,假设它在数值上是十六进制的"00000001",那么存放在内存中却是由低位到高位依次存放的,由低到高地址依次为:"01"、"00"、"00"、"00"(也就是说小端方式存放在内存中的时候,是按照含有最低位的字节存放在低地址,注意是字节,在内存中“位”没有地址,所以没有大端小端一说)。在传递帧的时候,也是按照一个字节一个字节的传输,而一个字节内部在实际上其实没有什么端的分别,但是wireshark一律使用“b7b6b5b4b3b2b1b0”这样的方式来用大端的方式显示。

            总之,需要注意网络层下面的帧的大端小端问题(不是网络中的字节序,TCP/IP中规定的网络字节序是Big Endian),大致就是:协议规定,传输的时候使用Little Endian;标准描述的时候用Big Endian和Little Endian都用;另外,Wire shark软件抓的包中,好象全都用Big Endian来进行标示(无论是信息窗口还是内存窗口都这样展示)。

     

    3、CSMA/CA的机制

            与以太网的CSMA/CD机制(冲突检测)相对,802.11采用的CSMA/CA机制(冲突避免)。采用这个机制,可以保证每次通信的原子性(即每次通信所需要传输的多种不同类型的帧之间没有夹杂其它通信的帧的干扰),大体过程是:

            (a)链路空闲下来之后,所有Station在发送帧之前都首先等待一段时间(即DIFS,又称帧间隔时间);

            (b)到达DIFS之后,所有的Station进入竞争时间窗口(就是竞争期间),将这个竞争时间窗口分割成多个Slot(退避时间间隔),然后每个Station随机选择一个Slot;

            (c)当某个Station到达它的Slot对应的时间之后,就开始发送数据。这里,选择的Slot越靠前,则表示Station在DIFS之后再等待的时间(退避时间)越短,也就会越早发送实际数据;

            (d)退避窗口的Slot有多个,选择的时候,可能某个Slot被多个站点同时选取,这个时候发送会产生真正的数据冲突(如果多个人同时发送,那么它们都要经过AP来转发,AP无法同时听见多个人的“说话声音”)那么Station就会再重新选择并发送;

            (e)当一个Station发送数据之后,所有Station会检测到链路忙,于是放弃尝试发送,等那个Station发送完数据之后,链路开始空闲,于是又进入到(a)重新开始这个过程。

            对于以上的机制,如果我们让某个Station经过DIFS之后,选择的Slot越小,就意味着它发送帧的机会越大,也就是说这个Station的优先权越高。这就是Qos(质量保证)的基本,前面也说过,Qos就是“以一定的优先级来保证传输的特定要求”,要获得这种优先级,就要有相应的条件(例如“花钱”)(有一种不常用的无竞争发送,其实就是DIFS之后,不退避而直接发送)。

            另外,其实对物理层上来说,所有的发送都是广播,单播与否只是在链路层以上分辨的。上面提到的检测链路是否忙,可以从链路上用软件方式进行(例如增加帧的特殊字段),也可以直接在物理层上进行,实际因为在物理层上成本较高,经常用的是前者,具体参见协议。软件检测大致的思路就是,进行一个通信的时候,这个通信包含多个帧,每个帧有不同的作用,发送的第一帧的时候,会通过其中的某个特殊字段(Duration字段,也叫NAV,即网络分配向量,是一个延迟时间值)告诉所有其它Station,在未来的一段时间内,链路被占用,以完成整个通信过程。这样,其它Station在此期间就不会发送数据干扰这次通信了,以后这个通信的每一帧以及其ACK确认帧之间都会有一个很小的时间间隔(小于DIFS,即SIFS),并且每帧会视情况延长那个Duration字段,保证整个通信期间确实不会有其它人干扰,这样整个通信就是原子性的了。

     

    4、帧的来源和目的地址

            因为无线网络中没有采用有线电缆而是采用无线电波做为传输介质,所以需要将其网络层以下的帧格式封装的更复杂,才能像在有线网络那样传输数据。其中,仅从标识帧的来源和去向方面,无线网络中的帧就需要有四个地址,而不像以太网那样简单只有有两个地址(源和目的)。这四个地址分别是:

            SRC:源地址(SA),和以太网中的一样,就是发帧的最初地址,在以太网和wifi中帧格式转换的时候,互相可以直接复制。

            DST:目的地址(DA),和以太网中的一样,就是最终接受数据帧的地址,在以太网和wifi中帧格式转换的时候,互相可以直接复制。

            TX:也就是Transmiter(TA),表示无线网络中目前实际发送帧者的地址(可能是最初发帧的人,也可能是转发时候的路由)。

            RX:也就是Receiver(RA),表示无线网络中,目前实际接收帧者的地址(可能是最终的接收者,也可能是接收帧以便转发给接收者的ap)。

            注意,其实,还有一个BSSID,用来区分不同网络的标识。在802.11帧中,有四个地址字段,一般只用到其中的三个,并且,这四个字段对应哪种地址或者使用哪些地址,根据帧中的另外一个DS字段以及帧的类型而有不同的解释。

     

            举例:

            (1)无线网络中的Station和以太网中的Host进行通信:

            Station<- - - - ->AP<---------->Host

            a)当Station->Host的时候:

            首先Station->AP,这时候Src=Station,Dst=Host,Tx=Station,Rx=AP,然后AP->Host,这时候Src=Station,Dst=Host,因为AP转发的时候,是在以太网中,所以没有Tx和Rx。

            b)当Host->Station的时候:

            首先Host->AP,这时候Src=Host,Dst=Station,然后AP->Station,这时候,Src=Host,Dst=Station,Tx=AP,Rx=Station。

            (2)无线网络中的Station之间进行通信:

            Station1<- - - - ->AP<- - - - ->Station2

            a)当Station1->Station2时

            首先Station1->AP,Src=Station1,Dst=Station2,Tx=Station1,Rx=AP,然后AP->Station2,Src=Station1, Dst=Station2, Tx=AP, Rx=Station2。

            可见,在无线网络中,始终存在Tx和Rx,但是,这四个地址中还是只有三个地址足矣。

            (3)当两个无线网络中的Station进行通信的时候:

            Station1<- - - - ->AP1<- - - - ->AP2<- - - - - ->Station2

            当Station1->Station2时:

            首先Station1->AP1,Src=Station,Dst=Station2,Tx=Station1,Rx=AP1,然后AP1->AP2,Src=Station, Dst=Station2, Tx=AP1, Rx=AP2,然后AP2->Station2,Src=Station1,Dst=Station2,Tx=AP2,Rx=Station2。

            注意,这个时候,AP起到桥接的作用,所以四个地址各不相同,同时,AP之间或者Station和AP之间的那部分连接,也可以是以太网。

            综上可知,无线网络中的Station想要通信,必须经过AP来进行“转发”,其实,Tx和Rx是无线网络中的发和收,也就是Radio;而Src和Dst是真正的发送源和接收者。

     

    5、Sleep和Power save(节电)

            其实,无线网络中的Power save是指Station的Sleep(睡眠),并且这个Sleep并不是整个系统的Sleep,确切来说,应该是其wifi中Receiver(接收天线)的Sleep。Station在睡眠的期间还是可以Transmit(发送)的,只是当AP知道Station的Receiver处于Sleep状态时,就不会给Station发送帧了。Station在Sleep之前,会给AP发送一个特殊的帧,告诉AP说它(Station)要睡眠了,AP通过这个帧来记住是这个Station睡眠了,然后AP就不会给这个Station单独发送数据了。

            当有和这个Station通信的包想通过AP转达的给这个Station时候,AP会帮这个Station将它们缓存起来,然后在Beacon广播帧中添加一个特殊的位(实际这个位是一个bitmap中的位,这个bitmap表示所有和该AP建立了关联的Station,而这个睡眠的Station的相应位为被置1则表示有消息要传达给这个Station),来表示这个Station有数据到达了(Beacon是定时广播的帧,前面说过它是用来通知无线网络,这个AP的状态),而不是直接发送给Station。而这个睡眠的Station,会在睡眠期间不时地醒来,以检查Beacon帧中的状态,当发现有给它的数据的时候,就会通过发送一个Power Poll的帧来收取数据,收取之后继续睡眠(所以ping一个睡眠状态的Station,响应的时间要慢好多)。

            对于发送给这个Station的广播帧,其处理方式和普通帧有一点不同:当有广播帧要传达给这个Station的时候,AP会为这个Station缓存发送给它的广播帧,但是缓存的时间是DTIM(一般为300ms)。注意:单播帧缓存的时间不一定是多少,广播帧却缓存DTIM的时间。AP每发送一个Beacon的时候,都会将Dtim减少1,而Station睡眠的时候,会不时地醒来,查看一下Beacon帧中的dtim值。当Station发现其DTIM值变成0的时候,就醒来长一些的时间,看看有没有广播给它的数据,如果有的话就用类似Power Save Poll的帧接受,没有则继续睡眠。

            这里,接收数据是根据是否有more data类似的字段来确认是否有更多的数据的;重发的帧是用类似retry的字段来标记。另外注意,当Station进行Sleep的时候,还是可以主动Tranmit消息的,当Station主动Transmit消息的时候,它会等待Reply,所以这个时候,Receiver是on的状态。用一个图示来标识Sleep,Receive,Transmit时的电源消耗状况,大致如下:

     

              power

                   ^

    trans        |                   ------------------------

                   |                   |                       |

    receive     |        -----------|                       |

                   |        |                                  |

    sleep       |--------|                                  |--------------------

                   |----------------------------------------------------------------------> time

     

            可见不同状态,电源消耗状态不同(传送比接收更耗电),另外,如果电源供电不足,在某个状态中就会出现通信失败的情况。(好像ap上面broadcom芯片中的睡眠之后,醒来立即重新发送的时候经常开始会失败,可能就是这个原因)。

     

      6、建立Association

            下面是Station和Ap建立开放Association的过程:

            (0)Ap周期性地广播Beacon帧

            (1)Station广播Probe Request到达Ap

            (2)Ap向Station发送Probe Reponse

            (3)Station向Ap发送ACK

            (4)Station向Ap发送Authentication Request

            (5)Ap向Station发送ACK

            (6)Ap向Station发送Authentication Reponse

            (7)Station向Ap发送ACK

            (8)Station向Ap发送Association Request

            (9)Ap向Station发送ACK

            (10)Ap向Station发送Association Reponse

            (11)Station向Ap发送ACK

            (12)Station和Ap开始相互通信。

            可见,广播帧不用回复,单播帧需要用ACK确认,ACK本身不用被确认。

     

     

    四、补充

    ============================

            有待添加。

     

     

    五、其它

    ============================

            本文内容主要来自学习的总结以及网络,主要集中于无线网络中物理层以上相对比较常见的部分,如果想要理解更详细和全面的内容则需参考相关书籍以及网络协议。由于对此方面的知识也是在初步学习之中,若文章中有错误和不完整之处,谢谢读者指正。^_^

    展开全文
  • WIFI基本原理

    2020-03-02 01:44:38
    参考链接: https://forum.huawei.com/enterprise/zh/thread-282749.html https://www.cnblogs.com/zhoading/p/8891206.html https://openwrt.org/zh-cn/doc/uci/wireless

    参考链接:  

    https://forum.huawei.com/enterprise/zh/thread-282749.html

    https://www.cnblogs.com/zhoading/p/8891206.html

    https://openwrt.org/zh-cn/doc/uci/wireless

     

     

     

    展开全文
  • 【Android wifi】wifi基本原理

    千次阅读 2016-03-08 09:54:31
    WIFI基本知识整理   这里对wifi的802.11协议中比较常见的知识做一个基本的总结和整理,便于后续的学习。因为无线网络中涉及术语很多,并且许多协议都是用英文描述,所以有些地方翻译出来会有歧义,这种情况就直接...

    原文:http://blog.chinaunix.net/uid-9525959-id-3326047.html

    WIFI基本知识整理 

            这里对wifi的802.11协议中比较常见的知识做一个基本的总结和整理,便于后续的学习。因为无线网络中涉及术语很多,并且许多协议都是用英文描述,所以有些地方翻译出来会有歧义,这种情况就直接英文来描述了。

     

    主要内容:

    一、基本概述

    二、实践基础

    三、一些原理

    四、补充

    五、其它

     

     

    一、基本概述

    ============================

    1、有线和无线网络

            目前有线网络中最著名的是以太网(Ethenet),但是无线网络WLAN是一个很有前景的发展领域,虽然可能不会完全取代以太网,但是它正拥有越来越多的用户,无线网络中最有前景的是Wifi。本文介绍无线网络相关内容。

            无线网络相比有线网络,还是有许多的缺点的:

            (*)通信双方因为是通过无线进行通信,所以通信之前需要建立连接;而有线网络就直接用线缆连接,不用这个过程了。

            (*)通信双方通信方式是半双工的通信方式;而有线网络可以是全双工。

            (*)通信时在网络层以下出错的概率非常高,所以帧的重传概率很大,需要在网络层之下的协议添加重传的机制(不能只依赖上面TCP/IP的延时等待重传等开销来保证);而有线网络出错概率非常小,无需在网络层有如此复杂的机制。

            (*)数据是在无线环境下进行的,所以抓包非常容易,存在安全隐患。

            (*)因为收发无线信号,所以功耗较大,对电池来说是一个考验。

            (*)相对有线网络吞吐量低,这一点正在逐步改善,802.11n协议可以达到600Mbps的吞吐量。

     

    2、协议

            Ethenet和Wifi采用的协议都属于IEEE 802协议集。其中,Ethenet以802.3协议做为其网络层以下的协议;而Wifi以802.11做为其网络层以下的协议。无论是有线网络,还是无线网络,其网络层以上的部分,基本一样。

            这里主要关注的是Wifi网络中相关的内容。Wifi的802.11协议包含许多子部分。其中按照时间顺序发展,主要有:

            (1)802.11a,1999年9月制定,工作在5gHZ的频率范围(频段宽度325MHZ),最大传输速率54mbps,但当时不是很流行,所以使用的不多。

            (2)802.11b,1999年9月制定,时间比802.11a稍晚,工作在2.4g的频率范围(频段宽度83.5MHZ),最大传输速率11mbps。

            (3)802.11g,2003年6月制定,工作在2.4gHZ频率范围(频段宽度83.5MHZ),最大传输速率54mbps。

            (4)802.11n,2009年才被IEEE批准,在2.4gHZ和5gHZ均可工作,最大的传输速率为600mbps。

            这些协议均为无线网络的通信所需的基本协议,最新发展的,一般要比最初的有所改善。

            另外值得注意的是,802.11n在MAC层上进行了一些重要的改进,所以导致网络性能有了很大的提升例如:

            (*)因为传输速率在很大的程度上取决于Channel(信道)的ChannelWidth有多宽,而802.11n中采用了一种技术,可以在传输数据的时候将两个信道合并为一个,再进行传输,极大地提高了传输速率(这又称HT-40,high through)。

            (*)802.11n的MIMO(多输入输出)特性,使得两对天线可以在同时同Channel上传输数据,而两者却能够不相互干扰(采用了OFDM特殊的调制技术) 

     

    3、术语

            讲述之前,我们需要对无线网络中一些常用的术语有所了解。这里先列出一些,后面描述中出现的新的术语,将会在描述中解释。

            (*)LAN:即局域网,是路由和主机组成的内部局域网,一般为有线网络。

            (*)WAN:即广域网,是外部一个更大的局域网。

            (*)WLAN(Wireless LAN,即无线局域网):前面我们说过LAN是局域网,其实大多数指的是有线网络中的局域网,无线网络中的局域网,一般用WLAN。

            (*)AP(Access point的简称,即访问点,接入点):是一个无线网络中的特殊节点,通过这个节点,无线网络中的其它类型节点可以和无线网络外部以及内部进行通信。这里,AP和无线路由都在一台设备上(即Cisco E3000)。

            (*)Station(工作站):表示连接到无线网络中的设备,这些设备通过AP,可以和内部其它设备或者无线网络外部通信。

            (*)Assosiate:连接。如果一个Station想要加入到无线网络中,需要和这个无线网络中的AP关联(即Assosiate)。

            (*)SSID:用来标识一个无线网络,后面会详细介绍,我们这里只需了解,每个无线网络都有它自己的SSID。

            (*)BSSID:用来标识一个BSS,其格式和MAC地址一样,是48位的地址格式。一般来说,它就是所处的无线接入点的MAC地址。某种程度来说,它的作用和SSID类似,但是SSID是网络的名字,是给人看的,BSSID是给机器看的,BSSID类似MAC地址。

            (*)BSS(Basic Service Set):由一组相互通信的工作站组成,是802.11无线网络的基本组件。主要有两种类型的IBSS和基础结构型网络。IBSS又叫ADHOC,组网是临时的,通信方式为Station<->Station,这里不关注这种组网方式;我们关注的基础结构形网络,其通信方式是Station<->AP<->Station,也就是所有无线网络中的设备要想通信,都得经过AP。在无线网络的基础形网络中,最重要的两类设备:AP和Station。

            (*)DS(Distributed System):即分布式系统。分布式系统属于802.11逻辑组件,负责将帧转发至目的地址,802.11并未规定其技术细节,大多数商业产品以桥接引擎合分步式系统媒介共同构成分布式系统。分步式系统是接入点之间转发帧的骨干网络,一般是以太网。其实,骨干网络并不是分步系统的全部,而是其媒介。主要有三点:骨干网(例如以太网)、桥接器(具有有线无线两个网络接口的接入点包含它)、属于骨干网上的接入点所管辖的基础性网络的station通信(和外界或者BSS内部的station)必须经过DS、而外部路由只知道station的mac地址,所以也需要通过分布式系统才能知道station的具体位置并且正确送到。分步式系统中的接入点之间必须相互传递与之关联的工作站的信息,这样整个分步式系统才能知道哪个station和哪个ap关联,保证分步式系统正常工作(即转达给正确的station)。分步式系统也可以是使用无线媒介(WDS),不一定一定是以太网。总之,分步式系统骨干网络(例如以太网)做为媒介,连接各个接入点,每个接入点与其内的station可构成BSS,各个接入点中的桥接控制器有到达骨干网络和其内部BSS无线网的接口(类似两个MAC地址),station通信需要通过分布式系统。

     

     

    二、实践基础

    ============================

    1、一些参数

    (*)MAC

            MAC(即Medium/MediaAccess Control, 介质访问控制),是数据链路层的一部分。MAC地址是烧录在NetworkInterfaceCard(即网卡,简称NIC)里的,它也叫硬件地址,是由48位(即bit,一字节为8位,即1byte=8bits)16进制的数字组成。其中0-23位叫做组织唯一标志符(organizationally unique,简称OUI),是识别LAN(局域网)节点的标识(在有些抓包工具抓包的时候会将前三个字节映射成某种组织名称的字符,也可以选择不显示这种映射)。24-47位是由厂家自己分配。

    (*)SSID

            表示一个子网的名字,无线路由通过这个名字可以为其它设备标识这个无线路由的子网。设备进行扫描的时候,就会将相应SSID扫描到,然后就能够选择相应的SSID连接到相应的无线网络(当然不扫描,理论上也可以直接指定自己事先已经知道的ssid进行连接)。SSID可以和其它的重复,这样扫描的时候会看到两个同样SSID的无线网络,其实这一般用于将一个无线网络扩大的情况(毕竟无线路由器无线信号的覆盖范围是有线的):当想要扩大一个无线网络(即SSID固定)的范围的时候,可以给多个路由设置相同的SSID来达到这个目的。(这也是漫游的原理,漫游的时候,我们可以在远方或者本地都能够打电话,也就是访问移动通信网络)。

            SSID和BSSID不一定一一对应,一个BSSID在不同的Channel上面可能会对应到多个SSID,但是它们在一个Channel是一一对应的;另外,漫游的时候,虽然SSID不变,但是BSSID一定是会变化的。我们经常可以看到实际数据包中的AP的MAC地址和BSSID只差几位,其实实际设备的MAC地址可能只有一个,和BSSID没什么对应关系。在一个包含了路由功能和AP功能的无线路由器(Fat AP)上面,很可能是:路由器有两个MAC地址,一个用于外网(WAN),一个用于内网(WLAN和LAN),一般路由器上面或者配置路由器的网页上面只标注外网的MAC地址;内网的MAC地址和外网MAC地址一般只有几位不同(甚至连续,也有些相差很多的例外)。

     

    (*)Band(频率范围)

            一般ap可以支持5g或2.4g两个频率范围段的无线信号。如果两者同时可以设置,而不是互斥那么,这个路由器还能够同时支持两种频段(频段即Band),这相当于这个ap可建立两个无线网络,它们采用不同的频段(这类似收音机在长波范围内收音和短波范围内收音)。

     

    (*)Channel(信道)

            Channel是对频段的进一步划分(将5G或者2.4G的频段范围再划分为几个小的频段,每个频段称作一个Channel),有”5.18GHZ“,“Auto(DFS)”等等,处于不同传输信道上面的数据,如果信道覆盖范围没有重叠,那么不会相互干扰。对于信道的使用,在国际上有所规定。其中有些信道是无需授权即可直接使用的(究竟是那个频段的那个信道,依照各个国家而不同),无需授权使用的意思是,传输数据的时候(无论以哪种无线方式),可以让设备收发的功率导致传输时的数据进入该信道的频率并在该信道所在频段宽度内进行传输;授权的使用的意思是,不允许传输时使用授权信道进行,否则会违反规定,并且干扰该信道上其他数据的传输。另外,除了wifi,微波、红外线、蓝牙(使用802.15协议)的工作频段也都有在2.4gHZ范围内的,所以,它们传输的时候会对wifi传输造成干扰,因为两者在不同的协议下进行通信,所以互相将对方传输的信号识别为噪声。有时候配置AP的时候,Channel中有一个类似“Auto”的选项值,这表示打开AP的时候,AP自己Scan周围的环境,选择一个干扰最小的Channel来进行通信,当选择好了一个Channel的时候,一般就不会改变了。

     

    (*)Channel Width(信道宽度)

            这里的Channel Width是信道的带宽,有”20M HZ“、”40M HZ“等,它表示一个Channel片段的宽度(假设5g的频段宽度总共为100M,平均划分为互不干扰的10个Channel,那么每个Channel的Channel Width就为100M/10=10M,实际Channel并不一定是完全不重叠的)。这个参数可能依赖于一些其它的选项,例如不是802.11N的协议,就可能不会有40M HZ的Channel Width(N模式有一个特点就是可以把两个Channel合并,通过提高ChannelWidth来提高吞吐量)。例如选择了"20M HZ"这个Channel Width之后,后面再选择一个“5.18GHZ”的Channel,则表示以5.18GHZ为中心的前"10M HZ"以及其后面的"10M HZ"频带范围被占用。

            至此可知,配置无线AP的时候,如果屋子里面有很多的AP(也就是无线路由接入点)的话,仔细设置它们的Channel Width和Channel可以保证它们相互之间的干扰(类似收音机里面的串台)尽可能小。当然,如果相互干扰了,那么Net Mode所指定的协议也会有相应的处理方式让他们之间进行协调(例如让谁先通信谁等一会再通信之类的),但是这样网络的性能就不如没有干扰的时候好了。

     

    (*)Wireless Security(无线网络的安全性)

            这里主要涉及WEP、WPA、WPA2和RC4、TKIP、AES。

            IEEE 802.11 所制定的是技术性标准 ,Wi-Fi 联盟所制定的是商业化标准 , 而 Wi-Fi 所制定的商业化标准基本上也都符合 IEEE 所制定的技术性标准。WEP 是1999年9月通过的 IEEE 802.11 标准的一部分;WPA(Wi-Fi Protected Access) 事实上就是由 Wi-Fi 联盟所制定的安全性标准 , 这个商业化标准存在的目的就是为了要支持 IEEE 802.11i 这个以技术为导向的安全性标准;而 WPA2 其实就是 WPA 的第二个版本。直观点说,WEP是较老的认证方法它有好几个弱点,因此在2003年被WPA淘汰,WPA又在2004年由完整的 IEEE 802.11i 标准(又称为 WPA2)所取代。

            WEP(Wired Equivalent Privacy),采用名为RC4的RSA加密技术;WPA(Wi-Fi Protected Access) ,采用新的TKIP算法,TKIP算法保留了RC4所以也有其弱点,但是这个时候更好的CCMP还没完成,所以先在WPA上用TKIP技术;WPA2是WPA的第2个版本,采用CCMP加密协定(在有些路由器等设备上设定加密协定或者加密算法的时候,可能会用类似AES之类的字眼替代CCMP)。所以WPA2+AES是安全性最强的。

            另外,在有些无线网路设备的参数中会看到像 WPA-Enterprise / WPA2-Enterprise 以及 WPA-Personal / WPA2-Personal 的字眼 , 其实 WPA-Enterprise / WPA2-Enterprise 就是 WPA / WPA2 ; WPA-Personal / WPA2-Personal 其实就是 WPA-PSK / WPA2-PSK, 也就是以 ”pre-share key” 或 ” passphrase” 的验证 (authentication) 模式来代替 IEEE 802.1X/EAP 的验证模式 ,PSK 模式下不须使用验证服务器 ( 例如 RADIUS Server), 所以特别适合家用或 SOHO 的使用者。

            还有,wep是旧的加密方式,工作于802.11B/G模式下而802.11N草案并不支持此加密方式,所以如果802.11N的设备采用wep加密方式后,它也只会工作在802.11b/g模式下,N的性能发挥不出来。

            实际中,在有些路由器上面,设置的时候,可能不是严格按照这个规定来设置的(例如设定了采用WPA方式,还可以选择AES),但是大体一样。

     

    (*)Region(区域)

            一般在无线网络中的AP上都有一个参数,表明它是处于哪个Region(地区)。Station根据AP中设置的Region调整其相应的发射功率以遵守该地区的规定。AP的调整过程一般都是手动设定,设置好AP所处的Region之后,这些信息就会在AP发送的Beacon帧(后面会说到)中包含了;通过这个AP连接到无线网络上的Station,从Beacon帧中了解到这些Region信息,并且根据这些信息中的规定和AP进行通信。如果AP开始设置错了,那么Station和AP通信的时候,采用的将会是不符合Region规定的频段,可能会对该Region中的其它传输网络造成干扰,这应当是“非法”的。

     

    (*)Transmission Rate

            设置传输速率。这里采用不同的无线网络传输协议(802.11a,802.11b,802.11g等),那么可以设置的速率范围有所不同,这里的速度是指理论的速度,实际中,由于各种干扰因素,传输的速率可能会比设置的小。

            一般而言,在无线网络中,对于某种协议的性能进行描述时,我们需要注意的是,描述时提到的传输速率(Datarate)和吞吐量(Throughput)是不同的。Datarate是理论上面最大数据传输速率,而Throughput是数据的实际最大吞吐量。因为厂家以及传输时所使用的协议等各种因素造成的开销,会导致实际吞吐量比理论吞吐量要小,一般实际最大吞吐为理论最大的50%左右(一个不太准确但是相对直观的估计:在网络中,高清视频所需的Throughput也就30mbps左右,网络上一般的视频也就4mbps左右)。

     

    (*)Qos(质量保证)

            无线网络中的QOS是质量保证,大致的意思是,传输数据的时候,考虑各种因素(例如收费策略,所处地区等),以一定的优先级来保证传输的特定要求(一般就是速度),如果带宽足够的话,QOS反而不需要了。

     

    (*)RTS Threshold / CTS Protection Mode:

            这里的RTS是Request-To-Send的简写,CTS是Clear-To-Send的简写。设置好RTS的阈值之后,如果超过这个阈值就会在发送信息之前先发送RTS,以减少干扰,相应的CTS会回应之前的RTS。一般都是AP发送CTS数据,而Station发送RTS数据。

            这里对RTS和CTS做一个简单解释:假设在同一个AP所覆盖的无线网络范围内的两个Station A和B,它们之间可能会因为距离的原因互相不可见(例如它们在AP网络范围的两端,而这两端的距离大于两者的信号覆盖范围),但是AP却知道它们是在自己的范围内。当一个A想要在AP的网络中进行通信的时候,必定要经过AP转发它的信息,由于A不知道B的存在,所以如果同时B也通过AP进行网络通信,那么会出现AP同时收到A、B两个Station的通信请求,而这在无线网络中是不允许的(无线网络中,同一时刻不能有多个人传输数据)。在这种情况下,B和A互相干扰了对方的通信,但是却互相不可见(不可见的节点互相被称作隐藏节点)。如果在一个网络中,这样的隐藏节点很多,那么势必会影响网络的性能(因为数据一旦发送失败,就要重传,隐藏节点会导致重传的机率增大)。这个时候,可采用RTS和CTS机制。即:在A想要通信的时候,先广播发送RTS给AP,告诉AP“它想要通信”,同时接受到RTS的别的Station(它们对发送RTS的Station而言可见)会知道A将要发送数据,于是它们不会发送数据以免干扰A;AP收到RTS之后,会广播发送CTS,告诉所有在AP范围内的Station(包括对A而言的隐藏节点B)”A将要通信(同时也相当于告诉A,A可以无干扰的发送信息了)”,这样对A而言的隐藏节点B也知道有一个A的存在并且要发送信息了,于是B就不会干扰A了。 这里,A和B两者可以在不同的网络上,也就是说,不同网络的工作站之间也可以通过RTS/CTS来清除相互的干扰。

     

    (*)Beacon Interval:

            表示无线路由定期广播其SSID的时间间隔。这个一般不会特别设置,就采用默认值即可。如果不广播了,那么Station端扫描的时候可能会发现不定期广播的AP对应的SSID的网络不见了,所以可能会断开连接。这里定期广播,表示AP会定时向其范围内广播SSID的信息,以表示AP的存在,这样Station进入一个区域之后,就能够通过扫描知道这个区域是否有AP的存在。当然,除了AP广播SSID以告知其无线网络存在之外,Station也可主动广播探寻包,在其能够覆盖的范围内询问是否有AP存在(即我们通常所说的扫描寻找接入点)。

     

    (*)DTIM Interval:

            DTIM/TIM表示告诉Station,AP在为Station做package buffer(例如Station睡眠的时候)的缓存时间。为了节省电池使用时间,处于无线网络中的Station可能会在一定时间之后自动进入休眠状态。这个时候,AP会为这个Station缓存发送给它的数据,而处于休眠状态的Station只会在一定时间间隔内给AP发送一个数据帧,以确认是否有发送给自己的数据存在。例如,当我们在主机上ping另外一台睡眠的机器的时候,收到另外一台机器响应的时间,要比它不睡眠的时候响应的时间长很多。

     

    (*)Fragmentation Threshold:

            表示一个package的分片阈值。我们可以设置分片大小,当发送的数据包超过这个阈值之后,802.11协议会自动对这个数据包进行分割。如果设置的这个分片值越小,那么整个数据包越容易传输成功(因为如果出错,那么只需要传送一个片段而不是整个包,无线wifi网络中数据传输时出错的概率比有线的以太网要大的多的多),当然开销也越大(因为需要额外的信息标记每个分片,以及各个分片传输成功之后涉及到的重组问题)。

     

    2、抓包

            一般来说,我们的机器上面的软件抓取无线网卡上面的包的时候,其实这些包的目标地址都是这个机器的无线网卡,因为不是发给这个机器无线网卡的包都被网卡过滤了。所以如果我们想要抓取所处无线网络环境下所有的包的时候,需要给机器配备一种特殊的设备(sniffer就是嗅探器),然后再通过抓包工具抓取并分析。有一个硬件设备叫做AirPcap,就是做这个用的,大有几百到上千美金,它可以同时做为嗅探器或者无线网卡使用,不过做为嗅探器的时候,会抓取所有经过它的包。这个工具目前只有Windows上面的驱动,所以使用这个工具,只能在Windows上面,配合Wireshark抓包软件进行抓包。

            这里假设采用AirPcap嗅探,Wireshark软件抓包(其它抓包软件,例如linux下面的tcpdump等分析类似)。不用图形方式详细展示具体的抓包过程以及分析方法了,主要说一下抓包(这里的包实际主要指的是网络层以下的包,更常见的称呼应该是数据帧)时候需要注意的问题。

            (*)Wireshark展示包的时候,大致都是按照协议规定的字段展示,也些地方按照它自己特定的方式展示。因为这里着重讲述一些抓包时注意的基本原理上面的东西,所以不会对此进行过多阐述。大致就是:Wireshark软件中,对包展示的时候,按照协议规定的字段分别用Header和Body两个部分展示;另外,在Header之前还有两个部分是Wireshark为方便用户而展示的包的大小、时间等全局信息(例如见过表示这个包在B和G mode中的Channel 1时,用"BG1"表示)。所以,其实我们分析的时候,实际应该按照后面的Header和Body两个部分进行。 后面将基于以上所述,进行进一步的讲解。

            (*)抓包的时候,需要首先确认这个包是否是完整、正确的包。只要是校验位(checksum)不对的,就是错误的包,也无法确定接收的时候那里出了差错,所以这个包是应该忽略的,几乎没有分析的价值。另外,抓包的时候,由于干扰等原因,抓取的内容可能不是在实际传输所处的Channel上的包(例如在Channel 1上面嗅探,却嗅探到了Channel 2上的包)。

            (*)抓取授权阶段的包,需要注意实际的授权是在后面进行的。Authentication的时候,开始阶段实际是Open的(即无授权),也就是说,开始实际已经建立好了连接,所以我们在抓包的时候,开始看到的一般都是通过验证,但是在后面紧接着采用了类似802.11x等安全加强的协议,来进行再次鉴权认证,如果这里无法通过则立即将已经建立的Association断开。这样的机制,是因为原来的802.11没有充分考虑安全才会这样的,这样也兼容了以前的802.11。

            (*)抓取的包的数据,要注意这个包是否是被加过密的。根据协议标准的描述,包中如果有dataprotected字段,则表示这个数据本身是被加了密的,不知道这个数据具体是什么,当然,如果有密码,wireshark也有一个可以按照这个密码解密的工具,有时候不好用。这里所说的数据加密和网络的加密不一样,可能访问网络本身是需要密码(网络是security的),而数据本身没有crpted(加密)。对于一个加了密的数据包,我们一般看不出来这个包到底是做什么用的或者什么类型的等等。

            (*)抓包的时候,要注意包中指示的源和目的地址以及包的序号。在无线网络中通信的时候,我们抓包的时候可能会看到被抓取的包对应AP的MAC地址是不存在的,其实抓包时AP的MAC是BSSID,它和实际标注的MAC地址不一定一样(但是一般都差不多,也就是之后最后面的几位不一样)。有时候,我们看到抓取的包中的MAC地址有许多只相差几位,那么可能它们都属于一个设备(因为虽然设备可能只标注了一个网卡的MAC地址,但是它却“虚拟”出或者实际有多个MAC地址),所以当我们看到包中对应两个AP的MAC地址几乎一样的时候,一般来说,这两个MAC地址很可能就是一个设备的。还有在抓包的时候,一个地址上面的包的sequence(序号)是连续的,除非丢包了导致重复或者缺失。如果一个设备虚拟出来两个地址,那么也可能由于没有经过什么处理,导致这两个地址上面的包共同起来是连续的(如前所述,这两个地址和MAC很接近,应该是BSSID)。

            (*)抓取的数据帧如果是广播帧则不需要确认(ACK),如果是单播帧,则一般需要确认(ACK)。例如,Probe帧是广播帧,所以它无对应的ACK确认帧,对Probe的回复则叫做Probe Response;注意ACK帧本身用于确认,是单播的,但是它本身却不需要再被确认了。从包中的目的MAC地址中,可以看出这个包是广播/多播帧还是单播帧。MAC第一个字节的第一个位是1,表示组播,前两位是1表示广播,第一个字节第一个位是0表示单播。这里注意,MAC不是值,而是一个Pattern,所以没有Endian之说,也没有那个位高,那个MAC大之说。例如:“a8:27:26:....:b7”,这里第一个字节就是a8(10101000),其第一个字节的第一位就是8的最“右”位,即“0”,所以它的第一个字节的第一个位是0,是一个单播地址。其实,这里涉及到大端小端问题,后面也会讲到,总之,以太网线路上按“Big Endian”字节序传送报文(也就是最高字节先传送),而比特序是”Little Endian”(也就是字节内最低位先传送)所以,一个十六进制表示法表示的MAC地址01-80-C2-00-00-00,传送时的bit顺序就是:1000 0000 0000 0001 0100 0011 0000 0000 0000 0000 0000 0000。

            (*)使用Wire Shark在抓包或者显示包的时候,都可以设置过滤器(filter)。抓包时候设置的过滤器叫做capture filter,它是用BPF(berkerley package filter)这个比较通用的语言来描述(注意这不是Wireshark专用的filter语言,而是一个通用的语言)。但是抓包期间的过滤,有时候不准,所以我们一般先将所有的包抓取下来,然后用WireShark中显示的过滤器(即view filter)来显示我们关注的包,这里我们可以用macro来定义比较复杂的显示过滤条件。保存的时候,可以用按照显示过滤还是抓取过滤的方式保存内容。

            (*)尽量不要抓取Channel Width为40MHZ的Channel上的帧。我们还需要注意的是,使用Sniffer抓取无线网络包的时候,AirPcap无法正常抓取40MHZ Channel Width的包,或者说对抓取这个Channel Width上面的包支持不好。如果非要抓取40MHZ Channel Width的包,那么就在40或者36号Channel上面进行抓取,并在Wireshark上面设置“channel=36,offset+1”(平时offset都是0),这样能够抓取 Channel Width为40MHZ的包(但是,其他Channel上面的40mHZ的包还是无法抓取),这是由AirPcap内部的芯片固件的问题决定的(估计broad com芯片公司也不愿花过多的精力来支持这个很少有人用的抓包工具的这个功能)。

            另外,假设一个无线工作站是基于Android系统的(例如智能手机或者平板电子书)那么我们可以利用“wpa_cli status”命令来可以查看当前设备的连接的SSID,BSSID,MAC,IP等信息,(这里“cli”=“command line interface”)。 还有更“复杂”的命令“wc”和“wl”,其中wc是比较上层的命令,wl是下层的命令(是基于芯片是否支持的,例如wl在broadcom芯片上支持,但是在ti上面就没有了)。

     

     

    三、一些原理

    ============================

    1、常见的帧

            在802.11中的帧有三种类型:管理帧(Management Frame,例如Beacon帧、Association帧)、控制帧(Control Frame,例如RTS帧、CTS帧、ACK帧)、数据帧(Data Frame,承载数据的载体,其中的DS字段用来标识方向很重要)。帧头部中的类型字段中会标识出该帧属于哪个字段。

    (*)ACK帧

            单播(unicast)帧都需要用ACK来确认,ACK本身不是广播帧,ACK在MAC上是unicast的,帧中有receive地址字段(用来标识是对谁的确认),但是它却不需要再确认了。ACK只有接收地址(receive)而无源地址(src)和序号(sequence),因为发送和接受是一个整体,发送之后,其他人(除了这个发送的接受者)都不会再发送数据了(无线协议中的冲突避免机制),所以接受者会发送一个没有src的ack帧给receiver,而接收ACK的一端会根据这个知道它收到了一个ACK帧(其实根据协议,应当把发送单播帧和收到它相应的ACK看作一个原子的不可分割的整体,表示一次成功的通信)。

     

    (*)Beacon帧

            Beacon帧定时广播发送,主要用来通知网络AP的存在性。Station和AP建立Association的时候,也需要用到Beacon。Station可以通过Scan来扫描到Beacon,从而得知AP的存在,也可以在扫描的时候通过主动发送Probe来探寻AP是否存在。也就是说,建立Association的时候有主动的扫描或者被动的扫描两种方式。另外,Beacon还包含了关于Power Save、以及地区等信息。

     

    (*)Association帧

            通常Association帧都有Probe Request和相应的Probe Response。Association的Request中有其所需要的Channel以及Data Rate等状态,以便让AP决定是否让它与自己建立Association。而关联是否成功,主要是看Response中的Status code是否为Success。

     

    (*)Data帧

            Data Frame具有方向,这个方向用DS(分布式系统)字段来标识,以区分不同类型帧中关于地址的解析方式;其它的类型Frame例如Control Frame或者管理帧中,这个字段是全零。这个字段用两位表示,这两个位的含义分别表示“To Ds”和“From Ds”,大致含义如下:

            (a)To DS:表示Station->AP,一般也叫Upload。

            (b)From DS表示AP->Station,一般也叫Download。

            这里,我们可以大致将DS看做AP,To/From是从AP的角度来考虑的。To DS就是让AP干活。另外Data Frame中还有一个比较重要的字段就是Sequence,表示帧的序号。重传帧序号一样,但是多了一个Retry的字段表示该帧是重传的。

            为了便于理解,这里再次详细解释一下DS字段的含义:

            To DS=0,From DS=0:表示Station之间的AD Hoc类似的通信,或者控制侦、管理侦。

            To DS=0,From DS=1:Station接收的侦。

            To DS=1,From DS = 0:Station发送的侦。

            To DS=1,From DS = 1:无线桥接器上的数据侦。

            这里,我们主要关注To DS和From DS分别是01和10的情况,DS虽然大致等于AP但是它不是AP,它其实是一个系统,从Station的角度来看,比较容易理解。并且To DS和From DS一定是无线网络上面数据侦才有的字段。

     

    2、帧和大端小端

            Ethernet和802.11都是按照Little Endian的方式来传输数据,也就是说,而MAC层传输的时候,是采用Little Endian的方式,一个字节一个字节的传输的,前面的低位字节先传输,后面的高位字节后传输(传输单位不是按位而是字节);在协议标准上描述一个帧的时候,一般是先按照Little Endian的方式对其进行总体描述,然后具体细节说每个字段的值,这时候这个字段值是Big Endian方式表示的,这一点应当注意。

            例如,协议标准中可能能对某个帧格式做如下的描述:

            |b0|b1|b2|b3|b4|b5|b6|b7|b8|b9|...|...|

            这里,最低位b0在最前面,所以这里采用的就是小端的方式来描述帧的总体格式信息。传输的时候,就按照这里的方式,以字节为单位向物理层进行传输(先传b0~b7然后b8~b16等等)。    但是,在解释这个帧的各个域的时候却采用大端的方式进行描述。假设b3=0,b2=1,b1=0,b0=0四者共同组成一个名字为“FLAG”的域,那么会有类似如下的描述:

            FLAG=4(即FLAG为0100):表示XXX。

            所以,协议标准中具体描述某个域的时候,一般直接用大端方式表示的数值(b3b2b1b0=0100)来描述;而传输数据帧或者在协议标准中描述整体帧的时候,中给出的却是小端的方式(b0b1b2b3=0010)。 这里的每个字段都是帧的一个部分,在管理帧(后面会说)中长度不固定的部分又叫IE(information Element) 。

            另外注意,内存地址是用来标记每个字节的而不是位,所以内存里面大端小端也是以字节而不是位为单位的(前面描述“大端“、”小端”的时候却以位序而非字节序,这一点需要明辨,不要混淆)。假设奔腾的机器,CPU为32位,采用Little Endian方式,那么表示1这个int类型整数的时候,假设它在数值上是十六进制的"00000001",那么存放在内存中却是由低位到高位依次存放的,由低到高地址依次为:"01"、"00"、"00"、"00"(也就是说小端方式存放在内存中的时候,是按照含有最低位的字节存放在低地址,注意是字节,在内存中“位”没有地址,所以没有大端小端一说)。在传递帧的时候,也是按照一个字节一个字节的传输,而一个字节内部在实际上其实没有什么端的分别,但是wireshark一律使用“b7b6b5b4b3b2b1b0”这样的方式来用大端的方式显示。

            总之,需要注意网络层下面的帧的大端小端问题(不是网络中的字节序,TCP/IP中规定的网络字节序是Big Endian),大致就是:协议规定,传输的时候使用Little Endian;标准描述的时候用Big Endian和Little Endian都用;另外,Wire shark软件抓的包中,好象全都用Big Endian来进行标示(无论是信息窗口还是内存窗口都这样展示)。

     

    3、CSMA/CA的机制

            与以太网的CSMA/CD机制(冲突检测)相对,802.11采用的CSMA/CA机制(冲突避免)。采用这个机制,可以保证每次通信的原子性(即每次通信所需要传输的多种不同类型的帧之间没有夹杂其它通信的帧的干扰),大体过程是:

            (a)链路空闲下来之后,所有Station在发送帧之前都首先等待一段时间(即DIFS,又称帧间隔时间);

            (b)到达DIFS之后,所有的Station进入竞争时间窗口(就是竞争期间),将这个竞争时间窗口分割成多个Slot(退避时间间隔),然后每个Station随机选择一个Slot;

            (c)当某个Station到达它的Slot对应的时间之后,就开始发送数据。这里,选择的Slot越靠前,则表示Station在DIFS之后再等待的时间(退避时间)越短,也就会越早发送实际数据;

            (d)退避窗口的Slot有多个,选择的时候,可能某个Slot被多个站点同时选取,这个时候发送会产生真正的数据冲突(如果多个人同时发送,那么它们都要经过AP来转发,AP无法同时听见多个人的“说话声音”)那么Station就会再重新选择并发送;

            (e)当一个Station发送数据之后,所有Station会检测到链路忙,于是放弃尝试发送,等那个Station发送完数据之后,链路开始空闲,于是又进入到(a)重新开始这个过程。

            对于以上的机制,如果我们让某个Station经过DIFS之后,选择的Slot越小,就意味着它发送帧的机会越大,也就是说这个Station的优先权越高。这就是Qos(质量保证)的基本,前面也说过,Qos就是“以一定的优先级来保证传输的特定要求”,要获得这种优先级,就要有相应的条件(例如“花钱”)(有一种不常用的无竞争发送,其实就是DIFS之后,不退避而直接发送)。

            另外,其实对物理层上来说,所有的发送都是广播,单播与否只是在链路层以上分辨的。上面提到的检测链路是否忙,可以从链路上用软件方式进行(例如增加帧的特殊字段),也可以直接在物理层上进行,实际因为在物理层上成本较高,经常用的是前者,具体参见协议。软件检测大致的思路就是,进行一个通信的时候,这个通信包含多个帧,每个帧有不同的作用,发送的第一帧的时候,会通过其中的某个特殊字段(Duration字段,也叫NAV,即网络分配向量,是一个延迟时间值)告诉所有其它Station,在未来的一段时间内,链路被占用,以完成整个通信过程。这样,其它Station在此期间就不会发送数据干扰这次通信了,以后这个通信的每一帧以及其ACK确认帧之间都会有一个很小的时间间隔(小于DIFS,即SIFS),并且每帧会视情况延长那个Duration字段,保证整个通信期间确实不会有其它人干扰,这样整个通信就是原子性的了。

     

    4、帧的来源和目的地址

            因为无线网络中没有采用有线电缆而是采用无线电波做为传输介质,所以需要将其网络层以下的帧格式封装的更复杂,才能像在有线网络那样传输数据。其中,仅从标识帧的来源和去向方面,无线网络中的帧就需要有四个地址,而不像以太网那样简单只有有两个地址(源和目的)。这四个地址分别是:

            SRC:源地址(SA),和以太网中的一样,就是发帧的最初地址,在以太网和wifi中帧格式转换的时候,互相可以直接复制。

            DST:目的地址(DA),和以太网中的一样,就是最终接受数据帧的地址,在以太网和wifi中帧格式转换的时候,互相可以直接复制。

            TX:也就是Transmiter(TA),表示无线网络中目前实际发送帧者的地址(可能是最初发帧的人,也可能是转发时候的路由)。

            RX:也就是Receiver(RA),表示无线网络中,目前实际接收帧者的地址(可能是最终的接收者,也可能是接收帧以便转发给接收者的ap)。

            注意,其实,还有一个BSSID,用来区分不同网络的标识。在802.11帧中,有四个地址字段,一般只用到其中的三个,并且,这四个字段对应哪种地址或者使用哪些地址,根据帧中的另外一个DS字段以及帧的类型而有不同的解释。

     

            举例:

            (1)无线网络中的Station和以太网中的Host进行通信:

            Station<- - - - ->AP<---------->Host

            a)当Station->Host的时候:

            首先Station->AP,这时候Src=Station,Dst=Host,Tx=Station,Rx=AP,然后AP->Host,这时候Src=Station,Dst=Host,因为AP转发的时候,是在以太网中,所以没有Tx和Rx。

            b)当Host->Station的时候:

            首先Host->AP,这时候Src=Host,Dst=Station,然后AP->Station,这时候,Src=Host,Dst=Station,Tx=AP,Rx=Station。

            (2)无线网络中的Station之间进行通信:

            Station1<- - - - ->AP<- - - - ->Station2

            a)当Station1->Station2时

            首先Station1->AP,Src=Station1,Dst=Station2,Tx=Station1,Rx=AP,然后AP->Station2,Src=Station1, Dst=Station2, Tx=AP, Rx=Station2。

            可见,在无线网络中,始终存在Tx和Rx,但是,这四个地址中还是只有三个地址足矣。

            (3)当两个无线网络中的Station进行通信的时候:

            Station1<- - - - ->AP1<- - - - ->AP2<- - - - - ->Station2

            当Station1->Station2时:

            首先Station1->AP1,Src=Station,Dst=Station2,Tx=Station1,Rx=AP1,然后AP1->AP2,Src=Station, Dst=Station2, Tx=AP1, Rx=AP2,然后AP2->Station2,Src=Station1,Dst=Station2,Tx=AP2,Rx=Station2。

            注意,这个时候,AP起到桥接的作用,所以四个地址各不相同,同时,AP之间或者Station和AP之间的那部分连接,也可以是以太网。

            综上可知,无线网络中的Station想要通信,必须经过AP来进行“转发”,其实,Tx和Rx是无线网络中的发和收,也就是Radio;而Src和Dst是真正的发送源和接收者。

     

    5、Sleep和Power save(节电)

            其实,无线网络中的Power save是指Station的Sleep(睡眠),并且这个Sleep并不是整个系统的Sleep,确切来说,应该是其wifi中Receiver(接收天线)的Sleep。Station在睡眠的期间还是可以Transmit(发送)的,只是当AP知道Station的Receiver处于Sleep状态时,就不会给Station发送帧了。Station在Sleep之前,会给AP发送一个特殊的帧,告诉AP说它(Station)要睡眠了,AP通过这个帧来记住是这个Station睡眠了,然后AP就不会给这个Station单独发送数据了。

            当有和这个Station通信的包想通过AP转达的给这个Station时候,AP会帮这个Station将它们缓存起来,然后在Beacon广播帧中添加一个特殊的位(实际这个位是一个bitmap中的位,这个bitmap表示所有和该AP建立了关联的Station,而这个睡眠的Station的相应位为被置1则表示有消息要传达给这个Station),来表示这个Station有数据到达了(Beacon是定时广播的帧,前面说过它是用来通知无线网络,这个AP的状态),而不是直接发送给Station。而这个睡眠的Station,会在睡眠期间不时地醒来,以检查Beacon帧中的状态,当发现有给它的数据的时候,就会通过发送一个Power Poll的帧来收取数据,收取之后继续睡眠(所以ping一个睡眠状态的Station,响应的时间要慢好多)。

            对于发送给这个Station的广播帧,其处理方式和普通帧有一点不同:当有广播帧要传达给这个Station的时候,AP会为这个Station缓存发送给它的广播帧,但是缓存的时间是DTIM(一般为300ms)。注意:单播帧缓存的时间不一定是多少,广播帧却缓存DTIM的时间。AP每发送一个Beacon的时候,都会将Dtim减少1,而Station睡眠的时候,会不时地醒来,查看一下Beacon帧中的dtim值。当Station发现其DTIM值变成0的时候,就醒来长一些的时间,看看有没有广播给它的数据,如果有的话就用类似Power Save Poll的帧接受,没有则继续睡眠。

            这里,接收数据是根据是否有more data类似的字段来确认是否有更多的数据的;重发的帧是用类似retry的字段来标记。另外注意,当Station进行Sleep的时候,还是可以主动Tranmit消息的,当Station主动Transmit消息的时候,它会等待Reply,所以这个时候,Receiver是on的状态。用一个图示来标识Sleep,Receive,Transmit时的电源消耗状况,大致如下:

     

              power

                   ^

    trans        |                   ------------------------

                   |                   |                       |

    receive     |        -----------|                       |

                   |        |                                  |

    sleep       |--------|                                  |--------------------

                   |----------------------------------------------------------------------> time

     

            可见不同状态,电源消耗状态不同(传送比接收更耗电),另外,如果电源供电不足,在某个状态中就会出现通信失败的情况。(好像ap上面broadcom芯片中的睡眠之后,醒来立即重新发送的时候经常开始会失败,可能就是这个原因)。

     

      6、建立Association

            下面是Station和Ap建立开放Association的过程:

            (0)Ap周期性地广播Beacon帧

            (1)Station广播Probe Request到达Ap

            (2)Ap向Station发送Probe Reponse

            (3)Station向Ap发送ACK

            (4)Station向Ap发送Authentication Request

            (5)Ap向Station发送ACK

            (6)Ap向Station发送Authentication Reponse

            (7)Station向Ap发送ACK

            (8)Station向Ap发送Association Request

            (9)Ap向Station发送ACK

            (10)Ap向Station发送Association Reponse

            (11)Station向Ap发送ACK

            (12)Station和Ap开始相互通信。

            可见,广播帧不用回复,单播帧需要用ACK确认,ACK本身不用被确认。

     

     

    四、补充

    ============================

            有待添加。

     

     

    五、其它

    ============================

            本文内容主要来自学习的总结以及网络,主要集中于无线网络中物理层以上相对比较常见的部分,如果想要理解更详细和全面的内容则需参考相关书籍以及网络协议。由于对此方面的知识也是在初步学习之中,若文章中有错误和不完整之处,谢谢读者指正。^_^

    展开全文
  • wifidog原理分析

    千次阅读 2016-08-25 19:08:16
    wifidog源码分析 - wifidog原理  wifidog是一个用于配合认证服务器实现无线网页认证功能的程序,常见的情景就是使用于公共场合的无线wifi接入点,首先移动设备会连接公共wifi接入点,之后会弹出网页要求...

    wifidog源码分析 - wifidog原理    

    wifidog是一个用于配合认证服务器实现无线网页认证功能的程序,常见的情景就是使用于公共场合的无线wifi接入点,首先移动设备会连接公共wifi接入点,之后会弹出网页要求输入用户名密码,认证过后才能够连入外网。其主页是http://dev.wifidog.org/

    1. 实现原理

      其实wifidog原理很简单,主要是通过管控iptables,配合认证服务器进行客户端的放行操作。wifidog在启动后都会自动启动三个线程,分别为客户端检测线程、wdctrl交互线程、认证服务器心跳检测线程。每当新用户连接无线AP并浏览网页时,wifidog会获取新用户的此次操作,并返回一个重定向到认证服务器的http于用户,此后用户通过认证服务器认证后,再继续浏览网页时,wifidog会询问认证服务器此用户权限,若放行则修改iptables放行此用户IP。

     

      主要流程如下

    • 添加关键路径对应的回调函数
    • 删除所有iptables路由表
    • 建立新的iptables路由表
    • 开启客户端检测线程(用于判断客户端是否在线,是否登出)
    • 开启wdctrl交互线程
    • 开启认证服务器心跳检测线程
    • 循环等待客户端连接(使用socket绑定2060端口并监听,实际上在建立新的iptables路由表规则时会将网关的80端口重定向到2060端口)

     

      回调函数

      回调函数主要用于根据用户http报文执行不同的操作,其原理就是分析http报文请求中有没有关键路径,若有,则执行关键路径对应的回调函数,若没有,则返回一个重定向到认证服务器的包给用户。一次典型的流程为

    • 用户连接无线AP,访问某网站(比如http://www.baidu.com)
    • wifidog获取到此http报文,检查是否包含关键路径,没有则返回重定向包给用户,将其重定向到认证服务器
    • 用户认证成功,认证服务器将用户重定向到无线AP网关,并包含关键路径"/wifidog/auth"和token
    • wifidog接收到用户重定向后访问的报文,检测到关键路径"/wifidog/auth",然后访问认证服务器进行token认证
    • 认证成功,wifidog修改iptables放行此用户(根据mac和ip进行放行)

     

      wifidog的iptables规则

      这一部分我没有仔细认真看源码,但可以推论出wifidog是怎么修改iptables的规则的,了解iptables基本原理的同学都清楚iptables实际上有两条路进行数据包处理,一条路会通过应用程序,一条路不同过应用程序,直接到POSTOUTPUT,而我认为wifidog建立的规则是

    • 只要是访问认证服务器的http请求都直接不通过wifidog发送出去
    • 只要是通过认证的客户端wifidog都会修改iptables让其数据直接从FORWARD到POSTOUTPUT,而不经过wifidog
    • 其他行为都必须进过wifidog处理

     

      客户端检测线程

      此线程每隔60s会遍历一次客户端列表,对每一个客户端列表统计流量,如果客户端在60s间隔内没有新产生的流量则不更新客户端的最新更新时间,当当前时间减去最新更新时间大于断线要求时间时,则会将此客户端从客户端列表删除,并修改iptables规则禁止其访问外部网络,然后发送此客户端登出包于认证服务器,认证服务器根据此登出包将此客户端做登出处理。如若没有超出断线要求时间,此线程还会发送客户端状态获取包于认证服务器,认证服务器返回此客户端在认证服务器上的信息,如若信息表示此客户端已在认证服务器上登出,wifidog则会执行此客户端下线操作。

     

      wdctrl交互线程

      其原理是使用unix socket进行进程间通信,具体实现在之后文章中体现

     

      认证服务器心跳检测线程

      原理也很简单,就是每隔60s将路由的一些系统信息发送给认证服务器,认证服务器接收到会返回一个回执

     

      循环等待客户端连接

      这里主要会接收到两种类型的客户端连接

    • 未认证的客户端打开网页操作,wifidog会接收到此http请求,并返回一个重定向到认证服务器的包于客户端
    • 经过认证服务器认证成功后,认证服务器自动将客户端重定向到无线AP的操作,wifidog接收到此类http请求后会检测关键路径"/tmp/wifidog",并把http请求中携带的token与认证服务器进行认证,认证成功后则修改iptables放行客户端。

     

    具体代码实现见之后章节




    wifidog源码分析 - 初始化阶段 



    Wifidog是一个linux下开源的认证网关软件,它主要用于配合认证服务器实现无线路由器的认证放行功能。

    wifidog是一个后台的服务程序,可以通过wdctrl命令对wifidog主程序进行控制。

    本文解释wifidog在启动阶段所做的初始化主要工作(代码片段1.1

    • 初始化配置(先将配置结构体初始化为默认值,在读取配置文件修改配置结构体)
    • 初始化已连接客户端列表(如果是通过wdctrl重启wifidog,将会读取之前wifidog的已连接客户端列表 代码片段1.2 代码片段1.3
    • 如无特殊情况,分离进程,建立守护进程 (代码片段1.1
    • 添加多个http请求回调函数(包括404错误回调函数) (见之后章节)
    • 摧毁删除现有的iptables路由表规则 (见之后章节)
    • 建立新的iptables路由表规则 (见之后章节)
    • 启动多个功能线程 (见之后章节)
    • 循环等待客户端连接 (见之后章节)

    代码片段1.1

    点击(此处)折叠或打开

    1. int main(int argc, char **argv) {

    2.     s_config *config = config_get_config(); //就是返回全局变量config结构体的地址
    3.     config_init(); //初始化全局变量config结构体为默认值

    4.     parse_commandline(argc, argv); //根据传入参数执行操作(如果参数有-x则会设置restart_orig_pid为已运行的wifidog的pid)

    5.     /* Initialize the config */
    6.     config_read(config->configfile); //根据配置文件设置全局变量config结构体
    7.     config_validate(); //判断GatewayInterface和AuthServer是否为空,空则无效退出程序。

    8.     /* Initializes the linked list of connected clients */
    9.     client_list_init(); //将已连接客户端链表置空。

    10.     /* Init the signals to catch chld/quit/etc */
    11.     init_signals(); //初始化一些信号

    12.     if (restart_orig_pid) { //用于restart,如果有已运行的wifidog,先会kill它
    13.         /*
    14.          * We were restarted and our parent is waiting for us to talk to it over the socket
    15.          */
    16.         get_clients_from_parent(); //从已运行的wifidog中获取客户端列表,详见 代码片段1.2

    17.         /*
    18.          * At this point the parent will start destroying itself and the firewall. Let it finish it's job before we continue
    19.          */

    20.         while (kill(restart_orig_pid, 0) != -1) { //kill已运行的wifidog
    21.             debug(LOG_INFO, "Waiting for parent PID %d to die before continuing loading", restart_orig_pid);
    22.             sleep(1);
    23.         }

    24.         debug(LOG_INFO, "Parent PID %d seems to be dead. Continuing loading.");
    25.     }

    26.     if (config->daemon) { //创建为守护进程,config->daemon默认值为-1

    27.         debug(LOG_INFO, "Forking into background");

    28.         switch(safe_fork()) {
    29.             case 0: /* child */
    30.                 setsid(); //创建新会话,脱离此终端,实现守护进程
    31.                 append_x_restartargv();
    32.                 main_loop(); //进入主循环(核心代码在此)。
    33.                 break;

    34.             default: /* parent */
    35.                 exit(0);
    36.                 break;
    37.         }
    38.     }
    39.     else {
    40.         append_x_restartargv();
    41.         main_loop();
    42.     }

    43.     return(0); /* never reached */
    44. }

    代码片段1.2(获取已启动的wifidog的客户端列表):

    此段代表描述了新启动的wifidog如何从已启动的wifidog程序中获取已连接的客户端列表。发送端见 代码片段1.3


    点击(此处)折叠或打开

    1. void get_clients_from_parent(void) {
    2.     int sock;
    3.     struct sockaddr_un sa_un;
    4.     s_config * config = NULL;
    5.     char linebuffer[MAX_BUF];
    6.     int len = 0;
    7.     char *running1 = NULL;
    8.     char *running2 = NULL;
    9.     char *token1 = NULL;
    10.     char *token2 = NULL;
    11.     char onechar;
    12.     char *command = NULL;
    13.     char *key = NULL;
    14.     char *value = NULL;
    15.     t_client * client = NULL;
    16.     t_client * lastclient = NULL;

    17.     config = config_get_config();
    18.     
    19.     debug(LOG_INFO, "Connecting to parent to download clients");

    20.     /* 连接socket */
    21.     sock = socket(AF_UNIX, SOCK_STREAM, 0);
    22.     memset(&sa_un, 0, sizeof(sa_un));
    23.     sa_un.sun_family = AF_UNIX;
    24.     strncpy(sa_un.sun_path, config->internal_sock, (sizeof(sa_un.sun_path) - 1)); //config->internal_sock的值为"/tmp/wifidog.sock"

    25.     /* 连接已启动的wifidog */
    26.     if (connect(sock, (struct sockaddr *)&sa_un, strlen(sa_un.sun_path) + sizeof(sa_un.sun_family))) {
    27.         debug(LOG_ERR, "Failed to connect to parent (%s) - client list not downloaded", strerror(errno));
    28.         return;
    29.     }

    30.     debug(LOG_INFO, "Connected to parent. Downloading clients");

    31.     LOCK_CLIENT_LIST();

    32.     command = NULL;
    33.     memset(linebuffer, 0, sizeof(linebuffer));
    34.     len = 0;
    35.     client = NULL;
    36.     /* 接收数据,逐个字符接收 */
    37.     /* 数据包格式为 CLIENT|ip=%s|mac=%s|token=%s|fw_connection_state=%u|fd=%d|counters_incoming=%llu|counters_outgoing=%llu|counters_last_updated=%lu\*/
    38.     while (read(sock, &onechar, 1) == 1) {
    39.         if (onechar == '\n') {
    40.             /* 如果接收到末尾('\n'),则转为'\0' */
    41.             onechar = '\0';
    42.         }
    43.         linebuffer[len++] = onechar;
    44.         
    45.         if (!onechar) {
    46.             /* 以下将数据转化为t_client结构体添加到客户端列表 */
    47.             debug(LOG_DEBUG, "Received from parent: [%s]", linebuffer);
    48.             running1 = linebuffer;
    49.             while ((token1 = strsep(&running1, "|")) != NULL) {
    50.                 if (!command) {
    51.                     /* The first token is the command */
    52.                     command = token1;
    53.                 }
    54.                 else {
    55.                 /* Token1 has something like "foo=bar" */
    56.                     running2 = token1;
    57.                     key = value = NULL;
    58.                     while ((token2 = strsep(&running2, "=")) != NULL) {
    59.                         if (!key) {
    60.                             key = token2;
    61.                         }
    62.                         else if (!value) {
    63.                             value = token2;
    64.                         }
    65.                     }
    66.                 }

    67.                 if (strcmp(command, "CLIENT") == 0) {
    68.                     /* This line has info about a client in the client list */
    69.                     if (!client) {
    70.                         /* Create a new client struct */
    71.                         client = safe_malloc(sizeof(t_client));
    72.                         memset(client, 0, sizeof(t_client));
    73.                     }
    74.                 }

    75.                 if (key && value) {
    76.                     if (strcmp(command, "CLIENT") == 0) {
    77.                         /* Assign the key into the appropriate slot in the connection structure */
    78.                         if (strcmp(key, "ip") == 0) {
    79.                             client->ip = safe_strdup(value);
    80.                         }
    81.                         else if (strcmp(key, "mac") == 0) {
    82.                             client->mac = safe_strdup(value);
    83.                         }
    84.                         else if (strcmp(key, "token") == 0) {
    85.                             client->token = safe_strdup(value);
    86.                         }
    87.                         else if (strcmp(key, "fw_connection_state") == 0) {
    88.                             client->fw_connection_state = atoi(value);
    89.                         }
    90.                         else if (strcmp(key, "fd") == 0) {
    91.                             client->fd = atoi(value);
    92.                         }
    93.                         else if (strcmp(key, "counters_incoming") == 0) {
    94.                             client->counters.incoming_history = atoll(value);
    95.                             client->counters.incoming = client->counters.incoming_history;
    96.                         }
    97.                         else if (strcmp(key, "counters_outgoing") == 0) {
    98.                             client->counters.outgoing_history = atoll(value);
    99.                             client->counters.outgoing = client->counters.outgoing_history;
    100.                         }
    101.                         else if (strcmp(key, "counters_last_updated") == 0) {
    102.                             client->counters.last_updated = atol(value);
    103.                         }
    104.                         else {
    105.                             debug(LOG_NOTICE, "I don't know how to inherit key [%s] value [%s] from parent", key, value);
    106.                         }
    107.                     }
    108.                 }
    109.             }

    110.             /* End of parsing this command */
    111.             if (client) {
    112.                 /* Add this client to the client list */
    113.                 if (!firstclient) {
    114.                     firstclient = client;
    115.                     lastclient = firstclient;
    116.                 }
    117.                 else {
    118.                     lastclient->next = client;
    119.                     lastclient = client;
    120.                 }
    121.             }

    122.             /* Clean up */
    123.             command = NULL;
    124.             memset(linebuffer, 0, sizeof(linebuffer));
    125.             len = 0;
    126.             client = NULL;
    127.         }
    128.     }

    129.     UNLOCK_CLIENT_LIST();
    130.     debug(LOG_INFO, "Client list downloaded successfully from parent");

    131.     close(sock);
    132. }

    代码片段1.3 (已启动的wifidog发送客户端列表到新启动的wifidog):

    点击(此处)折叠或打开

    1. //thread_wdctl_handler(void *arg)函数是wifidog启动后自动创建的控制线程,主要用于与wdctrl进行socket通信,根据wdctrl命令执行不同的操作。这里我们着重讲解的是wdctrl发送restart后wifidog的执行逻辑。
    2. static void *
    3. thread_wdctl_handler(void *arg)
    4. {
    5.     int fd,
    6.         done,
    7.         i;
    8.     char request[MAX_BUF];
    9.     ssize_t read_bytes,
    10.         len;

    11.     debug(LOG_DEBUG, "Entering thread_wdctl_handler....");

    12.     fd = (int)arg;
    13.     
    14.     debug(LOG_DEBUG, "Read bytes and stuff from %d", fd);

    15.     /* 初始化变量 */
    16.     read_bytes = 0;
    17.     done = 0;
    18.     memset(request, 0, sizeof(request));
    19.     
    20.     /* 读取命令 */
    21.     while (!done && read_bytes < (sizeof(request) - 1)) {
    22.         len = read(fd, request + read_bytes,
    23.                 sizeof(request) - read_bytes); //读取wdctrl发送的命令

    24.         /* 判断命令正确性 */
    25.         for (= read_bytes; i < (read_bytes + len); i++) {
    26.             if (request[i] == '\r' || request[i] == '\n') {
    27.                 request[i] = '\0';
    28.                 done = 1;
    29.             }
    30.         }
    31.         
    32.         /* Increment position */
    33.         read_bytes += len;
    34.     }

    35.         //判断命令
    36.     if (strncmp(request, "status", 6) == 0) {
    37.         wdctl_status(fd);
    38.     } else if (strncmp(request, "stop", 4) == 0) {
    39.         wdctl_stop(fd);
    40.     } else if (strncmp(request, "reset", 5) == 0) {
    41.         wdctl_reset(fd, (request + 6));
    42.     } else if (strncmp(request, "restart", 7) == 0) {
    43.         wdctl_restart(fd); //执行wdctl_restart(int afd)函数
    44.     }

    45.     if (!done) {
    46.         debug(LOG_ERR, "Invalid wdctl request.");
    47.                 //关闭套接字
    48.         shutdown(fd, 2);
    49.         close(fd);
    50.         pthread_exit(NULL);
    51.     }

    52.     debug(LOG_DEBUG, "Request received: [%s]", request);
    53.     
    54.         //关闭套接字
    55.     shutdown(fd, 2);
    56.     close(fd);
    57.     debug(LOG_DEBUG, "Exiting thread_wdctl_handler....");

    58.     return NULL;
    59. }


    60. //wdctl_restart(int afd)函数详解
    61. static void
    62. wdctl_restart(int afd)
    63. {
    64.     int sock,
    65.         fd;
    66.     char *sock_name;
    67.     struct sockaddr_un sa_un;
    68.     s_config * conf = NULL;
    69.     t_client * client = NULL;
    70.     char * tempstring = NULL;
    71.     pid_t pid;
    72.     ssize_t written;
    73.     socklen_t len;

    74.     conf = config_get_config();

    75.     debug(LOG_NOTICE, "Will restart myself");

    76.     /*
    77.      * 准备内部连接socket
    78.      */
    79.     memset(&sa_un, 0, sizeof(sa_un));
    80.     sock_name = conf->internal_sock; //conf->internal_sock值为"/tmp/wifidog.sock"
    81.     debug(LOG_DEBUG, "Socket name: %s", sock_name);

    82.     if (strlen(sock_name) > (sizeof(sa_un.sun_path) - 1)) {
    83.        
    84.         debug(LOG_ERR, "INTERNAL socket name too long");
    85.         return;
    86.     }

    87.     debug(LOG_DEBUG, "Creating socket");
    88.     sock = socket(PF_UNIX, SOCK_STREAM, 0); //建立内部socket套接字

    89.     debug(LOG_DEBUG, "Got internal socket %d", sock);

    90.     /* 如果sock_name文件存在,则删除*/
    91.     unlink(sock_name);

    92.     debug(LOG_DEBUG, "Filling sockaddr_un");
    93.     strcpy(sa_un.sun_path, sock_name); 
    94.     sa_un.sun_family = AF_UNIX;
    95.     
    96.     debug(LOG_DEBUG, "Binding socket (%s) (%d)", sa_un.sun_path, strlen(sock_name));
    97.     
    98.    
    99.     if (bind(sock, (struct sockaddr *)&sa_un, strlen(sock_name) + sizeof(sa_un.sun_family))) {
    100.         debug(LOG_ERR, "Could not bind internal socket: %s", strerror(errno));
    101.         return;
    102.     }

    103.     if (listen(sock, 5)) {
    104.         debug(LOG_ERR, "Could not listen on internal socket: %s", strerror(errno));
    105.         return;
    106.     }
    107.     
    108.     /*
    109.      * socket建立完成,创建子进程
    110.      */
    111.     debug(LOG_DEBUG, "Forking in preparation for exec()...");
    112.     pid = safe_fork();
    113.     if (pid > 0) {
    114.         /* 父进程 */

    115.         /* 等待子进程连接此socket :*/
    116.         debug(LOG_DEBUG, "Waiting for child to connect on internal socket");
    117.         len = sizeof(sa_un);
    118.         if ((fd = accept(sock, (struct sockaddr *)&sa_un, &len)) == -1){ //接受连接
    119.             debug(LOG_ERR, "Accept failed on internal socket: %s", strerror(errno));
    120.             close(sock);
    121.             return;
    122.         }

    123.         close(sock);

    124.         debug(LOG_DEBUG, "Received connection from child. Sending them all existing clients");

    125.         /*子进程已经完成连接,发送客户端列表 */
    126.         LOCK_CLIENT_LIST();
    127.         client = client_get_first_client(); //获取第一个客户端
    128.         while (client) {
    129.             /* Send this client */
    130.             safe_asprintf(&tempstring, "CLIENT|ip=%s|mac=%s|token=%s|fw_connection_state=%u|fd=%d|counters_incoming=%llu|counters_outgoing=%llu|counters_last_updated=%lu\n",client->ip, client->mac, client->token, client->fw_connection_state, client->fd, client->counters.incoming, client->counters.outgoing, client->counters.last_updated);
    131.             debug(LOG_DEBUG, "Sending to child client data: %s", tempstring);
    132.             len = 0;
    133.             while (len != strlen(tempstring)) {
    134.                 written = write(fd, (tempstring + len), strlen(tempstring) - len); //发送给子进程
    135.                 if (written == -1) {
    136.                     debug(LOG_ERR, "Failed to write client data to child: %s", strerror(errno));
    137.                     free(tempstring);
    138.                     break;
    139.                 }
    140.                 else {
    141.                     len += written;
    142.                 }
    143.             }
    144.             free(tempstring);
    145.             client = client->next;
    146.         }
    147.         UNLOCK_CLIENT_LIST();

    148.         close(fd);

    149.         debug(LOG_INFO, "Sent all existing clients to child. Committing suicide!");

    150.         shutdown(afd, 2);
    151.         close(afd);

    152.         
    153.         wdctl_stop(afd);
    154.     }
    155.     else {
    156.         /* 子进程,先关闭资源 */
    157.         close(wdctl_socket_server);
    158.         close(icmp_fd);
    159.         close(sock);
    160.         shutdown(afd, 2);
    161.         close(afd);
    162.         debug(LOG_NOTICE, "Re-executing myself (%s)", restartargv[0]);

    163.         setsid();
    164.         execvp(restartargv[0], restartargv); //执行外部命令,这里重新启动wifidog
    165.         
    166.         debug(LOG_ERR, "I failed to re-execute myself: %s", strerror(errno));
    167.         debug(LOG_ERR, "Exiting without cleanup");
    168.         exit(1);
    169.     }
    170. }

    小结

      客户端列表只有在restart命令中才会执行,实际上流程就是

    • 父wifidog准备socket
    • 父wifidog启动子wifidog
    • 子wifidog连接父wifidog
    • 客户端列表传递

    • 子wifidog终止父wifidog


    main_loop

      此函数几乎相当于我们写程序的main函数,主要功能都是在这里面实现的,函数主要实现了主循环,并启动了三个线程,这三个线程的功能具体见wifidog源码分析 - wifidog原理,在此函数最后主循环中会等待用户连接,新用户只要通过浏览器打开非认证服务器网页时主循环就会监听到,监听到后会启动一个处理线程。其流程为

    • 设置程序启动时间
    • 获取网关信息
    • 绑定http端口(80重定向到了2060)
    • 设置关键路径和404错误的回调函数
    • 重新建立iptables规则
    • 启动客户端检测线程 (稍后文章分析)
    • 启动wdctrl交互线程 (稍后文章分析)
    • 认证服务器心跳检测线程 (稍后文章分析)
    • 循环等待用户http请求,为每个请求启动一个处理线程。(代码片段1.2 代码片段1.3 代码片段1.4
    代码片段1.1

    1. static void
    2. main_loop(void)
    3. {
    4.     int result;
    5.     pthread_t tid;
    6.     s_config *config = config_get_config();
    7.     request *r;
    8.     void **params;

    9.     /* 设置启动时间 */
    10.     if (!started_time) {
    11.         debug(LOG_INFO, "Setting started_time");
    12.         started_time = time(NULL);
    13.     }
    14.     else if (started_time < MINIMUM_STARTED_TIME) {
    15.         debug(LOG_WARNING, "Detected possible clock skew - re-setting started_time");
    16.         started_time = time(NULL);
    17.     }

    18.     /* 获取网关IP,失败退出程序 */
    19.     if (!config->gw_address) {
    20.         debug(LOG_DEBUG, "Finding IP address of %s", config->gw_interface);
    21.         if ((config->gw_address = get_iface_ip(config->gw_interface)) == NULL) {
    22.             debug(LOG_ERR, "Could not get IP address information of %s, exiting...", config->gw_interface);
    23.             exit(1);
    24.         }
    25.         debug(LOG_DEBUG, "%s = %s", config->gw_interface, config->gw_address);
    26.     }

    27.     /* 获取网关ID,失败退出程序 */
    28.     if (!config->gw_id) {
    29.         debug(LOG_DEBUG, "Finding MAC address of %s", config->gw_interface);
    30.         if ((config->gw_id = get_iface_mac(config->gw_interface)) == NULL) {
    31.             debug(LOG_ERR, "Could not get MAC address information of %s, exiting...", config->gw_interface);
    32.             exit(1);
    33.         }
    34.         debug(LOG_DEBUG, "%s = %s", config->gw_interface, config->gw_id);
    35.     }

    36.     /* 初始化监听网关2060端口的socket */
    37.     debug(LOG_NOTICE, "Creating web server on %s:%d", config->gw_address, config->gw_port);
    38.     
    39.     if ((webserver = httpdCreate(config->gw_address, config->gw_port)) == NULL) {
    40.         debug(LOG_ERR, "Could not create web server: %s", strerror(errno));
    41.         exit(1);
    42.     }

    43.     debug(LOG_DEBUG, "Assigning callbacks to web server");
    44.     /* 设置关键路径及其回调函数,在代码片段1.2中会使用到 */
    45.     httpdAddCContent(webserver, "/", "wifidog", 0, NULL, http_callback_wifidog);
    46.     httpdAddCContent(webserver, "/wifidog", "", 0, NULL, http_callback_wifidog);
    47.     httpdAddCContent(webserver, "/wifidog", "about", 0, NULL, http_callback_about);
    48.     httpdAddCContent(webserver, "/wifidog", "status", 0, NULL, http_callback_status);
    49.     httpdAddCContent(webserver, "/wifidog", "auth", 0, NULL, http_callback_auth);
    50.     /* 设置404错误回调函数,在里面实现了重定向至认证服务器 */
    51.     httpdAddC404Content(webserver, http_callback_404);

    52.     /* 清除iptables规则 */
    53.     fw_destroy();
    54.     /* 重新设置iptables规则 */
    55.     if (!fw_init()) {
    56.         debug(LOG_ERR, "FATAL: Failed to initialize firewall");
    57.         exit(1);
    58.     }

    59.     /* 客户端检测线程 */
    60.     result = pthread_create(&tid_fw_counter, NULL, (void *)thread_client_timeout_check, NULL);
    61.     if (result != 0) {
    62.         debug(LOG_ERR, "FATAL: Failed to create a new thread (fw_counter) - exiting");
    63.         termination_handler(0);
    64.     }
    65.     pthread_detach(tid_fw_counter);

    66.     /* wdctrl交互线程 */
    67.     result = pthread_create(&tid, NULL, (void *)thread_wdctl, (void *)safe_strdup(config->wdctl_sock));
    68.     if (result != 0) {
    69.         debug(LOG_ERR, "FATAL: Failed to create a new thread (wdctl) - exiting");
    70.         termination_handler(0);
    71.     }
    72.     pthread_detach(tid);
    73.     
    74.     /* 认证服务器心跳检测线程 */
    75.     result = pthread_create(&tid_ping, NULL, (void *)thread_ping, NULL);
    76.     if (result != 0) {
    77.         debug(LOG_ERR, "FATAL: Failed to create a new thread (ping) - exiting");
    78.         termination_handler(0);
    79.     }
    80.     pthread_detach(tid_ping);
    81.     
    82.     debug(LOG_NOTICE, "Waiting for connections");
    83.     while(1) {
    84.         /* 监听2060端口等待用户http请求 */
    85.         r = httpdGetConnection(webserver, NULL);

    86.         /* 错误处理 */
    87.         if (webserver->lastError == -1) {
    88.             /* Interrupted system call */
    89.             continue; /* restart loop */
    90.         }
    91.         else if (webserver->lastError < -1) {
    92.             debug(LOG_ERR, "FATAL: httpdGetConnection returned unexpected value %d, exiting.", webserver->lastError);
    93.             termination_handler(0);
    94.         }
    95.         else if (!= NULL) {
    96.             /* 用户http请求接收成功 */
    97.             debug(LOG_INFO, "Received connection from %s, spawning worker thread", r->clientAddr);
    98.             params = safe_malloc(* sizeof(void *));
    99.             *params = webserver;
    100.             *(params + 1) = r;

    101.             /* 开启http请求处理线程 */
    102.             result = pthread_create(&tid, NULL, (void *)thread_httpd, (void *)params);
    103.             if (result != 0) {
    104.                 debug(LOG_ERR, "FATAL: Failed to create a new thread (httpd) - exiting");
    105.                 termination_handler(0);
    106.             }
    107.             pthread_detach(tid);
    108.         }
    109.         else {
    110.             ;
    111.         }
    112.     }

    113.     /* never reached */
    114. }

    用户连接启动线程(void thread_httpd(void * args))

    代码片段1.2

    此段代码是当有新用户(未认证的用户 代码片段1.3,已在认证服务器上认证但没有在wifidog认证的用户 代码片段1.4)连接时创建的线程,其主要功能为

    • 获取用户浏览器发送过来的http报头
    • 分析http报头,分析是否包含关键路径
    • 不包含关键路径则调用404回调函数
    • 包含关键路径则执行关键路径回调函数(这里主要讲解"/wifidog/auth"路径)

    1. void
    2. thread_httpd(void *args)
    3. {
    4.     void **params;
    5.     httpd *webserver;
    6.     request *r;
    7.     
    8.     params = (void **)args;
    9.     webserver = *params;
    10.     r = *(params + 1);
    11.     free(params);
    12.     
    13.     /* 获取http报文 */
    14.     if (httpdReadRequest(webserver, r) == 0) {
    15.         debug(LOG_DEBUG, "Processing request from %s", r->clientAddr);
    16.         debug(LOG_DEBUG, "Calling httpdProcessRequest() for %s", r->clientAddr);
    17.         /* 分析http报文 */
    18.         httpdProcessRequest(webserver, r);
    19.         debug(LOG_DEBUG, "Returned from httpdProcessRequest() for %s", r->clientAddr);
    20.     }
    21.     else {
    22.         debug(LOG_DEBUG, "No valid request received from %s", r->clientAddr);
    23.     }
    24.     debug(LOG_DEBUG, "Closing connection with %s", r->clientAddr);
    25.     httpdEndRequest(r);
    26. }



    27. /* 被thread_httpd调用 */
    28. void httpdProcessRequest(httpd *server, request *r)
    29. {
    30.     char dirName[HTTP_MAX_URL],
    31.         entryName[HTTP_MAX_URL],
    32.         *cp;
    33.     httpDir *dir;
    34.     httpContent *entry;

    35.     r->response.responseLength = 0;
    36.     strncpy(dirName, httpdRequestPath(r), HTTP_MAX_URL);
    37.     dirName[HTTP_MAX_URL-1]=0;
    38.     cp = rindex(dirName, '/');
    39.     if (cp == NULL)
    40.     {
    41.         printf("Invalid request path '%s'\n",dirName);
    42.         return;
    43.     }
    44.     strncpy(entryName, cp + 1, HTTP_MAX_URL);
    45.     entryName[HTTP_MAX_URL-1]=0;
    46.     if (cp != dirName)
    47.         *cp = 0;
    48.     else
    49.         *(cp+1) = 0;

    50.      /* 获取http报文中的关键路径,在main_loop中已经设置 */
    51.     dir = _httpd_findContentDir(server, dirName, HTTP_FALSE);
    52.     if (dir == NULL)
    53.     {
    54.         /* http报文中未包含关键路径,执行404回调函数(在404回调函数中新用户被重定向到认证服务器),见代码片段1.*/
    55.         _httpd_send404(server, r);
    56.         _httpd_writeAccessLog(server, r);
    57.         return;
    58.     }
    59.     /* 获取关键路径内容描述符 */
    60.     entry = _httpd_findContentEntry(r, dir, entryName);
    61.     if (entry == NULL)
    62.     {
    63.         _httpd_send404(server, r);
    64.         _httpd_writeAccessLog(server, r);
    65.         return;
    66.     }
    67.     if (entry->preload)
    68.     {
    69.         if ((entry->preload)(server) < 0)
    70.         {
    71.             _httpd_writeAccessLog(server, r);
    72.             return;
    73.         }
    74.     }
    75.     switch(entry->type)
    76.     {
    77.         case HTTP_C_FUNCT:
    78.         case HTTP_C_WILDCARD:
    79.             /* 如果是被认证服务器重定向到网关的用户,此处的关键路径为"/wifidog/auth",并执行回调函数 */
    80.             (entry->function)(server, r);
    81.             break;

    82.         case HTTP_STATIC:
    83.             _httpd_sendStatic(server, r, entry->data);
    84.             break;

    85.         case HTTP_FILE:
    86.             _httpd_sendFile(server, r, entry->path);
    87.             break;

    88.         case HTTP_WILDCARD:
    89.             if (_httpd_sendDirectoryEntry(server, r, entry,
    90.                         entryName)<0)
    91.             {
    92.                 _httpd_send404(server, r);
    93.             }
    94.             break;
    95.     }
    96.     _httpd_writeAccessLog(server, r);
    97. }

    代码片段1.3

    此段代码表示wifidog是如何通过http 404回调函数实现客户端重定向了,实际上就是在404回调函数中封装了一个307状态的http报头,http的307状态在http协议中就是用于重定向的,封装完成后通过已经与客户端连接的socket返回给客户端。步骤流程为

    • 判断本机是否处于离线状态
    • 判断认证服务器是否在线
    • 封装http 307报文
    • 发送于目标客户端

    1. void
    2. http_callback_404(httpd *webserver, request *r)
    3. {
    4.     char tmp_url[MAX_BUF],
    5.             *url;
    6.     s_config *config = config_get_config();
    7.     t_auth_serv *auth_server = get_auth_server();

    8.     memset(tmp_url, 0, sizeof(tmp_url));

    9.         snprintf(tmp_url, (sizeof(tmp_url) - 1), "http://%s%s%s%s",
    10.                         r->request.host,
    11.                         r->request.path,
    12.                         r->request.query[0] ? "?" : "",
    13.                         r->request.query);
    14.     url = httpdUrlEncode(tmp_url);

    15.     if (!is_online()) {
    16.         /* 本机处于离线状态,此函数调用结果由认证服务器检测线程设置 */
    17.         char * buf;
    18.         safe_asprintf(&buf, 
    19.             "

      We apologize, but it seems that the internet connection that powers this hotspot is temporarily unavailable.


      "
    20.             "

      If at all possible, please notify the owners of this hotspot that the internet connection is out of service.


      "
    21.             "

      The maintainers of this network are aware of this disruption. We hope that this situation will be resolved soon.


      "
    22.             "

      In a while please click here to try your request again.


      ", tmp_url);

    23.                 send_http_page(r, "Uh oh! Internet access unavailable!", buf);
    24.         free(buf);
    25.         debug(LOG_INFO, "Sent %s an apology since I am not online - no point sending them to auth server", r->clientAddr);
    26.     }
    27.     else if (!is_auth_online()) {
    28.         /* 认证服务器处于离线状态 */
    29.         char * buf;
    30.         safe_asprintf(&buf, 
    31.             "

      We apologize, but it seems that we are currently unable to re-direct you to the login screen.


      "
    32.             "

      The maintainers of this network are aware of this disruption. We hope that this situation will be resolved soon.


      "
    33.             "

      In a couple of minutes please click here to try your request again.


      ", tmp_url);

    34.                 send_http_page(r, "Uh oh! Login screen unavailable!", buf);
    35.         free(buf);
    36.         debug(LOG_INFO, "Sent %s an apology since auth server not online - no point sending them to auth server", r->clientAddr);
    37.     }
    38.     else {
    39.         /* 本机与认证服务器都在线,返回重定向包于客户端 */
    40.         char *urlFragment;
    41.         safe_asprintf(&urlFragment, "%sgw_address=%s&gw_port=%d&gw_id=%s&url=%s",
    42.             auth_server->authserv_login_script_path_fragment,
    43.             config->gw_address,
    44.             config->gw_port, 
    45.             config->gw_id,
    46.             url);
    47.         debug(LOG_INFO, "Captured %s requesting [%s] and re-directing them to login page", r->clientAddr, url);
    48.         http_send_redirect_to_auth(r, urlFragment, "Redirect to login page"); /* 实际上此函数中通过socket返回一个307状态的http报头给客户端,里面包含有认证服务器地址 */
    49.         free(urlFragment);
    50.     }
    51.     free(url);
    52. }


    代码片段1.4

    此段表明当客户端已经在认证服务器确认登陆,认证服务器将客户端重新重定向回网关,并在重定向包中包含关键路径"/wifidog/auth"和token,认证服务器所执行的操作。


    1. void 
    2. http_callback_auth(httpd *webserver, request *r)
    3. {
    4.     t_client *client;
    5.     httpVar * token;
    6.     char *mac;
    7.     /* 判断http报文是否包含登出logout */
    8.     httpVar *logout = httpdGetVariableByName(r, "logout");
    9.     if ((token = httpdGetVariableByName(r, "token"))) {
    10.         /* 获取http报文中的token */
    11.         if (!(mac = arp_get(r->clientAddr))) {
    12.             /* 获取客户端mac地址失败 */
    13.             debug(LOG_ERR, "Failed to retrieve MAC address for ip %s", r->clientAddr);
    14.             send_http_page(r, "WiFiDog Error", "Failed to retrieve your MAC address");
    15.         } else {
    16.             LOCK_CLIENT_LIST();
    17.             /* 判断客户端是否存在于列表中 */
    18.             if ((client = client_list_find(r->clientAddr, mac)) == NULL) {
    19.                 debug(LOG_DEBUG, "New client for %s", r->clientAddr);
    20.                 /* 将此客户端添加到客户端列表 */
    21.                 client_list_append(r->clientAddr, mac, token->value);
    22.             } else if (logout) {
    23.                 /* http报文为登出 */
    24.                 t_authresponse authresponse;
    25.                 s_config *config = config_get_config();
    26.                 unsigned long long incoming = client->counters.incoming;
    27.                 unsigned long long outgoing = client->counters.outgoing;
    28.                 char *ip = safe_strdup(client->ip);
    29.                 char *urlFragment = NULL;
    30.                 t_auth_serv *auth_server = get_auth_server();
    31.                 /* 修改iptables禁止客户端访问外网 */ 
    32.                 fw_deny(client->ip, client->mac, client->fw_connection_state);
    33.                 /* 从客户端列表中删除此客户端 */
    34.                 client_list_delete(client);
    35.                 debug(LOG_DEBUG, "Got logout from %s", client->ip);
    36.                 
    37.                 if (config->auth_servers != NULL) {
    38.                     UNLOCK_CLIENT_LIST();
    39.                     /* 发送登出认证包给认证服务器 */
    40.                     auth_server_request(&authresponse, REQUEST_TYPE_LOGOUT, ip, mac, token->value, 
    41.                                         incoming, outgoing);
    42.                     LOCK_CLIENT_LIST();
    43.                     
    44.                     /* 将客户端重定向到认证服务器 */
    45.                     debug(LOG_INFO, "Got manual logout from client ip %s, mac %s, token %s"
    46.                     "- redirecting them to logout message", client->ip, client->mac, client->token);
    47.                     safe_asprintf(&urlFragment, "%smessage=%s",
    48.                         auth_server->authserv_msg_script_path_fragment,
    49.                         GATEWAY_MESSAGE_ACCOUNT_LOGGED_OUT
    50.                     );
    51.                     http_send_redirect_to_auth(r, urlFragment, "Redirect to logout message");
    52.                     free(urlFragment);
    53.                 }
    54.                 free(ip);
    55.              } 
    56.              else {
    57.                 debug(LOG_DEBUG, "Client for %s is already in the client list", client->ip);
    58.             }
    59.             UNLOCK_CLIENT_LIST();
    60.             if (!logout) {
    61.                 /* 通过认证服务器认证此客户端token */
    62.                 authenticate_client(r);
    63.             }
    64.             free(mac);
    65.         }
    66.     } else {
    67.         send_http_page(r, "WiFiDog error", "Invalid token");
    68.     }
    69. }

    70. /* 此函数用于提交token到认证服务器进行认证 */
    71. void
    72. authenticate_client(request *r)
    73. {
    74.     t_client *client;
    75.     t_authresponse auth_response;
    76.     char *mac,
    77.         *token;
    78.     char *urlFragment = NULL;
    79.     s_config *config = NULL;
    80.     t_auth_serv *auth_server = NULL;

    81.     LOCK_CLIENT_LIST();

    82.     client = client_list_find_by_ip(r->clientAddr);
    83.     /* 判断此客户端是否在列表中 */
    84.     if (client == NULL) {
    85.         debug(LOG_ERR, "Could not find client for %s", r->clientAddr);
    86.         UNLOCK_CLIENT_LIST();
    87.         return;
    88.     }
    89.     
    90.     mac = safe_strdup(client->mac);
    91.     token = safe_strdup(client->token);
    92.     
    93.     UNLOCK_CLIENT_LIST();
    94.     
    95.     /* 提交token、客户端ip、客户端mac至认证服务器 */
    96.     auth_server_request(&auth_response, REQUEST_TYPE_LOGIN, r->clientAddr, mac, token, 0, 0);
    97.     
    98.     LOCK_CLIENT_LIST();
    99.     
    100.     /*再次判断客户端是否存在于列表中,保险起见,因为有可能在于认证服务器认证过程中,客户端检测线程把此客户端下线 */
    101.     client = client_list_find(r->clientAddr, mac);
    102.     
    103.     if (client == NULL) {
    104.         debug(LOG_ERR, "Could not find client node for %s (%s)", r->clientAddr, mac);
    105.         UNLOCK_CLIENT_LIST();
    106.         free(token);
    107.         free(mac);
    108.         return;
    109.     }
    110.     
    111.     free(token);
    112.     free(mac);

    113.     config = config_get_config();
    114.     auth_server = get_auth_server();

    115.         /* 判断认证服务器认证结果 */
    116.     switch(auth_response.authcode) {

    117.     case AUTH_ERROR:
    118.         /* 认证错误 */
    119.         debug(LOG_ERR, "Got %d from central server authenticating token %s from %s at %s", auth_response, client->token, client->ip, client->mac);
    120.         send_http_page(r, "Error!", "Error: We did not get a valid answer from the central server");
    121.         break;

    122.     case AUTH_DENIED:
    123.         /* 认证服务器拒绝此客户端 */
    124.         debug(LOG_INFO, "Got DENIED from central server authenticating token %s from %s at %s - redirecting them to denied message", client->token, client->ip, client->mac);
    125.         safe_asprintf(&urlFragment, "%smessage=%s",
    126.             auth_server->authserv_msg_script_path_fragment,
    127.             GATEWAY_MESSAGE_DENIED
    128.         );
    129.         http_send_redirect_to_auth(r, urlFragment, "Redirect to denied message");
    130.         free(urlFragment);
    131.         break;

    132.     case AUTH_VALIDATION:
    133.         /* 认证服务器处于等待此客户端电子邮件确认回执状态 */
    134.         debug(LOG_INFO, "Got VALIDATION from central server authenticating token %s from %s at %s"
    135.                 "- adding to firewall and redirecting them to activate message", client->token,
    136.                 client->ip, client->mac);
    137.         client->fw_connection_state = FW_MARK_PROBATION;
    138.         fw_allow(client->ip, client->mac, FW_MARK_PROBATION);
    139.         safe_asprintf(&urlFragment, "%smessage=%s",
    140.             auth_server->authserv_msg_script_path_fragment,
    141.             GATEWAY_MESSAGE_ACTIVATE_ACCOUNT
    142.         );
    143.         http_send_redirect_to_auth(r, urlFragment, "Redirect to activate message");
    144.         free(urlFragment);
    145.         break;

    146.     case AUTH_ALLOWED:
    147.         /* 认证通过 */
    148.         debug(LOG_INFO, "Got ALLOWED from central server authenticating token %s from %s at %s - "
    149.                 "adding to firewall and redirecting them to portal", client->token, client->ip, client->mac);
    150.         client->fw_connection_state = FW_MARK_KNOWN;
    151.         fw_allow(client->ip, client->mac, FW_MARK_KNOWN);
    152.         served_this_session++;
    153.         safe_asprintf(&urlFragment, "%sgw_id=%s",
    154.             auth_server->authserv_portal_script_path_fragment,
    155.             config->gw_id
    156.         );
    157.         http_send_redirect_to_auth(r, urlFragment, "Redirect to portal");
    158.         free(urlFragment);
    159.         break;

    160.     case AUTH_VALIDATION_FAILED:
    161.          /* 电子邮件确认回执超时 */
    162.         debug(LOG_INFO, "Got VALIDATION_FAILED from central server authenticating token %s from %s at %s "
    163.                 "- redirecting them to failed_validation message", client->token, client->ip, client->mac);
    164.         safe_asprintf(&urlFragment, "%smessage=%s",
    165.             auth_server->authserv_msg_script_path_fragment,
    166.             GATEWAY_MESSAGE_ACCOUNT_VALIDATION_FAILED
    167.         );
    168.         http_send_redirect_to_auth(r, urlFragment, "Redirect to failed validation message");
    169.         free(urlFragment);
    170.         break;

    171.     default:
    172.         debug(LOG_WARNING, "I don't know what the validation code %d means for token %s from %s at %s - sending error message", auth_response.authcode, client->token, client->ip, client->mac);
    173.         send_http_page(r, "Internal Error", "We can not validate your request at this time");
    174.         break;

    175.     }

    176.     UNLOCK_CLIENT_LIST();
    177.     return;
    178. }



    引言

      当wifidog启动时,会启动一个线程(thread_client_timeout_check)维护客户端列表,具体就是wifidog必须定时检测客户端列表中的每个客户端是否在线,而wifidog是通过两种方式进行检测客户端在线情况,一种是定时通过iptables获取客户端出入总流量更新客户端时间,通过最近更新时间进行判断(有新的出入流量则更新客户端时间,之后使用最新客户端时间与当前时间判断),一种是查询认证服务器,通过认证服务器的返回信息进行判断(将客户端IP和状态请求发送给认证服务器,认证服务器会返回客户端是否在线,这种情况是用于客户端是在认证服务器上正常登出)。

     

    thread_client_timeout_check

      此线程执行函数用于维护客户端列表,此线程是一个while (1)循环,每隔一个配置文件中的checkinterval时间间隔执行一次fw_sync_with_authserver函数,核心代码处于fw_sync_with_authserver函数中,我们先具体代码
    代码片段1.1

    1. void
    2. thread_client_timeout_check(const void *arg)
    3. {
    4.     pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    5.     pthread_mutex_t cond_mutex = PTHREAD_MUTEX_INITIALIZER;
    6.     struct timespec timeout;
    7.     
    8.     while (1) {
    9.         /* 设置超时时间 */
    10.         timeout.tv_sec = time(NULL) + config_get_config()->checkinterval;
    11.         timeout.tv_nsec = 0;

    12.         /* 使用pthread_cond_timedwait必须先上锁 */
    13.         pthread_mutex_lock(&cond_mutex);
    14.         
    15.         /* 等待超时 */
    16.         pthread_cond_timedwait(&cond, &cond_mutex, &timeout);

    17.         /* 解锁 */
    18.         pthread_mutex_unlock(&cond_mutex);
    19.     
    20.         debug(LOG_DEBUG, "Running fw_counter()");
    21.     
    22.         /* 执行核心代码 */ 
    23.         fw_sync_with_authserver();
    24.     }
    25. }

    fw_sync_with_authserver

      此函数是此线程的核心函数,维护客户端列表就在此中,其首先会遍历客户端列表,通过iptables获取每个客户端列表的出入流量,之后根据出口流量(入口流量不做判断,详见 代码片段1.3)更新客户端最近更新时间(last_updated),之后使用每个客户端最近更新时间与当前时间比较,如果超过超时间隔则判断为下线,而如果未超时,则还会从认证服务器中获取此客户端状态,确定其是否在线。具体流程如下

    • 更新客户端出入口流量,根据出口流量更新每个客户端的最近更新时间
    • 客户端超时则从客户端列表中移除并通过iptables禁止其访问网络,并告知认证服务器此客户端下线
    • 客户端未超时则从认证服务器获取此客户端信息,判断其是否通过认证服务器下线

    代码片段1.2


    1. void
    2. fw_sync_with_authserver(void)
    3. {
    4.     t_authresponse authresponse;
    5.     char *token, *ip, *mac;
    6.     t_client *p1, *p2;
    7.     unsigned long long incoming, outgoing;
    8.     s_config *config = config_get_config();

    9.     /* 根据iptables流量更新最近更新时间,具体代码见 代码片段1.*/
    10.     if (-== iptables_fw_counters_update()) {
    11.         debug(LOG_ERR, "Could not get counters from firewall!");
    12.         return;
    13.     }

    14.     LOCK_CLIENT_LIST();

    15.     /* 遍历客户端列表 */
    16.     for (p1 = p2 = client_get_first_client(); NULL != p1; p1 = p2) {
    17.         p2 = p1->next;

    18.         ip = safe_strdup(p1->ip);
    19.         token = safe_strdup(p1->token);
    20.         mac = safe_strdup(p1->mac);
    21.         outgoing = p1->counters.outgoing;
    22.         incoming = p1->counters.incoming;

    23.         UNLOCK_CLIENT_LIST();
    24.         /* ping一下此客户端,不清楚作用 */
    25.         icmp_ping(ip);
    26.         /* 将客户端的出入流量上传至认证服务器,此时如果此客户端在认证服务器上下线会返回告知wifidog */
    27.         if (config->auth_servers != NULL) {
    28.             auth_server_request(&authresponse, REQUEST_TYPE_COUNTERS, ip, mac, token, incoming, outgoing);
    29.         }
    30.         LOCK_CLIENT_LIST();

    31.         /* 从客户端列表获取IP,MAC对应客户端 */
    32.         if (!(p1 = client_list_find(ip, mac))) {
    33.             debug(LOG_ERR, "Node %s was freed while being re-validated!", ip);
    34.         } else {
    35.             time_t current_time=time(NULL);
    36.             debug(LOG_INFO, "Checking client %s for timeout: Last updated %ld (%ld seconds ago), timeout delay %ld seconds, current time %ld, ",
    37.                         p1->ip, p1->counters.last_updated, current_time-p1->counters.last_updated, config->checkinterval * config->clienttimeout, current_time);
    38.             /* 判断是否超时,(最近更新时间 + 超时时间 <= 当前时间) 表明以超过超时时间,下线 */
    39.             if (p1->counters.last_updated +
    40.                 (config->checkinterval * config->clienttimeout)
    41.                 <= current_time) {
    42.                 debug(LOG_INFO, "%s - Inactive for more than %ld seconds, removing client and denying in firewall",
    43.                         p1->ip, config->checkinterval * config->clienttimeout);
    44.                 /* 修改iptables禁止此客户端访问外网 */
    45.                 fw_deny(p1->ip, p1->mac, p1->fw_connection_state);
    46.                 /* 从客户端列表中删除此客户端 */
    47.                 client_list_delete(p1);

    48.                 /* 通知认证服务器此客户端下线 */
    49.                 if (config->auth_servers != NULL) {
    50.                     UNLOCK_CLIENT_LIST();
    51.                     auth_server_request(&authresponse, REQUEST_TYPE_LOGOUT, ip, mac, token, 0, 0);
    52.                     LOCK_CLIENT_LIST();
    53.                 }
    54.             } else {
    55.                 /* 未超时处理 */
    56.                 if (config->auth_servers != NULL) {
    57.                     /* 判断认证服务器返回信息 */
    58.                     switch (authresponse.authcode) {
    59.                         /* 认证服务器禁止其访问网络(下线或遭拒绝) */
    60.                         case AUTH_DENIED:
    61.                             debug(LOG_NOTICE, "%s - Denied. Removing client and firewall rules", p1->ip);
    62.                             fw_deny(p1->ip, p1->mac, p1->fw_connection_state);
    63.                             client_list_delete(p1);
    64.                             break;

    65.                         case AUTH_VALIDATION_FAILED:
    66.                             debug(LOG_NOTICE, "%s - Validation timeout, now denied. Removing client and firewall rules", p1->ip);
    67.                             fw_deny(p1->ip, p1->mac, p1->fw_connection_state);
    68.                             client_list_delete(p1);
    69.                             break;

    70.                         /* 认证服务器允许其访问网络(在线) */
    71.                         case AUTH_ALLOWED:
    72.                             if (p1->fw_connection_state != FW_MARK_KNOWN) {
    73.                                 debug(LOG_INFO, "%s - Access has changed to allowed, refreshing firewall and clearing counters", p1->ip);
    74.                                 if (p1->fw_connection_state != FW_MARK_PROBATION) {
    75.                                     p1->counters.incoming = p1->counters.outgoing = 0;
    76.                                 }
    77.                                 else {
    78.                            
    79.                                     debug(LOG_INFO, "%s - Skipped clearing counters after all, the user was previously in validation", p1->ip);
    80.                                 }
    81.                                 p1->fw_connection_state = FW_MARK_KNOWN;
    82.                                 fw_allow(p1->ip, p1->mac, p1->fw_connection_state);
    83.                             }
    84.                             break;

    85.                         case AUTH_VALIDATION:
    86.                             debug(LOG_INFO, "%s - User in validation period", p1->ip);
    87.                             break;

    88.                             case AUTH_ERROR:
    89.                                     debug(LOG_WARNING, "Error communicating with auth server - leaving %s as-is for now", p1->ip);
    90.                                     break;

    91.                         default:
    92.                             debug(LOG_ERR, "I do not know about authentication code %d", authresponse.authcode);
    93.                             break;
    94.                     }
    95.                 }
    96.             }
    97.         }

    98.         free(token);
    99.         free(ip);
    100.         free(mac);
    101.     }
    102.     UNLOCK_CLIENT_LIST();
    103. }

    代码片段1.3

    1. int
    2. iptables_fw_counters_update(void)
    3. {
    4.     FILE *output;
    5.     char *script,
    6.          ip[16],
    7.          rc;
    8.     unsigned long long int counter;
    9.     t_client *p1;
    10.     struct in_addr tempaddr;

    11.     /* 通过iptables获取其出口流量 */
    12.     safe_asprintf(&script, "%s %s", "iptables", "-v -n -x -t mangle -L " TABLE_WIFIDOG_OUTGOING);
    13.     iptables_insert_gateway_id(&script);
    14.     output = popen(script, "r");
    15.     free(script);
    16.     if (!output) {
    17.         debug(LOG_ERR, "popen(): %s", strerror(errno));
    18.         return -1;
    19.     }

    20.     /* iptables返回信息处理 */
    21.     while (('\n' != fgetc(output)) && !feof(output))
    22.         ;
    23.     while (('\n' != fgetc(output)) && !feof(output))
    24.         ;
    25.     while (output && !(feof(output))) {
    26.         rc = fscanf(output, "%*s %llu %*s %*s %*s %*s %*s %15[0-9.] %*s %*s %*s %*s %*s %*s", &counter, ip);
    27.         //rc = fscanf(output, "%*s %llu %*s %*s %*s %*s %*s %15[0-9.] %*s %*s %*s %*s %*s 0x%*u", &counter, ip);
    28.         if (== rc && EOF != rc) {
    29.             if (!inet_aton(ip, &tempaddr)) {
    30.                 debug(LOG_WARNING, "I was supposed to read an IP address but instead got [%s] - ignoring it", ip);
    31.                 continue;
    32.             }
    33.             debug(LOG_DEBUG, "Read outgoing traffic for %s: Bytes=%llu", ip, counter);
    34.             LOCK_CLIENT_LIST();
    35.             /* 通过ip获取客户端信息结构 */
    36.             if ((p1 = client_list_find_by_ip(ip))) {
    37.                 /* (上一次出口总流量(outgoing) + wifidog启动时的出口总流量(outgoing_history) < iptables返回的出口总流量) 表示此客户端有新的出口流量 */
    38.                 if ((p1->counters.outgoing - p1->counters.outgoing_history) < counter) {
    39.                     /* 更新上一次出口总流量(outgoing)为wifidog启动时的出口总流量(outgoing_history) + iptables返回总流量(counter) */
    40.                     p1->counters.outgoing = p1->counters.outgoing_history + counter;
    41.                     /* 更新最近更新时间为当前时间 */
    42.                     p1->counters.last_updated = time(NULL);
    43.                     debug(LOG_DEBUG, "%s - Updated counter.outgoing to %llu bytes. Updated last_updated to %d", ip, counter, p1->counters.last_updated);
    44.                 }
    45.             } else {
    46.                 debug(LOG_ERR, "Could not find %s in client list", ip);
    47.             }
    48.             UNLOCK_CLIENT_LIST();
    49.         }
    50.     }
    51.     pclose(output);

    52.     /* 通过iptables获取其入口流量,入口流量不做更新最近更新时间参考,只用于更新后上传至认证服务器,其原理同上,后面的代码不做详细分析 */
    53.     safe_asprintf(&script, "%s %s", "iptables", "-v -n -x -t mangle -L " TABLE_WIFIDOG_INCOMING);
    54.     iptables_insert_gateway_id(&script);
    55.     output = popen(script, "r");
    56.     free(script);
    57.     if (!output) {
    58.         debug(LOG_ERR, "popen(): %s", strerror(errno));
    59.         return -1;
    60.     }

    61.  
    62.     while (('\n' != fgetc(output)) && !feof(output))
    63.         ;
    64.     while (('\n' != fgetc(output)) && !feof(output))
    65.         ;
    66.     while (output && !(feof(output))) {
    67.         rc = fscanf(output, "%*s %llu %*s %*s %*s %*s %*s %*s %15[0-9.]", &counter, ip);
    68.         if (== rc && EOF != rc) {
    69.  
    70.             if (!inet_aton(ip, &tempaddr)) {
    71.                 debug(LOG_WARNING, "I was supposed to read an IP address but instead got [%s] - ignoring it", ip);
    72.                 continue;
    73.             }
    74.             debug(LOG_DEBUG, "Read incoming traffic for %s: Bytes=%llu", ip, counter);
    75.             LOCK_CLIENT_LIST();
    76.             if ((p1 = client_list_find_by_ip(ip))) {
    77.                 if ((p1->counters.incoming - p1->counters.incoming_history) < counter) {
    78.                     p1->counters.incoming = p1->counters.incoming_history + counter;
    79.                     debug(LOG_DEBUG, "%s - Updated counter.incoming to %llu bytes", ip, counter);
    80.                 }
    81.             } else {
    82.                 debug(LOG_ERR, "Could not find %s in client list", ip);
    83.             }
    84.             UNLOCK_CLIENT_LIST();
    85.         }
    86.     }
    87.     pclose(output);

    88.     return 1;
    89. }




    引言

      但wifidog启动时,会自动启动认证服务器心跳检测线程,此线程默认每隔60s与认证服务器交互一次,会将路由器的信息(系统启动时长,内存使用情况和系统平均负载)告知认证服务器,并通过一个"ping"字符串作为信号,而当认证服务器接收到此数据包后,会返回一个"pong"给路由器,具体我们看看代码。

     

    代码片段1.1

    此段代码很简单,就是调用ping函数,然后等待60s

    点击(此处)折叠或打开

    1. void
    2. thread_ping(void *arg)
    3. {
    4.     pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    5.     pthread_mutex_t cond_mutex = PTHREAD_MUTEX_INITIALIZER;
    6.     struct timespec timeout;
    7.     
    8.     while (1) {
    9.         /* 调用ping,具体代码看 代码片段1.*/
    10.         debug(LOG_DEBUG, "Running ping()");
    11.         ping();
    12.         
    13.         /* 睡眠一个checkinterval,默认为60s */
    14.         timeout.tv_sec = time(NULL) + config_get_config()->checkinterval;
    15.         timeout.tv_nsec = 0;


    16.         pthread_mutex_lock(&cond_mutex);
    17.         
    18.         pthread_cond_timedwait(&cond, &cond_mutex, &timeout);

    19.         pthread_mutex_unlock(&cond_mutex);
    20.     }

    代码片段1.2

    点击(此处)折叠或打开

    1. static void
    2. ping(void)
    3. {
    4.     ssize_t numbytes;
    5.     size_t totalbytes;
    6.     int sockfd, nfds, done;
    7.     char request[MAX_BUF];
    8.     fd_set readfds;
    9.     struct timeval timeout;
    10.     FILE * fh;
    11.     unsigned long int sys_uptime = 0;
    12.     unsigned int sys_memfree = 0;
    13.     float sys_load = 0;
    14.     t_auth_serv *auth_server = NULL;
    15.     auth_server = get_auth_server();
    16.     
    17.     debug(LOG_DEBUG, "Entering ping()");
    18.     
    19.     /* 其实认证服务器就是一个web服务器,路由器跟他做通信行为就是通过发送http请求进行通信,首先先连接认证服务器的http端口,获取其socket */
    20.     sockfd = connect_auth_server();
    21.     if (sockfd == -1) {
    22.         /* 无法连接认证服务器,connect_auth_server分析见 代码片段1.*/
    23.         return;
    24.     }

    25.     /*
    26.      * 从/proc文件系统获取路由器信息
    27.      */
    28.     if ((fh = fopen("/proc/uptime", "r"))) {
    29.         fscanf(fh, "%lu", &sys_uptime);
    30.         fclose(fh);
    31.     }
    32.     if ((fh = fopen("/proc/meminfo", "r"))) {
    33.         while (!feof(fh)) {
    34.             if (fscanf(fh, "MemFree: %u", &sys_memfree) == 0) {
    35.                 while (!feof(fh) && fgetc(fh) != '\n');
    36.             }
    37.             else {
    38.                 break;
    39.             }
    40.         }
    41.         fclose(fh);
    42.     }
    43.     if ((fh = fopen("/proc/loadavg", "r"))) {
    44.         fscanf(fh, "%f", &sys_load);
    45.         fclose(fh);
    46.     }

    47.     /*
    48.      * 准备http请求包
    49.      */
    50.     snprintf(request, sizeof(request) - 1,
    51.             "GET %s%sgw_id=%s&sys_uptime=%lu&sys_memfree=%u&sys_load=%.2f&wifidog_uptime=%lu HTTP/1.0\r\n"
    52.             "User-Agent: WiFiDog %s\r\n"
    53.             "Host: %s\r\n"
    54.             "\r\n",
    55.             auth_server->authserv_path,
    56.             auth_server->authserv_ping_script_path_fragment,
    57.             config_get_config()->gw_id,
    58.             sys_uptime,
    59.             sys_memfree,
    60.             sys_load,
    61.             (long unsigned int)((long unsigned int)time(NULL) - (long unsigned int)started_time),
    62.             VERSION,
    63.             auth_server->authserv_hostname);

    64.     debug(LOG_DEBUG, "HTTP Request to Server: [%s]", request);
    65.     /* 发送 */
    66.     send(sockfd, request, strlen(request), 0);

    67.     debug(LOG_DEBUG, "Reading response");
    68.     
    69.     numbytes = totalbytes = 0;
    70.     done = 0;
    71.     do {
    72.         FD_ZERO(&readfds);
    73.         FD_SET(sockfd, &readfds);
    74.         /* 设置超时30s */
    75.         timeout.tv_sec = 30;
    76.         timeout.tv_usec = 0;
    77.         nfds = sockfd + 1;

    78.         nfds = select(nfds, &readfds, NULL, NULL, &timeout);

    79.         if (nfds > 0) {
    80.             /* 多路复用 */
    81.             numbytes = read(sockfd, request + totalbytes, MAX_BUF - (totalbytes + 1));
    82.             if (numbytes < 0) {
    83.                 debug(LOG_ERR, "An error occurred while reading from auth server: %s", strerror(errno));
    84.                 close(sockfd);
    85.                 return;
    86.             }
    87.             else if (numbytes == 0) {
    88.                 done = 1;
    89.             }
    90.             else {
    91.                 totalbytes += numbytes;
    92.                 debug(LOG_DEBUG, "Read %d bytes, total now %d", numbytes, totalbytes);
    93.             }
    94.         }
    95.         else if (nfds == 0) {
    96.             debug(LOG_ERR, "Timed out reading data via select() from auth server");
    97.             close(sockfd);
    98.             return;
    99.         }
    100.         else if (nfds < 0) {
    101.             debug(LOG_ERR, "Error reading data via select() from auth server: %s", strerror(errno));
    102.             close(sockfd);
    103.             return;
    104.         }
    105.     } while (!done);
    106.     close(sockfd);

    107.     debug(LOG_DEBUG, "Done reading reply, total %d bytes", totalbytes);

    108.     request[totalbytes] = '\0';

    109.     debug(LOG_DEBUG, "HTTP Response from Server: [%s]", request);
    110.     /* 判断认证服务器返回包中有没有"Pong"字符串 */
    111.     if (strstr(request, "Pong") == 0) {
    112.         debug(LOG_WARNING, "Auth server did NOT say pong!");
    113.        
    114.     }
    115.     else {
    116.         debug(LOG_DEBUG, "Auth Server Says: Pong");
    117.     }

    118.     return; 
    119. }

    代码片段1.3

    connect_auth_server函数用于连接认证服务器并返回socket套接字,其具体实现是通过_connect_auth_server实现的,而在_connect_auth_server中,递归认证服务器列表,每次递归中首先会根据认证服务器域名获取ip,如果失败,会通过公共网站判断是否为DNS问题,再判断是否为认证服务器问题,如果都失败,继续递归,否则返回认证服务器socket。


    1. int connect_auth_server() {
    2.     int sockfd;

    3.     LOCK_CONFIG();
    4.     /* 连接认证服务器 */
    5.     sockfd = _connect_auth_server(0);
    6.     UNLOCK_CONFIG();

    7.     if (sockfd == -1) {
    8.         debug(LOG_ERR, "Failed to connect to any of the auth servers");
    9.         /* 标记认证服务器离线 */
    10.         mark_auth_offline();
    11.     }
    12.     else {
    13.         debug(LOG_DEBUG, "Connected to auth server");
    14.         /* 标记认证服务器在线 */
    15.         mark_auth_online();
    16.     }
    17.     return (sockfd);
    18. }



    19. int _connect_auth_server(int level) {
    20.     s_config *config = config_get_config();
    21.     t_auth_serv *auth_server = NULL;
    22.     struct in_addr *h_addr;
    23.     int num_servers = 0;
    24.     char * hostname = NULL;
    25.     /* 公共网站,用于判断DNS问题 */
    26.     char * popular_servers[] = {
    27.           "www.google.com",
    28.           "www.yahoo.com",
    29.           NULL
    30.     };
    31.     char ** popularserver;
    32.     char * ip;
    33.     struct sockaddr_in their_addr;
    34.     int sockfd;

    35.     /* 用于递归,因为可能会有多个认证服务器,如果第一个认证服务器无法连接,会递归尝试连接后面的认证服务器,此参数用于递归判断的,当成功连接任意一个认证服务器后停止 */
    36.     level++;

    37.     /*
    38.      * 获取认证服务器数量
    39.      */
    40.     for (auth_server = config->auth_servers; auth_server; auth_server = auth_server->next) {
    41.         num_servers++;
    42.     }
    43.     debug(LOG_DEBUG, "Level %d: Calculated %d auth servers in list", level, num_servers);
    44.         /* 已经尝试递归连接所有认证服务器,都不能连接 */
    45.     if (level > num_servers) {
    46.         return (-1);
    47.     }

    48.     /*
    49.      * 获取认证服务器列表中的第一个认证服务器
    50.      */
    51.     auth_server = config->auth_servers;
    52.     hostname = auth_server->authserv_hostname;
    53.     debug(LOG_DEBUG, "Level %d: Resolving auth server [%s]", level, hostname);
    54.     h_addr = wd_gethostbyname(hostname);
    55.     if (!h_addr) {
    56.         /*
    57.          * DNS解析错误,尝试解析公共网站判断是否为DNS错误
    58.          */
    59.         debug(LOG_DEBUG, "Level %d: Resolving auth server [%s] failed", level, hostname);

    60.         for (popularserver = popular_servers; *popularserver; popularserver++) {
    61.             debug(LOG_DEBUG, "Level %d: Resolving popular server [%s]", level, *popularserver);
    62.             h_addr = wd_gethostbyname(*popularserver);
    63.             /* 公共网站DNS解析正确 */
    64.             if (h_addr) {
    65.                 debug(LOG_DEBUG, "Level %d: Resolving popular server [%s] succeeded = [%s]", level, *popularserver, inet_ntoa(*h_addr));
    66.                 break;
    67.             }
    68.             else {
    69.                 debug(LOG_DEBUG, "Level %d: Resolving popular server [%s] failed", level, *popularserver);
    70.             }
    71.         }

    72.         if (h_addr) {
    73.             /* DNS正确,尝试递归下一个认证服务器 */
    74.             free (h_addr);

    75.             debug(LOG_DEBUG, "Level %d: Marking auth server [%s] as bad and trying next if possible", level, hostname);
    76.             if (auth_server->last_ip) {
    77.                 free(auth_server->last_ip);
    78.                 auth_server->last_ip = NULL;
    79.             }
    80.             /* 将此认证服务器放入bad_server链表,并将config->auth_server指向认证服务器的下一个节点 */
    81.             mark_auth_server_bad(auth_server);
    82.             /* 递归 */
    83.             return _connect_auth_server(level);
    84.         }
    85.         else {
    86.             /* DNS问题,标记路由器离线 */
    87.             mark_offline();
    88.             debug(LOG_DEBUG, "Level %d: Failed to resolve auth server and all popular servers. "
    89.                     "The internet connection is probably down", level);
    90.             return(-1);
    91.         }
    92.     }
    93.     else {
    94.         /* DNS解析成功 */
    95.         ip = safe_strdup(inet_ntoa(*h_addr));
    96.         debug(LOG_DEBUG, "Level %d: Resolving auth server [%s] succeeded = [%s]", level, hostname, ip);

    97.         if (!auth_server->last_ip || strcmp(auth_server->last_ip, ip) != 0) {
    98.             /* DNS解析到的IP与我们上一次连接的IP不同,更新上一次连接的IP */
    99.             debug(LOG_DEBUG, "Level %d: Updating last_ip IP of server [%s] to [%s]", level, hostname, ip);
    100.             if (auth_server->last_ip) free(auth_server->last_ip);
    101.             auth_server->last_ip = ip;

    102.             /* 将此新的认证服务器IP添加到iptables中的可访问外网地址中 */
    103.             fw_clear_authservers();
    104.             fw_set_authservers();
    105.         }
    106.         else {
    107.             /*
    108.              * DNS解析到的IP与我们上一次连接的IP相同
    109.              */
    110.             free(ip);
    111.         }

    112.         /*
    113.          * 连接
    114.          */
    115.         debug(LOG_DEBUG, "Level %d: Connecting to auth server %s:%d", level, hostname, auth_server->authserv_http_port);
    116.         their_addr.sin_family = AF_INET;
    117.         their_addr.sin_port = htons(auth_server->authserv_http_port);
    118.         their_addr.sin_addr = *h_addr;
    119.         memset(&(their_addr.sin_zero), '\0', sizeof(their_addr.sin_zero));
    120.         free (h_addr);

    121.         if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    122.             debug(LOG_ERR, "Level %d: Failed to create a new SOCK_STREAM socket: %s", strerror(errno));
    123.             return(-1);
    124.         }

    125.         if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1) {
    126.             /*
    127.              * 连接失败
    128.              * 将此认证服务器放入bad_server链表,并将config->auth_server指向认证服务器的下一个节点
    129.              */
    130.             debug(LOG_DEBUG, "Level %d: Failed to connect to auth server %s:%d (%s). Marking it as bad and trying next if possible", level, hostname, auth_server->authserv_http_port, strerror(errno));
    131.             close(sockfd);
    132.             mark_auth_server_bad(auth_server);
    133.             return _connect_auth_server(level); /* Yay */
    134.         }
    135.         else {
    136.             /*
    137.              * 连接成功
    138.              */
    139.             debug(LOG_DEBUG, "Level %d: Successfully connected to auth server %s:%d", level, hostname, auth_server->authserv_http_port);
    140.             return sockfd;
    141.         }
    142.     }
    143. }



    展开全文
  • WiFi定位原理

    千次阅读 2017-02-27 21:31:09
    WiFi定位方法基本上可以分为两大类: 1. 不基RSSI(无线信号强度值Received Sig‐nal Strength Indication  ) TOA(time ofarrival)TDOA(time difference of arrival)AOA(angle of arrival) 这些值...
  • WIFI工作原理

    万次阅读 2018-01-20 13:20:29
    Wi-Fi工作原理 第一部分 Android概述  在介绍Wi-Fi之前,先简要介绍一下Android系统,主要分析一下Android的按层实现的原理。Android层次结构是整个Android体系中所有应用实现的基础框架,而Android源代码结构则...
  • wifidog是一个用于配合认证服务器实现无线网页认证功能的程序,常见的情景就是使用于公共场合的无线wifi接入点,首先移动... 其实wifidog原理很简单,主要是通过管控iptables,配合认证服务器进行客户端的放行操作。wi
  • wifidog源码分析 - wifidog原理 wifidog是一个用于配合认证服务器实现无线网页认证功能的程序,常见的情景就是使用于公共场合的无线wifi接入点,首先移动设备会连接公共wifi接入点,之后会弹出网页要求输入...
  • WIFI定位原理分析

    万次阅读 2016-01-14 16:28:28
    WIFI + 基站 +GPS 三者混合的定位策略 定位精度: 基站精度,平均100~300米,视运营商基站覆盖范围而定。 WIFI精度,30~200米。 GPS精度,为30米左右。 在户外,先开启GPS再进行定位,结果较准。但GPS比较费电,且在...
  • WIFI P2P原理深入解析

    2021-08-13 16:18:26
    前言 关于Wi-Fi联盟推出的另外一项重要技术规范WiFi P2P。 该规范的商品名为Wi-Fi Direct, 它支持多个Wi-Fi设备在没有AP的情况下相互连接. 在Android平台的Wi-Fi相关...P2P基础知识 Wi-Fi P2P技术是Wi-Fi Displ...
  • Wifi 破解原理及教程

    万次阅读 2014-08-14 18:56:33
    想要了解这些,必须要了解加密以及破解原理。 工具/原料 电脑 足够多足够好的wifi信号源 usb无线网卡(非必需) 一点点耐心 基础知识 1 ...
  • WiFi工作原理、测试及生产注意事项》由会员分享,可在线阅读,更多相关《WiFi工作原理、测试及生产注意事项(26页珍藏版)》请在人人文库网上搜索。1、WiFi工作原理、测试及生产注意事项,刘志昌 产品工程部,摘要,一...
  • WiFi技术应用设计,针对常用的WiFi RF天线的基本原理的介绍和说明,简明易懂,利于初学者掌握天线的基本知识。
  • wifi自身的基本原理

    2015-09-09 23:04:00
    wifi数据传送原理: http://zhidao.baidu.com/link?url=wFQmtpUsD_98ughGadReDhtrVAh0mhNdvzsBjpM0Hv3kMhascxQKluMtBaxe5T7-NT0JBuXuMeA4WII08Rsi9ANPYlV_GUYt_rndpvFulwe 2、 wifi信号接收原理: ...
  • wifibt测试原理

    2014-10-18 15:51:23
    详尽介绍了wifi-bt测试中的测试标准以及测试项目
  • WiFi接入原理

    万次阅读 2019-05-08 17:40:21
    WiFi接入原理 工作站(STA,即无线客户端)首先需要通过主动/被动扫描发现周围的无线服务,再通过认证和关联两个过程后,才能和接入点(AP,Access Point)建立连接,最终接入无线局域网。 参考: Kelvel2012 ...
  • android中wifi原理详解 转
  • WiFi广告强推原理

    千次阅读 2018-11-22 10:42:40
    WiFi广告强推的基本技术原理和一些相关问题   WiFi广告推送原理就是利用微信公众号里微信连WiFi设备广告接口,向指定微信用户发送广告。目前绝大部分智能手机都在使用微信,广告魔盒硬件核心部分是探针,盒子只要...
  • 无线WiFi漫游的基本原理及搭建

    万次阅读 2016-01-07 00:05:07
    原谅地址:http://lcbk.net/wifi/812.html ...WiFi网络中关于“漫游”的议论纷争始终没有停止过,期间不免夹杂着一些误解,当然在协议范围内各方见仁见智,依然是主流声音。本篇将从多个视角谈论一下WiFi网络的漫
  • WiFi感知的基本原理和相关挑战

    千次阅读 2019-09-17 20:02:46
    --揭示感知的基本原理和感知极限 --将细、粗粒度的行为识别和室内定位应用统一起来 我们在国际上首次: --将原用于光波传播的菲涅耳区模型引入到室内来刻画无线电波的传输 --揭示了用WiFi信号实现感知的...
  • wifi smart config配网原理应用场景基本原理总结 应用场景 智能空调这样的家电,想要进行wifi联网,是需要手机的协助才能完成; 通常的配网方式有:wifi组播、wifi广播、蓝牙配网 本文介绍一下wifi组播的配网原理 ...
  • Wifi隔离的原理及实现

    千次阅读 2019-01-12 15:55:13
    1、wifi隔离是什么 无线隔离又称客户端隔离(client isolation),也称AP隔离,指的是阻止连接路由器的设备之间互相访问,多见于无线通信方面,常见于路由器设置中。 AP隔离非常类似有线网络的VLAN(虚拟局域网),将...
  • wifi网络工作原理

    千次阅读 2019-05-22 13:53:21
    通过将2.4G wifi射频降频后在cable中传输。  IEEE802.11只是定义了分配系统应该提供Service。整个无线局域网定义了9种服务:  A、分配系统:Association、Diassociation、Distribution、Integration、...
  • SimpleWiFi串口WiFi模块一键配置功能 概述 目前,在嵌入式领域,智能家居、智能工业、智能公交等等控制中,WiFi已经成为了一种普遍被采用的技术。 在智能工业控制,或者智能家居的主控系统中。单片机,成为了...
  • WiFi 连接过程其实主要就做了两件事:身份认证和密钥交换 为什么要进行身份认证 为了只让特定(合法)用户使用当前网络 为什么要进行密钥交换 为了使用加密的方式进行通信,防止通信信息被第三方截获破解导致隐私泄露 ...
  • WiFi基础知识.pptx

    2020-05-05 10:47:04
    Fluke WIFI基础知识,详细介绍WIFI发展历程,WIFI基本概念及工作原理。文章对WIFI组网设计及WIFI施工有很好的借鉴作用。
  • Wifi Firmware 加载原理分析

    千次阅读 2019-06-14 12:10:10
    前段时间移植 wifi 驱动到 android 的内核上,发现 firmware 的加载始终出错,问了几个人,都不是很了解,没办法,只好自己研究一下。 原理分析 从本质上来说, firmware 需要做的事情包括两件: 1, 通知用户态...
  • 上一篇文章说过WiFi都是由哪些原因造成的信息泄露,接下来我们从技术原理角度来看一看! 大家是不是都有过这样的经历,有时候感觉自己家地网变慢了,是不是被别人蹭网了呢?或者有时候我们也会出门蹭一蹭别人家的网...
  • 基本的android 通过wifi传输文件的例子,带有基本UI,类似茄子快传的原理.rar,太多无法一一验证是否可用,程序如果跑不起来需要自调,部分代码功能进行参考学习。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 19,682
精华内容 7,872
关键字:

wifi基本原理