mm_ip 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,8 @@
13
13
  - ✅ **IP请求频率监控** - 实时监控IP请求频率,防止高频访问
14
14
  - ✅ **自动拉黑机制** - 支持高频访问自动拉黑和违规次数超限自动拉黑
15
15
  - ✅ **违规记录系统** - 记录IP违规行为,支持违规次数统计和重置
16
+ - ✅ **IP地区检测** - 内置ip2region离线IP库,检测到国外IP自动加入黑名单
17
+ - ✅ **操作系统级拉黑** - 支持直接调用系统防火墙拉黑/释放IP(Windows netsh / Linux iptables)
16
18
  - ✅ **持久化存储** - 支持数据分文件存储(白名单、黑名单、违规记录独立存储)
17
19
  - ✅ **自动保存机制** - 支持定时自动保存和操作触发保存
18
20
  - ✅ **重启数据恢复** - 支持重启后自动加载持久化数据
@@ -89,10 +91,16 @@ new Ip(config)
89
91
  | `auto_black_enable` | boolean | true | 启用自动高频拉黑 |
90
92
  | `auto_black_threshold` | number | 50 | 自动拉黑阈值 |
91
93
  | `auto_black_duration` | number | 3600000 | 自动拉黑持续时间(毫秒) |
94
+ | `auto_black_grace_count` | number | 3 | 自动拉黑宽限次数,连续触限达到该次数后才拉黑 |
92
95
  | `violation_enable` | boolean | true | 启用违规记录 |
93
96
  | `violation_max_count` | number | 10 | 最大违规次数 |
94
97
  | `violation_reset_time` | number | 86400000 | 违规记录重置时间(毫秒) |
95
98
  | `violation_auto_black` | boolean | true | 违规次数超限自动拉黑 |
99
+ | `region_check_enable` | boolean | true | 启用国外IP检测 |
100
+ | `region_checker` | function | 自动加载ip2region | IP地区检测函数 (ip) => 地区信息 |
101
+ | `region_cache_time` | number | 86400000 | 地区检测结果缓存时间(毫秒) |
102
+ | `os_black_enable` | boolean | false | 启用系统级拉黑联动(需管理员权限,默认关闭避免误拦截) |
103
+ | `os_rule_prefix` | string | 'mm_ip_black_' | 系统防火墙规则名前缀 |
96
104
  | `dir` | string | './cache/ip' | 持久化存储目录 |
97
105
 
98
106
  ### 主要方法
@@ -124,6 +132,20 @@ new Ip(config)
124
132
  - `clearViolations()` - 清空所有违规记录
125
133
  - `getAllViolations()` - 获取所有违规记录
126
134
 
135
+ #### IP地区检测
136
+ - `setRegionChecker(fn)` - 设置自定义地区检测函数
137
+ - `getRegion(ip)` - 获取IP地区信息(带缓存)
138
+ - `isForeign(ip)` - 判断IP是否为国外IP
139
+ - `checkRegion(ip)` - 检测IP地区,国外IP自动加入黑名单
140
+ - `clearRegionCache()` - 清空地区检测结果缓存
141
+
142
+ #### 操作系统级拉黑
143
+ - `osBlack(ip)` - 在操作系统层拉黑IP(入站拦截,需管理员权限)
144
+ - `osUnblack(ip)` - 在操作系统层释放IP
145
+ - `isOsBlack(ip)` - 检查IP是否已在操作系统层拉黑
146
+
147
+ > 启用 `os_black_enable` 后,`addBlack()` 会自动同步系统防火墙拉黑,`delBlack()`/`del()` 会自动释放。运行环境需具备管理员权限,否则系统命令执行失败仅记录日志,不影响应用层黑名单功能。
148
+
127
149
  #### 持久化存储
128
150
  - `save()` - 手动保存数据
129
151
  - `load()` - 手动加载数据
package/README_EN.md CHANGED
@@ -13,6 +13,8 @@ A powerful IP management module that supports whitelist, blacklist management, I
13
13
  - ✅ **IP Request Frequency Monitoring** - Real-time IP request frequency monitoring to prevent high-frequency access
14
14
  - ✅ **Auto-Blacklist Mechanism** - Automatic blacklisting for high-frequency access and violation limit exceeding
15
15
  - ✅ **Violation Record System** - Record IP violation behaviors with violation count statistics and reset
16
+ - ✅ **IP Region Detection** - Built-in ip2region offline IP database, auto-blacklist foreign IPs
17
+ - ✅ **OS-Level Blacklisting** - Directly blacklist/release IPs via system firewall (Windows netsh / Linux iptables)
16
18
  - ✅ **Persistent Storage** - Support for separate file storage (whitelist, blacklist, violation records stored independently)
17
19
  - ✅ **Auto-Save Mechanism** - Support for timed auto-save and operation-triggered save
18
20
  - ✅ **Data Recovery on Restart** - Automatic loading of persistent data after restart
@@ -89,10 +91,16 @@ new Ip(config)
89
91
  | `auto_black_enable` | boolean | true | Enable auto-blacklist for high frequency |
90
92
  | `auto_black_threshold` | number | 50 | Auto-blacklist threshold |
91
93
  | `auto_black_duration` | number | 3600000 | Auto-blacklist duration (milliseconds) |
94
+ | `auto_black_grace_count` | number | 3 | Grace count before auto-blacklist |
92
95
  | `violation_enable` | boolean | true | Enable violation records |
93
96
  | `violation_max_count` | number | 10 | Maximum violation count |
94
97
  | `violation_reset_time` | number | 86400000 | Violation record reset time (milliseconds) |
95
98
  | `violation_auto_black` | boolean | true | Auto-blacklist when violation count exceeds limit |
99
+ | `region_check_enable` | boolean | true | Enable foreign IP detection |
100
+ | `region_checker` | function | auto load ip2region | IP region check function (ip) => region info |
101
+ | `region_cache_time` | number | 86400000 | Region check result cache time (ms) |
102
+ | `os_black_enable` | boolean | false | Enable OS-level blacklist sync (requires admin, off by default) |
103
+ | `os_rule_prefix` | string | 'mm_ip_black_' | System firewall rule name prefix |
96
104
  | `dir` | string | './cache/ip' | Persistent storage directory |
97
105
 
98
106
  ### Main Methods
@@ -124,6 +132,20 @@ new Ip(config)
124
132
  - `clearViolations()` - Clear all violation records
125
133
  - `getAllViolations()` - Get all violation records
126
134
 
135
+ #### IP Region Detection
136
+ - `setRegionChecker(fn)` - Set custom region check function
137
+ - `getRegion(ip)` - Get IP region info (with cache)
138
+ - `isForeign(ip)` - Check if IP is foreign
139
+ - `checkRegion(ip)` - Check IP region, auto-blacklist foreign IPs
140
+ - `clearRegionCache()` - Clear region check result cache
141
+
142
+ #### OS-Level Blacklisting
143
+ - `osBlack(ip)` - Blacklist IP at OS level (inbound block, requires admin)
144
+ - `osUnblack(ip)` - Release IP at OS level
145
+ - `isOsBlack(ip)` - Check if IP is blacklisted at OS level
146
+
147
+ > When `os_black_enable` is enabled, `addBlack()` automatically syncs the system firewall, and `delBlack()`/`del()` automatically release it. The running environment needs administrator privileges; otherwise, failed system commands only log errors and do not affect the application-level blacklist.
148
+
127
149
  #### Persistent Storage
128
150
  - `save()` - Manually save data
129
151
  - `load()` - Manually load data
@@ -2,26 +2,25 @@
2
2
  "black": [
3
3
  "192.168.100.2",
4
4
  "192.168.200.1",
5
- "192.168.200.2",
6
5
  "192.168.200.3"
7
6
  ],
8
7
  "black_time": [
9
8
  [
10
9
  "172.16.0.2",
11
- 1765868172389
10
+ 1787927071109
12
11
  ],
13
12
  [
14
13
  "192.168.100.2",
15
- 1765868205392
14
+ 1787928865115
16
15
  ],
17
16
  [
18
17
  "192.168.200.2",
19
- 1765868206419
18
+ 1787928361999
20
19
  ],
21
20
  [
22
21
  "192.168.200.3",
23
- 1765868206425
22
+ 1787928866136
24
23
  ]
25
24
  ],
26
- "timestamp": 1765868213494
25
+ "timestamp": 1787928873206
27
26
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "violation_count": [],
3
3
  "violation_time": [],
4
- "timestamp": 1765868213494
4
+ "timestamp": 1787928873206
5
5
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "white": [],
3
- "timestamp": 1765868213494
3
+ "timestamp": 1787928873206
4
4
  }
package/index.js CHANGED
@@ -17,10 +17,16 @@ class Ip {
17
17
  auto_black_enable: true, // 启用自动高频拉黑
18
18
  auto_black_threshold: 50, // 自动拉黑阈值(每分钟请求数)
19
19
  auto_black_duration: 3600000, // 自动拉黑持续时间(1小时)
20
+ auto_black_grace_count: 3, // 自动拉黑宽限次数,连续触限达到该次数后才拉黑
20
21
  violation_enable: true, // 启用违规记录
21
22
  violation_max_count: 10, // 最大违规次数
22
23
  violation_reset_time: 86400000, // 违规记录重置时间(24小时)
23
24
  violation_auto_black: true, // 违规次数超限自动拉黑
25
+ region_check_enable: true, // 启用国外IP检测
26
+ region_checker: null, // IP地区检测函数 (ip) => { country: 'CN' },支持同步或异步
27
+ region_cache_time: 86400000, // 地区检测结果缓存时间(24小时)
28
+ os_black_enable: false, // 启用系统级拉黑联动(需管理员权限,默认关闭避免误拦截)
29
+ os_rule_prefix: 'mm_ip_black_', // 系统防火墙规则名前缀
24
30
  dir: './cache/ip' // 持久化存储目录
25
31
  }
26
32
  /**
@@ -34,12 +40,19 @@ class Ip {
34
40
  this._black = new Set();
35
41
  this._req_count = new Map();
36
42
  this._req_time = new Map();
43
+ this._req_start = new Map(); // 请求窗口开始时间
37
44
  this._black_time = new Map(); // 记录拉黑时间
38
45
  this._violation_count = new Map(); // 违规次数记录
39
- this._violation_time = new Map(); // 违规时间记录
46
+ this._violation_time = new Map(); // 违规时间记录(首次违规时间)
47
+ this._violation_flagged = new Map(); // 同一请求窗口违规标记
48
+ this._region_cache = new Map(); // IP地区检测结果缓存
40
49
  this._logger = $.log || console;
41
50
 
42
51
  this.setConfig(config);
52
+ // 默认使用ip2region作为地区检测器
53
+ if (!this._config.region_checker && this._config.region_check_enable) {
54
+ this._initIp2Region();
55
+ }
43
56
  // 自动加载持久化数据
44
57
  if (this._config.dir) {
45
58
  this.load();
@@ -118,6 +131,11 @@ Ip.prototype.addBlack = function(ip) {
118
131
  this._black.add(ip);
119
132
  this.log('info', '添加黑名单IP:', ip);
120
133
 
134
+ // 系统级联动拉黑
135
+ if (this._config.os_black_enable) {
136
+ this.osBlack(ip);
137
+ }
138
+
121
139
  // 自动保存到持久化存储
122
140
  if (this._config.dir) {
123
141
  this.save();
@@ -160,6 +178,11 @@ Ip.prototype.delBlack = function(ip) {
160
178
  if (result) {
161
179
  this.log('info', '删除黑名单IP:', ip);
162
180
 
181
+ // 系统级联动释放
182
+ if (this._config.os_black_enable) {
183
+ this.osUnblack(ip);
184
+ }
185
+
163
186
  // 自动保存到持久化存储
164
187
  if (this._config.dir) {
165
188
  this.save();
@@ -214,30 +237,50 @@ Ip.prototype.record = function(ip) {
214
237
  }
215
238
  }
216
239
 
240
+ // 内网IP豁免:不参与限流、违规与地区检测,避免误拉黑内网访问
241
+ if (this._isPrivate(ip)) {
242
+ this._updateReqCount(ip);
243
+ return true;
244
+ }
245
+
217
246
  // 白名单直接通过
218
247
  if (this.isWhite(ip)) {
219
248
  this._updateReqCount(ip);
220
249
  return true;
221
250
  }
222
251
 
252
+ // 国外IP检测:检测到国外IP自动拉黑
253
+ if (this._config.region_check_enable && this._checkForeign(ip)) {
254
+ return false;
255
+ }
256
+
223
257
  // 检查频率限制
224
258
  if (!this._checkRateLimit(ip)) {
225
259
  this.log('warn', 'IP请求频率超限:', ip);
260
+ const rate = this.getReqRate(ip);
226
261
 
227
- // 记录违规:请求频率超限
262
+ // 同一请求窗口只记一次违规,避免连续请求导致违规次数暴涨
228
263
  if (this._config.violation_enable) {
229
- this.recordViolation(ip, '请求频率超限');
264
+ const flag_time = this._violation_flagged.get(ip);
265
+ const win_start = this._req_start.get(ip);
266
+ if (flag_time !== win_start) {
267
+ this.recordViolation(ip, '请求频率超限');
268
+ this._violation_flagged.set(ip, win_start);
269
+ }
230
270
  }
231
271
 
232
- // 检查是否触发自动拉黑
233
- if (this._config.auto_black_enable) {
234
- const rate = this.getReqRate(ip);
235
- if (rate >= this._config.auto_black_threshold) {
236
- this._autoBlack(ip);
237
- return false;
238
- }
272
+ // 达到宽限次数且频率超阈值才自动拉黑
273
+ if (this._config.auto_black_enable
274
+ && rate >= this._config.auto_black_threshold
275
+ && this.getViolationCount(ip) >= this._config.auto_black_grace_count) {
276
+ this._autoBlack(ip);
277
+ return false;
239
278
  }
240
279
 
280
+ // 重置计数窗口(软限流),下一次请求重新计数
281
+ this._req_start.set(ip, Date.now());
282
+ this._req_count.set(ip, 0);
283
+
241
284
  return false;
242
285
  }
243
286
 
@@ -266,17 +309,16 @@ Ip.prototype.getReqRate = function(ip) {
266
309
  if (typeof ip !== 'string') throw new TypeError('ip must be string');
267
310
 
268
311
  const now = Date.now();
269
- const lastTime = this._req_time.get(ip) || now;
312
+ const start = this._req_start.get(ip) || now;
270
313
  const count = this._req_count.get(ip) || 0;
271
314
 
272
- if (now - lastTime < this._config.check_interval) {
273
- // 在检查间隔内,计算实际频率
274
- const elapsed = Math.max(1, now - lastTime); // 避免除零
275
- return Math.round((count / elapsed) * this._config.check_interval);
276
- } else {
277
- // 超过检查间隔,频率为0
315
+ // 当前窗口已过期,频率为0
316
+ if (now - start >= this._config.check_interval) {
278
317
  return 0;
279
318
  }
319
+
320
+ // 按窗口换算为每分钟请求数,不做时间外推
321
+ return Math.round((count / this._config.check_interval) * 60000);
280
322
  };
281
323
 
282
324
  /**
@@ -361,6 +403,7 @@ Ip.prototype.clearBlack = function() {
361
403
  Ip.prototype.clearReq = function() {
362
404
  this._req_count.clear();
363
405
  this._req_time.clear();
406
+ this._req_start.clear();
364
407
  this.log('info', '清空请求记录');
365
408
  };
366
409
 
@@ -372,20 +415,17 @@ Ip.prototype.clearReq = function() {
372
415
  */
373
416
  Ip.prototype._checkRateLimit = function(ip) {
374
417
  const now = Date.now();
375
- const lastTime = this._req_time.get(ip) || 0;
418
+ let start = this._req_start.get(ip);
376
419
 
377
- // 检查时间间隔
378
- if (now - lastTime < this._config.check_interval) {
379
- const count = this.getReqCount(ip);
380
- if (count >= this._config.max_req_per_min) {
381
- return false;
382
- }
383
- } else {
384
- // 重置计数
420
+ // 无窗口或窗口已过期,开启新窗口并重置计数
421
+ if (start === undefined || now - start >= this._config.check_interval) {
422
+ start = now;
423
+ this._req_start.set(ip, start);
385
424
  this._req_count.set(ip, 0);
386
425
  }
387
426
 
388
- return true;
427
+ const count = this._req_count.get(ip) || 0;
428
+ return count < this._config.max_req_per_min;
389
429
  };
390
430
 
391
431
  /**
@@ -394,6 +434,10 @@ Ip.prototype._checkRateLimit = function(ip) {
394
434
  * @param {string} ip - IP地址
395
435
  */
396
436
  Ip.prototype._updateReqCount = function(ip) {
437
+ if (this._req_start.get(ip) === undefined) {
438
+ this._req_start.set(ip, Date.now());
439
+ }
440
+
397
441
  const count = this.getReqCount(ip) + 1;
398
442
  this._req_count.set(ip, count);
399
443
  this._req_time.set(ip, Date.now());
@@ -410,6 +454,28 @@ Ip.prototype._validIp = function(ip) {
410
454
  return ipRegex.test(ip);
411
455
  };
412
456
 
457
+ /**
458
+ * 判断IP是否为内网/回环地址(私有方法)
459
+ * @private
460
+ * @param {string} ip - IP地址
461
+ * @returns {boolean} 是否为内网IP
462
+ */
463
+ Ip.prototype._isPrivate = function(ip) {
464
+ const parts = ip.split('.');
465
+ if (parts.length !== 4) return false;
466
+
467
+ const first = parseInt(parts[0], 10);
468
+ const second = parseInt(parts[1], 10);
469
+
470
+ // 回环 127.0.0.0/8、A类 10.0.0.0/8、B类 172.16.0.0/12、C类 192.168.0.0/16
471
+ if (first === 127) return true;
472
+ if (first === 10) return true;
473
+ if (first === 172 && second >= 16 && second <= 31) return true;
474
+ if (first === 192 && second === 168) return true;
475
+
476
+ return false;
477
+ };
478
+
413
479
  /**
414
480
  * 批量添加IP到白名单
415
481
  * @param {Array} ip_list - IP地址数组
@@ -558,8 +624,11 @@ Ip.prototype.recordViolation = function(ip, reason) {
558
624
  const now = Date.now();
559
625
  const count = this.getViolationCount(ip) + 1;
560
626
 
627
+ // 记录首次违规时间,用于违规窗口重置
628
+ if (!this._violation_time.has(ip)) {
629
+ this._violation_time.set(ip, now);
630
+ }
561
631
  this._violation_count.set(ip, count);
562
- this._violation_time.set(ip, now);
563
632
 
564
633
  // 检查是否达到自动拉黑条件
565
634
  if (this._config.violation_auto_black && count >= this._config.violation_max_count) {
@@ -622,7 +691,7 @@ Ip.prototype.getViolationInfo = function(ip) {
622
691
 
623
692
  return {
624
693
  count: count,
625
- time: this._violation_time.get(ip)
694
+ time: this._violation_time.get(ip) // 首次违规时间
626
695
  };
627
696
  };
628
697
 
@@ -842,6 +911,349 @@ Ip.prototype.stopAutoSave = function() {
842
911
  }
843
912
  };
844
913
 
914
+ /**
915
+ * 设置IP地区检测函数
916
+ * @param {Function} fn - 地区检测函数,入参为IP地址,返回地区信息对象或国家代码
917
+ * @throws {TypeError} 当fn非函数时
918
+ */
919
+ Ip.prototype.setRegionChecker = function(fn) {
920
+ if (typeof fn !== 'function') throw new TypeError('fn must be function');
921
+
922
+ this._config.region_checker = fn;
923
+ this._config.region_check_enable = true;
924
+ };
925
+
926
+ /**
927
+ * 初始化ip2region地区检测器(私有方法)
928
+ * @private
929
+ */
930
+ Ip.prototype._initIp2Region = function() {
931
+ try {
932
+ const IP2Region = require('ip2region').default;
933
+ const searcher = new IP2Region();
934
+
935
+ this._ip2region = searcher;
936
+ this._config.region_checker = function(ip) {
937
+ const result = searcher.search(ip);
938
+ // 内网IP或查询失败时country为空,不返回地区信息
939
+ if (!result || !result.country) return null;
940
+ return { country: result.country };
941
+ };
942
+ this.log('info', '已加载ip2region地区检测器');
943
+ } catch (err) {
944
+ this.log('warn', 'ip2region加载失败,地区检测不可用:', err.message);
945
+ this._config.region_check_enable = false;
946
+ }
947
+ };
948
+
949
+ /**
950
+ * 获取IP地区信息(带缓存)
951
+ * @param {string} ip - IP地址
952
+ * @returns {Object|null|Promise} 地区信息对象,无检测函数或检测失败返回null,异步检测返回Promise
953
+ * @throws {TypeError} 当IP非字符串时
954
+ */
955
+ Ip.prototype.getRegion = function(ip) {
956
+ if (typeof ip !== 'string') throw new TypeError('ip must be string');
957
+ if (!this._config.region_checker) return null;
958
+
959
+ // 优先读取缓存
960
+ const cached = this._region_cache.get(ip);
961
+ if (cached && Date.now() - cached.time < this._config.region_cache_time) {
962
+ return cached.region;
963
+ }
964
+
965
+ try {
966
+ const result = this._config.region_checker(ip);
967
+
968
+ // 异步检测函数:缓存结果后返回
969
+ if (result && typeof result.then === 'function') {
970
+ return result.then(region => {
971
+ if (region) {
972
+ this._region_cache.set(ip, { region, time: Date.now() });
973
+ }
974
+ return region;
975
+ });
976
+ }
977
+
978
+ if (result) {
979
+ this._region_cache.set(ip, { region: result, time: Date.now() });
980
+ }
981
+ return result || null;
982
+ } catch (err) {
983
+ this.log('error', 'IP地区检测失败:', err.message);
984
+ return null;
985
+ }
986
+ };
987
+
988
+ /**
989
+ * 判断IP是否为国外IP
990
+ * @param {string} ip - IP地址
991
+ * @returns {boolean} 是否为国外IP
992
+ * @throws {TypeError} 当IP非字符串时
993
+ */
994
+ Ip.prototype.isForeign = function(ip) {
995
+ if (typeof ip !== 'string') throw new TypeError('ip must be string');
996
+
997
+ const region = this.getRegion(ip);
998
+ if (!region || typeof region.then === 'function') return false;
999
+
1000
+ return this._isForeign(region);
1001
+ };
1002
+
1003
+ /**
1004
+ * 检测IP地区,国外IP自动加入黑名单(永久)
1005
+ * @param {string} ip - IP地址
1006
+ * @returns {Object|null|Promise} 检测结果对象,检测失败返回null,异步检测返回Promise
1007
+ * @throws {TypeError} 当IP非字符串时
1008
+ */
1009
+ Ip.prototype.checkRegion = function(ip) {
1010
+ if (typeof ip !== 'string') throw new TypeError('ip must be string');
1011
+
1012
+ const region = this.getRegion(ip);
1013
+ if (region && typeof region.then === 'function') {
1014
+ return region.then(region_info => this._handleRegionCheck(ip, region_info));
1015
+ }
1016
+
1017
+ return this._handleRegionCheck(ip, region);
1018
+ };
1019
+
1020
+ /**
1021
+ * 清空IP地区检测结果缓存
1022
+ */
1023
+ Ip.prototype.clearRegionCache = function() {
1024
+ this._region_cache.clear();
1025
+ this.log('info', '清空IP地区缓存');
1026
+ };
1027
+
1028
+ /**
1029
+ * 判断地区信息是否为国外(私有方法)
1030
+ * @private
1031
+ * @param {Object|string} region - 地区信息对象或国家代码
1032
+ * @returns {boolean} 是否为国外
1033
+ */
1034
+ Ip.prototype._isForeign = function(region) {
1035
+ if (!region) return false;
1036
+
1037
+ let country = '';
1038
+ if (typeof region === 'string') {
1039
+ country = region;
1040
+ } else if (typeof region === 'object') {
1041
+ // 支持直接指定国外标识
1042
+ if (typeof region.is_foreign === 'boolean') {
1043
+ return region.is_foreign;
1044
+ }
1045
+ country = region.country || region.country_code || '';
1046
+ }
1047
+
1048
+ if (!country) return false;
1049
+
1050
+ const cn_list = ['cn', 'chn', '中国', '中国大陆', 'china'];
1051
+ return !cn_list.includes(country.toLowerCase());
1052
+ };
1053
+
1054
+ /**
1055
+ * 检测国外IP并自动拉黑(私有方法)
1056
+ * @private
1057
+ * @param {string} ip - IP地址
1058
+ * @returns {boolean} 是否已拉黑国外IP
1059
+ */
1060
+ Ip.prototype._checkForeign = function(ip) {
1061
+ if (!this._config.region_checker) return false;
1062
+ if (this._isPrivate(ip)) return false;
1063
+
1064
+ const region = this.getRegion(ip);
1065
+ // 异步检测函数无法同步拉黑,跳过自动处理
1066
+ if (!region || typeof region.then === 'function') return false;
1067
+
1068
+ if (this._isForeign(region)) {
1069
+ if (this.addBlack(ip)) {
1070
+ // 国外IP拉黑带过期时间,避免永久封禁
1071
+ this._black_time.set(ip, Date.now() + this._config.auto_black_duration);
1072
+ }
1073
+ this.log('warn', '检测到国外IP,自动拉黑:', ip);
1074
+ return true;
1075
+ }
1076
+
1077
+ return false;
1078
+ };
1079
+
1080
+ /**
1081
+ * 处理地区检测结果并拉黑国外IP(私有方法)
1082
+ * @private
1083
+ * @param {string} ip - IP地址
1084
+ * @param {Object} region - 地区信息
1085
+ * @returns {Object|null} 检测结果对象
1086
+ */
1087
+ Ip.prototype._handleRegionCheck = function(ip, region) {
1088
+ if (!region) return null;
1089
+ if (this._isPrivate(ip)) return null;
1090
+
1091
+ const is_foreign = this._isForeign(region);
1092
+ let blacklisted = false;
1093
+
1094
+ if (is_foreign) {
1095
+ blacklisted = this.addBlack(ip);
1096
+ if (blacklisted) {
1097
+ // 国外IP拉黑带过期时间,避免永久封禁
1098
+ this._black_time.set(ip, Date.now() + this._config.auto_black_duration);
1099
+ }
1100
+ this.log('warn', '检测到国外IP,加入黑名单:', ip);
1101
+ }
1102
+
1103
+ return {
1104
+ ip: ip,
1105
+ region: region,
1106
+ is_foreign: is_foreign,
1107
+ blacklisted: blacklisted
1108
+ };
1109
+ };
1110
+
1111
+ /**
1112
+ * 在操作系统层拉黑IP(入站拦截)
1113
+ * @param {string} ip - IP地址
1114
+ * @returns {boolean} 是否拉黑成功
1115
+ * @throws {TypeError} 当IP非字符串时
1116
+ */
1117
+ Ip.prototype.osBlack = function(ip) {
1118
+ if (typeof ip !== 'string') throw new TypeError('ip must be string');
1119
+ if (!this._validIp(ip)) return false;
1120
+
1121
+ const cmd = this._getOsCmd(ip, 'black');
1122
+ if (!cmd) return false;
1123
+
1124
+ const result = this._execOsCmd(cmd);
1125
+ if (result.success) {
1126
+ this.log('info', '系统层拉黑IP:', ip);
1127
+ return true;
1128
+ }
1129
+
1130
+ this.log('error', '系统层拉黑IP失败:', ip, result.output);
1131
+ return false;
1132
+ };
1133
+
1134
+ /**
1135
+ * 在操作系统层释放IP(解除入站拦截)
1136
+ * @param {string} ip - IP地址
1137
+ * @returns {boolean} 是否释放成功
1138
+ * @throws {TypeError} 当IP非字符串时
1139
+ */
1140
+ Ip.prototype.osUnblack = function(ip) {
1141
+ if (typeof ip !== 'string') throw new TypeError('ip must be string');
1142
+ if (!this._validIp(ip)) return false;
1143
+
1144
+ const cmd = this._getOsCmd(ip, 'unblack');
1145
+ if (!cmd) return false;
1146
+
1147
+ const result = this._execOsCmd(cmd);
1148
+ if (result.success) {
1149
+ this.log('info', '系统层释放IP:', ip);
1150
+ return true;
1151
+ }
1152
+
1153
+ this.log('error', '系统层释放IP失败:', ip, result.output);
1154
+ return false;
1155
+ };
1156
+
1157
+ /**
1158
+ * 检查IP是否已在操作系统层拉黑
1159
+ * @param {string} ip - IP地址
1160
+ * @returns {boolean} 是否已系统拉黑
1161
+ * @throws {TypeError} 当IP非字符串时
1162
+ */
1163
+ Ip.prototype.isOsBlack = function(ip) {
1164
+ if (typeof ip !== 'string') throw new TypeError('ip must be string');
1165
+ if (!this._validIp(ip)) return false;
1166
+
1167
+ const cmd = this._getOsCmd(ip, 'check');
1168
+ if (!cmd) return false;
1169
+
1170
+ const result = this._execOsCmd(cmd);
1171
+ if (!result.success) return false;
1172
+
1173
+ return result.output.includes(this._config.os_rule_prefix + ip)
1174
+ || result.output.includes(ip);
1175
+ };
1176
+
1177
+ /**
1178
+ * 生成系统拉黑命令(私有方法)
1179
+ * @private
1180
+ * @param {string} ip - IP地址
1181
+ * @param {string} action - 操作类型(black, unblack, check)
1182
+ * @returns {string|null} 系统命令,不支持的平台返回null
1183
+ */
1184
+ Ip.prototype._getOsCmd = function(ip, action) {
1185
+ if (process.platform === 'win32') {
1186
+ return this._getWinCmd(ip, action);
1187
+ }
1188
+ if (process.platform === 'linux') {
1189
+ return this._getLinuxCmd(ip, action);
1190
+ }
1191
+ return null;
1192
+ };
1193
+
1194
+ /**
1195
+ * 生成Windows防火墙命令(私有方法)
1196
+ * @private
1197
+ * @param {string} ip - IP地址
1198
+ * @param {string} action - 操作类型(black, unblack, check)
1199
+ * @returns {string|null} 系统命令
1200
+ */
1201
+ Ip.prototype._getWinCmd = function(ip, action) {
1202
+ const rule = this._config.os_rule_prefix + ip;
1203
+
1204
+ if (action === 'black') {
1205
+ return `netsh advfirewall firewall add rule name="${rule}" dir=in action=block remoteip=${ip}`;
1206
+ }
1207
+ if (action === 'unblack') {
1208
+ return `netsh advfirewall firewall delete rule name="${rule}"`;
1209
+ }
1210
+ if (action === 'check') {
1211
+ return `netsh advfirewall firewall show rule name="${rule}"`;
1212
+ }
1213
+ return null;
1214
+ };
1215
+
1216
+ /**
1217
+ * 生成Linux iptables命令(私有方法)
1218
+ * @private
1219
+ * @param {string} ip - IP地址
1220
+ * @param {string} action - 操作类型(black, unblack, check)
1221
+ * @returns {string|null} 系统命令
1222
+ */
1223
+ Ip.prototype._getLinuxCmd = function(ip, action) {
1224
+ if (action === 'black') {
1225
+ return `iptables -I INPUT -s ${ip} -j DROP`;
1226
+ }
1227
+ if (action === 'unblack') {
1228
+ return `iptables -D INPUT -s ${ip} -j DROP`;
1229
+ }
1230
+ if (action === 'check') {
1231
+ return `iptables -L INPUT -n`;
1232
+ }
1233
+ return null;
1234
+ };
1235
+
1236
+ /**
1237
+ * 执行系统命令(私有方法)
1238
+ * @private
1239
+ * @param {string} cmd - 系统命令
1240
+ * @returns {Object} 执行结果 { success, output }
1241
+ */
1242
+ Ip.prototype._execOsCmd = function(cmd) {
1243
+ try {
1244
+ const { execSync } = require('child_process');
1245
+ const output = execSync(cmd, {
1246
+ encoding: 'utf8',
1247
+ timeout: 8000,
1248
+ stdio: ['pipe', 'pipe', 'pipe']
1249
+ });
1250
+ return { success: true, output: output || '' };
1251
+ } catch (err) {
1252
+ const output = (err.stdout && err.stdout.toString()) || err.message || '';
1253
+ return { success: false, output: output };
1254
+ }
1255
+ };
1256
+
845
1257
  /**
846
1258
  * 删除IP记录(从黑名单、白名单、临时记录中删除)
847
1259
  * @param {string} ip - IP地址
@@ -864,6 +1276,11 @@ Ip.prototype.del = function(ip) {
864
1276
  if (this._black.has(ip)) {
865
1277
  this._black.delete(ip);
866
1278
  result.black = true;
1279
+
1280
+ // 系统级联动释放
1281
+ if (this._config.os_black_enable) {
1282
+ this.osUnblack(ip);
1283
+ }
867
1284
  }
868
1285
 
869
1286
  // 从白名单删除
@@ -883,12 +1300,19 @@ Ip.prototype.del = function(ip) {
883
1300
  result.violation_time = true;
884
1301
  }
885
1302
 
1303
+ // 清理请求窗口与违规标记
1304
+ this._req_start.delete(ip);
1305
+ this._violation_flagged.delete(ip);
1306
+
886
1307
  // 从黑名单过期时间记录删除
887
1308
  if (this._black_time.has(ip)) {
888
1309
  this._black_time.delete(ip);
889
1310
  result.black_time = true;
890
1311
  }
891
1312
 
1313
+ // 从地区缓存删除
1314
+ this._region_cache.delete(ip);
1315
+
892
1316
  // 如果有任何删除操作,记录日志并自动保存
893
1317
  if (Object.values(result).some(Boolean)) {
894
1318
  // 自动保存到持久化存储
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mm_ip",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "用于管理IP名单,可设置白名单、黑名单和IP检查,查询IP请求次数、频率等。",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,6 +21,7 @@
21
21
  "author": "qww",
22
22
  "license": "ISC",
23
23
  "dependencies": {
24
- "mm_expand": "^2.5.1"
24
+ "ip2region": "^2.3.0",
25
+ "mm_expand": "^2.5.4"
25
26
  }
26
27
  }
package/test.js CHANGED
@@ -16,7 +16,8 @@ const ip_manager = new Ip({
16
16
  violation_enable: true, // 启用违规记录
17
17
  violation_max_count: 3, // 降低阈值便于测试
18
18
  violation_reset_time: 10000, // 10秒便于测试
19
- violation_auto_black: true // 违规次数超限自动拉黑
19
+ violation_auto_black: true, // 违规次数超限自动拉黑
20
+ os_black_enable: false // 测试环境禁用系统级联动
20
21
  });
21
22
 
22
23
 
@@ -35,6 +36,91 @@ console.log('添加 10.0.0.1:', ip_manager.addBlack('10.0.0.1'));
35
36
  console.log('添加 10.0.0.2:', ip_manager.addBlack('10.0.0.2'));
36
37
  console.log('黑名单列表:', ip_manager.getAllBlack());
37
38
 
39
+ // 测试2.5: 国外IP检测功能
40
+ console.log('\n2.5 测试国外IP检测功能:');
41
+
42
+ // 创建独立实例,避免影响其他测试
43
+ const region_ip = new Ip({ dir: null, os_black_enable: false });
44
+
45
+ // 模拟地区检测函数
46
+ region_ip.setRegionChecker(function(ip) {
47
+ const region_map = {
48
+ '8.8.8.8': { country: 'US' }, // 国外
49
+ '1.1.1.1': 'US', // 国外(字符串国家代码)
50
+ '114.114.114.114': { country: 'CN' } // 国内
51
+ };
52
+ return region_map[ip] || null;
53
+ });
54
+
55
+ // 获取地区信息
56
+ console.log('获取 8.8.8.8 地区:', region_ip.getRegion('8.8.8.8'));
57
+ console.log('获取 114.114.114.114 地区:', region_ip.getRegion('114.114.114.114'));
58
+
59
+ // 判断国外
60
+ console.log('8.8.8.8 是否国外:', region_ip.isForeign('8.8.8.8'));
61
+ console.log('114.114.114.114 是否国外:', region_ip.isForeign('114.114.114.114'));
62
+
63
+ // checkRegion 检测并拉黑国外IP
64
+ console.log('检测 8.8.8.8 地区:', region_ip.checkRegion('8.8.8.8'));
65
+ console.log('8.8.8.8 是否在黑名单:', region_ip.isBlack('8.8.8.8'));
66
+ console.log('检测 114.114.114.114 地区:', region_ip.checkRegion('114.114.114.114'));
67
+ console.log('114.114.114.114 是否在黑名单:', region_ip.isBlack('114.114.114.114'));
68
+
69
+ // record 自动检测国外IP并拉黑
70
+ console.log('记录 1.1.1.1 请求:', region_ip.record('1.1.1.1'));
71
+ console.log('1.1.1.1 是否在黑名单:', region_ip.isBlack('1.1.1.1'));
72
+
73
+ // 未知地区IP不会被拉黑
74
+ console.log('记录未知IP 172.16.9.9 请求:', region_ip.record('172.16.9.9'));
75
+ console.log('172.16.9.9 是否在黑名单:', region_ip.isBlack('172.16.9.9'));
76
+
77
+ // 清空地区缓存
78
+ region_ip.clearRegionCache();
79
+ console.log('地区缓存已清空');
80
+
81
+ // 测试2.6: ip2region 接入测试
82
+ console.log('\n2.6 测试ip2region接入:');
83
+
84
+ // 使用默认配置创建实例,自动加载ip2region检测器
85
+ const ipr = new Ip({ dir: null, os_black_enable: false });
86
+ console.log('ip2region 检测器已加载:', typeof ipr.getRegion('8.8.8.8') === 'object');
87
+
88
+ // 国外IP自动拉黑
89
+ console.log('8.8.8.8 地区:', ipr.getRegion('8.8.8.8'));
90
+ console.log('8.8.8.8 是否国外:', ipr.isForeign('8.8.8.8'));
91
+ console.log('记录 8.8.8.8 请求:', ipr.record('8.8.8.8'));
92
+ console.log('8.8.8.8 是否在黑名单:', ipr.isBlack('8.8.8.8'));
93
+
94
+ // 国内IP正常放行
95
+ console.log('记录 114.114.114.114 请求:', ipr.record('114.114.114.114'));
96
+ console.log('114.114.114.114 是否在黑名单:', ipr.isBlack('114.114.114.114'));
97
+
98
+ // 内网IP不会被拉黑
99
+ console.log('记录 192.168.1.1 请求:', ipr.record('192.168.1.1'));
100
+ console.log('192.168.1.1 是否在黑名单:', ipr.isBlack('192.168.1.1'));
101
+
102
+ // 测试2.7: 系统级拉黑功能
103
+ console.log('\n2.7 测试系统级拉黑功能:');
104
+ const os_ip = new Ip({ dir: null });
105
+
106
+ // 无效IP不执行系统命令
107
+ console.log('无效IP osBlack:', os_ip.osBlack('not_a_ip'));
108
+ console.log('无效IP isOsBlack:', os_ip.isOsBlack('not_a_ip'));
109
+
110
+ // 系统级拉黑(需管理员权限,无权限时返回false且不崩溃)
111
+ console.log('系统拉黑 1.2.3.4:', os_ip.osBlack('1.2.3.4'));
112
+ console.log('1.2.3.4 是否系统拉黑:', os_ip.isOsBlack('1.2.3.4'));
113
+
114
+ // 系统级释放
115
+ console.log('系统释放 1.2.3.4:', os_ip.osUnblack('1.2.3.4'));
116
+ console.log('释放后是否系统拉黑:', os_ip.isOsBlack('1.2.3.4'));
117
+
118
+ // addBlack 联动系统拉黑
119
+ os_ip.setConfig({ os_black_enable: true });
120
+ console.log('联动addBlack系统拉黑 1.2.3.5:', os_ip.addBlack('1.2.3.5'));
121
+ console.log('1.2.3.5 在应用层黑名单:', os_ip.isBlack('1.2.3.5'));
122
+ console.log('delBlack 联动系统释放:', os_ip.delBlack('1.2.3.5'));
123
+
38
124
  // 测试3: 检查IP状态
39
125
  console.log('\n3. 测试检查IP状态:');
40
126
  console.log('192.168.1.1 在白名单:', ip_manager.isWhite('192.168.1.1'));
@@ -196,7 +282,8 @@ setTimeout(() => {
196
282
  const persistent_ip = new Ip({
197
283
  dir: './cache/ip',
198
284
  max_white: 3,
199
- max_black: 3
285
+ max_black: 3,
286
+ os_black_enable: false
200
287
  });
201
288
 
202
289
  // 添加测试数据
@@ -246,7 +333,8 @@ setTimeout(() => {
246
333
  // 创建新实例并加载数据
247
334
  console.log('创建新实例并加载数据:');
248
335
  const loaded_ip = new Ip({
249
- dir: './cache/ip'
336
+ dir: './cache/ip',
337
+ os_black_enable: false
250
338
  });
251
339
 
252
340
  console.log('加载后数据统计:');