分类 wifidog分析 下的文章

wifidog源码分析 - 用户连接过程(2)

用户连接启动线程(void thread_httpd(void * args))
代码片段1.3分析

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

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

代码片段1.3:

void
http_callback_404(httpd *webserver, request *r)
{
    char        tmp_url[MAX_BUF],
            *url;
    s_config    *config = config_get_config();
    t_auth_serv    *auth_server = get_auth_server();

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

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

    if (!is_online()) {
        /* 本机处于离线状态,此函数调用结果由认证服务器检测线程设置 */
        char * buf;
        safe_asprintf(&buf, 
            "<p>We apologize, but it seems that the internet connection that powers this hotspot is temporarily unavailable.</p>"
            "<p>If at all possible, please notify the owners of this hotspot that the internet connection is out of service.</p>"
            "<p>The maintainers of this network are aware of this disruption.  We hope that this situation will be resolved soon.</p>"
            "<p>In a while please <a href='%s'>click here</a> to try your request again.</p>", tmp_url);

                send_http_page(r, "Uh oh! Internet access unavailable!", buf);
        free(buf);
        debug(LOG_INFO, "Sent %s an apology since I am not online - no point sending them to auth server", r->clientAddr);
    }
    else if (!is_auth_online()) {
        /* 认证服务器处于离线状态 */
        char * buf;
        safe_asprintf(&buf, 
            "<p>We apologize, but it seems that we are currently unable to re-direct you to the login screen.</p>"
            "<p>The maintainers of this network are aware of this disruption.  We hope that this situation will be resolved soon.</p>"
            "<p>In a couple of minutes please <a href='%s'>click here</a> to try your request again.</p>", tmp_url);

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

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

void 
http_callback_auth(httpd *webserver, request *r)
{
    t_client    *client;
    httpVar * token;
    char    *mac;
    /* 判断http报文是否包含登出logout */
    httpVar *logout = httpdGetVariableByName(r, "logout");
    if ((token = httpdGetVariableByName(r, "token"))) {
        /* 获取http报文中的token */
        if (!(mac = arp_get(r->clientAddr))) {
            /* 获取客户端mac地址失败 */
            debug(LOG_ERR, "Failed to retrieve MAC address for ip %s", r->clientAddr);
            send_http_page(r, "WiFiDog Error", "Failed to retrieve your MAC address");
        } else {
            LOCK_CLIENT_LIST();
            /* 判断客户端是否存在于列表中 */
            if ((client = client_list_find(r->clientAddr, mac)) == NULL) {
                debug(LOG_DEBUG, "New client for %s", r->clientAddr);
                /* 将此客户端添加到客户端列表 */
                client_list_append(r->clientAddr, mac, token->value);
            } else if (logout) {
                /* http报文为登出 */
                t_authresponse  authresponse;
                s_config *config = config_get_config();
                unsigned long long incoming = client->counters.incoming;
                unsigned long long outgoing = client->counters.outgoing;
                char *ip = safe_strdup(client->ip);
                char *urlFragment = NULL;
                t_auth_serv    *auth_server = get_auth_server();
                /* 修改iptables禁止客户端访问外网 */                
                fw_deny(client->ip, client->mac, client->fw_connection_state);
                /* 从客户端列表中删除此客户端 */
                client_list_delete(client);
                debug(LOG_DEBUG, "Got logout from %s", client->ip);

                if (config->auth_servers != NULL) {
                    UNLOCK_CLIENT_LIST();
                    /* 发送登出认证包给认证服务器 */
                    auth_server_request(&authresponse, REQUEST_TYPE_LOGOUT, ip, mac, token->value, 
                                        incoming, outgoing);
                    LOCK_CLIENT_LIST();

                    /* 将客户端重定向到认证服务器 */
                    debug(LOG_INFO, "Got manual logout from client ip %s, mac %s, token %s"
                    "- redirecting them to logout message", client->ip, client->mac, client->token);
                    safe_asprintf(&urlFragment, "%smessage=%s",
                        auth_server->authserv_msg_script_path_fragment,
                        GATEWAY_MESSAGE_ACCOUNT_LOGGED_OUT
                    );
                    http_send_redirect_to_auth(r, urlFragment, "Redirect to logout message");
                    free(urlFragment);
                }
                free(ip);
             } 
             else {
                debug(LOG_DEBUG, "Client for %s is already in the client list", client->ip);
            }
            UNLOCK_CLIENT_LIST();
            if (!logout) {
                /* 通过认证服务器认证此客户端token */
                authenticate_client(r);
            }
            free(mac);
        }
    } else {
        send_http_page(r, "WiFiDog error", "Invalid token");
    }
}

/* 此函数用于提交token到认证服务器进行认证 */
void
authenticate_client(request *r)
{
    t_client    *client;
    t_authresponse    auth_response;
    char    *mac,
        *token;
    char *urlFragment = NULL;
    s_config    *config = NULL;
    t_auth_serv    *auth_server = NULL;

    LOCK_CLIENT_LIST();

    client = client_list_find_by_ip(r->clientAddr);
    /* 判断此客户端是否在列表中 */
    if (client == NULL) {
        debug(LOG_ERR, "Could not find client for %s", r->clientAddr);
        UNLOCK_CLIENT_LIST();
        return;
    }

    mac = safe_strdup(client->mac);
    token = safe_strdup(client->token);

    UNLOCK_CLIENT_LIST();

    /* 提交token、客户端ip、客户端mac至认证服务器 */
    auth_server_request(&auth_response, REQUEST_TYPE_LOGIN, r->clientAddr, mac, token, 0, 0);

    LOCK_CLIENT_LIST();

    /*再次判断客户端是否存在于列表中,保险起见,因为有可能在于认证服务器认证过程中,客户端检测线程把此客户端下线 */
    client = client_list_find(r->clientAddr, mac);

    if (client == NULL) {
        debug(LOG_ERR, "Could not find client node for %s (%s)", r->clientAddr, mac);
        UNLOCK_CLIENT_LIST();
        free(token);
        free(mac);
        return;
    }

    free(token);
    free(mac);

    config = config_get_config();
    auth_server = get_auth_server();

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

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

    case AUTH_DENIED:
        /* 认证服务器拒绝此客户端 */
        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);
        safe_asprintf(&urlFragment, "%smessage=%s",
            auth_server->authserv_msg_script_path_fragment,
            GATEWAY_MESSAGE_DENIED
        );
        http_send_redirect_to_auth(r, urlFragment, "Redirect to denied message");
        free(urlFragment);
        break;

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

    case AUTH_ALLOWED:
        /* 认证通过 */
        debug(LOG_INFO, "Got ALLOWED from central server authenticating token %s from %s at %s - "
                "adding to firewall and redirecting them to portal", client->token, client->ip, client->mac);
        client->fw_connection_state = FW_MARK_KNOWN;
        fw_allow(client->ip, client->mac, FW_MARK_KNOWN);
        served_this_session++;
        safe_asprintf(&urlFragment, "%sgw_id=%s",
            auth_server->authserv_portal_script_path_fragment,
            config->gw_id
        );
        http_send_redirect_to_auth(r, urlFragment, "Redirect to portal");
        free(urlFragment);
        break;

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

    default:
        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);
        send_http_page(r, "Internal Error", "We can not validate your request at this time");
        break;

    }

    UNLOCK_CLIENT_LIST();
    return;
}

本文章由 http://www.wifidog.pro/2015/02/03/wifidog%E7%94%A8%E6%88%B7%E8%BF%9E%E6%8E%A5_2.html 整理编辑,转载请注明出处

wifidog源码分析 - 用户连接过程(1)

引言
  之前的文章已经描述wifidog大概的一个工作流程,这里我们具体说说wifidog是怎么把一个新用户重定向到认证服务器中的,它又是怎么对一个已认证的用户实行放行操作的。我们已经知道wifidog在启动时会删除iptables中mangle、nat、filter表中的所有规则,并在这三个表中添加wifidog自己的规则,其规则简单来说就是将网关80端口重定向到指定端口(默认为2060),禁止非认证用户连接外网(除认证服务器外)。当有新用户连接路由器上网时,wifidog会通过监听2060端口获取新用户的http报文,通过报文即可知道报文是否携带token进行认证,如果没有token,wifidog会返回一个重定向的http报文给新用户,用户则会跳转到认证服务器进行认证,当认证成功后,认真服务器又会对用户重定向到网关,并在重定向报文中添加关键路径"/wifidog/auth"和token,wifidog重新获取到用户的http报文,检测到包含关键路径"/wifidog/auth"后,会通过认证服务器验证token是否有效,有效则修改iptables放行此客户端。

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

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

代码片段1.1

static void
main_loop(void)
{
    int result;
    pthread_t    tid;
    s_config *config = config_get_config();
    request *r;
    void **params;

    /* 设置启动时间 */
    if (!started_time) {
        debug(LOG_INFO, "Setting started_time");
        started_time = time(NULL);
    }
    else if (started_time < MINIMUM_STARTED_TIME) {
        debug(LOG_WARNING, "Detected possible clock skew - re-setting started_time");
        started_time = time(NULL);
    }

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

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

    /* 初始化监听网关2060端口的socket */
    debug(LOG_NOTICE, "Creating web server on %s:%d", config->gw_address, config->gw_port);

    if ((webserver = httpdCreate(config->gw_address, config->gw_port)) == NULL) {
        debug(LOG_ERR, "Could not create web server: %s", strerror(errno));
        exit(1);
    }

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

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

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

    /* wdctrl交互线程 */
    result = pthread_create(&tid, NULL, (void *)thread_wdctl, (void *)safe_strdup(config->wdctl_sock));
    if (result != 0) {
        debug(LOG_ERR, "FATAL: Failed to create a new thread (wdctl) - exiting");
        termination_handler(0);
    }
    pthread_detach(tid);

    /* 认证服务器心跳检测线程 */
    result = pthread_create(&tid_ping, NULL, (void *)thread_ping, NULL);
    if (result != 0) {
        debug(LOG_ERR, "FATAL: Failed to create a new thread (ping) - exiting");
        termination_handler(0);
    }
    pthread_detach(tid_ping);

    debug(LOG_NOTICE, "Waiting for connections");
    while(1) {
        /* 监听2060端口等待用户http请求 */
        r = httpdGetConnection(webserver, NULL);

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

            /* 开启http请求处理线程 */
            result = pthread_create(&tid, NULL, (void *)thread_httpd, (void *)params);
            if (result != 0) {
                debug(LOG_ERR, "FATAL: Failed to create a new thread (httpd) - exiting");
                termination_handler(0);
            }
            pthread_detach(tid);
        }
        else {
            ;
        }
    }

    /* never reached */
}

用户连接启动线程(void thread_httpd(void * args))
代码片段1.2分析

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

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

代码片段1.2

void
thread_httpd(void *args)
{
    void    **params;
    httpd    *webserver;
    request    *r;

    params = (void **)args;
    webserver = *params;
    r = *(params + 1);
    free(params);

    /* 获取http报文 */
    if (httpdReadRequest(webserver, r) == 0) {
        debug(LOG_DEBUG, "Processing request from %s", r->clientAddr);
        debug(LOG_DEBUG, "Calling httpdProcessRequest() for %s", r->clientAddr);
        /* 分析http报文 */
        httpdProcessRequest(webserver, r);
        debug(LOG_DEBUG, "Returned from httpdProcessRequest() for %s", r->clientAddr);
    }
    else {
        debug(LOG_DEBUG, "No valid request received from %s", r->clientAddr);
    }
    debug(LOG_DEBUG, "Closing connection with %s", r->clientAddr);
    httpdEndRequest(r);
}



/* 被thread_httpd调用 */
void httpdProcessRequest(httpd *server, request *r)
{
    char    dirName[HTTP_MAX_URL],
        entryName[HTTP_MAX_URL],
        *cp;
    httpDir    *dir;
    httpContent *entry;

    r->response.responseLength = 0;
    strncpy(dirName, httpdRequestPath(r), HTTP_MAX_URL);
    dirName[HTTP_MAX_URL-1]=0;
    cp = rindex(dirName, '/');
    if (cp == NULL)
    {
        printf("Invalid request path '%s'\n",dirName);
        return;
    }
    strncpy(entryName, cp + 1, HTTP_MAX_URL);
    entryName[HTTP_MAX_URL-1]=0;
    if (cp != dirName)
        *cp = 0;
    else
        *(cp+1) = 0;

     /* 获取http报文中的关键路径,在main_loop中已经设置 */
    dir = _httpd_findContentDir(server, dirName, HTTP_FALSE);
    if (dir == NULL)
    {
        /* http报文中未包含关键路径,执行404回调函数(在404回调函数中新用户被重定向到认证服务器),见代码片段1.3 */
        _httpd_send404(server, r);
        _httpd_writeAccessLog(server, r);
        return;
    }
    /* 获取关键路径内容描述符 */
    entry = _httpd_findContentEntry(r, dir, entryName);
    if (entry == NULL)
    {
        _httpd_send404(server, r);
        _httpd_writeAccessLog(server, r);
        return;
    }
    if (entry->preload)
    {
        if ((entry->preload)(server) < 0)
        {
            _httpd_writeAccessLog(server, r);
            return;
        }
    }
    switch(entry->type)
    {
        case HTTP_C_FUNCT:
        case HTTP_C_WILDCARD:
            /* 如果是被认证服务器重定向到网关的用户,此处的关键路径为"/wifidog/auth",并执行回调函数 */
            (entry->function)(server, r);
            break;

        case HTTP_STATIC:
            _httpd_sendStatic(server, r, entry->data);
            break;

        case HTTP_FILE:
            _httpd_sendFile(server, r, entry->path);
            break;

        case HTTP_WILDCARD:
            if (_httpd_sendDirectoryEntry(server, r, entry,
                        entryName)<0)
            {
                _httpd_send404(server, r);
            }
            break;
    }
    _httpd_writeAccessLog(server, r);
}

本文章由 http://www.wifidog.pro/2015/02/03/wifidog%E7%94%A8%E6%88%B7%E8%BF%9E%E6%8E%A5%E8%BF%87%E7%A8%8B_1.html 整理编辑,转载请注明出处

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;
        }
    }
}

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