inet 0.1.1 → 0.3.0
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/CHANGELOG.md +11 -0
- data/LICENSE +1 -0
- data/README.md +55 -4
- data/lib/inet/dns_resolver.rb +108 -0
- data/lib/inet/domain_name.rb +32 -0
- data/lib/inet/ipaddr.rb +222 -0
- data/lib/inet/ipv6.rb +67 -0
- data/lib/inet/set.rb +205 -0
- data/lib/inet/version.rb +1 -1
- data/lib/inet.rb +7 -1
- metadata +81 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1b022216eda75fb5d2cf23f4b69b0e225069b7ec53b16400709fd91b7a804b2
|
|
4
|
+
data.tar.gz: c3d2eb967e3cb4a3abef18ba46e99c3fdbb69d1cf8c8f4dab534f3a3e4b5d25a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54fb104ff7fb73fe2291fe583fa1ad11b14bcff967e4d3b591e3993aed508685958bd6b69932065b0e9ec7607650f7f7f3b76b4e831cfc29473afa781f01af7c
|
|
7
|
+
data.tar.gz: 401fa85bb5d0461dd6646facd4313e539b3a722e7a95aef16054e6e29d05f7306d8e8b13cbbe8a9fce3cbddccb324f70e6643f93e78cdcdf2a73825273bf7f08
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
1. **域名与 DNS**:从 algosec-rb 迁入 `Inet::DomainName` 和 `Inet::DnsResolver`,提供完整域名校验、左侧通配匹配、A/CNAME 解析与 TTL 缓存。
|
|
6
|
+
2. **通用上下文**:解析器以 `context:` 隔离调用方缓存,默认使用 `Time.now`,不依赖 ActiveSupport;声明所需标准库 gem 依赖。
|
|
7
|
+
|
|
8
|
+
## 0.2.0
|
|
9
|
+
|
|
10
|
+
1. **地址与服务运算**:从 algosec-rb 迁入 IPv4、IPv6 地址规范化、范围解析、CIDR 拆分、端口及协议服务编码,公共入口为 `Inet::IPAddr`、`Inet::IPv6`。
|
|
11
|
+
2. **区间集合**:提供不可变 `Inet::Set`,支持合并、交集、差集、包含、重叠、关系判断和 Marshal 持久化;无需 ActiveSupport 或其他运行时依赖。
|
|
12
|
+
3. **验证**:补充边界、非法输入、集合运算对照测试和真实 gem 隔离安装验证。
|
|
13
|
+
|
|
3
14
|
## 0.1.1
|
|
4
15
|
|
|
5
16
|
1. **自动发布**:接通 RubyGems Trusted Publishing,由 GitHub Actions 发布通过全部检查的同一份 gem,并提供对应的 GitHub Release 和 SHA256 校验文件。
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://github.com/gatework/inet/actions/workflows/release.yml)
|
|
4
4
|
[](https://rubygems.org/gems/inet)
|
|
5
5
|
|
|
6
|
-
`inet`
|
|
6
|
+
`inet` 提供 IPv4、IPv6 地址与服务范围运算,域名匹配、带 TTL 缓存的 DNS 解析,以及不可变整数区间集合。加载时不发起网络请求或启动后台任务。
|
|
7
7
|
|
|
8
|
-
要求 Ruby 3.4
|
|
8
|
+
要求 Ruby 3.4 或更高版本,只依赖标准库 gem,采用 MIT 许可证。
|
|
9
9
|
|
|
10
10
|
## 安装与使用
|
|
11
11
|
|
|
@@ -16,15 +16,66 @@ gem install inet
|
|
|
16
16
|
也可以在 Gemfile 中添加:
|
|
17
17
|
|
|
18
18
|
```ruby
|
|
19
|
-
gem "inet", "~> 0.
|
|
19
|
+
gem "inet", "~> 0.3"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
```ruby
|
|
23
23
|
require "inet"
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Inet::IPAddr.host_cidr("192.0.2.9/24") # => "192.0.2.9/24"
|
|
26
|
+
Inet::IPAddr.network_cidr("192.0.2.9/24") # => "192.0.2.0/24"
|
|
27
|
+
Inet::IPAddr.cidrs(Inet::IPAddr.parse("192.0.2.0/24")) # => [["192.0.2.0", 24]]
|
|
28
|
+
Inet::IPv6.canonical("2001:0db8::9/64") # => "2001:db8::/64"
|
|
29
|
+
Inet::IPv6.canonical("2001:db8::9/64", host: true) # => "2001:db8::9/64"
|
|
30
|
+
|
|
31
|
+
allowed = Inet::Set.new(1..10) - Inet::Set.new(3..7)
|
|
32
|
+
allowed.ranges # => [1..2, 8..10]
|
|
33
|
+
allowed.cardinality # => 5
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 接口约定
|
|
37
|
+
|
|
38
|
+
- `Inet::IPAddr` 继承标准库 `IPAddr`,只接受 IPv4 字符串或整数;设备格式中的前导零按十进制处理,`any` 表示 `0.0.0.0`,全部 IPv4 地址使用 `any/0`。
|
|
39
|
+
- `host_cidr` 保留主机位,`network_cidr` 归一化网络地址;可传第二个参数指定前缀长度或连续网络掩码。`host` 返回主机文本,`to_i` / `from_i` 转换整数,`prefix_length` / `netmask` / `wildcard_mask` 转换掩码。
|
|
40
|
+
- `parse` 接受主机、CIDR、点分掩码或 `起点-终点`,返回 `Inet::Set`。IPv4 的范围端点自动排序;`between` 接受两个端点,`subnet` / `network` 接受地址及掩码。`cidrs` 接受合法 IPv4 整数集合并返回 `[地址, 前缀]` 数组;`format_range` 接受合法闭区间并返回单个 CIDR 或端点表达式。
|
|
41
|
+
- `ports` 接受端口整数、区间、单个端口文本或以连字符/空格分隔的范围,限制为 `0..65535`。`protocol_number` 接受名称、符号或 `0..255` 的编号;`icmp?` 判断 ICMP/ICMPv6。`service("tcp/80-90")` 返回服务整数集合,高八位为协议号,低十六位为端口;省略端口使用零,`any` 或协议 `0` 表示整个 24 位服务空间。
|
|
42
|
+
- `Inet::IPv6.address` 返回标准库 `IPAddr`;`canonical` 规范化主机或 CIDR,`parse` 返回整数集合,`normalize` 输出规范文本。范围必须按从小到大排列且不带前缀;拒绝带区域标识 `%` 的地址。`from_i` 转换 128 位非负整数,`ranges` 把合法 IPv6 整数集合转换为地址文本端点对。
|
|
43
|
+
- `Inet::Set.new(*ranges)` 接受非负整数区间,自动合并重叠及相邻区间;半开区间转换为闭区间,拒绝空或逆序区间。`each` 遍历区间,`size` 是区间段数,`cardinality` 是整数元素总数。空集合的 `min` / `max` 返回 `nil`。
|
|
44
|
+
- 集合支持 `|`、`&`、`-`、`include?(整数)`、`cover?(整数或集合)`、`subset?`、`overlap?`、`relation`。关系返回 `:equal`、`:contains`、`:within` 或 `:other`。集合和值数组冻结,`to_a` 返回数组副本;相等性、哈希和 Marshal 恢复保持值语义。
|
|
45
|
+
|
|
46
|
+
非法地址、超范围值和不支持的类型抛出 `ArgumentError`(包括标准库 `IPAddr` 的对应子类)。集合自身不区分 IPv4、IPv6、端口或服务,调用方须保持同一数值空间,避免跨类型混算。
|
|
47
|
+
可分别 `require "inet/ipaddr"`、`require "inet/ipv6"` 或 `require "inet/set"`。
|
|
48
|
+
|
|
49
|
+
这三个模块迁自 Gatework 的 algosec-rb,保留其公共方法与运算行为;最低 Ruby 版本自带的 `Range#overlap?` 替代原 ActiveSupport 加载。源代码采用 MIT 许可证,版权声明见 [LICENSE](LICENSE)。
|
|
50
|
+
|
|
51
|
+
## 域名与 DNS
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
Inet::DomainName.normalize("WWW.Example.test.") # => "www.example.test"
|
|
55
|
+
Inet::DomainName.exact?("example.test") # => true
|
|
56
|
+
Inet::DomainName.matches?("*.example.test", "www.example.test") # => true
|
|
57
|
+
|
|
58
|
+
resolver = Inet::DnsResolver.new(context: { "device_id" => 42 }, servers: ["192.0.2.53"])
|
|
59
|
+
answer = resolver.resolve("www.example.test") # 此时才查询 DNS
|
|
60
|
+
answer.fetch("status") # => "resolved"、"unresolved" 或 "unsupported"
|
|
61
|
+
answer.fetch("addresses") # => IPv4 地址数组
|
|
26
62
|
```
|
|
27
63
|
|
|
64
|
+
域名只接受至少两个 ASCII 标签的完整名称,单个尾点会被移除;不支持 IDNA 转码和搜索域。
|
|
65
|
+
通配仅支持 `*.example.test`,匹配一层或多层子域,不匹配根名称 `example.test`。
|
|
66
|
+
`require "inet/domain_name"` 和 `require "inet/dns_resolver"` 也可独立加载。
|
|
67
|
+
|
|
68
|
+
解析器只查询 A 记录,跟随最多 8 个 CNAME,使用整条链中最短的 TTL;每次等待为 1 秒。
|
|
69
|
+
`servers:` 最多指定三个 DNS 服务器,省略时使用系统 DNS。`context:` 为字符串键的 JSON 对象,
|
|
70
|
+
由解析器复制并冻结,供调用方隔离租户或设备缓存。返回的 `source` 保留上下文,追加
|
|
71
|
+
`source`(系统 DNS 为 `system`,显式 DNS 为 `device`)和 `servers`,这两个键由解析器管理。
|
|
72
|
+
|
|
73
|
+
返回值及嵌套内容不可变,包含 `status`、`addresses`、`resolved_at`、`expires_at`、`source`、`error`。
|
|
74
|
+
失败或没有 A 记录返回 `unresolved`,不支持的域名返回 `unsupported`,都缓存 15 秒;不会把失败当成成功的空集合。
|
|
75
|
+
可选 `store:` 接收实现 `read(key)` 与 `write(key, value, expires_in:)` 的缓存,实例内也按 TTL 复用结果。
|
|
76
|
+
`clock:` 默认 `Time.now`;测试可注入 `transport:`(接收域名及显式服务器数组,返回 `[地址数组, TTL秒数]`)
|
|
77
|
+
或 `dns_class:`。加载 gem 不发起 DNS 查询。
|
|
78
|
+
|
|
28
79
|
## 开发
|
|
29
80
|
|
|
30
81
|
```sh
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "resolv"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
require_relative "ipaddr"
|
|
8
|
+
require_relative "domain_name"
|
|
9
|
+
|
|
10
|
+
module Inet
|
|
11
|
+
# 按调用方上下文和 DNS 视角缓存有期限的证据;不把 DNS 失败解释为空地址。
|
|
12
|
+
class DnsResolver
|
|
13
|
+
FAILURE_TTL = 15
|
|
14
|
+
MAX_CNAME_DEPTH = 8
|
|
15
|
+
attr_reader :context
|
|
16
|
+
|
|
17
|
+
def initialize(context: {}, servers: nil, store: nil, clock: -> { Time.now }, transport: nil, dns_class: Resolv::DNS)
|
|
18
|
+
servers = Array(servers)
|
|
19
|
+
raise ArgumentError, "DNS servers must contain at most three IP addresses" if servers.size > 3
|
|
20
|
+
|
|
21
|
+
servers.each { |server| ::IPAddr.new(server) }
|
|
22
|
+
@servers = servers.map { |server| server.to_s.dup.freeze }.freeze
|
|
23
|
+
@store = store
|
|
24
|
+
@clock = clock
|
|
25
|
+
@transport = transport
|
|
26
|
+
@dns_class = dns_class
|
|
27
|
+
effective = if @servers.empty?
|
|
28
|
+
Resolv::DNS::Config.default_config_hash.fetch(:nameserver).map do |server|
|
|
29
|
+
server.dup.freeze
|
|
30
|
+
end.freeze
|
|
31
|
+
else
|
|
32
|
+
@servers
|
|
33
|
+
end
|
|
34
|
+
@context = immutable_answer(context.merge("source" => @servers.empty? ? "system" : "device",
|
|
35
|
+
"servers" => effective))
|
|
36
|
+
@scope = Digest::SHA256.hexdigest(JSON.generate(@context))
|
|
37
|
+
@answers = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 同次解析只查一次;跨次操作复用 TTL 内缓存,失败缓存不被视作成功。
|
|
41
|
+
def resolve(domain)
|
|
42
|
+
name = DomainName.normalize(domain)
|
|
43
|
+
key = ["inet", "dns-v1", @scope, name]
|
|
44
|
+
now = @clock.call
|
|
45
|
+
cached = @answers[name] || @store&.read(key)
|
|
46
|
+
return @answers[name] ||= immutable_answer(cached) if cached && Time.iso8601(cached.fetch("expires_at")) > now
|
|
47
|
+
|
|
48
|
+
answer = lookup(name, now)
|
|
49
|
+
@store&.write(key, answer, expires_in: [Time.iso8601(answer.fetch("expires_at")) - now, 0].max)
|
|
50
|
+
@answers[name] = answer
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# 缓存编码器不保留冻结状态;复制恢复每层 JSON 值,避免调用方改写 DNS 证据。
|
|
56
|
+
def immutable_answer(value)
|
|
57
|
+
case value
|
|
58
|
+
when Hash then value.to_h { |key, item| [key, immutable_answer(item)] }.freeze
|
|
59
|
+
when Array then value.map { |item| immutable_answer(item) }.freeze
|
|
60
|
+
else value.dup.freeze
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 仅接受完整域名;通配和单标签主机名不借助本机搜索域猜测。
|
|
65
|
+
def lookup(name, now)
|
|
66
|
+
return record("unsupported", [], FAILURE_TTL, now, "Unsupported domain expression") unless DomainName.exact?(name)
|
|
67
|
+
|
|
68
|
+
addresses, ttl = @transport ? @transport.call(name, @servers) : query(name)
|
|
69
|
+
addresses = addresses.map { |ip| Inet::IPAddr.from_i(Inet::IPAddr.to_i(ip)) }.uniq.sort
|
|
70
|
+
return record("unresolved", [], FAILURE_TTL, now, "No A records") if addresses.empty?
|
|
71
|
+
|
|
72
|
+
record("resolved", addresses, Integer(ttl), now)
|
|
73
|
+
rescue Resolv::ResolvError, Resolv::ResolvTimeout, IOError, SystemCallError, ArgumentError => error
|
|
74
|
+
record("unresolved", [], FAILURE_TTL, now, error.message)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# CNAME 与 A 的 TTL 共同决定有效期,限制跳数和每次等待,防止循环和长时间阻塞。
|
|
78
|
+
def query(name)
|
|
79
|
+
servers = context.fetch("servers")
|
|
80
|
+
@dns_class.open(nameserver: servers, search: [], ndots: 1, raise_timeout_errors: true) do |dns|
|
|
81
|
+
dns.timeouts = 1
|
|
82
|
+
visited = []
|
|
83
|
+
ttls = []
|
|
84
|
+
MAX_CNAME_DEPTH.times do
|
|
85
|
+
raise Resolv::ResolvError, "CNAME cycle" if visited.include?(name)
|
|
86
|
+
|
|
87
|
+
visited << name
|
|
88
|
+
aliases = dns.getresources("#{name}.", Resolv::DNS::Resource::IN::CNAME)
|
|
89
|
+
if aliases.empty?
|
|
90
|
+
records = dns.getresources("#{name}.", Resolv::DNS::Resource::IN::A)
|
|
91
|
+
ttls.concat(records.map(&:ttl))
|
|
92
|
+
return [records.map { |rr| rr.address.to_s }, ttls.min || 0]
|
|
93
|
+
end
|
|
94
|
+
ttls.concat(aliases.map(&:ttl))
|
|
95
|
+
name = aliases.first.name.to_s.downcase.delete_suffix(".")
|
|
96
|
+
end
|
|
97
|
+
raise Resolv::ResolvError, "CNAME chain exceeds #{MAX_CNAME_DEPTH}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# 使用普通 JSON 值保存证据,配置缓存不持有网络连接或解析器对象。
|
|
102
|
+
def record(status, addresses, ttl, now, error = nil)
|
|
103
|
+
{ "status" => status, "addresses" => addresses.map(&:freeze).freeze, "resolved_at" => now.iso8601(6).freeze,
|
|
104
|
+
"expires_at" => (now + [ttl, 0].max).iso8601(6).freeze, "source" => context,
|
|
105
|
+
"error" => error&.dup&.freeze }.freeze
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inet
|
|
4
|
+
# 域名表达式只支持完整名称或最左侧标签通配,不使用子串匹配。
|
|
5
|
+
module DomainName
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def normalize(value)
|
|
9
|
+
name = value.to_s.downcase
|
|
10
|
+
# 一个尾点表示根域;多个尾点是空标签,跨层重复归一化也不能把它变成合法名称。
|
|
11
|
+
name.end_with?("..") ? name : name.delete_suffix(".")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def exact?(value)
|
|
15
|
+
name = normalize(value)
|
|
16
|
+
name.bytesize <= 253 && name.include?(".") &&
|
|
17
|
+
name.split(".", -1).all? { |label| /\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/.match?(label) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def wildcard?(value)
|
|
21
|
+
name = normalize(value)
|
|
22
|
+
name.start_with?("*.") && exact?(name.delete_prefix("*."))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def matches?(expression, domain)
|
|
26
|
+
pattern, name = [expression, domain].map { |value| normalize(value) }
|
|
27
|
+
return false unless exact?(name)
|
|
28
|
+
|
|
29
|
+
wildcard?(pattern) ? name.end_with?(pattern.delete_prefix("*")) : exact?(pattern) && pattern == name
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/inet/ipaddr.rb
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require_relative "set"
|
|
5
|
+
|
|
6
|
+
module Inet
|
|
7
|
+
# 在标准 IPAddr 能力上补充防火墙所需的 IPv4 和服务范围运算。
|
|
8
|
+
class IPAddr < ::IPAddr
|
|
9
|
+
IPV4_MAX = ::IPAddr::IN4MASK
|
|
10
|
+
PORT_MAX = (1 << 16) - 1
|
|
11
|
+
SERVICE_MAX = (1 << 24) - 1
|
|
12
|
+
# 服务编码中反复使用的协议号集中命名,避免上层散落裸数字。
|
|
13
|
+
ANY_PROTOCOL = 0
|
|
14
|
+
ICMP = 1
|
|
15
|
+
ICMPV6 = 58
|
|
16
|
+
TCP = 6
|
|
17
|
+
UDP = 17
|
|
18
|
+
PROTOCOL_NUMBERS = { "tcp" => TCP, "udp" => UDP, "icmp" => ICMP, "icmpv6" => ICMPV6,
|
|
19
|
+
"ms-rpc-tcp" => TCP, "ms-rpc-udp" => UDP }.freeze
|
|
20
|
+
|
|
21
|
+
# 规范化 IPv4 输入,并拒绝 IPv6、非法前缀及非十进制设备写法。
|
|
22
|
+
def initialize(address = "0.0.0.0", family = Socket::AF_INET)
|
|
23
|
+
unless [Socket::AF_INET, Socket::AF_UNSPEC].include?(family)
|
|
24
|
+
raise ArgumentError, "Firewall IP helpers require IPv4"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if address.is_a?(String)
|
|
28
|
+
address = address.sub(%r{\Aany(?=/|\z)}i, "0.0.0.0")
|
|
29
|
+
unless %r{\A[0-9]+(?:\.[0-9]+){3}(?:/(?:[0-9]+|[0-9]+(?:\.[0-9]+){3}))?\z}.match?(address)
|
|
30
|
+
raise ArgumentError, "Invalid IPv4 expression: #{address.inspect}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 设备格式把带前导零的八位组和前缀都按十进制解释。
|
|
34
|
+
address = address.gsub(/\b0+(?=[0-9])/, "")
|
|
35
|
+
elsif !address.is_a?(Integer)
|
|
36
|
+
raise ArgumentError, "IPv4 input must be a string or integer"
|
|
37
|
+
end
|
|
38
|
+
super(address, Socket::AF_INET)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 把地址转换成包含首尾整数的标准集合,供范围运算复用。
|
|
42
|
+
def to_set
|
|
43
|
+
raise ArgumentError, "Firewall IP helpers require IPv4" unless ipv4?
|
|
44
|
+
|
|
45
|
+
range = to_range
|
|
46
|
+
Set.new(range.begin.to_i..range.end.to_i)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
# 接口地址保留主机位;不能直接用 IPAddr.new("host/prefix").to_s。
|
|
51
|
+
def host_cidr(expression, mask = nil)
|
|
52
|
+
host, prefix = string(expression, label: "Interface address").split("/", 2)
|
|
53
|
+
"#{from_i(to_i(host))}/#{prefix_length(mask || prefix || 32)}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 路由目的统一归一化为网络地址和前缀。
|
|
57
|
+
def network_cidr(expression, mask = nil)
|
|
58
|
+
address = new(host_cidr(expression, mask))
|
|
59
|
+
"#{address}/#{address.prefix}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 取接口 CIDR 中的主机地址,供 NAT 出口使用。
|
|
63
|
+
def host(expression) = host_cidr(expression).split("/", 2).first
|
|
64
|
+
|
|
65
|
+
# 把不带前缀的 IPv4 文本转换为整数。
|
|
66
|
+
def to_i(address)
|
|
67
|
+
unless address.is_a?(String) && !address.include?("/")
|
|
68
|
+
raise ArgumentError, "Invalid IPv4 address: #{address.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
new(address).to_i
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 把 IPv4 整数转换为点分十进制文本。
|
|
75
|
+
def from_i(number)
|
|
76
|
+
value = integer(number, max: IPV4_MAX, label: "IPv4 integer")
|
|
77
|
+
new(value).to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 把前缀长度或掩码转换为规范前缀长度。
|
|
81
|
+
def prefix_length(mask)
|
|
82
|
+
mask = integer(mask, max: 32, label: "Prefix length") unless mask.is_a?(String)
|
|
83
|
+
new("0.0.0.0/#{mask}").prefix
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# 根据前缀长度生成点分十进制网络掩码。
|
|
87
|
+
def netmask(mask) = new(IPV4_MAX).mask(prefix_length(mask)).to_s
|
|
88
|
+
|
|
89
|
+
# 把通配符掩码转换为网络掩码。
|
|
90
|
+
def wildcard_mask(wildcard)
|
|
91
|
+
unless wildcard.is_a?(String) && /\A[0-9]+(?:\.[0-9]+){3}\z/.match?(wildcard)
|
|
92
|
+
raise ArgumentError, "Invalid IPv4 wildcard"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
(~new(wildcard)).to_s
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 根据地址和掩码计算覆盖的整数地址集合。
|
|
99
|
+
def subnet(address, mask = 32)
|
|
100
|
+
new(to_i(address)).mask(prefix_length(mask.nil? ? 32 : mask)).to_set
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 返回子网的网络地址文本。
|
|
104
|
+
def network(address, mask = 32) = from_i(subnet(address, mask).min)
|
|
105
|
+
|
|
106
|
+
# 解析主机、CIDR 或网络掩码表达式,也支持两个 IPv4 端点。
|
|
107
|
+
def parse(expression)
|
|
108
|
+
text = string(expression, label: "IPv4 expression")
|
|
109
|
+
parts = text.split("-", -1)
|
|
110
|
+
return between(*parts) if parts.size == 2
|
|
111
|
+
raise ArgumentError, "Invalid IPv4 expression: #{text}" unless parts.size == 1
|
|
112
|
+
|
|
113
|
+
new(text).to_set
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# 用两个端点创建有序的闭区间。
|
|
117
|
+
def between(first, last)
|
|
118
|
+
low, high = [to_i(first), to_i(last)].minmax
|
|
119
|
+
Set.new(low..high)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# 将地址范围精确拆成 CIDR,供持久化与命令生成共用。
|
|
123
|
+
def cidrs(set)
|
|
124
|
+
set.flat_map do |range|
|
|
125
|
+
cursor = range.begin
|
|
126
|
+
last = range.end
|
|
127
|
+
blocks = []
|
|
128
|
+
while cursor <= last
|
|
129
|
+
size = 1
|
|
130
|
+
size *= 2 while (cursor % (size * 2)).zero? && cursor + (size * 2) - 1 <= last
|
|
131
|
+
blocks << [IPAddr.from_i(cursor), 32 - (size.bit_length - 1)]
|
|
132
|
+
cursor += size
|
|
133
|
+
end
|
|
134
|
+
blocks
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# 把整数区间压缩成最短的单个网段或端点表达式。
|
|
139
|
+
def format_range(range)
|
|
140
|
+
unless range.is_a?(Range) && !range.exclude_end? && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
141
|
+
raise ArgumentError, "IPv4 notation requires a closed integer range"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
integer(range.begin, max: IPV4_MAX, label: "IPv4 lower bound")
|
|
145
|
+
integer(range.end, max: IPV4_MAX, label: "IPv4 upper bound")
|
|
146
|
+
raise ArgumentError, "IPv4 range is reversed" if range.begin > range.end
|
|
147
|
+
|
|
148
|
+
size = range.end - range.begin + 1
|
|
149
|
+
first = from_i(range.begin)
|
|
150
|
+
if size.nobits?(size - 1) && (range.begin % size).zero?
|
|
151
|
+
"#{first}/#{32 - (size.bit_length - 1)}"
|
|
152
|
+
else
|
|
153
|
+
"#{first}-#{from_i(range.end)}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# 解析单个端口或端口范围,并限制在 16 位端口空间内。
|
|
158
|
+
def ports(expression)
|
|
159
|
+
if expression.is_a?(Range)
|
|
160
|
+
result = Set.new(expression)
|
|
161
|
+
raise ArgumentError, "Port must be in 0..#{PORT_MAX}" if result.max > PORT_MAX
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
end
|
|
165
|
+
text = expression.is_a?(Integer) ? expression.to_s : string(expression, label: "Port")
|
|
166
|
+
match = /\A\s*([0-9]+)(?:(?:\s*-\s*|\s+)([0-9]+))?\s*\z/.match(text)
|
|
167
|
+
raise ArgumentError, "Invalid port expression: #{text}" unless match
|
|
168
|
+
|
|
169
|
+
first, last = [match[1], match[2] || match[1]].map do |endpoint|
|
|
170
|
+
integer(endpoint, max: PORT_MAX, label: "Port")
|
|
171
|
+
end.minmax
|
|
172
|
+
Set.new(first..last)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# 把协议名称或编号转换成服务编码使用的协议编号。
|
|
176
|
+
def protocol_number(protocol)
|
|
177
|
+
protocol = protocol.to_s if protocol.is_a?(Integer) || protocol.is_a?(Symbol)
|
|
178
|
+
name = string(protocol, label: "Protocol").downcase
|
|
179
|
+
return ANY_PROTOCOL if name == "any"
|
|
180
|
+
|
|
181
|
+
PROTOCOL_NUMBERS.fetch(name) { integer(name, max: 255, label: "Protocol number") }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# 两种 ICMP 都以消息类型和代码匹配,不把类型限制当作普通目的端口。
|
|
185
|
+
def icmp?(protocol) = [ICMP, ICMPV6].include?(protocol_number(protocol))
|
|
186
|
+
|
|
187
|
+
# 服务编码的高八位保存协议,低十六位保存目标端口;省略端口时使用零。
|
|
188
|
+
def service(expression)
|
|
189
|
+
text = string(expression, label: "Service expression")
|
|
190
|
+
protocol, port, *unexpected_parts = text.split("/", -1)
|
|
191
|
+
raise ArgumentError, "Invalid service expression: #{text}" unless protocol && unexpected_parts.empty?
|
|
192
|
+
|
|
193
|
+
ranges = ports(port || 0)
|
|
194
|
+
return Set.new(0..SERVICE_MAX) if protocol.casecmp?(ANY_PROTOCOL.to_s) || protocol.casecmp?("any")
|
|
195
|
+
|
|
196
|
+
base = protocol_number(protocol) << 16
|
|
197
|
+
Set.new(*ranges.map { |range| (base + range.begin)..(base + range.end) })
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
# 校验并转换整数输入,同时限制其数值范围。
|
|
203
|
+
def integer(value, max:, label:)
|
|
204
|
+
unless value.is_a?(Integer) || (value.is_a?(String) && /\A[0-9]+\z/.match?(value))
|
|
205
|
+
raise ArgumentError, "#{label} must be an integer in 0..#{max}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
number = value.is_a?(Integer) ? value : Integer(value, 10)
|
|
209
|
+
raise ArgumentError, "#{label} must be in 0..#{max}" unless number.between?(0, max)
|
|
210
|
+
|
|
211
|
+
number
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# 校验文本输入并保留原字符串,便于上层继续解析原始表达式。
|
|
215
|
+
def string(value, label:)
|
|
216
|
+
raise ArgumentError, "#{label} must be a string" unless value.is_a?(String)
|
|
217
|
+
|
|
218
|
+
value
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
data/lib/inet/ipv6.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require_relative "set"
|
|
5
|
+
|
|
6
|
+
module Inet
|
|
7
|
+
# IPv6 范围独立于 IPv4;JSON 端点使用地址文本,避免客户端整数精度丢失。
|
|
8
|
+
module IPv6
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def address(value)
|
|
12
|
+
validate_text!(value)
|
|
13
|
+
result = ::IPAddr.new(value)
|
|
14
|
+
raise ArgumentError, "Expected IPv6" unless result.ipv6?
|
|
15
|
+
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def canonical(value, host: false)
|
|
20
|
+
ip = address(value)
|
|
21
|
+
return "#{address(value.split('/').first)}/#{ip.prefix}" if host
|
|
22
|
+
|
|
23
|
+
value.include?("/") ? "#{ip}/#{ip.prefix}" : ip.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse(value)
|
|
27
|
+
validate_text!(value)
|
|
28
|
+
if value.include?("-")
|
|
29
|
+
parts = value.split("-", -1)
|
|
30
|
+
unless parts.size == 2 && parts.none? { |part| part.include?("/") }
|
|
31
|
+
raise ArgumentError, "IPv6 range needs two hosts"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
first, last = parts.map { |part| address(part).to_i }
|
|
35
|
+
Set.new(first..last)
|
|
36
|
+
else
|
|
37
|
+
ip = address(value)
|
|
38
|
+
Set.new(ip.to_range.begin.to_i..ip.to_range.end.to_i)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize(value)
|
|
43
|
+
validate_text!(value)
|
|
44
|
+
return canonical(value) unless value.include?("-")
|
|
45
|
+
|
|
46
|
+
range = parse(value).ranges.first
|
|
47
|
+
"#{from_i(range.begin)}-#{from_i(range.end)}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def from_i(value)
|
|
51
|
+
unless value.is_a?(Integer) && value.between?(0, ::IPAddr::IN6MASK)
|
|
52
|
+
raise ArgumentError, "IPv6 integer must be in 0..#{::IPAddr::IN6MASK}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
::IPAddr.new(value, Socket::AF_INET6).to_s
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ranges(set) = set.map { |range| [from_i(range.begin), from_i(range.end)] }
|
|
59
|
+
|
|
60
|
+
def validate_text!(value)
|
|
61
|
+
return if value.is_a?(String) && value == value.strip && !value.include?("%")
|
|
62
|
+
|
|
63
|
+
raise ArgumentError, "Invalid IPv6 address"
|
|
64
|
+
end
|
|
65
|
+
private_class_method :validate_text!
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/inet/set.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inet
|
|
4
|
+
# 不变量是区间有序、互不相交且已合并;遍历区间而不是逐个枚举主机。
|
|
5
|
+
class Set
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :ranges, :hash
|
|
9
|
+
|
|
10
|
+
# 校验、排序并合并区间,然后冻结集合以保证值语义。
|
|
11
|
+
def initialize(*ranges)
|
|
12
|
+
ordered = ranges.map { |range| interval_endpoints(range) }.sort_by(&:first)
|
|
13
|
+
merged = []
|
|
14
|
+
ordered.each do |first, last|
|
|
15
|
+
if !merged.empty? && first <= merged.last[1] + 1
|
|
16
|
+
merged.last[1] = [merged.last[1], last].max
|
|
17
|
+
else
|
|
18
|
+
merged << [first, last]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
@ranges = merged.map { |first, last| first..last }.freeze
|
|
22
|
+
@hash = @ranges.hash
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# 遍历规范化后的区间;没有代码块时返回枚举器。
|
|
27
|
+
def each(&block)
|
|
28
|
+
return enum_for(__method__) unless block
|
|
29
|
+
|
|
30
|
+
ranges.each(&block)
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 判断集合是否没有区间。
|
|
35
|
+
def empty? = ranges.empty?
|
|
36
|
+
|
|
37
|
+
# 返回区间段数量,而不是地址或端口总数。
|
|
38
|
+
def size = ranges.size
|
|
39
|
+
|
|
40
|
+
# 返回最小端点;空集合返回 nil。
|
|
41
|
+
def min = ranges.first&.begin
|
|
42
|
+
|
|
43
|
+
# 返回最大端点;空集合返回 nil。
|
|
44
|
+
def max = ranges.last&.end
|
|
45
|
+
|
|
46
|
+
# 计算所有闭区间包含的元素总数。
|
|
47
|
+
def cardinality = sum { |range| range.end - range.begin + 1 }
|
|
48
|
+
|
|
49
|
+
# 仅按规范化后的区间比较集合内容。
|
|
50
|
+
def ==(other) = other.is_a?(Set) && ranges == other.ranges
|
|
51
|
+
|
|
52
|
+
alias eql? ==
|
|
53
|
+
|
|
54
|
+
# 计算并集;两个集合为空或相同对象时直接复用已有值。
|
|
55
|
+
def |(other)
|
|
56
|
+
validate_operand!(other)
|
|
57
|
+
return self if other.empty? || equal?(other)
|
|
58
|
+
return other if empty?
|
|
59
|
+
|
|
60
|
+
self.class.new(*ranges, *other.ranges)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 用双指针扫描两个有序集合,计算交集。
|
|
64
|
+
def &(other)
|
|
65
|
+
validate_operand!(other)
|
|
66
|
+
return self if equal?(other) || empty?
|
|
67
|
+
return other if other.empty?
|
|
68
|
+
|
|
69
|
+
result = []
|
|
70
|
+
left = right = 0
|
|
71
|
+
while left < size && right < other.size
|
|
72
|
+
a = ranges[left]
|
|
73
|
+
b = other.ranges[right]
|
|
74
|
+
first = [a.begin, b.begin].max
|
|
75
|
+
last = [a.end, b.end].min
|
|
76
|
+
result << (first..last) if first <= last
|
|
77
|
+
left += 1 if a.end <= b.end
|
|
78
|
+
right += 1 if b.end <= a.end
|
|
79
|
+
end
|
|
80
|
+
self.class.new(*result)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# 从当前集合中减去另一个集合,逐段截去重叠部分。
|
|
84
|
+
def -(other)
|
|
85
|
+
validate_operand!(other)
|
|
86
|
+
return self if empty? || other.empty?
|
|
87
|
+
return self.class.new if equal?(other)
|
|
88
|
+
|
|
89
|
+
result = []
|
|
90
|
+
right = 0
|
|
91
|
+
each do |range|
|
|
92
|
+
cursor = range.begin
|
|
93
|
+
right += 1 while right < other.size && other.ranges[right].end < cursor
|
|
94
|
+
result.concat(subtract_interval(range, other.ranges, right))
|
|
95
|
+
end
|
|
96
|
+
self.class.new(*result)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 判断整数是否落在任一规范化区间内。
|
|
100
|
+
def include?(number)
|
|
101
|
+
raise ArgumentError, "Membership requires an integer" unless number.is_a?(Integer)
|
|
102
|
+
|
|
103
|
+
candidate = ranges.bsearch { |range| range.end >= number }
|
|
104
|
+
candidate ? candidate.begin <= number : false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 判断集合是否被当前集合覆盖;空集合始终被覆盖。
|
|
108
|
+
def cover?(other)
|
|
109
|
+
return include?(other) if other.is_a?(Integer)
|
|
110
|
+
|
|
111
|
+
validate_operand!(other)
|
|
112
|
+
return covers_interval?(other.ranges.first) if other.size == 1
|
|
113
|
+
|
|
114
|
+
index = 0
|
|
115
|
+
other.each do |range|
|
|
116
|
+
index += 1 while index < size && ranges[index].end < range.begin
|
|
117
|
+
return false if index == size || ranges[index].begin > range.begin || ranges[index].end < range.end
|
|
118
|
+
end
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# 判断当前集合是否是另一个集合的子集。
|
|
123
|
+
def subset?(other)
|
|
124
|
+
validate_operand!(other)
|
|
125
|
+
other.cover?(self)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# 判断两个有序集合是否存在重叠区间。
|
|
129
|
+
def overlap?(other)
|
|
130
|
+
validate_operand!(other)
|
|
131
|
+
left = right = 0
|
|
132
|
+
while left < size && right < other.size
|
|
133
|
+
a = ranges[left]
|
|
134
|
+
b = other.ranges[right]
|
|
135
|
+
return true if a.overlap?(b)
|
|
136
|
+
|
|
137
|
+
a.end < b.end ? left += 1 : right += 1
|
|
138
|
+
end
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# 返回相等、包含、被包含或互不相同四种关系之一。
|
|
143
|
+
def relation(other)
|
|
144
|
+
validate_operand!(other)
|
|
145
|
+
return :equal if self == other
|
|
146
|
+
return :contains if cover?(other)
|
|
147
|
+
return :within if subset?(other)
|
|
148
|
+
|
|
149
|
+
:other
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# 返回区间数组的浅副本,避免调用方修改内部数组。
|
|
153
|
+
def to_a = ranges.dup
|
|
154
|
+
|
|
155
|
+
# 哈希值只在当前进程有效;持久化区间,并在读取进程中重新建立冻结集合。
|
|
156
|
+
def marshal_dump = ranges
|
|
157
|
+
|
|
158
|
+
# 从持久化区间恢复集合的不变量和冻结状态。
|
|
159
|
+
def marshal_load(ranges)
|
|
160
|
+
initialize(*ranges)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# 把合法区间统一为闭区间端点。
|
|
166
|
+
def interval_endpoints(range)
|
|
167
|
+
unless range.is_a?(Range) && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
168
|
+
raise ArgumentError, "Intervals must have integer endpoints"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
first = range.begin
|
|
172
|
+
last = range.end - (range.exclude_end? ? 1 : 0)
|
|
173
|
+
raise ArgumentError, "Intervals must be nonnegative and ordered" if first.negative? || first > last
|
|
174
|
+
|
|
175
|
+
[first, last]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# 对单个区间做差,调用方复用已找到的右侧起点。
|
|
179
|
+
def subtract_interval(range, removed_ranges, scan)
|
|
180
|
+
result = []
|
|
181
|
+
cursor = range.begin
|
|
182
|
+
while scan < removed_ranges.size && removed_ranges[scan].begin <= range.end
|
|
183
|
+
removed = removed_ranges[scan]
|
|
184
|
+
result << (cursor..(removed.begin - 1)) if removed.begin > cursor
|
|
185
|
+
cursor = [cursor, removed.end + 1].max
|
|
186
|
+
break if cursor > range.end
|
|
187
|
+
|
|
188
|
+
scan += 1
|
|
189
|
+
end
|
|
190
|
+
result << (cursor..range.end) if cursor <= range.end
|
|
191
|
+
result
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# 单区间包含检查使用二分查找。
|
|
195
|
+
def covers_interval?(target)
|
|
196
|
+
candidate = ranges.bsearch { |range| range.end >= target.end }
|
|
197
|
+
candidate ? candidate.begin <= target.begin : false
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# 统一校验集合运算的右值类型。
|
|
201
|
+
def validate_operand!(other)
|
|
202
|
+
raise ArgumentError, "Expected a Set" unless other.is_a?(Set)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
data/lib/inet/version.rb
CHANGED
data/lib/inet.rb
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "inet/version"
|
|
4
|
+
require_relative "inet/set"
|
|
5
|
+
require_relative "inet/ipaddr"
|
|
6
|
+
require_relative "inet/ipv6"
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
require_relative "inet/domain_name"
|
|
9
|
+
require_relative "inet/dns_resolver"
|
|
10
|
+
|
|
11
|
+
# IP 地址、域名、DNS 解析、服务编码与不可变整数区间集合的公共命名空间。
|
|
6
12
|
module Inet
|
|
7
13
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,86 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- inet contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
10
|
+
date: 2026-10-24 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: digest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ipaddr
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: json
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: resolv
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: time
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: Domain matching, cached DNS, IP address ranges, service encoding and
|
|
83
|
+
immutable interval sets.
|
|
13
84
|
executables: []
|
|
14
85
|
extensions: []
|
|
15
86
|
extra_rdoc_files: []
|
|
@@ -19,6 +90,11 @@ files:
|
|
|
19
90
|
- README.md
|
|
20
91
|
- docs/releasing.md
|
|
21
92
|
- lib/inet.rb
|
|
93
|
+
- lib/inet/dns_resolver.rb
|
|
94
|
+
- lib/inet/domain_name.rb
|
|
95
|
+
- lib/inet/ipaddr.rb
|
|
96
|
+
- lib/inet/ipv6.rb
|
|
97
|
+
- lib/inet/set.rb
|
|
22
98
|
- lib/inet/version.rb
|
|
23
99
|
homepage: https://github.com/gatework/inet
|
|
24
100
|
licenses:
|
|
@@ -45,5 +121,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
45
121
|
requirements: []
|
|
46
122
|
rubygems_version: 4.0.20
|
|
47
123
|
specification_version: 4
|
|
48
|
-
summary:
|
|
124
|
+
summary: IP addresses, domain names, DNS and immutable interval sets
|
|
49
125
|
test_files: []
|