inet 0.2.0 → 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 +5 -0
- data/README.md +31 -3
- data/lib/inet/dns_resolver.rb +108 -0
- data/lib/inet/domain_name.rb +32 -0
- data/lib/inet/version.rb +1 -1
- data/lib/inet.rb +4 -1
- metadata +78 -6
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,10 @@
|
|
|
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
|
+
|
|
3
8
|
## 0.2.0
|
|
4
9
|
|
|
5
10
|
1. **地址与服务运算**:从 algosec-rb 迁入 IPv4、IPv6 地址规范化、范围解析、CIDR 拆分、端口及协议服务编码,公共入口为 `Inet::IPAddr`、`Inet::IPv6`。
|
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` 提供 IPv4、IPv6
|
|
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,7 +16,7 @@ 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
|
|
@@ -48,6 +48,34 @@ allowed.cardinality # => 5
|
|
|
48
48
|
|
|
49
49
|
这三个模块迁自 Gatework 的 algosec-rb,保留其公共方法与运算行为;最低 Ruby 版本自带的 `Range#overlap?` 替代原 ActiveSupport 加载。源代码采用 MIT 许可证,版权声明见 [LICENSE](LICENSE)。
|
|
50
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 地址数组
|
|
62
|
+
```
|
|
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
|
+
|
|
51
79
|
## 开发
|
|
52
80
|
|
|
53
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/version.rb
CHANGED
data/lib/inet.rb
CHANGED
|
@@ -5,6 +5,9 @@ require_relative "inet/set"
|
|
|
5
5
|
require_relative "inet/ipaddr"
|
|
6
6
|
require_relative "inet/ipv6"
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
require_relative "inet/domain_name"
|
|
9
|
+
require_relative "inet/dns_resolver"
|
|
10
|
+
|
|
11
|
+
# IP 地址、域名、DNS 解析、服务编码与不可变整数区间集合的公共命名空间。
|
|
9
12
|
module Inet
|
|
10
13
|
end
|
metadata
CHANGED
|
@@ -1,16 +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
|
-
|
|
13
|
-
|
|
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.
|
|
14
84
|
executables: []
|
|
15
85
|
extensions: []
|
|
16
86
|
extra_rdoc_files: []
|
|
@@ -20,6 +90,8 @@ files:
|
|
|
20
90
|
- README.md
|
|
21
91
|
- docs/releasing.md
|
|
22
92
|
- lib/inet.rb
|
|
93
|
+
- lib/inet/dns_resolver.rb
|
|
94
|
+
- lib/inet/domain_name.rb
|
|
23
95
|
- lib/inet/ipaddr.rb
|
|
24
96
|
- lib/inet/ipv6.rb
|
|
25
97
|
- lib/inet/set.rb
|
|
@@ -49,5 +121,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
49
121
|
requirements: []
|
|
50
122
|
rubygems_version: 4.0.20
|
|
51
123
|
specification_version: 4
|
|
52
|
-
summary:
|
|
124
|
+
summary: IP addresses, domain names, DNS and immutable interval sets
|
|
53
125
|
test_files: []
|