├── .github
└── workflows
│ └── Build.yml
├── LICENSE
├── README.md
├── README_CN.md
├── luci-app-watchdog
├── Makefile
├── htdocs
│ └── luci-static
│ │ └── resources
│ │ └── view
│ │ └── watchdog
│ │ ├── basic.js
│ │ └── log.js
├── po
│ ├── templates
│ │ └── watchdog.pot
│ └── zh_Hans
│ │ └── watchdog.po
└── root
│ └── usr
│ └── share
│ ├── luci
│ └── menu.d
│ │ └── luci-app-watchdog.json
│ ├── rpcd
│ └── acl.d
│ │ └── luci-app-watchdog.json
│ └── watchdog
│ └── api
│ ├── device_aliases.list
│ ├── ip_attribution.list
│ ├── ip_blacklist
│ ├── ipv4.list
│ └── ipv6.list
├── me
├── 1.png
├── 2.png
└── watchdog0.png
└── watchdog
├── Makefile
└── files
├── watchdog-call.libexec
├── watchdog.config
├── watchdog.init
└── watchdog.share
/.github/workflows/Build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | build:
10 | name: Build ${{ matrix.arch }}-${{ matrix.sdk }}
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | arch:
16 | - aarch64_cortex-a53
17 | - aarch64_cortex-a72
18 | - aarch64_generic
19 | - arm_arm1176jzf-s_vfp
20 | - arm_arm926ej-s
21 | - arm_cortex-a15_neon-vfpv4
22 | - arm_cortex-a5_vfpv4
23 | - arm_cortex-a7
24 | - arm_cortex-a7_neon-vfpv4
25 | - arm_cortex-a8_vfpv3
26 | - arm_cortex-a9
27 | - arm_cortex-a9_neon
28 | - arm_cortex-a9_vfpv3-d16
29 | - arm_fa526
30 | - arm_mpcore
31 | - arm_xscale
32 | - i386_pentium-mmx
33 | - i386_pentium4
34 | - mips64_octeonplus
35 | - mips_24kc
36 | - mips_4kec
37 | - mips_mips32
38 | - mipsel_24kc
39 | - mipsel_24kc_24kf
40 | - mipsel_74kc
41 | - mipsel_mips32
42 | - x86_64
43 |
44 | sdk:
45 | - openwrt-21.02
46 | - openwrt-22.03
47 | steps:
48 | - uses: actions/checkout@main
49 | with:
50 | fetch-depth: 0
51 | - name: Building packages
52 | uses: sbwml/openwrt-gh-action-sdk@go1.24
53 | env:
54 | ARCH: ${{ matrix.arch }}-${{ matrix.sdk }}
55 | FEEDNAME: packages_ci
56 | PACKAGES: luci-app-watchdog
57 | NO_REFRESH_CHECK: true
58 |
59 | - name: Upload artifacts
60 | uses: actions/upload-artifact@v4
61 | with:
62 | name: ${{ matrix.arch }}
63 | path: bin/packages/${{ matrix.arch }}/packages_ci/*.ipk
64 | - name: Create compress files
65 | run: |
66 | tar -zcvf ${{ matrix.sdk }}-${{ matrix.arch }}.tar.gz -C bin/packages/${{ matrix.arch }}/ packages_ci
67 |
68 |
69 | - name: Upload packages
70 | uses: ncipollo/release-action@v1
71 | with:
72 | name: ${{ github.ref_name }}
73 | token: ${{ secrets.GITHUB_TOKEN }}
74 | allowUpdates: true
75 | replacesArtifacts: true
76 | artifacts: "${{ matrix.sdk }}-${{ matrix.arch }}.tar.gz"
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 sirpdboy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |  [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
2 |
3 |
4 |
luci-app-watchdog
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | [中文](README_CN.md) | [English]
15 |
16 |
17 | 
18 |
19 | ### Plugin features: Solve the security login control problem of OPENWRT, adapt to OpenWR24.10, automatically adapt to Iptablet FW3 and NFT FW4. Can monitor WEB pages and SSH login status. Automatically blacklisted after multiple failures.
20 |
21 |
22 | ### Method for downloading source code:
23 |
24 | ```Brach
25 | # downloading
26 | git clone https://github.com/sirpdboy/luci-app-watchdog package/watchdog
27 | make menuconfig
28 |
29 | ```
30 | ### Configuration Menu
31 | ```Brach
32 | make menuconfig
33 | # find LuCI -> Applications, select luci-app-watchdog, save and exit
34 | ```
35 | ### compile
36 |
37 | ```Brach
38 | # compile
39 | make package/watchdog/luci-app-watchdog/compile V=s
40 | ```
41 |
42 | ## interface
43 |
44 | 
45 |
46 | 
47 |
48 | 
49 |
50 |
51 | ---------------
52 | 
53 |
54 |
55 | # My other project
56 |
57 | - Watch Dog : https://github.com/sirpdboy/luci-app-watchdog
58 | - Net Speedtest : https://github.com/sirpdboy/luci-app-netspeedtest
59 | - Task Plan : https://github.com/sirpdboy/luci-app-taskplan
60 | - Power Off Device : https://github.com/sirpdboy/luci-app-poweroffdevice
61 | - OpentoPD Theme : https://github.com/sirpdboy/luci-theme-opentopd
62 | - Ku Cat Theme : https://github.com/sirpdboy/luci-theme-kucat
63 | - Ku Cat Theme Config : https://github.com/sirpdboy/luci-app-kucat-config
64 | - NFT Time Control : https://github.com/sirpdboy/luci-app-timecontrol
65 | - Parent Control: https://github.com/sirpdboy/luci-theme-parentcontrol
66 | - Eqos Plus: https://github.com/sirpdboy/luci-app-eqosplus
67 | - Advanced : https://github.com/sirpdboy/luci-app-advanced
68 | - ddns-go : https://github.com/sirpdboy/luci-app-ddns-go
69 | - Advanced Plus): https://github.com/sirpdboy/luci-app-advancedplus
70 | - Net Wizard: https://github.com/sirpdboy/luci-app-netwizard
71 | - Part Exp: https://github.com/sirpdboy/luci-app-partexp
72 | - Lukcy: https://github.com/sirpdboy/luci-app-lukcy
73 |
74 | ## HELP
75 |
76 | |
|
|
77 | | :-----------------: | :-------------: |
78 | | |  |
79 |
80 |
81 |
82 |
83 |
84 |  [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
85 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 |  [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
2 |
3 |
4 |
luci-app-watchdog
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | [中文](README_CN.md) | [English]
15 |
16 |
17 | 
18 |
19 | ### 源码仓库:
20 |
21 | ## git clone https://github.com/sirpdboy/luci-app-watchdog
22 |
23 | 插件特色: 解决OPENWRT安全登陆管控问题,适配openwr24.10, 自动适配iptablet FW3和nft FW4. 可以监控WEB页和SSH登陆情况。 失败多次后自动拉黑到黑名单。
24 |
25 |
26 | ## 2025.7.11 看门狗 v1.0.5 解决OPENWRT安全问题。
27 |
28 | 更新日志:
29 |
30 | 1. 修复pidof显示问题。
31 |
32 | 2. 优化日志显示英文的问题。
33 |
34 | ## 2025.5.11 看门狗 v1.0.3 解决OPENWRT安全问题。
35 |
36 | 更新日志:
37 |
38 | 1. 解决上一版本服务启动不及时响应问题。
39 |
40 | 2. 增加加入黑名单提示,删除黑名单提示。
41 |
42 | 3. 优化日志显示英文的问题。
43 |
44 | ## 2025.5.1 看门狗 v1.0.1 解决OPENWRT安全问题。
45 |
46 | 更新日志:
47 |
48 | 1. 解决上一版本服务启动过多问题。
49 |
50 | 2. 插件超过一年使用权,开源给TG群的好伙伴们享用。好用请进TG群并点赞!!
51 |
52 |
53 | ## 2024.3.24 看门狗 1.0 解决OPENWRT安全管控问题。
54 |
55 | 更新日志:
56 |
57 | 1. 因好伙伴需要,定制插件看门狗 1.0.
58 |
59 | 2. 可以监控WEB页和SSH登陆情况。
60 |
61 | 3. 失败多次后自动拉黑到黑名单。
62 |
63 | ### 下载源码方法:
64 |
65 | ```Brach
66 |
67 | # 下载源码
68 |
69 | git clone https://github.com/sirpdboy/luci-app-watchdog package/watchdog
70 | make menuconfig
71 |
72 | ```
73 | ### 配置菜单
74 |
75 | ```Brach
76 | make menuconfig
77 | # 找到 LuCI -> Applications, 选择 luci-app-watchdog, 保存后退出。
78 | ```
79 |
80 | ### 编译
81 |
82 | ```Brach
83 | # 编译固件
84 | make package/watchdog/luci-app-watchdog/compile V=s
85 | ```
86 |
87 | ## 界面
88 |
89 | 
90 |
91 | 
92 |
93 |
94 | ---------------
95 | 
96 |
97 |
98 | ## 使用与授权相关说明
99 |
100 | - 本人开源的所有源码,任何引用需注明本处出处,如需修改二次发布必告之本人,未经许可不得做于任何商用用途。
101 |
102 |
103 | # My other project
104 |
105 | - 路由安全看门狗 :https://github.com/sirpdboy/luci-app-watchdog
106 | - 网络速度测试 :https://github.com/sirpdboy/luci-app-netspeedtest
107 | - 计划任务插件(原定时设置) : https://github.com/sirpdboy/luci-app-taskplan
108 | - 关机功能插件 : https://github.com/sirpdboy/luci-app-poweroffdevice
109 | - opentopd主题 : https://github.com/sirpdboy/luci-theme-opentopd
110 | - kucat酷猫主题: https://github.com/sirpdboy/luci-theme-kucat
111 | - kucat酷猫主题设置工具: https://github.com/sirpdboy/luci-app-kucat-config
112 | - NFT版上网时间控制插件: https://github.com/sirpdboy/luci-app-timecontrol
113 | - 家长控制: https://github.com/sirpdboy/luci-theme-parentcontrol
114 | - 定时限速: https://github.com/sirpdboy/luci-app-eqosplus
115 | - 系统高级设置 : https://github.com/sirpdboy/luci-app-advanced
116 | - ddns-go动态域名: https://github.com/sirpdboy/luci-app-ddns-go
117 | - 进阶设置(系统高级设置+主题设置kucat/agron/opentopd): https://github.com/sirpdboy/luci-app-advancedplus
118 | - 网络设置向导: https://github.com/sirpdboy/luci-app-netwizard
119 | - 一键分区扩容: https://github.com/sirpdboy/luci-app-partexp
120 | - lukcy大吉: https://github.com/sirpdboy/luci-app-lukcy
121 |
122 | ## 捐助
123 |
124 | 
125 |
126 | |
|
|
127 | | :-----------------: | :-------------: |
128 | | |  |
129 |
130 |
131 |
132 |
133 |
134 |  [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
135 |
136 |
--------------------------------------------------------------------------------
/luci-app-watchdog/Makefile:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
3 | # This is free software, licensed under the GNU General Public License v2.
4 | # See /LICENSE for more information.
5 |
6 | include $(TOPDIR)/rules.mk
7 |
8 | PKG_NAME:=luci-app-watchdog
9 | PKG_VERSION:=1.0.6
10 | PKG_RELEASE:=20250717
11 |
12 | PKG_MAINTAINER:=sirpdboy
13 | PKG_CONFIG_DEPENDS:=
14 |
15 | LUCI_TITLE:=LuCI support for watchdog
16 | LUCI_PKGARCH:=all
17 | LUCI_DEPENDS:=+watchdog
18 |
19 | include $(TOPDIR)/feeds/luci/luci.mk
20 |
21 | # call BuildPackage - OpenWrt buildroot signature
22 |
--------------------------------------------------------------------------------
/luci-app-watchdog/htdocs/luci-static/resources/view/watchdog/basic.js:
--------------------------------------------------------------------------------
1 | /* Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog */
2 |
3 | 'use strict';
4 | 'require view';
5 | 'require fs';
6 | 'require ui';
7 | 'require uci';
8 | 'require form';
9 | 'require poll';
10 |
11 | function checkProcess() {
12 | return fs.exec('/bin/pidof', ['watchdog']).then(function(res) {
13 | return {
14 | running: res.code === 0,
15 | pid: res.code === 0 ? res.stdout.trim() : null
16 | };
17 | }).catch(function() {
18 | return { running: false, pid: null };
19 | });
20 | }
21 |
22 | function renderStatus(isRunning) {
23 | var statusText = isRunning ? _('RUNNING') : _('NOT RUNNING');
24 | var color = isRunning ? 'green' : 'red';
25 | var icon = isRunning ? '✓' : '✗';
26 |
27 | return String.format(
28 | '%s %s %s',
29 | color, icon, _('watchdog'), statusText
30 | );
31 | }
32 | var cbiRichListValue = form.ListValue.extend({
33 | renderWidget: function (section_id, option_index, cfgvalue) {
34 | var choices = this.transformChoices();
35 | var widget = new ui.Dropdown((cfgvalue != null) ? cfgvalue : this.default, choices, {
36 | id: this.cbid(section_id),
37 | sort: this.keylist,
38 | optional: true,
39 | select_placeholder: this.select_placeholder || this.placeholder,
40 | custom_placeholder: this.custom_placeholder || this.placeholder,
41 | validate: L.bind(this.validate, this, section_id),
42 | disabled: (this.readonly != null) ? this.readonly : this.map.readonly
43 | });
44 |
45 | return widget.render();
46 | },
47 |
48 | value: function (value, title, description) {
49 | if (description) {
50 | form.ListValue.prototype.value.call(this, value, E([], [
51 | E('span', { 'class': 'hide-open' }, [title]),
52 | E('div', { 'class': 'hide-close', 'style': 'min-width:25vw' }, [
53 | E('strong', [title]),
54 | E('br'),
55 | E('span', { 'style': 'white-space:normal' }, description)
56 | ])
57 | ]));
58 | }
59 | else {
60 | form.ListValue.prototype.value.call(this, value, title);
61 | }
62 | }
63 | });
64 |
65 | return view.extend({
66 |
67 | render: function() {
68 |
69 | var m, s, o;
70 | m = new form.Map('watchdog', _('watchdog'), _('This is the security watchdog plugin for OpenWRT, which monitors and guards web login, SSH connections, and other situations.
If you encounter any issues while using it, please submit them here:') + '' + _('GitHub Project Address') + '');
71 | s = m.section(form.TypedSection);
72 | s.anonymous = true;
73 | s.render = function() {
74 | var statusView = E('p', { id: 'control_status' },
75 | ' ' + _('Checking status...'));
76 |
77 | poll.add(function() {
78 | return checkProcess()
79 | .then(function(res) {
80 | var status = renderStatus(res.running);
81 | if (res.running && res.pid) {
82 | status += ' (PID: ' + res.pid + ')';
83 | }
84 | statusView.innerHTML = status;
85 | })
86 | .catch(function(err) {
87 | statusView.innerHTML = '⚠ ' +
88 | _('Status check failed') + '';
89 | console.error('Status check error:', err);
90 | });
91 | });
92 |
93 | poll.start();
94 | return E('div', { class: 'cbi-section', id: 'status_bar' }, [ statusView ,
95 | E('div', { 'style': 'text-align: right; font-style: italic;' }, [
96 | E('span', {}, [
97 | _('© github '),
98 | E('a', {
99 | 'href': 'https://github.com/sirpdboy',
100 | 'target': '_blank',
101 | 'style': 'text-decoration: none;'
102 | }, 'by sirpdboy')
103 | ])
104 | ])
105 | ]);
106 | }
107 |
108 | s = m.section(form.NamedSection, 'config', 'watchdog', _(''));
109 | s.tab('basic', _('Basic Settings'));
110 | s.tab('blacklist', _('Black list'));
111 | s.addremove = false;
112 | s.anonymous = true;
113 |
114 | o = s.taboption('basic', form.Flag, 'enable', _('Enabled'));
115 | o = s.taboption('basic', form.Value, 'sleeptime', _('Check Interval (s)'));
116 | o.rmempty = false;
117 | o.placeholder = '60';
118 | o.datatype = 'and(uinteger,min(10))';
119 | o.description = _('Shorter intervals provide quicker response but consume more system resources.');
120 |
121 | o = s.taboption('basic', form.MultiValue, 'login_control', _('Login control'));
122 | o.value('web_logged', _('Web Login'));
123 | o.value('ssh_logged', _('SSH Login'));
124 | o.value('web_login_failed', _('Frequent Web Login Errors'));
125 | o.value('ssh_login_failed', _('Frequent SSH Login Errors'));
126 | o.modalonly = true;
127 |
128 | o = s.taboption('basic', form.Value, 'login_max_num', _('Login failure count'));
129 | o.default = '3';
130 | o.rmempty = false;
131 | o.datatype = 'and(uinteger,min(1))';
132 | o.depends({ login_control: "web_login_failed", '!contains': true });
133 | o.depends({ login_control: "ssh_login_failed", '!contains': true });
134 | o.description = _('Reminder and optional automatic IP ban after exceeding the number of times');
135 |
136 | o = s.taboption('blacklist', form.Flag, 'login_web_black', _('Auto-ban unauthorized login devices'));
137 | o.default = '0';
138 | o.depends({ login_control: "web_login_failed", '!contains': true });
139 | o.depends({ login_control: "ssh_login_failed", '!contains': true });
140 |
141 | o = s.taboption('blacklist', form.Value, 'login_ip_black_timeout', _('Blacklisting time (s)'));
142 | o.default = '86400';
143 | o.rmempty = false;
144 | o.datatype = 'and(uinteger,min(0))';
145 | o.depends('login_web_black', '1');
146 | o.description = _('\"0\" in ipset means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.');
147 |
148 | o = s.taboption('blacklist', form.TextValue, 'ip_black_list', _('IP blacklist'));
149 | o.rows = 8;
150 | o.wrap = 'soft';
151 | o.cfgvalue = function (section_id) {
152 | return fs.trimmed('/usr/share/watchdog/api/ip_blacklist');
153 | };
154 | o.write = function (section_id, formvalue) {
155 | return this.cfgvalue(section_id).then(function (value) {
156 | if (value == formvalue) {
157 | return
158 | }
159 | return fs.write('/usr/share/watchdog/api/ip_blacklist', formvalue.trim().replace(/\r\n/g, '\n') + '\n');
160 | });
161 | };
162 | o.depends('login_web_black', '1');
163 | o.description = _('Automatic ban blacklist list, with the ban time following the IP address');
164 |
165 | o = s.taboption('blacklist', form.Value, 'login_port_white', _('Port'));
166 | o.default = '';
167 | o.description = _('Open port after successful login
example:\"22\"、\"21:25\"、\"21:25,135:139\"');
168 | o.depends('port_release_enable', '1');
169 |
170 | o = s.taboption('blacklist', form.DynamicList, 'login_port_forward_list', _('Port Forwards'));
171 | o.default = '';
172 | o.description = _('Example: Forward port 13389 of this device (IPv4:10.0.0.1 / IPv6:fe80::10:0:0:2) to port 3389 of (IPv4:10.0.0.2 / IPv6:fe80::10:0:0:8)
\"10.0.0.1,13389,10.0.0.2,3389\"
\"fe80::10:0:0:1,13389,fe80::10:0:0:2,3389\"');
173 | o.depends('port_release_enable', '1');
174 |
175 | o = s.taboption('blacklist', form.Value, 'login_ip_white_timeout', _('Release time (s)'));
176 | o.default = '86400';
177 | o.datatype = 'and(uinteger,min(0))';
178 | o.description = _('\"0\" in ipset means permanent release, use with caution');
179 | o.depends('port_release_enable', '1');
180 |
181 |
182 | return m.render();
183 |
184 | }
185 | });
186 |
--------------------------------------------------------------------------------
/luci-app-watchdog/htdocs/luci-static/resources/view/watchdog/log.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 | 'require dom';
4 | 'require fs';
5 | 'require poll';
6 | 'require uci';
7 | 'require view';
8 | 'require form';
9 |
10 | return view.extend({
11 | render: function () {
12 | var css = `
13 | #log_textarea pre {
14 | padding: 10px; /* 内边距 */
15 | border-bottom: 1px solid #ddd; /* 边框颜色 */
16 | font-size: small;
17 | line-height: 1.3; /* 行高 */
18 | white-space: pre-wrap;
19 | word-wrap: break-word;
20 | overflow-y: auto;
21 | }
22 | .cbi-section small {
23 | margin-left: 1rem;
24 | font-size: small;
25 | color: #666; /* 深灰色文字 */
26 | }
27 | `;
28 |
29 | var log_textarea = E('div', { 'id': 'log_textarea' },
30 | E('img', {
31 | 'src': L.resource(['icons/loading.gif']),
32 | 'alt': _('Loading...'),
33 | 'style': 'vertical-align:middle'
34 | }, _('Collecting data ...'))
35 | );
36 |
37 | var log_path = '/tmp/watchdog/watchdog.log';
38 | var lastLogContent = '';
39 |
40 | var clear_log_button = E('div', {}, [
41 | E('button', {
42 | 'class': 'cbi-button cbi-button-remove',
43 | 'click': function (ev) {
44 | ev.preventDefault();
45 | var button = ev.target;
46 | button.disabled = true;
47 | button.textContent = _('Clear Logs...');
48 | fs.exec_direct('/usr/libexec/watchdog-call', ['clear_log'])
49 | .then(function () {
50 | button.textContent = _('Logs cleared successfully!');
51 | button.disabled = false;
52 | button.textContent = _('Clear Logs');
53 | // 立即刷新日志显示框
54 | var log = E('pre', { 'wrap': 'pre' }, [_('Log is clean.')]);
55 | dom.content(log_textarea, log);
56 | lastLogContent = '';
57 | })
58 | .catch(function () {
59 | button.textContent = _('Failed to clear log.');
60 | button.disabled = false;
61 | button.textContent = _('Clear Logs');
62 | });
63 | }
64 | }, _('Clear Logs'))
65 | ]);
66 |
67 | poll.add(L.bind(function () {
68 | return fs.read_direct(log_path, 'text')
69 | .then(function (res) {
70 | var newContent = res.trim() || _('Log is clean.');
71 |
72 | if (newContent !== lastLogContent) {
73 | var log = E('pre', { 'wrap': 'pre' }, [newContent]);
74 | dom.content(log_textarea, log);
75 | log.scrollTop = log.scrollHeight;
76 | lastLogContent = newContent;
77 | }
78 | }).catch(function (err) {
79 | var log;
80 | if (err.toString().includes('NotFoundError')) {
81 | log = E('pre', { 'wrap': 'pre' }, [_('Log file does not exist.')]);
82 | } else {
83 | log = E('pre', { 'wrap': 'pre' }, [_('Unknown error: %s').format(err)]);
84 | }
85 | dom.content(log_textarea, log);
86 | });
87 | }));
88 |
89 | return E('div', { 'class': 'cbi-map' }, [
90 | E('style', [css]),
91 | E('div', { 'class': 'cbi-section' }, [
92 | clear_log_button,
93 | log_textarea,
94 | E('small', {}, _('Refresh every 5 seconds.').format(L.env.pollinterval)),
95 | E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
96 | ]),
97 | E('div', { 'style': 'text-align: right; font-style: italic;' }, [
98 | E('span', {}, [
99 | _('© github '),
100 | E('a', {
101 | 'href': 'https://github.com/sirpdboy/luci-app-watchdog',
102 | 'target': '_blank',
103 | 'style': 'text-decoration: none;'
104 | }, 'by sirpdboy')
105 | ])
106 | ])
107 |
108 |
109 | ]);
110 | },
111 |
112 | handleSaveApply: null,
113 | handleSave: null,
114 | handleReset: null
115 | });
116 |
--------------------------------------------------------------------------------
/luci-app-watchdog/po/templates/watchdog.pot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sirpdboy/luci-app-watchdog/9fa20ba0303606f8787de53c23f3dd5e7d1c0516/luci-app-watchdog/po/templates/watchdog.pot
--------------------------------------------------------------------------------
/luci-app-watchdog/po/zh_Hans/watchdog.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "PO-Revision-Date: 2024-2025\n"
4 | "Last-Translator: https://github.com/sirpdboy/luci-app-watchdog \n"
5 | "Language-Team: \n"
6 | "Language: zh_Hans\n"
7 | "Content-Type: text/plain; charset=UTF-8\n"
8 | "Content-Transfer-Encoding: 8bit\n"
9 | "X-Generator: Weblate 4.18-dev\n"
10 |
11 | msgid "watchdog"
12 | msgstr "看门狗"
13 |
14 | msgid "Watch Dog"
15 | msgstr "看门狗"
16 |
17 | msgid "Basic Settings"
18 | msgstr "基本设置"
19 |
20 | msgid "Advance Setting"
21 | msgstr "高级设置"
22 |
23 | msgid "Checking status..."
24 | msgstr "检查状态..."
25 |
26 | msgid "Log"
27 | msgstr "日志"
28 |
29 | msgid "This is the security watchdog plugin for OpenWRT, which monitors and guards web login, SSH connections, and other situations.
If you encounter any issues while using it, please submit them here:"
30 | msgstr "这是openwrt的安全看门狗插件,监视和守护WEB登陆SSH连接等情况
如果你在使用中遇到问题,请到这里提交:"
31 |
32 | msgid "GitHub Project Address"
33 | msgstr "GitHub 项目地址"
34 |
35 | msgid "Login control"
36 | msgstr "管控内容"
37 |
38 | msgid "Black list"
39 | msgstr "黑名单"
40 |
41 | msgid "White list"
42 | msgstr "白名单"
43 |
44 | msgid "You may need to save the configuration before sending."
45 | msgstr "你可能需要先保存配置再进行发送"
46 |
47 | msgid "Unknown error: %s."
48 | msgstr "未知错误:%s"
49 |
50 | msgid "Check Interval (s)"
51 | msgstr "检测间隔时间(秒)"
52 |
53 | msgid "Shorter intervals provide quicker response but consume more system resources."
54 | msgstr "越短的间隔时间响应越快,但会占用更多的系统资源"
55 |
56 |
57 | msgid "Web Login"
58 | msgstr "Web 登录"
59 |
60 | msgid "SSH Login"
61 | msgstr "SSH 登录"
62 |
63 | msgid "Frequent Web Login Errors"
64 | msgstr "Web 频繁错误登录"
65 |
66 | msgid "Frequent SSH Login Errors"
67 | msgstr "SSH 频繁错误登录"
68 |
69 | msgid "Login failure count"
70 | msgstr "登录失败次数"
71 |
72 | msgid "Reminder and optional automatic IP ban after exceeding the number of times"
73 | msgstr "超过次数后记录并可选自动封禁IP"
74 |
75 |
76 |
77 | msgid "Please select device MAC"
78 | msgstr "请选择设备 MAC"
79 |
80 | msgid "Auto-ban unauthorized login devices"
81 | msgstr "自动封禁非法登录设备"
82 |
83 | msgid "Blacklisting time (s)"
84 | msgstr "拉黑时间(秒)"
85 |
86 | msgid "\"0\" in ipset means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI."
87 | msgstr "\"0\" 为永久拉黑,慎用。如不幸误操作,请更改设备 IP 进入 LUCI 界面清空规则。"
88 |
89 | msgid "Release port"
90 | msgstr "放行端口"
91 |
92 | msgid "Port"
93 | msgstr "端口"
94 |
95 | msgid "Open port after successful login
example:\"22\"、\"21:25\"、\"21:25,135:139\""
96 | msgstr "登录成功后开放端口
例:\"22\"、\"21:25\"、\"21:25,135:139\""
97 |
98 | msgid "If you have disabled LAN port inbound and forwarding in Firewall - Zone Settings, it won't work."
99 | msgstr "如在 防火墙 - 区域设置 中禁用了 LAN 口入站和转发,该功能将不起作用"
100 |
101 | msgid "Example: Forward port 13389 of this device (IPv4:10.0.0.1 / IPv6:fe80::10:0:0:2) to port 3389 of (IPv4:10.0.0.2 / IPv6:fe80::10:0:0:8)
\"10.0.0.1,13389,10.0.0.2,3389\"
\"fe80::10:0:0:1,13389,fe80::10:0:0:2,3389\""
102 | msgstr "例:将本机 (IPv4:10.0.0.1 / IPv6:fe80::10:0:0:2) 的 13389 端口转发到 (IPv4:10.0.0.2 / IPv6:fe80::10:0:0:8) 的 3389 端口:
\"10.0.0.1,13389,10.0.0.2,3389\"
\"fe80::10:0:0:2,13389,fe80::10:0:0:8,3389\""
103 |
104 | msgid "Release time (s)"
105 | msgstr "放行时间(秒)"
106 |
107 | msgid "\"0\" in ipset means permanent release, use with caution"
108 | msgstr "\"0\" 为永久放行,慎用"
109 |
110 | msgid "IP blacklist"
111 | msgstr "IP黑名单列表"
112 |
113 | msgid "Automatic ban blacklist list, with the ban time following the IP address"
114 | msgstr "自动封禁黑名单列表,IP后面是封禁时间"
115 |
116 |
117 | msgid "MAC Filtering Mode"
118 | msgstr "MAC 过滤模式"
119 |
120 | msgid "Ignore devices in the list"
121 | msgstr "忽略列表内设备"
122 |
123 | msgid "Ignored devices will not logged"
124 | msgstr "被忽略设备不做日志记录"
125 |
126 | msgid "Notify only devices in the list"
127 | msgstr "仅通知列表内设备"
128 |
129 | msgid "Notify only devices using this interface"
130 | msgstr "仅通知此接口设备"
131 |
132 | msgid "Multiple choice is not currently supported"
133 | msgstr "暂不支持多选"
134 |
135 | msgid "Ignored device list"
136 | msgstr "忽略设备列表"
137 |
138 | msgid "Followed device list"
139 | msgstr "关注设备列表"
140 |
141 | msgid "Notify only devices using this interface"
142 | msgstr "仅通知此接口设备"
143 |
144 |
145 | msgid "Login (Auto-Ban) Whitelist"
146 | msgstr "登录(自动封禁)白名单"
147 |
148 | msgid "Add the IP addresses in the list to the whitelist for the blocking function (if available), Only record in the log. Mask notation is currently not supported."
149 | msgstr "列表内 IP 加入封禁功能白名单(如果可用),仅在日志中记录,暂不支持掩码位表示"
150 |
151 | msgid "If you are not familiar with the meanings of these options, please do not modify them.
"
152 | msgstr "如果你不了解这些选项的含义,请不要修改这些选项
"
153 |
154 | msgid "Advanced Settings"
155 | msgstr "高级设置"
156 |
157 | msgid "Device online detection timeout (s)"
158 | msgstr "设备上线检测超时(秒)"
159 |
160 | msgid "Device offline detection timeout (s)"
161 | msgstr "设备离线检测超时(秒)"
162 |
163 | msgid "Offline detection count"
164 | msgstr "离线检测次数"
165 |
166 | msgid "If the device has good signal strength and no Wi-Fi sleep issues, you can reduce the above values.
Due to the mysterious nature of Wi-Fi sleep during the night, if you encounter frequent disconnections, please adjust the parameters accordingly.
..╮(╯_╰)╭.."
167 | msgstr "若设备信号强度良好,无息屏 WiFi 休眠问题,可以减少以上数值
因夜间 WiFi 息屏休眠较为玄学,遇到设备频繁推送断开,烦请自行调整参数
..╮(╯_╰)╭.."
168 |
169 |
170 | msgid "Devices"
171 | msgstr "设备"
172 |
173 | msgid "Disable active detection"
174 | msgstr "关闭主动探测"
175 |
176 | msgid "Maximum concurrent processes"
177 | msgstr "最大并发进程数"
178 |
179 | msgid "Do not change the setting value for low-performance devices, or reduce the parameters as appropriate."
180 | msgstr "低性能设备请勿更改设置值,或酌情减少参数"
181 |
182 |
183 | msgid "Online time"
184 | msgstr "在线时间"
185 |
186 | msgid "Clear Logs..."
187 | msgstr "清除日志..."
188 |
189 | msgid "Logs cleared successfully!"
190 | msgstr "日志清除成功!"
191 |
192 | msgid "Clear Logs"
193 | msgstr "清除日志"
194 |
195 | msgid "Refresh every 5 seconds."
196 | msgstr "每 5 秒刷新"
197 |
198 | msgid "Content 1"
199 | msgstr "内容1"
200 |
201 | msgid "Content 2"
202 | msgstr "内容2"
203 |
204 | msgid "Device 1"
205 | msgstr "设备1"
206 |
207 | msgid "Device 2"
208 | msgstr "设备2"
209 |
210 | msgid "Device 3"
211 | msgstr "设备3"
212 |
213 | msgid "Device 4"
214 | msgstr "设备4"
215 |
216 | msgid "Device %s logged into router via %s"
217 | msgstr "设备 %s 通过 %s 登录了路由器"
218 |
219 | msgid "/ (Homepage login)"
220 | msgstr "/ (首页登录)"
221 |
222 | msgid "%s frequent %s login attempts"
223 | msgstr "%s 频繁尝试 %s 登录"
224 |
225 | msgid "Block Information"
226 | msgstr "封禁信息"
227 |
228 | msgid "Device %s (%s) frequently attempted %s %s login"
229 | msgstr "设备 %s (%s) 频繁尝试 %s %s 登录"
230 |
231 | msgid "%s logged into router via %s"
232 | msgstr "%s 通过 %s 登录路由器"
233 |
234 | msgid "Login Information"
235 | msgstr "登录信息"
236 |
237 | msgid "Device %s (%s) logged into router via %s %s"
238 | msgstr "设备 %s (%s) 通过 %s %s 登录路由器"
239 |
240 | msgid "Time:"
241 | msgstr "时间:"
242 |
243 | msgid "Device IP:"
244 | msgstr "设备 IP:"
245 |
246 | msgid "Login Method:"
247 | msgstr "登录方式:"
248 |
249 | msgid "Initialization completed"
250 | msgstr "初始化完成"
251 |
252 | msgid "Start running"
253 | msgstr "开始运行"
254 |
255 | msgid "[Ban information]Cancel the ban IP:%s"
256 | msgstr "[封禁信息]取消封禁IP:%s "
257 |
258 | msgid "[Block Information]Add to blacklist IP: %s Attempts:%s Time:%s"
259 | msgstr "[封禁信息]添加黑名单IP:%s 尝试次数: %s 时间:%s"
260 |
261 | msgid "Failed to add to blacklist, invalid IP format: %s (removed from list)"
262 | msgstr "黑名单添加失败,IP %s 格式错误,已从列表中移除"
263 |
264 | msgid "Multiple interfaces detected or configuration error"
265 | msgstr "检测到多个接口或配置错误"
266 |
267 | msgid "Failed to read settings, please check configuration."
268 | msgstr "读取设置失败,请检查配置。"
269 |
270 | msgid "Log exceeded limit, keeping last 300 entries"
271 | msgstr "日志超出限制,保留最后300条记录"
272 |
273 | msgid "Whitelist add failed, IP format error"
274 | msgstr "白名单添加失败,IP格式错误"
275 |
276 | msgid "Failed to add to blacklist, invalid IP format: %s (removed from list)"
277 | msgstr "添加到黑名单失败,IP格式无效:%s(已从列表中删除)"
278 |
279 | msgid "[Ban information]Cancel the ban IP:%s"
280 | msgstr "[封禁信息]取消封禁IP:%s"
281 |
282 | msgid "[Block Information]Add to blacklist IP: %s Attempts:%s Time:%s"
283 | msgstr "[阻止信息]添加到黑名单IP:%s尝试次数:%s时间:%s"
284 |
285 | msgid ""
286 | msgstr ""
287 |
288 | msgid ""
289 | msgstr ""
290 |
291 |
--------------------------------------------------------------------------------
/luci-app-watchdog/root/usr/share/luci/menu.d/luci-app-watchdog.json:
--------------------------------------------------------------------------------
1 | {
2 | "admin/control/watchdog": {
3 | "title": "Watch Dog",
4 | "order": 10,
5 | "action": {
6 | "type": "view",
7 | "path": "watchdog/basic"
8 | },
9 | "depends": {
10 | "acl": [ "luci-app-watchdog" ],
11 | "uci": { "watchdog": true }
12 | }
13 | },
14 |
15 | "admin/control/watchdog/config": {
16 | "title": "Basic Settings",
17 | "order": 10,
18 | "action": {
19 | "type": "view",
20 | "path": "watchdog/basic"
21 | }
22 | },
23 | "admin/control/watchdog/log": {
24 | "title": "Log",
25 | "order": 40,
26 | "action": {
27 | "type": "view",
28 | "path": "watchdog/log"
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/luci-app-watchdog/root/usr/share/rpcd/acl.d/luci-app-watchdog.json:
--------------------------------------------------------------------------------
1 | {
2 | "luci-app-watchdog": {
3 | "description": "Grant UCI access for luci-app-watchdog",
4 | "read": {
5 | "file": {
6 | "/etc/init.d/watchdog": [ "exec" ],
7 | "/usr/share/watchdog/watchdog": [ "exec" ],
8 | "/tmp/watchdog/*": [ "read" ],
9 | "/usr/libexec/watchdog-call": [ "exec" ],
10 | "/bin/pidof": [ "exec" ]
11 | },
12 | "ubus": {
13 | "control": [ "list" ]
14 | },
15 | "uci": [ "watchdog" ]
16 | },
17 | "write": {
18 | "file": {
19 | "/tmp/watchdog/*": [ "write" ],
20 | "/usr/share/watchdog/api/*": [ "write" ]
21 | },
22 | "uci": [ "watchdog" ]
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/luci-app-watchdog/root/usr/share/watchdog/api/device_aliases.list:
--------------------------------------------------------------------------------
1 | # Examples :
2 | #XX:XX:XX:XX:XX:XX My Phone
3 | #192.168.1.2 My PC
4 |
--------------------------------------------------------------------------------
/luci-app-watchdog/root/usr/share/watchdog/api/ip_attribution.list:
--------------------------------------------------------------------------------
1 | https://ip.rss.ink/v1/qqwry?ip=${ip} | jq -r '.data.area'
2 | ip.plus/${ip} | sed -n 's/.*来自: //p'
3 | http://ip-api.com/json/${ip}?lang=zh-CN | jq -r '"\(.country) \(.regionName) \(.city)"'
--------------------------------------------------------------------------------
/luci-app-watchdog/root/usr/share/watchdog/api/ip_blacklist:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/luci-app-watchdog/root/usr/share/watchdog/api/ipv4.list:
--------------------------------------------------------------------------------
1 | ddns.oray.com/checkip
2 | www.net.cn/static/customercare/yourip.asp
3 | ip.3322.net
4 | ip.threep.top
5 | ip.atomo.cn
6 | ip.ddnspod.com
7 | 4.ipw.cn
8 | ipv4.ip.mir6.com
--------------------------------------------------------------------------------
/luci-app-watchdog/root/usr/share/watchdog/api/ipv6.list:
--------------------------------------------------------------------------------
1 | speed.neu6.edu.cn/getIP.php
2 | 6.ipw.cn
3 | ip.atomo.cn
4 | ip.ddnspod.com
5 | v6.ip.zxinc.org/getip
6 | ipv6.ip.mir6.com
--------------------------------------------------------------------------------
/me/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sirpdboy/luci-app-watchdog/9fa20ba0303606f8787de53c23f3dd5e7d1c0516/me/1.png
--------------------------------------------------------------------------------
/me/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sirpdboy/luci-app-watchdog/9fa20ba0303606f8787de53c23f3dd5e7d1c0516/me/2.png
--------------------------------------------------------------------------------
/me/watchdog0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sirpdboy/luci-app-watchdog/9fa20ba0303606f8787de53c23f3dd5e7d1c0516/me/watchdog0.png
--------------------------------------------------------------------------------
/watchdog/Makefile:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
3 | # This is free software, licensed under the GNU General Public License v2.
4 | # See /LICENSE for more information.
5 |
6 | include $(TOPDIR)/rules.mk
7 |
8 | PKG_NAME:=watchdog
9 | PKG_VERSION:=1
10 | PKG_RELEASE:=5
11 |
12 | include $(INCLUDE_DIR)/package.mk
13 |
14 | define Package/$(PKG_NAME)
15 | SECTION:=utils
16 | CATEGORY:=Utilities
17 | TITLE:=Routing Security Watchdog for OpenWrt
18 | DEPENDS:=+curl +bash
19 | endef
20 |
21 | define Package/$(PKG_NAME)/description
22 | Routing Security Watchdog for OpenWrt @sirpdboy
23 | endef
24 |
25 | define Build/Compile
26 | endef
27 |
28 | define Package/$(PKG_NAME)/conffiles
29 | /etc/config/watchdog
30 | endef
31 |
32 | define Package/$(PKG_NAME)/install
33 | $(INSTALL_DIR) $(1)/etc/config
34 | $(CP) ./files/watchdog.config $(1)/etc/config/watchdog
35 | $(INSTALL_DIR) $(1)/etc/init.d
36 | $(INSTALL_BIN) $(CURDIR)/files/watchdog.init $(1)/etc/init.d/watchdog
37 | $(INSTALL_DIR) $(1)/usr/libexec
38 | $(INSTALL_BIN) $(CURDIR)/files/watchdog-call.libexec $(1)/usr/libexec/watchdog-call
39 | $(INSTALL_DIR) $(1)/usr/share/watchdog
40 | $(INSTALL_BIN) $(CURDIR)/files/watchdog.share $(1)/usr/share/watchdog/watchdog
41 | endef
42 |
43 | $(eval $(call BuildPackage,$(PKG_NAME)))
44 |
--------------------------------------------------------------------------------
/watchdog/files/watchdog-call.libexec:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
4 | #
5 |
6 | logfile="/tmp/watchdog/watchdog.log"
7 | dir="/tmp/watchdog" && mkdir -p "${dir}"
8 | lang=$(uci get luci.main.lang 2>/dev/null)
9 | if [ -z "$lang" ] || [[ "$lang" == "auto" ]]; then
10 | lang=$(echo "${LANG:-${LANGUAGE:-${LC_ALL:-${LC_MESSAGES:-zh_cn}}}}" | awk -F'[ .@]' '{print tolower($1)}' | sed 's/-/_/' 2>/dev/null)
11 | fi
12 |
13 | translate() {
14 | # 处理特殊字符
15 | local lua_script=$(cat <"${logfile}"
27 |
28 | elif [ "$1" == "child" ]; then
29 | shift
30 | command_name=$1
31 | shift
32 | "$command_name" "$@"
33 | fi
34 |
--------------------------------------------------------------------------------
/watchdog/files/watchdog.config:
--------------------------------------------------------------------------------
1 |
2 | config watchdog 'config'
3 | option sleeptime '60'
4 | option debuglevel '1'
5 | option up_timeout '2'
6 | option down_timeout '10'
7 | option timeout_retry_count '2'
8 | option thread_num '3'
9 | list login_control 'web_logged'
10 | list login_control 'ssh_logged'
11 | list login_control 'web_login_failed'
12 | list login_control 'ssh_login_failed'
13 | option login_max_num '3'
14 | option enable '1'
15 | option login_web_black '1'
16 | option login_ip_black_timeout '86400'
17 |
18 |
--------------------------------------------------------------------------------
/watchdog/files/watchdog.init:
--------------------------------------------------------------------------------
1 | #!/bin/sh /etc/rc.common
2 | #
3 | # Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
4 | #
5 |
6 | START=99
7 | STOP=90
8 | USE_PROCD=1
9 | config=watchdog
10 | dir="/tmp/$config/"
11 |
12 | start_service() {
13 | procd_open_instance
14 | enable_value=$(uci get $config.config.enable 2>/dev/null || echo "0")
15 | [ "$enable_value" -ne "0" ] && procd_set_param command /usr/share/$config/$config && echo "$config is starting now ..."
16 | procd_close_instance
17 | }
18 |
19 | reload_service() {
20 | stop
21 | sleep 1
22 | start
23 | }
24 |
25 | clear_rule(){
26 |
27 | bin_nft=$(which nft 2>/dev/null)
28 | bin_iptables=$(which iptables 2>/dev/null)
29 | bin_ip6tables=$(which ip6tables 2>/dev/null)
30 | if [ -x "$bin_nft" ] && [ -x /sbin/fw4 ]; then
31 | nftables_ver="true"
32 | elif [ -x "$bin_iptables" ] || [ -x "$bin_ip6tables" ]; then
33 | iptables_ver="true"
34 | fi
35 |
36 | if [ -n "$nftables_ver" ]; then
37 | nft delete rule inet fw4 watchdog_input ip saddr @watchdog_blacklist 2>/dev/null
38 | nft delete rule inet fw4 watchdog_input ip6 saddr @watchdog_blacklistv6 2>/dev/null
39 | nft delete rule inet fw4 watchdog_input ether saddr @watchdog_blacklistbridge 2>/dev/null
40 | nft delete chain inet fw4 watchdog_input 2>/dev/null
41 | nft delete set inet fw4 watchdog_blacklist 2>/dev/null
42 | nft delete set inet fw4 watchdog_blacklistv6 2>/dev/null
43 | nft delete set inet fw4 watchdog_blacklistbridge 2>/dev/null
44 | elif [ -n "$iptables_ver" ]; then
45 | iptables -D INPUT -m set --match-set watchdog_blacklist src -j DROP 2>/dev/null
46 | iptables -D INPUT -m set --match-set watchdog_range src -j DROP 2>/dev/null
47 | ip6tables -D INPUT -m set --match-set watchdog_blacklistv6 src -j DROP 2>/dev/null
48 | ipset destroy watchdog_blacklist 2>/dev/null
49 | ipset destroy watchdog_blacklistv6 2>/dev/null
50 | ipset destroy watchdog_range 2>/dev/null
51 | fi
52 | }
53 | stop_service() {
54 | [ -f ${dir}child_pid ] && parent_pid=$(cat ${dir}child_pid)
55 | clear_rule
56 | [ -n "$parent_pid" ] && {
57 | child_pids=$(pgrep -P $parent_pid)
58 | echo "Terminating child processes of $config..."
59 | for child_pid in $child_pids; do
60 | kill $child_pid
61 | done
62 | }
63 | local pids=$(ps | grep "$config" | grep -v grep | grep -v $$ | awk '{print $1}')
64 | [ -n "$pids" ] && echo "$pids" | xargs kill 2>/dev/null
65 | echo "Terminating $config process..."
66 | }
67 |
68 | service_triggers() {
69 | procd_add_reload_trigger $config
70 | }
71 |
--------------------------------------------------------------------------------
/watchdog/files/watchdog.share:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-watchdog
4 | #
5 |
6 | APP=watchdog
7 |
8 | # 检测防火墙类型
9 | detect_firewall_type() {
10 | if command -v nft >/dev/null && [ -x /sbin/fw4 ]; then
11 | echo "nft"
12 | elif command -v iptables >/dev/null; then
13 | echo "iptables"
14 | else
15 | echo "unknown"
16 | fi
17 | }
18 |
19 | # 读取设置文件
20 | get_config() {
21 | while [[ "$*" != "" ]]; do
22 | [[ "$1" == "lang" ]] && {
23 | lang=$(uci get luci.main.lang 2>/dev/null)
24 | if [ -z "$lang" ] || [[ "$lang" == "auto" ]]; then
25 | lang=$(echo "${LANG:-${LANGUAGE:-${LC_ALL:-${LC_MESSAGES:-zh_cn}}}}" | awk -F'[ .@]' '{print tolower($1)}' | sed 's/-/_/' 2>/dev/null)
26 | fi
27 | } || {
28 | eval "${1}='$(uci get $APP.config.$1 2>/dev/null)'"
29 | }
30 | shift
31 | done
32 | }
33 |
34 |
35 |
36 | # 初始化设置信息
37 | read_config() {
38 | get_config \
39 | "enable" "sleeptime" "lang" \
40 | "login_control" "login_max_num" \
41 | "login_web_black" "login_ip_black_timeout" "port_release_enable" "login_port_white" "login_port_forward_list" "login_ip_white_timeout"
42 |
43 | (echo "$login_control" | grep -q "web_logged") && web_logged="true"
44 | (echo "$login_control" | grep -q "ssh_logged") && ssh_logged="true"
45 | (echo "$login_control" | grep -q "web_login_failed") && web_login_failed="true"
46 | (echo "$login_control" | grep -q "ssh_login_failed") && ssh_login_failed="true"
47 |
48 | (opkg list-installed | grep -w -q "^firewall4") && nftables_version="true"
49 | ip_blacklist_path="/usr/share/$APP/api/ip_blacklist"
50 | login_port_forward_list=$(echo "$login_port_forward_list" | sed 's/ /\n/g') 2>/dev/null
51 |
52 | ipv4_urllist=$(cat /usr/share/$APP/api/ipv4.list) 2>/dev/null
53 | ipv6_urllist=$(cat /usr/share/$APP/api/ipv6.list) 2>/dev/null
54 | [ -z "$sleeptime" ] && sleeptime="60"
55 | [ -z "$login_ip_black_timeout" ] && login_ip_black_timeout="86400"
56 | [ -z "$login_ip_white_timeout" ] && login_ip_white_timeout="600"
57 | [ "$iw_version" ] && wlan_interface=$(iw dev 2>/dev/null | grep Interface | awk '{print $2}') >/dev/null 2>&1
58 | [ -z "$server_port" ] && server_port="22"
59 |
60 | deltemp
61 | }
62 |
63 | # 初始化
64 | init() {
65 | # 检测程序开关
66 | enable_test
67 | [ -f "$logfile" ] && local logrow=$(grep -c "" "$logfile") || local logrow="0"
68 | [ "$logrow" -ne 0 ] && echo "----------------------------------" >>${logfile}
69 | log "[INFO] $(translate "Start running")"
70 | if [ -f "/usr/share/$APP/errlog" ]; then
71 | cat /usr/share/$APP/errlog >${logfile}
72 | log "[ERROR] $(translate "Loaded logs from previous restart")"
73 | fi
74 |
75 | # 文件清理
76 | rm -f "/usr/share/$APP/errlog" >/dev/null 2>&1
77 | LockFile unlock
78 |
79 | # 防火墙初始化
80 | [ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] && init_ip_black "ipv4"
81 | [ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] && init_ip_black "ipv6"
82 | [ -n "$port_release_enable" ] && [ "$port_release_enable" -eq "1" ] && init_ip_white "ipv4"
83 | [ -n "$port_release_enable" ] && [ "$port_release_enable" -eq "1" ] && init_ip_white "ipv6"
84 | set_ip_black
85 |
86 | return 0
87 | }
88 |
89 | # 主程序
90 | main() {
91 | # 限制并发进程
92 | dir="/tmp/$APP"
93 | logfile="${dir}/$APP.log"
94 | mkdir -p "$(dirname "$logfile")"
95 | get_config "thread_num"
96 | [ -z "$thread_num" ] || [ "$thread_num" -eq "0" ] && thread_num=5
97 | [ "$1" ] && [ $1 == "t1" ] && thread_num=1
98 | max_thread_num="$thread_num"
99 |
100 | FIFO_PATH="${dir}/fifo.$$"
101 | mkfifo "$FIFO_PATH"
102 | exec 5<>"$FIFO_PATH"
103 | rm "$FIFO_PATH" >/dev/null 2>&1
104 |
105 | for i in $(seq 1 "$max_thread_num"); do
106 | echo >&5
107 | done
108 | unset i
109 |
110 | # 定义锁文件
111 | lock_file="${dir}/$APP.lock"
112 | touch "$lock_file"
113 |
114 | # 设置信号处理
115 | trap cleanup SIGINT SIGTERM EXIT
116 | MAIN_PID=$$
117 | PROCESS_TAG="{watchdog}_${MAIN_PID}"
118 |
119 | # 初始化
120 | if [ "$1" ]; then
121 |
122 | silent_run read_config
123 | else
124 | silent_run read_config
125 | fi
126 |
127 | # 载入在线设备
128 | init
129 | [ $? -eq 1 ] && log "[ERROR] $(translate "Failed to read settings, please check configuration.")" && exit
130 | if [ -n "$web_logged" ] || [ -n "$ssh_logged" ] || [ -n "$web_login_failed" ] || [ -n "$ssh_login_failed" ]; then
131 | # 声明关联数组
132 | declare -A web_login_counts
133 | declare -A ssh_login_counts
134 | declare -A web_failed_counts
135 | declare -A ssh_failed_counts
136 | fi
137 |
138 | >"${dir}/send_enable.lock" && deltemp
139 | log "[INFO] $(translate "Initialization completed")"
140 | while [ "$enable" -eq "1" ]; do
141 | deltemp
142 |
143 |
144 | silent_run run_logins
145 | set_ip_black
146 | sleep $sleeptime
147 | done
148 | }
149 |
150 | # 隐藏输出
151 | # 不能直接包裹 var=$(echo $ssh_command) 等命令,待完善
152 | silent_run() {
153 | "$@" >/dev/null 2>&1
154 | }
155 |
156 |
157 | # 计算字符串显示宽度
158 | length_str() {
159 | [ -z "$1" ] && return
160 |
161 | local result
162 | # 调试模式不要输出信息
163 | {
164 | local str="$1"
165 | local length=0
166 |
167 | while IFS= read -r -n1 char; do
168 | local char_width
169 | char_width=$(printf "%s" "$char" | awk '{
170 | if (match($0, /[一-龥]/)) print 2;
171 | else print 1;
172 | }')
173 |
174 | length=$((length + char_width))
175 | done <<< "$str"
176 |
177 | result="$length"
178 | } > /dev/null 2>&1
179 |
180 | echo "$result"
181 | }
182 |
183 | # 字符串显示宽度处理
184 | cut_str() {
185 | [ -z "$1" ] && return
186 | [ -z "$2" ] && return
187 | local result
188 | # 调试模式不要输出信息
189 | {
190 | local str="$1"
191 | local max_width="$2"
192 | local current_width=0
193 |
194 | # 遍历字符串的每个字符
195 | for ((i = 0; i < ${#str}; i++)); do
196 | local char="${str:$i:1}"
197 | local char_width=$(length_str "$char")
198 |
199 | # 如果当前宽度加上当前字符的宽度超过最大宽度,则停止
200 | if [ $((current_width + char_width)) -gt "$max_width" ]; then
201 | break
202 | fi
203 |
204 | result="${result}${char}"
205 | current_width=$((current_width + char_width))
206 | done
207 |
208 | # 如果裁剪了字符串,则添加 ".."
209 | if [ "$current_width" -lt $(length_str "$str") ]; then
210 | result=$(echo "$result" | sed 's/ *$//')
211 | result="${result}.."
212 | fi
213 | } > /dev/null 2>&1
214 |
215 | echo "$result"
216 | }
217 |
218 | # 翻译
219 | translate() {
220 | local template="$1"
221 | shift # 移出第一个参数,剩余参数作为变量
222 |
223 | # 获取基础翻译
224 |
225 | local lua_script=$(cat <> "$logfile"
250 | }
251 |
252 |
253 |
254 | # 文件锁
255 | LockFile() {
256 | local fd=200
257 |
258 | if [ "$1" = "lock" ]; then
259 | eval "exec $fd>$lock_file"
260 | flock -n $fd
261 | if [ $? -ne 0 ]; then
262 | while ! flock -n $fd; do
263 | sleep 1
264 | done
265 | fi
266 | elif [ "$1" = "unlock" ]; then
267 | eval "exec $fd>&-"
268 | fi
269 | }
270 |
271 | # 检测退出信号
272 | cleanup() {
273 | local pids=$(ps | grep -E "\{watchdog\}_${MAIN_PID}|\{watchdog-call\}" | grep -v grep | awk '{print $1}')
274 | [ -n "$pids" ] && echo "$pids" | xargs kill 2>/dev/null
275 | LockFile unlock
276 | $EXIT_FLAG && exit 0
277 | }
278 |
279 | # 子进程调用
280 | run_with_tag() {
281 | [ -z "$1" ] && return
282 | local command_name=$1 # 第一个参数是命令名称
283 | shift # 移除第一个参数,剩下的参数传递给命令
284 | local command_path=$(readlink -f "$(which "$command_name")") # 检查命令路径
285 |
286 | # 如果是 BusyBox 的 applet,调用 watchdog-call
287 | if [[ "$command_path" == *"busybox"* ]]; then
288 | /usr/libexec/watchdog-call child "$command_name" "$@"
289 | else
290 | bash -c 'exec -a "$0" "$@"' "${PROCESS_TAG} ${command_name}" "$command_name" "$@"
291 | fi
292 | }
293 |
294 |
295 | # 清理临时文件
296 | deltemp() {
297 | rm -f "${dir}/send_enable.lock" >/dev/null 2>&1
298 | [ -f "$logfile" ] && local logrow=$(grep -c "" "$logfile") || local logrow="0"
299 | [ "$logrow" -gt 500 ] && tail -n 300 "$logfile" >"${logfile}.tmp" && mv "${logfile}.tmp" "$logfile" && log "[DEBUG] $(translate "Log exceeded limit, keeping last 300 entries")"
300 | }
301 |
302 | # ------------------------------------
303 | # 信息获取类
304 |
305 |
306 | # 查询 IP 归属地
307 | get_ip_attribution() {
308 | ip="$1"
309 | jq -e --arg ip "$ip" '.devices[] | select(.ip == $ip)' "$devices_json" >/dev/null 2>&1 && echo "本地局域网" && return
310 | local ip_attribution_urls=$(cat /usr/share/watchdog/api/ip_attribution.list)
311 | local sorted_attribution_urls=$(echo "$ip_attribution_urls" | awk 'BEGIN {srand()} {print rand() "\t" $0}' | sort -k1,1n | cut -f2-)
312 | local ip_attribution_url
313 | while IFS= read -r ip_attribution_url; do
314 | local login_ip_attribution=$(eval curl --connect-timeout 2 -m 2 -k -s "$ip_attribution_url" 2>/dev/null)
315 | [ -n "$login_ip_attribution" ] && echo "$login_ip_attribution" && break
316 | done <<<"$sorted_attribution_urls"
317 | }
318 |
319 |
320 | # 检测程序开关
321 | enable_test() {
322 | [ -z "$1" ] && local time_n=1
323 | for i in $(seq 1 $time_n); do
324 | get_config enable
325 | [ -z "$enable" ] || [ "$enable" -eq "0" ] && exit || sleep 1
326 | done
327 | unset i
328 | }
329 |
330 |
331 | # 自动封禁相关
332 | # 添加白名单
333 | add_ip_white() {
334 | [ -n "$port_release_enable" ] && [ "$port_release_enable" -eq "1" ] || return
335 | [ -z "$2" ] && timeout=$login_ip_white_timeout || timeout=$2
336 | # 检查 IP 版本
337 | unset ipset_name
338 | (echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && local ipset_name="$APP_whitelist"
339 | (echo "$1" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && local ipset_name="$APP_whitelistv6"
340 | [ -z "$ipset_name" ] && log "[ERROR] $(translate "Whitelist add failed, IP format error")" && return
341 |
342 | [ -n "$nftables_version" ] && {
343 | nft delete element inet fw4 $ipset_name { $1 } >/dev/null 2>&1
344 | nft add element inet fw4 $ipset_name { $1 expires ${timeout}s } #没找到刷新时间的命令,删除再添加
345 | } || {
346 | ipset -exist add $ipset_name $1 timeout $timeout
347 | }
348 | }
349 |
350 | # 初始化白名单
351 | init_ip_white() {
352 | [ -n "$port_release_enable" ] && [ "$port_release_enable" -eq "1" ] || return
353 | # 设置 IP 版本变量
354 | if [ $1 == "ipv4" ]; then
355 | ipset_name="$APP_whitelist"
356 | ip_version="ip"
357 | elif [ $1 == "ipv6" ]; then
358 | ipset_name="$APP_whitelistv6"
359 | ip_version="ip6"
360 | nat_table_cmd="family inet6"
361 | fi
362 |
363 | if [ -n "$nftables_version" ]; then
364 | ! nft list set inet fw4 $ipset_name >/dev/null 2>&1 && nft add set inet fw4 $ipset_name { type ${1}_addr\; flags timeout\; timeout ${login_ip_white_timeout}s\; }
365 | nft -- add chain inet fw4 $APP_dstnat { type nat hook prerouting priority -100 \; }
366 | nft add chain inet fw4 $APP_srcnat { type nat hook postrouting priority 100 \; }
367 | else
368 | ! ipset list $ipset_name >/dev/null 2>&1 && ipset create $ipset_name hash:ip timeout $login_ip_white_timeout $nat_table_cmd >/dev/null 2>&1
369 | fi
370 |
371 | # 端口放行
372 | if [ -n "$login_port_white" ]; then
373 | local login_port_white=$(echo "$login_port_white" | sed 's/ //g' | sed 's/,/, /g') 2>/dev/null
374 | if [ -n "$nftables_version" ]; then
375 | local count_accept_rules=$(nft list ruleset | grep -c "tcp dport.* ${login_port_white}.* $ip_version saddr @${ipset_name} counter packets .* accept comment \"\!watchdog Accept rule\"")
376 | if [ $count_accept_rules -eq 0 ]; then
377 | nft insert rule inet fw4 input tcp dport { $login_port_white } $ip_version saddr @$ipset_name counter accept comment \"\!watchdog Accept rule\" >/dev/null 2>&1
378 | elif [ $count_accept_rules -ne 1 ]; then
379 | local i=0
380 | local handles=$(nft --handle list ruleset | grep "\!watchdog Accept rule" | grep -v "tcp dport.* ${login_port_white}.* $ip_version saddr @${ipset_name} counter packets .* accept comment \"\!watchdog Accept rule\"" | awk '{print $NF}')
381 | for handle in $handles; do
382 | [ $i -eq 0 ] && i=1 && continue
383 | nft delete rule $handle
384 | done
385 | fi
386 | else
387 | ${ip_version}tables -C INPUT -m set --match-set $ipset_name src -p tcp -m multiport --dport $login_port_white -j ACCEPT >/dev/null 2>&1 || ${ip_version}tables -I INPUT -m set --match-set $ipset_name src -p tcp -m multiport --dport $login_port_white -j ACCEPT >/dev/null 2>&1
388 | fi
389 | fi
390 | unset handle
391 | # 端口转发
392 | while IFS= read -r port_forward; do
393 | port_forward=$(echo "$port_forward" | sed 's/,/ /g') 2>/dev/null
394 | [ $(echo $port_forward | awk -F" " '{print NF}') -ne "4" ] && continue
395 | local src_ip=$(echo ${port_forward} | awk '{print $1}')
396 | local src_port=$(echo ${port_forward} | awk '{print $2}')
397 | local dst_ip=$(echo ${port_forward} | awk '{print $3}')
398 | local dst_port=$(echo ${port_forward} | awk '{print $4}')
399 | if [ -n "$nftables_version" ]; then
400 | ! nft list ruleset | grep "$ip_version saddr @${ipset_name} tcp dport $src_port counter .* dnat $ip_version to $dst_ip:$dst_port comment \"\!watchdog DNAT rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 watchdog_dstnat meta nfproto $1 $ip_version saddr @${ipset_name} tcp dport $src_port counter dnat to "$dst_ip:$dst_port" comment \"\!watchdog DNAT rule\" >/dev/null 2>&1
401 | ! nft list ruleset | grep "$ip_version daddr $dst_ip tcp dport $dst_port counter .* snat $ip_version to $src_ip comment \"\!watchdog SNAT rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 watchdog_srcnat $ip_version daddr $dst_ip tcp dport $dst_port counter snat to $src_ip comment \"\!watchdog SNAT rule\" >/dev/null 2>&1
402 | else
403 | ${ip_version}tables -t nat -C PREROUTING -m set --match-set $ipset_name src -p tcp --dport $src_port -j DNAT --to-destination "$dst_ip:$dst_port" >/dev/null 2>&1 || ${ip_version}tables -t nat -I PREROUTING -m set --match-set $ipset_name src -p tcp --dport $src_port -j DNAT --to-destination "$dst_ip:$dst_port" >/dev/null 2>&1
404 | ${ip_version}tables -t nat -C POSTROUTING -m set --match-set $ipset_name src -p tcp -d $dst_ip --dport $dst_port -j SNAT --to-source $src_ip >/dev/null 2>&1 || ${ip_version}tables -t nat -I POSTROUTING -m set --match-set $ipset_name src -p tcp -d $dst_ip --dport $dst_port -j SNAT --to-source $src_ip >/dev/null 2>&1
405 | fi
406 | done <<<"$login_port_forward_list"
407 | unset port_forward
408 | }
409 |
410 | # 初始化黑名单规则
411 | init_ip_black() {
412 | [ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] || return
413 | # 设置 IP 版本变量
414 | if [ $1 == "ipv4" ]; then
415 | ipset_name="watchdog_blacklist"
416 | ip_version="ip"
417 | elif [ $1 == "ipv6" ]; then
418 | ipset_name="watchdog_blacklistv6"
419 | ip_version="ip6"
420 | nat_table_cmd="family inet6"
421 | fi
422 |
423 | [ -n "$nftables_version" ] && {
424 | ! nft list set inet fw4 ${ipset_name} >/dev/null 2>&1 && nft add set inet fw4 ${ipset_name} { type ${1}_addr\; flags timeout\; timeout ${login_ip_black_timeout}s\; }
425 | ! nft list ruleset | grep "$ip_version saddr @${ipset_name} counter .* comment \"\!watchdog Drop rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 input $ip_version saddr @${ipset_name} counter drop comment \"\!watchdog Drop rule\" >/dev/null 2>&1
426 | } || {
427 | ipset list $ipset_name >/dev/null 2>&1 || ipset create ${ipset_name} hash:ip timeout ${login_ip_black_timeout} ${nat_table_cmd} >/dev/null 2>&1
428 | ${ip_version}tables -C INPUT -m set --match-set ${ipset_name} src -j DROP >/dev/null 2>&1 || ${ip_version}tables -I INPUT -m set --match-set ${ipset_name} src -j DROP >/dev/null 2>&1
429 | }
430 | }
431 |
432 | # 添加黑名单
433 | add_ip_black() {
434 | local login_ip=$1
435 | [ -z "$login_ip" ] && return 0
436 | # 检查 IP 版本
437 | unset ipset_name
438 | (echo "$login_ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="watchdog_blacklist"
439 | (echo "$login_ip" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="watchdog_blacklistv6"
440 | [ -z "$ipset_name" ] && sed -i "/^$login_ip /d" "$ip_blacklist_path" && log "[WARN] $(translate "Failed to add to blacklist, invalid IP format: %s (removed from list)" "$login_ip")" && return 1
441 |
442 | ! cat "$ip_blacklist_path" | grep -q -w -i $login_ip && echo "$login_ip timeout $login_ip_black_timeout" >>"$ip_blacklist_path"
443 | [ -n "$nftables_version" ] && {
444 | nft list set inet fw4 ${ipset_name} | grep -qw "${login_ip}" && return 1 # IP 已存在
445 | nft add element inet fw4 $ipset_name { $login_ip expires ${login_ip_black_timeout}s } >/dev/null 2>&1
446 | } || {
447 | ipset -exist add $ipset_name $login_ip timeout ${login_ip_black_timeout} >/dev/null 2>&1
448 | }
449 | }
450 |
451 | # 移出黑名单
452 | del_ip_black() {
453 | [ -z "$1" ] && return
454 | sed -i "/^${1}/d" ${ip_blacklist_path}
455 |
456 | # 检查 IP 版本
457 | unset ipset_name
458 | (echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="watchdog_blacklist"
459 | (echo "$1" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="watchdog_blacklistv6"
460 | [ -z "$ipset_name" ] && return
461 |
462 | [ -n "$nftables_version" ] && {
463 | nft delete element inet fw4 ${ipset_name} { $1 } >/dev/null 2>&1
464 | } || {
465 | ipset list ${ipset_name} >/dev/null 2>&1 && ipset -! del ${ipset_name} ${1}
466 | }
467 | }
468 |
469 | # 设置防火墙列表
470 | set_ip_black() {
471 | # 检查换行,避免出错
472 | [ $(tail -n1 "${ip_blacklist_path}" | wc -l) -eq "0" ] && echo -e >>${ip_blacklist_path}
473 |
474 | # 从 ip_blacklist 文件逐行添加黑名单,add_ip_black() 处验证是否重复,此处不在验证
475 | for ip_black in $(cat ${ip_blacklist_path} | awk '{print $1}'); do
476 | add_ip_black "$ip_black"
477 | done
478 | # 当 ip_blacklist 文件清除 IP 时,从集合中清除 IP
479 | [ -n "$nftables_version" ] && fw_info_blacklist=$(nft list set inet fw4 watchdog_blacklist | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
480 | [ -n "$nftables_version" ] && fw_info_blacklistv6=$(nft list set inet fw4 watchdog_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
481 | [ -z "$nftables_version" ] && fw_info_blacklist=$(ipset list watchdog_blacklist | grep "timeout" 2>/dev/null)
482 | [ -z "$nftables_version" ] && fw_info_blacklistv6=$(ipset list watchdog_blacklistv6 | grep "timeout" 2>/dev/null)
483 |
484 | [ -n "$fw_info_blacklist" ] && [ -n "$fw_info_blacklistv6" ] && combined_fw_info_blacklist="${fw_info_blacklist}\n${fw_info_blacklistv6}"
485 | [ -z "$fw_info_blacklist" ] && combined_fw_info_blacklist="${fw_info_blacklistv6}" || combined_fw_info_blacklist="${fw_info_blacklist}"
486 |
487 | while IFS= read -r ip_black_info; do
488 | ip_black=$(echo "$ip_black_info" | grep -Eo "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}")
489 | [ -z "$ip_black" ] && ip_black=$(echo "$ip_black_info" | grep -Eo "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}")
490 | [ -z "$ip_black" ] && continue
491 | cat ${ip_blacklist_path} | grep -q -w -i ${ip_black} && sed -i "/^${ip_black}/d" ${ip_blacklist_path} && echo ${ip_black_info} >>${ip_blacklist_path} || {
492 | del_ip_black ${ip_black}
493 | log "$(translate "[Ban information]Cancel the ban IP:%s" "$ip_black")"
494 | }
495 | done <<<"$combined_fw_info_blacklist"
496 | }
497 |
498 | # 监听登录事件
499 | run_logins() {
500 | if [ -n "$web_logged" ] || [ -n "$ssh_logged" ] || [ -n "$web_login_failed" ] || [ -n "$ssh_login_failed" ]; then
501 | # 监听系统日志,-f 表示跟随实时日志,-p 表示日志级别为 notice
502 | run_with_tag logread -f -p notice | while IFS= read -r line; do
503 | [ -n "$web_logged" ] && {
504 | web_login_ip=$(echo "$line" | grep -i "accepted login" | awk '{print $NF}')
505 | [ -n "$web_login_ip" ] && process_login "$web_login_ip" $(echo "$line" | awk '{print $4}') web_login_counts
506 | }
507 |
508 | [ -n "$ssh_logged" ] && {
509 | ssh_login_ip=$(echo "$line" | grep -i "Password auth succeeded\|Pubkey auth succeeded" | awk '{print $NF}' | sed -nr 's#^(.*):.[0-9]{1,5}#\1#gp' | sed -e 's/%.*//')
510 | [ -n "$ssh_login_ip" ] && process_login "$ssh_login_ip" $(echo "$line" | awk '{print $4}') ssh_login_counts
511 | }
512 |
513 | [ -n "$web_login_failed" ] && {
514 | web_failed_ip=$(echo "$line" | grep -i "failed login" | awk '{print $NF}')
515 | [ -n "$web_failed_ip" ] && process_login "$web_failed_ip" $(echo "$line" | awk '{print $4}') web_failed_counts
516 | }
517 |
518 | [ -n "$ssh_login_failed" ] && {
519 | # 匹配特定的 SSH 登录失败情况并提取 IP 地址和时间
520 | ssh_failed_ip=$(echo "$line" | grep -iE "Bad password attempt|Login attempt for nonexistent user|Max auth tries reached" | awk '{print $NF}' | sed -nr 's#^(.*):[0-9]{1,5}#\1#gp' | sed -e 's/%.*//')
521 |
522 | # 如果未能提取到 IP,从日志标识符提取失败用户的 ID,并再次提取 IP
523 | if [ -z "$ssh_failed_ip" ]; then
524 | ssh_failed_num=$(echo "$line" | sed -n 's/.*authpriv\.warn dropbear\[\([0-9]\+\)\]: Login attempt for nonexistent user/\1/p')
525 | [ -n "$ssh_failed_num" ] && ssh_failed_ip=$(logread notice | grep "authpriv\.info dropbear\[${ssh_failed_num}\].*Child connection from" | awk '{print $NF}' | sed -nr 's#^(.*):[0-9]{1,5}#\1#gp' | sed -e 's/%.*//' | tail -n 1)
526 | fi
527 |
528 | # 如果成功提取到 IP 地址,调用 process_login 处理
529 | [ -n "$ssh_failed_ip" ] && process_login "$ssh_failed_ip" $(echo "$line" | awk '{print $4}') ssh_failed_counts
530 | }
531 |
532 | done
533 | sleep 1
534 | fi
535 | }
536 |
537 |
538 | # 处理登录事件
539 | # 参数:
540 | # $1: IP
541 | # $2: 日志时间 - 从日志中读取而不是使用当前时间,避免秒对应不上
542 | # $3: 数组名 - 记录 IP 和登录次数的关联数组名
543 | process_login() {
544 | local login_ip=$1
545 | local login_time=$2
546 | local -n login_counts=$3
547 |
548 | # 如果数组中不存在此 IP,初始化为 0
549 | if [ -z "${login_counts["$login_ip"]}" ]; then
550 | login_counts["$login_ip"]=0
551 | fi
552 | # +1
553 | login_counts["$login_ip"]=$((login_counts["$login_ip"] + 1))
554 | local count=${login_counts["$login_ip"]}
555 |
556 | # 封禁
557 | if [[ ("$3" == "web_failed_counts" || "$3" == "ssh_failed_counts") ]]; then
558 | if [[ $count -ge $login_max_num ]] ;then
559 | add_ip_black ${login_ip} && {
560 | unset login_counts["$login_ip"]
561 | login_send "$login_ip" "$login_time" "$3"
562 | log "$(translate "[Block Information]Add to blacklist IP: %s Attempts:%s Time:%s" "$login_ip" "$count" "$login_time" )"
563 | }
564 | else
565 | login_send "$login_ip" "$login_time" "$3"
566 | fi
567 |
568 | fi
569 |
570 | # 正常登录
571 | if [[ "$3" == "web_login_counts" || "$3" == "ssh_login_counts" ]]; then
572 | add_ip_white ${login_ip}
573 | del_ip_black ${login_ip} # 白名单已经优先于黑名单,但白名单集合有超时限制,防止下次修改代码忘记,上保险
574 | unset web_failed_counts["$login_ip"]
575 | unset ssh_failed_counts["$login_ip"]
576 | unset login_counts["$login_ip"]
577 | login_send "$login_ip" "$login_time" "$3"
578 | fi
579 | [ "${#login_counts[@]}" -gt "100" ] && login_counts=("${login_counts[@]: -100}")
580 | }
581 |
582 | # 登录提醒
583 | login_send() {
584 | local login_ip=$1
585 | local login_time=$2
586 | local log_type=$3
587 |
588 | local login_title
589 | local login_content
590 |
591 | >"${dir}/send_enable.lock"
592 |
593 | [ -z "$login_ip" ] && return
594 |
595 | [[ "$log_type" == "web"* ]] && local log_type_short="Web" || local log_type_short="SSH"
596 | [ -f "$logfile" ] && login_log=$(grep -w "$login_ip" "$logfile" | grep -v "\[info\]" | tail -n 1)
597 | [ -n "$login_log" ] && log_timestamp=$(date -d "$(echo "$login_log" | awk '{print $1, $2}')" +%s) || log_timestamp=0
598 |
599 | # 查询 IP 归属地
600 | local login_ip_attribution=$(get_ip_attribution "${login_ip}")
601 | # 登录方式
602 | if [[ "$log_type" == "web"* ]]; then
603 | # Web 登录、非法登录
604 | local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{print $13}' | tail -n 1)
605 | [ "$login_mode" = "/" ] && login_mode="$(translate "/ (Homepage login)")"
606 | elif [[ "$log_type" == "ssh_login"* ]]; then
607 | # SSH 登录
608 | local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{print $8}' | tail -n 1)
609 | else
610 | local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{for(i=8;i