surfguard 0.1.2 → 0.1.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/README.md +3 -1
- data/lib/surfguard/version.rb +1 -1
- data/lib/surfguard.rb +45 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33a528c627b74ee9bcd275aed8487baa4e357203bb70054dc809d30bba392b90
|
|
4
|
+
data.tar.gz: 50367ef6f086af1bcecf4da7ea6af2154be1d1f681f20677976f3a6a39dc7fca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac217d4ef2ff066da26d12b2457d93e639faedde02261dcb7bd23a7c587ea875b3eb2a97044ec110cf40deca66e6a71934b1698c3ee7de331893080aeed4beb4
|
|
7
|
+
data.tar.gz: 946be8043a78695c841c9f694b4910d4300a6876836affbfc150b914d6e3cb1175500981a9a8afc8bdf1dc83c34a5988199dc198eee35423760843ec6046acba
|
data/README.md
CHANGED
|
@@ -97,7 +97,9 @@ local overlay rather than routed as ordinary public IP destinations.
|
|
|
97
97
|
**1. Numeric parsing and name resolution are both part of the policy.** Before asking DNS,
|
|
98
98
|
Surfguard asks the system numeric-host parser used by `Socket`/`Net::HTTP` whether the token is an
|
|
99
99
|
address. This recognizes non-canonical decimal, hexadecimal, octal, and shortened IPv4 forms. A
|
|
100
|
-
numeric token is classified directly and is never sent through DNS or a search domain.
|
|
100
|
+
numeric token is classified directly and is never sent through DNS or a search domain. Malformed
|
|
101
|
+
IPv4-shaped variants with empty dot labels or invalid zone/prefix suffixes are refused rather than
|
|
102
|
+
reinterpreted as DNS names.
|
|
101
103
|
|
|
102
104
|
Names resolve with `Resolv.getaddresses`, which uses Ruby's usual hosts-plus-DNS chain, honours
|
|
103
105
|
search domains, and returns every address. The obvious alternatives each drop something a guard
|
data/lib/surfguard/version.rb
CHANGED
data/lib/surfguard.rb
CHANGED
|
@@ -137,6 +137,11 @@ module Surfguard
|
|
|
137
137
|
# the NAT64 well-known prefix it is a fixed /96, so decode the low 32 bits.
|
|
138
138
|
IPV4_TRANSLATABLE = IPAddr.new("::ffff:0:0:0/96") # RFC 2765
|
|
139
139
|
|
|
140
|
+
# IPv4-compatible IPv6 is deprecated and must never be a DNS fetch target.
|
|
141
|
+
# Classify the prefix directly instead of calling IPAddr#ipv4_compat?, which
|
|
142
|
+
# has been obsolete since Ruby 2.5 and is expected to disappear in ipaddr 2.x.
|
|
143
|
+
IPV4_COMPATIBLE = IPAddr.new("::/96")
|
|
144
|
+
|
|
140
145
|
# Every PUBLIC address the host resolves to, IPv4 ahead of IPv6, DNS order
|
|
141
146
|
# preserved within each family so a provider's round-robin still spreads load.
|
|
142
147
|
# Empty when the host resolves but every address is blocked; raises
|
|
@@ -206,7 +211,7 @@ module Surfguard
|
|
|
206
211
|
|
|
207
212
|
# DNS never legitimately returns an IPv4 address embedded these two ways, so
|
|
208
213
|
# refuse them regardless of the address they wrap.
|
|
209
|
-
if ipaddr.ipv4_mapped? ||
|
|
214
|
+
if ipaddr.ipv4_mapped? || IPV4_COMPATIBLE.include?(ipaddr)
|
|
210
215
|
true
|
|
211
216
|
elsif ipaddr.ipv4?
|
|
212
217
|
disallowed_ipv4?(ipaddr)
|
|
@@ -250,6 +255,13 @@ module Surfguard
|
|
|
250
255
|
# here. IPAddr remains as a fallback for syntax it alone accepts, notably a
|
|
251
256
|
# full-width /32 or /128 host prefix. nil means the token is a name.
|
|
252
257
|
def numeric_literals(host)
|
|
258
|
+
# Refuse malformed IPv4-shaped text before consulting the platform parser.
|
|
259
|
+
# Some connection layers may accept decorations that others reject; none
|
|
260
|
+
# may turn them into a public DNS name after Surfguard classified them.
|
|
261
|
+
if malformed_numeric_host_candidate?(host)
|
|
262
|
+
raise InvalidHost, "malformed numeric-looking host #{host.inspect}"
|
|
263
|
+
end
|
|
264
|
+
|
|
253
265
|
Socket.getaddrinfo(
|
|
254
266
|
host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 0, Socket::AI_NUMERICHOST
|
|
255
267
|
).map { |address| IPAddr.new(address[3]) }.uniq
|
|
@@ -270,11 +282,42 @@ module Surfguard
|
|
|
270
282
|
text = host.to_s
|
|
271
283
|
return true if text.include?(":") # Invalid IPv6 syntax is never a DNS name.
|
|
272
284
|
|
|
273
|
-
|
|
285
|
+
legacy_ipv4_shape?(text)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def malformed_numeric_host_candidate?(host)
|
|
289
|
+
text = host.to_s
|
|
290
|
+
return false if text.include?(":") # IPv6 literals and zones go to AI_NUMERICHOST.
|
|
291
|
+
|
|
292
|
+
# Isolate the address: drop every leading separator, then keep only what
|
|
293
|
+
# precedes the next one. Removing a single separator would let a second one
|
|
294
|
+
# ("//127.0.0.1", "%127.0.0.1%lo") hide the legacy IPv4 shape, so the token
|
|
295
|
+
# would reach the platform parser and, failing there, be handed to DNS as a
|
|
296
|
+
# name — the parser/resolver identity gap this check exists to close.
|
|
297
|
+
core = text.sub(%r{\A[%/]+}, "")[%r{\A[^%/]*}]
|
|
298
|
+
labels = core.split(".", -1)
|
|
299
|
+
malformed = core != text || labels.any?(&:empty?)
|
|
300
|
+
return false unless malformed && legacy_ipv4_shape?(core)
|
|
301
|
+
|
|
302
|
+
# Full-width host prefixes are documented inputs and IPAddr parses them
|
|
303
|
+
# unambiguously. Shorter or otherwise invalid prefixes remain malformed.
|
|
304
|
+
return false if full_width_host_literal?(text)
|
|
305
|
+
|
|
306
|
+
true
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def legacy_ipv4_shape?(text)
|
|
310
|
+
parts = text.split(".", -1).reject(&:empty?)
|
|
274
311
|
(1..4).cover?(parts.length) &&
|
|
275
312
|
parts.all? { |part| part.match?(/\A(?:0[xX][0-9A-Fa-f]+|[0-9]+)\z/) }
|
|
276
313
|
end
|
|
277
314
|
|
|
315
|
+
def full_width_host_literal?(text)
|
|
316
|
+
host_address?(IPAddr.new(text))
|
|
317
|
+
rescue IPAddr::InvalidAddressError
|
|
318
|
+
false
|
|
319
|
+
end
|
|
320
|
+
|
|
278
321
|
# nil for anything IPAddr cannot parse. A shortened prefix is not a host and
|
|
279
322
|
# is rejected rather than normalized to its network address or sent to DNS.
|
|
280
323
|
def ip_literal(host)
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: surfguard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 37signals
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
10
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: 'Surfguard resolves a hostname to the public IP addresses it points at
|
|
13
13
|
and refuses anything that would reach an internal network: private, loopback, link-local
|