2015年2月

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函数中,我们先具体代码,

void
thread_client_timeout_check(const void *arg)
{
    pthread_cond_t        cond = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t        cond_mutex = PTHREAD_MUTEX_INITIALIZER;
    struct    timespec    timeout;

    while (1) {
        /* 设置超时时间 */
        timeout.tv_sec = time(NULL) + config_get_config()->checkinterval;
        timeout.tv_nsec = 0;

        /* 使用pthread_cond_timedwait必须先上锁 */
        pthread_mutex_lock(&cond_mutex);

        /* 等待超时 */
        pthread_cond_timedwait(&cond, &cond_mutex, &timeout);

        /* 解锁 */
        pthread_mutex_unlock(&cond_mutex);

        debug(LOG_DEBUG, "Running fw_counter()");

        /* 执行核心代码 */  
        fw_sync_with_authserver();
    }
}

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

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

void
fw_sync_with_authserver(void)
{
    t_authresponse  authresponse;
    char            *token, *ip, *mac;
    t_client        *p1, *p2;
    unsigned long long        incoming, outgoing;
    s_config *config = config_get_config();

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

    LOCK_CLIENT_LIST();

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

        ip = safe_strdup(p1->ip);
        token = safe_strdup(p1->token);
        mac = safe_strdup(p1->mac);
        outgoing = p1->counters.outgoing;
        incoming = p1->counters.incoming;

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

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

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

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

                        /* 认证服务器允许其访问网络(在线) */
                        case AUTH_ALLOWED:
                            if (p1->fw_connection_state != FW_MARK_KNOWN) {
                                debug(LOG_INFO, "%s - Access has changed to allowed, refreshing firewall and clearing counters", p1->ip);
                                if (p1->fw_connection_state != FW_MARK_PROBATION) {
                                    p1->counters.incoming = p1->counters.outgoing = 0;
                                }
                                else {

                                    debug(LOG_INFO, "%s - Skipped clearing counters after all, the user was previously in validation", p1->ip);
                                }
                                p1->fw_connection_state = FW_MARK_KNOWN;
                                fw_allow(p1->ip, p1->mac, p1->fw_connection_state);
                            }
                            break;

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

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

                        default:
                            debug(LOG_ERR, "I do not know about authentication code %d", authresponse.authcode);
                            break;
                    }
                }
            }
        }

        free(token);
        free(ip);
        free(mac);
    }
    UNLOCK_CLIENT_LIST();
}

代码片段1.3:

int
iptables_fw_counters_update(void)
{
    FILE *output;
    char *script,
         ip[16],
         rc;
    unsigned long long int counter;
    t_client *p1;
    struct in_addr tempaddr;

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

    /* iptables返回信息处理 */
    while (('\n' != fgetc(output)) && !feof(output))
        ;
    while (('\n' != fgetc(output)) && !feof(output))
        ;
    while (output && !(feof(output))) {
        rc = fscanf(output, "%*s %llu %*s %*s %*s %*s %*s %15[0-9.] %*s %*s %*s %*s %*s %*s", &counter, ip);
        //rc = fscanf(output, "%*s %llu %*s %*s %*s %*s %*s %15[0-9.] %*s %*s %*s %*s %*s 0x%*u", &counter, ip);
        if (2 == rc && EOF != rc) {
            if (!inet_aton(ip, &tempaddr)) {
                debug(LOG_WARNING, "I was supposed to read an IP address but instead got [%s] - ignoring it", ip);
                continue;
            }
            debug(LOG_DEBUG, "Read outgoing traffic for %s: Bytes=%llu", ip, counter);
            LOCK_CLIENT_LIST();
            /* 通过ip获取客户端信息结构 */
            if ((p1 = client_list_find_by_ip(ip))) {
                /* (上一次出口总流量(outgoing) + wifidog启动时的出口总流量(outgoing_history) < iptables返回的出口总流量) 表示此客户端有新的出口流量 */
                if ((p1->counters.outgoing - p1->counters.outgoing_history) < counter) {
                    /* 更新上一次出口总流量(outgoing)为wifidog启动时的出口总流量(outgoing_history) + iptables返回总流量(counter) */
                    p1->counters.outgoing = p1->counters.outgoing_history + counter;
                    /* 更新最近更新时间为当前时间 */
                    p1->counters.last_updated = time(NULL);
                    debug(LOG_DEBUG, "%s - Updated counter.outgoing to %llu bytes.  Updated last_updated to %d", ip, counter, p1->counters.last_updated);
                }
            } else {
                debug(LOG_ERR, "Could not find %s in client list", ip);
            }
            UNLOCK_CLIENT_LIST();
        }
    }
    pclose(output);

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


    while (('\n' != fgetc(output)) && !feof(output))
        ;
    while (('\n' != fgetc(output)) && !feof(output))
        ;
    while (output && !(feof(output))) {
        rc = fscanf(output, "%*s %llu %*s %*s %*s %*s %*s %*s %15[0-9.]", &counter, ip);
        if (2 == rc && EOF != rc) {

            if (!inet_aton(ip, &tempaddr)) {
                debug(LOG_WARNING, "I was supposed to read an IP address but instead got [%s] - ignoring it", ip);
                continue;
            }
            debug(LOG_DEBUG, "Read incoming traffic for %s: Bytes=%llu", ip, counter);
            LOCK_CLIENT_LIST();
            if ((p1 = client_list_find_by_ip(ip))) {
                if ((p1->counters.incoming - p1->counters.incoming_history) < counter) {
                    p1->counters.incoming = p1->counters.incoming_history + counter;
                    debug(LOG_DEBUG, "%s - Updated counter.incoming to %llu bytes", ip, counter);
                }
            } else {
                debug(LOG_ERR, "Could not find %s in client list", ip);
            }
            UNLOCK_CLIENT_LIST();
        }
    }
    pclose(output);

    return 1;
}

本文章由 http://www.wifidog.pro/2015/02/02/wifidog%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%A3%80%E6%B5%8B.html 整理编辑,转载请注明出处

wifidog源码分析 - 认证服务器心跳检测线程

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

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

void
thread_ping(void *arg)
{
    pthread_cond_t        cond = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t        cond_mutex = PTHREAD_MUTEX_INITIALIZER;
    struct    timespec    timeout;

    while (1) {
        /* 调用ping,具体代码看 代码片段1.2 */
        debug(LOG_DEBUG, "Running ping()");
        ping();

        /* 睡眠一个checkinterval,默认为60s */
        timeout.tv_sec = time(NULL) + config_get_config()->checkinterval;
        timeout.tv_nsec = 0;


        pthread_mutex_lock(&cond_mutex);

        pthread_cond_timedwait(&cond, &cond_mutex, &timeout);

        pthread_mutex_unlock(&cond_mutex);
    }

代码片段1.2

static void
ping(void)
{
    ssize_t            numbytes;
    size_t                totalbytes;
    int            sockfd, nfds, done;
    char            request[MAX_BUF];
    fd_set            readfds;
    struct timeval        timeout;
    FILE * fh;
    unsigned long int sys_uptime  = 0;
    unsigned int      sys_memfree = 0;
    float             sys_load    = 0;
    t_auth_serv    *auth_server = NULL;
    auth_server = get_auth_server();

    debug(LOG_DEBUG, "Entering ping()");

    /* 其实认证服务器就是一个web服务器,路由器跟他做通信行为就是通过发送http请求进行通信,首先先连接认证服务器的http端口,获取其socket */
    sockfd = connect_auth_server();
    if (sockfd == -1) {
        /* 无法连接认证服务器,connect_auth_server分析见 代码片段1.3 */
        return;
    }

    /*
     * 从/proc文件系统获取路由器信息
     */
    if ((fh = fopen("/proc/uptime", "r"))) {
        fscanf(fh, "%lu", &sys_uptime);
        fclose(fh);
    }
    if ((fh = fopen("/proc/meminfo", "r"))) {
        while (!feof(fh)) {
            if (fscanf(fh, "MemFree: %u", &sys_memfree) == 0) {
                while (!feof(fh) && fgetc(fh) != '\n');
            }
            else {
                break;
            }
        }
        fclose(fh);
    }
    if ((fh = fopen("/proc/loadavg", "r"))) {
        fscanf(fh, "%f", &sys_load);
        fclose(fh);
    }

    /*
     * 准备http请求包
     */
    snprintf(request, sizeof(request) - 1,
            "GET %s%sgw_id=%s&sys_uptime=%lu&sys_memfree=%u&sys_load=%.2f&wifidog_uptime=%lu HTTP/1.0\r\n"
            "User-Agent: WiFiDog %s\r\n"
            "Host: %s\r\n"
            "\r\n",
            auth_server->authserv_path,
            auth_server->authserv_ping_script_path_fragment,
            config_get_config()->gw_id,
            sys_uptime,
            sys_memfree,
            sys_load,
            (long unsigned int)((long unsigned int)time(NULL) - (long unsigned int)started_time),
            VERSION,
            auth_server->authserv_hostname);

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

    debug(LOG_DEBUG, "Reading response");

    numbytes = totalbytes = 0;
    done = 0;
    do {
        FD_ZERO(&readfds);
        FD_SET(sockfd, &readfds);
        /* 设置超时30s */
        timeout.tv_sec = 30;
        timeout.tv_usec = 0;
        nfds = sockfd + 1;

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

        if (nfds > 0) {
            /* 多路复用 */
            numbytes = read(sockfd, request + totalbytes, MAX_BUF - (totalbytes + 1));
            if (numbytes < 0) {
                debug(LOG_ERR, "An error occurred while reading from auth server: %s", strerror(errno));
                close(sockfd);
                return;
            }
            else if (numbytes == 0) {
                done = 1;
            }
            else {
                totalbytes += numbytes;
                debug(LOG_DEBUG, "Read %d bytes, total now %d", numbytes, totalbytes);
            }
        }
        else if (nfds == 0) {
            debug(LOG_ERR, "Timed out reading data via select() from auth server");
            close(sockfd);
            return;
        }
        else if (nfds < 0) {
            debug(LOG_ERR, "Error reading data via select() from auth server: %s", strerror(errno));
            close(sockfd);
            return;
        }
    } while (!done);
    close(sockfd);

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

    request[totalbytes] = '\0';

    debug(LOG_DEBUG, "HTTP Response from Server: [%s]", request);
    /* 判断认证服务器返回包中有没有"Pong"字符串 */
    if (strstr(request, "Pong") == 0) {
        debug(LOG_WARNING, "Auth server did NOT say pong!");

    }
    else {
        debug(LOG_DEBUG, "Auth Server Says: Pong");
    }

    return;    
}

代码片段1.3

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

int connect_auth_server() {
    int sockfd;

    LOCK_CONFIG();
    /* 连接认证服务器 */
    sockfd = _connect_auth_server(0);
    UNLOCK_CONFIG();

    if (sockfd == -1) {
        debug(LOG_ERR, "Failed to connect to any of the auth servers");
        /* 标记认证服务器离线 */
        mark_auth_offline();
    }
    else {
        debug(LOG_DEBUG, "Connected to auth server");
        /* 标记认证服务器在线 */
        mark_auth_online();
    }
    return (sockfd);
}



int _connect_auth_server(int level) {
    s_config *config = config_get_config();
    t_auth_serv *auth_server = NULL;
    struct in_addr *h_addr;
    int num_servers = 0;
    char * hostname = NULL;
    /* 公共网站,用于判断DNS问题 */
    char * popular_servers[] = {
          "www.google.com",
          "www.yahoo.com",
          NULL
    };
    char ** popularserver;
    char * ip;
    struct sockaddr_in their_addr;
    int sockfd;

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

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

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

        for (popularserver = popular_servers; *popularserver; popularserver++) {
            debug(LOG_DEBUG, "Level %d: Resolving popular server [%s]", level, *popularserver);
            h_addr = wd_gethostbyname(*popularserver);
            /* 公共网站DNS解析正确 */
            if (h_addr) {
                debug(LOG_DEBUG, "Level %d: Resolving popular server [%s] succeeded = [%s]", level, *popularserver, inet_ntoa(*h_addr));
                break;
            }
            else {
                debug(LOG_DEBUG, "Level %d: Resolving popular server [%s] failed", level, *popularserver);
            }
        }

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

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

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

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

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

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

        if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1) {
            /*
             * 连接失败
             * 将此认证服务器放入bad_server链表,并将config->auth_server指向认证服务器的下一个节点
             */
            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));
            close(sockfd);
            mark_auth_server_bad(auth_server);
            return _connect_auth_server(level); /* Yay recursion! */
        }
        else {
            /*
             * 连接成功
             */
            debug(LOG_DEBUG, "Level %d: Successfully connected to auth server %s:%d", level, hostname, auth_server->authserv_http_port);
            return sockfd;
        }
    }
}

本文章由 请输入链接描述 整理编辑,转载请注明出处

wifidog源码分析 - 初始化阶段

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

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

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

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

代码片段1.1:
int main(int argc, char **argv) {

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

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

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

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

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

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

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

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

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

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

         debug(LOG_INFO, "Forking into background");

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

             default: /* parent */
                 exit(0);
                 break;
         }
     }
     else {
         append_x_restartargv();
         main_loop();
     }

     return(0); /* never reached */
}

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

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

void get_clients_from_parent(void) {
    int sock;
    struct sockaddr_un    sa_un;
    s_config * config = NULL;
    char linebuffer[MAX_BUF];
    int len = 0;
    char *running1 = NULL;
    char *running2 = NULL;
    char *token1 = NULL;
    char *token2 = NULL;
    char onechar;
    char *command = NULL;
    char *key = NULL;
    char *value = NULL;
    t_client * client = NULL;
    t_client * lastclient = NULL;

    config = config_get_config();

    debug(LOG_INFO, "Connecting to parent to download clients");

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

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

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

    LOCK_CLIENT_LIST();

    command = NULL;
    memset(linebuffer, 0, sizeof(linebuffer));
    len = 0;
    client = NULL;
    /* 接收数据,逐个字符接收 */
    /* 数据包格式为 CLIENT|ip=%s|mac=%s|token=%s|fw_connection_state=%u|fd=%d|counters_incoming=%llu|counters_outgoing=%llu|counters_last_updated=%lu\n */
    while (read(sock, &onechar, 1) == 1) {
        if (onechar == '\n') {
            /* 如果接收到末尾('\n'),则转为'\0' */
            onechar = '\0';
        }
        linebuffer[len++] = onechar;

        if (!onechar) {
            /* 以下将数据转化为t_client结构体添加到客户端列表 */
            debug(LOG_DEBUG, "Received from parent: [%s]", linebuffer);
            running1 = linebuffer;
            while ((token1 = strsep(&running1, "|")) != NULL) {
                if (!command) {
                    /* The first token is the command */
                    command = token1;
                }
                else {
                /* Token1 has something like "foo=bar" */
                    running2 = token1;
                    key = value = NULL;
                    while ((token2 = strsep(&running2, "=")) != NULL) {
                        if (!key) {
                            key = token2;
                        }
                        else if (!value) {
                            value = token2;
                        }
                    }
                }

                if (strcmp(command, "CLIENT") == 0) {
                    /* This line has info about a client in the client list */
                    if (!client) {
                        /* Create a new client struct */
                        client = safe_malloc(sizeof(t_client));
                        memset(client, 0, sizeof(t_client));
                    }
                }

                if (key && value) {
                    if (strcmp(command, "CLIENT") == 0) {
                        /* Assign the key into the appropriate slot in the connection structure */
                        if (strcmp(key, "ip") == 0) {
                            client->ip = safe_strdup(value);
                        }
                        else if (strcmp(key, "mac") == 0) {
                            client->mac = safe_strdup(value);
                        }
                        else if (strcmp(key, "token") == 0) {
                            client->token = safe_strdup(value);
                        }
                        else if (strcmp(key, "fw_connection_state") == 0) {
                            client->fw_connection_state = atoi(value);
                        }
                        else if (strcmp(key, "fd") == 0) {
                            client->fd = atoi(value);
                        }
                        else if (strcmp(key, "counters_incoming") == 0) {
                            client->counters.incoming_history = atoll(value);
                            client->counters.incoming = client->counters.incoming_history;
                        }
                        else if (strcmp(key, "counters_outgoing") == 0) {
                            client->counters.outgoing_history = atoll(value);
                            client->counters.outgoing = client->counters.outgoing_history;
                        }
                        else if (strcmp(key, "counters_last_updated") == 0) {
                            client->counters.last_updated = atol(value);
                        }
                        else {
                            debug(LOG_NOTICE, "I don't know how to inherit key [%s] value [%s] from parent", key, value);
                        }
                    }
                }
            }

            /* End of parsing this command */
            if (client) {
                /* Add this client to the client list */
                if (!firstclient) {
                    firstclient = client;
                    lastclient = firstclient;
                }
                else {
                    lastclient->next = client;
                    lastclient = client;
                }
            }

            /* Clean up */
            command = NULL;
            memset(linebuffer, 0, sizeof(linebuffer));
            len = 0;
            client = NULL;
        }
    }

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

    close(sock);
}

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

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

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

    fd = (int)arg;

    debug(LOG_DEBUG, "Read bytes and stuff from %d", fd);

    /* 初始化变量 */
    read_bytes = 0;
    done = 0;
    memset(request, 0, sizeof(request));

    /* 读取命令 */
    while (!done && read_bytes < (sizeof(request) - 1)) {
        len = read(fd, request + read_bytes,
                sizeof(request) - read_bytes);    //读取wdctrl发送的命令

        /* 判断命令正确性 */
        for (i = read_bytes; i < (read_bytes + len); i++) {
            if (request[i] == '\r' || request[i] == '\n') {
                request[i] = '\0';
                done = 1;
            }
        }

        /* Increment position */
        read_bytes += len;
    }

        //判断命令
    if (strncmp(request, "status", 6) == 0) {
        wdctl_status(fd);
    } else if (strncmp(request, "stop", 4) == 0) {
        wdctl_stop(fd);
    } else if (strncmp(request, "reset", 5) == 0) {
        wdctl_reset(fd, (request + 6));
    } else if (strncmp(request, "restart", 7) == 0) {
        wdctl_restart(fd);    //执行wdctl_restart(int afd)函数
    }

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

    debug(LOG_DEBUG, "Request received: [%s]", request);

        //关闭套接字
    shutdown(fd, 2);
    close(fd);
    debug(LOG_DEBUG, "Exiting thread_wdctl_handler....");

    return NULL;
}


//wdctl_restart(int afd)函数详解
static void
wdctl_restart(int afd)
{
    int    sock,
        fd;
    char    *sock_name;
    struct     sockaddr_un    sa_un;
    s_config * conf = NULL;
    t_client * client = NULL;
    char * tempstring = NULL;
    pid_t pid;
    ssize_t written;
    socklen_t len;

    conf = config_get_config();

    debug(LOG_NOTICE, "Will restart myself");

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

    if (strlen(sock_name) > (sizeof(sa_un.sun_path) - 1)) {

        debug(LOG_ERR, "INTERNAL socket name too long");
        return;
    }

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

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

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

    debug(LOG_DEBUG, "Filling sockaddr_un");
    strcpy(sa_un.sun_path, sock_name); 
    sa_un.sun_family = AF_UNIX;

    debug(LOG_DEBUG, "Binding socket (%s) (%d)", sa_un.sun_path, strlen(sock_name));


    if (bind(sock, (struct sockaddr *)&sa_un, strlen(sock_name) + sizeof(sa_un.sun_family))) {
        debug(LOG_ERR, "Could not bind internal socket: %s", strerror(errno));
        return;
    }

    if (listen(sock, 5)) {
        debug(LOG_ERR, "Could not listen on internal socket: %s", strerror(errno));
        return;
    }

    /*
     * socket建立完成,创建子进程
     */
    debug(LOG_DEBUG, "Forking in preparation for exec()...");
    pid = safe_fork();
    if (pid > 0) {
        /* 父进程 */

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

        close(sock);

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

        /*子进程已经完成连接,发送客户端列表 */
        LOCK_CLIENT_LIST();
        client = client_get_first_client();    //获取第一个客户端
        while (client) {
            /* Send this client */
            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);
            debug(LOG_DEBUG, "Sending to child client data: %s", tempstring);
            len = 0;
            while (len != strlen(tempstring)) {
                written = write(fd, (tempstring + len), strlen(tempstring) - len);    //发送给子进程
                if (written == -1) {
                    debug(LOG_ERR, "Failed to write client data to child: %s", strerror(errno));
                    free(tempstring);
                    break;
                }
                else {
                    len += written;
                }
            }
            free(tempstring);
            client = client->next;
        }
        UNLOCK_CLIENT_LIST();

        close(fd);

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

        shutdown(afd, 2);
        close(afd);


        wdctl_stop(afd);
    }
    else {
        /* 子进程,先关闭资源 */
        close(wdctl_socket_server);
        close(icmp_fd);
        close(sock);
        shutdown(afd, 2);
        close(afd);
        debug(LOG_NOTICE, "Re-executing myself (%s)", restartargv[0]);

        setsid();
        execvp(restartargv[0], restartargv);    //执行外部命令,这里重新启动wifidog

        debug(LOG_ERR, "I failed to re-execute myself: %s", strerror(errno));
        debug(LOG_ERR, "Exiting without cleanup");
        exit(1);
    }
}

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

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

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

wifidog源码分析 - wifidog原理

wifidog是一个用于配合认证服务器实现无线网页认证功能的程序,常见的情景就是使用于公共场合的无线wifi接入点,首先移动设备会连接公共wifi接入点,之后会弹出网页要求输入用户名密码,认证过后才能够连入外网。其主页是http://dev.wifidog.org/
实现原理
  其实wifidog原理很简单,主要是通过管控iptables,配合认证服务器进行客户端的放行操作。wifidog在启动后都会自动启动三个线程,分别为客户端检测线程、wdctrl交互线程、认证服务器心跳检测线程。每当新用户连接无线AP并浏览网页时,wifidog会获取新用户的此次操作,并返回一个重定向到认证服务器的http于用户,此后用户通过认证服务器认证后,再继续浏览网页时,wifidog会询问认证服务器此用户权限,若放行则修改iptables放行此用户IP。

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

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

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

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

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

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

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

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

7)循环等待客户端连接
  这里主要会接收到两种类型的客户端连接

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

具体代码实现见之后章节

本文章由 http://www.wifidog.pro/2015/02/02/wifidog%E5%8E%9F%E7%90%86.html 整理编辑,转载请注明出处