Hướng dẫn thực hành · MikroTik
Trong trang này
MikroTik PPPoE Monitor - Telegram Notification
Cài nhanh bằng Terminal
Chuẩn bị trên RouterOS 7
RouterOS 7 có thể chặn fetch hoặc Scheduler bằng device-mode. Kiểm tra trước khi cài:
/system/device-mode/print
Nếu thấy fetch: no hoặc scheduler: no, chỉ bật hai tính năng cần thiết:
/system/device-mode/update fetch=yes scheduler=yes
Trong thời gian router hiển thị, thường là 5 phút, rút nguồn rồi cắm lại để xác nhận vật lý. Không dùng /system reboot thay cho bước này. Sau khi router khởi động xong, kiểm tra lại:
/system/device-mode/print
Chỉ tiếp tục khi kết quả có đủ:
fetch: yes
scheduler: yes
flagged: no
Nếu gặp failure: not allowed by device-mode, chưa chạy lại installer ngay; hãy bật fetch và scheduler theo các bước trên. Nếu flagged: yes, dừng cài đặt và kiểm tra an toàn của router trước khi gỡ trạng thái cảnh báo. Installer không thể tự bật các tính năng này vì chính fetch đang bị RouterOS chặn.
Tài liệu chính thức: MikroTik RouterOS Device-mode.
Chạy installer
Mặc định installer dùng pppoe-out1 và tự thêm pppoe-out2 nếu interface này tồn tại. Dán từng bước ASCII sau vào Terminal:
- Nhập Bot Token và Chat ID:
:global pppMonInstallBotToken "BOT_TOKEN"; :global pppMonInstallChatId "CHAT_ID"
- Nếu interface không dùng tên mặc định, khai báo riêng trước lệnh
fetch:
:global pppMonInstallPppoe1 "WAN1"; :global pppMonInstallPppoe2 "WAN2";
Nếu dùng đúng pppoe-out1 và pppoe-out2, bỏ qua bước này. Cuối cùng tải và chạy installer:
/tool fetch url="https://mikrotik.thachanhitt.vn/pppoe-monitor-install.rsc" dst-path="pppoe-monitor-install.rsc" check-certificate=yes; /import file-name="pppoe-monitor-install.rsc"; /file remove [find where name="pppoe-monitor-install.rsc"]
Installer tự tạo hoặc cập nhật route probe, pppoe-monitor-config, pppoe-monitor, pppoe-monitor-reset và Scheduler pppoe-monitor-every-3m. Bot Token và Chat ID chỉ được lưu trong script cấu hình trên router, không nằm trong URL công khai.
Sau khi cài, chạy thử và xem log:
/system script run pppoe-monitor
/log print where message~"PPPoE-MON"
Phải thấy INSTALL | SUCCESS và DONE. Việc kiểm tra thực tế trên RouterOS 6.49.x hoặc 7.x vẫn bắt buộc trước khi dùng chính thức.
Cảnh báo mạng mở rộng (tùy chọn)
Phần này đã tách riêng: Cài đặt cảnh báo mạng mở rộng. File đó có lệnh cài nhanh, lệnh bật/tắt từng nhóm và hướng dẫn tạo thủ công trong WinBox/WebFig.
Cài thủ công
-
Đặt tên nhà trong System → Identity. Nếu dùng CLI, chỉ đặt tên ASCII:
/system identity set name="NHA-ANH-THACH" -
Tạo route probe:
/ip route add dst-address=8.8.8.8/32 gateway=pppoe-out1 comment="PPPoE-monitor-isp1" add dst-address=9.9.9.9/32 gateway=pppoe-out2 comment="PPPoE-monitor-isp2"Nếu chỉ có một WAN, bỏ dòng route
pppoe-out2. -
Vào System → Scripts → +:
- Name:
pppoe-monitor-config - Policy:
read,policy,test - Source: dán khối dưới và sửa thông tin thật. Nếu chỉ có một WAN, giữ
pppMonPppoe2là chuỗi rỗng.
:global pppMonBotToken "BOT_TOKEN" :global pppMonChatId "CHAT_ID" :global pppMonFailLimit 3 :global pppMonPingCount 3 :global pppMonRecoveryHoldTime 5m :global pppMonDebug true :global pppMonPppoe1 "pppoe-out1" :global pppMonPppoe2 "" :global pppMonProbe1 "8.8.8.8" :global pppMonProbe2 "9.9.9.9" - Name:
-
Tạo Script thứ hai:
- Name:
pppoe-monitor - Policy:
read,write,policy,test - Source: dán khối bên dưới, không cần nhập lại Bot Token.
- Name:
Source được dán vào giao diện Scripts, không dán vào Terminal. Trong CLI chỉ dùng ASCII, không dùng tiếng Việt có dấu. Mặc định dùng
pppoe-out1vàpppoe-out2. Nếu router đặt tên khác, đổi cảgateway=ở route vàpppoeName=ở dòng$checkLinetương ứng.
SOURCE
/system script run pppoe-monitor-config
:global pppMonBotToken
:global pppMonChatId
:global pppMonFailLimit
:global pppMonPingCount
:global pppMonRecoveryHoldTime
:global pppMonDebug
:global pppMonPppoe1
:global pppMonPppoe2
:global pppMonProbe1
:global pppMonProbe2
# Trạng thái dùng chung, tự khởi tạo và giữ giữa các lần Scheduler chạy.
:global pppMonFailCount
:global pppMonLastAlert
:global pppMonAlertOpen
:global pppMonOutageActive
:global pppMonOutageStartedAt
:global pppMonOutageStartUptime
:global pppMonRecoveryHoldUntil
:if ([:typeof $pppMonFailCount] != "array") do={
:set pppMonFailCount {"_init"=0}
}
:if ([:typeof $pppMonLastAlert] != "array") do={
:set pppMonLastAlert {"_init"=""}
}
:if ([:typeof $pppMonAlertOpen] != "array") do={
:set pppMonAlertOpen {"_init"=false}
}
:if ([:typeof $pppMonOutageActive] != "array") do={
:set pppMonOutageActive {"_init"=false}
}
:if ([:typeof $pppMonOutageStartedAt] != "array") do={
:set pppMonOutageStartedAt {"_init"=""}
}
:if ([:typeof $pppMonOutageStartUptime] != "array") do={
:set pppMonOutageStartUptime {"_init"=0}
}
:if ([:typeof $pppMonRecoveryHoldUntil] != "array") do={
:set pppMonRecoveryHoldUntil {"_init"=0}
}
:if ($pppMonDebug = true) do={
:log info ("[PPPoE-MON] RUN | router=" . [/system identity get name])
}
# ================================================================
# HÀM DÙNG CHUNG - không cần nhân bản theo từng ISP
# ================================================================
:local checkLine do={
:global pppMonBotToken
:global pppMonChatId
:global pppMonFailLimit
:global pppMonPingCount
:global pppMonRecoveryHoldTime
:global pppMonDebug
:global pppMonFailCount
:global pppMonLastAlert
:global pppMonAlertOpen
:global pppMonOutageActive
:global pppMonOutageStartedAt
:global pppMonOutageStartUptime
:global pppMonRecoveryHoldUntil
# Đọc thông tin PPPoE, định dạng và gửi một thông báo Telegram.
:local notify do={
:global pppMonBotToken
:global pppMonChatId
:global pppMonDebug
:local pppoeId [/interface pppoe-client find where name=$pppoeName]
:if ([:len $pppoeId] = 0) do={
:log error ("[PPPoE-MON] ERROR | PPPoE not found | interface=" . $pppoeName)
:return false
}
:local routerName [/system identity get name]
:local pppoeUser [/interface pppoe-client get $pppoeId user]
:local wanPort [/interface pppoe-client get $pppoeId interface]
:local pppoeComment [/interface pppoe-client get $pppoeId comment]
:local currentIp "Không có"
:if ([:len $pppoeUser] = 0) do={ :set pppoeUser "Không khai báo" }
:if ([:len $pppoeComment] = 0) do={ :set pppoeComment "Không có" }
:do {
:local addressIds [/ip address find where interface=$pppoeName]
:if ([:len $addressIds] > 0) do={
:set currentIp [/ip address get [:pick $addressIds 0] address]
}
} on-error={ :set currentIp "Không có" }
:local tgHeader ""
:if ($eventName = "down") do={
:set tgHeader "\F0\9F\94\B4 MẤT KẾT NỐI INTERNET"
} else={
:if ($eventName = "recovered-late") do={
:set tgHeader "\F0\9F\9F\A2 INTERNET ĐÃ KHÔI PHỤC SAU SỰ CỐ"
} else={
:set tgHeader "\F0\9F\9F\A2 INTERNET ĐÃ KHÔI PHỤC"
}
}
# Dùng tiền tố tg* để tránh trùng tên thuộc tính có sẵn của RouterOS.
:local tgText ($tgHeader . "\n")
:set tgText ($tgText . "------------------------------\n")
:set tgText ($tgText . "\F0\9F\8F\A0 Nhà / Router: " . $routerName . "\n")
:set tgText ($tgText . "\F0\9F\8C\90 Đường truyền: " . $lineLabel . "\n")
:set tgText ($tgText . "\F0\9F\91\A4 Tài khoản PPPoE: " . $pppoeUser . "\n")
:set tgText ($tgText . "\F0\9F\94\8C Interface: " . $pppoeName . " qua " . $wanPort . "\n")
:set tgText ($tgText . "\F0\9F\93\9D Comment: " . $pppoeComment . "\n")
:set tgText ($tgText . "\F0\9F\8C\8D IP hiện tại: " . $currentIp . "\n")
:set tgText ($tgText . "\F0\9F\93\A1 Điểm kiểm tra: " . $probeAddress . "\n")
:if ($eventName != "down") do={
:set tgText ($tgText . "Bắt đầu mất: " . $outageStartedAt . "\n")
:set tgText ($tgText . "Thời gian gián đoạn: " . $outageDuration . "\n")
:if ($eventName = "recovered-late") do={
:set tgText ($tgText . \
"Ghi chú: Không thể gửi lúc mất mạng; đây là báo bù.\n")
}
}
:set tgText ($tgText . "\F0\9F\95\92 Thời gian: " . [/system clock get date] . " " . [/system clock get time] . "\n")
# Encode các ký tự đặc biệt của application/x-www-form-urlencoded.
# Ký tự UTF-8 được giữ nguyên; Telegram nhận nội dung ở dạng UTF-8.
:local formText ""
:for formIndex from=0 to=([:len $tgText] - 1) do={
:local formChar [:pick $tgText $formIndex ($formIndex + 1)]
:if ($formChar = "%") do={
:set formText ($formText . "%25")
} else={
:if ($formChar = "&") do={
:set formText ($formText . "%26")
} else={
:if ($formChar = "+") do={
:set formText ($formText . "%2B")
} else={
:if ($formChar = "=") do={
:set formText ($formText . "%3D")
} else={
:if ($formChar = " ") do={
:set formText ($formText . "%20")
} else={
:if ($formChar = "\n") do={
:set formText ($formText . "%0A")
} else={
:if ($formChar = "\r") do={
# Bỏ CR, chỉ giữ LF để xuống dòng.
:set formText $formText
} else={
:set formText ($formText . $formChar)
}
}
}
}
}
}
}
}
:local telegramUrl ("https://api.telegram.org/bot" . $pppMonBotToken . "/sendMessage")
:local requestBody ("chat_id=" . $pppMonChatId . "&text=" . $formText)
:local telegramSent false
:if ($pppMonDebug = true) do={
:log info ("[PPPoE-MON] TELEGRAM | event=" . $eventName . \
" | line=" . $lineLabel . " | status=SENDING")
}
:do {
/tool fetch url=$telegramUrl http-method=post \
http-header-field="Content-Type:application/x-www-form-urlencoded" \
http-data=$requestBody keep-result=no check-certificate=yes
:set telegramSent true
:log info ("[PPPoE-MON] TELEGRAM | event=" . $eventName . \
" | line=" . $lineLabel . " | status=SUCCESS")
} on-error={
:log error ("[PPPoE-MON] TELEGRAM | event=" . $eventName . \
" | line=" . $lineLabel . " | status=FAILED")
}
:return $telegramSent
}
:local pppoeId [/interface pppoe-client find where name=$pppoeName]
:if ([:len $pppoeId] = 0) do={
:log error ("[PPPoE-MON] ERROR | check skipped | interface=" . $pppoeName)
:return
}
:local failStateType [:typeof ($pppMonFailCount->$lineKey)]
:if (($failStateType = "nothing") || ($failStateType = "nil")) do={
:set ($pppMonFailCount->$lineKey) 0
}
:local alertDateStateType [:typeof ($pppMonLastAlert->$lineKey)]
:if (($alertDateStateType = "nothing") || ($alertDateStateType = "nil")) do={
:set ($pppMonLastAlert->$lineKey) ""
}
:local alertOpenStateType [:typeof ($pppMonAlertOpen->$lineKey)]
:if (($alertOpenStateType = "nothing") || ($alertOpenStateType = "nil")) do={
:set ($pppMonAlertOpen->$lineKey) false
}
:local outageStateType [:typeof ($pppMonOutageActive->$lineKey)]
:if (($outageStateType = "nothing") || ($outageStateType = "nil")) do={
:set ($pppMonOutageActive->$lineKey) ($pppMonAlertOpen->$lineKey)
}
:local startedAtType [:typeof ($pppMonOutageStartedAt->$lineKey)]
:if (($startedAtType = "nothing") || ($startedAtType = "nil")) do={
:set ($pppMonOutageStartedAt->$lineKey) "Không xác định"
}
:local startUptimeType [:typeof ($pppMonOutageStartUptime->$lineKey)]
:if (($startUptimeType = "nothing") || ($startUptimeType = "nil")) do={
:set ($pppMonOutageStartUptime->$lineKey) 0
}
:local holdUntilType [:typeof ($pppMonRecoveryHoldUntil->$lineKey)]
:if (($holdUntilType = "nothing") || ($holdUntilType = "nil")) do={
:set ($pppMonRecoveryHoldUntil->$lineKey) 0
}
:if ($pppMonDebug = true) do={
:log info ("[PPPoE-MON] CHECK | line=" . $lineLabel . \
" | interface=" . $pppoeName . " | probe=" . $probeAddress)
}
:local pingReplies 0
:do {
:set pingReplies [/ping address=$probeAddress interface=$pppoeName \
count=$pppMonPingCount interval=500ms]
} on-error={ :set pingReplies 0 }
:if ($pingReplies > 0) do={
:if ($pppMonDebug = true) do={
:log info ("[PPPoE-MON] RESULT | line=" . $lineLabel . \
" | status=UP | replies=" . $pingReplies . "/" . $pppMonPingCount . \
" | previous-fails=" . ($pppMonFailCount->$lineKey))
}
:if (($pppMonOutageActive->$lineKey) = true) do={
:local recoveryEvent "recovered-late"
:if (($pppMonAlertOpen->$lineKey) = true) do={
:set recoveryEvent "up"
}
:local outageDuration "Không xác định"
:local savedUptime ($pppMonOutageStartUptime->$lineKey)
:if ([:typeof $savedUptime] = "time") do={
:set outageDuration ([/system resource get uptime] - $savedUptime)
}
:local recoverySent [$notify eventName=$recoveryEvent \
lineLabel=$lineLabel pppoeName=$pppoeName \
probeAddress=$probeAddress \
outageStartedAt=($pppMonOutageStartedAt->$lineKey) \
outageDuration=$outageDuration]
:if ($recoverySent = true) do={
:set ($pppMonAlertOpen->$lineKey) false
:set ($pppMonOutageActive->$lineKey) false
:set ($pppMonOutageStartedAt->$lineKey) ""
:set ($pppMonOutageStartUptime->$lineKey) 0
:set ($pppMonRecoveryHoldUntil->$lineKey) \
([/system resource get uptime] + $pppMonRecoveryHoldTime)
}
} else={
:set ($pppMonOutageStartedAt->$lineKey) ""
:set ($pppMonOutageStartUptime->$lineKey) 0
}
:set ($pppMonFailCount->$lineKey) 0
} else={
:local failCount (($pppMonFailCount->$lineKey) + 1)
:if ($failCount > $pppMonFailLimit) do={ :set failCount $pppMonFailLimit }
:set ($pppMonFailCount->$lineKey) $failCount
:if (($failCount = 1) && \
(($pppMonOutageActive->$lineKey) = false)) do={
:set ($pppMonOutageStartedAt->$lineKey) \
([/system clock get date] . " " . [/system clock get time])
:set ($pppMonOutageStartUptime->$lineKey) \
[/system resource get uptime]
}
:log warning ("[PPPoE-MON] RESULT | line=" . $lineLabel . \
" | status=DOWN | replies=0/" . $pppMonPingCount . \
" | fail=" . $failCount . "/" . $pppMonFailLimit)
:local today [/system clock get date]
:if ($failCount >= $pppMonFailLimit) do={
:set ($pppMonOutageActive->$lineKey) true
:if ((($pppMonAlertOpen->$lineKey) = false) && \
(($pppMonLastAlert->$lineKey) != $today)) do={
:local alertSent [$notify eventName="down" lineLabel=$lineLabel \
pppoeName=$pppoeName probeAddress=$probeAddress]
:if ($alertSent = true) do={
:set ($pppMonLastAlert->$lineKey) $today
:set ($pppMonAlertOpen->$lineKey) true
}
} else={
:if ($pppMonDebug = true) do={
:log info ("[PPPoE-MON] ALERT-SKIP | line=" . $lineLabel . \
" | reason=already-sent-today")
}
}
}
}
:if ($pppMonDebug = true) do={
:log info ("[PPPoE-MON] END | line=" . $lineLabel)
}
}
# ================================================================
# KHAI BÁO ĐƯỜNG TRUYỀN
# lineKey phải duy nhất, viết liền và không đổi sau khi đưa vào sử dụng.
# lineLabel là tên đẹp sẽ xuất hiện trong thông báo.
# ================================================================
$checkLine lineKey="isp1" lineLabel="ISP 1 - đường chính" \
pppoeName=$pppMonPppoe1 probeAddress=$pppMonProbe1
:if ([:len $pppMonPppoe2] > 0) do={
$checkLine lineKey="isp2" lineLabel="ISP 2 - dự phòng" \
pppoeName=$pppMonPppoe2 probeAddress=$pppMonProbe2
}
:if ($pppMonDebug = true) do={
:log info "[PPPoE-MON] DONE | all configured lines checked"
}
Scheduler
Vào System → Scheduler → +:
- Name:
pppoe-monitor-every-3m - Interval:
00:03:00 - On Event:
/system script run pppoe-monitor - Policy:
read,write,policy,test
CLI ASCII tương đương:
/system scheduler add name="pppoe-monitor-every-3m" interval=3m on-event="/system script run pppoe-monitor" policy=read,write,policy,test comment="PPPoE-monitor"
Thêm WAN
Thêm một route probe và một $checkLine ở cuối Source. CLI chỉ dùng ASCII:
/ip route add dst-address=1.1.1.1/32 gateway=pppoe-out3 comment="PPPoE-monitor-isp3"
$checkLine lineKey="isp3" lineLabel="ISP-3" pppoeName="pppoe-out3" probeAddress="1.1.1.1"
Kiểm tra
- Chạy thử: System → Scripts → pppoe-monitor → Run Script.
- Xem log: Log, tìm
PPPoE-MON; phải cóDONEsau khi kiểm tra hết WAN. - Giảm log thường kỳ: đổi
:global pppMonDebug truethànhfalsetrong Source.
Reset để test lại
Tạo script thứ hai trong System → Scripts với Name: pppoe-monitor-reset. Dán Source sau, rồi nhấn Run Script trước khi test. Tạo bằng cùng tài khoản đang sở hữu pppoe-monitor.
:global pppMonFailCount
:global pppMonLastAlert
:global pppMonAlertOpen
:global pppMonOutageActive
:global pppMonOutageStartedAt
:global pppMonOutageStartUptime
:global pppMonRecoveryHoldUntil
:set pppMonFailCount {"_init"=0}
:set pppMonLastAlert {"_init"=""}
:set pppMonAlertOpen {"_init"=false}
:set pppMonOutageActive {"_init"=false}
:set pppMonOutageStartedAt {"_init"=""}
:set pppMonOutageStartUptime {"_init"=0}
:set pppMonRecoveryHoldUntil {"_init"=0}
:log warning "[PPPoE-MON] STATE | reset"
CLI ASCII để xem log:
/log print where message~"PPPoE-MON"
Không đưa Bot Token vào GitHub hoặc chat công khai.