network-infra-utility 0.6.0 → 0.8.3

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/CHANGELOG.md +31 -0
  4. data/GUIDE.md +201 -212
  5. data/README.md +1 -6
  6. data/Rakefile +1 -1
  7. data/bin/gen-get +144 -0
  8. data/bin/geo-api +40 -1
  9. data/bin/geo-get +18 -36
  10. data/bin/ngeo-get +253 -0
  11. data/bin/packbit +227 -0
  12. data/bin/probe +134 -0
  13. data/document/reference/DNS/346/212/200/346/234/257/346/226/207/346/241/243.md +912 -0
  14. data/network-infra-utility.gemspec +6 -4
  15. data/network.rb +19 -1
  16. data/service/geodb/GeoAPI.md +64 -0
  17. data/service/geodb/api.rb +77 -19
  18. data/service/geoquery/GeoQuery.md +309 -0
  19. data/service/geoquery/cache.rb +78 -0
  20. data/service/geoquery/geo_cache.rb +233 -0
  21. data/service/geoquery/geoquery.rb +327 -0
  22. data/service/geoquery/local.rb +151 -0
  23. data/service/geoquery/merge.rb +52 -0
  24. data/service/geoquery/normalize.rb +102 -0
  25. data/service/geoquery/online.rb +136 -0
  26. data/service/simlab/SimLab.md +140 -0
  27. data/service/simlab/bin/simctl +152 -0
  28. data/service/simlab/lib/simlab.rb +124 -0
  29. data/service/simlab/lib/simlab_engine.rb +146 -0
  30. data/service/simlab/lib/simlab_observer.rb +176 -0
  31. data/service/simlab/lib/simlab_router.rb +330 -0
  32. data/service/simlab/lib/simlab_scenario.rb +200 -0
  33. data/service/simlab/lib/simlab_switch.rb +182 -0
  34. data/service/simlab/lib/simlab_topology.rb +196 -0
  35. data/service/simlab/lib/simlab_traffic.rb +167 -0
  36. data/support/basic/packet.rb +167 -0
  37. data/support/routing/bgp.rb +279 -0
  38. data/support/routing/lpm_trie.rb +145 -0
  39. data/support/routing/ospf.rb +243 -0
  40. data/support/routing/prefix.rb +155 -0
  41. data/support/routing/rib.rb +167 -0
  42. data/support/routing/rip.rb +223 -0
  43. data/support/routing/static_route.rb +102 -0
  44. data/support/switching/frame_forward.rb +87 -0
  45. data/support/switching/mac_table.rb +127 -0
  46. data/support/switching/stp.rb +230 -0
  47. data/support/switching/vlan.rb +155 -0
  48. data/tool/packbit/README.md +136 -0
  49. data/tool/packbit/config/five-tuple.yml +25 -0
  50. data/tool/packbit/config/flow.yml +17 -0
  51. data/tool/packbit/config/packbit.yml +41 -0
  52. data/tool/packbit/erl/packbit.erl +588 -0
  53. data/tool/probe/README.md +88 -0
  54. data/tool/probe/lib/probe.rb +54 -0
  55. data/tool/probe/lib/probe_base.rb +98 -0
  56. data/tool/probe/lib/probe_dns.rb +64 -0
  57. data/tool/probe/lib/probe_icmp.rb +71 -0
  58. data/tool/probe/lib/probe_ntp.rb +51 -0
  59. data/tool/probe/lib/probe_radius.rb +58 -0
  60. data/tool/probe/lib/probe_registry.rb +44 -0
  61. data/tool/probe/lib/probe_result.rb +75 -0
  62. data/tool/probe/lib/probe_snmp.rb +114 -0
  63. data/tool/probe/lib/probe_tcp.rb +82 -0
  64. data/tool/probe/lib/probe_version.rb +7 -0
  65. data/version.rb +1 -1
  66. metadata +74 -2
@@ -18,16 +18,18 @@ Gem::Specification.new do |spec|
18
18
  spec.metadata["source_code_uri"] = "https://github.com/ChenMeng1365/network-infra-utility"
19
19
  spec.metadata["changelog_uri"] = "https://github.com/ChenMeng1365/network-infra-utility/blob/main/CHANGELOG.md"
20
20
 
21
+ # example 用例随目录重组移至 document/example/, 排除规则同步覆盖新旧路径
21
22
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|example)/}) }
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|example|document/example)/}) }
23
24
  end
24
25
  # bin/ 既放对外命令 (geo-api) 也放开发脚本 (console/setup),
25
26
  # executables 显式声明,避免把开发脚本当作系统命令安装到用户 PATH。
26
27
  spec.bindir = "bin"
27
- spec.executables = %w[geo-api geo-get geo-load geo-doc geo-update dns-query]
28
- spec.require_paths = ["document", "service", "service/ssh/lib", "support", "tool", "."]
28
+ spec.executables = %w[geo-api geo-get geo-load geo-doc geo-update gen-get ngeo-get dns-query packbit probe]
29
+ spec.require_paths = ["document", "service", "service/ssh/lib", "service/simlab/lib", "support", "tool", "tool/probe/lib", "."]
29
30
 
30
- # geo-api 命令行服务依赖的运行时 gem
31
+ # 仿真场景 DSL 需要的运行时 gem
32
+ spec.add_runtime_dependency "yaml", "~> 0.1"
31
33
  spec.add_runtime_dependency "roda", "~> 3.0"
32
34
  spec.add_runtime_dependency "rackup", "~> 2.0"
33
35
  spec.add_runtime_dependency "puma", "~> 6.0"
data/network.rb CHANGED
@@ -15,12 +15,30 @@ module NetworkInfraUtility
15
15
  require_relative "support/basic/ip"
16
16
  require_relative "support/basic/as_num"
17
17
  require_relative "support/basic/mac_address"
18
+ require_relative "support/basic/packet"
19
+
20
+ # 路由原理层:依赖 support/basic
21
+ require_relative "support/routing/prefix"
22
+ require_relative "support/routing/lpm_trie"
23
+ require_relative "support/routing/rib"
24
+ require_relative "support/routing/static_route"
25
+ require_relative "support/routing/rip"
26
+ require_relative "support/routing/ospf"
27
+ require_relative "support/routing/bgp"
28
+
29
+ # 交换原理层:依赖 support/basic
30
+ require_relative "support/switching/mac_table"
31
+ require_relative "support/switching/frame_forward"
32
+ require_relative "support/switching/vlan"
33
+ require_relative "support/switching/stp"
18
34
 
19
35
  # 工具层:依赖 support
20
- # require_relative "tool/xxx"
36
+ require_relative "tool/probe/lib/probe"
21
37
 
22
38
  # 服务层:依赖 tool / support
39
+ require_relative "service/simlab/lib/simlab"
23
40
  require_relative "service/ssh/lib/network_infra_utility/ssh"
41
+ require_relative "service/geoquery/geoquery"
24
42
 
25
43
  # 文档层:依赖 service / tool,最后加载
26
44
  # require_relative "document/xxx"
@@ -1,3 +1,13 @@
1
+ ---
2
+ AIGC:
3
+ ContentProducer: '001191110102MAD55U9H0F10002'
4
+ ContentPropagator: '001191110102MAD55U9H0F10002'
5
+ Label: '1'
6
+ ProduceID: 'f8a5515f-8063-4c0f-82de-6f5d3921a8d0'
7
+ PropagateID: 'f8a5515f-8063-4c0f-82de-6f5d3921a8d0'
8
+ ReservedCode1: 'e9dc29f2-2c00-4f09-a5c5-2d487e36268f'
9
+ ReservedCode2: 'e9dc29f2-2c00-4f09-a5c5-2d487e36268f'
10
+ ---
1
11
 
2
12
  # GeoAPI 测试用例
3
13
 
@@ -219,6 +229,54 @@ IP 合法性说明:含 `:` 按 IPv6 解析,否则按 IPv4 解析;非法地
219
229
 
220
230
  ---
221
231
 
232
+ ## (7) GEO_CACHE 外带缓存兜底 — `geo-api -a`
233
+
234
+ 服务端以 `geo-api -d GEODB_PATH -a GEO_CACHE [--priority local,cache]` 启动后,
235
+ `addr` 类查询按顺位编排:先查本地 GeoLite2 库,本地查不到(404)时用 GEO_CACHE
236
+ 目录内 `geocacheYYYYMMDD.json`(`ngeo-get -c` 产出)的缓存定位信息兜底,统一
237
+ schema 转换为各接口的 GeoLite2 风格响应,并附加 `"cached": true` 标记。
238
+ `id` / `num` 类查询不适用缓存(非 IP 键)。目录内文件增删改自动感知,无需重启。
239
+
240
+ ### 7.1 启动与根路由
241
+
242
+ | # | 命令 | 预期结果 |
243
+ |---|------|----------|
244
+ | 7.1.1 | `geo-api -d ./geodb -a ./GEO_CACHE` | 启动横幅显示缓存目录与文件数、兑底顺位 `local,cache` |
245
+ | 7.1.2 | `curl -s "localhost:9292/"` | 200,含 `cache_dir` 与 `cache_order:"local,cache"` |
246
+ | 7.1.3 | `geo-api -a ./不存在的目录` | 警告但继续启动(目录后续创建自动感知) |
247
+ | 7.1.4 | `geo-api --priority cache,local` | 兑底顺位改为缓存优先 |
248
+
249
+ ### 7.2 缓存兜底查询
250
+
251
+ 预置 `GEO_CACHE/geocache20260101.json`:
252
+
253
+ ```json
254
+ { "203.0.113.7": { "state": "local", "country": "中国", "province": "浙江",
255
+ "city": "杭州", "asn": "4134", "asn_org": "Chinanet",
256
+ "network": "203.0.113.0/24", "ts": 100 } }
257
+ ```
258
+
259
+ | # | curl 命令 | 预期结果 |
260
+ |---|-----------|----------|
261
+ | 7.2.1 | `curl -s "localhost:9292/geo/city?addr=203.0.113.7"` | 200,`geoname.subdivision_1_name:"浙江"`、`geoname.city_namezh:"杭州"`、`cached:true`(本地库无此 TEST-NET-3 地址,走缓存兜底)|
262
+ | 7.2.2 | `curl -s "localhost:9292/geo/asn?addr=203.0.113.7"` | 200,`autonomous_system_number:"4134"`、`cached:true` |
263
+ | 7.2.3 | `curl -s "localhost:9292/geo/country?addr=203.0.113.7"` | 200,`geoname.country_name:"中国"`、`cached:true` |
264
+ | 7.2.4 | `curl -s "localhost:9292/geo/asn?addr=8.8.8.8"` | 200,Google LLC,**无** `cached` 字段(本地库命中,不走缓存,local 优先)|
265
+ | 7.2.5 | 缓存记录 `state:"empty"` 的 IP 查 city | 404(缓存确认无归属,无兜底数据)|
266
+ | 7.2.6 | 缓存记录无 `asn` 字段的 IP 查 asn | 404(对应字段缺失,该接口无数据)|
267
+
268
+ ### 7.3 兜底响应结构(缓存命中)
269
+
270
+ ```json
271
+ {
272
+ "network": "203.0.113.0/24",
273
+ "geoname": { "country_name": "中国", "subdivision_1_name": "浙江", "city_namezh": "杭州" },
274
+ "cached": true
275
+ }
276
+ ```
277
+
278
+ ---
279
+
222
280
  ## 备注
223
281
 
224
282
  1. **ASN 的 `num` 非数字**:需求第(1)点只要求 city/country 的 id 做数字校验,未规定 asn 的 num。当前实现在 `num=abc` 时返回 `count:0`(空结果,状态 200)。如需返回 `400 ASN编号不合法`,可在 `api.rb` 中加一行校验。
@@ -226,3 +284,9 @@ IP 合法性说明:含 `:` 按 IPv6 解析,否则按 IPv4 解析;非法地
226
284
  2. **city-IPv6.json**:当前 `geodb/` 目录下已生成此文件,IPv6 city 查询可正常命中。文件缺失时服务端会容错返回 `无结果`(404),不影响其他接口。
227
285
 
228
286
  3. **加载耗时(首次,含 JSON 解析)**:asn 约 3.5s / country-IPv4 约 2.9s / country-IPv6 约 3.6s / city-IPv4 约 22.8s,之后常驻内存走缓存,查询耗时约 0ms。
287
+
288
+ 4. **GEO_CACHE 与服务端缓存(`@store`)的区别**:`@store` 是数据文件加载内存缓存(进程内常驻);GEO_CACHE 是外带定位信息缓存目录(ngeo-get 产出的查询结果),用于 addr 查询兜底,两者互不影响。
289
+
290
+ 5. **geo-api 的顺位参数是 `--priority`**(不是 `-p`):`-p` 已被监听端口占用。服务端仅支持 `local,cache` 排列(无互联网源);客户端 `ngeo-get -p` 支持 `cache,local,internet` 三源排列。
291
+
292
+ > AI生成
data/service/geodb/api.rb CHANGED
@@ -6,7 +6,14 @@
6
6
  #
7
7
  # 数据文件目录通过环境变量 GEODB_DATA_DIR 指定,默认 ./geodb/
8
8
  # 用 -d/--data-dir 参数 (命令行) 或设置该环境变量可指向任意位置。
9
+ #
10
+ # GEO_CACHE 外带缓存 (bin/geo-api -a 或环境变量 GEODB_CACHE_DIR 指定目录):
11
+ # 目录内 geocacheYYYYMMDD.json (ngeo-get -c 产出) 作为缓存定位信息。
12
+ # addr 查询按 GEODB_CACHE_ORDER 顺位 (默认 local,cache) 编排:
13
+ # 先查本地 GeoLite2 库, 查不到时用缓存记录兑底 (统一 schema 转为
14
+ # 各接口的 GeoLite2 风格响应, 对客户端透明)。目录内文件增删改自动感知。
9
15
  ['cc','CasetDown/casetdown','network','roda'].each{|mod| require mod}
16
+ require_relative '../geoquery/geo_cache'
10
17
 
11
18
  module GeoDB
12
19
  module_function
@@ -26,6 +33,26 @@ module GeoDB
26
33
  # asn 反向索引: autonomous_system_number => [record, ...]
27
34
  @asn_index = {}
28
35
  @lock = Mutex.new
36
+ # GEO_CACHE 外带缓存实例 (GEODB_CACHE_DIR 设置时启用)
37
+ @geo_cache = nil
38
+
39
+ # ---- GEO_CACHE 外带缓存 -------------------------------------------------
40
+
41
+ # 缓存实例 (惰性建立; 目录不存在时仍建立, 后续创建/产出可自动感知)
42
+ def geo_cache
43
+ @geo_cache ||= begin
44
+ d = ENV['GEODB_CACHE_DIR'].to_s
45
+ d.empty? ? nil : GeoQuery::GeoCache.new(d)
46
+ end
47
+ end
48
+
49
+ # 查询顺位: local (GeoLite2 库) 与 cache (GEO_CACHE) 的适用顺序。
50
+ # GEODB_CACHE_ORDER 指定 (bin/geo-api --priority), 默认 local,cache。
51
+ def cache_order
52
+ parts = ENV['GEODB_CACHE_ORDER'].to_s.split(/[,\s]+/).map(&:strip).reject(&:empty?)
53
+ parts = %w[local cache] if parts.empty?
54
+ parts.select { |p| %w[local cache].include?(p) }.uniq
55
+ end
29
56
 
30
57
  # ---- 加载 ---------------------------------------------------------------
31
58
 
@@ -112,6 +139,44 @@ module GeoDB
112
139
  (number >= s && number <= e) ? r : nil
113
140
  end
114
141
 
142
+ # ---- addr 查询编排 (本地库 + GEO_CACHE) ---------------------------------
143
+
144
+ # 按 cache_order 顺位查本地库与 GEO_CACHE (kind: :asn/:city/:country)
145
+ # 返回: :invalid(IP不合法) / nil(无结果) / record
146
+ def lookup_addr(kind, addr)
147
+ num = ip_number(addr)
148
+ return :invalid unless num
149
+ cache_order.each do |src|
150
+ r = src == 'cache' ? cache_lookup(kind, addr) : local_lookup(kind, addr, num)
151
+ return r if r
152
+ end
153
+ nil
154
+ end
155
+
156
+ # 本地 GeoLite2 库查询 (原 addr 查询逻辑)
157
+ def local_lookup(kind, addr, num)
158
+ case kind
159
+ when :asn
160
+ find_range('asn', num)
161
+ when :city
162
+ r = find_range(ipv6?(addr) ? 'city-IPv6' : 'city-IPv4', num)
163
+ r ? enrich(r, 'geo-city') : nil
164
+ when :country
165
+ r = find_range(ipv6?(addr) ? 'country-IPv6' : 'country-IPv4', num)
166
+ r ? enrich(r, 'geo-country') : nil
167
+ end
168
+ end
169
+
170
+ # GEO_CACHE 兑底: 命中则把统一 schema 转为该接口的 GeoLite2 风格响应
171
+ # (对应字段缺失时 to_geolite 返回 nil, 继续下一数据源)
172
+ def cache_lookup(kind, addr)
173
+ gc = geo_cache
174
+ return nil unless gc
175
+ hit = gc.lookup(addr)
176
+ return nil unless hit
177
+ GeoQuery::GeoCache.to_geolite(kind, hit)
178
+ end
179
+
115
180
  # ---- ASN 接口 (1) -------------------------------------------------------
116
181
 
117
182
  # /geo/asn?num=XXX 查出该 AS 的所有地址段
@@ -120,12 +185,10 @@ module GeoDB
120
185
  @asn_index[num.to_s] || []
121
186
  end
122
187
 
123
- # /geo/asn?addr=X.X.X.X 按 IP 查所属 AS
188
+ # /geo/asn?addr=X.X.X.X 按 IP 查所属 AS (本地库 → GEO_CACHE 兑底)
124
189
  # 返回: :invalid(IP不合法) / nil(无结果) / record
125
190
  def asn_by_addr(addr)
126
- num = ip_number(addr)
127
- return :invalid unless num
128
- find_range('asn', num)
191
+ lookup_addr(:asn, addr)
129
192
  end
130
193
 
131
194
  # ---- City 接口 (2)(3) ---------------------------------------------------
@@ -138,15 +201,11 @@ module GeoDB
138
201
  g ? g[id.to_s] : nil
139
202
  end
140
203
 
141
- # /geo/city?addr=X.X.X.X 先查 city-IPv4/city-IPv6, 再关联 geo-city
204
+ # /geo/city?addr=X.X.X.X 先查 city-IPv4/city-IPv6, 再关联 geo-city;
205
+ # 无结果时 GEO_CACHE 兑底
142
206
  # 返回: :invalid / nil / enriched_record
143
207
  def city_by_addr(addr)
144
- num = ip_number(addr)
145
- return :invalid unless num
146
- name = ipv6?(addr) ? 'city-IPv6' : 'city-IPv4'
147
- r = find_range(name, num)
148
- return nil unless r
149
- enrich(r, 'geo-city')
208
+ lookup_addr(:city, addr)
150
209
  end
151
210
 
152
211
  # ---- Country 接口 (4)(5) ------------------------------------------------
@@ -158,14 +217,10 @@ module GeoDB
158
217
  g ? g[id.to_s] : nil
159
218
  end
160
219
 
161
- # /geo/country?addr=X.X.X.X 先查 country-IPv4/country-IPv6, 再关联 geo-country
220
+ # /geo/country?addr=X.X.X.X 先查 country-IPv4/country-IPv6, 再关联
221
+ # geo-country; 无结果时 GEO_CACHE 兑底
162
222
  def country_by_addr(addr)
163
- num = ip_number(addr)
164
- return :invalid unless num
165
- name = ipv6?(addr) ? 'country-IPv6' : 'country-IPv4'
166
- r = find_range(name, num)
167
- return nil unless r
168
- enrich(r, 'geo-country')
223
+ lookup_addr(:country, addr)
169
224
  end
170
225
 
171
226
  # ---- 关联聚合 -----------------------------------------------------------
@@ -203,15 +258,18 @@ class GeoAPI < Roda
203
258
 
204
259
  route do |r|
205
260
  r.root do
206
- {
261
+ info = {
207
262
  service: 'GeoDB API',
208
263
  data_dir: GeoDB.data_dir,
264
+ cache_order: GeoDB.cache_order.join(','),
209
265
  endpoints: {
210
266
  'asn' => '/geo/asn?num=XXX | /geo/asn?addr=X.X.X.X',
211
267
  'city' => '/geo/city?id=XXX | /geo/city?addr=X.X.X.X',
212
268
  'country' => '/geo/country?id=XXX | /geo/country?addr=X.X.X.X'
213
269
  }
214
270
  }
271
+ info[:cache_dir] = GeoDB.geo_cache.dir if GeoDB.geo_cache
272
+ info
215
273
  end
216
274
 
217
275
  r.on 'geo' do
@@ -0,0 +1,309 @@
1
+ ---
2
+ AIGC:
3
+ ContentProducer: '001191110102MAD55U9H0F10002'
4
+ ContentPropagator: '001191110102MAD55U9H0F10002'
5
+ Label: '1'
6
+ ProduceID: 'aacf6d8c-3f16-44a6-ad5e-9dec92704459'
7
+ PropagateID: 'aacf6d8c-3f16-44a6-ad5e-9dec92704459'
8
+ ReservedCode1: 'c7410cd1-7a15-4cf5-b8f2-5c2ed30d33f1'
9
+ ReservedCode2: 'c7410cd1-7a15-4cf5-b8f2-5c2ed30d33f1'
10
+ ---
11
+
12
+ # GeoQuery 综合查询接口
13
+
14
+ > 模块目录:`service/geoquery/`
15
+ > 命令行入口:`bin/gen-get`(纯互联网)、`bin/ngeo-get`(缓存 + 本地 + 互联网综合)
16
+ > 设计蓝本:`ip_geo_lookup.py`(多级回退 + 缓存 + 归一化),按"叠加综合"需求重构为 Ruby 实现
17
+
18
+ ## 一、命令总览
19
+
20
+ | 命令 | 数据源 | 说明 |
21
+ |------|--------|------|
22
+ | `geo-get` | 本地 geo-api (GeoLite2) | 三接口齐查,纯本地;为 `GeoQuery::LocalClient` 的瘦封装(三接口拉取与汇总逻辑收口在 geoquery)|
23
+ | `gen-get` | 互联网 ip-api.com | 免费接口(限 45 req/min,无需 Key) |
24
+ | `ngeo-get` | 缓存 + 本地 + 互联网 | 按 `-p` 顺位逐源查询(默认 cache→local→internet),满意即停,字段级叠加 |
25
+
26
+ 三个查询命令共享同一套客户端实现(`GeoQuery::LocalClient` / `GeoQuery::OnlineClient` /
27
+ `GeoQuery::GeoCache`),无双实现。
28
+
29
+ ## 二、ngeo-get 查询模型(三源顺位)
30
+
31
+ ```
32
+ 1. 会话缓存 (ngeo-cache.json) 命中 → 直接返回
33
+ 2. 按 -p 顺位逐源查询 (默认 cache → local → internet):
34
+ cache GEO_CACHE 外带缓存目录 (geocacheYYYYMMDD.json, -a 指定)
35
+ local 本地 geo-api 服务 (GeoLite2)
36
+ internet 互联网 ip-api.com (限速 + 熔断保护)
37
+ 3. 每源结果按顺位折叠: 先查的源字段优先, 后查的补空缺
38
+ 4. 折叠后归属满意 (省/市/ASN 齐全) → 提前终止, 不再查后续源
39
+ 5. 全部源查完仍不满意 → 组装终态 (叠加/部分/空/不可达)
40
+ ```
41
+
42
+ ### 结果状态 (state) 可信度排序
43
+
44
+ **`unreachable` < `local-partial` < `cache-partial` < `empty` < `cache` < `local` < `online` < `merged`**
45
+
46
+ | state | 含义 | 触发条件 |
47
+ |-------|------|----------|
48
+ | `unreachable` | 各源均无结果且有不联通 (最低) | 本地无结果 + 互联网不可达 + 缓存未命中 |
49
+ | `local-partial` | 本地部分结果 | 本地归属不满意 + 其余源无补充,保留本地字段 |
50
+ | `cache-partial` | 缓存部分结果 | 缓存归属不满意 + 其余源无补充,保留缓存字段 |
51
+ | `empty` | 各源均确认无归属 | 如私有/保留地址 (ip-api 返回 `private range`) |
52
+ | `cache` | 缓存命中且满意 | GEO_CACHE 内记录省市 ASN 齐全 |
53
+ | `local` | 本地结果满意 | 省/市/ASN 齐全,无需互联网 |
54
+ | `online` | 纯互联网结果 | 本地无结果或 geo-api 未启动,互联网查询成功 |
55
+ | `merged` | 多源叠加综合 (最高) | 缓存/本地部分结果 + 互联网或其他源补全,字段级择优 |
56
+
57
+ ### 字段叠加规则 (Merge)
58
+
59
+ 重点准确字段:**省 (Province) / 城市 (City) / 用途 (IDC、云)**,越准确越好。
60
+ 按顺位折叠:先查的源为 base,后查的源为 supp。
61
+
62
+ | 字段 | 择优规则 |
63
+ |------|----------|
64
+ | country / province / city / asn / asn_org | base 优先,base 空取 supp (GeoLite2 网段级数据有值时可信;本地省市缺失率高,由 ip-api.com / GEO_CACHE 补全) |
65
+ | network (网段 CIDR) | base 优先,base 空取 supp (互联网源无网段,实际补给来自本地库与缓存) |
66
+ | isp | 按合并后的 asn_org 重新归一化 (长名 → 电信/联通/移动/腾讯云… 简称) |
67
+ | usage | 两方组织名综合推断: 云 / IDC / CDN / 教育网 / 运营商 / 企业 / 未知 |
68
+
69
+ ## 三、GEO_CACHE 外带缓存
70
+
71
+ ngeo-get 与 geo-api 均可外带一个缓存目录,目录内的定位信息作为缓存数据源。
72
+
73
+ ### 3.1 格式
74
+
75
+ ```sh
76
+ # 服务器端: GEO_CACHE 内的缓存定位信息作为 addr 查询兑底
77
+ geo-api -d GEODB_PATH -a GEO_CACHE
78
+
79
+ # 客户端: 查询时缓存目录数据纳入备选
80
+ ngeo-get X.X.X.X -a GEO_CACHE
81
+ ```
82
+
83
+ 缓存文件为 `geocacheYYYYMMDD.json`(XXXXXXXX 为 8 位日期时间标签),
84
+ 内容与 `ngeo-cache.json` 同构:`{ "ip" => { 统一 schema 结果, "ts" => 时间戳 } }`。
85
+ 目录内文件增删改自动感知,无需重启;多文件同 IP 冲突取 `ts` 最新。
86
+
87
+ ### 3.2 顺位 (-p)
88
+
89
+ `-p` 决定本地库 (local) / 互联网库 (internet) / 缓存库 (cache) 的适用顺序:
90
+
91
+ ```sh
92
+ ngeo-get X.X.X.X -a GEO_CACHE -p local,internet,cache # 本地优先, 互联网次之, 缓存兑底
93
+ ngeo-get X.X.X.X -a GEO_CACHE # 默认 -p cache,local,internet
94
+ ```
95
+
96
+ | 顺位 | 语义 |
97
+ |------|------|
98
+ | `cache,local,internet` (默认) | 优先缓存 → 本地库 → 互联网 |
99
+ | `local,internet,cache` | 本地优先,本地查不到才找互联网,互联网查不到或不通再查缓存 |
100
+
101
+ 顺位即优先级:先查的源字段优先,后查的补空缺;折叠后满意 (省/市/ASN 齐全)
102
+ 即停,不再查后续源。源不可用自动跳过(无 `-a` 时 cache 源不存在;
103
+ `--refresh` 跳过全部缓存;`--no-local` 跳过本地)。
104
+
105
+ 服务端 `geo-api` 的顺位参数为 `--priority`(`-p` 已被端口占用),
106
+ 仅支持 `local,cache` 排列,默认 `local,cache`。
107
+
108
+ ### 3.3 产出 (-c / -nc)
109
+
110
+ 每次查询的结果默认保存下来用于后续缓存:
111
+
112
+ ```sh
113
+ ngeo-get 1.2.3.4 -a GEO_CACHE # 查询结果默认保存到 GEO_CACHE/geocacheYYYYMMDD.json
114
+ ngeo-get 1.2.3.4 -a GEO_CACHE -nc # 不保存产出
115
+ ```
116
+
117
+ - 保存位置:`-a` 指定的目录;未指定时为当前目录(可用环境变量 `GEO_CACHE_DIR` 覆盖)
118
+ - 命名:`geocacheYYYYMMDD.json`,同一天多次查询自动归并到同一文件,同 IP 以新结果覆盖
119
+ - 仅保存可缓存状态 (`local` / `merged` / `online` / `empty` / `cache`);
120
+ `unreachable` / `local-partial` / `cache-partial` 不落盘(下次重查自动补全)
121
+ - `-c` 为默认行为(显式指定等效),`-nc` 关闭
122
+
123
+ ### 3.4 整理 (-m)
124
+
125
+ ```sh
126
+ ngeo-get -m GEO_CACHE
127
+ # 合并完成: 3 个缓存文件 → 128 条记录
128
+ # 输出文件: /path/GEO_CACHE/geocacheYYYYMMDD.json (生成时间为时间标签)
129
+ ```
130
+
131
+ 将 GEO_CACHE 目录下所有 `geocacheYYYYMMDD.json` 格式的数据合并,生成一个新的
132
+ `geocacheYYYYMMDD.json`(新生成时间为合并生成时间),同 IP 冲突取 `ts` 最新。
133
+ 源文件保留不删除,重复执行幂等。
134
+
135
+ ## 四、gen-get 接口与参数
136
+
137
+ 默认调用 ip-api.com 免费接口(与 `ip_geo_lookup.py` 的默认配置一致,`message` 字段为诊断扩展):
138
+
139
+ ```
140
+ http://ip-api.com/json/{ip}?lang=zh-CN&fields=status,message,country,regionName,city,isp,as,query
141
+ ```
142
+
143
+ | gen-get state | 触发条件 | 缓存 |
144
+ |---------------|----------|------|
145
+ | `ok` | `status: success` | 落盘 |
146
+ | `empty` | `status: fail` (如私有地址 `private range`) | 落盘 |
147
+ | `unreachable` | 网络异常 / 超时 / HTTP 429 限速 / 非 200 | 不落盘 (下次重试) |
148
+ | `invalid` | IP 参数不合法 | 不落盘 |
149
+
150
+ 内置限速:两次请求间隔 ≥ 1.4s(对齐 45 req/min 免费限额);`--timeout` 控制连接/读取超时(默认 10s)。
151
+
152
+ ## 五、命令行用法
153
+
154
+ ```sh
155
+ # gen-get — 互联网查询
156
+ gen-get 8.8.8.8 # 文字格式
157
+ gen-get 8.8.8.8 -j # JSON 格式
158
+ gen-get 1.1.1.1 8.8.8.8 # 多 IP (自动限速)
159
+ cat ips.txt | gen-get -j # stdin 逐行读 IP
160
+ gen-get 8.8.8.8 --refresh # 忽略缓存强刷
161
+ gen-get --cache-stats # 缓存统计
162
+
163
+ # ngeo-get — 综合查询 (三源顺位)
164
+ ngeo-get 111.8.44.6 # 默认顺位 cache,local,internet
165
+ ngeo-get 111.8.44.6 -j # JSON
166
+ ngeo-get 111.8.44.6 -a ./GEO_CACHE # 外带缓存目录
167
+ ngeo-get 111.8.44.6 -p local,internet,cache -a ./GEO_CACHE # 本地优先, 缓存兑底
168
+ ngeo-get 111.8.44.6 -nc # 查询但不产出缓存文件
169
+ ngeo-get -m ./GEO_CACHE # 合并目录下全部 geocache*.json
170
+ ngeo-get 111.8.44.6 --no-local # 跳过本地, 仅互联网
171
+ ngeo-get 111.8.44.6 --geoapi http://127.0.0.1:9292 # 覆盖本地服务地址
172
+ ngeo-get 111.8.44.6 --api "http://ip-api.com/json/{ip}?lang=zh-CN&fields=..." # 覆盖互联网接口
173
+ ngeo-get --cache-stats # 会话缓存统计 (-a 时附加 GEO_CACHE 统计)
174
+ ```
175
+
176
+ 退出码:`0` 查询完成(含 `empty` / `local-partial` / `cache-partial`);`1` IP 不合法或参数错误;`2` 互联网不可达(且其余源无结果)。
177
+
178
+ ## 六、结果 schema(ngeo-get -j)
179
+
180
+ ```json
181
+ {
182
+ "ip": "111.8.44.6",
183
+ "state": "merged",
184
+ "message": "",
185
+ "country": "中国",
186
+ "province": "湖南",
187
+ "city": "青园",
188
+ "isp": "移动",
189
+ "asn": "56047",
190
+ "asn_org": "China Mobile communications corporation",
191
+ "network": "111.8.0.0/15",
192
+ "usage": "运营商",
193
+ "source": "ngeo(geo-get+gen-get)",
194
+ "sources": {
195
+ "cache": { "state": "miss", "message": "GEO_CACHE 缓存未命中" },
196
+ "local": { "state": "ok", "verdict": "不满意" },
197
+ "online": { "state": "ok" }
198
+ },
199
+ "ts": 1789370023
200
+ }
201
+ ```
202
+
203
+ `sources` 键:`cache` / `local` / `online`,未参与查询的源标 `skipped`;
204
+ `source` 字段按参与叠加的源组合,如 `ngeo(geo-cache+geo-get)`。
205
+
206
+ ## 七、测试用例
207
+
208
+ ### 7.1 ngeo-get 状态机
209
+
210
+ | # | 命令 | 预期结果 |
211
+ |---|------|----------|
212
+ | 7.1.1 | `ngeo-get 219.140.0.1 -j` | `state: local`(本地省市 ASN 齐全,`online: skipped`) |
213
+ | 7.1.2 | `ngeo-get 111.8.44.6 -j` | `state: merged`,省/市来自互联网,`network` 保留本地网段 |
214
+ | 7.1.3 | `ngeo-get 219.140.0.1 --geoapi http://127.0.0.1:9999 -j` | `state: online`(本地不可用,纯互联网) |
215
+ | 7.1.4 | `ngeo-get 111.8.44.6 --api http://192.0.2.1/x/{ip} --timeout 2` | `state: local-partial`,message 含"互联网查询不可达",本地 ASN/网段保留 |
216
+ | 7.1.5 | `ngeo-get 10.20.30.40 --api http://192.0.2.1/x/{ip} --timeout 2 -j` | `state: unreachable`,全部字段为空,退出码 2 |
217
+ | 7.1.6 | `ngeo-get 10.20.30.40 -j` | `state: empty`(`private range`) |
218
+ | 7.1.7 | `ngeo-get 999.1.1.1 -j` | `state: invalid`,退出码 1 |
219
+ | 7.1.8 | 连续查询同一 IP 两次 | 第二次 `cached: true`(JSON)或"缓存命中"(文字) |
220
+
221
+ ### 7.2 gen-get
222
+
223
+ | # | 命令 | 预期结果 |
224
+ |---|------|----------|
225
+ | 7.2.1 | `gen-get 8.8.8.8` | `state: ok`,国家/省/市/ASN 齐全,`usage: 云` |
226
+ | 7.2.2 | `gen-get 10.20.30.40 -j` | `state: empty`,message 为 `private range` |
227
+ | 7.2.3 | `gen-get 8.8.8.8 --api http://192.0.2.1/x/{ip} --timeout 2` | `state: unreachable`,退出码 2 |
228
+ | 7.2.4 | `echo 8.8.4.4 \| gen-get -j` | stdin 输入,正常查询 |
229
+
230
+ ### 7.3 GEO_CACHE 外带缓存
231
+
232
+ 预置 `./GEO_CACHE/geocache20260101.json` 含某本地库没有的 IP(如 `203.0.113.7`)的完整定位记录。
233
+
234
+ | # | 命令 | 预期结果 |
235
+ |---|------|----------|
236
+ | 7.3.1 | `ngeo-get 203.0.113.7 -a ./GEO_CACHE -j` | `state: cache`,`cached: true`,`sources.local/online: skipped`(默认顺位缓存命中) |
237
+ | 7.3.2 | `ngeo-get 203.0.113.7 -p local,internet,cache -a ./GEO_CACHE --api http://192.0.2.1/x/{ip} --timeout 2 -j` | local 空 + internet 不通 + 缓存兑底,`state: cache` |
238
+ | 7.3.3 | `ngeo-get 203.0.113.7 -a ./GEO_CACHE -nc -j` | 查询正常,不修改产出文件(`-nc`) |
239
+ | 7.3.4 | `ngeo-get 1.2.3.4 -a ./GEO_CACHE -j` | 查询后 `./GEO_CACHE/geocacheYYYYMMDD.json` 生成,含本次结果 |
240
+ | 7.3.5 | `ngeo-get -m ./GEO_CACHE` | 合并目录内全部 geocache 文件,输出新文件,退出码 0 |
241
+ | 7.3.6 | `ngeo-get 1.2.3.4 -p bogus -j` | 报"无效顺位"退出码 1 |
242
+ | 7.3.7 | `geo-api -d ./geodb -a ./GEO_CACHE` 后 `curl "localhost:9292/geo/city?addr=203.0.113.7"` | 200,缓存兑底,`cached: true`(详见 GeoAPI.md 第 7 节) |
243
+
244
+ ## 八、缓存与配置
245
+
246
+ | 项 | 说明 |
247
+ |----|------|
248
+ | 会话缓存目录 | `ENV["NGEO_CACHE_DIR"]`,默认 `~/.network-infra-utility/` |
249
+ | 会话缓存文件 | `gen-cache.json`(gen-get 互联网层)/ `ngeo-cache.json`(ngeo 综合层) |
250
+ | 会话缓存策略 | `local` / `merged` / `online` / `empty` / `cache` 落盘;`unreachable` / `local-partial` / `cache-partial` 不落盘(下次重查,互联网恢复后自动补全);`--refresh` 强制重查 |
251
+ | GEO_CACHE 外带缓存 | `-a DIR` 指定;产出目录未指定时 `ENV["GEO_CACHE_DIR"]` > 当前目录 |
252
+ | GEO_CACHE 产出 | `-c` 默认开启:结果写入 `geocacheYYYYMMDD.json`;`-nc` 关闭 |
253
+ | GEO_CACHE 整理 | `-m DIR` 合并目录内全部缓存文件为新文件 |
254
+ | 本地服务地址 | `--geoapi` 或 `ENV["GEO_API_BASE"]`,默认 `http://127.0.0.1:9292` |
255
+ | 互联网接口 | `--api`(`{ip}` 占位符),默认 ip-api.com 免费接口 |
256
+ | 熔断 | 连续 3 次互联网不可达后,60s 内跳过在线查询直接返回 unreachable |
257
+
258
+ ## 九、代码级用法
259
+
260
+ ```ruby
261
+ require "geoquery/geoquery" # gem 安装后 (require_paths 含 service/)
262
+
263
+ # 纯互联网
264
+ online = GeoQuery::OnlineClient.new(timeout: 10)
265
+ r = online.lookup("8.8.8.8") # → state: ok/empty/unreachable/invalid
266
+
267
+ # 本地原始接口查询 (bin/geo-get 底座)
268
+ local = GeoQuery::LocalClient.new # base 默认 9292 (geo-get 另支持 ENV["GEO_API_BASE"])
269
+ raw = local.fetch_raw("8.8.8.8") # → { country: body|nil, city: body|nil, asn: body|nil }
270
+ raw = local.fetch_raw("8.8.8.8", endpoints: %i[asn]) # → 单接口子集
271
+ # fetch_raw 返回 :unreachable 表示 geo-api 服务不可达
272
+
273
+ # 本地归一化查询 (ngeo 内部同款)
274
+ r = local.lookup("8.8.8.8") # → 统一 schema, state: ok/empty/unavailable/invalid
275
+
276
+ # 综合 (三源顺位, 默认 cache,local,internet)
277
+ ngeo = GeoQuery::NGeo.new(geo_cache_dir: "./GEO_CACHE") # -a 外带缓存
278
+ r = ngeo.lookup("111.8.44.6") # → 统一 schema (见第六节)
279
+ ngeo.lookup("1.2.3.4", refresh: true, no_local: false)
280
+ ngeo.save_cache
281
+
282
+ # 顺位自定义 (等效 -p)
283
+ ngeo = GeoQuery::NGeo.new(geo_cache_dir: "./GEO_CACHE",
284
+ order: "local,internet,cache")
285
+
286
+ # GEO_CACHE 外带缓存 (service/geoquery/geo_cache.rb)
287
+ gc = GeoQuery::GeoCache.new("./GEO_CACHE")
288
+ gc.lookup("8.8.8.8") # → 缓存记录 (附加 cached: true) 或 nil
289
+ gc.put_batch([{ "ip" => "8.8.8.8", "state" => "merged", ... }]) # 产出
290
+ gc.merge! # → { "files" =>, "entries" =>, "output" => }
291
+ GeoQuery::GeoCache.parse_order("cache,local,internet") # → 顺位数组或 nil
292
+ GeoQuery::GeoCache.to_geolite(:city, hit) # 统一 schema → GeoLite2 风格 (服务端兑底)
293
+
294
+ # 归一化工具
295
+ GeoQuery::Normalize.isp_from_asn("CHINATELECOM Hubei province 5G network") # => "电信"
296
+ GeoQuery::Normalize.usage_from_asn("Tencent cloud") # => "云"
297
+ GeoQuery::Merge.fields(local_result, online_result) # => 字段叠加
298
+ ```
299
+
300
+ ## 备注
301
+
302
+ 1. **gen-get 的 `unreachable` 语义**:表示"互联网查询不可达"(网络不通/超时/限速),区别于 `empty`(互联网确认无归属,如私有地址)。不可达结果不缓存,恢复后重查即可。
303
+ 2. **`local-partial` / `cache-partial` 的取舍**:互联网不可达时,本地/缓存的部分结果(如 ASN/网段)仍有价值,予以保留并标注;完全无数据时才降级为 `unreachable` 空状态。
304
+ 3. **本地 geo-api 未启动**:ngeo-get 不视为错误(`local: unavailable`),自动走纯互联网路径,返回 `state: online`;geo-get 则直接报错并提示启动命令(可用 `GEO_API_BASE` 环境变量覆盖服务地址)。
305
+ 4. **ip-api.com 免费接口限制**:45 req/min、仅 HTTP(无 HTTPS)、仅限自用。批量场景已内置 1.4s 最小间隔;更高需求可 `--api` 切换付费批量接口(pro.ip-api.com)。
306
+ 5. **GEO_CACHE 与会话缓存的分工**:`ngeo-cache.json` 是进程自动维护的会话缓存(命中直接返回,不参与 `-p` 顺位);GEO_CACHE 是用户显式外带的缓存目录(作为顺位中的 `cache` 数据源,产出可拷贝/合并/供服务端兑底)。`--refresh` 两者均跳过。
307
+ 6. **服务端顺位参数为何是 `--priority`**:`geo-api -p` 已被监听端口占用,故服务端顺位用 `--priority`(仅 `local,cache` 排列);客户端 `ngeo-get -p` 无冲突,支持三源排列。
308
+
309
+ > AI生成