【Fail2ban 進階實戰】化被動為主動!打造專屬 Telegram 遠端控制中心 (支援 Ban/Unban 指令)

透過我之前的文章(fail2ban與telegram連動),我介紹了如何讓 Fail2ban 在抓到壞人時,即時推播通知到我們的 Telegram 上。但身為一個伺服器管理員,只有「看」是不夠的。

試想一個情境:如果你在外面用手機網路連回伺服器,不小心密碼打錯太多次,自己被 Fail2ban 鎖在門外怎麼辦? 難道要等到回家用固定 IP 才能解鎖嗎?

這篇進階教學,將帶你用一小段 Python 腳本,把你的 Telegram 機器人升級成 「伺服器專屬控制中心」。你將可以直接在 Telegram 輸入 /unban 解鎖自己,或是輸入 /status 隨時查看各個監獄的狀況!


⚠️ 核心安全觀念:專屬識別

既然要讓 Telegram 可以對伺服器下達系統指令,安全性就是第一考量。 我們接下來寫的程式碼,會內建「身分驗證」機制。機器人只會認你的 Chat ID,如果路人甲亂連你的機器人下指令,系統會直接無視,確保伺服器的絕對安全。

你需要準備好:

  1. 你的 Telegram Bot Token (從 BotFather 取得)
  2. 你的專屬 Chat ID

步驟一:建立 Python 虛擬環境

為了不干擾 Ubuntu 系統預設的 Python 環境,我們為機器人建立一個獨立的家。請依序執行以下指令:

# 安裝 venv 虛擬環境套件
sudo apt update && sudo apt install python3-venv -y

# 建立機器人專屬資料夾並進入
sudo mkdir -p /opt/f2b-tg-bot
cd /opt/f2b-tg-bot

# 建立虛擬環境
sudo python3 -m venv venv

# 進入虛擬環境並安裝 Telegram 機器人專用套件
sudo ./venv/bin/pip install pyTelegramBotAPI

步驟二:撰寫核心控制腳本 (附避坑指南)

接下來我們要寫入機器人的靈魂。

建立並編輯 Python 檔案:

sudo nano /opt/f2b-tg-bot/bot.py

將以下程式碼貼上:

import telebot
import subprocess
import time

# ⚠️ 注意這裡!必須使用單引號或雙引號將字串包起來!
TOKEN = '你的_BOT_TOKEN'
ALLOWED_CHAT_ID = '你的_CHAT_ID' # 非常重要!防駭客關鍵

bot = telebot.TeleBot(TOKEN)

# 安全檢查:只允許你的 Chat ID 執行指令
def is_authorized(message):
    return str(message.chat.id) == str(ALLOWED_CHAT_ID)

@bot.message_handler(commands=['start', 'help'])
def send_welcome(message):
    if not is_authorized(message): return
    help_text = (
        "🛡️ **伺服器 Fail2ban 控制中心**\n\n"
        "可用指令:\n"
        "`/status` - 查看 Fail2ban 總覽\n"
        "`/jail <監獄名稱>` - 查看特定監獄狀態 (例如 /jail recidive)\n"
        "`/unban <IP>` - 從所有監獄中解鎖該 IP\n"
        "`/ban <監獄名稱> <IP>` - 手動將 IP 關入特定監獄"
    )
    bot.reply_to(message, help_text, parse_mode="Markdown")

@bot.message_handler(commands=['status'])
def check_status(message):
    if not is_authorized(message): return
    result = subprocess.getoutput("fail2ban-client status")
    bot.reply_to(message, f"```\n{result}\n```", parse_mode="Markdown")

@bot.message_handler(commands=['jail'])
def check_jail(message):
    if not is_authorized(message): return
    try:
        jail_name = message.text.split()[1]
        result = subprocess.getoutput(f"fail2ban-client status {jail_name}")
        bot.reply_to(message, f"```\n{result}\n```", parse_mode="Markdown")
    except IndexError:
        bot.reply_to(message, "請指定監獄名稱,例如:`/jail recidive`", parse_mode="Markdown")

@bot.message_handler(commands=['unban'])
def unban_ip(message):
    if not is_authorized(message): return
    try:
        ip = message.text.split()[1]
        result = subprocess.getoutput(f"fail2ban-client unban {ip}")
        bot.reply_to(message, f"✅ 解鎖結果:\n```\n{result}\n```", parse_mode="Markdown")
    except IndexError:
        bot.reply_to(message, "請提供 IP,例如:`/unban 192.168.1.1`", parse_mode="Markdown")

@bot.message_handler(commands=['ban'])
def ban_ip(message):
    if not is_authorized(message): return
    try:
        parts = message.text.split()
        jail_name = parts[1]
        ip = parts[2]
        result = subprocess.getoutput(f"fail2ban-client set {jail_name} banip {ip}")
        bot.reply_to(message, f"⛔ 封鎖結果:\n```\n{result}\n```", parse_mode="Markdown")
    except IndexError:
        bot.reply_to(message, "格式錯誤,請使用:`/ban 監獄名稱 IP`", parse_mode="Markdown")

# 讓機器人持續運作,遇到網路錯誤自動重試
while True:
    try:
        bot.polling(none_stop=True, interval=1, timeout=20)
    except Exception as e:
        time.sleep(5) 

🚨 常見新手錯誤 (踩坑經驗分享)

我自己一開始在填寫 TOKENALLOWED_CHAT_ID 時,是直接把數字貼上去,像這樣: TOKEN = 9876543210:AAG... 這會導致服務啟動失敗,並在日誌中噴出 SyntaxError: invalid syntax 錯誤! 原因: 在 Python 中,這些金鑰與 ID 屬於「字串」,必須使用單引號 '' 或雙引號 "" 將它們包起來。請務必再三檢查第 6、7 行的格式!


步驟三:設定為系統背景服務 (Systemd)

為了讓這支程式在伺服器重開機時能自動啟動,且在背景穩定運行,我們要把它註冊為系統服務。

建立服務設定檔:

sudo nano /etc/systemd/system/f2b-tg-bot.service

貼上以下內容:

[Unit]
Description=Fail2ban Telegram Control Bot
After=network.target fail2ban.service

[Service]
Type=simple
User=root
WorkingDirectory=/opt/f2b-tg-bot
ExecStart=/opt/f2b-tg-bot/venv/bin/python /opt/f2b-tg-bot/bot.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

步驟四:啟動機器人與成果驗收

最後,重新載入系統守護行程並啟動服務:

# 重新載入設定
sudo systemctl daemon-reload

# 啟動並設定開機自啟動
sudo systemctl enable --now f2b-tg-bot

# 檢查運作狀態
sudo systemctl status f2b-tg-bot

如果看到綠色的 active (running),恭喜你,設定大功告成!

現在你可以打開 Telegram,對著你的機器人輸入指令:

📱 拿起手機測試吧!

  • 傳送 /help 呼叫功能選單。
  • 傳送 /status 看看你的伺服器目前有幾個監獄正在運作。
  • 最棒的是,以後如果有朋友或自己的 IP 不小心被鎖了,只要優雅地拿出手機輸入 /unban 1.2.3.4,一秒鐘就能解決問題!

透過這個實作,我們不僅掌握了 Fail2ban 的防禦機制,更將伺服器的管理權限完美地延伸到了我們的手機上。這才是真正現代化、自動化的伺服器管理體驗!

【Fail2ban 進階實戰】效能與通知雙重升級:導入 ipset 與 Telegram 靜音防轟炸設定

在上一篇 [【Fail2ban 進階實戰】設定 recidive 累犯監獄,將惡意 IP 永久打入冷宮] 中,我們成功建立了一套能自動揪出死不悔改的攻擊者,並給予永久封鎖的防禦機制。

但隨著伺服器穩定運作,你可能會遇到兩個「甜蜜的煩惱」:

  1. 效能隱憂recidive 監獄累積了數十甚至數百個 IP,傳統的 iptables 一次寫入一條規則,當名單越來越長,防火牆比對封包的負擔也會隨之增加。
  2. 重啟轟炸:每次重開機或重啟 Fail2ban 服務時,系統會從資料庫把舊的黑名單重新載入,導致連動的 Telegram 機器人瞬間連發數十上百條「封鎖通知」,讓人不堪其擾。

這篇進階教學將帶你透過三個步驟,徹底解決這些痛點,讓你的伺服器防禦系統不僅強大,而且更輕量、更安靜!

步驟一:導入 ipset,將 O(n) 線性比對升級為 O(1) 哈希查詢

預設的 Fail2ban 會為每一個被封鎖的 IP 建立一條 iptables 規則。當封包進入伺服器時,系統必須由上而下逐一比對(這就是 O(n) 的線性搜尋)。當黑名單累積到上千筆時,會白白消耗 CPU 資源。

解決方案是改用 ipset。它會在核心建立一個哈希集合(Hash Table),不管集合裡有 10 個還是 10,000 個 IP,防火牆都只需要比對一次(O(1) 複雜度),效能極高。

1. 安裝 ipset

首先,確保你的 Ubuntu 伺服器已經安裝了該工具:

sudo apt update && sudo apt install ipset -y

2. 修改 recidive 監獄設定

開啟你的 /etc/fail2ban/jail.local,找到 [recidive] 區段,將 banaction 改為支援 ipset 的版本:

[recidive]
enabled = true
# 啟用 ipset 並封鎖所有通訊埠 (IPv4 & IPv6)
banaction = iptables-ipset-proto6-allports

# 直接呼叫 banaction 與自定義的 telegram 動作,並傳入監獄名稱參數
action = %(banaction)s
         telegram[name=recidive]

備註:這裡我們使用了 telegram[name=recidive],將監獄名稱當作參數傳遞給下一步的腳本,這對後續的客製化訊息非常重要。

步驟二:防重啟轟炸與動態訊息 (Telegram 腳本魔改)

接下來我們要解決「重開機就被 Telegram 洗版」的問題。

核心邏輯是:利用「時間差」來判斷。 當 Fail2ban 重啟時載入的舊 IP,其發生攻擊的時間(<time>)會是好幾個小時或幾天前;而真正新鮮的攻擊,時間差一定在幾秒鐘之內。

同時,我們也可以利用剛才傳入的 <name> 參數,讓 recidive(永久封鎖)和一般監獄(如 sshd)發出不同側重點的警報訊息。

修改 Telegram Action 設定

開啟你的 Telegram 動作設定檔(例如 /etc/fail2ban/action.d/telegram.conf),將 actionban 的區段修改如下:

[Definition]

actionban = now=$(date +%%s) \
t_int=$(echo "" | cut -d. -f1) \
diff=$(($now - $t_int)) \
if [ "$diff" -le 60 ]; then \
if [ "" = "recidive" ]; then \
MSG="[Fail2ban 永久封鎖]%%0A監獄:%%0A目標:%%0A結果:該 IP 已多次累犯,執行永久禁封"; \
else \
MSG="[Fail2ban 警報]%%0A監獄:%%0A目標:%%0A次數:第 次嘗試"; \
fi; \
curl -s -X POST "https://api.telegram.org/bot/sendMessage" \
-d chat_id= \
-d text="$MSG" -d parse_mode="HTML"; \
fi

(行尾的 \ 是續行符號,確保這些判斷式被視為同一道指令執行,後面絕對不能有空格。)

步驟三:關鍵程式碼解析 (為什麼要用 cut?)

在上面的腳本中,有一行非常關鍵的處理: t_int=$(echo "<time>" | cut -d. -f1)

為什麼需要這行? <time> 是 Fail2ban 傳遞給腳本的變數,代表攻擊發生的時間戳記(Timestamp)。但在某些 Fail2ban 版本中,這個時間戳會帶有小數點(包含毫秒),例如 1714521600.123

問題在於,Linux Shell 內建的數學運算 $(()) 不支援浮點數(小數)計算。如果不先處理,系統就會報錯導致通知發送失敗。

因此,我們引入了 cut 這個強大的文字處理工具:

  • echo "<time>":將帶有小數點的時間字串印出。
  • | (Pipe):將印出的字串丟給下一個指令處理。
  • cut -d.:告訴系統以「小數點 (.)」作為分隔符號(Delimiter)。
  • -f1:取得分隔後的第一個欄位(Field 1),也就是小數點前面的「整數」部分。

處理過後,純整數的 $t_int 就能順利與當前時間 $now 進行相減。只要時間差 ($diff) 小於等於 60 秒,系統才會判定這是「即時的新封鎖」並發送通知,完美過濾掉重啟時載入的舊資料!

驗證與總結

設定完成後,重新啟動 Fail2ban:

sudo systemctl restart fail2ban

你可以觀察到兩個明顯的改變:

  1. 你的 Telegram 靜悄悄的,再也不會因為重啟服務而遭受數十條訊息轟炸。
  2. 在終端機輸入 sudo ipset list,你會看到系統已經自動建立了一個名為 f2b-recidive 的集合,並且悄悄地將所有的惡意 IP 都關進了這個高效能的牢籠中。而在輸入 sudo iptables -L -n 時,原本落落長的規則也變得乾淨俐落。