Repository: ajvondrak/remote_ip Branch: main Commit: da061f05feea Files: 71 Total size: 195.7 KB Directory structure: gitextract_gtwens9r/ ├── .formatter.exs ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── .tool-versions ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bench/ │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── check.exs │ ├── lib/ │ │ └── bench/ │ │ └── inputs.ex │ ├── mix.exs │ └── parse.exs ├── coveralls.json ├── extras/ │ └── algorithm.md ├── integration/ │ ├── tests/ │ │ ├── basic/ │ │ │ ├── .formatter.exs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── lib/ │ │ │ │ └── basic.ex │ │ │ ├── mix.exs │ │ │ └── test/ │ │ │ ├── basic_test.exs │ │ │ └── test_helper.exs │ │ ├── custom/ │ │ │ ├── .formatter.exs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── config/ │ │ │ │ └── config.exs │ │ │ ├── mix.exs │ │ │ └── test/ │ │ │ ├── custom_test.exs │ │ │ └── test_helper.exs │ │ ├── debug/ │ │ │ ├── .formatter.exs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── config/ │ │ │ │ └── config.exs │ │ │ ├── mix.exs │ │ │ └── test/ │ │ │ ├── debug_test.exs │ │ │ └── test_helper.exs │ │ ├── parsers/ │ │ │ ├── .formatter.exs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── config/ │ │ │ │ └── config.exs │ │ │ ├── lib/ │ │ │ │ ├── parsers/ │ │ │ │ │ └── forwarding.ex │ │ │ │ └── parsers.ex │ │ │ ├── mix.exs │ │ │ └── test/ │ │ │ ├── parsers_test.exs │ │ │ └── test_helper.exs │ │ └── purge/ │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config/ │ │ │ └── config.exs │ │ ├── mix.exs │ │ └── test/ │ │ ├── purge_test.exs │ │ └── test_helper.exs │ └── tests.exs ├── lib/ │ ├── remote_ip/ │ │ ├── block.ex │ │ ├── debugger.ex │ │ ├── headers.ex │ │ ├── options.ex │ │ ├── parser.ex │ │ └── parsers/ │ │ ├── forwarded.ex │ │ └── generic.ex │ └── remote_ip.ex ├── mix.exs └── test/ ├── .formatter.exs ├── remote_ip/ │ ├── block_test.exs │ ├── headers_test.exs │ ├── options_test.exs │ └── parsers/ │ ├── forwarded_test.exs │ └── generic_test.exs ├── remote_ip_test.exs └── test_helper.exs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .formatter.exs ================================================ [ inputs: ["{mix,.formatter}.exs", "lib/**/*.ex", "integration/tests.exs"], line_length: 80, subdirectories: ["test", "integration/tests/*"] ] ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: workflow_dispatch: push: branches: - main pull_request: branches: - main env: MIX_ENV: ci # The approach to the CI matrix: # # * In general, remote_ip only maintains support for the minimum supported # version of Elixir, so we only need to test the versions listed in # https://hexdocs.pm/elixir/compatibility-and-deprecations.html # # * To avoid combinatorial explosion of Elixir/OTP pairs, and to err on the # side of caution, each Elixir version is paired with its lowest supported # OTP version. # # * Additionally, the highest Elixir version is also paired with its highest # supported OTP version, to cover the latest & greatest case. jobs: ci: strategy: fail-fast: false matrix: include: - elixir: '1.12' otp: '23' - elixir: '1.13' otp: '23' - elixir: '1.14' otp: '23' - elixir: '1.15' otp: '24' - elixir: '1.16' otp: '24' - elixir: '1.16' otp: '26' name: Elixir ${{ matrix.elixir }} (OTP ${{ matrix.otp }}) runs-on: ubuntu-20.04 steps: - name: Checkout repository uses: actions/checkout@v3 - id: install name: Install Elixir uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.otp }} elixir-version: ${{ matrix.elixir }} - name: Restore cached build uses: actions/cache@v3 with: key: builds@elixir-${{ steps.install.outputs.elixir-version }}-otp-${{ steps.install.outputs.otp-version }}-mix-${{ hashFiles('mix.lock') }} path: | deps _build - name: Install dependencies run: mix do deps.get, deps.compile - name: Check formatting run: mix format --check-formatted - name: Compile run: mix compile --warnings-as-errors - name: Run unit tests run: mix coveralls.github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Restore cached integrations uses: actions/cache@v3 with: key: integrations@elixir-${{ steps.install.outputs.elixir-version }}-otp-${{ steps.install.outputs.otp-version }}-mix-${{ hashFiles('integration/tests/*/mix.lock') }} path: | integration/tests/*/deps integration/tests/*/_build - name: Run integration tests run: mix integrate - name: Restore cached PLTs uses: actions/cache@v3 with: key: plts@elixir-${{ steps.install.outputs.elixir-version }}-otp-${{ steps.install.outputs.otp-version }}-mix-${{ hashFiles('mix.lock') }} path: | priv/plts restore-keys: | plts@elixir-${{ steps.install.outputs.elixir-version }}-otp-${{ steps.install.outputs.otp-version }}-mix- - name: Run dialyzer run: mix dialyzer ================================================ FILE: .gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build # If you run "mix test --cover", coverage assets end up here. /cover # The directory Mix downloads your dependencies sources to. /deps # Where 3rd-party dependencies like ExDoc output generated docs. /doc # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Dialyzer's Persistent Lookup Tables (PLTs) cache expensive analyses, but they # change depending on the version of Elixir/OTP, so we don't want to commit the # outputs directly to version control. E.g., having one version's PLTs could # mess up continuous integration on a different version. /priv/plts/*.plt /priv/plts/*.plt.hash ================================================ FILE: .tool-versions ================================================ elixir 1.16-otp-26 erlang 26.2.5 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing **Table Of Contents** * [Issues](#issues) * [Getting the wrong IP](#getting-the-wrong-ip) * [Other issues](#other-issues) * [Pull Requests](#pull-requests) * [General guidelines](#general-guidelines) ## Issues ### Getting the wrong IP There are many reasons you might not be getting the `remote_ip` value you expect. Before opening an issue, enable `RemoteIp.Debugger` and reproduce your problematic request. ```elixir config :remote_ip, debug: true ``` ```console $ mix deps.compile --force remote_ip ``` Then you should see logs like these: ``` [debug] Processing remote IP headers: ["x-forwarded-for"] parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded} proxies: ["1.2.0.0/16", "2.3.4.5/32"] clients: ["1.2.3.4/32"] [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsing IPs from forwarding headers: [{"x-forwarded-for", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}] [debug] {2, 3, 4, 5} is a known proxy IP [debug] {10, 0, 0, 1} is a reserved IP [debug] {1, 2, 3, 4} is a known client IP [debug] Processed remote IP, found client {1, 2, 3, 4} to replace {127, 0, 0, 1} ``` This can help you narrow down the issue: * Do you see these logs at all? If not, `RemoteIp` might not even be called by your pipeline. Try debugging your code. * Are you getting the request headers you expect? Your particular proxies might not be sending the forwarding headers they should be. * Did you configure `:headers` right? `RemoteIp` only pays attention to the forwarding headers you specify. * Did you configure `:proxies` right? If you don't configure an IP as a known proxy, `RemoteIp` assumes it's a legitimate client. * Did you configure `:clients` right? Loopback or private IPs are automatically identified as proxies. If you need to carve out exceptions, you should add the relevant IP ranges to the list of known clients. * Are all the IPs being parsed correctly? `RemoteIp` will ignore values that it cannot parse. Either this is a bug in `RemoteIp` or a bad header. * Are the forwarding headers in the right order? IPs are processed last-to-first to prevent spoofing. Make sure you understand [the algorithm](extras/algorithm.md). * Are there multiple "competing" forwarding headers? The order we see the `req_headers` in the `Plug.Conn` matters for the last-to-first processing. Unfortunately, servers like [cowboy](https://github.com/ninenines/cowboy) can distort the order of incoming headers, since [Erlang maps](http://erlang.org/doc/man/maps.html) do not [preserve ordering](https://medium.com/@jlouis666/breaking-erlang-maps-1-31952b8729e6) (cf. [[1]](https://github.com/elixir-plug/plug_cowboy/blob/7bf68cd757c1a052e227112b681b77066fd84d2b/lib/plug/cowboy/conn.ex#L125-L127), [[2]](https://github.com/erlang/otp/blob/2c882ec2d504019f07104b3240a989148dfc1fa3/lib/stdlib/doc/src/maps.xml#L409)). You *might* be able to configure `RemoteIp` to avoid your particular problematic situation. If none of the above apply, you may have found a bug in `RemoteIp`, so please go ahead and open an issue. ### Other issues All manner of issues are welcome. However, I don't often have much time to work on open source things, so my turnaround is usually pretty slow. You can help by giving as much context as possible: * :bug: Bugs * How can it be reproduced? * Do the logs help? * What was the expected behavior? * What was the actual behavior? * :sparkles: Feature requests * What problem would it solve? * How would it work? * Why does it belong in this library? * :question: Questions * Before asking why you're getting the wrong IP, do your [due diligence](#getting-the-wrong-ip). * The more details you can provide, the better! ## Pull Requests If there's some bug you've fixed or feature you've implemented, contribute your changes through the usual means: 1. Fork this project. 2. Commit your changes. 3. Open a pull request. ### General guidelines A few notes about getting your pull request accepted: * [Write good commit messages.](https://chris.beams.io/posts/git-commit/) > **The seven rules of a great Git commit message** > > > Keep in mind: [This](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [has](https://www.git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines) [all](https://github.com/torvalds/subsurface-for-dirk/blob/master/README#L92-L120) [been](http://who-t.blogspot.co.at/2009/12/on-commit-messages.html) [said](https://github.com/erlang/otp/wiki/writing-good-commit-messages) [before](https://github.com/spring-projects/spring-framework/blob/30bce7/CONTRIBUTING.md#format-commit-messages). > > 1. Separate subject from body with a blank line > 2. Limit the subject line to 50 characters > 3. Capitalize the subject line > 4. Do not end the subject line with a period > 5. Use the imperative mood in the subject line > 6. Wrap the body at 72 characters > 7. Use the body to explain *what* and *why* vs. *how* * Keep the scope of your PR tight. * **Do** make sure your PR accomplishes one specific thing. * **Don't** make unnecessary or unrelated changes. * Keep your history clean. * **Do** make sure each commit pertains conceptually to a single change. * **Don't** leave your commits disorganized with various works-in-progress. [Rewrite](https://git-scm.com/book/id/v2/Git-Tools-Rewriting-History) [history](https://git-rebase.io/) [if](https://programmerfriend.com/git-best-practices/) [necessary](http://justinhileman.info/article/changing-history/). * Write a good PR description. * What problem are you trying to solve? * Who does the problem affect? * When did this problem happen? Is it tied to a specific version? * Where is the source of the issue? Is it an external project? Can you link to a relevant discussion? * How did you solve it? * Why is this the proper solution? * Write tests, if appropriate. * Proper documentation is appreciated. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Alex Vondrak Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # RemoteIp [![build status](https://github.com/ajvondrak/remote_ip/workflows/build/badge.svg)](https://github.com/ajvondrak/remote_ip/actions?query=workflow%3Abuild) [![coverage status](https://coveralls.io/repos/github/ajvondrak/remote_ip/badge.svg?branch=main)](https://coveralls.io/github/ajvondrak/remote_ip?branch=main) [![hex.pm version](https://img.shields.io/hexpm/v/remote_ip)](https://hex.pm/packages/remote_ip) A [plug](https://github.com/elixir-lang/plug) to rewrite the [`Plug.Conn`](https://hexdocs.pm/plug/Plug.Conn.html)'s `remote_ip` based on forwarding headers. Generic comma-separated headers like `X-Forwarded-For`, `X-Real-Ip`, and `X-Client-Ip` are all recognized, as well as the [RFC 7239](https://tools.ietf.org/html/rfc7239) `Forwarded` header. You can specify any number of forwarding headers to recognize and even configure your own parsers. IPs are processed last-to-first to prevent IP spoofing. Loopback/private IPs are ignored by default, but known proxies & clients are configurable, so you have full control over which IPs are considered legitimate. **If your app is not behind at least one proxy, you should not use this plug.** See [the algorithm](extras/algorithm.md) for more details. ## Installation Add `:remote_ip` to your list of dependencies in `mix.exs`: ```elixir def deps do [{:remote_ip, "~> 1.2"}] end ``` ## Usage Add the `RemoteIp` plug to your app's plug pipeline: ```elixir defmodule MyApp do use Plug.Router plug RemoteIp plug :match plug :dispatch # get "/" do ... end ``` You can also use `RemoteIp.from/2` outside of a plug pipeline to extract the remote IP from a list of headers: ```elixir x_headers = [{"x-forwarded-for", "1.2.3.4"}] RemoteIp.from(x_headers) ``` See the [documentation](https://hexdocs.pm/remote_ip) for full details on usage, configuration options, and troubleshooting. ## Motivation ### Problem: Your app is behind a proxy and you want to know the original client's IP address. [Proxies](https://en.wikipedia.org/wiki/Proxy_server) are pervasive for some purpose or another in modern HTTP infrastructure: encryption, load balancing, caching, compression, and more can be done via proxies. But a proxy makes HTTP requests appear to your app as if they came from the proxy's IP address. How is your app to know the "actual" requesting IP address (e.g., so you can geolocate a user)? **Solution:** Many proxies prevent this information loss by adding HTTP headers to communicate the requesting client's IP address. There is no single, universal header. Though [`X-Forwarded-For`](https://en.wikipedia.org/wiki/X-Forwarded-For) is common, options include [`X-Real-IP`](http://nginx.org/en/docs/http/ngx_http_realip_module.html), [`X-Client-IP`](http://httpd.apache.org/docs/trunk/mod/mod_remoteip.html), and [others](http://stackoverflow.com/a/916157). Due to this lack of standardization, [RFC 7239](https://tools.ietf.org/html/rfc7239) defines the `Forwarded` header, fulfilling a [relevant XKCD truism](https://xkcd.com/927). ### Problem: Plug does not derive `remote_ip` from headers such as `X-Forwarded-For`. Per the [`Plug.Conn` docs](https://hexdocs.pm/plug/Plug.Conn.html#module-request-fields): > * `remote_ip` - the IP of the client, example: `{151, 236, 219, 228}`. This > field is meant to be overwritten by plugs that understand e.g. the > `X-Forwarded-For` header or HAProxy's PROXY protocol. It defaults to peer's > IP. Note that the field is _meant_ to be overwritten. Plug does not actually do any overwriting itself. The [Cowboy changelog](https://github.com/ninenines/cowboy/blob/master/CHANGELOG.md#084) espouses a similar platform of non-involvement: > Because each server's proxy situation differs, it is better that this function is implemented by the application directly. **Solution:** As definitively verified in [elixir-lang/plug#319](https://github.com/elixir-lang/plug/issues/319), users are intended to hand-roll their own header parsers. ### Problem: Ain't nobody got time for that. **Solution:** There are a handful of plugs available on [Hex](https://hex.pm). There are also the comments left in the [elixir-lang/plug#319](https://github.com/elixir-lang/plug/issues/319) thread that may give you some ideas, but I consider them to be non-starters - copying/pasting code from github comments isn't much better than hand-rolling an implementation. ### Problem: Existing solutions are incomplete and have subtle bugs. None of the available solutions I have seen are ideal. In this sort of plug, you want: * **Configurable headers and parsers:** With so many different headers being used, you should be able to configure the ones you need and how to parse them. * **Configurable proxies and clients:** With multiple proxy hops, there may be several IPs in the forwarding headers. Without being able to tell the plug which of those IPs are actually known to be proxies, you may get one of them back as the `remote_ip`. * **Security and correctness:** Parsing forwarding headers can be surprisingly subtle, and it's easy to open yourself up to [IP spoofing](http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) vulnerabilities. Existing packages all fail on one or more of these fronts: * [plug\_cloudflare](https://hex.pm/packages/plug_cloudflare) - not a general purpose library, but is more secure & correct if you're specifically parsing the `CF-Connecting-IP` * [plug\_forwarded\_peer](https://hex.pm/packages/plug_forwarded_peer) - only parses `Forwarded` and `X-Forwarded-For` (`X-Forwarded-For` takes precedence over `Forwarded`), does not parse all of RFC 7239's supported syntax correctly, vulnerable to IP spoofing * [plug\_x\_forwarded\_for](https://hex.pm/packages/plug_x_forwarded_for) - only configurable for a single header, all headers parsed the same as `X-Forwarded-For`, vulnerable to IP spoofing * [remote\_ip\_rewriter](https://hex.pm/packages/remote_ip_rewriter) - can only configure one header, all headers parsed the same as `X-Forwarded-For`, cannot configure loopback/private IPs as known clients * [trusted\_proxy\_rewriter](https://hex.pm/packages/trusted_proxy_rewriter) - outdated fork of remote\_ip\_rewriter, has even more limited functionality **Solution:** These are the sorts of things application developers should not have to worry about. `RemoteIp` aims to be the proper solution to all of these problems. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for instructions on how to open issues or pull requests. ## Prior Art While `RemoteIp` has morphed into something distinct from the Rails middleware of the same name, the Rails code was definitely where I started. So I'd like to explicitly acknowledge the inspiration: this plug would not have been possible without poring over the existing implementation, discussions, documentation, and commits that went into the Rails code. :heart: Required reading for anyone who wants to think way too much about forwarding headers: * [@gingerlime](https://github.com/gingerlime)'s [blog post](http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) explaining IP spoofing * Rails' [`RemoteIp` middleware](https://github.com/rails/rails/blob/v4.1.4/actionpack/lib/action_dispatch/middleware/remote_ip.rb) * Rails' [tests](https://github.com/rails/rails/blob/92703a9ea5d8b96f30e0b706b801c9185ef14f0e/actionpack/test/dispatch/request_test.rb#L62) * The extensive discussion on [rails/rails#7980](https://github.com/rails/rails/pull/7980) ================================================ FILE: bench/.formatter.exs ================================================ # Used by "mix format" [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ] ================================================ FILE: bench/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Ignore package tarball (built via "mix hex.build"). bench-*.tar # Temporary files for e.g. tests /tmp ================================================ FILE: bench/README.md ================================================ # Benchmarks For the purposes of remote\_ip, we need a library to (1) parse strings from CIDR notation into a usable representation for (2) checking if an IP falls within a certain block. At the time of this writing, there are a couple [CIDR libraries](https://hex.pm/packages?search=cidr) available on Hex.pm: * [inet\_cidr](https://hex.pm/packages/inet_cidr) * [erl\_cidr](https://hex.pm/packages/erl_cidr) (an Erlang wrapper around inet\_cidr) * [cidr](https://hex.pm/packages/cidr) * [cider](https://hex.pm/packages/cider) Due to the shortcomings of these libraries, remote\_ip rolls its own `RemoteIp.Block` module. This app serves as a testing ground for comparing remote\_ip's implementation against the others to validate whether it's actually an improvement. ## Results Using [benchee](https://github.com/bencheeorg/benchee), we run a block of code repeatedly for a static amount of time (after a warmup) and count how many iterations we got through. Thus, in the _iterations per second_ results charted below, bigger is better: it means we performed more operations in a given amount of time. ### Parsing CIDRs This benchmark generates 1,000 random CIDR strings and measures the time it takes to parse them all with each different library. ![A data plot for the CIDR parsing benchmarks](img/parse.png) ```console $ mix run parse.exs Randomizing with seed 294598 Operating System: macOS CPU Information: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz Number of Available Cores: 8 Available memory: 16 GB Elixir 1.11.4 Erlang 23.2.7 Benchmark suite executing with the following configuration: warmup: 2 s time: 5 s memory time: 0 ns parallel: 1 inputs: none specified Estimated total run time: 28 s Benchmarking cider... Benchmarking cidr... Benchmarking inet_cidr... Benchmarking remote_ip... Generated tmp/parse.html Generated tmp/parse_cider.html Generated tmp/parse_cidr.html Generated tmp/parse_comparison.html Generated tmp/parse_inet_cidr.html Generated tmp/parse_remote_ip.html Name ips average deviation median 99th % cider 269.94 3.70 ms ±7.72% 3.61 ms 4.74 ms remote_ip 264.49 3.78 ms ±9.78% 3.70 ms 5.22 ms inet_cidr 243.27 4.11 ms ±9.25% 4.08 ms 5.51 ms cidr 169.52 5.90 ms ±11.24% 5.77 ms 7.79 ms Comparison: cider 269.94 remote_ip 264.49 - 1.02x slower +0.0763 ms inet_cidr 243.27 - 1.11x slower +0.41 ms cidr 169.52 - 1.59x slower +2.19 ms ``` ### Checking IPs This benchmark generates 1,000 random CIDRs and 1,000 randoms IPs, then uses each library to check every combination (1,000 CIDRs x 1,000 IPs = 1,000,000 checks per iteration). To avoid conflating the performance results, we perform the CIDR parsing ahead of time and don't measure it in the actual benchmark. Similarly, both remote\_ip and cider must encodes the incoming IPs as integers, so we perform that step outside of the measurement as well. ![A data plot for the IP checking benchmarks](img/check.png) ```console $ mix run check.exs Randomizing with seed 570890 Operating System: macOS CPU Information: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz Number of Available Cores: 8 Available memory: 16 GB Elixir 1.11.4 Erlang 23.2.7 Benchmark suite executing with the following configuration: warmup: 2 s time: 5 s memory time: 0 ns parallel: 1 inputs: none specified Estimated total run time: 28 s Benchmarking cider... Benchmarking cidr... Benchmarking inet_cidr... Benchmarking remote_ip... Generated tmp/check.html Generated tmp/check_cider.html Generated tmp/check_cidr.html Generated tmp/check_comparison.html Generated tmp/check_inet_cidr.html Generated tmp/check_remote_ip.html Name ips average deviation median 99th % remote_ip 14.60 68.50 ms ±1.43% 68.65 ms 70.67 ms cider 10.68 93.66 ms ±2.41% 93.86 ms 97.84 ms inet_cidr 6.37 156.88 ms ±2.89% 157.15 ms 165.46 ms cidr 1.53 652.73 ms ±0.91% 652.71 ms 661.14 ms Comparison: remote_ip 14.60 cider 10.68 - 1.37x slower +25.16 ms inet_cidr 6.37 - 2.29x slower +88.38 ms cidr 1.53 - 9.53x slower +584.23 ms ``` ## Other considerations Performance is a nice, objective nail in the coffin for these other libraries. But they also have other issues that make them impractical to use: * inet\_cidr doesn't parse individual IP addresses, so instead of `"1.2.3.4"` you have to say `"1.2.3.4/32"` * cidr's `CIDR.match/2` interface is awkward compared to the boolean `contains?/2` functions provided by everyone else * cider fails to distinguish between IPv4 and IPv6 addresses, so it produces false positives like ```elixir iex> Cider.contains?({0, 0, 0, 1}, Cider.parse("::1")) true ``` remote\_ip's implementation neatly handles all of these limitations while also performing significantly better. ================================================ FILE: bench/check.exs ================================================ Bench.Inputs.seed ips = Bench.Inputs.ips(1_000) parsed_ips = %{ remote_ip: Enum.map(ips, &RemoteIp.Block.encode/1), inet_cidr: ips, cider: Enum.map(ips, &Cider.ip!/1), cidr: ips, } cidrs = Bench.Inputs.cidrs(1_000) parsed_cidrs = %{ remote_ip: Enum.map(cidrs, &RemoteIp.Block.parse!/1), inet_cidr: Enum.map(cidrs, &InetCidr.parse_cidr!(&1, true)), cider: Enum.map(cidrs, &Cider.parse/1), cidr: Enum.map(cidrs, &CIDR.parse/1), } suite = %{ remote_ip: fn -> Enum.each(parsed_ips[:remote_ip], fn ip -> Enum.each(parsed_cidrs[:remote_ip], &RemoteIp.Block.contains?(&1, ip)) end) end, inet_cidr: fn -> Enum.each(parsed_ips[:inet_cidr], fn ip -> Enum.each(parsed_cidrs[:inet_cidr], &InetCidr.contains?(&1, ip)) end) end, cider: fn -> Enum.each(parsed_ips[:cider], fn ip -> Enum.each(parsed_cidrs[:cider], &Cider.contains?(ip, &1)) end) end, cidr: fn -> Enum.each(parsed_ips[:cidr], fn ip -> Enum.each(parsed_cidrs[:cidr], &CIDR.match(&1, ip)) end) end, } formatters = [ {Benchee.Formatters.HTML, file: "tmp/check.html", auto_open: false}, Benchee.Formatters.Console, ] Benchee.run(suite, formatters: formatters) ================================================ FILE: bench/lib/bench/inputs.ex ================================================ defmodule Bench.Inputs do def seed do seed = case System.fetch_env("SEED") do {:ok, var} -> String.to_integer(var) :error -> System.system_time(:microsecond) |> rem(1_000_000) end IO.puts("Randomizing with seed #{seed}\n") :rand.seed(:exs1024, {seed, seed, seed}) end def cidrs(n) do Stream.repeatedly(fn -> cidr() end) |> Enum.take(n) end def cidr do case Enum.random([:ipv4, :ipv6]) do :ipv4 -> cidr(ipv4()) :ipv6 -> cidr(ipv6()) end end def cidr({a, b, c, d}) do cidr({a, b, c, d}, Enum.random(0..32)) end def cidr({a, b, c, d, e, f, g, h}) do cidr({a, b, c, d, e, f, g, h}, Enum.random(0..128)) end def cidr(ip, prefix) do "#{:inet.ntoa(ip)}/#{prefix}" end def ips(n) do Stream.repeatedly(fn -> ip() end) |> Enum.take(n) end def ip do case Enum.random([:ipv4, :ipv6]) do :ipv4 -> ipv4() :ipv6 -> ipv6() end end def ipv4 do Stream.repeatedly(fn -> Enum.random(0..255) end) |> Enum.take(4) |> List.to_tuple() end def ipv6 do Stream.repeatedly(fn -> Enum.random(0..0xFFFF) end) |> Enum.take(8) |> List.to_tuple() end end ================================================ FILE: bench/mix.exs ================================================ defmodule Bench.MixProject do use Mix.Project def project do [ app: :bench, version: "0.0.0", elixir: "~> 1.12", deps: deps() ] end def application do [ extra_applications: [:logger] ] end defp deps do [ {:benchee, "~> 1.3"}, {:benchee_html, "~> 1.0"}, {:inet_cidr, "~> 1.0"}, {:cidr, "~> 1.0"}, {:cider, "~> 0.3"}, {:remote_ip, path: ".."} ] end end ================================================ FILE: bench/parse.exs ================================================ Bench.Inputs.seed cidrs = Bench.Inputs.cidrs(1_000) suite = %{ remote_ip: fn -> Enum.each(cidrs, &RemoteIp.Block.parse!/1) end, inet_cidr: fn -> Enum.each(cidrs, &InetCidr.parse_cidr!(&1, true)) end, cider: fn -> Enum.each(cidrs, &Cider.parse/1) end, cidr: fn -> Enum.each(cidrs, &CIDR.parse/1) end, } formatters = [ {Benchee.Formatters.HTML, file: "tmp/parse.html", auto_open: false}, Benchee.Formatters.Console, ] Benchee.run(suite, formatters: formatters) ================================================ FILE: coveralls.json ================================================ { "skip_files": [ "lib/remote_ip/debugger.ex" ], "coverage_options": { "minimum_coverage": 100 } } ================================================ FILE: extras/algorithm.md ================================================ # Algorithm To avoid IP spoofing vulnerabilities, `RemoteIp` employs a very particular algorithm. Its work is divided into two main phases: 1. Parse the right `req_headers`. 2. Compute the right `remote_ip`. We will analyze these steps in detail to understand the benefits and caveats of the algorithm. Much of it relies on configuration values given by `RemoteIp.Options`. You may also log the steps of this algorithm using the `RemoteIp.Debugger`. As a running example, consider the following request route: * Client at IP `1.2.3.4` sends an HTTP request to Proxy 1 (no forwarding headers) * Proxy 1 at IP `1.1.1.1` adds an `X-Forwarded-For: 1.2.3.4` header and forwards to Proxy 2 * Proxy 2 at IP `2.2.2.2` adds Proxy 1 to the header with `X-Forwarded-For: 1.2.3.4, 1.1.1.1` and forwards to Proxy 3 * Proxy 3 at IP `3.3.3.3` adds a `Forwarded: for=2.2.2.2` header and forwards to the application * Application receives the request from IP `3.3.3.3` with forwarding headers `X-Forwarded-For: 1.2.3.4, 1.1.1.1` and `Forwarded: for=2.2.2.2` ## Parsing headers There are many different forwarding headers in the wild, including `Forwarded`, `X-Forwarded-For`, `X-Client-Ip`, and `X-Real-Ip`. The header that gets used depends on the configuration of the proxy your app sits behind. If there are multiple proxies in play, it's conceivable for you to have more than one such header. The `:headers` option tells `RemoteIp` which specific headers to parse for IP addresses. The default value casts a wide net, but you should ideally specify only those headers which you're certain you require. Otherwise, it would be trivial for a malicious client to add an extra header that could interfere with finding the correct IP. To start off the algorithm, all of the configured headers are taken from the `Plug.Conn`'s `req_headers`. Their relative ordering is maintained, because order matters when there are multiple hops between proxies. In our running example, assuming both `Forwarded` and `X-Forwarded-For` were in the `:headers` option (which they are by default), we want to process the list ```elixir # This is what we want [{"x-forwarded-for", "1.2.3.4, 1.1.1.1"}, {"forwarded", "for=2.2.2.2"}] ``` and *not* the reverse ```elixir # This is NOT what we want [{"forwarded", "for=2.2.2.2"}, {"x-forwarded-for", "1.2.3.4, 1.1.1.1"}] ``` Let's assume we get the former. In reality, however, we usually can't rely on the stable ordering of headers in an HTTP request. For example, the [Cowboy](https://github.com/ninenines/cowboy/) server presently [uses maps](https://github.com/elixir-plug/plug_cowboy/blob/f82f2ff982f04fb4faa3a12fd2b08a7cc56ebe15/lib/plug/cowboy/conn.ex#L125-L127) to represent headers, which don't preserve key order, so everything could get jumbled up. Configuring multiple headers might still be useful if, for example, you expect some requests to only have header A and other requests to only have header B, but never both at the same time. So `RemoteIp` doesn't limit you to just the one choice. After selecting the allowed headers, each string is parsed for its IP addresses. In the common case, we parse comma-separated IPs with `RemoteIp.Parsers.Generic`. This works for headers like `X-Forwarded-For`, `X-Client-Ip` and `X-Real-Ip`. But you can also configure custom parsers using the `:parsers` option. For instance, by default we include `RemoteIp.Parsers.Forwarded` to parse the format specified by RFC 7239. Each parser returns a list of IPs, each of the [`:inet.ip_address/0` type](http://erlang.org/doc/man/inet.html#type-ip_address). If there were any errors (e.g., a malformed header), this should be an empty list. But any one header may also specify multiple IPs, so once again it's important that the relative order is maintained. Thus, in our running example, the `X-Forwarded-For` header should parse as ```elixir # This is what we want [{1, 2, 3, 4}, {1, 1, 1, 1}] ``` and *not* another order like ```elixir # This is NOT what we want [{1, 1, 1, 1}, {1, 2, 3, 4}] ``` The lists returned by each parser are then concatenated together to form one chain of IPs. In our running example, the resulting addresses are ```elixir [{1, 2, 3, 4}, {1, 1, 1, 1}, {2, 2, 2, 2}] ``` ## Finding the client With the list of IPs parsed, `RemoteIp` must then calculate the proper `remote_ip`. To [prevent IP spoofing](http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/), IPs are processed right-to-left. You can think of it as going backwards through the chain of hops: 1. Application is receiving a request from Proxy 3 2. The Proxy 3 to Application hop set the header `Forwarded: for=2.2.2.2` 3. The Proxy 2 to Proxy 3 hop added `1.1.1.1` to the `X-Forwarded-For` header 4. The Proxy 1 to Proxy 2 hop set the `X-Forwarded-For: 1.2.3.4` header 5. Client is sending a request to Proxy 1 We work backwards until we find something that looks like a client IP. This is dictated by the `:proxies` option, which configures the list of known proxy IPs. Any IP that is *not* a known proxy is assumed to be a client. In our running example: 1. The peer address `{3, 3, 3, 3}` is automatically assumed to be a proxy IP, so go through the headers 2. `{2, 2, 2, 2}` is a known proxy IP, so go one hop back 3. `{1, 1, 1, 1}` is a known proxy IP, so go one hop back 4. `{1, 2, 3, 4}` is not a known proxy IP, so we assume it's the client Notice that the peer address is *always* assumed to be "wrong". Therefore, you should not use `RemoteIp` unless your app is behind at least one proxy. Otherwise, it would be trivial for a malicious client to spoof their IP address: if they just set a header themselves, we'll automatically use it to rewrite the `Plug.Conn`'s original (correct) peer address. It's also important to go *backwards* through the chain, or else the client could similarly spoof their IP. For instance, consider if the client in the running example had initially sent the header `Forwarded: for=6.7.8.9`. Then the headers would have come in as ```elixir [{"forwarded", "for=6.7.8.9"}, {"x-forwarded-for", "1.2.3.4, 1.1.1.1"}, {"forwarded", "for=2.2.2.2"}] ``` which would parse out as the IPs ```elixir [{6, 7, 8, 9}, {1, 2, 3, 4}, {1, 1, 1, 1}, {2, 2, 2, 2}] ``` If we were to go *forwards* through this list, we'd immediately return `{6, 7, 8, 9}` as the client IP, even though it was being spoofed by our malicious user. Instead, going backwards still gives us `{1, 2, 3, 4}` even though the client is attempting to spoof the IP with their own headers. This works no matter how many extra headers the client sends. This logic generalizes to any bad actors in the middle of the chain, too. If we add an IP to the `:proxies` list, we're trusting the forwarding headers that it sets. As such, we're implicitly trusting the incoming peer address, even without configuring it. So in our running example, it's impossible not to trust Proxy 3. We believe it when it says the request came from Proxy 2. But if we didn't trust Proxy 2, that's where we stop: we say the client is `{2, 2, 2, 2}` and won't dig further because we don't trust the `X-Forwarded-For` header that came from Proxy 2. Not only are known proxies' headers trusted, but also requests forwarded for [loopback](https://en.wikipedia.org/wiki/Loopback) and [private](https://en.wikipedia.org/wiki/Private_network) IPs: * IPv4 loopback - `127.0.0.0/8` * IPv6 loopback - `::1/128` * IPv4 private network - `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` * IPv6 unique local address - `fc00::/7` These IPs are skipped automatically because they are used internally and are thus generally not the actual client address in production. However, if (say) your app is only deployed in a [VPN](https://en.wikipedia.org/wiki/Virtual_private_network)/[LAN](https://en.wikipedia.org/wiki/Local_area_network), then your clients might actually have these internal IPs. To prevent loopback/private addresses from being considered proxies, configure them as known clients using the `:clients` option. This goes for anything you have listed in `:proxies` as well. For example, you might say that a whole CIDR block belongs to proxies, but then carve out an exception for a single client in that block. ================================================ FILE: integration/tests/basic/.formatter.exs ================================================ [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], import_deps: [:plug] ] ================================================ FILE: integration/tests/basic/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Ignore package tarball (built via "mix hex.build"). basic-*.tar # Temporary files for e.g. tests /tmp ================================================ FILE: integration/tests/basic/README.md ================================================ # Basic integration test This app plugs `RemoteIp` into a simple `Plug.Router` pipeline. Sans configuration, we expect remote\_ip to have been compiled without any debug logs. An app this straightforward serves as a basic smoke test for standard usage of the plug. ================================================ FILE: integration/tests/basic/lib/basic.ex ================================================ defmodule Basic do use Plug.Router plug RemoteIp plug :match plug :dispatch get "/ip" do send_resp(conn, 200, :inet.ntoa(conn.remote_ip)) end end ================================================ FILE: integration/tests/basic/mix.exs ================================================ defmodule Basic.MixProject do use Mix.Project def project do [ app: :basic, version: "0.0.0", elixir: "~> 1.12", deps: [remote_ip: [path: "../../.."]] ] end def application do [extra_applications: [:logger]] end end ================================================ FILE: integration/tests/basic/test/basic_test.exs ================================================ defmodule BasicTest do use ExUnit.Case use Plug.Test import ExUnit.CaptureLog def xff(conn, header) do put_req_header(conn, "x-forwarded-for", header) end def call(conn, opts \\ []) do Basic.call(conn, Basic.init(opts)) end test "GET /ip" do conn = conn(:get, "/ip") |> xff("1.2.3.4,2.3.4.5,127.0.0.1") assert "" == capture_log(fn -> assert call(conn).resp_body == "2.3.4.5" end) end end ================================================ FILE: integration/tests/basic/test/test_helper.exs ================================================ ExUnit.start() ================================================ FILE: integration/tests/custom/.formatter.exs ================================================ [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ] ================================================ FILE: integration/tests/custom/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Ignore package tarball (built via "mix hex.build"). custom-*.tar # Temporary files for e.g. tests /tmp ================================================ FILE: integration/tests/custom/README.md ================================================ # Custom integration test This app customizes the subset of debug messages it wants remote\_ip to actually log. All the others should be removed at compile time. We test this by directly calling `RemoteIp.call/2` & `RemoteIp.from/2` to inspect their log output. This doesn't enumerate all the possible customizations or anything, but it does provide a smoke test. Here, we log the parsed IPs and the resulting remote IP. ```elixir config :remote_ip, debug: [:ips, :ip] ``` ================================================ FILE: integration/tests/custom/config/config.exs ================================================ import Config config :logger, :console, colors: [enabled: false], format: "[$level] $message\n" config :remote_ip, debug: [:ips, :ip] ================================================ FILE: integration/tests/custom/mix.exs ================================================ defmodule Custom.MixProject do use Mix.Project def project do [ app: :custom, version: "0.0.0", elixir: "~> 1.12", deps: [remote_ip: [path: "../../.."]] ] end def application do [extra_applications: [:logger]] end end ================================================ FILE: integration/tests/custom/test/custom_test.exs ================================================ defmodule CustomTest do use ExUnit.Case import ExUnit.CaptureLog @head [ {"user-agent", "test"}, {"x-forwarded-for", "3.14.15.9, 26.53.58.97, 93.238.46.26"} ] @conn %Plug.Conn{ remote_ip: {127, 0, 0, 1}, req_headers: @head } def call(conn, opts \\ []) do RemoteIp.call(conn, RemoteIp.init(opts)) end def from(head, opts \\ []) do RemoteIp.from(head, opts) end test "hit with RemoteIp.call/2" do assert capture_log(fn -> call(@conn) end) == """ [debug] Parsed IPs from forwarding headers: [{3, 14, 15, 9}, {26, 53, 58, 97}, {93, 238, 46, 26}] [debug] Processed remote IP, found client {93, 238, 46, 26} to replace {127, 0, 0, 1} """ end test "miss with RemoteIp.call/2" do assert capture_log(fn -> call(@conn, headers: []) end) == """ [debug] Parsed IPs from forwarding headers: [] [debug] Processed remote IP, no client found to replace {127, 0, 0, 1} """ end test "hit with RemoteIp.from/2" do assert capture_log(fn -> from(@head) end) == """ [debug] Parsed IPs from forwarding headers: [{3, 14, 15, 9}, {26, 53, 58, 97}, {93, 238, 46, 26}] [debug] Processed remote IP, found client {93, 238, 46, 26} """ end test "miss with RemoteIp.from/2" do assert capture_log(fn -> from(@head, headers: []) end) == """ [debug] Parsed IPs from forwarding headers: [] [debug] Processed remote IP, no client found """ end end ================================================ FILE: integration/tests/custom/test/test_helper.exs ================================================ ExUnit.start() ================================================ FILE: integration/tests/debug/.formatter.exs ================================================ [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ] ================================================ FILE: integration/tests/debug/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Ignore package tarball (built via "mix hex.build"). debug-*.tar # Temporary files for e.g. tests /tmp ================================================ FILE: integration/tests/debug/README.md ================================================ # Debug integration test This app compiles remote\_ip with the configuration ```elixir config :remote_ip, debug: true ``` and calls `RemoteIp.call/2` & `RemoteIp.from/2` directly to inspect their debug logs on some basic examples. This gives us coverage on all the possible types of debug messages across the remote\_ip codebase. ================================================ FILE: integration/tests/debug/config/config.exs ================================================ import Config config :logger, :console, colors: [enabled: false], format: "[$level] $message\n" config :remote_ip, debug: true ================================================ FILE: integration/tests/debug/mix.exs ================================================ defmodule Debug.MixProject do use Mix.Project def project do [ app: :debug, version: "0.0.0", elixir: "~> 1.12", deps: [remote_ip: [path: "../../.."]] ] end def application do [extra_applications: [:logger]] end end ================================================ FILE: integration/tests/debug/test/debug_test.exs ================================================ defmodule DebugTest do use ExUnit.Case import ExUnit.CaptureLog @head [ {"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"} ] @conn %Plug.Conn{ remote_ip: {127, 0, 0, 1}, req_headers: @head } def call(opts) do RemoteIp.call(@conn, RemoteIp.init(opts)) end def from(opts) do RemoteIp.from(@head, opts) end describe "RemoteIp.call/2" do test "no client" do opts = [ headers: ~w[xff], proxies: ~w[1.2.0.0/16 2.3.4.5/32], clients: ~w[] ] assert capture_log(fn -> call(opts) end) == """ [debug] Processing remote IP headers: ["xff"] parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded} proxies: ["1.2.0.0/16", "2.3.4.5/32"] clients: [] [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsing IPs from forwarding headers: [{"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}] [debug] {2, 3, 4, 5} is a known proxy IP [debug] {10, 0, 0, 1} is a reserved IP [debug] {1, 2, 3, 4} is a known proxy IP [debug] Processed remote IP, no client found to replace {127, 0, 0, 1} """ end test "known client" do opts = [ headers: ~w[xff], proxies: ~w[1.2.0.0/16 2.3.4.5/32], clients: ~w[1.2.3.4/32] ] assert capture_log(fn -> call(opts) end) == """ [debug] Processing remote IP headers: ["xff"] parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded} proxies: ["1.2.0.0/16", "2.3.4.5/32"] clients: ["1.2.3.4/32"] [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsing IPs from forwarding headers: [{"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}] [debug] {2, 3, 4, 5} is a known proxy IP [debug] {10, 0, 0, 1} is a reserved IP [debug] {1, 2, 3, 4} is a known client IP [debug] Processed remote IP, found client {1, 2, 3, 4} to replace {127, 0, 0, 1} """ end test "assumed client" do opts = [ headers: ~w[xff], proxies: ~w[2.3.4.5/32], clients: ~w[] ] assert capture_log(fn -> call(opts) end) == """ [debug] Processing remote IP headers: ["xff"] parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded} proxies: ["2.3.4.5/32"] clients: [] [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsing IPs from forwarding headers: [{"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}] [debug] {2, 3, 4, 5} is a known proxy IP [debug] {10, 0, 0, 1} is a reserved IP [debug] {1, 2, 3, 4} is an unknown IP, assuming it's the client [debug] Processed remote IP, found client {1, 2, 3, 4} to replace {127, 0, 0, 1} """ end end describe "RemoteIp.from/2" do test "no client" do opts = [ headers: ~w[], proxies: ~w[1.2.0.0/16 2.3.4.5/32], clients: ~w[1.0.0.0/8 2.0.0.0/8 3.0.0.0/8] ] assert capture_log(fn -> from(opts) end) == """ [debug] Processing remote IP headers: [] parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded} proxies: ["1.2.0.0/16", "2.3.4.5/32"] clients: ["1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8"] [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsing IPs from forwarding headers: [] [debug] Parsed IPs from forwarding headers: [] [debug] Processed remote IP, no client found """ end test "known client" do opts = [ headers: ~w[x-forwarded-for], proxies: ~w[1.2.0.0/16 2.3.4.5/32], clients: ~w[3.0.0.0/8] ] assert capture_log(fn -> from(opts) end) == """ [debug] Processing remote IP headers: ["x-forwarded-for"] parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded} proxies: ["1.2.0.0/16", "2.3.4.5/32"] clients: ["3.0.0.0/8"] [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsing IPs from forwarding headers: [{"x-forwarded-for", "3.14.15.9"}] [debug] Parsed IPs from forwarding headers: [{3, 14, 15, 9}] [debug] {3, 14, 15, 9} is a known client IP [debug] Processed remote IP, found client {3, 14, 15, 9} """ end test "assumed client" do opts = [ headers: ~w[x-forwarded-for xff], proxies: ~w[2.3.4.5/32 3.0.0.0/8], clients: ~w[] ] assert capture_log(fn -> from(opts) end) == """ [debug] Processing remote IP headers: ["x-forwarded-for", "xff"] parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded} proxies: ["2.3.4.5/32", "3.0.0.0/8"] clients: [] [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsing IPs from forwarding headers: [{"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}] [debug] Parsed IPs from forwarding headers: [{3, 14, 15, 9}, {1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}] [debug] {2, 3, 4, 5} is a known proxy IP [debug] {10, 0, 0, 1} is a reserved IP [debug] {1, 2, 3, 4} is an unknown IP, assuming it's the client [debug] Processed remote IP, found client {1, 2, 3, 4} """ end end end ================================================ FILE: integration/tests/debug/test/test_helper.exs ================================================ ExUnit.start() ================================================ FILE: integration/tests/parsers/.formatter.exs ================================================ [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], import_deps: [:plug] ] ================================================ FILE: integration/tests/parsers/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Ignore package tarball (built via "mix hex.build"). parsers-*.tar # Temporary files for e.g. tests /tmp ================================================ FILE: integration/tests/parsers/README.md ================================================ # Parsers integration test This app recognizes a custom header named `"forwarding"` which is parsed with the app's own custom implementation of the `RemoteIp.Parser` behaviour. The header is completely made up. Its format is ``` type=ip ``` where * `type` is either `proxy` or `client` * `ip` is a valid IP address Of course, you wouldn't expect to rely on the header _telling_ you if an IP was a proxy. Bad actors could easily spoof the header (at least if it's plaintext like this). In the real world, you'd configure the `RemoteIp` plug. But this format makes for more interesting tests. This is an integration test so that we can compile remote\_ip with debugging enabled, then make sure that the custom `:parsers` option gets logged. ================================================ FILE: integration/tests/parsers/config/config.exs ================================================ import Config config :logger, :console, colors: [enabled: false], format: "[$level] $message\n" config :remote_ip, debug: [:options, :ips] ================================================ FILE: integration/tests/parsers/lib/parsers/forwarding.ex ================================================ defmodule Parsers.Forwarding do @behaviour RemoteIp.Parser @impl RemoteIp.Parser def parse(value) do [type, address] = String.split(value, "=") case type do "proxy" -> [] "client" -> {:ok, ip} = :inet.parse_strict_address(address |> to_charlist()) [ip] end end end ================================================ FILE: integration/tests/parsers/lib/parsers.ex ================================================ defmodule Parsers do use Plug.Router plug RemoteIp, headers: ~w[forwarding], parsers: %{"forwarding" => Parsers.Forwarding} plug :match plug :dispatch get "/ip" do send_resp(conn, 200, :inet.ntoa(conn.remote_ip)) end end ================================================ FILE: integration/tests/parsers/mix.exs ================================================ defmodule Parsers.MixProject do use Mix.Project def project do [ app: :parsers, version: "0.0.0", elixir: "~> 1.12", deps: [remote_ip: [path: "../../.."]] ] end def application do [extra_applications: [:logger]] end end ================================================ FILE: integration/tests/parsers/test/parsers_test.exs ================================================ defmodule ParsersTest do use ExUnit.Case use Plug.Test import ExUnit.CaptureLog def call(conn, opts \\ []) do Parsers.call(conn, Parsers.init(opts)) end test "GET /ip" do head = [ {"forwarding", "client=1.2.3.4"}, {"forwarding", "proxy=10.20.30.40"}, {"forwarding", "client=2.3.4.5"}, {"forwarding", "proxy=20.30.40.50"} ] conn = %{conn(:get, "/ip") | req_headers: head} logs = capture_log(fn -> assert call(conn).resp_body == "2.3.4.5" end) assert logs == """ [debug] Processing remote IP headers: ["forwarding"] parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded, "forwarding" => Parsers.Forwarding} proxies: [] clients: [] [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {2, 3, 4, 5}] """ end end ================================================ FILE: integration/tests/parsers/test/test_helper.exs ================================================ ExUnit.start() ================================================ FILE: integration/tests/purge/.formatter.exs ================================================ [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ] ================================================ FILE: integration/tests/purge/.gitignore ================================================ # The directory Mix will write compiled artifacts to. /_build/ # If you run "mix test --cover", coverage assets end up here. /cover/ # The directory Mix downloads your dependencies sources to. /deps/ # Where third-party dependencies like ExDoc output generated docs. /doc/ # Ignore .fetch files in case you like to edit your project deps locally. /.fetch # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez # Ignore package tarball (built via "mix hex.build"). purge-*.tar # Temporary files for e.g. tests /tmp ================================================ FILE: integration/tests/purge/README.md ================================================ # Purge integration test This app enables remote\_ip debugging, just like the [debug](../debug) integration test. However, it also adjusts the `Logger` configuration to make sure that the [`:compile_time_purge_matching` option](https://hexdocs.pm/logger/1.11.3/Logger.html#module-application-configuration) still works when remote\_ip gets recompiled. Specifically, we purge messages with a level lower than `:info`, which includes the `:debug` messages that `RemoteIp.Debugger` generates. This means that when we compile remote\_ip, none of the debug statements should survive. Even when we set the log level to `:debug` at runtime in the tests, the logs should have been purged at compile-time. This is an important regression to test because older versions of remote\_ip instructed people to disable debug logs using `:compile_time_purge_matching` (cf. [`4512fe5`](https://github.com/ajvondrak/remote_ip/commit/4512fe53cd2b9c2e03924b12961e48a1ff5b0299)), so we should make an effort to ensure their configurations keep working. Of course, it's still possible they used a `:module`/`:function` matcher that is no longer relevant due to the changing internals of the remote\_ip code. But that's on them for matching against private implementation details. 🙃 ================================================ FILE: integration/tests/purge/config/config.exs ================================================ import Config config :logger, :console, format: "[$level] $message\n", colors: [enabled: false] config :logger, compile_time_purge_matching: [[level_lower_than: :info]] config :remote_ip, debug: true ================================================ FILE: integration/tests/purge/mix.exs ================================================ defmodule Purge.MixProject do use Mix.Project def project do [ app: :purge, version: "0.0.0", elixir: "~> 1.12", deps: [remote_ip: [path: "../../.."]] ] end def application do [extra_applications: [:logger]] end end ================================================ FILE: integration/tests/purge/test/purge_test.exs ================================================ defmodule PurgeTest do use ExUnit.Case import ExUnit.CaptureLog @head [{"x-forwarded-for", "3.14.15.9"}] @conn %Plug.Conn{ remote_ip: {127, 0, 0, 1}, req_headers: @head } def call(conn, opts \\ []) do RemoteIp.call(conn, RemoteIp.init(opts)) end def from(head, opts \\ []) do RemoteIp.from(head, opts) end test "logs get purged at compile time" do Logger.configure(level: :debug) assert capture_log(fn -> call(@conn) end) == "" assert capture_log(fn -> from(@head) end) == "" end end ================================================ FILE: integration/tests/purge/test/test_helper.exs ================================================ ExUnit.start() ================================================ FILE: integration/tests.exs ================================================ defmodule Integration.Tests do @path Path.join(__DIR__, "tests") if IO.ANSI.enabled?() do @color "--color" else @color "--no-color" end def run do File.ls!(@path) |> Enum.map(&run/1) |> summarize() end def run(app) do IO.puts("---> Running integration tests on #{app} app") with 0 <- mix(app, "deps.clean", ["--build", "remote_ip"]), 0 <- mix(app, "deps.get"), 0 <- mix(app, "test", [@color]) do {app, :pass} else _ -> {app, :fail} end end def mix(app, task, args \\ []) do cmd = [task | args] dir = Path.expand(app, @path) out = IO.binstream(:stdio, :line) IO.puts(["-->", "mix" | cmd] |> Enum.join(" ")) {_, status} = System.cmd("mix", cmd, cd: dir, into: out) status end def summarize(results) do count(results) Enum.each(results, fn {app, :pass} -> passed(app) {app, :fail} -> failed(app) end) end def count(results) do tests = length(results) fails = Enum.count(results, fn {_, flag} -> flag == :fail end) msg = [plural(tests, "integration test"), ", ", plural(fails, "failure")] IO.puts("") if fails > 0 do IO.ANSI.format([:red | msg]) |> IO.puts() else IO.ANSI.format([:green | msg]) |> IO.puts() end end def plural(1, string), do: "1 #{string}" def plural(n, string), do: "#{n} #{string}s" def passed(app) do IO.ANSI.format([:green, " ✓ #{app}"]) |> IO.puts() end def failed(app) do IO.ANSI.format([:red, " ✗ #{app}"]) |> IO.puts() System.at_exit(fn _ -> exit({:shutdown, 1}) end) end end Integration.Tests.run() ================================================ FILE: lib/remote_ip/block.ex ================================================ defmodule RemoteIp.Block do import Bitwise alias __MODULE__ @moduledoc false defstruct [:proto, :net, :mask] def encode({a, b, c, d}) do <> = <> {:v4, ip} end def encode({a, b, c, d, e, f, g, h}) do <> = <> {:v6, ip} end def contains?(%Block{proto: proto, net: net, mask: mask}, {proto, ip}) do (ip &&& mask) == net end def contains?(%Block{}, {_, _}) do false end def parse!(cidr) do case parse(cidr) do {:ok, block} -> block {:error, message} -> raise ArgumentError, message end end def parse(cidr) do case process(:parts, String.split(cidr, "/", parts: 2)) do {:error, e} -> {:error, "#{e} in CIDR #{inspect(cidr)}"} ok -> ok end end defp process(:parts, [ip, prefix]) do with {:ok, ip} <- process(:ip, ip), {:ok, prefix} <- process(:prefix, prefix) do process(:block, ip, prefix) end end defp process(:parts, [ip]) do with {:ok, ip} <- process(:ip, ip) do process(:block, ip) end end defp process(:ip, address) do case :inet.parse_strict_address(address |> to_charlist()) do {:ok, ip} -> {:ok, encode(ip)} {:error, _} -> {:error, "Invalid address #{inspect(address)}"} end end defp process(:prefix, prefix) do try do {:ok, String.to_integer(prefix)} rescue ArgumentError -> {:error, "Invalid prefix #{inspect(prefix)}"} end end defp process(:block, {:v4, ip}) do process(:block, {:v4, ip}, 32) end defp process(:block, {:v6, ip}) do process(:block, {:v6, ip}, 128) end defp process(:block, {:v4, ip}, prefix) when prefix in 0..32 do ones = 0xFFFFFFFF <> = <>> prefix)::32>> {:ok, %Block{proto: :v4, net: ip &&& mask, mask: mask}} end defp process(:block, {:v6, ip}, prefix) when prefix in 0..128 do ones = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF <> = <>> prefix)::128>> {:ok, %Block{proto: :v6, net: ip &&& mask, mask: mask}} end defp process(:block, _, prefix) do {:error, "Invalid prefix #{inspect(prefix)}"} end defimpl String.Chars, for: Block do def to_string(%Block{proto: :v4, net: net, mask: mask}) do <> = <> "#{:inet.ntoa({a, b, c, d})}/#{bits(mask)}" end def to_string(%Block{proto: :v6, net: net, mask: mask}) do <> = <> "#{:inet.ntoa({a, b, c, d, e, f, g, h})}/#{bits(mask)}" end defp bits(mask) do ones = for <<1::1 <- :binary.encode_unsigned(mask)>>, do: 1 length(ones) end end end ================================================ FILE: lib/remote_ip/debugger.ex ================================================ defmodule RemoteIp.Debugger do require Logger @moduledoc """ Compile-time debugging facilities. `RemoteIp` uses the `debug/3` macro to instrument its implementation with *debug events* at compile time. If an event is enabled, the macro will expand into a `Logger.debug/2` call with a specific message. If an event is disabled, the logging will be purged, thus generating no extra code and having no impact on run time. ## Basic usage Events are fired on every call to `RemoteIp.call/2` or `RemoteIp.from/2`. To enable or disable all debug events at once, you can set a boolean in your `Config` file: ```elixir config :remote_ip, debug: true ``` By default, the debugger is turned off (i.e., `debug: false`). Because `RemoteIp.Debugger` works at compile time, you must make sure to recompile the `:remote_ip` dependency whenever you change the configuration: ```console $ mix deps.clean --build remote_ip ``` ## Advanced usage You may also pass a list of atoms into the `:debug` configuration naming which events to log. These are all the possible events: * `:options` - the keyword options *after* any runtime configuration has been evaluated (see `RemoteIp.Options`) * `:headers` - all incoming headers, either from the `Plug.Conn`'s `req_headers` or the list passed directly into `RemoteIp.from/2`; useful for seeing if you're even getting the forwarding headers you expect in the first place * `:forwarding` - the subset of headers (as configured by `RemoteIp.Options`) that contain forwarding information * `:ips` - the entire sequence of IP addresses parsed from the forwarding headers, in order * `:type` - for each IP (until we find the client), classifies the address either as a known client, a known proxy, a reserved address, or none of the above (and thus presumably a client) * `:ip` - the final result of the remote IP processing; when rewriting the `Plug.Conn`'s `remote_ip`, the message will tell you the original IP that is being replaced Therefore, `debug: true` is equivalent to passing in all of the above: ```elixir config :remote_ip, debug: [:options, :headers, :forwarding, :ips, :type, :ip] ``` But you could disable certain events by removing them from the list. For example, to log only the incoming headers and resulting IP: ```elixir config :remote_ip, debug: [:headers, :ip] ``` ## Interactions with `Logger` Since they both work at compile time, your configuration of `:logger` will also affect the operation of `RemoteIp.Debugger`. For example, it's possible to enable debugging but still purge all the resulting logs: ```elixir # All events *would* be logged... config :remote_ip, debug: true # ...But :debug logs will actually get purged at compile time config :logger, compile_time_purge_matching: [[level_lower_than: :info]] ``` """ @doc """ An internal macro for generating debug logs. There is no reason for you to call this directly. It's used to instrument the `RemoteIp` module at compilation time. """ @spec debug(atom(), [any()], do: any()) :: any() defmacro debug(id, inputs \\ [], do: output) do if debug?(id) do quote do inputs = unquote(inputs) output = unquote(output) unquote(__MODULE__).__log__(unquote(id), inputs, output) output end else output end end @debug Application.compile_env(:remote_ip, :debug, false) cond do is_list(@debug) -> defp debug?(id), do: Enum.member?(@debug, id) is_boolean(@debug) -> defp debug?(_), do: @debug end def __log__(id, inputs, output) do Logger.debug(__message__(id, inputs, output)) end def __message__(:options, [], options) do headers = inspect(options[:headers]) parsers = inspect(options[:parsers]) proxies = inspect(options[:proxies] |> Enum.map(&to_string/1)) clients = inspect(options[:clients] |> Enum.map(&to_string/1)) [ "Processing remote IP\n", " headers: #{headers}\n", " parsers: #{parsers}\n", " proxies: #{proxies}\n", " clients: #{clients}" ] end def __message__(:headers, [], headers) do "Taking forwarding headers from #{inspect(headers)}" end def __message__(:forwarding, [], headers) do "Parsing IPs from forwarding headers: #{inspect(headers)}" end def __message__(:ips, [], ips) do "Parsed IPs from forwarding headers: #{inspect(ips)}" end def __message__(:type, [ip], type) do case type do :client -> "#{inspect(ip)} is a known client IP" :proxy -> "#{inspect(ip)} is a known proxy IP" :reserved -> "#{inspect(ip)} is a reserved IP" :unknown -> "#{inspect(ip)} is an unknown IP, assuming it's the client" end end def __message__(:ip, [old_conn], new_conn) do origin = inspect(old_conn.remote_ip) client = inspect(new_conn.remote_ip) if client != origin do "Processed remote IP, found client #{client} to replace #{origin}" else "Processed remote IP, no client found to replace #{origin}" end end def __message__(:ip, [], ip) do if ip == nil do "Processed remote IP, no client found" else "Processed remote IP, found client #{inspect(ip)}" end end end ================================================ FILE: lib/remote_ip/headers.ex ================================================ defmodule RemoteIp.Headers do @moduledoc """ Functions for parsing IPs from multiple types of forwarding headers. """ @doc """ Extracts all headers with the given names. Note that `Plug.Conn` headers are assumed to have been normalized to lowercase, so the names you give should be in lowercase as well. ## Examples iex> [{"x-foo", "foo"}, {"x-bar", "bar"}, {"x-baz", "baz"}] ...> |> RemoteIp.Headers.take(["x-foo", "x-baz", "x-qux"]) [{"x-foo", "foo"}, {"x-baz", "baz"}] iex> [{"x-dup", "foo"}, {"x-dup", "bar"}, {"x-dup", "baz"}] ...> |> RemoteIp.Headers.take(["x-dup"]) [{"x-dup", "foo"}, {"x-dup", "bar"}, {"x-dup", "baz"}] """ @spec take(Plug.Conn.headers(), [binary()]) :: Plug.Conn.headers() def take(headers, names) do Enum.filter(headers, fn {name, _} -> name in names end) end @doc """ Parses IP addresses out of the given headers. For each header name/value pair, the value is parsed for zero or more IP addresses by the parser corresponding to the name. If no such parser exists in the given map, we fall back to `RemoteIp.Parsers.Generic`. The IPs are concatenated together into a single flat list. Note that the relative order is preserved. That is, each header produce multiple IPs that are kept in the order given by that specific header. Then, in the case of multiple headers, the concatenated list maintains the same order as the headers appeared in the original name/value list. Due to the error-safe nature of the `RemoteIp.Parser` behaviour, headers that do not actually contain valid IP addresses should be safely ignored. ## Examples iex> [{"x-one", "1.2.3.4, 2.3.4.5"}, {"x-two", "3.4.5.6, 4.5.6.7"}] ...> |> RemoteIp.Headers.parse() [{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}, {4, 5, 6, 7}] iex> [{"forwarded", "for=1.2.3.4"}, {"x-forwarded-for", "2.3.4.5"}] ...> |> RemoteIp.Headers.parse() [{1, 2, 3, 4}, {2, 3, 4, 5}] iex> [{"accept", "*/*"}, {"user-agent", "ua"}, {"x-real-ip", "1.2.3.4"}] ...> |> RemoteIp.Headers.parse() [{1, 2, 3, 4}] """ @spec parse(Plug.Conn.headers(), %{binary() => RemoteIp.Parser.t()}) :: [ :inet.ip_address() ] def parse(headers, parsers \\ RemoteIp.Options.default(:parsers)) do Enum.flat_map(headers, fn {name, value} -> parser = Map.get(parsers, name, RemoteIp.Parsers.Generic) parser.parse(value) end) end end ================================================ FILE: lib/remote_ip/options.ex ================================================ defmodule RemoteIp.Options do @headers ~w[forwarded x-forwarded-for x-client-ip x-real-ip] @parsers %{"forwarded" => RemoteIp.Parsers.Forwarded} @proxies [] @clients [] @moduledoc """ The keyword options given to `RemoteIp.init/1` or `RemoteIp.from/2`. You shouldn't need to use this module directly. Its functions are used internally by `RemoteIp` to process configurations and support MFA-style [runtime options](#module-runtime-options). You may pass any of the following keyword arguments into the plug (they get passed to `RemoteIp.init/1`). You can also pass the same keywords directly to `RemoteIp.from/2`. ## `:headers` The `:headers` option should be a list of strings. These are the names of headers that contain forwarding information. The default is ```elixir #{inspect(@headers, pretty: true)} ``` Every request header whose name exactly matches one of these strings will be parsed for IP addresses, which are then used to determine the routing information and ultimately the original client IP. Note that `Plug` normalizes headers to lowercase, so this option should consist of lowercase names. In production, you likely want this to be a singleton - a list of only one string. There are a couple reasons: 1. You usually can't rely on servers to preserve the relative ordering of headers in the HTTP request. For example, the [Cowboy](https://github.com/ninenines/cowboy/) server presently [uses maps](https://github.com/elixir-plug/plug_cowboy/blob/f82f2ff982f04fb4faa3a12fd2b08a7cc56ebe15/lib/plug/cowboy/conn.ex#L125-L127) to represent headers, which don't preserve key order. The order in which we process IPs matters because we take that as the routing information for the request. So if you have multiple competing headers, the routing might be ambiguous, and you could get bad results. 2. It could also be a security issue. Say you're only expecting one header like `X-Forwarded-For`, but configure multiple headers like `["x-forwarded-for", "x-real-ip"]`. Then it'd be easy for a malicious user to just set an extra `X-Real-Ip` header and interfere with the IP parsing (again, due to the sensitive nature of header ordering). We still allow multiple headers because: 1. Users can get up & running faster if the default configuration recognizes all of the common headers. 2. You shouldn't be relying that heavily on IP addresses for security. Even a single plain-text header has enough problems on its own that we can't guarantee its results are accurate. For more details, see the documentation for [the algorithm](algorithm.md). 3. It's more general. Networking setups are often very idiosyncratic, and we want to give users the option to use multiple headers if that's what they need. ## `:parsers` The `:parsers` option should be a map from strings to modules. Each string should be a header name (lowercase), and each module should implement the `RemoteIp.Parser` behaviour. The default is ```elixir #{inspect(@parsers, pretty: true)} ``` Headers with the given name are parsed using the given module. If a header is not found in this map, it will be parsed by `RemoteIp.Parsers.Generic`. So you can use this option to: * add a parser for your own custom header * specialize on the generic parsing of headers like `"x-forwarded-for"` * replace any of the default parsers with one of your own The map you provide for this option is automatically merged into the default using `Map.merge/2`. That way, the stock parsers won't be overridden unless you explicitly provide your own replacement. ## `:proxies` The `:proxies` option should be a list of strings - either individual IPs or ranges in [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) notation. The default is ```elixir #{inspect(@proxies, pretty: true)} ``` For the sake of efficiency, you should prefer CIDR notation where possible. So instead of listing out 256 different addresses for the `1.2.3.x` block, you should say `"1.2.3.0/24"`. These proxies are skipped by [the algorithm](algorithm.md) and are never considered the original client IP, unless specifically overruled by the `:clients` option. In addition to the proxies listed here, note that the following [reserved IP addresses](https://en.wikipedia.org/wiki/Reserved_IP_addresses) are also skipped automatically, as they are presumed to be internal addresses that don't belong to the client: * IPv4 loopback: `127.0.0.0/8` * IPv6 loopback: `::1/128` * IPv4 private network: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` * IPv6 unique local address: `fc00::/7` ## `:clients` The `:clients` option should be a list of strings - either individual IPs or ranges in [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) notation. The default is ```elixir #{inspect(@clients, pretty: true)} ``` For the sake of efficiency, you should prefer CIDR notation where possible. So instead of listing out 256 different addresses for the `1.2.3.x` block, you should say `"1.2.3.0/24"`. These addresses are never considered to be proxies by [the algorithm](algorithm.md). For example, if you configure the `:proxies` option to include `"1.2.3.0/24"` and the `:clients` option to include `"1.2.3.4"`, then every IP in the `1.2.3.x` block would be considered a proxy *except* for `1.2.3.4`. This option can also be used on reserved IP addresses that would otherwise be skipped automatically. For example, if your routing works through a local network, you might actually consider addresses in the `10.x.x.x` block to be clients. You could permit the entire block with `"10.0.0.0/8"`, or even specific IPs in this range like `"10.1.2.3"`. ## Runtime options Every option can also accept a tuple of three elements: `{module, function, arguments}` (MFA). These are passed to `Kernel.apply/3` at runtime, allowing you to dynamically configure the plug, even though the `Plug.Builder` generally calls `c:Plug.init/1` at compilation time. The return value from an MFA should be the same as if you were passing the literal into that option. For instance, the `:proxies` MFA should return a list of IP/CIDR strings. The MFAs you give are re-evaluated on *each call* to `RemoteIp.call/2` or `RemoteIp.from/2`. So be careful not to do anything too expensive at runtime. For example, don't download a list of known proxies, or else it will be re-downloaded on every request. Consider caching the download instead, perhaps using a library like [`Cachex`](https://hexdocs.pm/cachex). ## Examples ### Basic usage Suppose you know: * you are behind proxies in the `1.2.x.x` block * the proxies use the `X-Real-Ip` header * but the IP `1.2.3.4` is actually a client, not one of the proxies Then you could say: ```elixir defmodule MyApp do use Plug.Router plug RemoteIp, headers: ~w[x-real-ip], proxies: ~w[1.2.0.0/16], clients: ~w[1.2.3.4] plug :match plug :dispatch # get "/" do ... end ``` The same options may also be passed into `RemoteIp.from/2`: ```elixir defmodule MySocket do use Phoenix.Socket @options [ headers: ~w[x-real-ip], proxies: ~w[1.2.0.0/16], clients: ~w[1.2.3.4] ] def connect(params, socket, connect_info) do ip = RemoteIp.from(connect_info[:x_headers], @options) # ... end end ``` ### Custom parser Suppose your proxies are using a header with a special format. The name of the header is `X-Special` and the format looks like `ip=127.0.0.1`. First, you'd implement a custom parser: ```elixir defmodule SpecialParser do @behaviour RemoteIp.Parser @impl RemoteIp.Parser def parse(header) do ip = String.replace_prefix(header, "ip=", "") case :inet.parse_strict_address(ip |> to_charlist()) do {:ok, parsed} -> [parsed] _ -> [] end end end ``` Then you would configure the plug with that parser. Make sure to also specify the `:headers` option so that the `X-Special` header actually gets passed to the parser. ```elixir defmodule SpecialApp do use Plug.Router plug RemoteIp, headers: ~w[x-special], parsers: %{"x-special" => SpecialParser} plug :match plug :dispatch # get "/" do ... end ``` ### Using MFAs Suppose you're deploying a release and you want to get the proxy IPs from an environment variable. Because the release is compiled ahead of time, you shouldn't do a `System.get_env/1` inline - it'll just be the value of the environment variable circa compilation time (probably empty!). ```elixir defmodule CompiledApp do use Plug.Router # DON'T DO THIS: the value of the env var gets compiled into the release plug RemoteIp, proxies: System.get_env("PROXIES") |> String.split(",") plug :match plug :dispatch # get "/" do ... end ``` Instead, you can use an MFA to look up the variable at runtime: ```elixir defmodule RuntimeApp do use Plug.Router plug RemoteIp, proxies: {__MODULE__, :proxies, []} def proxies do System.get_env("PROXIES") |> String.split(",", trim: true) end plug :match plug :dispatch # get "/" do ... end ``` """ @doc """ The default value for the given option. """ def default(option) def default(:headers), do: @headers def default(:parsers), do: @parsers def default(:proxies), do: @proxies def default(:clients), do: @clients @doc """ Processes keyword options, delaying the evaluation of MFAs until `unpack/1`. """ def pack(options) do [ headers: pack(options, :headers), parsers: pack(options, :parsers), proxies: pack(options, :proxies), clients: pack(options, :clients) ] end defp pack(options, option) do case Keyword.get(options, option, default(option)) do {m, f, a} -> {m, f, a} value -> evaluate(option, value) end end @doc """ Evaluates options processed by `pack/1`, applying MFAs as needed. """ def unpack(options) do [ headers: unpack(options, :headers), parsers: unpack(options, :parsers), proxies: unpack(options, :proxies), clients: unpack(options, :clients) ] end defp unpack(options, option) do case Keyword.get(options, option) do {m, f, a} -> evaluate(option, apply(m, f, a)) value -> value end end defp evaluate(:headers, headers) do headers end defp evaluate(:parsers, parsers) do Map.merge(default(:parsers), parsers) end defp evaluate(:proxies, proxies) do proxies |> Enum.map(&RemoteIp.Block.parse!/1) end defp evaluate(:clients, clients) do clients |> Enum.map(&RemoteIp.Block.parse!/1) end end ================================================ FILE: lib/remote_ip/parser.ex ================================================ defmodule RemoteIp.Parser do @moduledoc """ Defines the interface for parsing headers into IP addresses. `RemoteIp.Headers.parse/1` dynamically dispatches to different parser modules depending on the name of the header. For example, the `"forwarded"` header is parsed by `RemoteIp.Parsers.Forwarded`, which implements this behaviour. """ @typedoc """ Any module that implements the `RemoteIp.Parser` behaviour. """ @type t() :: module() @doc """ Parses the specific header's value into a list of IP addresses. This callback should be error-safe. For instance, if the header's value is invalid, it should return an empty list. The actual work of converting an individual IP address string into the tuple type should typically be done using `:inet` functions such as `:inet.parse_strict_address/1`. Note that a header may also contain more than one IP address. The order of the list is important because it's interpreted as routing information. Conceptually, the leftmost IP is the source of the request (the client), the rightmost IP is the destination (your server), and anything in the middle lists the proxy hops in order. However, in reality, there may be bad actors or strange routing that makes this more complicated. It's the job of `RemoteIp` to sort that out. This callback should *only* be concerned with faithfully parsing the literal order given by the header. """ @callback parse(header :: binary()) :: [:inet.ip_address()] end ================================================ FILE: lib/remote_ip/parsers/forwarded.ex ================================================ defmodule RemoteIp.Parsers.Forwarded do use Combine @behaviour RemoteIp.Parser @moduledoc """ [RFC 7239](https://tools.ietf.org/html/rfc7239) compliant parser for `Forwarded` headers. This module implements the `RemoteIp.Parser` behaviour. IPs are parsed out of the `for=` pairs across each forwarded element. ## Examples iex> RemoteIp.Parsers.Forwarded.parse("for=1.2.3.4;by=2.3.4.5") [{1, 2, 3, 4}] iex> RemoteIp.Parsers.Forwarded.parse("for=\\"[::1]\\", for=\\"[::2]\\"") [{0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 2}] iex> RemoteIp.Parsers.Forwarded.parse("invalid") [] """ @impl RemoteIp.Parser def parse(header) do case Combine.parse(header, forwarded()) do [elements] -> Enum.flat_map(elements, &parse_forwarded_for/1) _ -> [] end end defp parse_forwarded_for(pairs) do case fors_from(pairs) do [string] -> parse_ip(string) _ambiguous -> [] end end defp fors_from(pairs) do for {key, val} <- pairs, String.downcase(key) == "for", do: val end defp parse_ip(string) do case Combine.parse(string, ip_address()) do [ip] -> [ip] _ -> [] end end # https://tools.ietf.org/html/rfc7239#section-4 defp forwarded do sep_by(forwarded_element(), comma()) |> eof() end defp forwarded_element do sep_by1(forwarded_pair(), char(";")) end defp forwarded_pair do pair = [token(), ignore(char("=")), value()] pipe(pair, &List.to_tuple/1) end defp value do either(token(), quoted_string()) end # https://tools.ietf.org/html/rfc7230#section-3.2.6 defp token do word_of(~r/[!#$%&'*+\-.^_`|~0-9a-zA-Z]/) end defp quoted_string do quoted(string_of(either(qdtext(), quoted_pair()))) end defp quoted(parser) do between(char("\""), parser, char("\"")) end defp string_of(parser) do map(many(parser), &Enum.join/1) end defp qdtext do word_of(~r/[\t \x21\x23-\x5B\x5D-\x7E\x80-\xFF]/) end @quotable ([?\t] ++ Enum.to_list(0x21..0x7E) ++ Enum.to_list(0x80..0xFF)) |> Enum.map(&<<&1::utf8>>) defp quoted_pair do ignore(char("\\")) |> one_of(char(), @quotable) end # https://tools.ietf.org/html/rfc7230#section-7 defp comma do skip(many(either(space(), tab()))) |> char(",") |> skip(many(either(space(), tab()))) end # https://tools.ietf.org/html/rfc7239#section-6 defp ip_address do node_name() |> ignore(option(ignore(char(":")) |> node_port())) |> eof() end defp node_name do choice([ ipv4_address(), between(char("["), ipv6_address(), char("]")), ignore(string("unknown")), ignore(obfuscated()) ]) end defp node_port(previous) do previous |> either(port(), obfuscated()) end defp port do # Have to try to parse the wider integers first due to greediness. For # example, the port "12345" would be matched by fixed_integer(1) and the # remaining "2345" would cause a parse error for the eof in ip_address/0. choice(Enum.map(5..1//-1, &fixed_integer/1)) end defp obfuscated do word_of(~r/^_[a-zA-Z0-9._\-]+/) end # Could follow the ABNF described in # https://tools.ietf.org/html/rfc3986#section-3.2.2, but prefer to lean on # the existing :inet parser - we want its output anyway. defp ipv4_address do map(word_of(~r/[0-9.]/), fn string -> case :inet.parse_ipv4strict_address(string |> to_charlist()) do {:ok, ip} -> ip {:error, :einval} -> {:error, "Invalid IPv4 address"} end end) end defp ipv6_address do map(word_of(~r/[0-9a-f:.]/i), fn string -> case :inet.parse_ipv6strict_address(string |> to_charlist()) do {:ok, ip} -> ip {:error, :einval} -> {:error, "Invalid IPv6 address"} end end) end end ================================================ FILE: lib/remote_ip/parsers/generic.ex ================================================ defmodule RemoteIp.Parsers.Generic do @behaviour RemoteIp.Parser @moduledoc """ Generic parser for forwarding headers. This module implements the `RemoteIp.Parser` behaviour. When there is not a more specific parser, `RemoteIp.Headers.parse/1` falls back to using this one. The value is parsed simply as a comma-separated list of IPs. This is suitable for a wide range of headers, such as `X-Forwarded-For`, `X-Real-IP`, and `X-Client-IP`. Any amount of whitespace is allowed before and after the commas, as well as at the beginning & end of the input. ## Examples iex> RemoteIp.Parsers.Generic.parse("1.2.3.4, 5.6.7.8") [{1, 2, 3, 4}, {5, 6, 7, 8}] iex> RemoteIp.Parsers.Generic.parse(" ::1 ") [{0, 0, 0, 0, 0, 0, 0, 1}] iex> RemoteIp.Parsers.Generic.parse("invalid") [] """ @impl RemoteIp.Parser def parse(header) do header |> split_commas() |> parse_ips() end defp split_commas(header) do header |> String.trim() |> String.split(~r/\s*,\s*/) end defp parse_ips(strings) do List.foldr(strings, [], fn string, ips -> case parse_ip(string) do {:ok, ip} -> [ip | ips] {:error, _} -> ips end end) end defp parse_ip(string) do try do :inet.parse_strict_address(string |> to_charlist()) rescue UnicodeConversionError -> {:error, :invalid_unicode} end end end ================================================ FILE: lib/remote_ip.ex ================================================ defmodule RemoteIp do import RemoteIp.Debugger @behaviour Plug @moduledoc """ A plug to rewrite the `Plug.Conn`'s `remote_ip` based on forwarding headers. Generic comma-separated headers like `X-Forwarded-For`, `X-Real-Ip`, and `X-Client-Ip` are all recognized, as well as the [RFC 7239](https://tools.ietf.org/html/rfc7239) `Forwarded` header. IPs are processed last-to-first to prevent IP spoofing. Read more in the documentation for [the algorithm](algorithm.md). This plug is highly configurable, giving you the power to adapt it to your particular networking infrastructure: * IPs can come from any header(s) you want. You can even implement your own custom parser if you're using a special format. * You can configure the IPs of known proxies & clients so that you never get the wrong results. * All options are configurable at runtime, so you can deploy a single release but still customize it using environment variables, the `Application` environment, or any other arbitrary mechanism. * Still not getting the right IP? You can recompile the plug with debugging enabled to generate logs, and even fine-tune the verbosity by selecting which events to track. ## Usage This plug should be early in your pipeline, or else the `remote_ip` might not get rewritten before your route's logic executes. In [Phoenix](https://hexdocs.pm/phoenix), this might mean plugging `RemoteIp` into your endpoint before the router: ```elixir defmodule MyApp.Endpoint do use Phoenix.Endpoint, otp_app: :my_app plug RemoteIp # plug ... # plug ... plug MyApp.Router end ``` But if you only want to rewrite IPs in a narrower part of your app, you could of course put it in an individual pipeline of your router. In an ordinary `Plug.Router`, you should make sure `RemoteIp` comes before the `:match`/`:dispatch` plugs: ```elixir defmodule MyApp do use Plug.Router plug RemoteIp plug :match plug :dispatch # get "/" do ... end ``` You can also use `RemoteIp.from/2` to determine an IP from a list of headers. This is useful outside of the plug pipeline, where you may not have access to the `Plug.Conn`. For example, you might only be getting the `x_headers` from [`Phoenix.Socket`](https://hexdocs.pm/phoenix/Phoenix.Socket.html): ```elixir defmodule MySocket do use Phoenix.Socket def connect(params, socket, connect_info) do ip = RemoteIp.from(connect_info[:x_headers]) # ... end end ``` ## Configuration Options may be passed as a keyword list via `RemoteIp.init/1` or directly into `RemoteIp.from/2`. At a high level, the following options are available: * `:headers` - a list of header names to consider * `:parsers` - a map from header names to custom parser modules * `:clients` - a list of known client IPs, either plain or in CIDR notation * `:proxies` - a list of known proxy IPs, either plain or in CIDR notation You can specify any option using a tuple of `{module, function_name, arguments}`, which will be called dynamically at runtime to get the equivalent value. For more details about these options, see `RemoteIp.Options`. ## Troubleshooting Getting the right configuration can be tricky. Requests might come in with unexpected headers, or maybe you didn't account for certain proxies, or any number of other issues. Luckily, you can debug `RemoteIp.call/2` and `RemoteIp.from/2` by updating your `Config` file: ```elixir config :remote_ip, debug: true ``` and recompiling the `:remote_ip` dependency: ```console $ mix deps.clean --build remote_ip $ mix deps.compile ``` Then it will generate log messages showing how the IP gets computed. For more details about these messages, as well advanced usage, see `RemoteIp.Debugger`. ## Metadata When you use this plug, `RemoteIp.call/2` will populate the `Logger` metadata under the key `:remote_ip`. This will be the string representation of the final value of the `Plug.Conn`'s `remote_ip`. Even if no client was found in the headers, we still set the metadata to the original IP. You can use this in your logs by updating your `Config` file: ```elixir config :logger, message: "$metadata[$level] $message\\n", metadata: [:remote_ip] ``` Then your logs will look something like this: ```log [info] Running ExampleWeb.Endpoint with cowboy 2.8.0 at 0.0.0.0:4000 (http) [info] Access ExampleWeb.Endpoint at http://localhost:4000 remote_ip=1.2.3.4 [info] GET / remote_ip=1.2.3.4 [debug] Processing with ExampleWeb.PageController.index/2 Parameters: %{} Pipelines: [:browser] remote_ip=1.2.3.4 [info] Sent 200 in 21ms ``` Note that metadata will *not* be set by `RemoteIp.from/2`. """ @impl Plug @doc """ The `c:Plug.init/1` callback. This accepts the keyword options described by `RemoteIp.Options`. Because plug initialization typically happens at compile time, we make sure not to evaluate runtime options until `call/2`. """ def init(opts) do RemoteIp.Options.pack(opts) end @impl Plug @doc """ The `c:Plug.call/2` callback. Rewrites the `Plug.Conn`'s `remote_ip` based on its forwarding headers. Each call will re-evaluate all runtime options. See `RemoteIp.Options` for details. """ def call(conn, opts) do debug :ip, [conn] do ip = ip_from(conn.req_headers, opts) || conn.remote_ip add_metadata(ip) %{conn | remote_ip: ip} end end @doc """ Extracts the remote IP from a list of headers. In cases where you don't have access to a full `Plug.Conn` struct, you can use this function to process the remote IP from a list of key-value pairs representing the headers. You may specify the same options as if you were using the plug. Runtime options are evaluated each time you call this function. See `RemoteIp.Options` for details. If no client IP can be found in the given headers, this function will return `nil`. ## Examples iex> RemoteIp.from([{"x-forwarded-for", "1.2.3.4"}]) {1, 2, 3, 4} iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}] ...> |> RemoteIp.from(headers: ~w[x-foo]) {1, 2, 3, 4} iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}] ...> |> RemoteIp.from(headers: ~w[x-bar]) {2, 3, 4, 5} iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}] ...> |> RemoteIp.from(headers: ~w[x-baz]) nil """ @spec from(Plug.Conn.headers(), keyword()) :: :inet.ip_address() | nil def from(headers, opts \\ []) do debug :ip do ip_from(headers, init(opts)) end end defp ip_from(headers, opts) do opts = options_from(opts) client_from(ips_from(headers, opts), opts) end defp options_from(opts) do debug :options do RemoteIp.Options.unpack(opts) end end defp ips_from(headers, opts) do debug :ips do headers = forwarding_from(headers, opts) RemoteIp.Headers.parse(headers, opts[:parsers]) end end defp forwarding_from(headers, opts) do debug :forwarding do debug(:headers, do: headers) |> RemoteIp.Headers.take(opts[:headers]) end end defp client_from(ips, opts) do Enum.reverse(ips) |> Enum.find(&client?(&1, opts)) end defp client?(ip, opts) do type(ip, opts) in [:client, :unknown] end # https://en.wikipedia.org/wiki/Loopback # https://en.wikipedia.org/wiki/Private_network # https://en.wikipedia.org/wiki/Reserved_IP_addresses @reserved ~w[ 127.0.0.0/8 ::1/128 fc00::/7 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ] |> Enum.map(&RemoteIp.Block.parse!/1) defp type(ip, opts) do debug :type, [ip] do ip = RemoteIp.Block.encode(ip) cond do opts[:clients] |> contains?(ip) -> :client opts[:proxies] |> contains?(ip) -> :proxy @reserved |> contains?(ip) -> :reserved true -> :unknown end end end defp contains?(blocks, ip) do Enum.any?(blocks, &RemoteIp.Block.contains?(&1, ip)) end defp add_metadata(remote_ip) do case :inet.ntoa(remote_ip) do {:error, _} -> :ok ip -> Logger.metadata(remote_ip: to_string(ip)) end end end ================================================ FILE: mix.exs ================================================ defmodule RemoteIp.Mixfile do use Mix.Project def project do [ app: :remote_ip, version: "1.2.0", elixir: "~> 1.12", description: description(), package: package(), deps: deps(), aliases: aliases(), dialyzer: dialyzer(), docs: docs(), test_coverage: test_coverage() ] end def application do [extra_applications: [:logger]] end defp description do "A plug to rewrite the Plug.Conn's remote_ip based on request headers" <> " such as Forwarded, X-Forwarded-For, X-Client-Ip, and X-Real-Ip" end defp package do %{ files: ~w[lib mix.exs README.md LICENSE], licenses: ["MIT"], links: %{"GitHub" => "https://github.com/ajvondrak/remote_ip"} } end defp deps do [ {:combine, "~> 0.10"}, {:plug, "~> 1.14"}, {:ex_doc, "~> 0.34", only: :dev, runtime: false}, {:dialyxir, "~> 1.4", only: [:ci, :dev], runtime: false}, {:excoveralls, "~> 0.18", only: [:ci, :test], runtime: false}, {:castore, "~> 1.0", only: [:ci, :test], runtime: false} ] end defp aliases do [integrate: "run integration/tests.exs"] end defp dialyzer do [plt_file: {:no_warn, "priv/plts/dialyzer.plt"}] end defp docs do [ source_url: "https://github.com/ajvondrak/remote_ip", main: "RemoteIp", extras: ["extras/algorithm.md"] ] end defp test_coverage() do [tool: ExCoveralls] end end ================================================ FILE: test/.formatter.exs ================================================ [ inputs: ["**/*.exs"], import_deps: [:plug], # This is an arbitrarily long line length. While most of the code conforms to # an 80-character limit, many of the parsing tests involve gnarly strings & # IP tuples that are just nicer to have on a single line. There's no good way # of expressing this at a finer granularity (e.g., flagging specific sections # of code), so we just let the tests get away with murder in general. line_length: 800 ] ================================================ FILE: test/remote_ip/block_test.exs ================================================ defmodule RemoteIp.BlockTest do use ExUnit.Case, async: true import Bitwise alias RemoteIp.Block def octets(n) do Stream.repeatedly(fn -> Enum.random(0..255) end) |> Enum.take(n) end def ipv4(octets) do octets |> Enum.join(".") end def hextets(n) do Stream.repeatedly(fn -> Enum.random(0..65_535) end) |> Enum.take(n) end def ipv6(hextets) do hextets |> Enum.map(&Integer.to_string(&1, 16)) |> Enum.join(":") end test "parse vs parse!" do {:ok, success} = Block.parse("127.0.0.1") assert Block.parse!("127.0.0.1") == success {:error, error} = Block.parse("127001") assert_raise ArgumentError, error, fn -> Block.parse!("127001") end end test "parsing invalid CIDR" do assert_raise ArgumentError, ~S'Invalid address "invalid" in CIDR "invalid"', fn -> Block.parse!("invalid") end end test "IPv4 block to string" do assert "3.14.15.92/32" == Block.parse!("3.14.15.92/32") |> to_string() assert "3.14.15.0/24" == Block.parse!("3.14.15.92/24") |> to_string() assert "3.14.0.0/16" == Block.parse!("3.14.15.92/16") |> to_string() assert "3.0.0.0/8" == Block.parse!("3.14.15.92/8") |> to_string() assert "0.0.0.0/0" == Block.parse!("3.14.15.92/0") |> to_string() end test "IPv6 block to string" do assert "123::456/128" == Block.parse!("123::456/128") |> to_string() assert "123::/64" == Block.parse!("123::456/64") |> to_string() assert "::/0" == Block.parse!("123::456/0") |> to_string() end describe "parsing IPv4" do test "invalid address and prefix" do assert_raise ArgumentError, ~S'Invalid address "3.14" in CIDR "3.14/159"', fn -> Block.parse!("3.14/159") end end test "invalid address" do assert_raise ArgumentError, ~S'Invalid address "3.141.592.6" in CIDR "3.141.592.6/5"', fn -> Block.parse!("3.141.592.6/5") end end test "invalid prefix" do assert_raise ArgumentError, ~S'Invalid prefix "invalid" in CIDR "0.0.0.0/invalid"', fn -> Block.parse!("0.0.0.0/invalid") end end test "negative prefix" do assert_raise ArgumentError, ~S'Invalid prefix -1 in CIDR "0.0.0.0/-1"', fn -> Block.parse!("0.0.0.0/-1") end end test "oversized prefix" do assert_raise ArgumentError, ~S'Invalid prefix 33 in CIDR "0.0.0.0/33"', fn -> Block.parse!("0.0.0.0/33") end end test "address sans prefix" do ip = ipv4(octets(4)) assert Block.parse!(ip) == Block.parse!("#{ip}/32") end test "address with zero prefix" do ip = ipv4(octets(4)) assert %Block{proto: :v4, net: 0} = Block.parse!("#{ip}/0") end test "addresses with valid prefixes" do [a, b, c, d] = octets(4) ip = ipv4([a, b, c, d]) {:v4, net_a} = Block.encode({a, 0, 0, 0}) {:v4, net_b} = Block.encode({a, b, 0, 0}) {:v4, net_c} = Block.encode({a, b, c, 0}) {:v4, net_x} = Block.encode({a, b &&& 0b11110000, 0, 0}) assert %Block{proto: :v4, net: ^net_a} = Block.parse!("#{ip}/8") assert %Block{proto: :v4, net: ^net_b} = Block.parse!("#{ip}/16") assert %Block{proto: :v4, net: ^net_c} = Block.parse!("#{ip}/24") assert %Block{proto: :v4, net: ^net_x} = Block.parse!("#{ip}/12") end test "masks" do assert %Block{mask: 0b00000000000000000000000000000000} = Block.parse!("0.0.0.0/0") assert %Block{mask: 0b10000000000000000000000000000000} = Block.parse!("0.0.0.0/1") assert %Block{mask: 0b11000000000000000000000000000000} = Block.parse!("0.0.0.0/2") assert %Block{mask: 0b11100000000000000000000000000000} = Block.parse!("0.0.0.0/3") assert %Block{mask: 0b11110000000000000000000000000000} = Block.parse!("0.0.0.0/4") assert %Block{mask: 0b11111000000000000000000000000000} = Block.parse!("0.0.0.0/5") assert %Block{mask: 0b11111100000000000000000000000000} = Block.parse!("0.0.0.0/6") assert %Block{mask: 0b11111110000000000000000000000000} = Block.parse!("0.0.0.0/7") assert %Block{mask: 0b11111111000000000000000000000000} = Block.parse!("0.0.0.0/8") assert %Block{mask: 0b11111111100000000000000000000000} = Block.parse!("0.0.0.0/9") assert %Block{mask: 0b11111111110000000000000000000000} = Block.parse!("0.0.0.0/10") assert %Block{mask: 0b11111111111000000000000000000000} = Block.parse!("0.0.0.0/11") assert %Block{mask: 0b11111111111100000000000000000000} = Block.parse!("0.0.0.0/12") assert %Block{mask: 0b11111111111110000000000000000000} = Block.parse!("0.0.0.0/13") assert %Block{mask: 0b11111111111111000000000000000000} = Block.parse!("0.0.0.0/14") assert %Block{mask: 0b11111111111111100000000000000000} = Block.parse!("0.0.0.0/15") assert %Block{mask: 0b11111111111111110000000000000000} = Block.parse!("0.0.0.0/16") assert %Block{mask: 0b11111111111111111000000000000000} = Block.parse!("0.0.0.0/17") assert %Block{mask: 0b11111111111111111100000000000000} = Block.parse!("0.0.0.0/18") assert %Block{mask: 0b11111111111111111110000000000000} = Block.parse!("0.0.0.0/19") assert %Block{mask: 0b11111111111111111111000000000000} = Block.parse!("0.0.0.0/20") assert %Block{mask: 0b11111111111111111111100000000000} = Block.parse!("0.0.0.0/21") assert %Block{mask: 0b11111111111111111111110000000000} = Block.parse!("0.0.0.0/22") assert %Block{mask: 0b11111111111111111111111000000000} = Block.parse!("0.0.0.0/23") assert %Block{mask: 0b11111111111111111111111100000000} = Block.parse!("0.0.0.0/24") assert %Block{mask: 0b11111111111111111111111110000000} = Block.parse!("0.0.0.0/25") assert %Block{mask: 0b11111111111111111111111111000000} = Block.parse!("0.0.0.0/26") assert %Block{mask: 0b11111111111111111111111111100000} = Block.parse!("0.0.0.0/27") assert %Block{mask: 0b11111111111111111111111111110000} = Block.parse!("0.0.0.0/28") assert %Block{mask: 0b11111111111111111111111111111000} = Block.parse!("0.0.0.0/29") assert %Block{mask: 0b11111111111111111111111111111100} = Block.parse!("0.0.0.0/30") assert %Block{mask: 0b11111111111111111111111111111110} = Block.parse!("0.0.0.0/31") assert %Block{mask: 0b11111111111111111111111111111111} = Block.parse!("0.0.0.0/32") end test "reserved ranges" do assert Block.parse!("127.0.0.0/8") == %Block{ proto: :v4, net: :binary.decode_unsigned(<<127, 0, 0, 0>>), mask: :binary.decode_unsigned(<<255, 0, 0, 0>>) } assert Block.parse!("10.0.0.0/8") == %Block{ proto: :v4, net: :binary.decode_unsigned(<<10, 0, 0, 0>>), mask: :binary.decode_unsigned(<<255, 0, 0, 0>>) } assert Block.parse!("172.16.0.0/12") == %Block{ proto: :v4, net: :binary.decode_unsigned(<<172, 16, 0, 0>>), mask: :binary.decode_unsigned(<<255, 240, 0, 0>>) } assert Block.parse!("192.168.0.0/16") == %Block{ proto: :v4, net: :binary.decode_unsigned(<<192, 168, 0, 0>>), mask: :binary.decode_unsigned(<<255, 255, 0, 0>>) } end end describe "parsing IPv6" do test "invalid address and prefix" do assert_raise ArgumentError, ~S'Invalid address "a:b:c" in CIDR "a:b:c/1/2/3"', fn -> Block.parse!("a:b:c/1/2/3") end end test "invalid address" do assert_raise ArgumentError, ~S'Invalid address "f7::u" in CIDR "f7::u/12"', fn -> Block.parse!("f7::u/12") end end test "invalid prefix" do assert_raise ArgumentError, ~S'Invalid prefix "1/2/3" in CIDR "::a:b:c/1/2/3"', fn -> Block.parse!("::a:b:c/1/2/3") end end test "negative prefix" do assert_raise ArgumentError, ~S'Invalid prefix -1 in CIDR "::/-1"', fn -> Block.parse!("::/-1") end end test "oversized prefix" do assert_raise ArgumentError, ~S'Invalid prefix 129 in CIDR "::/129"', fn -> Block.parse!("::/129") end end test "address sans prefix" do ip = ipv6(hextets(8)) assert Block.parse!(ip) == Block.parse!("#{ip}/128") end test "address with zero prefix" do ip = ipv6(hextets(8)) assert %Block{proto: :v6, net: 0} = Block.parse!("#{ip}/0") end test "addresses with valid prefixes" do [a, b, c, d, e, f, g, h] = hextets(8) ip = ipv6([a, b, c, d, e, f, g, h]) {:v6, net_a} = Block.encode({a, 0, 0, 0, 0, 0, 0, 0}) {:v6, net_b} = Block.encode({a, b, 0, 0, 0, 0, 0, 0}) {:v6, net_c} = Block.encode({a, b, c, 0, 0, 0, 0, 0}) {:v6, net_d} = Block.encode({a, b, c, d, 0, 0, 0, 0}) {:v6, net_e} = Block.encode({a, b, c, d, e, 0, 0, 0}) {:v6, net_f} = Block.encode({a, b, c, d, e, f, 0, 0}) {:v6, net_g} = Block.encode({a, b, c, d, e, f, g, 0}) {:v6, net_x} = Block.encode({a, b, c, d, e &&& 0b1000000000000000, 0, 0, 0}) assert %Block{proto: :v6, net: ^net_a} = Block.parse!("#{ip}/16") assert %Block{proto: :v6, net: ^net_b} = Block.parse!("#{ip}/32") assert %Block{proto: :v6, net: ^net_c} = Block.parse!("#{ip}/48") assert %Block{proto: :v6, net: ^net_d} = Block.parse!("#{ip}/64") assert %Block{proto: :v6, net: ^net_e} = Block.parse!("#{ip}/80") assert %Block{proto: :v6, net: ^net_f} = Block.parse!("#{ip}/96") assert %Block{proto: :v6, net: ^net_g} = Block.parse!("#{ip}/112") assert %Block{proto: :v6, net: ^net_x} = Block.parse!("#{ip}/65") end test "masks" do assert %Block{mask: 0b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/0") assert %Block{mask: 0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/1") assert %Block{mask: 0b11000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/2") assert %Block{mask: 0b11100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/3") assert %Block{mask: 0b11110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/4") assert %Block{mask: 0b11111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/5") assert %Block{mask: 0b11111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/6") assert %Block{mask: 0b11111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/7") assert %Block{mask: 0b11111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/8") assert %Block{mask: 0b11111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/9") assert %Block{mask: 0b11111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/10") assert %Block{mask: 0b11111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/11") assert %Block{mask: 0b11111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/12") assert %Block{mask: 0b11111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/13") assert %Block{mask: 0b11111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/14") assert %Block{mask: 0b11111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/15") assert %Block{mask: 0b11111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/16") assert %Block{mask: 0b11111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/17") assert %Block{mask: 0b11111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/18") assert %Block{mask: 0b11111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/19") assert %Block{mask: 0b11111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/20") assert %Block{mask: 0b11111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/21") assert %Block{mask: 0b11111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/22") assert %Block{mask: 0b11111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/23") assert %Block{mask: 0b11111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/24") assert %Block{mask: 0b11111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/25") assert %Block{mask: 0b11111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/26") assert %Block{mask: 0b11111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/27") assert %Block{mask: 0b11111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/28") assert %Block{mask: 0b11111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/29") assert %Block{mask: 0b11111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/30") assert %Block{mask: 0b11111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/31") assert %Block{mask: 0b11111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/32") assert %Block{mask: 0b11111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/33") assert %Block{mask: 0b11111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/34") assert %Block{mask: 0b11111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/35") assert %Block{mask: 0b11111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/36") assert %Block{mask: 0b11111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/37") assert %Block{mask: 0b11111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/38") assert %Block{mask: 0b11111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/39") assert %Block{mask: 0b11111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/40") assert %Block{mask: 0b11111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/41") assert %Block{mask: 0b11111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/42") assert %Block{mask: 0b11111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/43") assert %Block{mask: 0b11111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/44") assert %Block{mask: 0b11111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/45") assert %Block{mask: 0b11111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/46") assert %Block{mask: 0b11111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/47") assert %Block{mask: 0b11111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/48") assert %Block{mask: 0b11111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/49") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/50") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/51") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/52") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/53") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/54") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/55") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/56") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/57") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/58") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/59") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/60") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/61") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/62") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/63") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/64") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/65") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/66") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/67") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/68") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/69") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/70") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/71") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000} = Block.parse!("::/72") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000} = Block.parse!("::/73") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000} = Block.parse!("::/74") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000} = Block.parse!("::/75") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000} = Block.parse!("::/76") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000} = Block.parse!("::/77") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000} = Block.parse!("::/78") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000} = Block.parse!("::/79") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000} = Block.parse!("::/80") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000} = Block.parse!("::/81") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000} = Block.parse!("::/82") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000} = Block.parse!("::/83") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000} = Block.parse!("::/84") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000} = Block.parse!("::/85") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000} = Block.parse!("::/86") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000} = Block.parse!("::/87") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000} = Block.parse!("::/88") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000} = Block.parse!("::/89") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000} = Block.parse!("::/90") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000} = Block.parse!("::/91") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000} = Block.parse!("::/92") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000} = Block.parse!("::/93") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000} = Block.parse!("::/94") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000} = Block.parse!("::/95") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000} = Block.parse!("::/96") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000} = Block.parse!("::/97") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000} = Block.parse!("::/98") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000} = Block.parse!("::/99") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000} = Block.parse!("::/100") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000} = Block.parse!("::/101") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000} = Block.parse!("::/102") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000} = Block.parse!("::/103") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000} = Block.parse!("::/104") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000} = Block.parse!("::/105") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000} = Block.parse!("::/106") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000} = Block.parse!("::/107") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000} = Block.parse!("::/108") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000} = Block.parse!("::/109") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000} = Block.parse!("::/110") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000} = Block.parse!("::/111") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000} = Block.parse!("::/112") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000} = Block.parse!("::/113") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000} = Block.parse!("::/114") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000} = Block.parse!("::/115") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000} = Block.parse!("::/116") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000} = Block.parse!("::/117") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000} = Block.parse!("::/118") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000} = Block.parse!("::/119") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000} = Block.parse!("::/120") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000} = Block.parse!("::/121") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000} = Block.parse!("::/122") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000} = Block.parse!("::/123") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000} = Block.parse!("::/124") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000} = Block.parse!("::/125") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100} = Block.parse!("::/126") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110} = Block.parse!("::/127") assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111} = Block.parse!("::/128") end test "reserved ranges" do assert Block.parse!("::1/128") == %Block{ proto: :v6, net: 0x00000000000000000000000000000001, mask: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF } assert Block.parse!("fc00::/7") == %Block{ proto: :v6, net: 0xFC000000000000000000000000000000, mask: 0xFE000000000000000000000000000000 } end end describe "IPv4 membership" do test "inside block" do block = Block.parse!("192.168.0.0/16") for a <- 0..255, b <- 0..255 do assert Block.contains?(block, Block.encode({192, 168, a, b})) end end test "outside block" do block = Block.parse!("192.168.0.0/16") refute Block.contains?(block, Block.encode({192, 167, 255, 255})) refute Block.contains?(block, Block.encode({192, 169, 0, 0})) refute Block.contains?(block, Block.encode({191, 168, 0, 0})) refute Block.contains?(block, Block.encode({191, 255, 255, 255})) refute Block.contains?(block, Block.encode({194, 0, 0, 0})) refute Block.contains?(block, Block.encode({31, 41, 59, 27})) end test "with exact match" do block = Block.parse!("127.0.0.1/32") refute Block.contains?(block, Block.encode({127, 0, 0, 0})) assert Block.contains?(block, Block.encode({127, 0, 0, 1})) refute Block.contains?(block, Block.encode({127, 0, 0, 2})) end test "with zero-length prefix" do block = Block.parse!("0.0.0.0/0") [a, b, c, d] = octets(4) assert Block.contains?(block, Block.encode({a, b, c, d})) end test "with lower bits that are masked off" do block = Block.parse!("192.168.100.14/24") assert block.net == :binary.decode_unsigned(<<192, 168, 100, 0>>) for member <- 0..255 do assert Block.contains?(block, Block.encode({192, 168, 100, member})) end end test "against IPv6" do block = Block.parse!("0.0.0.0/0") [a, b, c, d, e, f, g, h] = hextets(8) refute Block.contains?(block, Block.encode({a, b, c, d, e, f, g, h})) end end describe "IPv6 membership" do test "inside block" do block = Block.parse!("1111:2222:3333:4444:5555:6666:7777:8800/120") for member <- 0x8800..0x88FF do assert Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, member})) end end test "outside block" do block = Block.parse!("1111:2222:3333:4444:5555:6666:7777:8800/120") refute Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0x87FF})) refute Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0x8900})) end test "with exact match" do block = Block.parse!("::1/128") refute Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 0})) assert Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 1})) refute Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 2})) end test "with zero-length prefix" do block = Block.parse!("::/0") [a, b, c, d, e, f, g, h] = hextets(8) assert Block.contains?(block, Block.encode({a, b, c, d, e, f, g, h})) end test "with lower bits that are masked off" do block = Block.parse!("a:b:c:d:e:f::/48") assert block.net == :binary.decode_unsigned(<<0x000A::16, 0x000B::16, 0x00C::16, 0::16, 0::16, 0::16, 0::16, 0::16>>) [d, e, f, g, h] = hextets(5) assert Block.contains?(block, Block.encode({0x000A, 0x000B, 0x000C, d, e, f, g, h})) end test "against IPv4" do block = Block.parse!("::/0") [a, b, c, d] = octets(4) refute Block.contains?(block, Block.encode({a, b, c, d})) end end end ================================================ FILE: test/remote_ip/headers_test.exs ================================================ defmodule RemoteIp.HeadersTest do use ExUnit.Case, async: true doctest RemoteIp.Headers test "taking from an empty list of headers" do headers = [] allowed = ["a", "b", "c"] assert RemoteIp.Headers.take(headers, allowed) == [] end test "taking no headers" do headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}] allowed = [] assert RemoteIp.Headers.take(headers, allowed) == [] end test "taking all headers" do headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}] allowed = ["a", "b", "c"] assert RemoteIp.Headers.take(headers, allowed) == headers end test "taking a subset of headers" do headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}] allowed = ["a", "c"] assert RemoteIp.Headers.take(headers, allowed) == [{"a", "1"}, {"c", "3"}] end test "taking a superset of headers" do headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}] allowed = ["a", "z"] assert RemoteIp.Headers.take(headers, allowed) == [{"a", "1"}] end test "taking a disjoint set of headers" do headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}] allowed = ["x", "y", "z"] assert RemoteIp.Headers.take(headers, allowed) == [] end test "taking duplicate headers" do headers = [{"a", "1"}, {"a", "2"}, {"b", "3"}] allowed = ["a"] assert RemoteIp.Headers.take(headers, allowed) == [{"a", "1"}, {"a", "2"}] end test "parsing Forwarded headers" do ips = [ {1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}, {3, 4, 5, 6}, {0, 0, 0, 0, 4, 5, 6, 7} ] headers = [ {"forwarded", ~S'for=1.2.3.4'}, {"forwarded", ~S'for="[::2:3:4:5]";proto=http;host=example.com'}, {"forwarded", ~S'proto=http;for=3.4.5.6;by=127.0.0.1'}, {"forwarded", ~S'proto=http;host=example.com;for="[::4:5:6:7]"'} ] assert RemoteIp.Headers.parse(headers) == ips headers = [ {"forwarded", ~S'for=1.2.3.4, for="[::2:3:4:5]";proto=http;host=example.com'}, {"forwarded", ~S'proto=http;for=3.4.5.6;by=127.0.0.1'}, {"forwarded", ~S'proto=http;host=example.com;for="[::4:5:6:7]"'} ] assert RemoteIp.Headers.parse(headers) == ips headers = [ {"forwarded", ~S'for=1.2.3.4, for="[::2:3:4:5]";proto=http;host=example.com, proto=http;for=3.4.5.6;by=127.0.0.1'}, {"forwarded", ~S'proto=http;host=example.com;for="[::4:5:6:7]"'} ] assert RemoteIp.Headers.parse(headers) == ips headers = [ {"forwarded", ~S'for=1.2.3.4'}, {"forwarded", ~S'for="[::2:3:4:5]";proto=http;host=example.com'}, {"forwarded", ~S'proto=http;for=3.4.5.6;by=127.0.0.1, proto=http;host=example.com;for="[::4:5:6:7]"'} ] assert RemoteIp.Headers.parse(headers) == ips headers = [ {"forwarded", ~S'for=1.2.3.4'}, {"forwarded", ~S'for="[::2:3:4:5]";proto=http;host=example.com, proto=http;for=3.4.5.6;by=127.0.0.1, proto=http;host=example.com;for="[::4:5:6:7]"'} ] assert RemoteIp.Headers.parse(headers) == ips end test "parsing generic headers" do headers = [ {"generic", "1.1.1.1, unknown, 2.2.2.2"}, {"generic", " 3.3.3.3 , 4.4.4.4,not_an_ip"}, {"generic", "5.5.5.5,::6:6:6:6"}, {"generic", "unknown,5,7.7.7.7"} ] ips = [ {1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}, {4, 4, 4, 4}, {5, 5, 5, 5}, {0, 0, 0, 0, 6, 6, 6, 6}, {7, 7, 7, 7} ] assert RemoteIp.Headers.parse(headers) == ips end test "parsing an unrecognized header falls back to generic parsing" do headers = [ {"x-forwarded-for", "1.1.1.1,2.2.2.2"}, {"x-real-ip", "3.3.3.3, 4.4.4.4"}, {"x-client-ip", "5.5.5.5"} ] ips = [ {1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}, {4, 4, 4, 4}, {5, 5, 5, 5} ] assert RemoteIp.Headers.parse(headers) == ips end test "parsing multiple kinds of headers" do headers = [ {"forwarded", "for=1.1.1.1"}, {"x-forwarded-for", "2.2.2.2"}, {"forwarded", "for=3.3.3.3, for=4.4.4.4"}, {"x-forwarded-for", "invalid"}, {"forwarded", "for=5.5.5.5"}, {"x-forwarded-for", "6.6.6.6, 7.7.7.7"}, {"invalid", "header"} ] ips = [ {1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}, {4, 4, 4, 4}, {5, 5, 5, 5}, {6, 6, 6, 6}, {7, 7, 7, 7} ] assert RemoteIp.Headers.parse(headers) == ips end defmodule Custom do @behaviour RemoteIp.Parser @impl RemoteIp.Parser def parse(_) do [{1, 2, 3, 4}] end end test "using custom parsers" do headers = [ {"x-custom", "parser's gonna parse"}, {"x-forwarded-for", "2.3.4.5"}, {"forwarded", "for=3.4.5.6"} ] ips = [ {1, 2, 3, 4}, {2, 3, 4, 5} ] assert RemoteIp.Headers.parse(headers, %{"x-custom" => Custom}) == ips end end ================================================ FILE: test/remote_ip/options_test.exs ================================================ defmodule RemoteIp.OptionsTest do use ExUnit.Case, async: true defmodule MFA do use Agent def setup do {:ok, _} = Agent.start_link(fn -> [] end, name: __MODULE__) :ok end def get(opt) do Agent.get(__MODULE__, fn opts -> Keyword.get(opts, opt) end) end def put(opt, val) do Agent.update(__MODULE__, fn opts -> Keyword.put(opts, opt, val) end) end end setup do MFA.setup() end describe "pack" do test "unknown option" do packed = RemoteIp.Options.pack(unknown: :option) refute Keyword.has_key?(packed, :unknown) assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :proxies) assert Keyword.has_key?(packed, :clients) end test ":headers default" do packed = RemoteIp.Options.pack([]) assert "forwarded" in packed[:headers] assert "x-forwarded-for" in packed[:headers] assert "x-client-ip" in packed[:headers] assert "x-real-ip" in packed[:headers] end test ":headers list" do packed = RemoteIp.Options.pack(headers: ~w[a b c]) assert packed[:headers] == ~w[a b c] assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :proxies) assert Keyword.has_key?(packed, :clients) end test ":headers mfa" do packed = RemoteIp.Options.pack(headers: {MFA, :get, [:headers]}) assert packed[:headers] == {MFA, :get, [:headers]} assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :proxies) assert Keyword.has_key?(packed, :clients) end test ":parsers map" do packed = RemoteIp.Options.pack(parsers: %{"foo" => Bar}) assert is_map(packed[:parsers]) assert packed[:parsers]["foo"] == Bar assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :proxies) assert Keyword.has_key?(packed, :clients) end test ":parsers default" do packed = RemoteIp.Options.pack([]) assert is_map(packed[:parsers]) assert packed[:parsers]["forwarded"] == RemoteIp.Parsers.Forwarded assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :proxies) assert Keyword.has_key?(packed, :clients) end test ":parsers mfa" do packed = RemoteIp.Options.pack(parsers: {MFA, :get, [:parsers]}) assert packed[:parsers] == {MFA, :get, [:parsers]} assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :proxies) assert Keyword.has_key?(packed, :clients) end test ":proxies default" do packed = RemoteIp.Options.pack([]) assert packed[:proxies] == [] assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :clients) end test ":proxies list" do packed = RemoteIp.Options.pack(proxies: ~w[123.0.0.0/8]) assert [%RemoteIp.Block{} = block] = packed[:proxies] assert to_string(block) == "123.0.0.0/8" assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :clients) end test ":proxies mfa" do packed = RemoteIp.Options.pack(proxies: {MFA, :get, [:proxies]}) assert packed[:proxies] == {MFA, :get, [:proxies]} assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :clients) end test ":clients default" do packed = RemoteIp.Options.pack([]) assert packed[:clients] == [] assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :proxies) end test ":clients list" do packed = RemoteIp.Options.pack(clients: ~w[234.0.0.0/8]) assert [%RemoteIp.Block{} = block] = packed[:clients] assert to_string(block) == "234.0.0.0/8" assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :proxies) end test ":clients mfa" do packed = RemoteIp.Options.pack(clients: {MFA, :get, [:clients]}) assert packed[:clients] == {MFA, :get, [:clients]} assert Keyword.has_key?(packed, :headers) assert Keyword.has_key?(packed, :parsers) assert Keyword.has_key?(packed, :proxies) end end describe "unpack" do test ":headers default" do packed = RemoteIp.Options.pack([]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:headers] == packed[:headers] end test ":headers list" do packed = RemoteIp.Options.pack(headers: ~w[a b c]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:headers] == packed[:headers] end test ":headers mfa" do packed = RemoteIp.Options.pack(headers: {MFA, :get, [:headers]}) MFA.put(:headers, ~w[a b c]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:headers] == ~w[a b c] MFA.put(:headers, ~w[d e f]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:headers] == ~w[d e f] end test ":parsers default" do packed = RemoteIp.Options.pack([]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:parsers] == packed[:parsers] end test ":parsers map" do packed = RemoteIp.Options.pack(parsers: %{"foo" => Bar}) unpacked = RemoteIp.Options.unpack(packed) parsers = %{"forwarded" => RemoteIp.Parsers.Forwarded, "foo" => Bar} assert unpacked[:parsers] == parsers end test ":parsers mfa" do packed = RemoteIp.Options.pack(parsers: {MFA, :get, [:parsers]}) MFA.put(:parsers, %{"foo" => Bar}) unpacked = RemoteIp.Options.unpack(packed) parsers = %{"forwarded" => RemoteIp.Parsers.Forwarded, "foo" => Bar} assert unpacked[:parsers] == parsers MFA.put(:parsers, %{"bar" => Baz}) unpacked = RemoteIp.Options.unpack(packed) parsers = %{"forwarded" => RemoteIp.Parsers.Forwarded, "bar" => Baz} assert unpacked[:parsers] == parsers end test ":proxies default" do packed = RemoteIp.Options.pack([]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:proxies] == packed[:proxies] end test ":proxies list" do packed = RemoteIp.Options.pack(proxies: ~w[123.0.0.0/8 234.0.0.0/8]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:proxies] == packed[:proxies] end test ":proxies mfa" do packed = RemoteIp.Options.pack(proxies: {MFA, :get, [:proxies]}) MFA.put(:proxies, ~w[123.0.0.0/8]) unpacked = RemoteIp.Options.unpack(packed) assert [%RemoteIp.Block{} = block] = unpacked[:proxies] assert to_string(block) == "123.0.0.0/8" MFA.put(:proxies, ~w[234.0.0.0/8]) unpacked = RemoteIp.Options.unpack(packed) assert [%RemoteIp.Block{} = block] = unpacked[:proxies] assert to_string(block) == "234.0.0.0/8" end test ":clients default" do packed = RemoteIp.Options.pack([]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:clients] == packed[:clients] end test ":clients list" do packed = RemoteIp.Options.pack(clients: ~w[123.0.0.0/8 234.0.0.0/8]) unpacked = RemoteIp.Options.unpack(packed) assert unpacked[:clients] == packed[:clients] end test ":clients mfa" do packed = RemoteIp.Options.pack(clients: {MFA, :get, [:clients]}) MFA.put(:clients, ~w[123.0.0.0/8]) unpacked = RemoteIp.Options.unpack(packed) assert [%RemoteIp.Block{} = block] = unpacked[:clients] assert to_string(block) == "123.0.0.0/8" MFA.put(:clients, ~w[234.0.0.0/8]) unpacked = RemoteIp.Options.unpack(packed) assert [%RemoteIp.Block{} = block] = unpacked[:clients] assert to_string(block) == "234.0.0.0/8" end end end ================================================ FILE: test/remote_ip/parsers/forwarded_test.exs ================================================ defmodule RemoteIp.Parsers.ForwardedTest do use ExUnit.Case, async: true alias RemoteIp.Parsers.Forwarded doctest Forwarded describe "parsing" do test "RFC 7239 examples" do parsed = Forwarded.parse(~S'for="_gazonk"') assert parsed == [] parsed = Forwarded.parse(~S'For="[2001:db8:cafe::17]:4711"') assert parsed == [{8193, 3512, 51966, 0, 0, 0, 0, 23}] parsed = Forwarded.parse(~S'for=192.0.2.60;proto=http;by=203.0.113.43') assert parsed == [{192, 0, 2, 60}] parsed = Forwarded.parse(~S'for=192.0.2.43, for=198.51.100.17') assert parsed == [{192, 0, 2, 43}, {198, 51, 100, 17}] end test "case insensitivity" do assert [{0, 0, 0, 0}] == Forwarded.parse(~S'for=0.0.0.0') assert [{0, 0, 0, 0}] == Forwarded.parse(~S'foR=0.0.0.0') assert [{0, 0, 0, 0}] == Forwarded.parse(~S'fOr=0.0.0.0') assert [{0, 0, 0, 0}] == Forwarded.parse(~S'fOR=0.0.0.0') assert [{0, 0, 0, 0}] == Forwarded.parse(~S'For=0.0.0.0') assert [{0, 0, 0, 0}] == Forwarded.parse(~S'FoR=0.0.0.0') assert [{0, 0, 0, 0}] == Forwarded.parse(~S'FOr=0.0.0.0') assert [{0, 0, 0, 0}] == Forwarded.parse(~S'FOR=0.0.0.0') assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse(~S'for="[::]"') assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse(~S'foR="[::]"') assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse(~S'fOr="[::]"') assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse(~S'fOR="[::]"') assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse(~S'For="[::]"') assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse(~S'FoR="[::]"') assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse(~S'FOr="[::]"') assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse(~S'FOR="[::]"') end test "IPv4" do assert [] == Forwarded.parse(~S'for=') assert [] == Forwarded.parse(~S'for=1') assert [] == Forwarded.parse(~S'for=1.2') assert [] == Forwarded.parse(~S'for=1.2.3') assert [] == Forwarded.parse(~S'for=1000.2.3.4') assert [] == Forwarded.parse(~S'for=1.2000.3.4') assert [] == Forwarded.parse(~S'for=1.2.3000.4') assert [] == Forwarded.parse(~S'for=1.2.3.4000') assert [] == Forwarded.parse(~S'for=1abc.2.3.4') assert [] == Forwarded.parse(~S'for=1.2abc.3.4') assert [] == Forwarded.parse(~S'for=1.2.3.4abc') assert [] == Forwarded.parse(~S'for=1.2.3abc.4') assert [] == Forwarded.parse(~S'for=1.2.3.4abc') assert [] == Forwarded.parse(~S'for="1.2.3.4') assert [] == Forwarded.parse(~S'for=1.2.3.4"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for=1.2.3.4') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="1.2.3.4"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="\1.2\.3.\4"') end test "IPv4 with port" do assert [] == Forwarded.parse(~S'for=1.2.3.4:') assert [] == Forwarded.parse(~S'for=1.2.3.4:1') assert [] == Forwarded.parse(~S'for=1.2.3.4:12') assert [] == Forwarded.parse(~S'for=1.2.3.4:123') assert [] == Forwarded.parse(~S'for=1.2.3.4:1234') assert [] == Forwarded.parse(~S'for=1.2.3.4:12345') assert [] == Forwarded.parse(~S'for=1.2.3.4:123456') assert [] == Forwarded.parse(~S'for=1.2.3.4:_underscore') assert [] == Forwarded.parse(~S'for=1.2.3.4:no_underscore') assert [] == Forwarded.parse(~S'for="1.2.3.4:"') assert [] == Forwarded.parse(~S'for="1.2.3.4:123456"') assert [] == Forwarded.parse(~S'for="1.2.3.4:no_underscore"') assert [] == Forwarded.parse(~S'for="1.2\.3.4\:no_un\der\score"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="1.2.3.4:1"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="1.2.3.4:12"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="1.2.3.4:123"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="1.2.3.4:1234"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="1.2.3.4:12345"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="1.2.3.4:_underscore"') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for="\1.2\.3.4\:_po\r\t"') end test "improperly formatted IPv6" do assert [] == Forwarded.parse(~S'for=[127.0.0.1]') assert [] == Forwarded.parse(~S'for="[127.0.0.1]"') assert [] == Forwarded.parse(~S'for=::127.0.0.1') assert [] == Forwarded.parse(~S'for=[::127.0.0.1]') assert [] == Forwarded.parse(~S'for="::127.0.0.1"') assert [] == Forwarded.parse(~S'for="[::127.0.0.1"') assert [] == Forwarded.parse(~S'for="::127.0.0.1]"') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8"') assert [] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8]"') end test "IPv6 with port" do assert [] == Forwarded.parse(~S'for=::1.2.3.4:') assert [] == Forwarded.parse(~S'for=::1.2.3.4:1') assert [] == Forwarded.parse(~S'for=::1.2.3.4:12') assert [] == Forwarded.parse(~S'for=::1.2.3.4:123') assert [] == Forwarded.parse(~S'for=::1.2.3.4:1234') assert [] == Forwarded.parse(~S'for=::1.2.3.4:12345') assert [] == Forwarded.parse(~S'for=::1.2.3.4:123456') assert [] == Forwarded.parse(~S'for=::1.2.3.4:_underscore') assert [] == Forwarded.parse(~S'for=::1.2.3.4:no_underscore') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:1') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:12') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:123') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:1234') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:12345') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:123456') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:_underscore') assert [] == Forwarded.parse(~S'for=[::1.2.3.4]:no_underscore') assert [] == Forwarded.parse(~S'for="::1.2.3.4:"') assert [] == Forwarded.parse(~S'for="::1.2.3.4:123456"') assert [] == Forwarded.parse(~S'for="::1.2.3.4:no_underscore"') assert [] == Forwarded.parse(~S'for="::1.2\.3.4\:no_un\der\score"') assert [] == Forwarded.parse(~S'for="[::1.2.3.4]:"') assert [] == Forwarded.parse(~S'for="[::1.2.3.4]:123456"') assert [] == Forwarded.parse(~S'for="[::1.2.3.4]:no_underscore"') assert [] == Forwarded.parse(~S'for="\[::1.2\.3.4]\:no_un\der\score"') assert [] == Forwarded.parse(~S'for="::1.2.3.4:1"') assert [] == Forwarded.parse(~S'for="::1.2.3.4:12"') assert [] == Forwarded.parse(~S'for="::1.2.3.4:123"') assert [] == Forwarded.parse(~S'for="::1.2.3.4:1234"') assert [] == Forwarded.parse(~S'for="::1.2.3.4:12345"') assert [] == Forwarded.parse(~S'for="::1.2.3.4:_underscore"') assert [] == Forwarded.parse(~S'for="::\1.2\.3.4\:_po\r\t"') assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse(~S'for="[::1.2.3.4]:1"') assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse(~S'for="[::1.2.3.4]:12"') assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse(~S'for="[::1.2.3.4]:123"') assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse(~S'for="[::1.2.3.4]:1234"') assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse(~S'for="[::1.2.3.4]:12345"') assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse(~S'for="[::1.2.3.4]:_underscore"') assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse(~S'for="[::\1.2\.3.4\]\:_po\r\t"') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:1') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:12') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:123') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:1234') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:12345') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:123456') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:_underscore') assert [] == Forwarded.parse(~S'for=1:2:3:4:5:6:7:8:no_underscore') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:1') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:12') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:123') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:1234') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:12345') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:123456') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:_underscore') assert [] == Forwarded.parse(~S'for=[1:2:3:4:5:6:7:8]:no_underscore') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:123456"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:no_underscore"') assert [] == Forwarded.parse(~S'for="::1.2\.3.4\:no_un\der\score"') assert [] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:"') assert [] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:123456"') assert [] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:no_underscore"') assert [] == Forwarded.parse(~S'for="\[1:2\:3:4:5:6:7:8]\:no_un\der\score"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:1"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:12"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:123"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:1234"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:12345"') assert [] == Forwarded.parse(~S'for="1:2:3:4:5:6:7:8:_underscore"') assert [] == Forwarded.parse(~S'for="\1:2\:3:4:5:6:7:8\:_po\r\t"') assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:1"') assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:12"') assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:123"') assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:1234"') assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:12345"') assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse(~S'for="[1:2:3:4:5:6:7:8]:_underscore"') assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse(~S'for="[1:2:3:4:\5:6\:7:8\]\:_po\r\t"') end test "IPv6 without ::" do assert [{0x0001, 0x0023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456:7890:a:bc:def:d34d]"') assert [{0x0001, 0x0023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23:456:7890:a:bc:1.2.3.4]"') end test "IPv6 with :: at position 0" do assert [{0x0000, 0x0023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[::23:456:7890:a:bc:def:d34d]"') assert [{0x0000, 0x0023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[::23:456:7890:a:bc:1.2.3.4]"') assert [{0x0000, 0x0000, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[::456:7890:a:bc:def:d34d]"') assert [{0x0000, 0x0000, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[::456:7890:a:bc:1.2.3.4]"') assert [{0x0000, 0x0000, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[::7890:a:bc:def:d34d]"') assert [{0x0000, 0x0000, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[::7890:a:bc:1.2.3.4]"') assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[::a:bc:def:d34d]"') assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[::a:bc:1.2.3.4]"') assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[::bc:def:d34d]"') assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[::bc:1.2.3.4]"') assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[::def:d34d]"') assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[::1.2.3.4]"') assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34D}] == Forwarded.parse(~S'for="[::d34d]"') assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[::]"') end test "IPv6 with :: at position 1" do assert [{0x0001, 0x000, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1::456:7890:a:bc:def:d34d]"') assert [{0x0001, 0x000, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1::456:7890:a:bc:1.2.3.4]"') assert [{0x0001, 0x000, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1::7890:a:bc:def:d34d]"') assert [{0x0001, 0x000, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1::7890:a:bc:1.2.3.4]"') assert [{0x0001, 0x000, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1::a:bc:def:d34d]"') assert [{0x0001, 0x000, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1::a:bc:1.2.3.4]"') assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1::bc:def:d34d]"') assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1::bc:1.2.3.4]"') assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1::def:d34d]"') assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1::1.2.3.4]"') assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34D}] == Forwarded.parse(~S'for="[1::d34d]"') assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[1::]"') end test "IPv6 with :: at position 2" do assert [{0x0001, 0x023, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23::7890:a:bc:def:d34d]"') assert [{0x0001, 0x023, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23::7890:a:bc:1.2.3.4]"') assert [{0x0001, 0x023, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23::a:bc:def:d34d]"') assert [{0x0001, 0x023, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23::a:bc:1.2.3.4]"') assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23::bc:def:d34d]"') assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23::bc:1.2.3.4]"') assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23::def:d34d]"') assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23::1.2.3.4]"') assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34D}] == Forwarded.parse(~S'for="[1:23::d34d]"') assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[1:23::]"') end test "IPv6 with :: at position 3" do assert [{0x0001, 0x023, 0x0456, 0x0000, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456::a:bc:def:d34d]"') assert [{0x0001, 0x023, 0x0456, 0x0000, 0x000A, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23:456::a:bc:1.2.3.4]"') assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456::bc:def:d34d]"') assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23:456::bc:1.2.3.4]"') assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456::def:d34d]"') assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23:456::1.2.3.4]"') assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456::d34d]"') assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[1:23:456::]"') end test "IPv6 with :: at position 4" do assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456:7890::bc:def:d34d]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x00BC, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23:456:7890::bc:1.2.3.4]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456:7890::def:d34d]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23:456:7890::1.2.3.4]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0000, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456:7890::d34d]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[1:23:456:7890::]"') end test "IPv6 with :: at position 5" do assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x0000, 0x0DEF, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456:7890:a::def:d34d]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x0000, 0x0102, 0x0304}] == Forwarded.parse(~S'for="[1:23:456:7890:a::1.2.3.4]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x0000, 0x0000, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456:7890:a::d34d]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[1:23:456:7890:a::]"') end test "IPv6 with :: at position 6" do assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0000, 0xD34D}] == Forwarded.parse(~S'for="[1:23:456:7890:a:bc::d34d]"') assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[1:23:456:7890:a:bc::]"') end test "IPv6 with leading zeroes" do assert [{0x0000, 0x0001, 0x0002, 0x0003, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[0:01:002:0003:0000::]"') assert [{0x000A, 0x001A, 0x002A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[0a:01a:002a::]"') assert [{0x00AB, 0x01AB, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[0ab:01ab::]"') assert [{0x0ABC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse(~S'for="[0abc::]"') end test "IPv6 with mixed case" do assert [{0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD}] == Forwarded.parse(~S'for="[abcd:abcD:abCd:abCD:aBcd:aBcD:aBCd:aBCD]"') assert [{0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD}] == Forwarded.parse(~S'for="[Abcd:AbcD:AbCd:AbCD:ABcd:ABcD:ABCd:ABCD]"') end test "semicolons" do assert [{1, 2, 3, 4}] == Forwarded.parse(~S'for=1.2.3.4;proto=http;by=2.3.4.5') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'proto=http;for=1.2.3.4;by=2.3.4.5') assert [{1, 2, 3, 4}] == Forwarded.parse(~S'proto=http;by=2.3.4.5;for=1.2.3.4') assert [] == Forwarded.parse(~S'for=1.2.3.4proto=http;by=2.3.4.5') assert [] == Forwarded.parse(~S'proto=httpfor=1.2.3.4;by=2.3.4.5') assert [] == Forwarded.parse(~S'proto=http;by=2.3.4.5for=1.2.3.4') assert [] == Forwarded.parse(~S'for=1.2.3.4;proto=http;by=2.3.4.5;') assert [] == Forwarded.parse(~S'proto=http;for=1.2.3.4;by=2.3.4.5;') assert [] == Forwarded.parse(~S'proto=http;by=2.3.4.5;for=1.2.3.4;') assert [] == Forwarded.parse(~S'for=1.2.3.4;proto=http;for=2.3.4.5') assert [] == Forwarded.parse(~S'for=1.2.3.4;for=2.3.4.5;proto=http') assert [] == Forwarded.parse(~S'proto=http;for=1.2.3.4;for=2.3.4.5') end test "parameters other than `for`" do assert [] == Forwarded.parse(~S'by=1.2.3.4') assert [] == Forwarded.parse(~S'host=example.com') assert [] == Forwarded.parse(~S'proto=http') assert [] == Forwarded.parse(~S'by=1.2.3.4;proto=http;host=example.com') end test "bad whitespace" do assert [] == Forwarded.parse(~S'for= 1.2.3.4') assert [] == Forwarded.parse(~S'for = 1.2.3.4') assert [] == Forwarded.parse(~S'for=1.2.3.4; proto=http') assert [] == Forwarded.parse(~S'for=1.2.3.4 ;proto=http') assert [] == Forwarded.parse(~S'for=1.2.3.4 ; proto=http') assert [] == Forwarded.parse(~S'proto=http; for=1.2.3.4') assert [] == Forwarded.parse(~S'proto=http ;for=1.2.3.4') assert [] == Forwarded.parse(~S'proto=http ; for=1.2.3.4') end test "commas" do assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(~S'for=1.2.3.4, for=2.3.4.5') assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}] == Forwarded.parse(~S'for=1.2.3.4, for="[::2:3:4:5]"') assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}] == Forwarded.parse(~S'for=1.2.3.4, for="[::2:3:4:5]"') assert [{0, 0, 0, 0, 1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(~S'for="[::1:2:3:4]", for=2.3.4.5') assert [{0, 0, 0, 0, 1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}] == Forwarded.parse(~S'for="[::1:2:3:4]", for="[::2:3:4:5]"') end test "optional whitespace" do assert [{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}, {4, 5, 6, 7}, {5, 6, 7, 8}] == Forwarded.parse("for=1.2.3.4,for=2.3.4.5,\sfor=3.4.5.6\s,for=4.5.6.7\s,\sfor=5.6.7.8") assert [{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}, {4, 5, 6, 7}, {5, 6, 7, 8}] == Forwarded.parse("for=1.2.3.4,for=2.3.4.5,\tfor=3.4.5.6\t,for=4.5.6.7\t,\tfor=5.6.7.8") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\s\s,\s\sfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\s\s,\s\tfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\s\s,\t\sfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\s\s,\t\tfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\s\t,\s\sfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\s\t,\s\tfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\s\t,\t\sfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\s\t,\t\tfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\s,\s\sfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\s,\s\tfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\s,\t\sfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\s,\t\tfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\t,\s\sfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\t,\s\tfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\t,\t\sfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\t,\t\tfor=2.3.4.5") assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse("for=1.2.3.4\t\s\s\s\s\t\s\t\s\t,\t\s\s\t\tfor=2.3.4.5") end test "commas and semicolons" do assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}, {3, 4, 5, 6}, {0, 0, 0, 0, 4, 5, 6, 7}] == Forwarded.parse(~S'for=1.2.3.4, for="[::2:3:4:5]";proto=http;host=example.com, proto=http;for=3.4.5.6;by=127.0.0.1, proto=http;host=example.com;for="[::4:5:6:7]"') end end end ================================================ FILE: test/remote_ip/parsers/generic_test.exs ================================================ defmodule RemoteIp.Parsers.GenericTest do use ExUnit.Case, async: true alias RemoteIp.Parsers.Generic doctest Generic describe "parsing" do test "bad IPs" do assert [] == Generic.parse("") assert [] == Generic.parse(" ") assert [] == Generic.parse("not_an_ip") assert [] == Generic.parse("unknown") assert [] == Generic.parse(<<240, 253, 253, 253>>) end test "bad IPv4" do assert [] == Generic.parse("1") assert [] == Generic.parse("1.2") assert [] == Generic.parse("1.2.3") assert [] == Generic.parse("1000.2.3.4") assert [] == Generic.parse("1.2000.3.4") assert [] == Generic.parse("1.2.3000.4") assert [] == Generic.parse("1.2.3.4000") assert [] == Generic.parse("1abc.2.3.4") assert [] == Generic.parse("1.2abc.3.4") assert [] == Generic.parse("1.2.3.4abc") assert [] == Generic.parse("1.2.3abc.4") assert [] == Generic.parse("1.2.3.4abc") assert [] == Generic.parse("1.2.3.4.5") end test "bad IPv6" do assert [] == Generic.parse("1:") assert [] == Generic.parse("1:2") assert [] == Generic.parse("1:2:3") assert [] == Generic.parse("1:2:3:4") assert [] == Generic.parse("1:2:3:4:5") assert [] == Generic.parse("1:2:3:4:5:6") assert [] == Generic.parse("1:2:3:4:5:6:7") assert [] == Generic.parse("1:2:3:4:5:6:7:8:") assert [] == Generic.parse("1:2:3:4:5:6:7:8:9") assert [] == Generic.parse("1:::2:3:4:5:6:7:8") assert [] == Generic.parse("a:b:c:d:e:f::g") end test "IPv4" do assert [{1, 2, 3, 4}] == Generic.parse("1.2.3.4") assert [{1, 2, 3, 4}] == Generic.parse(" 1.2.3.4 ") end test "IPv6 without ::" do assert [{0x0001, 0x0023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1:23:456:7890:a:bc:def:d34d") assert [{0x0001, 0x0023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1:23:456:7890:a:bc:1.2.3.4") end test "IPv6 with :: at position 0" do assert [{0x0000, 0x0023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("::23:456:7890:a:bc:def:d34d") assert [{0x0000, 0x0023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("::23:456:7890:a:bc:1.2.3.4") assert [{0x0000, 0x0000, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("::456:7890:a:bc:def:d34d") assert [{0x0000, 0x0000, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("::456:7890:a:bc:1.2.3.4") assert [{0x0000, 0x0000, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("::7890:a:bc:def:d34d") assert [{0x0000, 0x0000, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("::7890:a:bc:1.2.3.4") assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("::a:bc:def:d34d") assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("::a:bc:1.2.3.4") assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("::bc:def:d34d") assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0102, 0x0304}] == Generic.parse("::bc:1.2.3.4") assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Generic.parse("::def:d34d") assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse("::1.2.3.4") assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34D}] == Generic.parse("::d34d") assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("::") end test "IPv6 with :: at position 1" do assert [{0x0001, 0x000, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1::456:7890:a:bc:def:d34d") assert [{0x0001, 0x000, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1::456:7890:a:bc:1.2.3.4") assert [{0x0001, 0x000, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1::7890:a:bc:def:d34d") assert [{0x0001, 0x000, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1::7890:a:bc:1.2.3.4") assert [{0x0001, 0x000, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1::a:bc:def:d34d") assert [{0x0001, 0x000, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1::a:bc:1.2.3.4") assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1::bc:def:d34d") assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1::bc:1.2.3.4") assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Generic.parse("1::def:d34d") assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse("1::1.2.3.4") assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34D}] == Generic.parse("1::d34d") assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("1::") end test "IPv6 with :: at position 2" do assert [{0x0001, 0x023, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1:23::7890:a:bc:def:d34d") assert [{0x0001, 0x023, 0x0000, 0x7890, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1:23::7890:a:bc:1.2.3.4") assert [{0x0001, 0x023, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1:23::a:bc:def:d34d") assert [{0x0001, 0x023, 0x0000, 0x0000, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1:23::a:bc:1.2.3.4") assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1:23::bc:def:d34d") assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1:23::bc:1.2.3.4") assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Generic.parse("1:23::def:d34d") assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse("1:23::1.2.3.4") assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34D}] == Generic.parse("1:23::d34d") assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("1:23::") end test "IPv6 with :: at position 3" do assert [{0x0001, 0x023, 0x0456, 0x0000, 0x000A, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1:23:456::a:bc:def:d34d") assert [{0x0001, 0x023, 0x0456, 0x0000, 0x000A, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1:23:456::a:bc:1.2.3.4") assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1:23:456::bc:def:d34d") assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1:23:456::bc:1.2.3.4") assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Generic.parse("1:23:456::def:d34d") assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse("1:23:456::1.2.3.4") assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34D}] == Generic.parse("1:23:456::d34d") assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("1:23:456::") end test "IPv6 with :: at position 4" do assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x00BC, 0x0DEF, 0xD34D}] == Generic.parse("1:23:456:7890::bc:def:d34d") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x00BC, 0x0102, 0x0304}] == Generic.parse("1:23:456:7890::bc:1.2.3.4") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0DEF, 0xD34D}] == Generic.parse("1:23:456:7890::def:d34d") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse("1:23:456:7890::1.2.3.4") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0000, 0xD34D}] == Generic.parse("1:23:456:7890::d34d") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("1:23:456:7890::") end test "IPv6 with :: at position 5" do assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x0000, 0x0DEF, 0xD34D}] == Generic.parse("1:23:456:7890:a::def:d34d") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x0000, 0x0102, 0x0304}] == Generic.parse("1:23:456:7890:a::1.2.3.4") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x0000, 0x0000, 0xD34D}] == Generic.parse("1:23:456:7890:a::d34d") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x0000, 0x0000, 0x0000}] == Generic.parse("1:23:456:7890:a::") end test "IPv6 with :: at position 6" do assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0000, 0xD34D}] == Generic.parse("1:23:456:7890:a:bc::d34d") assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000A, 0x00BC, 0x0000, 0x0000}] == Generic.parse("1:23:456:7890:a:bc::") end test "IPv6 with leading zeroes" do assert [{0x0000, 0x0001, 0x0002, 0x0003, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("0:01:002:0003:0000::") assert [{0x000A, 0x001A, 0x002A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("0a:01a:002a::") assert [{0x00AB, 0x01AB, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("0ab:01ab::") assert [{0x0ABC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse("0abc::") end test "IPv6 with mixed case" do assert [{0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD}] == Generic.parse("abcd:abcD:abCd:abCD:aBcd:aBcD:aBCd:aBCD") assert [{0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD, 0xABCD}] == Generic.parse("Abcd:AbcD:AbCd:AbCD:ABcd:ABcD:ABCd:ABCD") end test "commas with optional whitespace" do assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse("127.0.0.1,::1") assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse("127.0.0.1,\s::1") assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse("127.0.0.1\s,::1") assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse("127.0.0.1\s,\s::1") assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse("\s\t\s\t127.0.0.1\t\t\s\s,\s\t\t\s::1\t") end end end ================================================ FILE: test/remote_ip_test.exs ================================================ defmodule RemoteIpTest do use ExUnit.Case, async: true use Plug.Test doctest RemoteIp @unknown [ {"forwarded", "for=unknown"}, {"x-forwarded-for", "not_an_ip"}, {"x-client-ip", "_obf"}, {"x-real-ip", "1.2.3"}, {"custom", "::g"} ] @loopback [ {"forwarded", "for=127.0.0.1"}, {"x-forwarded-for", "::1"}, {"x-client-ip", "127.0.0.2"}, {"x-real-ip", "::::::1"}, {"custom", "127.127.127.127"} ] @private [ {"forwarded", "for=10.0.0.1"}, {"x-forwarded-for", "172.16.0.1"}, {"x-client-ip", "fd00::"}, {"x-real-ip", "192.168.10.10"}, {"custom", "172.31.41.59"} ] @public_v4 [ {"forwarded", "for=2.71.82.8"}, {"x-forwarded-for", "2.71.82.8"}, {"x-client-ip", "2.71.82.8"}, {"x-real-ip", "2.71.82.8"}, {"custom", "2.71.82.8"} ] @public_v6 [ {"forwarded", "for=\"[::2.71.82.8]\""}, {"x-forwarded-for", "::247:5208"}, {"x-client-ip", "0:0:0:0:0:0:2.71.82.8"}, {"x-real-ip", "0::0:247:5208"}, {"custom", "0:0::2.71.82.8"} ] def call(conn, opts \\ []) do RemoteIp.call(conn, RemoteIp.init(opts)) end describe "call/2" do test "no headers" do peer = {86, 75, 30, 9} head = [] conn = %Plug.Conn{remote_ip: peer, req_headers: head} assert call(conn).remote_ip == peer assert Logger.metadata()[:remote_ip] == "86.75.30.9" end test "invalid ip address" do peer = "an invalid ip" head = [] conn = %Plug.Conn{remote_ip: peer, req_headers: head} assert call(conn).remote_ip == peer assert Logger.metadata()[:remote_ip] == nil end for {header, value} <- @unknown do test "#{header} header from unknown IP" do peer = {1, 2, 3, 4} head = [{unquote(header), unquote(value)}] conn = %Plug.Conn{remote_ip: peer, req_headers: head} opts = [headers: [unquote(header)]] assert call(conn, opts).remote_ip == peer assert Logger.metadata()[:remote_ip] == "1.2.3.4" end end for {header, value} <- @loopback do test "#{header} header from loopback IP" do peer = {0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF} head = [{unquote(header), unquote(value)}] conn = %Plug.Conn{remote_ip: peer, req_headers: head} opts = [headers: [unquote(header)]] assert call(conn, opts).remote_ip == peer assert Logger.metadata()[:remote_ip] == "d:e:a:d:b:e:e:f" end end for {header, value} <- @private do test "#{header} header from private IP" do peer = {0xDE, 0xAD, 0, 0, 0, 0, 0xBE, 0xEF} head = [{unquote(header), unquote(value)}] conn = %Plug.Conn{remote_ip: peer, req_headers: head} opts = [headers: [unquote(header)]] assert call(conn, opts).remote_ip == peer assert Logger.metadata()[:remote_ip] == "de:ad::be:ef" end end for {header, value} <- @public_v4 do test "#{header} header from public IP (v4)" do peer = {3, 141, 59, 27} head = [{unquote(header), unquote(value)}] conn = %Plug.Conn{remote_ip: peer, req_headers: head} opts = [headers: [unquote(header)]] assert call(conn, opts).remote_ip == {2, 71, 82, 8} assert Logger.metadata()[:remote_ip] == "2.71.82.8" end end for {header, value} <- @public_v6 do test "#{header} header from public IP (v6)" do peer = {3, 141, 59, 27} head = [{unquote(header), unquote(value)}] conn = %Plug.Conn{remote_ip: peer, req_headers: head} opts = [headers: [unquote(header)]] assert call(conn, opts).remote_ip == {0, 0, 0, 0, 0, 0, 583, 21000} assert Logger.metadata()[:remote_ip] == "::2.71.82.8" end end end describe "from/2" do test "no headers" do head = [] assert RemoteIp.from(head) == nil assert Logger.metadata()[:remote_ip] == nil end for {header, value} <- @unknown do test "#{header} header from unknown IP" do head = [{unquote(header), unquote(value)}] opts = [headers: [unquote(header)]] assert RemoteIp.from(head, opts) == nil assert Logger.metadata()[:remote_ip] == nil end end for {header, value} <- @loopback do test "#{header} header from loopback IP" do head = [{unquote(header), unquote(value)}] opts = [headers: [unquote(header)]] assert RemoteIp.from(head, opts) == nil assert Logger.metadata()[:remote_ip] == nil end end for {header, value} <- @private do test "#{header} header from private IP" do head = [{unquote(header), unquote(value)}] opts = [headers: [unquote(header)]] assert RemoteIp.from(head, opts) == nil assert Logger.metadata()[:remote_ip] == nil end end for {header, value} <- @public_v4 do test "#{header} header from public IP (v4)" do head = [{unquote(header), unquote(value)}] opts = [headers: [unquote(header)]] assert RemoteIp.from(head, opts) == {2, 71, 82, 8} assert Logger.metadata()[:remote_ip] == nil end end for {header, value} <- @public_v6 do test "#{header} header from public IP (v6)" do head = [{unquote(header), unquote(value)}] opts = [headers: [unquote(header)]] assert RemoteIp.from(head, opts) == {0, 0, 0, 0, 0, 0, 583, 21000} assert Logger.metadata()[:remote_ip] == nil end end end @proxies [ {"forwarded", "for=1.2.3.4"}, {"x-forwarded-for", "::a"}, {"x-client-ip", "1:2:3:4:5:6:7:8"}, {"x-real-ip", "4.4.4.4"} ] describe ":proxies option" do test "can block presumed clients" do head = @proxies opts = [proxies: ~w[1.2.0.0/16 ::a/128 4.0.0.0/8 1::/30]] assert RemoteIp.from(head, opts) == nil end test "cannot block known clients" do head = @proxies opts = [proxies: ~w[0.0.0.0/0 ::/0], clients: ~w[1.2.0.0/16]] assert RemoteIp.from(head, opts) == {1, 2, 3, 4} end test "always includes reserved IPs" do head = @proxies ++ @loopback ++ @private opts = [proxies: ~w[1.2.0.0/16 ::a/128 4.0.0.0/8 1::/30 8.8.8.8/32]] assert RemoteIp.from(head, opts) == nil end test "can be an MFA" do head = [{"x-forwarded-for", "1.2.3.4, 2.3.4.5"}] opts = [proxies: {Application, :get_env, [:remote_ip_test, :proxies]}] Application.put_env(:remote_ip_test, :proxies, []) assert RemoteIp.from(head, opts) == {2, 3, 4, 5} Application.put_env(:remote_ip_test, :proxies, ~w[2.0.0.0/8]) assert RemoteIp.from(head, opts) == {1, 2, 3, 4} end end @clients [ {"forwarded", "for=2.71.82.81"}, {"x-forwarded-for", "82.84.59.0"}, {"x-client-ip", "45.235.36.0"}, {"x-real-ip", "28.74.71.35"} ] describe ":clients option" do test "can allow reserved IPs" do head = @loopback ++ @private opts = [clients: ~w[192.168.10.0/24]] assert RemoteIp.from(head, opts) == {192, 168, 10, 10} end test "can allow known proxies" do head = @clients opts = [ proxies: ~w[2.0.0.0/8 82.84.0.0/16 45.235.36.0/24 28.74.71.35/32], clients: ~w[2.71.0.0/16] ] assert RemoteIp.from(head, opts) == {2, 71, 82, 81} end test "doesn't impact presumed clients" do head = @clients opts = [clients: ~w[2.0.0.0/8 82.84.0.0/16 45.235.36.0/24 28.74.71.35/32]] assert RemoteIp.from(head, opts) == {28, 74, 71, 35} end test "can be an MFA" do head = [{"x-forwarded-for", "1.2.3.4, 127.0.0.1"}] opts = [clients: {Application, :get_env, [:remote_ip_test, :clients]}] Application.put_env(:remote_ip_test, :clients, []) assert RemoteIp.from(head, opts) == {1, 2, 3, 4} Application.put_env(:remote_ip_test, :clients, ~w[127.0.0.0/8]) assert RemoteIp.from(head, opts) == {127, 0, 0, 1} end end @headers [ {"forwarded", "for=1.2.3.4"}, {"x-forwarded-for", "1.2.3.4"}, {"x-client-ip", "1.2.3.4"}, {"x-real-ip", "1.2.3.4"} ] describe ":headers option" do test "specifies which headers to use" do head = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}, {"c", "3.4.5.6"}] assert RemoteIp.from(head, headers: ~w[a b]) == {2, 3, 4, 5} assert RemoteIp.from(head, headers: ~w[a c]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[b a]) == {2, 3, 4, 5} assert RemoteIp.from(head, headers: ~w[b c]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[c a]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[c b]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[a]) == {1, 2, 3, 4} assert RemoteIp.from(head, headers: ~w[b]) == {2, 3, 4, 5} assert RemoteIp.from(head, headers: ~w[c]) == {3, 4, 5, 6} end for {header, value} <- @headers do test "includes #{header} by default" do head = [{unquote(header), unquote(value)}] assert RemoteIp.from(head) == {1, 2, 3, 4} end end test "overrides the defaults when specified" do head = @headers opts = [headers: ~w[custom]] fail = "default headers are still being parsed" refute RemoteIp.from(head, opts) == {1, 2, 3, 4}, fail end test "doesn't care about order" do head = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}, {"c", "3.4.5.6"}] assert RemoteIp.from(head, headers: ~w[a b c]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[a c b]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[b a c]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[b c a]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[c a b]) == {3, 4, 5, 6} assert RemoteIp.from(head, headers: ~w[c b a]) == {3, 4, 5, 6} end test "can be an MFA" do head = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}] opts = [headers: {Application, :get_env, [:remote_ip_test, :headers]}] Application.put_env(:remote_ip_test, :headers, ~w[a]) assert RemoteIp.from(head, opts) == {1, 2, 3, 4} Application.put_env(:remote_ip_test, :headers, ~w[b]) assert RemoteIp.from(head, opts) == {2, 3, 4, 5} end end describe "multiple headers" do test "from unknown to unknown" do head = [{"forwarded", "for=unknown,for=_obf"}] opts = [] assert RemoteIp.from(head, opts) == nil end test "from unknown to loopback" do head = [{"x-forwarded-for", "unknown,::1"}] opts = [] assert RemoteIp.from(head, opts) == nil end test "from unknown to private" do head = [{"x-client-ip", "_obf, fc00:ABCD"}] opts = [] assert RemoteIp.from(head, opts) == nil end test "from unknown to proxy" do head = [{"x-real-ip", "not_an_ip , 1.2.3.4"}] opts = [proxies: ~w[1.0.0.0/12]] assert RemoteIp.from(head, opts) == nil end test "from unknown to client" do head = [{"custom", "unknown ,1.2.3.4"}] opts = [headers: ~w[custom]] assert RemoteIp.from(head, opts) == {1, 2, 3, 4} end test "from loopback to unknown" do head = [{"forwarded", "for=\"[::1]\""}, {"x-forwarded-for", "_bogus"}] opts = [] assert RemoteIp.from(head, opts) == nil end test "from loopback to loopback" do head = [{"x-client-ip", "127.0.0.1"}, {"x-real-ip", "127.0.0.1"}] opts = [] assert RemoteIp.from(head, opts) == nil end test "from loopback to private" do head = [{"custom", "127.0.0.10"}, {"forwarded", "for=\"[fc00::1]\""}] opts = [headers: ~w[forwarded custom]] assert RemoteIp.from(head, opts) == nil end test "from loopback to proxy" do head = [{"forwarded", "for=127.0.0.1"}, {"forwarded", "for=1.2.3.4"}] opts = [proxies: ~w[1.2.3.4/32]] assert RemoteIp.from(head, opts) == nil end test "from loopback to client" do head = [{"x-forwarded-for", "127.0.0.1"}, {"x-forwarded-for", "1.2.3.4"}] opts = [] assert RemoteIp.from(head, opts) == {1, 2, 3, 4} end test "from private to unknown" do head = [{"x-client-ip", "fc00::ABCD"}, {"x-client-ip", "_obf"}] opts = [] assert RemoteIp.from(head, opts) == nil end test "from private to loopback" do head = [{"x-real-ip", "192.168.1.2"}, {"x-real-ip", "::1"}] opts = [] assert RemoteIp.from(head, opts) == nil end test "from private to private" do head = [{"custom", "10.0.0.1"}, {"custom", "10.0.0.2"}] opts = [headers: ~w[custom]] assert RemoteIp.from(head, opts) == nil end test "from private to proxy" do head = [{"forwarded", "for=10.0.10.0, for=\"[::1.2.3.4]\""}] opts = [proxies: ~w[::/64]] assert RemoteIp.from(head, opts) == nil end test "from private to client" do head = [{"x-forwarded-for", "10.0.10.0, ::1.2.3.4"}] opts = [proxies: ~w[255.0.0.0/8]] assert RemoteIp.from(head, opts) == {0, 0, 0, 0, 0, 0, 258, 772} end test "from proxy to unknown" do head = [{"x-client-ip", "a:b:c:d:e:f::,unknown"}] opts = [proxies: ~w[::/0]] assert RemoteIp.from(head, opts) == nil end test "from proxy to loopback" do head = [ {"x-real-ip", "2001:0db8:85a3:0000:0000:8A2E:0370:7334"}, {"x-real-ip", "127.0.0.2"} ] opts = [proxies: ~w[2001:0db8:85a3::8A2E:0370:7334/128]] assert RemoteIp.from(head, opts) == nil end test "from proxy to private" do head = [{"custom", "3.4.5.6 , 172.16.1.2"}] opts = [headers: ~w[custom], proxies: ~w[3.0.0.0/8]] assert RemoteIp.from(head, opts) == nil end test "from proxy to proxy" do head = [{"forwarded", "for=1.2.3.4, for=1.2.3.5"}] opts = [proxies: ~w[1.2.3.0/24]] assert RemoteIp.from(head, opts) == nil end test "from proxy to client" do head = [{"x-forwarded-for", "::1:2:3:4, ::3:4:5:6"}] opts = [proxies: ~w[::1:2:3:4/128]] assert RemoteIp.from(head, opts) == {0, 0, 0, 0, 3, 4, 5, 6} end test "from client to unknown" do head = [{"x-client-ip", "a:b:c:d:e:f::,unknown"}] opts = [proxies: ~w[b::/64]] assert RemoteIp.from(head, opts) == {10, 11, 12, 13, 14, 15, 0, 0} end test "from client to loopback" do head = [{"x-real-ip", "127.0.0.1"}, {"x-real-ip", "127.0.0.2"}] opts = [clients: ~w[127.0.0.1/32]] assert RemoteIp.from(head, opts) == {127, 0, 0, 1} end test "from client to private" do head = [{"custom", "::1.2.3.4, 10.0.10.0"}] opts = [proxies: ~w[1:2:3:4::/64], headers: ~w[custom]] assert RemoteIp.from(head, opts) == {0, 0, 0, 0, 0, 0, 258, 772} end test "from client to proxy" do head = [{"forwarded", "for=1.2.3.4,for=3.4.5.6"}] opts = [proxies: ~w[3.4.5.0/24]] assert RemoteIp.from(head, opts) == {1, 2, 3, 4} end test "from client to client" do head = [{"x-forwarded-for", "1.2.3.4"}, {"x-forwarded-for", "10.45.0.1"}] opts = [clients: ~w[10.45.0.0/16]] assert RemoteIp.from(head, opts) == {10, 45, 0, 1} end test "more than two hops" do head = [ {"forwarded", "for=\"[fe80::0202:b3ff:fe1e:8329]\""}, {"forwarded", "for=1.2.3.4"}, {"x-forwarded-for", "172.16.0.10"}, {"x-client-ip", "::1, ::1"}, {"x-real-ip", "2.3.4.5, fc00::1, 2.4.6.8"} ] opts = [proxies: ~w[2.0.0.0/8]] assert RemoteIp.from(head, opts) == {1, 2, 3, 4} end end defmodule ParserA do @behaviour RemoteIp.Parser @impl RemoteIp.Parser def parse(value) do ips = RemoteIp.Parsers.Generic.parse(value) ips |> Enum.map(fn {a, b, c, d} -> {10 + a, 10 + b, 10 + c, 10 + d} end) end end defmodule ParserB do @behaviour RemoteIp.Parser @impl RemoteIp.Parser def parse(value) do ips = RemoteIp.Parsers.Generic.parse(value) ips |> Enum.map(fn {a, b, c, d} -> {20 + a, 20 + b, 20 + c, 20 + d} end) end end defmodule ParserC do @behaviour RemoteIp.Parser @impl RemoteIp.Parser def parse(value) do ips = RemoteIp.Parsers.Generic.parse(value) ips |> Enum.map(fn {a, b, c, d} -> {30 + a, 30 + b, 30 + c, 30 + d} end) end end describe ":parsers option" do test "can customize parsers for specific headers" do headers = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}, {"c", "3.4.5.6"}] parsers = %{"a" => ParserA, "b" => ParserB, "c" => ParserC} assert RemoteIp.from(headers, parsers: parsers, headers: ~w[a]) == {11, 12, 13, 14} assert RemoteIp.from(headers, parsers: parsers, headers: ~w[b]) == {22, 23, 24, 25} assert RemoteIp.from(headers, parsers: parsers, headers: ~w[c]) == {33, 34, 35, 36} end test "doesn't clobber generic parser on other headers" do headers = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}, {"c", "3.4.5.6"}] parsers = %{"a" => ParserA, "c" => ParserC} assert RemoteIp.from(headers, parsers: parsers, headers: ~w[a]) == {11, 12, 13, 14} assert RemoteIp.from(headers, parsers: parsers, headers: ~w[b]) == {2, 3, 4, 5} assert RemoteIp.from(headers, parsers: parsers, headers: ~w[c]) == {33, 34, 35, 36} end test "doesn't clobber Forwarded parser by default" do headers = [{"forwarded", "for=1.2.3.4"}] parsers = %{"a" => ParserA, "b" => ParserB, "c" => ParserC} options = [parsers: parsers, headers: ~w[forwarded a b c]] assert RemoteIp.from(headers, options) == {1, 2, 3, 4} end test "can clobber Forwarded parser" do headers = [{"forwarded", "1.2.3.4"}] parsers = %{"forwarded" => ParserA} options = [parsers: parsers, headers: ~w[forwarded]] assert RemoteIp.from(headers, options) == {11, 12, 13, 14} end test "can be an MFA" do headers = [{"x", "1.2.3.4"}] parsers = {Application, :get_env, [:remote_ip_test, :parsers]} options = [parsers: parsers, headers: ~w[x]] Application.put_env(:remote_ip_test, :parsers, %{"x" => ParserA}) assert RemoteIp.from(headers, options) == {11, 12, 13, 14} Application.put_env(:remote_ip_test, :parsers, %{"x" => ParserC}) assert RemoteIp.from(headers, options) == {31, 32, 33, 34} end end defmodule App do use Plug.Router plug RemoteIp, parsers: {__MODULE__, :parsers, []}, headers: {__MODULE__, :config, ["HEADERS"]}, proxies: {__MODULE__, :config, ["PROXIES"]}, clients: {__MODULE__, :config, ["CLIENTS"]} plug :match plug :dispatch get "/ip" do send_resp(conn, 200, :inet.ntoa(conn.remote_ip)) end def config(var) do System.get_env() |> Map.get(var, "") |> String.split(",", trim: true) end def parsers do Enum.into(config("PARSERS"), %{}, fn spec -> [header, parser] = String.split(spec, ":") {header, :"Elixir.RemoteIpTest.#{parser}"} end) end end test "runtime configuration" do try do conn = conn(:get, "/ip") conn = conn |> put_req_header("a", "1.2.3.4, 192.168.0.1, 2.3.4.5") conn = conn |> put_req_header("b", "3.4.5.6, 192.168.0.1, 4.5.6.7") assert App.call(conn, App.init([])).resp_body == "127.0.0.1" System.put_env("HEADERS", "a") assert App.call(conn, App.init([])).resp_body == "2.3.4.5" System.put_env("PARSERS", "a:ParserA") assert App.call(conn, App.init([])).resp_body == "12.13.14.15" System.put_env("PARSERS", "a:ParserB,c:ParserC") assert App.call(conn, App.init([])).resp_body == "22.23.24.25" System.put_env("PROXIES", "22.0.0.0/8,212.188.0.0/16") assert App.call(conn, App.init([])).resp_body == "21.22.23.24" System.delete_env("PARSERS") System.put_env("PROXIES", "2.1.0.0/16,2.2.0.0/16,2.3.0.0/16") assert App.call(conn, App.init([])).resp_body == "1.2.3.4" System.put_env("CLIENTS", "192.0.0.0/8,1.2.3.4/32") assert App.call(conn, App.init([])).resp_body == "192.168.0.1" System.put_env("HEADERS", "b,c,d") assert App.call(conn, App.init([])).resp_body == "4.5.6.7" System.put_env("PROXIES", "4.5.6.0/24") assert App.call(conn, App.init([])).resp_body == "192.168.0.1" System.put_env("CLIENTS", "4.5.7.0/24") assert App.call(conn, App.init([])).resp_body == "3.4.5.6" after System.delete_env("PARSERS") System.delete_env("HEADERS") System.delete_env("PROXIES") System.delete_env("CLIENTS") end end end ================================================ FILE: test/test_helper.exs ================================================ ExUnit.start(capture_log: true)