# ol.client-ip A 0-dependency ring middleware for determining a request's real client IP address from HTTP headers ## ol.client-ip.cidr # ol.client-ip.cidr ## cidr-parts ```clojure (cidr-parts cidr) ``` Parse an ip network string into its prefix and signifcant bits `cidr` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1) Returns a vector of [^InetAddress prefix ^Integer bits] [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/cidr.clj#L26-L39) --- ## contains? ```clojure (contains? cidr ip) (contains? cidr-ip cidr-mask ip) ``` Check if the given CIDR contains the given IP address. Arguments: cidr - CIDR notation string (e.g. "192.168.0.0/24" or "2001:db8::/32") ip - IP address to check (can be InetAddress or string) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/cidr.clj#L41-L61) --- ## reserved-ranges [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/cidr.clj#L66-L93) --- ## reserved? ```clojure (reserved? ip) ``` Check if an IP is in a reserved range (loopback or private network). [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/cidr.clj#L95-L98) ## ol.client-ip.core # ol.client-ip.core Core implementation of the client-ip library for extracting client IPs from forwarding headers. The client-ip library is designed to accurately determine the original client IP address from HTTP forwarding headers in Ring applications. It uses a strategy-based approach where different strategies handle different network configurations. ## Main Features * Strategy-based IP detection for different network setups * Processes IPs according to the chosen strategy to prevent spoofing * Supports all common headers: X-Forwarded-For, X-Real-IP, Forwarded (RFC 7239), etc. ## Usage The main entry point is the `wrap-client-ip` middleware function that requires a strategy and adds the `:ol/client-ip` key to the Ring request map: ```clojure (require '[ol.client-ip.core :refer [wrap-client-ip]] '[ol.client-ip.strategy :as strategy]) (def app (-> handler (wrap-client-ip {:strategy (strategy/rightmost-non-private-strategy "x-forwarded-for")}) ;; other middleware )) ``` You can also use `from-request` to extract client IPs directly from requests: ```clojure (from-request request (strategy/rightmost-trusted-count-strategy "x-forwarded-for" 2)) ;; => "203.0.113.195" ``` See the strategy documentation for more details on choosing the right strategy. ## from-request ```clojure (from-request request strategy) ``` Extract the client IP from a Ring request using the specified strategy. This is the core function for determining client IPs. It takes a Ring request and a strategy instance, then uses the strategy to determine the client IP from the request headers and remote address. Refer to ns docs and the [`wrap-client-ip`](#wrap-client-ip) docstring for more usage information. Returns the client InetAddress , or nil if no client IP can be determined. [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/core.clj#L43-L58) --- ## wrap-client-ip ```clojure (wrap-client-ip handler options) ``` Ring middleware that adds the client IP to the request map using strategies. This middleware extracts the original client IP address using the specified strategy and adds it to the request map as `:ol/client-ip`. The strategy determines how headers are processed and which IP is considered the client IP. ## Options The options map must contain: * `:strategy` - A strategy instance (required) ## Strategy Selection Choose the strategy that matches your network configuration: * `RemoteAddrStrategy` - Direct connections (no reverse proxy) * `SingleIPHeaderStrategy` - Single trusted reverse proxy with single IP headers * `RightmostNonPrivateStrategy` - Multiple proxies, all with private IPs * `RightmostTrustedCountStrategy` - Fixed number of trusted proxies * `RightmostTrustedRangeStrategy` - Known trusted proxy IP ranges * `ChainStrategy` - Try multiple strategies with fallback ## Examples ```clojure ;; Single reverse proxy with X-Real-IP (def app (-> handler (wrap-client-ip {:strategy (strategy/single-ip-header-strategy "x-real-ip")}) ;; other middleware )) ;; Multiple proxies with private IPs (def app (-> handler (wrap-client-ip {:strategy (strategy/rightmost-non-private-strategy "x-forwarded-for")}) ;; other middleware )) ;; Chain strategy with fallback (def app (-> handler (wrap-client-ip {:strategy (strategy/chain-strategy [(strategy/single-ip-header-strategy "x-real-ip") (strategy/remote-addr-strategy)])}) ;; other middleware )) ``` The middleware adds the `:ol/client-ip` key to the request map, containing the determined client InetAddress or nil. [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/core.clj#L60-L121) ## ol.client-ip.ip # ol.client-ip.ip IP address utilities for conversion and classification. This namespace provides functions for working with IP addresses, particularly for converting between string, InetAddress, and numeric representations. It follows RFC 5952 for IPv6 address formatting and provides utilities for type checking and serialization. **📌 NOTE**\ None of the functions in this namespace accept ip addreses input as strings. Use [`ol.client-ip.parse-ip/from-string`](api/ol-client-ip-parse-ip.adoc#from-string) to convert strings to InetAddress objects. The key operations available are: * Type checking with `ipv4?` and `ipv6?` * String formatting with `format-ip` * Numeric conversion with `->numeric` and `numeric->` These functions are designed to work with the `java.net.InetAddress` class hierarchy and its subclasses `Inet4Address` and `Inet6Address`. Refer to [`ol.client-ip.parse-ip`](api/ol-client-ip-parse-ip.adoc) and [`ol.client-ip.cidr`](api/ol-client-ip-cidr.adoc) for more useful functions. ## ipv4? ```clojure (ipv4? ip) ``` Checks if an IP address is an IPv4 address. This function determines if the given IP address is an instance of `java.net.Inet4Address`. Arguments: ip - An IP address object (InetAddress) Returns: `true` if the IP address is IPv4, `false` otherwise Examples: ```clojure (ipv4? (parse-ip/from-string "192.168.1.1")) ;; => true (ipv4? (parse-ip/from-string "::1")) ;; => false ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L28-L46) --- ## ipv6? ```clojure (ipv6? ip) ``` Checks if an IP address is an IPv6 address. This function determines if the given IP address is an instance of `java.net.Inet6Address`. Arguments: ip - An IP address object (InetAddress) Returns: `true` if the IP address is IPv6, `false` otherwise Examples: ```clojure (ipv6? (parse-ip/from-string "2001:db8::1")) ;; => true (ipv6? (parse-ip/from-string "192.168.1.1")) ;; => false ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L48-L66) --- ## format-ipv6 ```clojure (format-ipv6 ip) ``` Converts an IPv6 address to its canonical string representation. This function formats IPv6 addresses according to RFC 5952, which includes: - Using lowercase hexadecimal digits - Compressing the longest run of consecutive zero fields with :: - Not using :: to compress a single zero field - Including scope IDs with % delimiter for link-local addresses Arguments: ip - An IPv6 address object (Inet6Address) Returns: A string representation of the IPv6 address formatted according to RFC 5952 Examples: ```clojure (format-ipv6 (parse-ip/from-string "2001:db8:0:0:0:0:0:1")) ;; => "2001:db8::1" (format-ipv6 (parse-ip/from-string "fe80::1%eth0")) ;; => "fe80::1%eth0" ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L121-L152) --- ## format-ip ```clojure (format-ip ip) ``` Converts an IP address to its canonical string representation. This function formats IP addresses according to standard conventions: - IPv4 addresses use dotted decimal notation (e.g., "192.168.0.1") - IPv6 addresses follow RFC 5952 with compressed notation using :: for the longest run of zeros, and embedded IPv4 addresses where appropriate The implementation handles scope IDs for IPv6 link-local addresses and properly compresses IPv6 addresses according to the standard rules. Arguments: ip - An IP address object (InetAddress) Returns: A string representation of the IP address Throws: IllegalArgumentException - if the input is not a valid InetAddress Examples: ```clojure (format-ip (parse-ip/from-string "192.168.1.1")) ;; => "192.168.1.1" (format-ip (parse-ip/from-string "2001:db8::1")) ;; => "2001:db8::1" (format-ip (parse-ip/from-string "::ffff:1.2.3.4")) ;; => "::ffff:1.2.3.4" ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L154-L185) --- ## ->numeric ```clojure (->numeric ip) ``` Converts an IP address to its numeric representation as a BigInteger. This function takes an InetAddress (either IPv4 or IPv6) and returns a BigInteger representing the address. The BigInteger can be used for IP address arithmetic, comparison, or storage. Arguments: ip - An IP address object (InetAddress) Returns: A BigInteger representing the IP address Examples: ```clojure (->numeric (parse-ip/from-string "192.0.2.1")) ;; => 3221225985 (->numeric (parse-ip/from-string "2001:db8::1")) ;; => 42540766411282592856903984951653826561 ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L187-L209) --- ## numeric-> ```clojure (numeric-> address version) ``` Converts a BigInteger to an InetAddress. This function takes a numeric representation of an IP address as a BigInteger and converts it back to an InetAddress object. The `ipv6?` flag determines whether to create an IPv4 or IPv6 address. Arguments: address - A BigInteger representing the IP address version - One of :v6 or :v4 indicating whether this is an IPv6 address or an IPv4 address Returns: An InetAddress object representing the numeric address, or nil Throws: ExceptionInfo - If the BigInteger is negative or too large for the specified address type Examples: ```clojure ;; Convert back to IPv4 (numeric-> (BigInteger. "3221226113") :v4) ;; => #object[java.net.Inet4Address 0x578d1c5 "/192.0.2.129"] ;; Convert back to IPv6 (numeric-> (BigInteger. "42540766411282592856903984951653826561") :v6) ;; => #object[java.net.Inet6Address 0x14832c23 "/2001:db8:0:0:0:0:0:1"] ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L211-L267) --- ## numeric-v6-> ```clojure (numeric-v6-> address) ``` Converts a BigInteger to an IPv6 address. This is a convenience wrapper around `numeric->` that automatically sets the `ipv6?` flag to true. Arguments: address - A BigInteger representing the IPv6 address Returns: An Inet6Address object or nil if the input is invalid See also: <<numeric--GT-,`numeric->`>> Examples: ```clojure (numeric-v6-> (BigInteger. "42540766411282592856903984951653826561")) ;; => #object[java.net.Inet6Address 0x42668812 "/2001:db8:0:0:0:0:0:1"] ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L269-L292) --- ## numeric-v4-> ```clojure (numeric-v4-> address) ``` Converts a BigInteger to an IPv4 address. Arguments: address - A BigInteger representing the IPv4 address Returns: An Inet4Address object or nil if the input is invalid See also: <<numeric--GT-,`numeric->`>> Examples: ```clojure (numeric-v4-> (BigInteger. "3221226113")) ;; => #object[java.net.Inet4Address 0x6b88aeff "/192.0.2.129"] ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L294-L314) ## ol.client-ip.parse-ip # ol.client-ip.parse-ip Parse a string representation of an IPv4 or IPv6 address into an InetAddress instance. Unlike `InetAddress/getByName`, the functions in this namespace never cause DNS services to be accessed. This avoids potentially blocking/side-effecting network calls that can occur when using the JDK’s built-in methods to parse IP addresses. This implementation is inspired by Google Guava’s InetAddresses class, which provides similar functionality in Java. This code focuses on strict validation of IP address formats according to relevant RFCs. Features: - Non-blocking IP address parsing with pure functions - Strict RFC-compliant validation - Support for all IPv4 formats - Support for all IPv6 formats (including compressed notation and embedded IPv4) - Support for IPv6 scope identifiers The main entry point is `parse-ip`, which takes a string and returns an InetAddress instance or nil if the input is an invalid ip address literal. ## bytes->inet-address ```clojure (bytes->inet-address addr) (bytes->inet-address addr scope) ``` Convert a byte array into an InetAddress. Args: addr: the raw 4-byte or 16-byte IP address in big-endian order scope: optional scope identifier for IPv6 addresses [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/parse_ip.clj#L231-L256) --- ## from-string ```clojure (from-string ip-string) ``` Returns the InetAddress having the given string representation or nil otherwise. This function parses IP address strings without performing DNS lookups, making it suitable for environments where DNS lookups would cause unwanted blocking or side effects. It supports: - IPv4 addresses in dotted decimal format (e.g., "192.168.1.1") - IPv6 addresses in hex format with optional compression (e.g., "2001:db8::1") - IPv6 addresses with scope IDs (e.g., "fe80::1%eth0" or "fe80::1%1") - IPv6 addresses with embedded IPv4 (e.g., "::ffff:192.168.1.1") If the input is already an InetAddress, it is returned unchanged. Args: ip-string: A string representing an IP address or an InetAddress Returns: An InetAddress object, or nil if the input couldn’t be parsed [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/parse_ip.clj#L258-L290) ## ol.client-ip.protocols # ol.client-ip.protocols ## ClientIPStrategy Protocol for determining client IP from headers and remote address. Returns InetAddress or nil if no valid client ip is found. Implementations should: * Be thread-safe and reusable across requests * Validate configuration at creation time (throw on invalid config) _protocol_ [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/protocols.clj#L5-L21) ### client-ip ```clojure (client-ip this headers remote-addr) ``` Extract the client IP from request headers and remote address. Args: headers: Ring-style headers map (lowercase keys) remote-addr: String from Ring request :remote-addr Returns: InetAddress or nil if no valid client IP found. ## ol.client-ip.strategy # ol.client-ip.strategy Strategy implementations for determining client IP addresses. This namespace provides various strategy implementations for extracting the real client IP from HTTP headers and connection information. Each strategy is designed for specific network configurations: * `RemoteAddrStrategy` - Direct connections (no reverse proxy) * `SingleIPHeaderStrategy` - Single trusted reverse proxy with single IP headers * `RightmostNonPrivateStrategy` - Multiple proxies, all with private IPs * `LeftmostNonPrivateStrategy` - Get IP closest to original client (not secure) * `RightmostTrustedCountStrategy` - Fixed number of trusted proxies * `RightmostTrustedRangeStrategy` - Known trusted proxy IP ranges * `ChainStrategy` - Try multiple strategies in order Strategies are created once and reused across requests. They are thread-safe and designed to fail fast on configuration errors while being resilient to malformed input during request processing. ## parse-forwarded ```clojure (parse-forwarded header-value) ``` Parse a Forwarded header value according to RFC 7239. Returns a sequence of InetAddress objects, or an empty sequence if none are valid. [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L119-L130) --- ## split-host-zone ```clojure (split-host-zone ip-string) ``` Split IPv6 zone from IP address. IPv6 addresses can include zone identifiers (scope IDs) separated by '%'. For example: 'fe80::1%eth0' or 'fe80::1%1' Args: ip-string: String IP address that may contain a zone identifier Returns: Vector of [ip zone] where zone is the zone identifier string, or [ip nil] if no zone is present. Examples: (split-host-zone "192.168.1.1") => ["192.168.1.1" nil] (split-host-zone "fe80::1%eth0") => ["fe80::1" "eth0"] (split-host-zone "fe80::1%1") => ["fe80::1" "1"] [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L150-L171) --- ## remote-addr-strategy ```clojure (remote-addr-strategy) ``` Create a strategy that uses the direct client socket IP. This strategy extracts the IP address from the request’s :remote-addr field, which represents the direct client socket connection. Use this when your server accepts direct connections from clients (not behind a reverse proxy). The strategy strips any port information and validates the IP is not zero/unspecified (0.0.0.0 or ::). Returns: RemoteAddrStrategy instance Example: (remote-addr-strategy) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L184-L200) --- ## single-ip-header-strategy ```clojure (single-ip-header-strategy header-name) ``` Create a strategy that extracts IP from headers containing a single IP address. This strategy is designed for headers like X-Real-IP, CF-Connecting-IP, True-Client-IP, etc. that contain only one IP address. Use this when you have a single trusted reverse proxy that sets a reliable single-IP header. The strategy validates that the header name is not X-Forwarded-For or Forwarded, as these headers can contain multiple IPs and should use different strategies. Args: header-name: String name of the header to check (case-insensitive) Returns: SingleIPHeaderStrategy instance Throws: ExceptionInfo if header-name is invalid or refers to multi-IP headers Examples: (single-ip-header-strategy "x-real-ip") (single-ip-header-strategy "cf-connecting-ip") [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L208-L235) --- ## rightmost-non-private-strategy ```clojure (rightmost-non-private-strategy header-name) ``` Create a strategy that gets the rightmost non-private IP from forwarding headers. This strategy processes IPs in reverse order (rightmost first) and returns the first IP that is not in a private/reserved range. Use this when all your reverse proxies have private IP addresses, so the rightmost non-private IP should be the real client IP. The strategy supports X-Forwarded-For and Forwarded headers that contain comma-separated IP lists. Args: header-name: String name of the header to check (case-insensitive) Should be "x-forwarded-for" or "forwarded" Returns: RightmostNonPrivateStrategy instance Throws: ExceptionInfo if header-name is invalid or refers to single-IP headers Examples: (rightmost-non-private-strategy "x-forwarded-for") (rightmost-non-private-strategy "forwarded") [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L250-L278) --- ## leftmost-non-private-strategy ```clojure (leftmost-non-private-strategy header-name) ``` Create a strategy that gets the leftmost non-private IP from forwarding headers. This strategy processes IPs in forward order (leftmost first) and returns the first IP that is not in a private/reserved range. This gets the IP closest to the original client. ⚠️ ***WARNING: NOT FOR SECURITY USE*** ⚠️ This strategy is easily spoofable since clients can add arbitrary IPs to the beginning of forwarding headers. Use only when security is not a concern and you want the IP closest to the original client. The strategy supports X-Forwarded-For and Forwarded headers that contain comma-separated IP lists. Args: header-name: String name of the header to check (case-insensitive) Should be "x-forwarded-for" or "forwarded" Returns: LeftmostNonPrivateStrategy instance Throws: ExceptionInfo if header-name is invalid or refers to single-IP headers Examples: (leftmost-non-private-strategy "x-forwarded-for") (leftmost-non-private-strategy "forwarded") [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L293-L325) --- ## rightmost-trusted-count-strategy ```clojure (rightmost-trusted-count-strategy header-name trusted-count) ``` Create a strategy that returns IP at specific position based on known proxy count. This strategy is for when you have a fixed number of trusted proxies and want to get the IP at the specific position that represents the original client. Given N trusted proxies, the client IP should be at position -(N+1) from the end. For example, with 2 trusted proxies and header 'client, proxy1, proxy2, proxy3': - Total IPs: 4 - Skip last 2 (trusted): positions 2,3 - Return IP at position 1 (proxy1) Args: header-name: String name of the header to check (case-insensitive) Should be "x-forwarded-for" or "forwarded" trusted-count: Number of trusted proxies (must be > 0) Returns: RightmostTrustedCountStrategy instance Throws: ExceptionInfo if header-name is invalid or trusted-count <= 0 Examples: (rightmost-trusted-count-strategy "x-forwarded-for" 1) (rightmost-trusted-count-strategy "forwarded" 2) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L338-L371) --- ## rightmost-trusted-range-strategy ```clojure (rightmost-trusted-range-strategy header-name trusted-ranges) ``` Create a strategy that returns rightmost IP not in trusted ranges. This strategy processes IPs in reverse order (rightmost first) and returns the first IP that is not within any of the specified trusted IP ranges. Use this when you know the specific IP ranges of your trusted proxies. The strategy supports X-Forwarded-For and Forwarded headers that contain comma-separated IP lists. Args: header-name: String name of the header to check (case-insensitive) Should be "x-forwarded-for" or "forwarded" trusted-ranges: Collection of CIDR ranges (strings) that represent trusted proxies Each range should be a valid CIDR notation like "192.168.1.0/24" Returns: RightmostTrustedRangeStrategy instance Throws: ExceptionInfo if header-name is invalid or trusted-ranges is empty/invalid Examples: (rightmost-trusted-range-strategy "x-forwarded-for" ["10.0.0.0/8" "192.168.0.0/16"]) (rightmost-trusted-range-strategy "forwarded" ["172.16.0.0/12"]) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L386-L424) --- ## chain-strategy ```clojure (chain-strategy strategies) ``` Create a strategy that tries multiple strategies in order until one succeeds. This strategy allows fallback scenarios where you want to try different IP detection methods in priority order. Each strategy is tried in sequence until one returns a non-empty result. Common use case: Try a single-IP header first, then fall back to remote address. Args: strategies: Vector of strategy instances to try in order Must contain at least one strategy Returns: ChainStrategy instance Throws: ExceptionInfo if strategies is empty or contains invalid strategies Examples: (chain-strategy [(single-ip-header-strategy "x-real-ip") (remote-addr-strategy)]) (chain-strategy [(rightmost-non-private-strategy "x-forwarded-for") (single-ip-header-strategy "x-real-ip") (remote-addr-strategy)]) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L435-L470) ## Changelog # Changelog All notable changes to this project will be documented in this file. This project uses https://www.taoensso.com/break-versioning[*Break Versioning*]. ## ++[++UNRELEASED++]++ ## `v0.1.1` (2025-08-18) Sorry, this is my first from-scratch clojars release, and I’m struggling to get the metadata working. There are no changes. ## `v0.1.0` (2025-08-18) This is the first public release of this codebase under the `ol.client-ip` moniker. Variations of this code has been used in production for a long time, but nonetheless I want to be cautious so we’re starting with sub-1.0 versioning. Please report any problems and let me know if anything is unclear, inconvenient, etc. Thank you! 🙏 ## ol.client-ip # ol.client-ip > A 0-dependency ring middleware for determining a request’s real client IP address from HTTP headers ![doc](https://img.shields.io/badge/doc-outskirtslabs-orange.svg) ![status: stable](https://img.shields.io/badge/status-stable-brightgreen.svg) ![Build Status](https://github.com/outskirtslabs/client-ip/actions/workflows/ci.yml/badge.svg) ![Clojars Project](https://img.shields.io/clojars/v/com.outskirtslabs/client-ip.svg) `X-Forwarded-For` and other client IP headers are [often used incorrectly](https://adam-p.ca/blog/2022/03/x-forwarded-for/), resulting in bugs and security vulnerabilities. This library provides strategies for extracting the correct client IP based on your network configuration. It is based on the golang reference implementation [realclientip/realclientip-go](https://github.com/realclientip/realclientip-go). Quick feature list: * ring middleware determining the client’s IP address * 0 dependency IP address string parsing with guaranteed no trips to the hosts' DNS services, which can block and timeout (unlike Java’s `InetAddress/getByName`) * rightmost-ish strategies support the `X-Forwarded-For` and `Forwarded` (RFC 7239) headers * IPv6 zone identifiers support Note that there is no dependency on ring, the public API could also be used for pedestal or sieppari-style interceptors. Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**. ## Installation ```clojure ;; deps.edn {:deps {com.outskirtslabs/client-ip {:mvn/version "0.1.1"}}} ;; Leiningen [com.outskirtslabs/client-ip "0.1.1"] ``` ## Quick Start ```clojure (ns myapp.core (:require [ol.client-ip.core :as client-ip] [ol.client-ip.strategy :as strategy])) ;; Simple case: behind a trusted proxy that sets X-Real-IP (def app (-> handler (client-ip/wrap-client-ip {:strategy (strategy/single-ip-header-strategy "x-real-ip")}))) ;; The client IP is now available in the request (defn handler [request] (let [client-ip (:ol/client-ip request)] {:status 200 :body (str "Your IP is: " client-ip)})) ``` For detailed guidance on choosing strategies, see [Usage Guide](usage.adoc). Choosing the wrong strategy can result in IP address spoofing security vulnerabilities. ## Documentation * [Docs](https://docs.outskirtslabs.com/ol.client-ip/0.1/) * [API Reference](https://docs.outskirtslabs.com/ol.client-ip/0.1/api) * [Support via GitHub Issues](https://github.com/outskirtslabs/client-ip/issues) ## Recommended Reading You think it is an easy question: > I have an HTTP application, I just want to know the IP address of my client. But who is the client? > The computer on the other end of the network connection? But which network connection? The one connected to your HTTP application is probably a reverse proxy or load balancer. > Well I mean the "user’s IP address" It ain’t so easy kid. There are many good articles on the internet that discuss the perils and pitfalls of trying to answer this deceptively simple question. You _should_ read one or two of them to get an idea of the complexity in this space. Libraries, like this one, _cannot_ hide the complexity from you, there is no abstraction nor encapsulation nor "default best practice". Below are some of those good articles: * MDN: [X-Forwarded-For: Security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#security_and_privacy_concerns) * [The perils of the "real" client IP](https://adam-p.ca/blog/2022/03/x-forwarded-for/) ([archive link](https://web.archive.org/web/20250416042714/https://adam-p.ca/blog/2022/03/x-forwarded-for/)) * [Rails IP Spoofing Vulnerabilities and Protection](https://www.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) ([archive link](https://web.archive.org/web/20250421121810/https://www.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/)) * [ol.client-ip: A Clojure Library to Prevent IP Spoofing](https://casey.link/blog/client-ip-ring-middleware/) ## Security See [here](https://github.com/outskirtslabs/client-ip/security/advisories) for security advisories or to report a security vulnerability. ## License Copyright (C) 2025 Casey Link <casey@outskirtslabs.com> Distributed under the [MIT License](https://github.com/outskirtslabs/client-ip/blob/main/LICENSE). ## Security policy # Security policy ## Advisories All security advisories will be posted https://github.com/outskirtslabs/client-ip/security/advisories[on GitHub]. ## Reporting a vulnerability Please report possible security vulnerabilities https://github.com/outskirtslabs/client-ip/security/advisories[via GitHub], or by emailing me at `casey@outskirtslabs.com`. You may encrypt emails with [my public PGP key](https://casey.link/pgp.asc). For the organization-wide security policy, see [Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy). Thank you! — [Casey Link](https://casey.link) ## Usage Guide # Usage Guide This guide explains when and why to use each strategy. The choice of strategy is critical -- _using the wrong strategy can lead to IP spoofing vulnerabilities_. ## Understanding Your Network Setup Before choosing a strategy, you need to understand your production environment’s network topology: 1. Are you behind reverse proxies? (nginx, Apache, load balancers, CDNs) 2. How many trusted proxies are there? 3. What headers do they set? The answers determine which strategy is appropriate and secure for your setup. ## Terminology * **socket-level IP**\ The IP address that a server or proxy observes at the TCP socket level from whatever is directly connecting to it. This is always trustworthy since it cannot be spoofed at the socket level, regardless of whether the connection is from an end user or another proxy in the chain. * **private IP addresses**\ IP addresses reserved for private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) that are not routable on the public internet. These typically represent your internal infrastructure components. * **reverse proxy / proxy / load balancer / CDN**\ An intermediary server that sits between clients and your application server, forwarding client requests. Examples include: nginx, Apache, caddy, AWS ALB, Cloudflare, Cloudfront, Fastly, Akamai, Fly.io, Hetzner Load Balancer. * **trusted proxy**\ A reverse proxy under your control or operated by a service you trust, whose IP forwarding headers you can rely on. The key characteristic is that you know what headers it sets and clients cannot bypass it. * **IP spoofing**\ The practice of sending packets with a forged source IP address, or in the context of HTTP headers, setting fake client IP values in headers like `X-Forwarded-For` to deceive the application about the request’s true origin. * **trivially spoofable**\ Headers or IP values that can be easily faked by any client without special network access. For example, any client can send `X-Forwarded-For: 1.2.3.4` in their request. * **XFF**\ Short for `X-Forwarded-For`, the most common HTTP header used by proxies to communicate the original client IP address through a chain of proxies. Each proxy typically appends the IP it received the request from. * **rightmost-ish**\ A parsing strategy that extracts IP addresses from the right side of forwarding headers like `X-Forwarded-For`, typically skipping a known number of trusted proxies to find the IP added by the first trusted proxy. This is the only trustworthy approach since you control what your trusted proxies add to the header chain ([see this article](https://adam-p.ca/blog/2022/03/x-forwarded-for)). * **leftmost-ish**\ A parsing strategy that takes the leftmost (first) IP address in forwarding headers, representing the alleged original client. This is highly untrustworthy and vulnerable to spoofing since any client can set arbitrary values at the beginning of these headers ([see this article](https://adam-p.ca/blog/2022/03/x-forwarded-for)). ## Strategy Reference ### Remote Address Strategy / No middleware at all! * **When to use**\ Your server accepts direct connections from clients without any reverse proxies or load balancers. * **Why**\ The socket-level remote address is the actual client IP when there are no intermediaries. Using this library you can use the remote-addr-strategy, which is provided to be comprehensive, however you can also just use the existing [`:remote-addr`](https://github.com/ring-clojure/ring/blob/master/SPEC.md#remote-addr) key in your ring request map. ```clojure (strategy/remote-addr-strategy) ;; OR, just fetch the remote addr from the ring request map (:remote-addr request) ``` * **Example network** ``` Client -> Your Server ``` * **Security**\ Safe -- the remote address cannot be spoofed at the socket level. --- ### Single IP Header Strategy * **When to use**\ You have a trusted reverse proxy that accepts connections directly from clients and sets a specific header with the client IP. * **Why**\ Many Cloud Provider’s CDNs and load balancers products provide a single authoritative header with the real client IP. This is the most secure approach when you have such a setup. * **Provider headers with trusted values** * Cloudflare (everyone): `cf-connecting-ip` -- this is a socket-level IP value ([docs](https://developers.cloudflare.com/fundamentals/get-started/http-request-headers/)) * Cloudflare (enterprise): `true-client-ip` -- also a socket-level IP value, and just for Enterprise customers with backwards compatibility requirements ([docs](https://developers.cloudflare.com/fundamentals/get-started/http-request-headers/)) * Fly.io: `fly-client-ip` -- the socket ip address ([docs](https://www.fly.io/docs/networking/request-headers/#fly-client-ip)) * Azure FrontDoor: `x-azure-socketip` -- the socket IP address associated with the TCP connection that the current request originated from ([docs](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol)) * nginx with correctly configured [`ngx_http_realip_module`](https://nginx.org/en/docs/http/ngx_http_realip_module.html) * Do not use `proxy_set_header X-Real-IP $remote_addr;` * **Provider headers that require extra config** These providers offer headers that out of the box are trivially spoofable, and require extra configuration (in your provider’s management interface) to configure securely. * Akamai: `true-client-ip` -- trivially spoofable by default, refer to [this writeup](https://adam-p.ca/blog/2022/03/x-forwarded-for/#akamai) * Fastly: `fastly-client-ip` -- trivially spoofable, you must use vcl to configure it [fastly docs](https://www.fastly.com/documentation/reference/http/http-headers/Fastly-Client-IP/) * **Provider headers to avoid** **⚠️ WARNING**\ In nearly all of these cases you are better off using XFF and reasoning about the number of proxies or their network addresses. * Azure FrontDoor: `x-azure-clientip` -- trivially spoofable as it is the leftmost-ish XFF ([docs](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol)) ```clojure ;; Clients connect to your application server *only* through and *directly* through Cloudflare (strategy/single-ip-header-strategy "cf-connecting-ip") ;; Clients connect to your application server *only* through and *directly* through Fly.io (strategy/single-ip-header-strategy "fly-client-ip") ``` * **Example networks** ``` Client -> Cloudflare -> Your Server Client -> nginx -> Your Server Client -> Load Balancer -> Your Server ``` * **Security considerations** * Ensure the header cannot be spoofed by clients * Verify clients cannot bypass your proxy * Use only the header _your_ trusted proxy sets **⚠️ WARNING**\ Common mistakes * Using `x-forwarded-for` with this strategy (use rightmost strategies instead) * Not validating that clients must go through your proxy * Changes to your topology that break a rule that was previously true * For example, adding another proxy in the chain or exposing servers on a different interface * Using headers that can be set by untrusted sources --- ### Rightmost Non-Private Strategy * **When to use**\ You have multiple reverse proxies between the internet and the server, all using private IP addresses, and they append to `X-Forwarded-For` or `Forwarded` headers. * **Why**\ In a typical private network setup, the rightmost non-private IP in the trusted forwarding chain is the real client IP. Private IPs represent your infrastructure. ```clojure (strategy/rightmost-non-private-strategy "x-forwarded-for") ;; Using RFC 7239 Forwarded header (strategy/rightmost-non-private-strategy "forwarded") ``` * **Example network** ``` Client -> Internet Proxy -> Private Load Balancer -> Private App Server (1.2.3.4) (10.0.1.1) (10.0.2.1) X-Forwarded-For: 1.2.3.4, 10.0.1.1 Result: 1.2.3.4 (rightmost non-private) ``` * **Security**\ Secure. Attackers can still spoof the leftmost entries, but not the rightmost non-private IP. **⚠️ WARNING**\ Common Mistakes * Do not use when your proxies have public IP addresses * Do not use when you need to trust specific proxy IPs (use trusted range strategy instead) --- ### Rightmost Trusted Count Strategy * **When to use**\ You know exactly how many trusted proxies append IPs to the header, and you want the IP added by the first trusted proxy. * **Why**\ When you have a fixed, known number of trusted proxies, counting backwards gives you the IP that was added by your first trusted proxy (the client IP it saw). ```clojure ;; Two trusted proxies (strategy/rightmost-trusted-count-strategy "x-forwarded-for" 2) ;; One trusted proxy (strategy/rightmost-trusted-count-strategy "forwarded" 1) ``` * **Example with count=2** ``` Client -> Proxy1 -> Proxy2 -> Your Server (adds A) (adds B) X-Forwarded-For: A, B With count=2: Skip 2 from right, return A ``` * **Security**\ Secure, when your proxy count is stable and known. **⚠️ WARNING**\ Common Mistakes * Count must exactly match your trusted proxy count * If the count is wrong, you’ll get incorrect results or errors * Network topology changes require updating the count on the application server --- ### Rightmost Trusted Range Strategy * **When to use**\ You know the IP ranges of all your trusted proxies and want the rightmost IP that’s not from a trusted source. * **Why**\ This is the most flexible strategy for complex infrastructures where you know your proxy IPs but they might change within known ranges. ```clojure ;; You have a VPC where your client-facing load balancer could be any ip address ;; inside the 10.1.1.0/24 subnet (az1) or 10.1.2.0/24 (az2) (strategy/rightmost-trusted-range-strategy "x-forwarded-for" ["10.1.1.0/24" "10.1.2.0/24"]) ;; Including Cloudflare ranges (these are examples, do not copy them!) (strategy/rightmost-trusted-range-strategy "x-forwarded-for" ["173.245.48.0/20" "103.21.244.0/22" ; Example Cloudflare IPv4 ranges "2400:cb00::/32"]) ; Example Cloudflare IPv6 range ``` * **Example** ``` Client -> Cloudflare -> Your Load Balancer -> App Server 1.2.3.4 173.245.48.1 10.0.1.1 X-Forwarded-For: 1.2.3.4, 173.245.48.1, 10.0.1.1 Trusted ranges: ["173.245.48.0/20", "10.0.0.0/8"] Result: 1.2.3.4 (rightmost IP not in trusted ranges) ``` * **Security**\ Secure, when ranges are properly maintained. **⚠️ WARNING**\ Common Mistakes * Forgetting to keep the trusted ranges up to date * Not including all possible proxy IPs * When using a cloud provider, not using their API for current up to date ranges --- ### Chain Strategy * **When to use**\ You have multiple possible network paths to your server and need fallback behavior. **⚠️ WARNING**\ Do not abuse ChainStrategy to check multiple headers. There is likely only one header you should be checking, and checking more can leave you vulnerable to IP spoofing. Each strategy should represent a different network path, not multiple ways to parse the same path. * **Why**\ Real-world deployments can have multiple possible configurations (direct connections or via CDN). Chain strategy tries each approach until one succeeds. ```clojure ;; Clients can connect via cloudflare or directly to your server (strategy/chain-strategy [(strategy/single-ip-header-strategy "cf-connecting-ip") (strategy/remote-addr-strategy)]) ;; Multiple fallback levels (strategy/chain-strategy [(strategy/rightmost-trusted-range-strategy "x-forwarded-for" ["10.0.0.0/8"]) (strategy/rightmost-non-private-strategy "x-forwarded-for") (strategy/remote-addr-strategy)]) ``` * **Example use cases** * Development vs production environments * Gradual migration between proxy setups * Handling both CDN and direct traffic --- ### Leftmost Non-Private Strategy **⚠️ WARNING**\ DO NOT USE UNLESS YOU REALLY KNOW WHAT YOU ARE DOING. The leftmost IP can be trivially spoofed by clients. * **When to use**\ Rarely recommended. Only when you specifically need the IP allegedly closest to the original client (knowing full well that it can be spoofed). * **Why**\ This gives you the "apparent" client IP but offers _no security_ against spoofing. ```clojure (strategy/leftmost-non-private-strategy "x-forwarded-for") (strategy/leftmost-non-private-strategy "forwarded") ``` * **Example** ``` X-Forwarded-For: 1.2.3.4, 2.3.4.5, 192.168.1.1 Result: 1.2.3.4 (leftmost non-private) ``` * **Valid use cases** * Debugging or logging where you want the "claimed" client IP * Analytics where approximate location matters more than accuracy ## Testing Your Configuration Before deploying features that rely on the client IP address, deploy this middleware with logging into your production network and verify your strategy works correctly: 1. Test with expected traffic: Ensure you get the right IP for normal requests 2. Test spoofing attempts: Verify that fake headers are ignored, you can spoof headers easily with `curl -H "X-Forwarded-For: 1.2.3.4" ...` 3. Monitor for empty results: The middleware returns `nil` when a failure occurs, this could indicate an attack or a configuration problem. ## Common Pitfalls 1. Using multiple headers: Never chain strategies that check different headers from the same request 2. Wrong header choice: For example, using `x-forwarded-for` when your proxy sets `x-real-ip` 3. Ignoring network changes: Proxy counts or ranges change but your app configuration doesn’t 4. Development vs production: Different strategies needed for different environments 5. Not validating proxy control: Assuming headers can’t be spoofed when they can, don’t just trust the hyperscalers or commercial CDNs, _verify_. Remember: _the right strategy depends entirely on your specific network configuration_. When in doubt, analyze real traffic in a real network setting. ## ol.client-ip.cidr # ol.client-ip.cidr ## cidr-parts ```clojure (cidr-parts cidr) ``` Parse an ip network string into its prefix and signifcant bits `cidr` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1) Returns a vector of [^InetAddress prefix ^Integer bits] [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/cidr.clj#L26-L39) --- ## contains? ```clojure (contains? cidr ip) (contains? cidr-ip cidr-mask ip) ``` Check if the given CIDR contains the given IP address. Arguments: cidr - CIDR notation string (e.g. "192.168.0.0/24" or "2001:db8::/32") ip - IP address to check (can be InetAddress or string) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/cidr.clj#L41-L61) --- ## reserved-ranges [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/cidr.clj#L66-L93) --- ## reserved? ```clojure (reserved? ip) ``` Check if an IP is in a reserved range (loopback or private network). [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/cidr.clj#L95-L98) ## ol.client-ip.core # ol.client-ip.core Core implementation of the client-ip library for extracting client IPs from forwarding headers. The client-ip library is designed to accurately determine the original client IP address from HTTP forwarding headers in Ring applications. It uses a strategy-based approach where different strategies handle different network configurations. ## Main Features * Strategy-based IP detection for different network setups * Processes IPs according to the chosen strategy to prevent spoofing * Supports all common headers: X-Forwarded-For, X-Real-IP, Forwarded (RFC 7239), etc. ## Usage The main entry point is the `wrap-client-ip` middleware function that requires a strategy and adds the `:ol/client-ip` key to the Ring request map: ```clojure (require '[ol.client-ip.core :refer [wrap-client-ip]] '[ol.client-ip.strategy :as strategy]) (def app (-> handler (wrap-client-ip {:strategy (strategy/rightmost-non-private-strategy "x-forwarded-for")}) ;; other middleware )) ``` You can also use `from-request` to extract client IPs directly from requests: ```clojure (from-request request (strategy/rightmost-trusted-count-strategy "x-forwarded-for" 2)) ;; => "203.0.113.195" ``` See the strategy documentation for more details on choosing the right strategy. ## from-request ```clojure (from-request request strategy) ``` Extract the client IP from a Ring request using the specified strategy. This is the core function for determining client IPs. It takes a Ring request and a strategy instance, then uses the strategy to determine the client IP from the request headers and remote address. Refer to ns docs and the [`wrap-client-ip`](#wrap-client-ip) docstring for more usage information. Returns the client InetAddress , or nil if no client IP can be determined. [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/core.clj#L43-L58) --- ## wrap-client-ip ```clojure (wrap-client-ip handler options) ``` Ring middleware that adds the client IP to the request map using strategies. This middleware extracts the original client IP address using the specified strategy and adds it to the request map as `:ol/client-ip`. The strategy determines how headers are processed and which IP is considered the client IP. ## Options The options map must contain: * `:strategy` - A strategy instance (required) ## Strategy Selection Choose the strategy that matches your network configuration: * `RemoteAddrStrategy` - Direct connections (no reverse proxy) * `SingleIPHeaderStrategy` - Single trusted reverse proxy with single IP headers * `RightmostNonPrivateStrategy` - Multiple proxies, all with private IPs * `RightmostTrustedCountStrategy` - Fixed number of trusted proxies * `RightmostTrustedRangeStrategy` - Known trusted proxy IP ranges * `ChainStrategy` - Try multiple strategies with fallback ## Examples ```clojure ;; Single reverse proxy with X-Real-IP (def app (-> handler (wrap-client-ip {:strategy (strategy/single-ip-header-strategy "x-real-ip")}) ;; other middleware )) ;; Multiple proxies with private IPs (def app (-> handler (wrap-client-ip {:strategy (strategy/rightmost-non-private-strategy "x-forwarded-for")}) ;; other middleware )) ;; Chain strategy with fallback (def app (-> handler (wrap-client-ip {:strategy (strategy/chain-strategy [(strategy/single-ip-header-strategy "x-real-ip") (strategy/remote-addr-strategy)])}) ;; other middleware )) ``` The middleware adds the `:ol/client-ip` key to the request map, containing the determined client InetAddress or nil. [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/core.clj#L60-L121) ## ol.client-ip.ip # ol.client-ip.ip IP address utilities for conversion and classification. This namespace provides functions for working with IP addresses, particularly for converting between string, InetAddress, and numeric representations. It follows RFC 5952 for IPv6 address formatting and provides utilities for type checking and serialization. **📌 NOTE**\ None of the functions in this namespace accept ip addreses input as strings. Use [`ol.client-ip.parse-ip/from-string`](api/ol-client-ip-parse-ip.adoc#from-string) to convert strings to InetAddress objects. The key operations available are: * Type checking with `ipv4?` and `ipv6?` * String formatting with `format-ip` * Numeric conversion with `->numeric` and `numeric->` These functions are designed to work with the `java.net.InetAddress` class hierarchy and its subclasses `Inet4Address` and `Inet6Address`. Refer to [`ol.client-ip.parse-ip`](api/ol-client-ip-parse-ip.adoc) and [`ol.client-ip.cidr`](api/ol-client-ip-cidr.adoc) for more useful functions. ## ipv4? ```clojure (ipv4? ip) ``` Checks if an IP address is an IPv4 address. This function determines if the given IP address is an instance of `java.net.Inet4Address`. Arguments: ip - An IP address object (InetAddress) Returns: `true` if the IP address is IPv4, `false` otherwise Examples: ```clojure (ipv4? (parse-ip/from-string "192.168.1.1")) ;; => true (ipv4? (parse-ip/from-string "::1")) ;; => false ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L28-L46) --- ## ipv6? ```clojure (ipv6? ip) ``` Checks if an IP address is an IPv6 address. This function determines if the given IP address is an instance of `java.net.Inet6Address`. Arguments: ip - An IP address object (InetAddress) Returns: `true` if the IP address is IPv6, `false` otherwise Examples: ```clojure (ipv6? (parse-ip/from-string "2001:db8::1")) ;; => true (ipv6? (parse-ip/from-string "192.168.1.1")) ;; => false ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L48-L66) --- ## format-ipv6 ```clojure (format-ipv6 ip) ``` Converts an IPv6 address to its canonical string representation. This function formats IPv6 addresses according to RFC 5952, which includes: - Using lowercase hexadecimal digits - Compressing the longest run of consecutive zero fields with :: - Not using :: to compress a single zero field - Including scope IDs with % delimiter for link-local addresses Arguments: ip - An IPv6 address object (Inet6Address) Returns: A string representation of the IPv6 address formatted according to RFC 5952 Examples: ```clojure (format-ipv6 (parse-ip/from-string "2001:db8:0:0:0:0:0:1")) ;; => "2001:db8::1" (format-ipv6 (parse-ip/from-string "fe80::1%eth0")) ;; => "fe80::1%eth0" ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L121-L152) --- ## format-ip ```clojure (format-ip ip) ``` Converts an IP address to its canonical string representation. This function formats IP addresses according to standard conventions: - IPv4 addresses use dotted decimal notation (e.g., "192.168.0.1") - IPv6 addresses follow RFC 5952 with compressed notation using :: for the longest run of zeros, and embedded IPv4 addresses where appropriate The implementation handles scope IDs for IPv6 link-local addresses and properly compresses IPv6 addresses according to the standard rules. Arguments: ip - An IP address object (InetAddress) Returns: A string representation of the IP address Throws: IllegalArgumentException - if the input is not a valid InetAddress Examples: ```clojure (format-ip (parse-ip/from-string "192.168.1.1")) ;; => "192.168.1.1" (format-ip (parse-ip/from-string "2001:db8::1")) ;; => "2001:db8::1" (format-ip (parse-ip/from-string "::ffff:1.2.3.4")) ;; => "::ffff:1.2.3.4" ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L154-L185) --- ## ->numeric ```clojure (->numeric ip) ``` Converts an IP address to its numeric representation as a BigInteger. This function takes an InetAddress (either IPv4 or IPv6) and returns a BigInteger representing the address. The BigInteger can be used for IP address arithmetic, comparison, or storage. Arguments: ip - An IP address object (InetAddress) Returns: A BigInteger representing the IP address Examples: ```clojure (->numeric (parse-ip/from-string "192.0.2.1")) ;; => 3221225985 (->numeric (parse-ip/from-string "2001:db8::1")) ;; => 42540766411282592856903984951653826561 ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L187-L209) --- ## numeric-> ```clojure (numeric-> address version) ``` Converts a BigInteger to an InetAddress. This function takes a numeric representation of an IP address as a BigInteger and converts it back to an InetAddress object. The `ipv6?` flag determines whether to create an IPv4 or IPv6 address. Arguments: address - A BigInteger representing the IP address version - One of :v6 or :v4 indicating whether this is an IPv6 address or an IPv4 address Returns: An InetAddress object representing the numeric address, or nil Throws: ExceptionInfo - If the BigInteger is negative or too large for the specified address type Examples: ```clojure ;; Convert back to IPv4 (numeric-> (BigInteger. "3221226113") :v4) ;; => #object[java.net.Inet4Address 0x578d1c5 "/192.0.2.129"] ;; Convert back to IPv6 (numeric-> (BigInteger. "42540766411282592856903984951653826561") :v6) ;; => #object[java.net.Inet6Address 0x14832c23 "/2001:db8:0:0:0:0:0:1"] ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L211-L267) --- ## numeric-v6-> ```clojure (numeric-v6-> address) ``` Converts a BigInteger to an IPv6 address. This is a convenience wrapper around `numeric->` that automatically sets the `ipv6?` flag to true. Arguments: address - A BigInteger representing the IPv6 address Returns: An Inet6Address object or nil if the input is invalid See also: <<numeric--GT-,`numeric->`>> Examples: ```clojure (numeric-v6-> (BigInteger. "42540766411282592856903984951653826561")) ;; => #object[java.net.Inet6Address 0x42668812 "/2001:db8:0:0:0:0:0:1"] ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L269-L292) --- ## numeric-v4-> ```clojure (numeric-v4-> address) ``` Converts a BigInteger to an IPv4 address. Arguments: address - A BigInteger representing the IPv4 address Returns: An Inet4Address object or nil if the input is invalid See also: <<numeric--GT-,`numeric->`>> Examples: ```clojure (numeric-v4-> (BigInteger. "3221226113")) ;; => #object[java.net.Inet4Address 0x6b88aeff "/192.0.2.129"] ``` [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L294-L314) ## ol.client-ip.parse-ip # ol.client-ip.parse-ip Parse a string representation of an IPv4 or IPv6 address into an InetAddress instance. Unlike `InetAddress/getByName`, the functions in this namespace never cause DNS services to be accessed. This avoids potentially blocking/side-effecting network calls that can occur when using the JDK’s built-in methods to parse IP addresses. This implementation is inspired by Google Guava’s InetAddresses class, which provides similar functionality in Java. This code focuses on strict validation of IP address formats according to relevant RFCs. Features: - Non-blocking IP address parsing with pure functions - Strict RFC-compliant validation - Support for all IPv4 formats - Support for all IPv6 formats (including compressed notation and embedded IPv4) - Support for IPv6 scope identifiers The main entry point is `parse-ip`, which takes a string and returns an InetAddress instance or nil if the input is an invalid ip address literal. ## bytes->inet-address ```clojure (bytes->inet-address addr) (bytes->inet-address addr scope) ``` Convert a byte array into an InetAddress. Args: addr: the raw 4-byte or 16-byte IP address in big-endian order scope: optional scope identifier for IPv6 addresses [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/parse_ip.clj#L231-L256) --- ## from-string ```clojure (from-string ip-string) ``` Returns the InetAddress having the given string representation or nil otherwise. This function parses IP address strings without performing DNS lookups, making it suitable for environments where DNS lookups would cause unwanted blocking or side effects. It supports: - IPv4 addresses in dotted decimal format (e.g., "192.168.1.1") - IPv6 addresses in hex format with optional compression (e.g., "2001:db8::1") - IPv6 addresses with scope IDs (e.g., "fe80::1%eth0" or "fe80::1%1") - IPv6 addresses with embedded IPv4 (e.g., "::ffff:192.168.1.1") If the input is already an InetAddress, it is returned unchanged. Args: ip-string: A string representing an IP address or an InetAddress Returns: An InetAddress object, or nil if the input couldn’t be parsed [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/parse_ip.clj#L258-L290) ## ol.client-ip.protocols # ol.client-ip.protocols ## ClientIPStrategy Protocol for determining client IP from headers and remote address. Returns InetAddress or nil if no valid client ip is found. Implementations should: * Be thread-safe and reusable across requests * Validate configuration at creation time (throw on invalid config) _protocol_ [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/protocols.clj#L5-L21) ### client-ip ```clojure (client-ip this headers remote-addr) ``` Extract the client IP from request headers and remote address. Args: headers: Ring-style headers map (lowercase keys) remote-addr: String from Ring request :remote-addr Returns: InetAddress or nil if no valid client IP found. ## ol.client-ip.strategy # ol.client-ip.strategy Strategy implementations for determining client IP addresses. This namespace provides various strategy implementations for extracting the real client IP from HTTP headers and connection information. Each strategy is designed for specific network configurations: * `RemoteAddrStrategy` - Direct connections (no reverse proxy) * `SingleIPHeaderStrategy` - Single trusted reverse proxy with single IP headers * `RightmostNonPrivateStrategy` - Multiple proxies, all with private IPs * `LeftmostNonPrivateStrategy` - Get IP closest to original client (not secure) * `RightmostTrustedCountStrategy` - Fixed number of trusted proxies * `RightmostTrustedRangeStrategy` - Known trusted proxy IP ranges * `ChainStrategy` - Try multiple strategies in order Strategies are created once and reused across requests. They are thread-safe and designed to fail fast on configuration errors while being resilient to malformed input during request processing. ## parse-forwarded ```clojure (parse-forwarded header-value) ``` Parse a Forwarded header value according to RFC 7239. Returns a sequence of InetAddress objects, or an empty sequence if none are valid. [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L119-L130) --- ## split-host-zone ```clojure (split-host-zone ip-string) ``` Split IPv6 zone from IP address. IPv6 addresses can include zone identifiers (scope IDs) separated by '%'. For example: 'fe80::1%eth0' or 'fe80::1%1' Args: ip-string: String IP address that may contain a zone identifier Returns: Vector of [ip zone] where zone is the zone identifier string, or [ip nil] if no zone is present. Examples: (split-host-zone "192.168.1.1") => ["192.168.1.1" nil] (split-host-zone "fe80::1%eth0") => ["fe80::1" "eth0"] (split-host-zone "fe80::1%1") => ["fe80::1" "1"] [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L150-L171) --- ## remote-addr-strategy ```clojure (remote-addr-strategy) ``` Create a strategy that uses the direct client socket IP. This strategy extracts the IP address from the request’s :remote-addr field, which represents the direct client socket connection. Use this when your server accepts direct connections from clients (not behind a reverse proxy). The strategy strips any port information and validates the IP is not zero/unspecified (0.0.0.0 or ::). Returns: RemoteAddrStrategy instance Example: (remote-addr-strategy) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L184-L200) --- ## single-ip-header-strategy ```clojure (single-ip-header-strategy header-name) ``` Create a strategy that extracts IP from headers containing a single IP address. This strategy is designed for headers like X-Real-IP, CF-Connecting-IP, True-Client-IP, etc. that contain only one IP address. Use this when you have a single trusted reverse proxy that sets a reliable single-IP header. The strategy validates that the header name is not X-Forwarded-For or Forwarded, as these headers can contain multiple IPs and should use different strategies. Args: header-name: String name of the header to check (case-insensitive) Returns: SingleIPHeaderStrategy instance Throws: ExceptionInfo if header-name is invalid or refers to multi-IP headers Examples: (single-ip-header-strategy "x-real-ip") (single-ip-header-strategy "cf-connecting-ip") [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L208-L235) --- ## rightmost-non-private-strategy ```clojure (rightmost-non-private-strategy header-name) ``` Create a strategy that gets the rightmost non-private IP from forwarding headers. This strategy processes IPs in reverse order (rightmost first) and returns the first IP that is not in a private/reserved range. Use this when all your reverse proxies have private IP addresses, so the rightmost non-private IP should be the real client IP. The strategy supports X-Forwarded-For and Forwarded headers that contain comma-separated IP lists. Args: header-name: String name of the header to check (case-insensitive) Should be "x-forwarded-for" or "forwarded" Returns: RightmostNonPrivateStrategy instance Throws: ExceptionInfo if header-name is invalid or refers to single-IP headers Examples: (rightmost-non-private-strategy "x-forwarded-for") (rightmost-non-private-strategy "forwarded") [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L250-L278) --- ## leftmost-non-private-strategy ```clojure (leftmost-non-private-strategy header-name) ``` Create a strategy that gets the leftmost non-private IP from forwarding headers. This strategy processes IPs in forward order (leftmost first) and returns the first IP that is not in a private/reserved range. This gets the IP closest to the original client. ⚠️ ***WARNING: NOT FOR SECURITY USE*** ⚠️ This strategy is easily spoofable since clients can add arbitrary IPs to the beginning of forwarding headers. Use only when security is not a concern and you want the IP closest to the original client. The strategy supports X-Forwarded-For and Forwarded headers that contain comma-separated IP lists. Args: header-name: String name of the header to check (case-insensitive) Should be "x-forwarded-for" or "forwarded" Returns: LeftmostNonPrivateStrategy instance Throws: ExceptionInfo if header-name is invalid or refers to single-IP headers Examples: (leftmost-non-private-strategy "x-forwarded-for") (leftmost-non-private-strategy "forwarded") [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L293-L325) --- ## rightmost-trusted-count-strategy ```clojure (rightmost-trusted-count-strategy header-name trusted-count) ``` Create a strategy that returns IP at specific position based on known proxy count. This strategy is for when you have a fixed number of trusted proxies and want to get the IP at the specific position that represents the original client. Given N trusted proxies, the client IP should be at position -(N+1) from the end. For example, with 2 trusted proxies and header 'client, proxy1, proxy2, proxy3': - Total IPs: 4 - Skip last 2 (trusted): positions 2,3 - Return IP at position 1 (proxy1) Args: header-name: String name of the header to check (case-insensitive) Should be "x-forwarded-for" or "forwarded" trusted-count: Number of trusted proxies (must be > 0) Returns: RightmostTrustedCountStrategy instance Throws: ExceptionInfo if header-name is invalid or trusted-count <= 0 Examples: (rightmost-trusted-count-strategy "x-forwarded-for" 1) (rightmost-trusted-count-strategy "forwarded" 2) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L338-L371) --- ## rightmost-trusted-range-strategy ```clojure (rightmost-trusted-range-strategy header-name trusted-ranges) ``` Create a strategy that returns rightmost IP not in trusted ranges. This strategy processes IPs in reverse order (rightmost first) and returns the first IP that is not within any of the specified trusted IP ranges. Use this when you know the specific IP ranges of your trusted proxies. The strategy supports X-Forwarded-For and Forwarded headers that contain comma-separated IP lists. Args: header-name: String name of the header to check (case-insensitive) Should be "x-forwarded-for" or "forwarded" trusted-ranges: Collection of CIDR ranges (strings) that represent trusted proxies Each range should be a valid CIDR notation like "192.168.1.0/24" Returns: RightmostTrustedRangeStrategy instance Throws: ExceptionInfo if header-name is invalid or trusted-ranges is empty/invalid Examples: (rightmost-trusted-range-strategy "x-forwarded-for" ["10.0.0.0/8" "192.168.0.0/16"]) (rightmost-trusted-range-strategy "forwarded" ["172.16.0.0/12"]) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L386-L424) --- ## chain-strategy ```clojure (chain-strategy strategies) ``` Create a strategy that tries multiple strategies in order until one succeeds. This strategy allows fallback scenarios where you want to try different IP detection methods in priority order. Each strategy is tried in sequence until one returns a non-empty result. Common use case: Try a single-IP header first, then fall back to remote address. Args: strategies: Vector of strategy instances to try in order Must contain at least one strategy Returns: ChainStrategy instance Throws: ExceptionInfo if strategies is empty or contains invalid strategies Examples: (chain-strategy [(single-ip-header-strategy "x-real-ip") (remote-addr-strategy)]) (chain-strategy [(rightmost-non-private-strategy "x-forwarded-for") (single-ip-header-strategy "x-real-ip") (remote-addr-strategy)]) [source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L435-L470) ## Changelog # Changelog All notable changes to this project will be documented in this file. This project uses https://www.taoensso.com/break-versioning[*Break Versioning*]. ## ++[++UNRELEASED++]++ ## `v0.1.1` (2025-08-18) Sorry, this is my first from-scratch clojars release, and I’m struggling to get the metadata working. There are no changes. ## `v0.1.0` (2025-08-18) This is the first public release of this codebase under the `ol.client-ip` moniker. Variations of this code has been used in production for a long time, but nonetheless I want to be cautious so we’re starting with sub-1.0 versioning. Please report any problems and let me know if anything is unclear, inconvenient, etc. Thank you! 🙏 ## ol.client-ip # ol.client-ip > A 0-dependency ring middleware for determining a request’s real client IP address from HTTP headers ![doc](https://img.shields.io/badge/doc-outskirtslabs-orange.svg) ![status: stable](https://img.shields.io/badge/status-stable-brightgreen.svg) ![Build Status](https://github.com/outskirtslabs/client-ip/actions/workflows/ci.yml/badge.svg) ![Clojars Project](https://img.shields.io/clojars/v/com.outskirtslabs/client-ip.svg) `X-Forwarded-For` and other client IP headers are [often used incorrectly](https://adam-p.ca/blog/2022/03/x-forwarded-for/), resulting in bugs and security vulnerabilities. This library provides strategies for extracting the correct client IP based on your network configuration. It is based on the golang reference implementation [realclientip/realclientip-go](https://github.com/realclientip/realclientip-go). Quick feature list: * ring middleware determining the client’s IP address * 0 dependency IP address string parsing with guaranteed no trips to the hosts' DNS services, which can block and timeout (unlike Java’s `InetAddress/getByName`) * rightmost-ish strategies support the `X-Forwarded-For` and `Forwarded` (RFC 7239) headers * IPv6 zone identifiers support Note that there is no dependency on ring, the public API could also be used for pedestal or sieppari-style interceptors. Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**. ## Installation ```clojure ;; deps.edn {:deps {com.outskirtslabs/client-ip {:mvn/version "0.1.1"}}} ;; Leiningen [com.outskirtslabs/client-ip "0.1.1"] ``` ## Quick Start ```clojure (ns myapp.core (:require [ol.client-ip.core :as client-ip] [ol.client-ip.strategy :as strategy])) ;; Simple case: behind a trusted proxy that sets X-Real-IP (def app (-> handler (client-ip/wrap-client-ip {:strategy (strategy/single-ip-header-strategy "x-real-ip")}))) ;; The client IP is now available in the request (defn handler [request] (let [client-ip (:ol/client-ip request)] {:status 200 :body (str "Your IP is: " client-ip)})) ``` For detailed guidance on choosing strategies, see [Usage Guide](usage.adoc). Choosing the wrong strategy can result in IP address spoofing security vulnerabilities. ## Documentation * [Docs](https://docs.outskirtslabs.com/ol.client-ip/0.1/) * [API Reference](https://docs.outskirtslabs.com/ol.client-ip/0.1/api) * [Support via GitHub Issues](https://github.com/outskirtslabs/client-ip/issues) ## Recommended Reading You think it is an easy question: > I have an HTTP application, I just want to know the IP address of my client. But who is the client? > The computer on the other end of the network connection? But which network connection? The one connected to your HTTP application is probably a reverse proxy or load balancer. > Well I mean the "user’s IP address" It ain’t so easy kid. There are many good articles on the internet that discuss the perils and pitfalls of trying to answer this deceptively simple question. You _should_ read one or two of them to get an idea of the complexity in this space. Libraries, like this one, _cannot_ hide the complexity from you, there is no abstraction nor encapsulation nor "default best practice". Below are some of those good articles: * MDN: [X-Forwarded-For: Security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#security_and_privacy_concerns) * [The perils of the "real" client IP](https://adam-p.ca/blog/2022/03/x-forwarded-for/) ([archive link](https://web.archive.org/web/20250416042714/https://adam-p.ca/blog/2022/03/x-forwarded-for/)) * [Rails IP Spoofing Vulnerabilities and Protection](https://www.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) ([archive link](https://web.archive.org/web/20250421121810/https://www.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/)) * [ol.client-ip: A Clojure Library to Prevent IP Spoofing](https://casey.link/blog/client-ip-ring-middleware/) ## Security See [here](https://github.com/outskirtslabs/client-ip/security/advisories) for security advisories or to report a security vulnerability. ## License Copyright (C) 2025 Casey Link <casey@outskirtslabs.com> Distributed under the [MIT License](https://github.com/outskirtslabs/client-ip/blob/main/LICENSE). ## Security policy # Security policy ## Advisories All security advisories will be posted https://github.com/outskirtslabs/client-ip/security/advisories[on GitHub]. ## Reporting a vulnerability Please report possible security vulnerabilities https://github.com/outskirtslabs/client-ip/security/advisories[via GitHub], or by emailing me at `casey@outskirtslabs.com`. You may encrypt emails with [my public PGP key](https://casey.link/pgp.asc). For the organization-wide security policy, see [Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy). Thank you! — [Casey Link](https://casey.link) ## Usage Guide # Usage Guide This guide explains when and why to use each strategy. The choice of strategy is critical -- _using the wrong strategy can lead to IP spoofing vulnerabilities_. ## Understanding Your Network Setup Before choosing a strategy, you need to understand your production environment’s network topology: 1. Are you behind reverse proxies? (nginx, Apache, load balancers, CDNs) 2. How many trusted proxies are there? 3. What headers do they set? The answers determine which strategy is appropriate and secure for your setup. ## Terminology * **socket-level IP**\ The IP address that a server or proxy observes at the TCP socket level from whatever is directly connecting to it. This is always trustworthy since it cannot be spoofed at the socket level, regardless of whether the connection is from an end user or another proxy in the chain. * **private IP addresses**\ IP addresses reserved for private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) that are not routable on the public internet. These typically represent your internal infrastructure components. * **reverse proxy / proxy / load balancer / CDN**\ An intermediary server that sits between clients and your application server, forwarding client requests. Examples include: nginx, Apache, caddy, AWS ALB, Cloudflare, Cloudfront, Fastly, Akamai, Fly.io, Hetzner Load Balancer. * **trusted proxy**\ A reverse proxy under your control or operated by a service you trust, whose IP forwarding headers you can rely on. The key characteristic is that you know what headers it sets and clients cannot bypass it. * **IP spoofing**\ The practice of sending packets with a forged source IP address, or in the context of HTTP headers, setting fake client IP values in headers like `X-Forwarded-For` to deceive the application about the request’s true origin. * **trivially spoofable**\ Headers or IP values that can be easily faked by any client without special network access. For example, any client can send `X-Forwarded-For: 1.2.3.4` in their request. * **XFF**\ Short for `X-Forwarded-For`, the most common HTTP header used by proxies to communicate the original client IP address through a chain of proxies. Each proxy typically appends the IP it received the request from. * **rightmost-ish**\ A parsing strategy that extracts IP addresses from the right side of forwarding headers like `X-Forwarded-For`, typically skipping a known number of trusted proxies to find the IP added by the first trusted proxy. This is the only trustworthy approach since you control what your trusted proxies add to the header chain ([see this article](https://adam-p.ca/blog/2022/03/x-forwarded-for)). * **leftmost-ish**\ A parsing strategy that takes the leftmost (first) IP address in forwarding headers, representing the alleged original client. This is highly untrustworthy and vulnerable to spoofing since any client can set arbitrary values at the beginning of these headers ([see this article](https://adam-p.ca/blog/2022/03/x-forwarded-for)). ## Strategy Reference ### Remote Address Strategy / No middleware at all! * **When to use**\ Your server accepts direct connections from clients without any reverse proxies or load balancers. * **Why**\ The socket-level remote address is the actual client IP when there are no intermediaries. Using this library you can use the remote-addr-strategy, which is provided to be comprehensive, however you can also just use the existing [`:remote-addr`](https://github.com/ring-clojure/ring/blob/master/SPEC.md#remote-addr) key in your ring request map. ```clojure (strategy/remote-addr-strategy) ;; OR, just fetch the remote addr from the ring request map (:remote-addr request) ``` * **Example network** ``` Client -> Your Server ``` * **Security**\ Safe -- the remote address cannot be spoofed at the socket level. --- ### Single IP Header Strategy * **When to use**\ You have a trusted reverse proxy that accepts connections directly from clients and sets a specific header with the client IP. * **Why**\ Many Cloud Provider’s CDNs and load balancers products provide a single authoritative header with the real client IP. This is the most secure approach when you have such a setup. * **Provider headers with trusted values** * Cloudflare (everyone): `cf-connecting-ip` -- this is a socket-level IP value ([docs](https://developers.cloudflare.com/fundamentals/get-started/http-request-headers/)) * Cloudflare (enterprise): `true-client-ip` -- also a socket-level IP value, and just for Enterprise customers with backwards compatibility requirements ([docs](https://developers.cloudflare.com/fundamentals/get-started/http-request-headers/)) * Fly.io: `fly-client-ip` -- the socket ip address ([docs](https://www.fly.io/docs/networking/request-headers/#fly-client-ip)) * Azure FrontDoor: `x-azure-socketip` -- the socket IP address associated with the TCP connection that the current request originated from ([docs](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol)) * nginx with correctly configured [`ngx_http_realip_module`](https://nginx.org/en/docs/http/ngx_http_realip_module.html) * Do not use `proxy_set_header X-Real-IP $remote_addr;` * **Provider headers that require extra config** These providers offer headers that out of the box are trivially spoofable, and require extra configuration (in your provider’s management interface) to configure securely. * Akamai: `true-client-ip` -- trivially spoofable by default, refer to [this writeup](https://adam-p.ca/blog/2022/03/x-forwarded-for/#akamai) * Fastly: `fastly-client-ip` -- trivially spoofable, you must use vcl to configure it [fastly docs](https://www.fastly.com/documentation/reference/http/http-headers/Fastly-Client-IP/) * **Provider headers to avoid** **⚠️ WARNING**\ In nearly all of these cases you are better off using XFF and reasoning about the number of proxies or their network addresses. * Azure FrontDoor: `x-azure-clientip` -- trivially spoofable as it is the leftmost-ish XFF ([docs](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol)) ```clojure ;; Clients connect to your application server *only* through and *directly* through Cloudflare (strategy/single-ip-header-strategy "cf-connecting-ip") ;; Clients connect to your application server *only* through and *directly* through Fly.io (strategy/single-ip-header-strategy "fly-client-ip") ``` * **Example networks** ``` Client -> Cloudflare -> Your Server Client -> nginx -> Your Server Client -> Load Balancer -> Your Server ``` * **Security considerations** * Ensure the header cannot be spoofed by clients * Verify clients cannot bypass your proxy * Use only the header _your_ trusted proxy sets **⚠️ WARNING**\ Common mistakes * Using `x-forwarded-for` with this strategy (use rightmost strategies instead) * Not validating that clients must go through your proxy * Changes to your topology that break a rule that was previously true * For example, adding another proxy in the chain or exposing servers on a different interface * Using headers that can be set by untrusted sources --- ### Rightmost Non-Private Strategy * **When to use**\ You have multiple reverse proxies between the internet and the server, all using private IP addresses, and they append to `X-Forwarded-For` or `Forwarded` headers. * **Why**\ In a typical private network setup, the rightmost non-private IP in the trusted forwarding chain is the real client IP. Private IPs represent your infrastructure. ```clojure (strategy/rightmost-non-private-strategy "x-forwarded-for") ;; Using RFC 7239 Forwarded header (strategy/rightmost-non-private-strategy "forwarded") ``` * **Example network** ``` Client -> Internet Proxy -> Private Load Balancer -> Private App Server (1.2.3.4) (10.0.1.1) (10.0.2.1) X-Forwarded-For: 1.2.3.4, 10.0.1.1 Result: 1.2.3.4 (rightmost non-private) ``` * **Security**\ Secure. Attackers can still spoof the leftmost entries, but not the rightmost non-private IP. **⚠️ WARNING**\ Common Mistakes * Do not use when your proxies have public IP addresses * Do not use when you need to trust specific proxy IPs (use trusted range strategy instead) --- ### Rightmost Trusted Count Strategy * **When to use**\ You know exactly how many trusted proxies append IPs to the header, and you want the IP added by the first trusted proxy. * **Why**\ When you have a fixed, known number of trusted proxies, counting backwards gives you the IP that was added by your first trusted proxy (the client IP it saw). ```clojure ;; Two trusted proxies (strategy/rightmost-trusted-count-strategy "x-forwarded-for" 2) ;; One trusted proxy (strategy/rightmost-trusted-count-strategy "forwarded" 1) ``` * **Example with count=2** ``` Client -> Proxy1 -> Proxy2 -> Your Server (adds A) (adds B) X-Forwarded-For: A, B With count=2: Skip 2 from right, return A ``` * **Security**\ Secure, when your proxy count is stable and known. **⚠️ WARNING**\ Common Mistakes * Count must exactly match your trusted proxy count * If the count is wrong, you’ll get incorrect results or errors * Network topology changes require updating the count on the application server --- ### Rightmost Trusted Range Strategy * **When to use**\ You know the IP ranges of all your trusted proxies and want the rightmost IP that’s not from a trusted source. * **Why**\ This is the most flexible strategy for complex infrastructures where you know your proxy IPs but they might change within known ranges. ```clojure ;; You have a VPC where your client-facing load balancer could be any ip address ;; inside the 10.1.1.0/24 subnet (az1) or 10.1.2.0/24 (az2) (strategy/rightmost-trusted-range-strategy "x-forwarded-for" ["10.1.1.0/24" "10.1.2.0/24"]) ;; Including Cloudflare ranges (these are examples, do not copy them!) (strategy/rightmost-trusted-range-strategy "x-forwarded-for" ["173.245.48.0/20" "103.21.244.0/22" ; Example Cloudflare IPv4 ranges "2400:cb00::/32"]) ; Example Cloudflare IPv6 range ``` * **Example** ``` Client -> Cloudflare -> Your Load Balancer -> App Server 1.2.3.4 173.245.48.1 10.0.1.1 X-Forwarded-For: 1.2.3.4, 173.245.48.1, 10.0.1.1 Trusted ranges: ["173.245.48.0/20", "10.0.0.0/8"] Result: 1.2.3.4 (rightmost IP not in trusted ranges) ``` * **Security**\ Secure, when ranges are properly maintained. **⚠️ WARNING**\ Common Mistakes * Forgetting to keep the trusted ranges up to date * Not including all possible proxy IPs * When using a cloud provider, not using their API for current up to date ranges --- ### Chain Strategy * **When to use**\ You have multiple possible network paths to your server and need fallback behavior. **⚠️ WARNING**\ Do not abuse ChainStrategy to check multiple headers. There is likely only one header you should be checking, and checking more can leave you vulnerable to IP spoofing. Each strategy should represent a different network path, not multiple ways to parse the same path. * **Why**\ Real-world deployments can have multiple possible configurations (direct connections or via CDN). Chain strategy tries each approach until one succeeds. ```clojure ;; Clients can connect via cloudflare or directly to your server (strategy/chain-strategy [(strategy/single-ip-header-strategy "cf-connecting-ip") (strategy/remote-addr-strategy)]) ;; Multiple fallback levels (strategy/chain-strategy [(strategy/rightmost-trusted-range-strategy "x-forwarded-for" ["10.0.0.0/8"]) (strategy/rightmost-non-private-strategy "x-forwarded-for") (strategy/remote-addr-strategy)]) ``` * **Example use cases** * Development vs production environments * Gradual migration between proxy setups * Handling both CDN and direct traffic --- ### Leftmost Non-Private Strategy **⚠️ WARNING**\ DO NOT USE UNLESS YOU REALLY KNOW WHAT YOU ARE DOING. The leftmost IP can be trivially spoofed by clients. * **When to use**\ Rarely recommended. Only when you specifically need the IP allegedly closest to the original client (knowing full well that it can be spoofed). * **Why**\ This gives you the "apparent" client IP but offers _no security_ against spoofing. ```clojure (strategy/leftmost-non-private-strategy "x-forwarded-for") (strategy/leftmost-non-private-strategy "forwarded") ``` * **Example** ``` X-Forwarded-For: 1.2.3.4, 2.3.4.5, 192.168.1.1 Result: 1.2.3.4 (leftmost non-private) ``` * **Valid use cases** * Debugging or logging where you want the "claimed" client IP * Analytics where approximate location matters more than accuracy ## Testing Your Configuration Before deploying features that rely on the client IP address, deploy this middleware with logging into your production network and verify your strategy works correctly: 1. Test with expected traffic: Ensure you get the right IP for normal requests 2. Test spoofing attempts: Verify that fake headers are ignored, you can spoof headers easily with `curl -H "X-Forwarded-For: 1.2.3.4" ...` 3. Monitor for empty results: The middleware returns `nil` when a failure occurs, this could indicate an attack or a configuration problem. ## Common Pitfalls 1. Using multiple headers: Never chain strategies that check different headers from the same request 2. Wrong header choice: For example, using `x-forwarded-for` when your proxy sets `x-real-ip` 3. Ignoring network changes: Proxy counts or ranges change but your app configuration doesn’t 4. Development vs production: Different strategies needed for different environments 5. Not validating proxy control: Assuming headers can’t be spoofed when they can, don’t just trust the hyperscalers or commercial CDNs, _verify_. Remember: _the right strategy depends entirely on your specific network configuration_. When in doubt, analyze real traffic in a real network setting.