network-infra-utility 0.5.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.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/CHANGELOG.md +31 -0
- data/GUIDE.md +201 -0
- data/README.md +1 -6
- data/Rakefile +1 -1
- data/bin/dns-query +4 -2
- data/bin/gen-get +144 -0
- data/bin/geo-api +40 -1
- data/bin/geo-get +18 -36
- data/bin/geo-update +429 -0
- data/bin/ngeo-get +253 -0
- data/bin/packbit +227 -0
- data/bin/probe +134 -0
- data/document/reference/DNS/346/212/200/346/234/257/346/226/207/346/241/243.md +912 -0
- data/network-infra-utility.gemspec +6 -4
- data/network.rb +19 -1
- data/service/geodb/GeoAPI.md +64 -0
- data/service/geodb/api.rb +77 -19
- data/service/geoquery/GeoQuery.md +309 -0
- data/service/geoquery/cache.rb +78 -0
- data/service/geoquery/geo_cache.rb +233 -0
- data/service/geoquery/geoquery.rb +327 -0
- data/service/geoquery/local.rb +151 -0
- data/service/geoquery/merge.rb +52 -0
- data/service/geoquery/normalize.rb +102 -0
- data/service/geoquery/online.rb +136 -0
- data/service/simlab/SimLab.md +140 -0
- data/service/simlab/bin/simctl +152 -0
- data/service/simlab/lib/simlab.rb +124 -0
- data/service/simlab/lib/simlab_engine.rb +146 -0
- data/service/simlab/lib/simlab_observer.rb +176 -0
- data/service/simlab/lib/simlab_router.rb +330 -0
- data/service/simlab/lib/simlab_scenario.rb +200 -0
- data/service/simlab/lib/simlab_switch.rb +182 -0
- data/service/simlab/lib/simlab_topology.rb +196 -0
- data/service/simlab/lib/simlab_traffic.rb +167 -0
- data/service/ssh/README.md +50 -37
- data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +14 -1
- data/support/basic/packet.rb +167 -0
- data/support/routing/bgp.rb +279 -0
- data/support/routing/lpm_trie.rb +145 -0
- data/support/routing/ospf.rb +243 -0
- data/support/routing/prefix.rb +155 -0
- data/support/routing/rib.rb +167 -0
- data/support/routing/rip.rb +223 -0
- data/support/routing/static_route.rb +102 -0
- data/support/switching/frame_forward.rb +87 -0
- data/support/switching/mac_table.rb +127 -0
- data/support/switching/stp.rb +230 -0
- data/support/switching/vlan.rb +155 -0
- data/tool/packbit/README.md +136 -0
- data/tool/packbit/config/five-tuple.yml +25 -0
- data/tool/packbit/config/flow.yml +17 -0
- data/tool/packbit/config/packbit.yml +41 -0
- data/tool/packbit/erl/packbit.erl +588 -0
- data/tool/probe/README.md +88 -0
- data/tool/probe/lib/probe.rb +54 -0
- data/tool/probe/lib/probe_base.rb +98 -0
- data/tool/probe/lib/probe_dns.rb +64 -0
- data/tool/probe/lib/probe_icmp.rb +71 -0
- data/tool/probe/lib/probe_ntp.rb +51 -0
- data/tool/probe/lib/probe_radius.rb +58 -0
- data/tool/probe/lib/probe_registry.rb +44 -0
- data/tool/probe/lib/probe_result.rb +75 -0
- data/tool/probe/lib/probe_snmp.rb +114 -0
- data/tool/probe/lib/probe_tcp.rb +82 -0
- data/tool/probe/lib/probe_version.rb +7 -0
- data/version.rb +1 -1
- metadata +78 -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生成
|