1mod weighted;
4
5use std::{net::IpAddr, path::PathBuf, time::Duration};
6
7use pharos_corefile::{Cursor, Error};
8use pharos_proto::{Message, NameRef, NameWirePart, Rcode, Type};
9use pharos_server::{Context, Next, Outcome, Plugin};
10
11#[derive(Debug)]
12pub struct LoadBalance {
13 pub weight_file: Option<PathBuf>,
14 pub reload: Duration,
15 weights: Option<weighted::Weights>,
16 weighted_policy: bool,
17 prefer: Vec<Subnet>,
18}
19
20impl LoadBalance {
21 pub(crate) fn parse(cursor: &mut Cursor<'_>) -> Result<Self, Error> {
22 let mut lb = Self {
23 weight_file: None,
24 reload: Duration::from_secs(30),
25 weights: None,
26 weighted_policy: false,
27 prefer: Vec::new(),
28 };
29 while cursor.next() {
30 let args = cursor.remaining_args();
31 match args.first().map(String::as_str) {
32 None | Some("round_robin") if args.len() <= 1 => lb.weighted_policy = false,
33 Some("weighted") if args.len() == 2 => {
34 lb.weighted_policy = true;
35 lb.prefer.clear();
36 lb.reload = Duration::from_secs(30);
37 lb.weight_file = Some(PathBuf::from(&args[1]));
39 while cursor.next_block() {
40 if cursor.val() != "reload" {
41 return Err(cursor.error("unknown weighted property"));
42 }
43 let args = cursor.remaining_args();
44 if args.len() != 1 {
45 return Err(cursor.arg_err());
46 }
47 lb.reload = crate::health::parse_duration(&args[0])
48 .ok_or_else(|| cursor.error("invalid reload duration"))?;
49 }
50 }
51 _ => return Err(cursor.arg_err()),
52 }
53 while cursor.next_block() {
54 if cursor.val() != "prefer" {
55 return Err(cursor.error("unknown loadbalance property"));
56 }
57 for cidr in cursor.remaining_args() {
58 lb.prefer
59 .push(Subnet::parse(&cidr).ok_or_else(|| cursor.error("invalid CIDR"))?);
60 }
61 }
62 }
63 lb.weights = lb
64 .weight_file
65 .as_ref()
66 .map(|path| weighted::Weights::new(path.clone(), lb.reload));
67 Ok(lb)
68 }
69
70 pub(crate) fn register_hooks(&self, lifecycle: &mut pharos_server::Lifecycle) {
71 if let Some(weights) = &self.weights {
72 weights.register_hooks(lifecycle);
73 }
74 }
75}
76
77impl Plugin for LoadBalance {
78 async fn serve<N: Next + Send>(&self, cx: &mut Context<'_>, next: N) -> Outcome {
79 let outcome = next.run(cx).await;
80 if outcome == Outcome::Response {
81 shuffle(
82 cx.response,
83 self.weights.as_ref().filter(|_| self.weighted_policy),
84 &self.prefer,
85 );
86 }
87 outcome
88 }
89}
90
91#[derive(Clone, Copy, Default)]
92struct Record {
93 start: u16,
94 end: u16,
95 destination: u16,
96 sequence: u16,
97 group: u8,
98 weight: u8,
99 priority: usize,
100}
101
102const EMPTY_RECORD: Record = Record {
103 start: 0,
104 end: 0,
105 destination: 0,
106 sequence: 0,
107 group: 0,
108 weight: 0,
109 priority: 0,
110};
111
112struct Scratch {
113 records: [Record; 5957],
114 order: [Record; 5957],
115 original: [u8; 65535],
116 pointers: [Pointer; 32768],
117 seen: [u8; 8192],
118 direct: [u8; 8192],
119 labels: [u8; 8192],
120}
121
122#[derive(Clone, Copy, Default)]
123struct Pointer {
124 source: u16,
125 destination: u16,
126 target: u16,
127 original_target: u16,
128}
129
130#[derive(Debug)]
131struct Subnet {
132 address: IpAddr,
133 prefix: u32,
134}
135
136impl Subnet {
137 fn parse(value: &str) -> Option<Self> {
138 let (address, prefix) = value.split_once('/')?;
139 let mut address: IpAddr = address.parse().ok()?;
140 if prefix.is_empty() || !prefix.bytes().all(|byte| byte.is_ascii_digit()) {
141 return None;
142 }
143 let mut prefix: u32 = prefix.parse().ok()?;
144 if prefix > if address.is_ipv4() { 32 } else { 128 } {
145 return None;
146 }
147 if let IpAddr::V6(ip) = address
148 && prefix >= 96
149 && let Some(ip) = ip.to_ipv4_mapped()
150 {
151 address = IpAddr::V4(ip);
152 prefix -= 96;
153 }
154 Some(Self { address, prefix })
155 }
156
157 fn contains(&self, rr: &pharos_proto::Record<'_>) -> bool {
160 let address = match (rr.rtype, rr.rdata) {
161 (Type::A, [a, b, c, d]) => IpAddr::V4(std::net::Ipv4Addr::new(*a, *b, *c, *d)),
162 (Type::AAAA, bytes) if bytes.len() == 16 => IpAddr::V6(std::net::Ipv6Addr::from(
163 <[u8; 16]>::try_from(bytes).unwrap(),
164 ))
165 .to_canonical(),
166 _ => return false,
167 };
168 match (self.address, address) {
169 (IpAddr::V4(network), IpAddr::V4(ip)) => {
170 let mask = u32::MAX.checked_shl(32 - self.prefix).unwrap_or(0);
171 u32::from(network) & mask == u32::from(ip) & mask
172 }
173 (IpAddr::V6(network), IpAddr::V6(ip)) => {
174 let mask = u128::MAX.checked_shl(128 - self.prefix).unwrap_or(0);
175 u128::from(network) & mask == u128::from(ip) & mask
176 }
177 _ => false,
178 }
179 }
180}
181
182thread_local! {
183 static SCRATCH: std::cell::RefCell<Scratch> = const { std::cell::RefCell::new(Scratch {
184 records: [EMPTY_RECORD; 5957],
185 order: [EMPTY_RECORD; 5957],
186 original: [0; 65535],
187 pointers: [Pointer { source: 0, destination: 0, target: 0, original_target: 0 }; 32768],
188 seen: [0; 8192],
189 direct: [0; 8192],
190 labels: [0; 8192],
191 }) };
192 static RANDOM: std::cell::Cell<u64> = {
193 let mut seed = [0; 8];
194 let _ = getrandom::getrandom(&mut seed);
195 std::cell::Cell::new(u64::from_ne_bytes(seed).max(1))
196 };
197}
198
199fn random(limit: usize) -> usize {
200 RANDOM.with(|random| {
201 let mut value = random.get();
202 value ^= value << 13;
203 value ^= value >> 7;
204 value ^= value << 17;
205 random.set(value);
206 (value % limit as u64) as usize
207 })
208}
209
210fn relocated(records: &[Record], offset: usize) -> usize {
211 let index = records.partition_point(|record| usize::from(record.start) <= offset);
212 if index == 0 {
213 return offset;
214 }
215 let record = records[index - 1];
216 if offset < usize::from(record.end) {
217 usize::from(record.destination) + offset - usize::from(record.start)
218 } else {
219 offset
220 }
221}
222
223fn bit(bits: &[u8], offset: usize) -> bool {
224 bits[offset / 8] & (1 << (offset % 8)) != 0
225}
226
227fn mark(bits: &mut [u8], offset: usize) {
228 bits[offset / 8] |= 1 << (offset % 8);
229}
230
231struct PointerCollector<'a> {
232 records: &'a [Record],
233 pointers: &'a mut [Pointer],
234 seen: &'a mut [u8],
235 direct: &'a mut [u8],
236 labels: &'a mut [u8],
237 count: usize,
238}
239
240impl PointerCollector<'_> {
241 fn visit(&mut self, name: NameRef<'_>) -> bool {
242 let mut previous = None;
243 let mut pointer_limit = usize::MAX;
244 for part in name.wire_parts() {
245 let (offset, len) = match part {
246 NameWirePart::Label { offset, len } => (offset, len),
247 NameWirePart::Root { offset } => (offset, 1),
248 NameWirePart::Pointer { offset, .. } => (offset, 2),
249 };
250 let destination = relocated(self.records, offset);
251 if destination + len > pointer_limit
253 || previous
254 .is_some_and(|(old_end, new_end)| old_end == offset && new_end != destination)
255 || (0..len).any(|i| relocated(self.records, offset + i) != destination + i)
256 {
257 return false;
258 }
259 if let NameWirePart::Pointer { target, .. } = part {
260 if offset >= name.offset() && offset < name.offset() + name.wire_len() {
261 mark(self.direct, offset);
262 }
263 let target_after = relocated(self.records, target);
264 if target_after > 0x3fff || target_after >= destination {
266 return false;
267 }
268 if !bit(self.seen, offset) {
269 if self.count == self.pointers.len() {
270 return false;
271 }
272 mark(self.seen, offset);
273 self.pointers[self.count] = Pointer {
274 source: offset as u16,
275 destination: destination as u16,
276 target: target_after as u16,
277 original_target: target as u16,
278 };
279 self.count += 1;
280 }
281 pointer_limit = destination;
282 previous = None;
283 } else {
284 for byte in offset..offset + len {
285 mark(self.labels, byte);
286 }
287 previous = Some((offset + len, destination + len));
288 }
289 }
290 true
291 }
292
293 fn safe(&mut self) -> bool {
294 if !self.pointers[..self.count].iter().all(|pointer| {
296 let source = usize::from(pointer.source);
297 pointer.target == pointer.original_target
298 || (bit(self.direct, source)
299 && !bit(self.labels, source)
300 && !bit(self.labels, source + 1))
301 }) {
302 return false;
303 }
304 let pointers = &mut self.pointers[..self.count];
305 pointers.sort_unstable_by_key(|pointer| pointer.source);
306 pointers.windows(2).all(|pair| {
307 usize::from(pair[0].source) + 1 != usize::from(pair[1].source)
308 || pair[0].target.to_be_bytes()[1]
309 == 0xc0 | (pair[1].target.to_be_bytes()[0] & 0x3f)
310 })
311 }
312}
313
314fn shuffle(wire: &mut [u8], weights: Option<&weighted::Weights>, prefer: &[Subnet]) {
315 if wire.len() > usize::from(u16::MAX) {
316 return;
317 }
318 let weight_data = weights.map(weighted::Weights::load);
319 SCRATCH.with(|scratch| {
320 let Ok(message) = Message::parse(wire) else {
321 return;
322 };
323 if message.rcode() != Rcode::NOERROR
325 || message
326 .questions()
327 .next()
328 .is_none_or(|q| matches!(q.qtype, Type::AXFR | Type::IXFR))
329 || message.additionals().any(|rr| rr.rtype == Type::TSIG)
330 {
331 return;
332 }
333 let weighted_query = message
334 .questions()
335 .next()
336 .is_some_and(|q| matches!(q.qtype, Type::A | Type::AAAA | Type::SRV));
337 if weights.is_some() && !weighted_query && prefer.is_empty() {
338 return;
339 }
340 let mut scratch = scratch.borrow_mut();
341 let Scratch {
342 records,
343 order,
344 original,
345 pointers,
346 seen,
347 direct,
348 labels,
349 } = &mut *scratch;
350 let mut sections = [(0usize, 0usize); 3];
352 let mut count = 0;
353 for (section, iter) in [
354 message.answers(),
355 message.authorities(),
356 message.additionals(),
357 ]
358 .into_iter()
359 .enumerate()
360 {
361 let first = count;
362 for rr in iter {
363 if count == records.len() {
364 return;
365 }
366 records[count] = Record {
367 start: (rr.ttl_offset - 4 - rr.name.wire_len()) as u16,
368 end: (rr.rdata_offset + rr.rdata.len()) as u16,
369 destination: (rr.ttl_offset - 4 - rr.name.wire_len()) as u16,
370 sequence: 0,
371 group: match rr.rtype {
372 Type::CNAME => 0,
373 Type::A | Type::AAAA => 2,
374 Type::MX => 3,
375 _ => 1,
376 },
377 weight: weight_data
378 .as_ref()
379 .map_or(1, |data| weighted::weight(data, &rr)),
380 priority: prefer
381 .iter()
382 .position(|subnet| subnet.contains(&rr))
383 .unwrap_or(prefer.len()),
384 };
385 count += 1;
386 }
387 sections[section] = (first, count);
388 }
389 order[..count].copy_from_slice(&records[..count]);
390 for (section, (first, end)) in sections.into_iter().enumerate() {
391 if end <= first + 1 || (weights.is_some() && section == 1) {
392 continue;
393 }
394 let section_start = usize::from(records[first].start);
395 let items = &mut order[first..end];
396 if weights.is_some() && prefer.is_empty() && !items.iter().any(|item| item.group == 2) {
397 continue;
398 }
399 items.sort_unstable_by_key(|item| (item.group, item.start));
401 for group in [2, 3] {
402 let first = items
403 .iter()
404 .position(|item| item.group == group)
405 .unwrap_or(items.len());
406 let end = items
407 .iter()
408 .rposition(|item| item.group == group)
409 .map_or(first, |i| i + 1);
410 if weights.is_some() {
411 if weighted_query && group == 2 && first < end {
412 let total: usize = items[first..end]
413 .iter()
414 .map(|item| usize::from(item.weight))
415 .sum();
416 let mut choice = random(total);
417 for i in first..end {
418 if choice < usize::from(items[i].weight) {
419 items.swap(first, i);
420 break;
421 }
422 choice -= usize::from(items[i].weight);
423 }
424 }
425 } else {
426 for i in first..end.saturating_sub(1) {
427 items.swap(i, i + random(end - i));
428 }
429 }
430 }
431 if !prefer.is_empty() && section != 1 {
432 for (sequence, item) in items.iter_mut().enumerate() {
433 item.sequence = sequence as u16;
434 }
435 let key = |record: &Record| {
436 let group = match record.group {
437 0 => 0,
438 2 => 1,
439 3 => 2,
440 _ => 3,
441 };
442 (
443 group,
444 if group == 1 { record.priority } else { 0 },
445 record.sequence,
446 )
447 };
448 items.sort_unstable_by_key(key);
449 }
450 let mut dest = section_start;
451 for item in items.iter() {
452 let len = usize::from(item.end - item.start);
453 let index = records[..count]
454 .binary_search_by_key(&item.start, |record| record.start)
455 .expect("record came from this Message");
456 records[index].destination = dest as u16;
457 dest += len;
458 }
459 }
460 seen.fill(0);
461 direct.fill(0);
462 labels.fill(0);
463 let pointer_count = {
464 let mut collector = PointerCollector {
465 records: &records[..count],
466 pointers,
467 seen,
468 direct,
469 labels,
470 count: 0,
471 };
472 for question in message.questions() {
473 if !collector.visit(question.name) {
474 return;
475 }
476 }
477 for rr in message
478 .answers()
479 .chain(message.authorities())
480 .chain(message.additionals())
481 {
482 if !collector.visit(rr.name) {
483 return;
484 }
485 let Ok(names) = rr.compressible_names() else {
486 return;
487 };
488 for name in names.into_iter().flatten() {
489 if !collector.visit(name) {
490 return;
491 }
492 }
493 }
494 if !collector.safe() {
495 return;
496 }
497 collector.count
498 };
499 original[..wire.len()].copy_from_slice(wire);
500 for item in &order[..count] {
501 let index = records[..count]
502 .binary_search_by_key(&item.start, |record| record.start)
503 .expect("record came from this Message");
504 let destination = usize::from(records[index].destination);
505 let len = usize::from(item.end - item.start);
506 wire[destination..destination + len]
507 .copy_from_slice(&original[usize::from(item.start)..usize::from(item.end)]);
508 }
509 for pointer in &pointers[..pointer_count] {
511 let destination = usize::from(pointer.destination);
512 wire[destination..destination + 2]
513 .copy_from_slice(&(0xc000 | pointer.target).to_be_bytes());
514 }
515 });
516}