-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Implement private/reserved IP lookup interface #6512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aerosol
wants to merge
5
commits into
master
Choose a base branch
from
ip-tools
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| name: IANA registry check | ||
|
|
||
| on: | ||
| schedule: | ||
| # Runs at 09:00 UTC on the 1st of every month. | ||
| - cron: "0 9 1 * *" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| check-iana-registries: | ||
| name: Check IANA IP registries up to date | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
|
|
||
| - uses: marocchino/tool-versions-action@18a164fa2b0db1cc1edf7305fcb17ace36d1c306 # v1.2.0 | ||
| id: versions | ||
|
|
||
| - uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0 | ||
| with: | ||
| elixir-version: ${{ steps.versions.outputs.elixir }} | ||
| otp-version: ${{ steps.versions.outputs.erlang }} | ||
|
|
||
| - run: mix deps.get | ||
|
|
||
| - run: mix download_ip_registries | ||
|
|
||
| - name: Fail if the downloaded registries differ from what's committed | ||
| run: | | ||
| # IANA serves these CSVs with CRLF line endings while the committed files use LF; | ||
| # --ignore-space-at-eol treats that difference as a non-change so only real content changes fail the check. | ||
| if ! git diff --exit-code --quiet --ignore-space-at-eol -- priv/ip/iana-ipv4-special-registry.csv priv/ip/iana-ipv6-special-registry.csv; then | ||
| echo "IANA special-purpose registries are out of date. Run 'mix download_ip_registries' and commit the changes." | ||
| git diff --ignore-space-at-eol -- priv/ip/iana-ipv4-special-registry.csv priv/ip/iana-ipv6-special-registry.csv | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Notify team on success | ||
| if: ${{ success() }} | ||
| uses: fjogeleit/http-request-action@551353b829c3646756b2ec2b3694f819d7957495 # v2.0.0 | ||
| with: | ||
| url: ${{ secrets.BUILD_NOTIFICATION_URL }} | ||
| method: 'POST' | ||
| customHeaders: '{"Content-Type": "application/json"}' | ||
| data: '{"content": "<p>IANA IP registry is up to date.</p>"}' | ||
|
|
||
| - name: Notify team on failure | ||
| if: ${{ failure() }} | ||
| uses: fjogeleit/http-request-action@551353b829c3646756b2ec2b3694f819d7957495 # v2.0.0 | ||
| with: | ||
| url: ${{ secrets.BUILD_NOTIFICATION_URL }} | ||
| method: 'POST' | ||
| customHeaders: '{"Content-Type": "application/json"}' | ||
| data: '{"content": "<p>⚠️ IANA IP registry check <a href=\"https://github.com/plausible/analytics/actions/runs/${{ github.run_id }}\">failed</a>.</p>"}' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| defmodule Plausible.IP.Tools do | ||
| @moduledoc """ | ||
| Recognizes reserved and private IPv4/IPv6 addresses. | ||
| See: `Plausible.IP.Tools.Registry` | ||
| """ | ||
|
|
||
| @external_resource Plausible.IP.Tools.Registry.ipv4_registry_path() | ||
| @external_resource Plausible.IP.Tools.Registry.ipv6_registry_path() | ||
|
|
||
| @clauses Plausible.IP.Tools.Registry.entries() | ||
|
|
||
| @doc """ | ||
| Returns the ranges used in `reserved?/1`, for testing purposes. | ||
| """ | ||
| @spec ranges() :: [%{cidr: String.t(), name: String.t(), reserved: boolean()}] | ||
| def ranges do | ||
| Enum.map(@clauses, &Map.take(&1, [:cidr, :name, :reserved])) | ||
| end | ||
|
|
||
| @doc """ | ||
| Determines whether IP falls within a reserved or private address range. | ||
| Accepts already parsed `:inet` address tuples. | ||
|
|
||
| IPv4-mapped IPv6 addresses (`::ffff:a.b.c.d`) are unwrapped and delegate to | ||
| the IPv4 rules for the embedded address, rather than being uniformly | ||
| treated as reserved. | ||
| """ | ||
| @spec reserved?(:inet.ip_address()) :: boolean() | ||
| for %{pattern: pattern, guard: guard, reserved: reserved} <- @clauses do | ||
| def reserved?(unquote(pattern)) when unquote(guard), do: unquote(reserved) | ||
| end | ||
|
|
||
| def reserved?({0, 0, 0, 0, 0, 0xFFFF, hi, lo}) do | ||
| reserved?({div(hi, 256), rem(hi, 256), div(lo, 256), rem(lo, 256)}) | ||
| end | ||
|
|
||
| def reserved?(ip) when is_tuple(ip) and tuple_size(ip) == 4, do: false | ||
| def reserved?(ip) when is_tuple(ip) and tuple_size(ip) == 8, do: false | ||
|
|
||
| @doc """ | ||
| Determines if IP is allowed, i.e. valid and not reserved/private. | ||
| """ | ||
| @spec allowed?(:inet.ip_address() | String.t()) :: boolean() | ||
| def allowed?(ip) when is_binary(ip) do | ||
| case :inet.parse_address(String.to_charlist(ip)) do | ||
| {:ok, address} -> allowed?(address) | ||
| {:error, _reason} -> false | ||
| end | ||
| end | ||
|
|
||
| def allowed?(ip) when is_tuple(ip) and tuple_size(ip) in [4, 8] do | ||
| not reserved?(ip) | ||
| end | ||
|
|
||
| def allowed?(_ip), do: false | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| defmodule Plausible.IP.Tools.Registry do | ||
| @moduledoc """ | ||
| Compile-time-only helpers for `Plausible.IP.Tools`. | ||
|
|
||
| Parses IANA RFC4180 CSV plus supplemental multicast ranges | ||
| into a list of function clause meta data. | ||
| """ | ||
|
|
||
| import Bitwise | ||
|
|
||
| def ipv4_registry_path, | ||
| do: Application.app_dir(:plausible, "priv/ip/iana-ipv4-special-registry.csv") | ||
|
|
||
| def ipv6_registry_path, | ||
| do: Application.app_dir(:plausible, "priv/ip/iana-ipv6-special-registry.csv") | ||
|
|
||
| def entries() do | ||
| ipv4 = load(ipv4_registry_path()) | ||
|
|
||
| ipv6 = | ||
| ipv6_registry_path() | ||
| |> load() | ||
| # IPv4-mapped IPv6 is excluded here: it isn't a range | ||
| # that's uniformly reserved or public, it's a container for an entire | ||
| # embedded IPv4 address. Plausible.IP.Tools handles it by unwrapping | ||
| # the embedded address and delegating back to the IPv4 clauses instead. | ||
| |> Enum.reject(&(&1.cidr == "::ffff:0:0/96")) | ||
|
|
||
| (ipv4 ++ ipv6 ++ multicast()) | ||
| |> Enum.sort_by(& &1.prefix_len, :desc) | ||
| |> Enum.map(&to_clause/1) | ||
| end | ||
|
|
||
| defp multicast do | ||
| [ | ||
| entry_from_cidr("224.0.0.0/4", "Multicast", true), | ||
| entry_from_cidr("ff00::/8", "Multicast", true) | ||
| ] | ||
| end | ||
|
|
||
| defp rows_to_entries(rows), do: Enum.flat_map(rows, &row_to_entry/1) | ||
|
|
||
| # Address Block,Name,RFC,Allocation Date,Termination Date,Source, | ||
| # Destination,Forwardable,Globally Reachable,Reserved-by-Protocol | ||
| defp row_to_entry(row) do | ||
| address_block = Enum.at(row, 0) | ||
| name = Enum.at(row, 1) | ||
| termination_date = row |> Enum.at(4) |> strip_footnotes() | ||
| globally_reachable = row |> Enum.at(8) |> strip_footnotes() | ||
|
|
||
| reserved? = termination_date == "N/A" and globally_reachable == "False" | ||
|
|
||
| address_block | ||
| |> strip_footnotes() | ||
| |> String.split(",") | ||
| |> Enum.map(&String.trim/1) | ||
| |> Enum.reject(&(&1 == "")) | ||
| |> Enum.map(&entry_from_cidr(&1, name, reserved?)) | ||
| end | ||
|
|
||
| defp strip_footnotes(nil), do: nil | ||
|
|
||
| defp strip_footnotes(field) do | ||
| field | ||
| |> String.replace(~r/\s*\[\d+\]/, "") | ||
| |> String.trim() | ||
| end | ||
|
|
||
| defp entry_from_cidr(cidr, name, reserved?) do | ||
| [addr_str, prefix_str] = String.split(cidr, "/") | ||
| {:ok, address} = :inet.parse_address(String.to_charlist(addr_str)) | ||
|
|
||
| %{ | ||
| cidr: cidr, | ||
| name: name, | ||
| words: Tuple.to_list(address), | ||
| prefix_len: String.to_integer(prefix_str), | ||
| reserved: reserved? | ||
| } | ||
| end | ||
|
|
||
| defp to_clause(%{ | ||
| words: words, | ||
| prefix_len: prefix_len, | ||
| reserved: reserved, | ||
| cidr: cidr, | ||
| name: name | ||
| }) do | ||
| word_bits = if length(words) == 4, do: 8, else: 16 | ||
| indexed_words = Enum.with_index(words) | ||
|
|
||
| pattern_words = Enum.map(indexed_words, &pattern_word(&1, word_bits, prefix_len)) | ||
|
|
||
| guard = | ||
| indexed_words | ||
| |> Enum.map(&guard_for_word(&1, word_bits, prefix_len)) | ||
| |> Enum.reject(&is_nil/1) | ||
| |> combine_guards() | ||
|
|
||
| %{pattern: {:{}, [], pattern_words}, guard: guard, reserved: reserved, cidr: cidr, name: name} | ||
| end | ||
|
|
||
| defp pattern_word({word, index}, word_bits, prefix_len) do | ||
| bits_before = index * word_bits | ||
| bits_after = bits_before + word_bits | ||
|
|
||
| cond do | ||
| bits_after <= prefix_len -> word | ||
| bits_before >= prefix_len -> Macro.var(:"_w#{index}", nil) | ||
| true -> Macro.var(:"w#{index}", nil) | ||
| end | ||
| end | ||
|
|
||
| defp guard_for_word({word, index}, word_bits, prefix_len) do | ||
| bits_before = index * word_bits | ||
| bits_after = bits_before + word_bits | ||
|
|
||
| if bits_before < prefix_len and bits_after > prefix_len do | ||
| var = Macro.var(:"w#{index}", nil) | ||
| bits_in_word = prefix_len - bits_before | ||
| free_bits = word_bits - bits_in_word | ||
| full_mask = (1 <<< word_bits) - 1 | ||
| mask = full_mask - ((1 <<< free_bits) - 1) | ||
| masked_value = word &&& mask | ||
|
|
||
| quote do | ||
| band(unquote(var), unquote(mask)) === unquote(masked_value) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| defp combine_guards([]), do: true | ||
| defp combine_guards([guard]), do: guard | ||
|
|
||
| defp combine_guards([guard | rest]) do | ||
| Enum.reduce(rest, guard, fn g, acc -> | ||
| quote do | ||
| unquote(acc) and unquote(g) | ||
| end | ||
| end) | ||
| end | ||
|
|
||
| defp load(path) do | ||
| path | ||
| |> File.read!() | ||
| |> NimbleCSV.RFC4180.parse_string() | ||
| |> rows_to_entries() | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| defmodule Mix.Tasks.DownloadIpRegistries do | ||
| @moduledoc """ | ||
| Refreshes the IANA special-purpose address registries used by | ||
| `Plausible.IP.Tools` at compile time to recognize reserved/private IPs. | ||
| """ | ||
|
|
||
| use Mix.Task | ||
| require Logger | ||
|
|
||
| alias Plausible.IP | ||
|
|
||
| # coveralls-ignore-start | ||
|
|
||
| @sources %{ | ||
| IP.Tools.Registry.ipv4_registry_path() => | ||
| "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry-1.csv", | ||
| IP.Tools.Registry.ipv6_registry_path() => | ||
| "https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry-1.csv" | ||
| } | ||
|
|
||
| def run(_) do | ||
| Application.ensure_all_started(:req) | ||
|
|
||
| for {path, url} <- @sources do | ||
| path |> Path.dirname() |> File.mkdir_p!() | ||
|
|
||
| Logger.notice("Downloading #{url}") | ||
|
|
||
| case Req.get(url) do | ||
| {:ok, %{status: 200, body: body}} -> | ||
| File.write!(path, body) | ||
| Logger.notice("Saved #{path}") | ||
|
|
||
| other -> | ||
| Logger.error("Unable to download #{url}. Response: #{inspect(other)}") | ||
| end | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,6 +103,7 @@ defmodule Plausible.MixProject do | |
| {:location, git: "https://github.com/plausible/location.git"}, | ||
| {:mox, "~> 1.0", only: [:test, :ce_test, :e2e_test]}, | ||
| {:nanoid, "~> 2.1.0"}, | ||
| {:nimble_csv, "~> 1.3"}, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's used elsewhere too but was never put up as an explicit dependency |
||
| {:nimble_totp, "~> 1.0"}, | ||
| {:oban, "~> 2.20.1"}, | ||
| {:observer_cli, "~> 1.7"}, | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| Address Block,Name,RFC,Allocation Date,Termination Date,Source,Destination,Forwardable,Globally Reachable,Reserved-by-Protocol | ||
| 0.0.0.0/8,"""This network""","[RFC791], Section 3.2",1981-09,N/A,True,False,False,False,True | ||
| 0.0.0.0/32,"""This host on this network""","[RFC1122], Section 3.2.1.3",1981-09,N/A,True,False,False,False,True | ||
| 10.0.0.0/8,Private-Use,[RFC1918],1996-02,N/A,True,True,True,False,False | ||
| 100.64.0.0/10,Shared Address Space,[RFC6598],2012-04,N/A,True,True,True,False,False | ||
| 127.0.0.0/8,Loopback,"[RFC1122], Section 3.2.1.3",1981-09,N/A,False [1],False [1],False [1],False [1],True | ||
| 169.254.0.0/16,Link Local,[RFC3927],2005-05,N/A,True,True,False,False,True | ||
| 172.16.0.0/12,Private-Use,[RFC1918],1996-02,N/A,True,True,True,False,False | ||
| 192.0.0.0/24 [2],IETF Protocol Assignments,"[RFC6890], Section 2.1",2010-01,N/A,False,False,False,False,False | ||
| 192.0.0.0/29,IPv4 Service Continuity Prefix,[RFC7335],2011-06,N/A,True,True,True,False,False | ||
| 192.0.0.8/32,IPv4 dummy address,[RFC7600],2015-03,N/A,True,False,False,False,False | ||
| 192.0.0.9/32,Port Control Protocol Anycast,[RFC7723],2015-10,N/A,True,True,True,True,False | ||
| 192.0.0.10/32,Traversal Using Relays around NAT Anycast,[RFC8155],2017-02,N/A,True,True,True,True,False | ||
| "192.0.0.170/32, 192.0.0.171/32",NAT64/DNS64 Discovery,"[RFC8880][RFC7050], Section 2.2",2013-02,N/A,False,False,False,False,True | ||
| 192.0.2.0/24,Documentation (TEST-NET-1),[RFC5737],2010-01,N/A,False,False,False,False,False | ||
| 192.31.196.0/24,AS112-v4,[RFC7535],2014-12,N/A,True,True,True,True,False | ||
| 192.52.193.0/24,AMT,[RFC7450],2014-12,N/A,True,True,True,True,False | ||
| 192.88.99.0/24,Deprecated (6to4 Relay Anycast),[RFC7526],2001-06,2015-03,,,,, | ||
| 192.88.99.2/32,6a44-relay anycast address,[RFC6751],2012-10,N/A,True,True,True,False,False | ||
| 192.168.0.0/16,Private-Use,[RFC1918],1996-02,N/A,True,True,True,False,False | ||
| 192.175.48.0/24,Direct Delegation AS112 Service,[RFC7534],1996-01,N/A,True,True,True,True,False | ||
| 198.18.0.0/15,Benchmarking,[RFC2544],1999-03,N/A,True,True,True,False,False | ||
| 198.51.100.0/24,Documentation (TEST-NET-2),[RFC5737],2010-01,N/A,False,False,False,False,False | ||
| 203.0.113.0/24,Documentation (TEST-NET-3),[RFC5737],2010-01,N/A,False,False,False,False,False | ||
| 240.0.0.0/4,Reserved,"[RFC1112], Section 4",1989-08,N/A,False,False,False,False,True | ||
| 255.255.255.255/32,Limited Broadcast,"[RFC8190] | ||
| [RFC919], Section 7",1984-10,N/A,False,True,False,False,True |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| Address Block,Name,RFC,Allocation Date,Termination Date,Source,Destination,Forwardable,Globally Reachable,Reserved-by-Protocol | ||
| ::1/128,Loopback Address,[RFC4291],2006-02,N/A,False,False,False,False,True | ||
| ::/128,Unspecified Address,[RFC4291],2006-02,N/A,True,False,False,False,True | ||
| ::ffff:0:0/96,IPv4-mapped Address,[RFC4291],2006-02,N/A,False,False,False,False,True | ||
| 64:ff9b::/96,IPv4-IPv6 Translat.,[RFC6052],2010-10,N/A,True,True,True,True,False | ||
| 64:ff9b:1::/48,IPv4-IPv6 Translat.,[RFC8215],2017-06,N/A,True,True,True,False,False | ||
| 100::/64,Discard-Only Address Block,[RFC6666],2012-06,N/A,True,True,True,False,False | ||
| 100:0:0:1::/64,Dummy IPv6 Prefix,[RFC9780],2025-04,N/A,True,False,False,False,False | ||
| 2001::/23,IETF Protocol Assignments,[RFC2928],2000-09,N/A,False [1],False [1],False [1],False [1],False | ||
| 2001::/32,TEREDO,"[RFC4380] | ||
| [RFC8190]",2006-01,N/A,True,True,True,N/A [2],False | ||
| 2001:1::1/128,Port Control Protocol Anycast,[RFC7723],2015-10,N/A,True,True,True,True,False | ||
| 2001:1::2/128,Traversal Using Relays around NAT Anycast,[RFC8155],2017-02,N/A,True,True,True,True,False | ||
| 2001:1::3/128,DNS-SD Service Registration Protocol Anycast,[RFC9665],2024-04,N/A,True,True,True,True,False | ||
| 2001:2::/48,Benchmarking,[RFC5180][RFC Errata 1752],2008-04,N/A,True,True,True,False,False | ||
| 2001:3::/32,AMT,[RFC7450],2014-12,N/A,True,True,True,True,False | ||
| 2001:4:112::/48,AS112-v6,[RFC7535],2014-12,N/A,True,True,True,True,False | ||
| 2001:10::/28,Deprecated (previously ORCHID),[RFC4843],2007-03,2014-03,,,,, | ||
| 2001:20::/28,ORCHIDv2,[RFC7343],2014-07,N/A,True,True,True,True,False | ||
| 2001:30::/28,Drone Remote ID Protocol Entity Tags (DETs) Prefix,[RFC9374],2022-12,N/A,True,True,True,True,False | ||
| 2001:db8::/32,Documentation,[RFC3849],2004-07,N/A,False,False,False,False,False | ||
| 2002::/16 [3],6to4,[RFC3056],2001-02,N/A,True,True,True,N/A [3],False | ||
| 2620:4f:8000::/48,Direct Delegation AS112 Service,[RFC7534],2011-05,N/A,True,True,True,True,False | ||
| 3fff::/20,Documentation,[RFC9637],2024-07,N/A,False,False,False,False,False | ||
| 5f00::/16,Segment Routing (SRv6) SIDs,[RFC9602],2024-04,N/A,True,True,True,False,False | ||
| fc00::/7,Unique-Local,"[RFC4193] | ||
| [RFC8190]",2005-10,N/A,True,True,True,False [4],False | ||
| fe80::/10,Link-Local Unicast,[RFC4291],2006-02,N/A,True,True,False,False,True |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.