1
use super::*;
2

            
3
const INGRESS: &str = "10.104.0.0/16";
4

            
5
22
fn peer(addr: &str) -> SocketAddr {
6
22
    SocketAddr::new(addr.parse().expect("test address parses"), 45678)
7
22
}
8

            
9
82
fn ip(addr: &str) -> IpAddr {
10
82
    addr.parse().expect("test address parses")
11
82
}
12

            
13
#[test]
14
2
fn cidr_matches_only_inside_its_prefix() {
15
2
    let cidr = Cidr::parse(INGRESS).expect("valid cidr");
16

            
17
2
    assert!(cidr.contains(ip("10.104.0.9")));
18
2
    assert!(cidr.contains(ip("10.104.255.1")));
19
2
    assert!(!cidr.contains(ip("10.105.0.9")));
20
2
    assert!(!cidr.contains(ip("10.4.0.9")));
21
2
}
22

            
23
#[test]
24
2
fn cidr_handles_prefixes_that_do_not_land_on_a_byte() {
25
2
    let cidr = Cidr::parse("192.160.0.0/12").expect("valid cidr");
26

            
27
2
    assert!(cidr.contains(ip("192.160.0.1")));
28
2
    assert!(cidr.contains(ip("192.170.5.5")));
29
2
    assert!(!cidr.contains(ip("192.176.0.1")));
30
2
}
31

            
32
#[test]
33
2
fn a_bare_address_is_a_single_host_range() {
34
2
    let cidr = Cidr::parse("10.104.0.9").expect("valid address");
35

            
36
2
    assert!(cidr.contains(ip("10.104.0.9")));
37
2
    assert!(!cidr.contains(ip("10.104.0.10")));
38
2
}
39

            
40
#[test]
41
2
fn cidr_rejects_nonsense() {
42
2
    assert!(Cidr::parse("not-an-address").is_none());
43
2
    assert!(Cidr::parse("10.104.0.0/33").is_none());
44
2
    assert!(Cidr::parse("10.104.0.0/abc").is_none());
45
2
}
46

            
47
#[test]
48
2
fn a_v4_range_never_matches_a_v6_peer() {
49
2
    let cidr = Cidr::parse("0.0.0.0/0").expect("valid cidr");
50

            
51
2
    assert!(!cidr.contains(ip("::1")));
52
2
}
53

            
54
#[test]
55
2
fn an_untrusted_peer_cannot_forge_its_address() {
56
2
    let trusted = TrustedProxies::from_spec(INGRESS);
57

            
58
2
    assert_eq!(
59
2
        trusted.client_ip(peer("203.0.113.7"), Some("1.2.3.4")),
60
2
        ip("203.0.113.7"),
61
        "a header from an untrusted peer must be ignored outright"
62
    );
63
2
}
64

            
65
#[test]
66
2
fn a_trusted_proxy_speaks_for_its_client() {
67
2
    let trusted = TrustedProxies::from_spec(INGRESS);
68

            
69
2
    assert_eq!(
70
2
        trusted.client_ip(peer("10.104.0.9"), Some("203.0.113.7")),
71
2
        ip("203.0.113.7")
72
    );
73
2
}
74

            
75
#[test]
76
2
fn a_client_supplied_prefix_cannot_shadow_the_observed_hop() {
77
2
    let trusted = TrustedProxies::from_spec(INGRESS);
78

            
79
    // The attacker sent `X-Forwarded-For: 1.2.3.4`; our proxy appended what it
80
    // actually saw. Only the rightmost untrusted entry is evidence.
81
2
    assert_eq!(
82
2
        trusted.client_ip(peer("10.104.0.9"), Some("1.2.3.4, 203.0.113.7")),
83
2
        ip("203.0.113.7"),
84
        "throttling must key on the hop our proxy observed, not the one the client claimed"
85
    );
86
2
}
87

            
88
#[test]
89
2
fn trusted_hops_are_skipped_from_the_right() {
90
2
    let trusted = TrustedProxies::from_spec(INGRESS);
91

            
92
2
    assert_eq!(
93
2
        trusted.client_ip(peer("10.104.0.9"), Some("203.0.113.7, 10.104.0.3")),
94
2
        ip("203.0.113.7")
95
    );
96
2
}
97

            
98
#[test]
99
2
fn a_trusted_peer_with_no_usable_header_falls_back_to_itself() {
100
2
    let trusted = TrustedProxies::from_spec(INGRESS);
101

            
102
2
    assert_eq!(
103
2
        trusted.client_ip(peer("10.104.0.9"), None),
104
2
        ip("10.104.0.9")
105
    );
106
2
    assert_eq!(
107
2
        trusted.client_ip(peer("10.104.0.9"), Some("")),
108
2
        ip("10.104.0.9")
109
    );
110
2
    assert_eq!(
111
2
        trusted.client_ip(peer("10.104.0.9"), Some("garbage, junk")),
112
2
        ip("10.104.0.9")
113
    );
114
2
}
115

            
116
#[test]
117
2
fn trusting_nothing_always_uses_the_peer() {
118
2
    let trusted = TrustedProxies::from_spec("");
119

            
120
2
    assert_eq!(
121
2
        trusted.client_ip(peer("10.104.0.9"), Some("203.0.113.7")),
122
2
        ip("10.104.0.9"),
123
        "an unconfigured deployment must not honour forwarded headers"
124
    );
125
2
}
126

            
127
/// `from_spec` is lenient by design; `from_env` is the strict door. Keeping the
128
/// distinction tested stops someone "fixing" the leniency and breaking callers
129
/// that pass a literal.
130
#[test]
131
2
fn from_spec_drops_unparseable_entries_where_from_env_would_refuse() {
132
2
    let trusted = TrustedProxies::from_spec("nonsense, 10.104.0.0/16, /16");
133

            
134
2
    assert!(trusted.trusts(ip("10.104.0.9")));
135
2
    assert!(!trusted.trusts(ip("203.0.113.7")));
136
2
}
137

            
138
#[test]
139
2
fn a_v4_mapped_hop_is_the_same_client_as_its_plain_form() {
140
2
    let trusted = TrustedProxies::from_spec(INGRESS);
141
2
    let ingress = peer("10.104.0.9");
142

            
143
2
    assert_eq!(
144
2
        trusted.client_ip(ingress, Some("::ffff:203.0.113.7")),
145
2
        ip("203.0.113.7"),
146
        "a mapped address must not buy a second budget under a different key"
147
    );
148
2
}
149

            
150
#[test]
151
2
fn a_v4_mapped_proxy_is_still_recognised_as_trusted() {
152
2
    let trusted = TrustedProxies::from_spec(INGRESS);
153

            
154
2
    assert!(
155
2
        trusted.trusts(ip("::ffff:10.104.0.9")),
156
        "the mapped form of a trusted proxy must not be treated as an outsider"
157
    );
158
2
}
159

            
160
#[test]
161
2
fn every_forwarded_header_line_is_read_not_just_the_first() {
162
2
    let mut headers = axum::http::HeaderMap::new();
163
2
    headers.append("x-forwarded-for", "1.2.3.4".parse().expect("valid header"));
164
2
    headers.append(
165
        "x-forwarded-for",
166
2
        "203.0.113.7".parse().expect("valid header"),
167
    );
168

            
169
2
    let joined = forwarded_for(&headers).expect("header present");
170
2
    let trusted = TrustedProxies::from_spec(INGRESS);
171

            
172
2
    assert_eq!(
173
2
        trusted.client_ip(peer("10.104.0.9"), Some(&joined)),
174
2
        ip("203.0.113.7"),
175
        "a client-supplied header line must not hide the hop the proxy appended"
176
    );
177
2
}
178

            
179
#[test]
180
2
fn an_absent_forwarded_header_reads_as_nothing() {
181
2
    assert!(forwarded_for(&axum::http::HeaderMap::new()).is_none());
182
2
}
183

            
184
#[test]
185
2
fn throttle_key_folds_an_ipv6_host_range_but_not_a_network() {
186
2
    assert_eq!(
187
2
        throttle_key(ip("2001:db8:1:1::dead")),
188
2
        throttle_key(ip("2001:db8:1:1::beef")),
189
        "hosts inside one /64 are one client"
190
    );
191
2
    assert_ne!(
192
2
        throttle_key(ip("2001:db8:1:1::1")),
193
2
        throttle_key(ip("2001:db8:1:2::1")),
194
        "a different /64 is a different client"
195
    );
196
2
}
197

            
198
#[test]
199
2
fn throttle_key_leaves_ipv4_alone() {
200
2
    assert_eq!(throttle_key(ip("203.0.113.7")), ip("203.0.113.7"));
201
2
    assert_ne!(
202
2
        throttle_key(ip("203.0.113.7")),
203
2
        throttle_key(ip("203.0.113.8"))
204
    );
205
2
}
206

            
207
#[test]
208
2
fn throttle_key_folds_a_mapped_address_onto_its_v4_form() {
209
2
    assert_eq!(throttle_key(ip("::ffff:203.0.113.7")), ip("203.0.113.7"));
210
2
}
211

            
212
// `TRUSTED_PROXIES` is process-global, so these cases share one test rather
213
// than racing each other across threads.
214
#[test]
215
2
fn a_partly_unparseable_list_is_refused_outright() {
216
    // Dropping the bad entry with a warning would leave the good one trusted and
217
    // the other ingress path silently untrusted — every user on it sharing one
218
    // login budget, which is what this variable exists to prevent.
219
    // SAFETY: single-threaded within this test.
220
2
    unsafe {
221
2
        std::env::set_var("TRUSTED_PROXIES", "10.104.0.0/20,not-a-cidr");
222
2
    }
223
2
    assert!(matches!(
224
2
        TrustedProxies::from_env(),
225
        Err(TrustedProxyError::Unparseable(_))
226
    ));
227

            
228
2
    unsafe {
229
2
        std::env::set_var("TRUSTED_PROXIES", "10.104.0.0/20,10.244.0.0/16");
230
2
    }
231
2
    let trusted = TrustedProxies::from_env().expect("both entries parse");
232
2
    assert!(trusted.trusts(ip("10.104.0.7")));
233
2
    assert!(trusted.trusts(ip("10.244.2.231")));
234

            
235
2
    unsafe {
236
2
        std::env::remove_var("TRUSTED_PROXIES");
237
2
    }
238
2
}
239

            
240
#[test]
241
2
fn from_env_demands_an_explicit_decision() {
242
    // SAFETY: single-threaded within this test; the variable is read only here.
243
2
    unsafe {
244
2
        std::env::remove_var("TRUSTED_PROXIES");
245
2
    }
246
2
    assert!(
247
2
        matches!(TrustedProxies::from_env(), Err(TrustedProxyError::Unset)),
248
        "an unset variable must not be guessed either way"
249
    );
250

            
251
2
    unsafe {
252
2
        std::env::set_var("TRUSTED_PROXIES", "garbage");
253
2
    }
254
2
    assert!(matches!(
255
2
        TrustedProxies::from_env(),
256
        Err(TrustedProxyError::Unparseable(_))
257
    ));
258

            
259
    // Entries that are all empty separators leave nothing to parse at all.
260
2
    unsafe {
261
2
        std::env::set_var("TRUSTED_PROXIES", " , ");
262
2
    }
263
2
    assert!(matches!(
264
2
        TrustedProxies::from_env(),
265
        Err(TrustedProxyError::NoUsableEntry(_))
266
    ));
267

            
268
2
    unsafe {
269
2
        std::env::set_var("TRUSTED_PROXIES", "none");
270
2
    }
271
2
    let trusted = TrustedProxies::from_env().expect("`none` is a valid answer");
272
2
    assert!(!trusted.trusts(ip("10.104.0.9")));
273

            
274
2
    unsafe {
275
2
        std::env::set_var("TRUSTED_PROXIES", INGRESS);
276
2
    }
277
2
    let trusted = TrustedProxies::from_env().expect("a CIDR list is a valid answer");
278
2
    assert!(trusted.trusts(ip("10.104.0.9")));
279

            
280
2
    unsafe {
281
2
        std::env::remove_var("TRUSTED_PROXIES");
282
2
    }
283
2
}
284

            
285
#[test]
286
2
fn both_deployed_proxy_ranges_are_trusted() {
287
    // The production value. Two ranges, because the nginx ingress is a
288
    // hostNetwork DaemonSet: from another node the peer is the VPC address, but
289
    // from the node the web pod itself runs on it is that node's cilium_host,
290
    // inside the pod CIDR. Measured 2026-09-05.
291
2
    let trusted = TrustedProxies::from_spec("10.104.0.0/20,10.244.0.0/16");
292

            
293
2
    assert!(
294
2
        trusted.trusts(ip("10.104.0.7")),
295
        "another node's VPC address"
296
    );
297
2
    assert!(
298
2
        trusted.trusts(ip("10.244.2.231")),
299
        "the co-located node's cilium_host"
300
    );
301
2
    assert!(
302
2
        !trusted.trusts(ip("203.0.113.7")),
303
        "a real client is still not a proxy"
304
    );
305

            
306
    // A client behind either path keeps its own identity.
307
2
    assert_eq!(
308
2
        trusted.client_ip(peer("10.244.2.231"), Some("203.0.113.7")),
309
2
        ip("203.0.113.7"),
310
        "same-node ingress must speak for its client too, or that share of users \
311
         collapses into one shared budget"
312
    );
313
2
}