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