Making IP-based routing decisions in Clojure

Our web app needs to block/allow certain ips and ip ranges. We are using ring/reitit, but I am willing to hear about other options.

What solutions have been used for webapps to make routing decisions (in Apache these are Allow/Deny statements) in Clojure?

We do this in software because we expose IP blocking to our Member Services team (e.g., if they see a scammer with multiple accounts on the same IP address) – so we store IP addresses in the database, along with the length of the block (we automatically expire most bans after a certain period of time), then we check the user’s IP address at login/registration and auto-suspend the account if their IP is currently blocked.

However, such users are still able to browse any of the public content and traffic from such IPs still reaches out apps. From time to time, we also need to block traffic from certain IP address ranges completely and we do that at the CDN/firewall level so traffic doesn’t even reach our load balancer, let alone our apps.

The approach you take is really going to depend on what you are trying to achieve.

1 Like

Normally we do Apache for everything – in fact, it reverse-proxies to our apps to get the URLs we want to behave most easily – but it seems desirable to make this an in-app solution, as that is actually part of our behavior. I understood Undertow (or whatever other Java server) to be functionally equivalent for apache, so figured there might be an in-built solution for detecting/dispatching on IP addresses.

We don’t expect ips to need to change, but overall we need have three categories of routes:

A) public
B) A chunk of routes that are only open to a certain range
C) A particular route that is only open to one or two specific IP addresses (for receiving api responses from a 3rd party)

All three of these need to exist at once, but during dev they will sometimes change or be on-off.

You said you want in-app solution but I strongly encourage you to implement this at the very front of your infrastructure stack - to be concrete, we use AWS CloudFront/WAF for this purpose and I think it’s a perfect fit for you, especially since the IP addresses are static.

Also, it might be a very good idea to implement extra authentication layer (in your app or through an integration with a 3rd party auth provider).
The reasons for this are multitude including:

  • firewall rules getting obsolete simply because people forget to update them
  • IP addresses changing (dynamic IPs)
  • shared IP address used by many different users - see CGNAT (Carrier-grade NAT)

Nginx is scriptable via openresty/lua:

if you want full blown features also via openresty:

Epic lua rant that always make my day.

Thanks for the recommendations so far. I’ve seen recommended so far nginx, aws, and Lua/nginx. I clearly failed to explain my reasons for wanting a Clojure solution.

I am not our system admin; I persuaded a trial of nginx a few years ago and overall it didn’t work out. So, as far as options, it is either Apache or within our Clojure app. I want it to be within our Clojure app because it gives more fine-grained control of our routes and because in this app, the routing controls are PART of the business logic, and it would be far nicer to include this within our code rather than creating disparate solutions that span multiple languages, scopes, and software environments.

So I am specifically looking for a Clojure/Uberjar solution; failing that (which has never really happened before) I guess I could go nitty-gritty on our Apache setup, but I don’t believe it should come to that. What is a Ring method of IP-based routing?

if you google ring clojure geoip this repo comes up:

but in all honesty… your apache setup seems fine for your needs as of right now. but agree with both @seancorfield and @jumar to leave this it right at the edge. as you’ll be bound to double up your ip filtering logic in both your proxy and your app in many cases.

I concur, have a zero-trust model, using IPs as auth isn’t ideal, having real auth is still a good idea.

Hum, not sure, the http headers XFF contain source IPs, not sure if they are mandatory though. Something else probably gives you the remote IP.

Ring has :remote-addr but that won’t always be correct if you’re using a CDN and/or a load balancer and you’ll need to write a short middleware function to pull the correct value from whatever headers are provided.

At work, we have both F5 BigIP and various CDNs in front of things and so we have to account for the headers for these and how IP addresses and headers are passed through. CloudFlare, for example, can be configured to pass Class E IPv4 addresses to the backend but also pass IPv6 addresses in headers. You’ll have to think about IPv4 vs IPv6 in any IP-based routing solution you set up, BTW.

Also, I seem to recall there’s an assumption in some Ring middleware about the comma-separated sequence of IPs that are passed through proxy pipelines – and it’s definitely wrong for our stack at work and we overwrite :remote-addr for that reason. See How to get the client IP address in ring-clojure? - Stack Overflow for example for some of the issues involved.

1 Like

I second what Sean said above and the link he shared is quite useful.

At work, we don’t do IP filtering at the application level,
but we do log client IPs and you need the same information for that.
We run on AWS - our app is sitting behind CloudFront + AWS, then ELB load balancer and finally nginx.
To get the client IP, we simply use this piece of code (we only work with IPv4):

(defn get-client-ip [req]
  (if-let [ips (get-in req [:headers "x-forwarded-for"])]
    (-> ips (string/split #",") first)
    (:remote-addr req "")))

However, notice that it’s (depending on the servers’ configuration) perfectly possible for the client to spoof this information (x-forwarded-for),
so I wouldn’t rely 100% on this for IP filtering.

1 Like

Our middleware is similar but tuned to CloudFlare:

(defn wrap-client-ip
  "Middleware to set :remote-addr if X-Forwarded-For header is present.
  We also set :remote-addr-raw and :remote-addr-country if the appropriate
  headers are present (per WS-13526).

  Note: unlike ring-headers/proxy-headers, we want the _first_ IP in the
  header -- the client -- not the one sent via the last proxy (which, in our
  case, is the F5 Big-IP load balancer)."
  (fn [req]
    (let [headers (:headers req)
          fwd-for (some-> (get headers "x-forwarded-for")
                          (str/split #",")
          raw-ip  (or (get headers "cf-connecting-ipv6")
                      (get headers "cf-connecting-ip")
                      (:remote-addr req))
          country (get headers "cf-ipcountry")
          (cond-> (assoc req :remote-addr-raw raw-ip)
            fwd-for (assoc :remote-addr fwd-for)
            country (assoc :remote-addr-country country))]
      (with-log-context (if-let [ip (:remote-addr req+ip)] {:ip ip} {})
        (handler (update req+ip :remote-addr sanitize-ip))))))

And that’s the reference to what I was talking about earlier about the Ring middleware pulling in the “wrong” IP address.

1 Like

I guess the cloudflare IP in your case couldn’t be spoofed, so probably most reliable, I guess depending what you’re trying to achieve.

1 Like

Oh, we don’t trust the IP address for anything but we do record the IP address because it is useful for tracking multiple accounts, scammers, and dealing with chargebacks. But it’s not anything for security :smile:

1 Like