// EDNS(0) OPT pseudo-record support (RFC 6891).
///|
pub struct OptRR {
udp_payload_size : UInt16
ext_rcode : UInt16
edns_version : UInt16
flags : UInt16
options : Array[OptOption]
}
///|
pub struct OptOption {
opt_code : UInt16
opt_data : Array[Byte]
}
///|
pub let edns_flag_do : UInt16 = 0x8000
///|
pub let edns_default_udp_size : UInt16 = 1232
///|
pub fn OptRR::default() -> OptRR {
{
udp_payload_size: edns_default_udp_size,
ext_rcode: 0,
edns_version: 0,
flags: 0,
options: Array::new(capacity=0),
}
}
///|
pub fn OptRR::with_dnssec(payload_size : UInt16) -> OptRR {
{
udp_payload_size: payload_size,
ext_rcode: 0,
edns_version: 0,
flags: edns_flag_do,
options: Array::new(capacity=0),
}
}
///|
fn OptRR::validate_fields(self : OptRR) -> Result[Array[Byte], String] {
if self.udp_payload_size < max_udp_payload {
return Err("EDNS UDP payload size must be at least 512")
}
if self.ext_rcode > 255 || self.edns_version > 255 {
return Err("EDNS extended RCODE and version must fit in 8 bits")
}
encode_opt_options(self.options)
}
///|
fn encode_opt_options(
options : Array[OptOption],
) -> Result[Array[Byte], String] {
if options.length() > max_edns_options {
return Err("too many EDNS options")
}
let out : Array[Byte] = Array::new(capacity=32)
for option in options {
match validate_edns_option(option) {
Ok(_) => ()
Err(error) => return Err(error)
}
if option.opt_data.length() > 65535 {
return Err("EDNS option exceeds 65535 octets")
}
append_u16(out, option.opt_code.to_int())
append_u16(out, option.opt_data.length())
append_bytes(out, option.opt_data)
}
if out.length() > 65535 {
Err("OPT RDATA exceeds 65535 octets")
} else {
Ok(out)
}
}
///|
fn decode_opt_options(
bytes : Array[Byte],
offset : Int,
end : Int,
) -> Result[Array[OptOption], String] {
if offset < 0 || end < offset || end > bytes.length() {
return Err("invalid OPT RDATA range")
}
let options : Array[OptOption] = Array::new(capacity=4)
let pos = Ref(offset)
while pos.val < end {
if end - pos.val < 4 {
return Err("truncated EDNS option header")
}
if options.length() >= max_edns_options {
return Err("too many EDNS options")
}
let code = ((bytes[pos.val].to_int() << 8) | bytes[pos.val + 1].to_int()).to_uint16()
let length = (bytes[pos.val + 2].to_int() << 8) |
bytes[pos.val + 3].to_int()
pos.val = pos.val + 4
if length > end - pos.val {
return Err("truncated EDNS option data")
}
let data = match wire_copy_range(bytes, pos.val, length) {
Ok(value) => value
Err(err) => return Err(err)
}
let option = { opt_code: code, opt_data: data }
match validate_edns_option(option) {
Ok(_) => ()
Err(error) => return Err(error)
}
options.push(option)
pos.val = pos.val + length
}
Ok(options)
}
///|
pub fn OptRR::encode_checked(self : OptRR) -> Result[Array[Byte], String] {
let option_bytes = match self.validate_fields() {
Ok(bytes) => bytes
Err(err) => return Err(err)
}
let out : Array[Byte] = Array::new(capacity=11 + option_bytes.length())
out.push(0) // owner name is the root domain
append_u16(out, qtype_opt.to_int())
append_u16(out, self.udp_payload_size.to_int())
let ttl = (self.ext_rcode.to_int() << 24) |
(self.edns_version.to_int() << 16) |
self.flags.to_int()
append_u32(out, ttl.reinterpret_as_uint())
append_u16(out, option_bytes.length())
append_bytes(out, option_bytes)
Ok(out)
}
///|
pub fn OptRR::encode(self : OptRR) -> Array[Byte] {
match self.encode_checked() {
Ok(bytes) => bytes
Err(error) => abort(error)
}
}
///|
pub fn OptRR::to_rr_checked(self : OptRR) -> Result[RR, String] {
let option_bytes = match self.validate_fields() {
Ok(bytes) => bytes
Err(error) => return Err(error)
}
let rdata = RData::OPT(self.options)
let rdlength = option_bytes.length().to_uint16()
let ttl = ((self.ext_rcode.to_int() << 24) |
(self.edns_version.to_int() << 16) |
self.flags.to_int()).reinterpret_as_uint()
Ok({
name: "",
rtype: qtype_opt,
rclass: self.udp_payload_size,
ttl,
rdlength,
rdata,
})
}
///|
/// Compatibility wrapper for the original API. Invalid EDNS fields abort
/// explicitly instead of constructing an RR that bypasses checked validation.
pub fn OptRR::to_rr(self : OptRR) -> RR {
match self.to_rr_checked() {
Ok(record) => record
Err(error) => abort(error)
}
}
///|
pub fn opt_from_rr(rr : RR) -> Result[OptRR, String] {
if rr.rtype != qtype_opt {
return Err("resource record is not an OPT record")
}
if rr.name != "" {
return Err("OPT owner name must be the root domain")
}
// RFC 6891 section 6.2.3: receivers treat a peer's advertised size below
// 512 as 512. Outbound configuration remains strict in `validate_fields`.
let udp_payload_size = if rr.rclass < max_udp_payload {
max_udp_payload
} else {
rr.rclass
}
let ext_rcode = ((rr.ttl >> 24) & 0xFF).to_uint16()
let edns_version = ((rr.ttl >> 16) & 0xFF).to_uint16()
let flags = (rr.ttl & 0xFFFF).to_uint16()
match rr.rdata {
OPT(options) => {
let encoded = match encode_opt_options(options) {
Ok(bytes) => bytes
Err(error) => return Err(error)
}
if rr.rdlength.to_int() != encoded.length() {
return Err("OPT RDLENGTH does not match encoded option data")
}
Ok({ udp_payload_size, ext_rcode, edns_version, flags, options })
}
_ => Err("OPT record does not contain OPT RDATA")
}
}
// This helper decodes only an OPT RDATA field. The RR class/TTL fields are
// intentionally supplied as defaults because a caller that needs them should
// decode the surrounding RR and call opt_from_rr.
///|
pub fn decode_opt_rr(
bytes : Array[Byte],
offset : Int,
rdlength : UInt16,
_msg_start : Int,
) -> Result[(OptRR, Int), String] {
let end = offset + rdlength.to_int()
let options = match decode_opt_options(bytes, offset, end) {
Ok(value) => value
Err(err) => return Err(err)
}
Ok(
(
{
udp_payload_size: max_udp_payload,
ext_rcode: 0,
edns_version: 0,
flags: 0,
options,
},
end,
),
)
}
///|
pub fn build_query_with_edns_checked(
id : UInt16,
name : String,
qtype : UInt16,
recurse : Bool,
opt : OptRR,
) -> Result[Message, String] {
let query = build_query(id, name, qtype, recurse)
match opt.to_rr_checked() {
Ok(record) => Ok({ ..query, additionals: [record] })
Err(error) => Err(error)
}
}
///|
/// Compatibility wrapper for the original API. Resolver internals use the
/// checked variant so malformed input is returned as a typed error.
pub fn build_query_with_edns(
id : UInt16,
name : String,
qtype : UInt16,
recurse : Bool,
opt : OptRR,
) -> Message {
match build_query_with_edns_checked(id, name, qtype, recurse, opt) {
Ok(query) => query
Err(error) => abort(error)
}
}