surfguard 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 743b735d0f0abcce2bcfa3660b7f658cceb1a760b90d970a86df76e91cb19622
4
- data.tar.gz: 866a867fbde519a8512ae93c4fb6d8051d01b7de1591210e7f40f071376860a0
3
+ metadata.gz: 2bbb6e8891a860c9c927fbcced35382c1ecdaf965bc4d3e41d56b06f94c5adf2
4
+ data.tar.gz: 0f309037fa0b37414dd964a9d15afdf0b3a68dbd706a90ee5f24660d8721dd4e
5
5
  SHA512:
6
- metadata.gz: c3c1b340115fa7bc1190bec41e76ae2171ec23dfab10855122ddc91c3efb74864cdeed7960c4ab6c8d60770f04f520712e36118bf5c39750486b69a9f735f310
7
- data.tar.gz: 587059deea1f701b3a8df14558a7b85c4dc24deb229a4fb61bdb02358c4120647c2e2624da498d4ce15658cbe5de77a1e394af50bf8acd8ac5f9ec080ee6cdef
6
+ metadata.gz: 8f12a95962cfa260c0fc1754d09c78b34c95e3db42dd35837b77b6532a030fd0562cf227d57be5b979c243832cfdd1c71612c495e6c305031f519050dd33d28f
7
+ data.tar.gz: 6f571b898b5c21214b12c27e2dcf6b8cec6b0fce2005cc48aa9b82753a49b4fd7f1b743abe20bb84141cf393f5c9a7e04d98efe9c57d7bb2692452d5ef0346a5
data/README.md CHANGED
@@ -7,19 +7,10 @@ One SSRF address policy for Ruby apps that fetch a URL someone else supplied.
7
7
  It consolidates several drifting in-house copies of this policy into one — copies that had grown
8
8
  four different ideas of what "internal" means, including one that decoded a NAT64 prefix whose length
9
9
  is not recoverable from the address. This gem is their union, decided once and tested against the
10
- full IPv4 × IPv6 range matrix.
10
+ IPv4 × IPv6 range matrix it enforces.
11
11
 
12
12
  ## Installation
13
13
 
14
- Not yet published to RubyGems — `v0.1.0` will be the first packaged release. Until it ships,
15
- install from source:
16
-
17
- ```ruby
18
- gem "surfguard", github: "basecamp/surfguard"
19
- ```
20
-
21
- Once `v0.1.0` is published:
22
-
23
14
  ```ruby
24
15
  gem "surfguard"
25
16
  ```
@@ -35,7 +26,7 @@ Surfguard.resolve_public_ips("feeds.example.com")
35
26
  # => ["93.184.216.34", "2606:2800:220:1:248:1893:25c8:1946"] (IPv4 first, blocked removed)
36
27
  # Iterate THIS list on failover; do not resolve again inside a retry loop.
37
28
 
38
- # Non-pinning caller (hands the hostname straight to Net::HTTP, which resolves again):
29
+ # Non-pinning preflight (hands the hostname to Net::HTTP, which resolves again):
39
30
  Surfguard.resolvable_public_ip?("https://feeds.example.com/atom") # => true only if EVERY address is public
40
31
  Surfguard.enforce_public_ip(url) # raises otherwise
41
32
 
@@ -46,6 +37,21 @@ Surfguard.resolve_public_ip(url) # => "93.184.216.34" or nil
46
37
  Surfguard.blocked_address?(IPAddr.new("169.254.169.254")) # => true
47
38
  ```
48
39
 
40
+ The non-pinning helpers conservatively require every address in their lookup to be public. They do
41
+ not bind the later connection to that answer, so a second lookup can still change underneath them.
42
+ Pinning is required when attacker-controlled DNS or DNS rebinding is in scope.
43
+
44
+ The direct address APIs accept endpoint addresses, not networks. A prefix shorter than `/32` or
45
+ `/128` is refused rather than normalized into a host: `blocked_address?("10.0.0.1/8")` is `true`,
46
+ and `resolve_public_ips("10.0.0.1/8")` is `[]`.
47
+
48
+ ### Caller responsibilities
49
+
50
+ Surfguard classifies an address; it is not an HTTP client. The URL helpers do not restrict schemes,
51
+ follow redirects, or make the request. Allow only the schemes your fetcher supports, and validate
52
+ and pin every redirect target just as you did the original target. A pinning client must connect to
53
+ the selected returned address while retaining the original hostname for HTTP Host and TLS identity.
54
+
49
55
  ## Refused vs. unresolvable
50
56
 
51
57
  "We refuse that address" and "the host didn't answer" are different answers, and
@@ -71,35 +77,55 @@ question it was asked and never raises; use `enforce_public_ip` or
71
77
  |---|---|
72
78
  | IPv4 private (10/8, 172.16/12, 192.168/16), loopback (127/8), link-local (169.254/16) | refuse |
73
79
  | CGNAT (100.64/10), benchmark (198.18/15), TEST-NETs, IETF (192.0.0/24), 6to4 relay anycast (192.88.99/24), multicast (224/4), reserved (240/4), "this" (0/8) | refuse |
74
- | IPv6 ULA (fc00::/7, incl. IMDSv6 `fd00:ec2::254`), loopback (::1), link-local (fe80::/10), site-local (fec0::/10), multicast (ff00::/8), unspecified (::), discard (100::/64), Teredo (2001::/32), docs (2001:db8::/32), benchmark (2001:2::/48) | refuse |
80
+ | Azure WireServer `168.63.129.16/32` | refuse a fixed Azure platform/fabric alias, not an RFC special-use range |
81
+ | IPv6 ULA (fc00::/7, incl. IMDSv6 `fd00:ec2::254`), loopback (::1), link-local (fe80::/10), site-local (fec0::/10), multicast (ff00::/8), unspecified (::), discard/dummy (100::/64, 100:0:0:1::/64), documentation (2001:db8::/32, 3fff::/20), SRv6 SID (5f00::/16) | refuse |
82
+ | IETF protocol assignments (2001::/23) | refuse by default, including Teredo, benchmark, PCP/TURN/DNS-SD anycast to nearby infrastructure, deprecated ORCHID, ORCHIDv2, DET, and unallocated space; allow only AMT (2001:3::/32) and AS112-v6 (2001:4:112::/48) |
75
83
  | IPv4-mapped `::ffff:0:0/96`, IPv4-compatible `::/96` | refuse outright |
76
84
  | **SIIT `::ffff:0:0:0/96`** | decode embedded IPv4 (low 32 bits), re-check |
77
85
  | NAT64 well-known `64:ff9b::/96` | decode embedded IPv4 (low 32 bits), re-check |
78
86
  | **RFC 8215 NAT64 local-use `64:ff9b:1::/48`** | **refuse outright** — the Pref64 length is not recoverable from the address (RFC 6052 §2.2), so a low-32-bit decode reads the wrong octets; and the block is never globally routed |
79
87
  | 6to4 `2002::/16` | refuse (a 6to4 address is just an IPv4 address in disguise) |
80
88
 
81
- ## Two things worth knowing
89
+ This is an SSRF address policy, not a label-only copy of the IANA special-purpose registry. It
90
+ refuses internal, non-destination, non-global domain, and overlay-identifier ranges plus transition
91
+ encodings that can conceal a blocked target. Actual globally reachable IP-layer services such as
92
+ AMT and AS112 remain valid; ORCHID and DET identifiers do not, because they can be interpreted by a
93
+ local overlay rather than routed as ordinary public IP destinations.
82
94
 
83
- **1. The resolver is part of the policy.** Surfguard resolves with `Resolv.getaddresses`, which
84
- honours `/etc/hosts` and search domains **and** returns every address. The obvious alternatives each
85
- drop something a guard can't afford to lose:
95
+ ## Three things worth knowing
96
+
97
+ **1. Numeric parsing and name resolution are both part of the policy.** Before asking DNS,
98
+ Surfguard asks the system numeric-host parser used by `Socket`/`Net::HTTP` whether the token is an
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.
101
+
102
+ Names resolve with `Resolv.getaddresses`, which uses Ruby's usual hosts-plus-DNS chain, honours
103
+ search domains, and returns every address. The obvious alternatives each drop something a guard
104
+ can't afford to lose:
86
105
 
87
106
  - `Resolv.getaddress` honours `/etc/hosts` but returns only the **first** address — so an AAAA-only
88
107
  host deterministically takes the IPv6 path, and a multi-homed host is validated on one address
89
108
  while the connection may use another.
90
- - `Resolv::DNS.open` returns every address but **ignores** `/etc/hosts` and search domains so it
91
- validates a different set than `Net::HTTP` (which resolves through `getaddrinfo`) will actually
92
- connect to. Demonstrable: `Resolv.getaddresses("localhost")` → `["::1","127.0.0.1"]` while
93
- `Resolv::DNS.open`'s `each_address("localhost")` → `["::","0.0.0.0"]`.
109
+ - `Resolv::DNS.open` returns every DNS address but **ignores** `/etc/hosts` and the other parts of
110
+ the default resolver chain.
94
111
 
95
- `getaddresses` is both complete (every address) and faithful (the same chain the connection layer
96
- uses).
112
+ Ruby `Resolv` is not a universal substitute for the system `getaddrinfo` name-service chain. A host
113
+ with custom NSS sources such as mDNS or LDAP can produce a different answer at connection time.
114
+ Pinning a returned address avoids that second name lookup; non-pinning deployments need a resolver
115
+ configuration in which Ruby's hosts-plus-DNS answers match the connection layer, or an independent
116
+ egress control. Resolver-chain equivalence does not eliminate the separate DNS-rebinding window.
97
117
 
98
118
  **2. A resolver-level "no AAAA" switch is not a mitigation.** Disabling AAAA at the system resolver
99
119
  (for example Kamal's `dns-opt: no-aaaa`) is a glibc `getaddrinfo` option. Surfguard resolves through
100
120
  pure-Ruby `Resolv`, which requests AAAA regardless, so IPv6 answers still reach it. Don't treat that
101
121
  deploy setting as if it narrowed Surfguard's input.
102
122
 
123
+ **3. Custom DNS64 prefixes need their own enforcement.** Surfguard can decode and re-check the
124
+ fixed NAT64 well-known prefix `64:ff9b::/96`, and it refuses the RFC 8215 local-use block outright.
125
+ It cannot infer the embedded IPv4 address in an arbitrary network-specific Pref64 from the
126
+ synthesized IPv6 address alone. A deployment using DNS64 with another Pref64 must enforce the same
127
+ blocked-address policy at its DNS64/NAT64 or egress layer.
128
+
103
129
  ## Testing
104
130
 
105
131
  ```bash
@@ -116,5 +142,5 @@ issue tracker.
116
142
  ## Status
117
143
 
118
144
  Extracted and consolidated from several in-house SSRF guards, tested against the full
119
- IPv4 × IPv6 special-use matrix. Resolve-and-classify only; callers pin. See the
145
+ policy matrix. Resolve-and-classify only; callers pin. See the
120
146
  [releases page](https://github.com/basecamp/surfguard/releases) for versions and changes.
data/SECURITY.md CHANGED
@@ -23,16 +23,20 @@ Anything that lets a blocked address through, for example:
23
23
  - An address in a blocked range that Surfguard classifies as public.
24
24
  - A resolution path (encoding, embedding, transition mechanism) that reaches a blocked address
25
25
  despite validation.
26
- - A discrepancy between what Surfguard validates and what a caller following the documented
27
- pinning contract would connect to.
26
+ - A parser or resolver discrepancy that lets a caller following a documented API contract connect
27
+ to a different address than Surfguard classified.
28
+
29
+ Surfguard does not perform the fetch itself. Scheme restrictions, redirect handling, connection
30
+ pinning, custom DNS64 prefixes, and nonstandard NSS sources have caller or deployment requirements
31
+ documented in the README. Those boundaries by themselves are not classification bugs, but a bypass
32
+ when the documented contract is followed qualifies.
28
33
 
29
34
  Non-security bugs and questions belong in the
30
35
  [regular issue tracker](https://github.com/basecamp/surfguard/issues).
31
36
 
32
37
  ## Supported versions
33
38
 
34
- No packaged release exists yet, so `main` is the supported version. Once releases ship, it
35
- becomes the latest released version. Fixes ship as a new release, not backports.
39
+ The latest released version. Fixes ship as a new release, not backports.
36
40
 
37
41
  ## What to expect
38
42
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Surfguard
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/surfguard.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "ipaddr"
4
4
  require "resolv"
5
+ require "socket"
5
6
  require "uri"
6
7
 
7
8
  require_relative "surfguard/version"
@@ -15,13 +16,16 @@ require_relative "surfguard/version"
15
16
  # Two policy decisions the copies drifted on, settled here and explained where
16
17
  # they live in the code:
17
18
  #
18
- # * Resolver (see .resolve_public_ips): resolve with Resolv.getaddresses, which
19
- # honours /etc/hosts and search domains AND returns every address. One copy
20
- # used Resolv.getaddress (honours hosts, but only the FIRST address, so an
21
- # AAAA-only host deterministically selected the IPv6 path); another used
22
- # Resolv::DNS.open (all addresses, but ignores /etc/hosts, so it validated a
23
- # different set than Net::HTTP would actually connect to). getaddresses is
24
- # both complete and faithful to what the connection layer resolves.
19
+ # * Resolver (see .resolve_public_ips): first use getaddrinfo's numeric-only
20
+ # parser so legacy decimal, hex, octal, and short IPv4 forms mean exactly
21
+ # what they do to Net::HTTP; never send those tokens to DNS. Names resolve
22
+ # with Resolv.getaddresses, which honours /etc/hosts and search domains AND
23
+ # returns every address. One copy used Resolv.getaddress (honours hosts, but
24
+ # only the FIRST address, so an AAAA-only host deterministically selected the
25
+ # IPv6 path); another used Resolv::DNS.open (all addresses, but ignores
26
+ # /etc/hosts, so it validated a different set than Net::HTTP would connect
27
+ # to). getaddresses is complete for Ruby's hosts-plus-DNS chain; system
28
+ # getaddrinfo may additionally consult NSS sources such as mDNS or LDAP.
25
29
  #
26
30
  # * `no-aaaa` in Kamal `dns-opt` is NOT a mitigation. It is a glibc
27
31
  # getaddrinfo option; every guard here resolves through pure-Ruby Resolv,
@@ -39,6 +43,12 @@ require_relative "surfguard/version"
39
43
  module Surfguard
40
44
  class Violation < StandardError; end
41
45
 
46
+ # Internal signal for something IPAddr can parse, but which is a network
47
+ # rather than a host. Keep it separate from a lookup failure: callers must
48
+ # refuse these inputs without asking DNS to reinterpret them as names.
49
+ class InvalidHost < ArgumentError; end
50
+ private_constant :InvalidHost
51
+
42
52
  # The host answered with no address at all. Deliberately NOT a Violation:
43
53
  # nothing was refused here, the lookup simply came back empty, which is
44
54
  # usually a transient upstream failure. A caller that gives up permanently on
@@ -49,15 +59,17 @@ module Surfguard
49
59
 
50
60
  extend self
51
61
 
52
- # IPv4 special-use ranges that must never be a fetch target (RFC 5735/6890,
53
- # plus CGNAT and benchmarking). RFC1918 / loopback / link-local are also
54
- # covered by the IPAddr predicates in #disallowed_ipv4?; they are restated here
55
- # so the policy is complete and auditable in one place.
62
+ # IPv4 special-use ranges and exact platform aliases that must never be a
63
+ # fetch target (RFC 5735/6890, plus CGNAT and benchmarking). RFC1918 /
64
+ # loopback / link-local are also covered by the IPAddr predicates in
65
+ # #disallowed_ipv4?; they are restated here so the policy is complete and
66
+ # auditable in one place.
56
67
  DISALLOWED_IPV4 = [
57
68
  IPAddr.new("0.0.0.0/8"), # "This" network (RFC 1122)
58
69
  IPAddr.new("10.0.0.0/8"), # Private (RFC 1918)
59
70
  IPAddr.new("100.64.0.0/10"), # Carrier-grade NAT (RFC 6598)
60
71
  IPAddr.new("127.0.0.0/8"), # Loopback (RFC 1122)
72
+ IPAddr.new("168.63.129.16/32"), # Azure host-node WireServer virtual IP
61
73
  IPAddr.new("169.254.0.0/16"), # Link-local (RFC 3927) — includes the cloud metadata endpoint
62
74
  IPAddr.new("172.16.0.0/12"), # Private (RFC 1918)
63
75
  IPAddr.new("192.0.0.0/24"), # IETF protocol assignments (RFC 6890)
@@ -78,14 +90,31 @@ module Surfguard
78
90
  DISALLOWED_IPV6 = [
79
91
  IPAddr.new("::/128"), # Unspecified (RFC 4291)
80
92
  IPAddr.new("100::/64"), # Discard-only (RFC 6666)
93
+ IPAddr.new("100:0:0:1::/64"), # Dummy IPv6 destination (RFC 9780)
81
94
  IPAddr.new("2001::/32"), # Teredo (RFC 4380)
82
95
  IPAddr.new("2001:2::/48"), # Benchmark testing (RFC 5180)
83
96
  IPAddr.new("2001:db8::/32"), # Documentation (RFC 3849)
84
97
  IPAddr.new("2002::/16"), # 6to4 (RFC 3056)
98
+ IPAddr.new("3fff::/20"), # Documentation (RFC 9637)
99
+ IPAddr.new("5f00::/16"), # SRv6 SIDs, confined to SR domains (RFC 9602)
85
100
  IPAddr.new("fec0::/10"), # Deprecated site-local (RFC 3879)
86
101
  IPAddr.new("ff00::/8") # Multicast (RFC 4291)
87
102
  ].freeze
88
103
 
104
+ # The IANA parent assignment is non-source, non-destination, non-forwardable,
105
+ # and non-global unless a more-specific allocation says otherwise. Refuse it
106
+ # by default so unallocated/future-special space cannot become an internal
107
+ # fetch path. These are the current, narrowly scoped IP-layer services whose
108
+ # registrations explicitly permit globally reachable destinations without
109
+ # selecting nearby infrastructure. PCP, TURN, and DNS-SD SRP anycast remain
110
+ # refused because they deliberately target on-path or local services. ORCHID,
111
+ # ORCHIDv2, and DET are overlay identifiers, not ordinary IP-layer locators.
112
+ IETF_PROTOCOL_ASSIGNMENTS = IPAddr.new("2001::/23")
113
+ GLOBALLY_REACHABLE_IETF_ASSIGNMENTS = [
114
+ IPAddr.new("2001:3::/32"), # AMT (RFC 7450)
115
+ IPAddr.new("2001:4:112::/48") # AS112-v6 (RFC 7535)
116
+ ].freeze
117
+
89
118
  # NAT64 embeds an IPv4 target that must be re-checked as IPv4. The well-known
90
119
  # prefix is a fixed /96, so the embedded octets are always the low 32 bits:
91
120
  # decode and re-check them, and NAT64 to a public address still resolves.
@@ -123,6 +152,8 @@ module Surfguard
123
152
  .partition(&:ipv4?)
124
153
  .flatten
125
154
  .map(&:to_s)
155
+ rescue InvalidHost
156
+ []
126
157
  end
127
158
 
128
159
  # True only if the URL's host resolves to at least one address and NONE are
@@ -171,6 +202,7 @@ module Surfguard
171
202
  # it cannot parse is blocked.
172
203
  def blocked_address?(ip)
173
204
  ipaddr = ip.is_a?(IPAddr) ? ip : IPAddr.new(ip.to_s)
205
+ return true unless host_address?(ipaddr)
174
206
 
175
207
  # DNS never legitimately returns an IPv4 address embedded these two ways, so
176
208
  # refuse them regardless of the address they wrap.
@@ -191,15 +223,19 @@ module Surfguard
191
223
 
192
224
  private
193
225
 
194
- # An IP-literal host skips DNS so a public literal URL resolves directly and an
195
- # internal literal is still caught by blocked_address?. Otherwise resolve via
196
- # Resolv.getaddresses the Hosts+DNS chain, matching what the connection layer
197
- # will use, and returning every address. Returns [IPAddr].
226
+ # A numeric host skips DNS so a public literal URL resolves directly and an
227
+ # internal literal is still caught by blocked_address?. Socket's numeric-only
228
+ # parser comes first because it accepts every legacy IPv4 spelling the
229
+ # connection layer does (decimal, hex, octal, and shortened forms), while
230
+ # IPAddr does not. Otherwise resolve via Resolv.getaddresses, returning every
231
+ # address from Ruby's hosts-plus-DNS chain. System getaddrinfo may consult
232
+ # additional NSS sources; pinning an address returned here avoids that second
233
+ # resolution. Returns [IPAddr].
198
234
  def resolve(host)
199
- literal = ip_literal(host)
235
+ literals = numeric_literals(host)
200
236
 
201
- if literal
202
- [ literal ]
237
+ if literals
238
+ literals
203
239
  else
204
240
  Resolv.getaddresses(host).map { |a| IPAddr.new(a) }
205
241
  end
@@ -207,15 +243,53 @@ module Surfguard
207
243
  []
208
244
  end
209
245
 
210
- # nil for anything that isn't already an address. Kept separate so the DNS
211
- # call above sits in the method body, where the ResolvError rescue can reach
212
- # it inside a sibling rescue clause it could not.
246
+ # Every address Socket's numeric-only parser finds. AI_NUMERICHOST guarantees
247
+ # this call never falls through to DNS. Its success is authoritative: these
248
+ # tokens must not be reinterpreted as names by Resolv, because Net::HTTP's
249
+ # getaddrinfo call will reinterpret them as the numeric addresses returned
250
+ # here. IPAddr remains as a fallback for syntax it alone accepts, notably a
251
+ # full-width /32 or /128 host prefix. nil means the token is a name.
252
+ def numeric_literals(host)
253
+ Socket.getaddrinfo(
254
+ host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 0, Socket::AI_NUMERICHOST
255
+ ).map { |address| IPAddr.new(address[3]) }.uniq
256
+ rescue SocketError
257
+ literal = ip_literal(host)
258
+ return [ literal ] if literal
259
+
260
+ # An ordinary name may proceed to Resolv after AI_NUMERICHOST says it is
261
+ # not numeric. A token shaped like a legacy IPv4 spelling may not: if the
262
+ # system numeric parser itself failed abnormally, asking DNS would recreate
263
+ # the parser/resolver identity gap this method exists to close.
264
+ raise InvalidHost, "numeric-looking host #{host.inspect} could not be classified" if numeric_host_candidate?(host)
265
+
266
+ nil
267
+ end
268
+
269
+ def numeric_host_candidate?(host)
270
+ text = host.to_s
271
+ return true if text.include?(":") # Invalid IPv6 syntax is never a DNS name.
272
+
273
+ parts = text.split(".", -1)
274
+ (1..4).cover?(parts.length) &&
275
+ parts.all? { |part| part.match?(/\A(?:0[xX][0-9A-Fa-f]+|[0-9]+)\z/) }
276
+ end
277
+
278
+ # nil for anything IPAddr cannot parse. A shortened prefix is not a host and
279
+ # is rejected rather than normalized to its network address or sent to DNS.
213
280
  def ip_literal(host)
214
- IPAddr.new(host)
281
+ ipaddr = IPAddr.new(host)
282
+ raise InvalidHost, "#{host.inspect} is a network, not a host" unless host_address?(ipaddr)
283
+
284
+ ipaddr
215
285
  rescue IPAddr::InvalidAddressError
216
286
  nil
217
287
  end
218
288
 
289
+ def host_address?(ipaddr)
290
+ ipaddr.prefix == (ipaddr.ipv4? ? 32 : 128)
291
+ end
292
+
219
293
  def host_of(url)
220
294
  # URI#host is "" for "http://" and nil when there's no authority at all.
221
295
  # Neither is a name to look up, so both are malformed rather than a lookup
@@ -232,7 +306,10 @@ module Surfguard
232
306
  end
233
307
 
234
308
  def disallowed_ipv6?(ipaddr)
309
+ return false if GLOBALLY_REACHABLE_IETF_ASSIGNMENTS.any? { |range| range.include?(ipaddr) }
310
+
235
311
  ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? ||
312
+ IETF_PROTOCOL_ASSIGNMENTS.include?(ipaddr) ||
236
313
  DISALLOWED_IPV6.any? { |range| range.include?(ipaddr) }
237
314
  end
238
315
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: surfguard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - 37signals