├── .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 | ![hello](https://views.whatilearened.today/views/github/sirpdboy/deplives.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](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 | ![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg) 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 | ![screenshots](./me/watchdog0.png) 45 | 46 | ![screenshots](./me/1.png) 47 | 48 | ![screenshots](./me/2.png) 49 | 50 | 51 | --------------- 52 | ![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg) 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 | |![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | 79 | 80 | 81 | no 82 | 83 | 84 | ![hello](https://visitor-badge-deno.deno.dev/sirpdboy.sirpdboy.svg) [![](https://img.shields.io/badge/TGGroup-ClickJoin-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) 85 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | ![hello](https://views.whatilearened.today/views/github/sirpdboy/deplives.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](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 | ![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg) 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 | ![screenshots](./me/1.png) 90 | 91 | ![screenshots](./me/2.png) 92 | 93 | 94 | --------------- 95 | ![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg) 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 | ![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明3.jpg) 125 | 126 | | 图飞了 | 图飞了 | 127 | | :-----------------: | :-------------: | 128 | |![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | 129 | 130 | 131 | 图飞了 132 | 133 | 134 | ![](https://visitor-badge-deno.deno.dev/sirpdboy.sirpdboy.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](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