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
@@ -0,0 +1,151 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "net/http"
6
+ require "uri"
7
+ require_relative "normalize"
8
+
9
+ module GeoQuery
10
+ # ------------------------------------------------------------------
11
+ # 本地查询客户端 — 向运行中的 geo-api 服务 (geo-get 的数据源) 查询
12
+ # ------------------------------------------------------------------
13
+ # 一次拉取 /geo/country /geo/city /geo/asn 三接口:
14
+ # - lookup 归一化为统一 schema (与 OnlineClient 同构, 供 ngeo 编排)
15
+ # - fetch_raw 返回各接口原始响应 (bin/geo-get 的底座, 保持其输出契约)
16
+ # geo-api 未启动不算错误, state 标记 unavailable, 由上层 (ngeo) 决定
17
+ # 是否回退互联网。
18
+ #
19
+ # 状态映射 (state):
20
+ # ok 三接口至少一个有数据
21
+ # empty 三接口均 404 (IP 不在本地库范围)
22
+ # unavailable geo-api 服务不可达 (未启动/端口不对)
23
+ # invalid IP 参数不合法
24
+ class LocalClient
25
+ DEFAULT_BASE = "http://127.0.0.1:9292"
26
+ TIMEOUT = 15
27
+ ENDPOINTS = %i[country city asn].freeze
28
+
29
+ attr_reader :base
30
+
31
+ def initialize(base: DEFAULT_BASE, timeout: TIMEOUT)
32
+ @base = base.chomp("/")
33
+ @timeout = timeout
34
+ end
35
+
36
+ # 返回统一 schema (与 OnlineClient 同构):
37
+ # { "ip", "state", "message", "country", "province", "city",
38
+ # "isp", "asn", "asn_org", "network", "usage", "source", "ts" }
39
+ def lookup(ip)
40
+ unless valid_ip?(ip)
41
+ return base(ip, "invalid", "IP 不合法")
42
+ end
43
+
44
+ raw = fetch_raw(ip)
45
+ return base(ip, "unavailable", "geo-api 服务不可达 (#@base)") if raw == :unreachable
46
+
47
+ if raw.values.compact.empty?
48
+ return base(ip, "empty", "本地库无该 IP 归属数据")
49
+ end
50
+
51
+ compose(ip, raw)
52
+ end
53
+
54
+ # 原始接口查询 (bin/geo-get 底座):
55
+ # endpoints: ENDPOINTS 的子集, 默认三接口齐查
56
+ # 返回:
57
+ # Hash { :country => body|nil, :city => body|nil, :asn => body|nil }
58
+ # (仅含请求的接口; 404/400/非 200 → nil)
59
+ # :unreachable geo-api 服务不可达 (连接失败/超时)
60
+ def fetch_raw(ip, endpoints: ENDPOINTS)
61
+ result = {}
62
+ endpoints.each do |ep|
63
+ r = fetch_endpoint(ep, ip.to_s)
64
+ return :unreachable if r == :unavailable
65
+ result[ep] = r.is_a?(Hash) ? r : nil
66
+ end
67
+ result
68
+ end
69
+
70
+ private
71
+
72
+ def base(ip, state, message = "")
73
+ {
74
+ "ip" => ip, "state" => state, "message" => message,
75
+ "country" => "", "province" => "", "city" => "",
76
+ "isp" => "", "asn" => "", "asn_org" => "", "network" => "",
77
+ "usage" => "", "source" => "geo-get(geo-api)",
78
+ }
79
+ end
80
+
81
+ # 单接口查询: 200 → body(Hash); 404 → nil; 400 → :bad;
82
+ # 连接失败 → :unavailable (调用方遇之立即终止, 不再请求其余接口)
83
+ def fetch_endpoint(endpoint, ip)
84
+ uri = URI("#{@base}/geo/#{endpoint}?addr=#{URI.encode_www_form_component(ip)}")
85
+ res = Net::HTTP.get_response(uri)
86
+ case res.code.to_i
87
+ when 200 then JSON.parse(res.body)
88
+ when 404 then nil
89
+ when 400 then :bad
90
+ else nil
91
+ end
92
+ rescue JSON::ParserError
93
+ nil
94
+ rescue => e
95
+ @last_error = "#{e.class}: #{e.message}"
96
+ :unavailable
97
+ end
98
+
99
+ # 三接口数据归一化为统一 schema (raw: fetch_raw 的结果)
100
+ def compose(ip, raw)
101
+ city = raw[:city]
102
+ country = raw[:country]
103
+ asn = raw[:asn]
104
+ g_city = city.is_a?(Hash) ? (city["geoname"] || {}) : {}
105
+ g_country = country.is_a?(Hash) ? (country["geoname"] ||
106
+ country["registered_country"] || {}) : {}
107
+ rc_city = city.is_a?(Hash) ? (city["registered_country"] || {}) : {}
108
+
109
+ country_name = g_city["country_name"].to_s
110
+ country_name = g_country["country_name"].to_s if country_name.empty?
111
+ country_name = rc_city["country_name"].to_s if country_name.empty?
112
+
113
+ province = g_city["subdivision_1_name"].to_s
114
+ city_name = g_city["city_namezh"].to_s
115
+ city_name = g_city["city_name"].to_s if city_name.empty?
116
+
117
+ asn_num = asn.is_a?(Hash) ? asn["autonomous_system_number"].to_s : ""
118
+ asn_org = asn.is_a?(Hash) ? asn["autonomous_system_organization"].to_s : ""
119
+
120
+ network = [city, country, asn].map { |r| r.is_a?(Hash) ? r["network"].to_s : "" }
121
+ .find { |n| !n.empty? }.to_s
122
+
123
+ {
124
+ "ip" => ip,
125
+ "state" => "ok",
126
+ "message" => "",
127
+ "country" => country_name,
128
+ "province" => province,
129
+ "city" => city_name,
130
+ "isp" => Normalize.isp_from_asn(asn_org),
131
+ "asn" => asn_num,
132
+ "asn_org" => asn_org,
133
+ "network" => network,
134
+ "usage" => Normalize.usage_from_asn(asn_org),
135
+ "source" => "geo-get(geo-api)",
136
+ "ts" => Time.now.to_i,
137
+ }
138
+ end
139
+
140
+ def valid_ip?(ip)
141
+ ip = ip.to_s.strip
142
+ return false if ip.empty?
143
+ if ip.include?(":")
144
+ ip.match?(/\A[0-9A-Fa-f:]+\z/) && ip.count(":") >= 2
145
+ else
146
+ ip.match?(/\A\d{1,3}(\.\d{1,3}){3}\z/) &&
147
+ ip.split(".").all? { |o| o.to_i < 256 }
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,52 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "normalize"
5
+
6
+ module GeoQuery
7
+ # ------------------------------------------------------------------
8
+ # 字段级合并 — 按顺位折叠: 先查的源 (base) 与后查的源 (supp) 结果叠加
9
+ # ------------------------------------------------------------------
10
+ # 综合两方准确字段得出最终结果, 重点字段: 省 (Province) / 城市 (City) /
11
+ # 用途 (IDC、云)。择优原则:
12
+ #
13
+ # country / province / city / asn / asn_org : base 优先, base 空取 supp
14
+ # (GeoLite2 网段级数据有值时可信度高; 实践中本地省市缺失率高,
15
+ # 由 ip-api.com / GEO_CACHE 补全 — 对应 ip_geo_lookup.py 的补全策略)
16
+ # network : base 优先, base 空取 supp (网段 CIDR 谁有取谁;
17
+ # 互联网源恒为空, 实际补给来自本地库与 GEO_CACHE 缓存)
18
+ # isp : 按合并后的 asn_org 重新归一化
19
+ # usage : 两方组织名综合推断, 取第一个识别出具体用途者 (base 优先)
20
+ module Merge
21
+ module_function
22
+
23
+ FIELDS = %w[country province city asn asn_org].freeze
24
+
25
+ # base / supp 为统一 schema 的 Hash (local/online 或任意两源折叠),
26
+ # 返回合并后的字段 Hash (不含 ip/state/message/source/sources, 由上层组装)
27
+ def fields(base, supp)
28
+ base ||= {}
29
+ supp ||= {}
30
+
31
+ merged = {}
32
+ FIELDS.each do |f|
33
+ merged[f] = nz(base[f]).empty? ? nz(supp[f]) : nz(base[f])
34
+ end
35
+ # 网段: base 优先, 空取 supp (互联网源无网段, 缓存/本地有)
36
+ merged["network"] = nz(base["network"]).empty? ? nz(supp["network"]) : nz(base["network"])
37
+
38
+ # 运营商: 用合并后的组织名重新归一化 (长名 → 简称)
39
+ merged["isp"] = Normalize.isp_from_asn(merged["asn_org"])
40
+
41
+ # 用途: 两方组织名综合推断
42
+ merged["usage"] = Normalize.usage_of(base["asn_org"], supp["asn_org"],
43
+ base["isp"], supp["isp"])
44
+
45
+ merged
46
+ end
47
+
48
+ def nz(v)
49
+ v.nil? ? "" : v.to_s
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,102 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # GeoQuery — IP 归属综合查询服务 (本地 geo-api + 互联网 ip-api.com)
5
+ #
6
+ # 命令行入口: bin/gen-get (纯互联网) / bin/ngeo-get (本地+互联网综合)
7
+ # 详见同目录 GeoQuery.md。
8
+ module GeoQuery
9
+ # ------------------------------------------------------------------
10
+ # 公共: IP 合法性校验 (含 ":" 按 IPv6, 否则按 IPv4)
11
+ # ------------------------------------------------------------------
12
+ module_function
13
+
14
+ def valid_ip?(ip)
15
+ ip = ip.to_s.strip
16
+ return false if ip.empty?
17
+ if ip.include?(":")
18
+ ip.match?(/\A[0-9A-Fa-f:]+\z/) && ip.count(":") >= 2
19
+ else
20
+ ip.match?(/\A\d{1,3}(\.\d{1,3}){3}\z/) &&
21
+ ip.split(".").all? { |o| o.to_i < 256 }
22
+ end
23
+ end
24
+
25
+ # ------------------------------------------------------------------
26
+ # 归一化: 运营商 / 用途
27
+ # ------------------------------------------------------------------
28
+ # 移植自 ip_geo_lookup.py 的 _isp_from_asn, 并扩展用途 (usage) 推断:
29
+ # usage 值域: 云 / IDC / CDN / 教育网 / 运营商 / 企业 / 未知
30
+ module Normalize
31
+ module_function
32
+
33
+ # ASN 组织名 → 运营商/机构简称 (中文优先; /x 允许多行书写)
34
+ ISP_RULES = [
35
+ # 三大运营商
36
+ [/chinanet|china[\s-]?telecom|chinatelecom|电信/ix, "电信"],
37
+ [/china[\s-]?unicom|unicom|联通/ix, "联通"],
38
+ [/china[\s-]?mobile|移动|(?:^|\b)cmcc\b/ix, "移动"],
39
+ [/china[\s-]?radio(?:\s|&)?tv|crtc|广电/ix, "广电"],
40
+ # 教育 / 鹏博士
41
+ [/cernet|教育/ix, "教育网"],
42
+ [/dr[\s.]?peng|鹏博士|great\s?wall\s?broadband/ix, "鹏博士"],
43
+ # 云厂商 (先于企业判定)
44
+ [/tencent/ix, "腾讯云"],
45
+ [/alibaba|aliyun/ix, "阿里云"],
46
+ [/huawei/ix, "华为云"],
47
+ [/baidu/ix, "百度云"],
48
+ [/jd[\s-]?cloud|jingdong|京东/ix, "京东云"],
49
+ [/volcengine|bytedance|字节|火山/ix, "火山引擎"],
50
+ [/ucloud/ix, "UCloud"],
51
+ [/kingsoft|金山/ix, "金山云"],
52
+ [/(?:^|\b)aws\b|amazon/ix, "AWS"],
53
+ [/microsoft|azure/ix, "Azure"],
54
+ [/(?:^|\b)gcp\b|google\s?cloud|google\s?llc|(?:^|\b)google\b/ix, "谷歌云"],
55
+ [/oracle/ix, "Oracle云"],
56
+ # CDN
57
+ [/cloudflare/ix, "Cloudflare"],
58
+ [/akamai/ix, "Akamai"],
59
+ [/fastly/ix, "Fastly"],
60
+ [/wangsu|网宿|chinacache/ix, "网宿CDN"],
61
+ ].freeze
62
+
63
+ # ASN 组织名 → 用途类别
64
+ # (正则加 /x: 多行书写时忽略模式空白, 行首缩进不参与匹配)
65
+ USAGE_RULES = [
66
+ [/tencent|alibaba|aliyun|huawei|baidu|jd[\s-]?cloud|jingdong|京东|
67
+ volcengine|bytedance|字节|火山|ucloud|kingsoft|金山|
68
+ (?:^|\b)aws\b|amazon|microsoft|azure|(?:^|\b)gcp\b|google|oracle/ix, "云"],
69
+ [/idc|data[\s-]?cent|数据中心/ix, "IDC"],
70
+ [/cloudflare|akamai|fastly|wangsu|网宿|chinacache|cdn/ix, "CDN"],
71
+ [/cernet|教育/ix, "教育网"],
72
+ [/chinanet|china[\s-]?telecom|chinatelecom|电信|china[\s-]?unicom|unicom|联通|
73
+ china[\s-]?mobile|(?:^|\b)cmcc\b|移动|china[\s-]?radio|crtc|广电|
74
+ dr[\s.]?peng|鹏博士/ix, "运营商"],
75
+ [/apple|microsoft|oracle|ibm|intel|cisco|ford|toyota|bank|inc\.?\z/ix, "企业"],
76
+ ].freeze
77
+
78
+ # 从 ASN 组织名推断运营商简称; 无法识别返回原文, 空输入返回空串。
79
+ def isp_from_asn(org)
80
+ return "" if org.to_s.empty?
81
+ ISP_RULES.each { |re, name| return name if org =~ re }
82
+ org.to_s
83
+ end
84
+
85
+ # 从 ASN 组织名推断用途类别 (云 / IDC / CDN / 教育网 / 运营商 / 企业 / 未知)
86
+ def usage_from_asn(org)
87
+ return "未知" if org.to_s.empty?
88
+ USAGE_RULES.each { |re, usage| return usage if org =~ re }
89
+ "未知"
90
+ end
91
+
92
+ # 综合推断: 依次尝试本地与互联网两方的组织名/ISP 描述,
93
+ # 取第一个能识别出非"未知"用途的结果 (本地优先, 组织名优先)。
94
+ def usage_of(*candidates)
95
+ candidates.each do |c|
96
+ usage = usage_from_asn(c.to_s)
97
+ return usage if usage != "未知"
98
+ end
99
+ "未知"
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,136 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "net/http"
6
+ require "uri"
7
+ require_relative "normalize"
8
+
9
+ module GeoQuery
10
+ # ------------------------------------------------------------------
11
+ # 互联网查询客户端 (gen-get 核心) — 免费 ip-api.com 接口
12
+ # ------------------------------------------------------------------
13
+ # 接口与参数沿用 ip_geo_lookup.py 的默认配置:
14
+ # http://ip-api.com/json/{ip}?lang=zh-CN&fields=status,message,country,regionName,city,isp,as,query
15
+ # (免费版无需 Key, 限 45 req/min; message 字段为诊断扩展, 不影响归属字段)
16
+ #
17
+ # 状态映射 (state):
18
+ # ok status:success 互联网查询成功
19
+ # empty status:fail 互联网确认无归属 (如私有/保留地址)
20
+ # unreachable 网络/HTTP 异常 互联网查询不可达 (不缓存, 下次重试)
21
+ # invalid IP 参数不合法
22
+ class OnlineClient
23
+ DEFAULT_API = "http://ip-api.com/json/{ip}?lang=zh-CN&fields=status,message,country,regionName,city,isp,as,query"
24
+ MIN_INTERVAL = 1.4 # 两次请求最小间隔 (秒), 对齐 45 req/min 免费限额
25
+
26
+ attr_reader :api
27
+
28
+ def initialize(api: DEFAULT_API, timeout: 10)
29
+ @api = api || DEFAULT_API
30
+ @timeout = timeout || 10
31
+ @lock = Mutex.new
32
+ @last_ts = 0.0
33
+ end
34
+
35
+ # 查询单个 IP, 返回统一 schema:
36
+ # { "ip", "state", "message", "country", "province", "city",
37
+ # "isp", "asn", "asn_org", "network", "usage", "source", "ts" }
38
+ def lookup(ip)
39
+ unless valid_ip?(ip)
40
+ return base(ip, "invalid", "IP 不合法")
41
+ end
42
+
43
+ raw = fetch(ip)
44
+ case raw[:state]
45
+ when :unreachable
46
+ base(ip, "unreachable", raw[:message]).merge("ts" => Time.now.to_i)
47
+ when :empty
48
+ base(ip, "empty", raw[:message]).merge("ts" => Time.now.to_i)
49
+ else
50
+ parse_success(ip, raw[:body])
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def base(ip, state, message = "")
57
+ {
58
+ "ip" => ip, "state" => state, "message" => message,
59
+ "country" => "", "province" => "", "city" => "",
60
+ "isp" => "", "asn" => "", "asn_org" => "", "network" => "",
61
+ "usage" => "", "source" => "gen-get(ip-api.com)",
62
+ }
63
+ end
64
+
65
+ # 发请求并做状态归类, 返回 {state: :ok/:empty/:unreachable, body:, message:}
66
+ def fetch(ip)
67
+ throttle!
68
+ uri = URI(@api.sub("{ip}", URI.encode_www_form_component(ip)))
69
+ res = Net::HTTP.start(uri.hostname, uri.port,
70
+ open_timeout: @timeout, read_timeout: @timeout) do |http|
71
+ http.get(uri.request_uri)
72
+ end
73
+ case res.code.to_i
74
+ when 200
75
+ body = JSON.parse(res.body)
76
+ if body["status"] == "success"
77
+ { state: :ok, body: body }
78
+ else
79
+ { state: :empty, message: body["message"].to_s }
80
+ end
81
+ when 429
82
+ { state: :unreachable, message: "ip-api.com 限速 (HTTP 429), 稍后重试" }
83
+ else
84
+ { state: :unreachable, message: "ip-api.com 返回 HTTP #{res.code}" }
85
+ end
86
+ rescue => e
87
+ { state: :unreachable, message: "#{e.class}: #{e.message}" }
88
+ end
89
+
90
+ # 全局最小间隔限速 (进程内), 多线程调用也保持 45 req/min 以内
91
+ def throttle!
92
+ @lock.synchronize do
93
+ wait = MIN_INTERVAL - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @last_ts)
94
+ sleep(wait) if wait > 0
95
+ @last_ts = Process.clock_gettime(Process::CLOCK_MONOTONIC)
96
+ end
97
+ end
98
+
99
+ def parse_success(ip, d)
100
+ # as 字段格式: "AS4134 Chinanet"
101
+ asn = ""
102
+ asn_org = d["as"].to_s
103
+ if (m = asn_org.match(/\AAS(\d+)\s+(.*)\z/))
104
+ asn = m[1]
105
+ asn_org = m[2]
106
+ end
107
+ isp_raw = [asn_org, d["isp"]].reject { |s| s.to_s.empty? }.first.to_s
108
+ {
109
+ "ip" => ip,
110
+ "state" => "ok",
111
+ "message" => "",
112
+ "country" => d["country"].to_s,
113
+ "province" => d["regionName"].to_s,
114
+ "city" => d["city"].to_s,
115
+ "isp" => Normalize.isp_from_asn(isp_raw),
116
+ "asn" => asn,
117
+ "asn_org" => asn_org,
118
+ "network" => "",
119
+ "usage" => Normalize.usage_from_asn(asn_org),
120
+ "source" => "gen-get(ip-api.com)",
121
+ "ts" => Time.now.to_i,
122
+ }
123
+ end
124
+
125
+ def valid_ip?(ip)
126
+ ip = ip.to_s.strip
127
+ return false if ip.empty?
128
+ if ip.include?(":")
129
+ ip.match?(/\A[0-9A-Fa-f:]+\z/) && ip.count(":") >= 2
130
+ else
131
+ ip.match?(/\A\d{1,3}(\.\d{1,3}){3}\z/) &&
132
+ ip.split(".").all? { |o| o.to_i < 256 }
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,140 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ =begin HELP
5
+ SimLab - 路由交换模拟仿真
6
+
7
+ = 概述
8
+
9
+ SimLab 是 network-infra-utility 的路由交换仿真子系统,
10
+ 位于 service/simlab/ 下。它将 support 层的路由/交换原子能力
11
+ 组合成可运行的虚拟网络,并提供场景加载、事件驱动、观测导出等功能。
12
+
13
+ = 架构分层
14
+
15
+ support/ ← 原子原理层(纯内存,无 IO)
16
+ basic/packet.rb ← EthernetFrame / IPPacket / ARP / ICMP
17
+ routing/
18
+ prefix.rb ← CIDR 前缀解析/比较
19
+ lpm_trie.rb ← 最长前缀匹配 Trie
20
+ rib.rb ← 路由信息库 RIB
21
+ static_route.rb ← 静态路由
22
+ rip.rb ← 距离矢量协议 RIP
23
+ ospf.rb ← 链路状态协议 OSPF
24
+ bgp.rb ← 路径矢量协议 BGP
25
+ switching/
26
+ mac_table.rb ← MAC 地址表
27
+ frame_forward.rb ← 帧转发决策
28
+ vlan.rb ← 802.1Q VLAN
29
+ stp.rb ← 生成树协议
30
+
31
+ service/simlab/ ← 仿真服务层(编排+IO)
32
+ lib/simlab.rb ← 组合根入口
33
+ lib/simlab_topology.rb ← 拓扑模型
34
+ lib/simlab_engine.rb ← 离散事件引擎
35
+ lib/simlab_router.rb ← 虚拟路由器
36
+ lib/simlab_switch.rb ← 虚拟交换机
37
+ lib/simlab_traffic.rb ← 流量注入
38
+ lib/simlab_observer.rb ← 观测导出
39
+ lib/simlab_scenario.rb ← YAML 场景加载
40
+ bin/simctl ← Thor CLI
41
+
42
+ = 分层纪律
43
+
44
+ 1. support 层:纯内存、无 IO、无 Time.now、无全局状态
45
+ 2. service 层:负责编排、生命周期、事件调度、文件/CLI
46
+ 3. 协议出口唯一:所有协议通过 RIB#add 安装路由
47
+ 4. 组件通过 tick(now) 被动推进,不自己计时
48
+ 5. device 间不直接互调,由 engine 按链路投递
49
+
50
+ = 快速开始
51
+
52
+ # 运行场景
53
+ ruby service/simlab/bin/simctl simulate example/simlab/ospf_basic.yml
54
+
55
+ # 导出 RIB
56
+ ruby service/simlab/bin/simctl dump --rib R1 example/simlab/ospf_basic.yml
57
+
58
+ # 路径追踪
59
+ ruby service/simlab/bin/simctl trace --from R1 --to 192.168.2.1 example/simlab/ospf_basic.yml
60
+
61
+ # 列出场景
62
+ ruby service/simlab/bin/simctl list example/simlab/ospf_basic.yml
63
+
64
+ = 场景 YAML 格式
65
+
66
+ ---
67
+ topology:
68
+ routers:
69
+ R1:
70
+ interfaces: {Gi0/0: 10.0.0.1/24}
71
+ ospf: {router_id: 1.1.1.1}
72
+ switches:
73
+ S1:
74
+ ports: [Gi0/1, Gi0/2]
75
+ access_ports: {Gi0/1: 10}
76
+ links:
77
+ - [R1:Gi0/0, R2:Gi0/0, {bandwidth: 100000, delay: 10}]
78
+ hosts:
79
+ H1: {ip: 192.168.1.2/24, gateway: 192.168.1.1}
80
+ protocols:
81
+ ospf:
82
+ neighbors:
83
+ - {router: R1, neighbor_rid: 2.2.2.2, port: Gi0/0, cost: 10}
84
+ simulation:
85
+ until: 100
86
+ protocol_interval: 10
87
+ traffic:
88
+ - {type: ping, from: R1, to: 192.168.2.1, at: 50}
89
+ - {type: link_down, device: R1, port: Gi0/0, at: 80}
90
+ expect:
91
+ - rib_converged: [R1, R2]
92
+
93
+ = 已实现功能清单
94
+
95
+ == Support 路由层
96
+ - [x] CIDR 前缀解析/规范化/包含比较
97
+ - [x] 最长前缀匹配 Trie (IPv4/IPv6)
98
+ - [x] RIB 路由信息库(多协议共存/优选排序)
99
+ - [x] 静态路由(增删/递归解析)
100
+ - [x] RIP 距离矢量(Bellman-Ford/水平分割/毒性反转/hold-down)
101
+ - [x] OSPF 链路状态(LSA 泛洪/Dijkstra SPF)
102
+ - [x] BGP 路径矢量(AS_PATH 防环/LOCAL_PREF/MED 优选/policy 钩子)
103
+
104
+ == Support 交换层
105
+ - [x] MAC 地址表(学习/老化/静态表项)
106
+ - [x] 帧转发决策(泛洪/转发/丢弃/hairpin 过滤/ACL 钩子)
107
+ - [x] 802.1Q VLAN(access/trunk 端口/native VLAN/白名单)
108
+ - [x] STP 生成树(BPDU/根桥选举/端口角色/状态机)
109
+
110
+ == Service 仿真层
111
+ - [x] 拓扑模型(节点/链路/邻接关系)
112
+ - [x] 离散事件引擎(虚拟时钟/事件队列/延迟事件)
113
+ - [x] 虚拟路由器(RIB+LPM+Static+RIP/OSPF/BGP 组合)
114
+ - [x] 虚拟交换机(MAC+Forward+VLAN+STP 流水线)
115
+ - [x] 流量注入(ping/链路故障/节点故障/协议交换)
116
+ - [x] 观测导出(RIB/MAC/trace/收敛检测)
117
+ - [x] YAML 场景加载器
118
+ - [x] Thor CLI (simctl)
119
+
120
+ = 扩展路线(文档已设计,待实现)
121
+
122
+ support/
123
+ routing/isis.rb ← IS-IS 链路状态 IGP
124
+ mpls/ ← MPLS 标签/LDP/RSVP-TE/VRF/L3VPN
125
+ segment/ ← SR/SRv6 SID
126
+ evpn/ ← EVPN NLRI/VXLAN
127
+ te/ ← TED/CSPF/TE Tunnel
128
+ flexe/ ← FlexE 时隙化 shim
129
+ vpdn/ ← L2TP/PPP/VPDN 会话
130
+ service/simlab/
131
+ capability.rb ← 设备能力声明
132
+ profile.rb ← 节点 profile
133
+ pipeline.rb ← 统一转发流水线
134
+ service_model.rb ← L3VPN/L2VPN/EVPN 业务实例
135
+ segment_policy.rb ← SR/SRv6 policy
136
+ telemetry.rb ← 额外观测
137
+ scenario_schema.rb ← YAML DSL 扩展
138
+ =end
139
+
140
+ > AI生成