X-Forwarded-For parsing and trusted proxy detection for OCaml
1(** X-Forwarded-For parsing and trusted proxy detection.
2
3 This module provides utilities for extracting client IP addresses from
4 X-Forwarded-For headers when running behind reverse proxies, with support
5 for trusted proxy validation to prevent IP spoofing.
6
7 {2 Example}
8
9 {[
10 (* Configure trusted proxies *)
11 let trusted = Xff.private_ranges
12
13 (* Extract client IP from request *)
14 let client_ip =
15 Xff.get_client_ip_string ~socket_ip:(Some socket_addr)
16 ~xff_header:(Some "203.0.113.50, 10.0.0.1")
17 ~trusted_proxies:(Some trusted)
18 (* Returns "203.0.113.50" if socket_ip is in trusted range *)
19 ]}
20
21 {2 References}
22
23 - {{:https://datatracker.ietf.org/doc/html/rfc7239} RFC 7239} - Forwarded
24 HTTP Extension
25 - X-Forwarded-For is a de-facto standard *)
26
27(** {1 Types} *)
28
29type ip = Ipaddr.t
30(** An IP address (IPv4 or IPv6). *)
31
32type prefix = Ipaddr.Prefix.t
33(** A CIDR prefix for matching IP ranges. *)
34
35(** {1 CIDR Parsing} *)
36
37val parse_cidr : string -> (prefix, [ `Msg of string ]) result
38(** [parse_cidr s] parses a CIDR string like ["192.168.0.0/16"] or
39 ["2001:db8::/32"]. *)
40
41val parse_cidr_exn : string -> prefix
42(** [parse_cidr_exn s] is like {!parse_cidr} but raises [Invalid_argument] on
43 error. *)
44
45(** {1 Trusted Proxy Detection} *)
46
47val ip_in_prefix : ip -> prefix -> bool
48(** [ip_in_prefix ip prefix] returns [true] if [ip] is within the CIDR [prefix].
49*)
50
51val is_trusted_proxy : ip -> prefix list -> bool
52(** [is_trusted_proxy ip prefixes] returns [true] if [ip] matches any prefix in
53 the list. *)
54
55(** {1 X-Forwarded-For Parsing} *)
56
57val parse_xff : string -> ip list
58(** [parse_xff header] parses an X-Forwarded-For header value and returns all
59 valid IP addresses. The header format is ["client, proxy1, proxy2, ..."].
60 Invalid entries are silently skipped. Port suffixes are stripped. *)
61
62val first_xff_ip : string -> ip option
63(** [first_xff_ip header] extracts the leftmost (original client) IP from an
64 X-Forwarded-For header. Returns [None] if no valid IP is found. *)
65
66(** {1 Client IP Extraction} *)
67
68val client_ip :
69 socket_ip:ip option ->
70 xff_header:string option ->
71 trusted_proxies:prefix list option ->
72 ip option
73(** [client_ip ~socket_ip ~xff_header ~trusted_proxies] extracts the real client
74 IP address considering trusted proxy configuration.
75
76 - If [socket_ip] is from a trusted proxy and [xff_header] is present,
77 returns the client IP from X-Forwarded-For
78 - Otherwise returns [socket_ip]
79 - Returns [None] if [socket_ip] is [None]
80
81 This prevents IP spoofing: only connections from trusted proxies can set the
82 client IP via X-Forwarded-For. *)
83
84val client_ip_string :
85 socket_ip:ip option ->
86 xff_header:string option ->
87 trusted_proxies:prefix list option ->
88 string
89(** [client_ip_string ~socket_ip ~xff_header ~trusted_proxies] is like
90 {!client_ip} but returns a string. Returns ["unknown"] if the IP cannot be
91 determined. *)
92
93(** {1 Common Trusted Proxy Ranges} *)
94
95val private_ranges : prefix list
96(** [private_ranges] is a list of RFC 1918 private ranges, loopback, and IPv6
97 equivalents: [10.0.0.0/8], [172.16.0.0/12], [192.168.0.0/16], [127.0.0.0/8],
98 [::1/128], [fc00::/7], [fe80::/10]. *)
99
100val cloudflare_ranges : prefix list
101(** Cloudflare proxy IP ranges (IPv4 and IPv6). See
102 {{:https://www.cloudflare.com/ips/} Cloudflare IPs}. *)