IP門禁:保姆式教你用PHP實現一個IP防火牆

2022-08-06 18:00:47

最近我遇到一個需求,我的一臺伺服器總是遭到埠掃描和惡意登入攻擊,對此可以怎麼辦呢?似乎除了內網隔離、增強密碼認證、證書登入、設定防火牆iptables,網上找不到什麼別的方案,對了,還用堡壘機的方案。

這些方案實際上都無法解決我的問題。這是一臺公網伺服器,並沒有什麼複雜的網路結構,所以不能建立內網隔離。調整賬號的密碼策略,自然是一個方案,但是人工操作太麻煩,而且我一般經常換電腦使用,如果修改密碼,公司的和家裡的電腦都要更新,很麻煩。設定防火牆自然是運維的基本操作,但是iptables的設定太麻煩,ufw工具還好些,firewall-cmd就麻煩些,而且有一個巨大的痛點,眾所周知,大家的出網IP都會經常變,好不容易在命令列裡一個字母一個字母的設定好了,睡了一覺,白費了。堡壘機更不是一個主流的方案,有點大材小用,用了堡壘機,反而不能隨意使用系統,更何況還沒聽說過那個免費的堡壘機呢。【推薦:】

那怎麼辦呢,作為一個資深的PHP開發者,伺服器這塊的應用還不是手到擒來,當初連內網穿透都能輕鬆實現,一個IP過濾系統,小意思。所以我打算自己開發這樣一個專案,首先能夠實現IP過濾,另外,可以輕鬆地將IP加入到白名單裡,比如存取一個網頁,就自動加入到白名單。

整個專案不到幾個小時就研發完了,起碼滿足了我自己的需求,並且實現了這樣幾個特性:

  • 多程序
  • 支援並行
  • 守護行程
  • 可以通過網頁面板管理IP
  • 流量統計
  • 攔截記錄

現在我們來一步一步的實現這個系統。

第一步,首先能夠簡簡單單的過濾IP

使用PHP監聽埠並且轉發資料的框架很多,對此我選擇workerman,原因有3

  • 執行簡單穩定
  • 方法介面簡單
  • 內建程序守護

至於具體的安裝方法,可以參考他的官方檔案。

版權宣告:本文由phpreturn.comPHP武器庫官網)原創和首發,所有權利歸phpreturnPHP武器庫)所有,本站允許任何形式的轉載/參照文章,但必須同時註明出處。

0f7d3163d512f1d5ed4ba46e69929b1.jpg

workerman的使用方法非常簡單,只要10行程式碼,就實現了IP轉發+白名單過濾:

$worker = new Worker('tcp:0.0.0.0:' . Config::get('door.port_in'));
// 監聽一個埠
$worker->count = 2;
// 設定多程序
$worker->onConnect = function (TcpConnection $connection) {
    // 獲取IP白名單
    $list_ip = AppIp::where('status', 0)->cache(3)->column('ip');
    $remote_ip = $connection->getRemoteIp();
    // 攔截IP
    if (!in_array($remote_ip, $list_ip)) {
        $connection->close();
    }
    // 放行連線,連線內部目標埠
    $to_connection = new AsyncTcpConnection('tcp:127.0.0.1:' . Config::get('door.port_to'));
    // 互相轉發流量
    $connection->pipe($to_connection);
    $to_connection->pipe($connection);
    $to_connection->connect();
}

正如上面程式碼所示,只有簡單幾行,便實現了IP監聽和轉發,其中IP白名單通過資料庫查詢,並且快取。

第二步,與ThinkPHP命令列整合在一起

為了專案開發方便,我都會使用ThinkPHP框架進行開發,它夠簡單,功能也比較齊全。

90a646662d0d418b6403066b1dc6ae5.jpg

最終實現的命令列效果如下:

版權宣告:本文由phpreturn.comPHP武器庫官網)原創和首發,所有權利歸phpreturnPHP武器庫)所有,本站允許任何形式的轉載/參照文章,但必須同時註明出處。

執行命令
php think door start
php think door start --mode d  // 守護行程重新啟動
重新啟動
php think door restart
停止
php think door stop

workerman的命令引數與thinkphp並不相容,但是實現這樣的效果並不難,實際上很簡單,程式碼如下:

<?php

declare(strict_types=1);

namespace app\common\command;

use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;

class Door extends Command
{
    protected function configure()
    {
        // 指令設定
        $this->setName('door')
            // 設定think的命令引數
            ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload|status|connections", 'start')
            ->addOption('mode', 'm', Option::VALUE_OPTIONAL, 'Run the workerman server in daemon mode.')
            ->setDescription('the door command');
    }
    protected function execute(Input $input, Output $output)
    {
        // 指令輸出
        $output->writeln('door');
        $action = $input->getArgument('action');
        $mode = $input->getOption('mode');
        // 重新構造命令列引數,以便相容workerman的命令
        global $argv;
        $argv = [];
        array_unshift($argv, 'think', $action);
        if ($mode == 'd') {
            $argv[] = '-d';
        } else if ($mode == 'g') {
            $argv[] = '-g';
        }
        // ...workerman的程式碼
    }
}

在上面的程式碼中,主要做了兩件事:

  • 實現ThinkPHP的命令設定
  • 將命令引數重新構造為workerman相容的方式

第三步,實現管理面板

使用PHP實現一個管理面板太簡單了,PHP到處都是這樣的後臺框架,這裡我選擇ulthon_admin,這是我自己開發維護的,它基於ThinkPHP6,很簡單,為客製化而生,不搞所謂的「外掛」和「市場」生態,能夠自動生成CURD程式碼,並且內建幾了幾個有趣的面板。

最終效果如下:

6a1cf89468ce897aa700f03f437ed0d.jpg5e7ee7ff5cdfa74c0176ab3b79ad04a.jpg

以上是ulthon_admin內建的兩款面板效果,分別是:科幻、畫素。

對於面板的管理,這裡多做介紹,這算是PHP開發者的基本功,誰還不會個CURD啊。

版權宣告:本文由phpreturn.comPHP武器庫官網)原創和首發,所有權利歸phpreturnPHP武器庫)所有,本站允許任何形式的轉載/參照文章,但必須同時註明出處。

第四步,進階,更好的效能和流量統計

我們的IP攔截使用者端需要執行在伺服器上,並且直接連線資料庫,如果每次收到請求都要查詢資料庫,那麼很有可能導致連線不通暢,尤其是使用者端和資料庫本身位置較遠的時候。在第一步的程式碼中,我們只是簡單的使用了查詢快取,但是還不夠,還可以優化。並且我們可以在管理面板的截圖中看到,我們是可以統計流量和攔截次數的,現在我們要實現這些功能:

流量統計

首先我們將第一個步中,流量轉發部分的程式碼改造成如下的樣子:

<?php
// 向TO發起連線
$to_connection = new AsyncTcpConnection('tcp://127.0.0.1:' . Config::get('door.port_to'));
$to_connection->onMessage = function ($source, $data) use ($connection, $remote_ip) {
    // 接收到來自TO的資料,返回的資料
    $connection->send($data);
    // 將流量統計儲存到記憶體裡
    Cache::inc(md5($remote_ip) . '-to', strlen($data));
};
// 流程和流量控制
$to_connection->onClose = function ($source) use ($connection) {
    $connection->close();
};
$connection->onBufferFull = function ($dest) use ($to_connection) {
    $to_connection->pauseRecv();
};
$connection->onBufferDrain = function ($dest) use ($to_connection) {
    $to_connection->resumeRecv();
};
$connection->onMessage = function ($source, $data) use ($to_connection, $remote_ip) {
    // 接收來自IN的資料,請求的資料
    $to_connection->send($data);
    // 將流量統計儲存到記憶體裡
    Cache::inc(md5($remote_ip) . '-in', strlen($data));
};
// 流程和流量控制
$connection->onClose = function ($source) use ($to_connection) {
    $to_connection->close();
};
$to_connection->onBufferFull = function ($dest) use ($connection) {
    $connection->pauseRecv();
};
$to_connection->onBufferDrain = function ($dest) use ($connection) {
    $connection->resumeRecv();
};

在第一部的程式碼中,只用兩行便實現了這些程式碼:

// 放行連線,連線內部目標埠
$to_connection = new AsyncTcpConnection('tcp:127.0.0.1:' . Config::get('door.port_to'));
// 互相轉發流量
$connection->pipe($to_connection);
$to_connection->pipe($connection);

這裡使用的是workerman內建的流量轉發,它很好用,但是這裡我們要統計流量,所以我們手動轉發流量。

這裡我們將統計的資料儲存到快取裡,而不是直接連線資料庫更新,這是為了更好的連線效能。我們會另外開啟一個程序將這些改動更新到資料庫。後面會介紹到。

攔截統計

我們將第一步中的載入IP白名單的邏輯改成下面這樣:

版權宣告:本文由phpreturn.comPHP武器庫官網)原創和首發,所有權利歸phpreturnPHP武器庫)所有,本站允許任何形式的轉載/參照文章,但必須同時註明出處。

<?php
$worker->onConnect = function (TcpConnection $connection) {
    $disable_cache_key = 'disable_ip_list';
    $list_ip = Cache::get($disable_cache_key);
    if (empty($list_ip)) {
        $connection->close();
    }
    $remote_ip = $connection->getRemoteIp();
    if (!in_array($remote_ip, $list_ip)) {
        AppIpReject::initRecord($remote_ip);
        $connection->close();
    }
};

在這裡我們不連線資料庫查詢,而是直接從本地快取讀取白名單,這樣會有更好的效能。我們會在另一個程序中更新這份白名單。

另外我們可以看到,攔截的IP呼叫了一個靜態方法,這裡的功能很簡單,判斷資料庫中該IP是否存在,如果不存在則新增,如果存在,則更新攔截次數+·1。這裡就不多介紹了。這裡也沒有必要做什麼效能優化,反正本來就是攔截的IP,優化個毛。

高效能處理快取資料

上面我們介紹,我們會另外開啟一個程序,維護IP白名單,並且將流量統計提交到資料庫。這就是這個程序:

<?php
$worker_ip = new Worker();
$worker_ip->name = 'report';
$worker_ip->onWorkerStart = function () {
    Timer::add(5, function () {
        $disable_cache_key = 'disable_ip_list';
        $list_ip = AppIp::where('status', 1)->column('ip');
        Cache::set($disable_cache_key, $list_ip);
        foreach ($list_ip as  $ip) {
            $ip_md5 = md5($ip);
            $in_length = Cache::pull("$ip_md5-in");
            // 請求的資料
            $to_length = Cache::pull("$ip_md5-to");
            // 返回的資料
            if (!empty($in_length) || !empty($to_length)) {
                $model_ip = AppIp::where('ip', $ip)->find();
                $model_ip->in_buffer += $in_length;
                $model_ip->to_buffer += $to_length;
                $model_ip->save();
            }
        }
    });
};

他做的事情很簡單,讀取快取,更新資料到資料庫,並且更新IP白名單。這裡不需要考慮它和資料庫之間的效能問題,這是額外的程序,不影響埠的連線和轉發。

下一步,更好的效能設計

以上,只有幾行程式碼,幾個小時(如果不含設計系統的時間,程式碼量可能只有一兩個小時。還能再怎麼優化呢?實際上還是可以優化的。

更好的記憶體驅動

這裡使用的是ThinkPHP內建的檔案快取,儲存到磁碟上,以上方法,在大量連線並行時,肯定受制於磁碟的效能。所以自然而然,我們可以使用記憶體快取。

版權宣告:本文由phpreturn.comPHP武器庫官網)原創和首發,所有權利歸phpreturnPHP武器庫)所有,本站允許任何形式的轉載/參照文章,但必須同時註明出處。

但是使用記憶體快取,redis可以嗎?並不好。這裡是使用者端,它只是想簡簡單單實現一個攔截轉發,還要再部署redis,不可取。

但實際上,workerman本身內建了資料共用元件,這是一個很好的方案。相當於一個極簡的redis。完美符合我們的需求。但是我並沒有實現這個功能,目前的系統已經符合我的場景。

更好的使用者端

目前攔截IP使用者端和管理面板整合在一起,使用相同的設定,面板基於ThinkPHP,使用者端只是ThinkPHP的一個命令。我之所以這樣做,是希望直接在Workerman中使用ThinkPHP的眾多特性(資料庫、快取

實際上,我們可以將使用者端的程式碼,另外開一個專案,使使用者端和麵板獨立開。在面板上實現通用得API。使用者端通過API運算元據。這樣使用者端就不需要連線資料庫。好處多多。

但是這樣也帶來的更多的工作量,這種情況下,我們自然而然的認為使用者端的環境不安全,所以要做許可權認證,登入認證。介面開發也要寫更多的程式碼。

總結

這篇文章主要介紹了我實現IP防火牆的思路。這些技術,需要開發者有豐富的網站開發經驗,這個要求不高,但是也要有基本的網路開發經驗,這就有一定的門檻。Workerman非常簡單,但是Workerman不是HTTP,這不是一般的網站開發,需要一定的學習和思路轉變。但是對於我來說,輕車駕熟。如果我去找其他的方案,學習、部署、測試,可能還不如我自己開發來更快。

版權宣告:本文由phpreturn.comPHP武器庫官網)原創和首發,所有權利歸phpreturnPHP武器庫)所有,本站允許任何形式的轉載/參照文章,但必須同時註明出處。

IP白名單是怎麼管理的呢,既可以通過面板新增,也可以存取面板的一個頁面,自動獲取出網IP新增到白名單中,使用體驗和很好。

實際上還有更好的方式,那就是做一個rss伺服器,自動獲取訂閱rss的客戶單的出網IP加入到白名單。但是我本身沒有使用rss的習慣,並且手機上也沒有好的rss閱讀器,也不想每次更新IP白名單都要特意開啟它,也就沒使用這個方案。

我把它開源了,如果有需要可以參考: https://gitee.com/augushong/ip-door 。

更多

這個系統,跟iptables相比,只是有一個更方便的IP白名單管理體驗而已,相當於一個簡單堡壘機。他可以實現,將一些埠隱藏起來,只有「我」能連線。

比如將ssh的埠隱藏起來,通過ip門禁轉發過去。再比如將80埠隱藏起來,通過ip門禁轉發過去。

目前我的系統還沒有實現多個埠的同時繫結轉發,但是核心的思路是一樣的,可以參考使用。

版權宣告:本文由phpreturn.comPHP武器庫官網)原創和首發,所有權利歸phpreturnPHP武器庫)所有,本站允許任何形式的轉載/參照文章,但必須同時註明出處。

以上就是IP門禁:保姆式教你用PHP實現一個IP防火牆的詳細內容,更多請關注TW511.COM其它相關文章!