wifidog原理分析

news/2024/11/24 7:24:52/

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

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源码分析 - 客户端检测线程 


引言

  当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源码分析 - 认证服务器心跳检测线程 


引言

  但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. }




http://www.ppmy.cn/news/675980.html

相关文章

wifidog+authpuppy搭建WiFi 接入设备认证测试平台

0&#xff1a;前提 其实搭建认证环境都是基础&#xff0c;重要的是要对WiFidog的代码进行详细的研究&#xff0c;了解清楚wifidog与authpuppy之间进行了哪些数据交互&#xff0c;WiFidog的程序框架及iptables的建立及生效规则&#xff0c;这才是重点。WiFidog和之前nodogsplas…

wifidog认证流程图

一. 用户上线 1. 用户访问网络&#xff0c;通过iptables将未认证的用户dnat到wifidog进程&#xff0c;wifidog通过307报文将用户重定向到认证服务器 2. 用户打开认证服务器登录页面&#xff0c;输入用户名密码&#xff0c;发送认证请求 3. 认证成功的话服务器会发送302报文&…

openwrt-看门狗watchdog

一、硬件watchdog和软件watchdog Linux内核不仅为各种不同类型的watchdog硬件电路提供了驱动&#xff0c;还提供了一个基于定时器的纯软件watchdog驱动&#xff0c;软件watchdog基于内核的定时器实现&#xff0c;当内核或中断出现异常时&#xff0c;软件watchdog是无法复位系统…

wifidog+authpuppy认证页面的配置

路由器上用的是wifidog client&#xff0c;服务器后端管理用的是authpuppy。 首先&#xff0c;选择自己的操作系统&#xff0c;在http://www.authpuppy.org/doc/Main_Page网站里面&#xff0c;我的操作系统是Linux。 然后到这个网站http://www.authpuppy.org/doc/Getting_Star…

wifidog认证流程(图文版)

学习使用wifidog一段时间了&#xff0c;觉得这玩意真的不错&#xff0c;虽然有些代码写的不够严谨&#xff0c;运行效率不够高&#xff0c;但是少量人数情况下实现portal是很好的方案。 下面是我摘自一个博客的内容和apfree写的文档中的一部分发上来的&#xff0c;希望能对研究…

分享wifidog的增强版: wifidogx

j同学前阵子有段时间在维护wifidog&#xff0c;然后就重复发明了个轮子:wifidogx。当然也不能算完全重复&#xff0c;j同学认为以下几方面wifidog做的不是很好&#xff1a; 多线程的架构欠佳&#xff0c;性能不强&#xff0c;健壮性也不好&#xff0c;坑还多。&#xff08;LOC…

wifidog 认证

首先简单介绍一下什么是Portal认证&#xff0c;Portal认证&#xff0c;通常也会叫Web认证&#xff0c;未认证用户上网时&#xff0c;设备强制用户登录到特定站点&#xff0c;用户可以免费访问其中的服务。当用户需要使用互联网中的其它信息时&#xff0c;必须在门户网站进行认证…

wifidog安装以及自写wifidog认证服务器

前言 最近在做关于路由器认证相关的工作&#xff0c;由于需求&#xff0c;认证的过程同往常的网页认证有稍许不同&#xff0c;因此&#xff0c;自己开始尝试编写wifidog的认证服务器&#xff0c;查阅了中外的一些资料&#xff0c;现将经验总结一下。 Wifidog的原理 下图是在…