分类 wifidog分析 下的文章

IOS系统Wi-Fi图标无法点亮及Portal弹出慢问题

近期,部分客户反馈,少量的 IOS 或 OS X 终端,在使用 Portal 认证的情况下,会出现认证卡住、Portal 页面弹出慢、Wi-Fi 图标点不亮等问题,且其他 Wi-Fi 厂商客户也爆出了一样的问题。

通过对这些问题做了详细的分析,目前得出以下结果,简要描述为:最近苹果终端的 Wi-Fi 功能,在某些认证方式的场景下出现了一些问题,通过技术研究分析,发现这些问题和 Wi-Fi 设备本身没有关系,导致的原因是苹果服务器和苹果终端延迟发包。目前我司设备已经有了规避措施,第一时间联系了 Apple 公司的400和技术支持中心,暂时还未取得相关的技术解决回复。

上述内容是分析后的结论,可能有人还是没有感觉。如果你想了解更为详细的分析过程,下面的内容足够满足你,准备好了吗?

【正常iOS系统点亮WiFi图标过程】
1、连上WiFi后iOS会自动发起探测帧:http://captive.apple.com/hotspot-detect.html
2、首先DNS解析该域名,然后自动发送一个HTTP/1.0的探测帧请求到http://captive.apple.com/hotspot-detect.html
3、终端接收到苹果服务器探测回应,如果回应报文头部为success,那么认为网络是通的,同时,状态栏的WIFI图标出现,流程结束。
966e11927d70aaf0da3b25a3d4a090fd.jpg
【正常iOS系统自动弹出portal流程】
1、连上WiFi后iOS会自动发起探测帧:http://captive.apple.com/hotspot-detect.htm
2、首先DNS解析该域名,然后自动发送一个HTTP/1.0的探测帧请求到 http://captive.apple.com/hotspot-detect.html
3、终端接收到探测回应,回应报文头部不是success,不点亮WiFi图标。
050e22cd381938496b892bb3e6808546.jpg
4、终端会自动打开一个页面,在这个页面中再请求一次http://captive.apple.com/hotspot-detect.html,这一次,使用的是HTTP/1.1。
5、此时我们控制器会使用苹果服务器IP给终端回复一个302 moved跳转到http://auth.wifi.com
9e33ad7a3ed6df1782ecc5a7e8061937.jpg
6、接下来便进入到portal页面及认证流程并点亮WiFi图标,流程结束。

【部分iOS WiFi图标无法点亮、portal页面弹出缓慢原因】
目前出现的苹果终端WiFi图标无法点亮、portal页面弹出缓慢的问题,从问题终端抓包分析,该终端在走到正常流程第三步后,没有再继续下面的流程,发HTTP/1.1的GET包。(也没有发其他数据包,除了探测网关欺骗的ARP包外),一直等到几十秒或几分钟后,才开始继续接下来的流程。
从上面问题终端的数据包截图可以看到,终端在19:26:03收到HTTP/1.0 200 ok包之后,一直等到19:26:47才发出HTTP/1.1 GET包,才开始继续后面的流程。
在这持续的44s时间中,WiFi图标未点亮、portal页面也不会弹出,造成WiFi图标无法点亮、portal页面弹出缓慢,客户认为WiFi连接不上的情况。
从目前部分区域问题终端抓包分析,均为终端自身不再继续发包(除了探测网关欺骗的ARP包外)导致无法重定向,和Wi-Fi设备无关。

【规避措施】
针对以上终端自身不发包行为,从无线上无更多的方法让终端继续发包。出现这种情况时,我司无线设备目前通过规避措施,在SSID中勾选iOS不自动弹出portal页面,用户手动访问网页来解决。或者,可以暂时新建其他的认证方式。
请输入图片描述
勾选不自动弹portal,用户手动打开浏览器弹出portal页面流程如下:
用户打开浏览器,手动访问任意网页(以下数据包为访问百度新闻),此时终端在GET百度新闻时,控制器使用百度新闻的IP给终端回复302 moved temporarily跳转到http://auth.wifi.com弹出portal页面,进入认证过程。
cfef7c0ae873c0752c522ccffe0b5a89.jpg
本文只写了 Portal 页面弹出慢的问题,其他问题欢迎后续和我们的技术人员一起交流。

解决WiFiDog在高版本内核下不能使用问题

修改wifidog中的utl.c文件, 修改get_iface_ip 函数
if ((sockd = socket (AF_INET, SOCK_PACKET, htons(0x8086))) < 0) {
这句建议修改为 sockfd = socket(AF_INET,SOCK_DGRAM,0);
原因: 新kernel对于原模式过时

本文章由:http://www.wifidog.pro/2016/04/06/%E8%A7%A3%E5%86%B3WiFiDog%E5%9C%A8%E9%AB%98%E7%89%88%E6%9C%AC%E5%86%85%E6%A0%B8%E4%B8%8B%E4%B8%8D%E8%83%BD%E4%BD%BF%E7%94%A8%E9%97%AE%E9%A2%98.html整理编辑,转载请注明出处

wifidog源码分析Lighttpd1.4.20源码分析之fdevent系统(4) -----连接socket的处理与超时处理

前面讲了lighttpd是怎样使用fdevent系统的,以及监听socket的处理过程。这一篇我们来看一看lighttpd是怎样处理连接socket的。
首先,我们来看看lighttpd是怎样建立和客户端的连接的。前面在讲监听socket的处理过程中其实已经讲解了连接的建立过程。lighttpd监测监听socket的IO事件,如果有可读事件发生,那么表示有新的连接请求,然后调用network.c/network_server_handle_fdevent()来处理连接请求。network_server_handle_fdevent()函数调用connections.c/connection_accept() 接受客户端的请求,建立连接。在建立连接的同时,就得到了连接socket的fd,也就是accept函数的返回值。
建立连接之后,这个连接对应的状态机的状态被设置为CON_STATE_REQUEST_START,就是开始读取客户端发过来的request信息。在从connection_accept函数跳回network_server_handle_fdevent()函数的for循环中后,程序紧接着就调用了一次connection_state_machine()函数,这个函数是根据当前连接的状态机的状态设置状态机的下一个状态,CON_STATE_REQUEST_START的下一个状态是CON_STATE_READ,这个状态表示连接正在读取客户端发送的数据。当连接的状态机被设置成CON_STATE_READ后,在connection_state_machine()函数的最后,有这样一个switch语句:

switch (con->state)
    {
    case CON_STATE_READ_POST:
    case CON_STATE_READ:
    case CON_STATE_CLOSE:
        fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_IN);
        break;
    case CON_STATE_WRITE:
        /*
         * request write-fdevent only if we really need it
         * - if we have data to write
         * - if the socket is not writable yet
         */
        if (!chunkqueue_is_empty(con->write_queue) && (con->is_writable == 0) &&(con->traffic_limit_reached == 0))
        {
            fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_OUT);
        }
        else
        {
            fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
        }
        break;
    default:
        fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
        break;
    }

  上面这个switch语句将状态处在CON_STATE_READ_POST,CON_STATE_READ和CON_STATE_CLOSE的连接对应的连接socket fd加入到fdevent系统中,并监听可读事件。将处CON_STATE_WRITE状态且有数据要写的连接对应的socket fd加入到fdevent系统中,并监听可写事件。其他状态的连接则把对应的fd从fdevent系统中删除,因为这些连接不会有IO事件发生。
  这样,连接socket fd就被加入到了fdevent系统中。下面就是等待IO事件的发生。程序在前面已经提到过,如下:

if ((n = fdevent_poll(srv->ev, 1000)) > 0)
        {
            int revents;
            int fd_ndx;
            fd_ndx = -1;
            do
            {
                fdevent_handler handler;
                void *context;
                handler_t r;
                fd_ndx = fdevent_event_next_fdndx(srv->ev, fd_ndx);
                revents = fdevent_event_get_revent(srv->ev, fd_ndx);
                fd = fdevent_event_get_fd(srv->ev, fd_ndx);
                handler = fdevent_get_handler(srv->ev, fd);
                context = fdevent_get_context(srv->ev, fd);
                switch (r = (*handler) (srv, context, revents))
                {
                case HANDLER_FINISHED:
                case HANDLER_GO_ON:
                case HANDLER_WAIT_FOR_EVENT:
                case HANDLER_WAIT_FOR_FD:
                    break;
                case HANDLER_ERROR:
                    /*
                     * should never happen
                     */
                    SEGFAULT();
                    break;
                default:
                    log_error_write(srv, __FILE__, __LINE__, "d", r);
                    break;
                }
            }while (--n > 0);
        } 

  这段程序在前面已经讲解过。对于fdevent系统,它不关心自己处理的fd是连接fd还是监听fd,它所做的就是对于发生了这个fd所希望的IO事件以后,调用这个fd对应的处理函数处理IO事件。连接fd对应的处理函数是connections.c/connection_handle_fdevent()函数。函数的代码如下:

handler_t connection_handle_fdevent(void *s, void *context,int revents)
{
    server *srv = (server *) s;
    connection *con = context;
    //把这个连接加到作业队列中。
     joblist_append(srv, con);
    if (revents & FDEVENT_IN)
    {
        con->is_readable = 1;
    }
    if (revents & FDEVENT_OUT)
    {
        con->is_writable = 1;
        /*
         * we don't need the event twice
         */
    }
    if (revents & ~(FDEVENT_IN | FDEVENT_OUT))
    {
        /*
         * looks like an error 即可读又可写,可能是一个错误。
         */
        /*
         * FIXME: revents = 0x19 still means that we should read from the queue
         */
        if (revents & FDEVENT_HUP)
        {
            if (con->state == CON_STATE_CLOSE)
            {
                con->close_timeout_ts = 0;
            }
            else
            {
                /*
                 * sigio reports the wrong event here there was no HUP at all
                 */
                connection_set_state(srv, con, CON_STATE_ERROR);
            }
        }
        else if (revents & FDEVENT_ERR)
        {
            connection_set_state(srv, con, CON_STATE_ERROR);
        }
        else
        {
            log_error_write(srv, __FILE__, __LINE__, "sd","connection closed: poll() -> ???", revents);
        }
    }
    if (con->state == CON_STATE_READ|| con->state == CON_STATE_READ_POST)
    {
        connection_handle_read_state(srv, con);
 //继续读取数据,直到数据读取完毕
     }
 // 数据的写回并没有放给状态机去处理。
     if (con->state == CON_STATE_WRITE&& !chunkqueue_is_empty(con->write_queue) && con->is_writable)
    {
        if (-1 == connection_handle_write(srv, con))
        {
            connection_set_state(srv, con, CON_STATE_ERROR);
            log_error_write(srv, __FILE__, __LINE__, "ds", con->fd,"handle write failed.");
        }
        else if (con->state == CON_STATE_WRITE)
        {
            //写数据出错,记录当前时间,用来判断连接超时。
             con->write_request_ts = srv->cur_ts;
        }
    }
    if (con->state == CON_STATE_CLOSE)
    {
        /*
         * flush the read buffers 清空缓冲区中的数据。
         */
        int b;
        //获取缓冲区中数据的字节数
         if (ioctl(con->fd, FIONREAD, &b))
        {
            log_error_write(srv, __FILE__, __LINE__, "ss","ioctl() failed", strerror(errno));
        }
        if (b > 0)
        {
            char buf[1024];
            log_error_write(srv, __FILE__, __LINE__, "sdd","CLOSE-read()", con->fd, b);
            //将缓冲区中的数据读取后并丢弃,此时连接已经关闭,数据是无用数据。
             read(con->fd, buf, sizeof(buf));
        }
        else
        {
            /*
             * nothing to read 缓冲区中没有数据。复位连接关闭超时计时。
             */
            con->close_timeout_ts = 0;
        }
    }
    return HANDLER_FINISHED;
}

  可以看到,connection_handle_fdevent()函数根据当前连接fd所发生的IO事件,对connection结构体中的标记变量赋值,如is_writable,is_readable等,并做一些时间的记录。这些事件所对应的真正的IO处理则交给状态机处理。状态机根据这些标记变量进行相应的动作处理。
  这样,对于fdevent系统对于一次连接fd的IO事件就处理结束了。当然,真正的处理工作是由状态机来完成。下面的图简要的描述了fdevent系统对连接fd和监听fd的处理:
1.jpg

  下面我们来看一看连接超时的处理。连接超时有三种:读数据超时,写数据超时和关闭超时。处理超时的代码在server.c中的main函数woker进程开始部分:

/**
         * alarm函数发出的信号,表示一秒钟已经过去了。
         */
        if (handle_sig_alarm)
        {
            /*
             * a new second  新的一秒开始了。。。
             */
#ifdef USE_ALARM
            /*
             * reset notification 重置
             */
            handle_sig_alarm = 0;
 #endif
            /*
             * get current time  当前时间。精确到一秒
             */
            min_ts = time(NULL);
            /**
             * 这里判断和服务器记录的当前时间是否相同。
             * 相同,则表示服务器还在这一秒中,继续处理请求等。
             * 如果不相同,则进入了一个新的周期(当然周期是一秒)。这就要做一些触发和检查以及清理的动作。
             * 如插件的触发连接的超时清理状态缓存等。
             * 其中,最主要的工作是检查连接的超时。
             */
            if (min_ts != srv->cur_ts)
            {
                int cs = 0;
                connections *conns = srv->conns;
                handler_t r;
                switch (r = plugins_call_handle_trigger(srv))
                {
                    case HANDLER_GO_ON:
                        break;
                    case HANDLER_ERROR:
                        log_error_write(srv, __FILE__, __LINE__, "s","one of the triggers failed");
                        break;
                    default:
                        log_error_write(srv, __FILE__, __LINE__, "d", r);
                        break;
                }
                /*
                 * trigger waitpid
                 */
                srv->cur_ts = min_ts;
                /*
                 * cleanup stat-cache 清理状态缓存。每秒钟清理一次。
                 */
                stat_cache_trigger_cleanup(srv);
                /**
                 * check all connections for timeouts
                 */
                for (ndx = 0; ndx < conns->used; ndx++)
                {
                    int changed = 0;
                    connection *con;
                    int t_diff;



                    con = conns->ptr[ndx];

                    //连接的状态是在读
                     if (con->state == CON_STATE_READ|| con->state == CON_STATE_READ_POST)
                    {
                        if (con->request_count == 1) //连接处理一个请求
                         {
                            if (srv->cur_ts - con->read_idle_ts >con->conf.max_read_idle)
                            {
                                /*
                                 * time - out
                                 */
                                connection_set_state(srv, con, CON_STATE_ERROR);
                                changed = 1;
                            }
                        }  //这个连接同时处理多个请求
                         else
                        {
                            if (srv->cur_ts - con->read_idle_ts> con->conf.max_keep_alive_idle)
                            {
                                /*
                                 * time - out
                                 */
                                connection_set_state(srv, con, CON_STATE_ERROR);
                                changed = 1;
                            }
                        }
                    }
                    //连接的状态是写
                     if ((con->state == CON_STATE_WRITE)&& (con->write_request_ts != 0))
                    {
                        if (srv->cur_ts - con->write_request_ts> con->conf.max_write_idle)
                        {
                            /*
                             * time - out
                             */
#if 1
                            log_error_write(srv, __FILE__, __LINE__,"sbsosds", "NOTE: a request for",
                                            con->request.uri, "timed outafter writing", con->bytes_written, "bytes. We waited",
                                        (int) con->conf. max_write_idle,
                                            "seconds. If this a problemincrease server.max-write-idle");
#endif
                            connection_set_state(srv, con, CON_STATE_ERROR);
                            changed = 1;
                        }
                    }

                    /*
                     * we don't like div by zero 防止除0。。。
                     */
                    if (0 ==(t_diff = srv->cur_ts - con->connection_start))
                            t_diff = 1;

                    /**
                     * 下面的if语句不是用来判断连接是否超时。
                     * lighttpd对每个连接设置了一个kbytes_per_second,这个变量设定每个连接在一秒钟内多能传输的最大数据量。
                     * 如果传送的数据大于这个值,那么这个连接将停止传输数据,被追加到作业队列中等待下一次处理。
                     * 作者这样做估计是为了平衡各个连接之间的数据传输。
                     */
                    if (con->traffic_limit_reached && (con->conf.kbytes_per_second == 0|| ((con->bytes_written / t_diff)< con->conf.kbytes_per_second * 1024)))
                    {
                        /*
                         * enable connection again
                         */
                        con->traffic_limit_reached = 0;
                        changed = 1;
                    }

                    if (changed)
                    {
                        connection_state_machine(srv, con);
                    }
                    con->bytes_written_cur_second = 0;
                    *(con->conf.global_bytes_per_second_cnt_ptr) = 0;
                }//end of for( ndx = 0; ...
                if (cs == 1)
                    fprintf(stderr, "\n");
            }//end of if (min_ts != srv->cur_ts)...
        }//end of if (handle_sig_alarm)...

在这个If语句中,作者的本意是通过alarm信号来判断时间是否到一秒种。handle_sig_alarm就是标记是否已经过了一秒钟。在server.c的信号处理函数sigaction_handler()中可以看到:

case SIGALRM:     //超时信号
         handle_sig_alarm = 1;
         break;

当收到SIGALRM信号时,标记handle_sig_alarm为1。
下面的代码是启动计时器。两段代码都被宏包围。说明需要定义宏USE_ALARM才启动计时器。

#ifdef USE_ALARM
    struct itimerval interval;
    interval.it_interval.tv_sec = 1;
    interval.it_interval.tv_usec = 0;
    interval.it_value.tv_sec = 1;
    interval.it_value.tv_usec = 0;
#endif
#ifdef USE_ALARM
    signal(SIGALRM, signal_handler);
    if (setitimer(ITIMER_REAL, &interval, NULL))
    {
        log_error_write(srv, __FILE__, __LINE__, "s", "setting timer failed");
        return -1;
    }
    getitimer(ITIMER_REAL, &interval);
#endif

下面寻找宏USE_ALARM的定义。仍然在server.c文件中:

/*
 * IRIX doesn't like the alarm based time() optimization
 */
/*
 * #define USE_ALARM
 */

  不过,这个唯一的定义被注释掉了。。。
  那么,也就是说,作者并没有使用计时器产生SIGALRM信号来判断时间是否过了一秒。其实,上面处理连接超时的代码中,作者通过判断当前时间和服务器记录的当前时间来判断时间是否过了一秒。如果两个时间不一样,那么时间就过了一秒。不使用SIGALRM信号,可以减少很多信号处理,降低程序的复杂度。没有使用SIGALRM信号,那么handle_sig_alarm就一直是1。子进程每循环一次都要比较服务器记录的时间和当前时间。
  下面继续看超时处理。在上面的处理程序中,lighttpd通过比较read_idle_ts,write_request_ts和当前时间的差值来判断连接是否读超时或写超时。如果这两个差值分别大于max_read_idle和max_write_idle则表示超时。如果一个连接正在处理多个请求时,读超时是和max_keep_alive_idle比较。这些上限值在配置中设置。
  那么,read_idle_ts和write_request_ts又是记录的什么呢?
  对于read_idle_ts,在连接进入CON_STATE_REQUEST_START状态时,记录了当前时间。如果连接长时间没有去读取request请求,则也表示连接超时。当连接开始读数据时,read_idle_ts记录开始读数据的时间。这个不多说了。
  对于write_request_ts,在处理CON_STATE_WRITE状态时,有对其赋值的语句。在connection_handle_fdevent函数中也有。其实,都是在调用connection_handle_write函数出错并且连接处在CON_STATE_WRITE状态时,记录当前时间。
  通过这两个变量可以看出,lighttpd对读和写的超时处理是不一样的。对于读,设定了最长时间,不管读多少数据,一旦时间超了就算超时。而对于写,只有在写出错的时候才开始计算超时。如果没有出错,那么写数据花再多的时间也不算超时。这就有一个问题了,如果客户端上传的数据很多呢?这样没上传完就有可能被判断为超时。其实,lighttpd做为一个web服务器,其假设上传的数据都是有限的。在绝大多数情况下,上传数据都是很小的,也就是http头等,而下载的数据往往很多。因此,这样处理可以提高效率。如果需要上传大量数据,可以修改配置中的超时限制。(PS:这点不太确定,望高手讲解。)
  lighttpd每过一秒钟就要轮询连接,检查是否超时。如果连接很多时,这将浪费大量的时间。虽然这样很低效,但是处理简单,程序复杂度低。在真正的使用中,效率也没有想像中的那么差。
  至此,lighttpd的fdevent系统就介绍完毕了。从下一篇开始,我们将走进lighttpd的状态机。

本文章由 http://www.wifidog.pro/2015/04/21/wifidog%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90Lighttpd-socket%E7%9A%84%E5%A4%84%E7%90%86.html 整理编辑,转载请注明出处