Skip to main content

pharos_plugins/
loadbalance.rs

1//! Loadbalance Directive configuration.
2
3mod 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                    // shortcut: relative paths use the working directory; join with `root` once that Plugin exists.
38                    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    // RFC 1035 §3.4.1: A RDATA is a 32-bit Internet address.
158    // RFC 3596 §2.2: AAAA RDATA is a 128-bit address in network byte order.
159    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            // A referenced name can cross record boundaries; its encoding must stay contiguous.
252            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                // RFC 1035 §4.1.4: compression pointers use a 14-bit offset to a prior occurrence.
265                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        // Rewriting must not alter opaque data, literal labels, or overlapping pointers.
295        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        // RFC 8945 §4.3.2: a TSIG MAC covers the DNS message in wire format.
324        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        // Every RR occupies at least 11 bytes; no valid DNS Message can exceed this bound.
351        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            // The original offset preserves order within each category without allocation.
400            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        // RFC 1035 §4.1.4: a compression pointer has two leading one bits and a 14-bit offset.
510        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}