fix(ext/node): handle "ttl" option in "dns" module (#27676)

Closes #27669

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
siaeyy 2025-04-30 01:49:10 +03:00 committed by GitHub
parent 6d0035411b
commit 1a171f10df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 130 additions and 67 deletions

View file

@ -35,6 +35,7 @@ import {
const UDP_DGRAM_MAXSIZE = 65507; const UDP_DGRAM_MAXSIZE = 65507;
const { const {
ArrayPrototypeMap,
Error, Error,
Number, Number,
NumberIsNaN, NumberIsNaN,
@ -78,12 +79,13 @@ async function resolveDns(query, recordType, options) {
} }
try { try {
return await op_dns_resolve({ const res = await op_dns_resolve({
cancelRid, cancelRid,
query, query,
recordType, recordType,
options, options,
}); });
return ArrayPrototypeMap(res, (recordWithTtl) => recordWithTtl.data);
} finally { } finally {
if (options?.signal) { if (options?.signal) {
options.signal[abortSignal.remove](abortHandler); options.signal[abortSignal.remove](abortHandler);

View file

@ -788,7 +788,7 @@ pub fn op_net_accept_vsock() -> Result<(), NetError> {
#[derive(Serialize, Eq, PartialEq, Debug)] #[derive(Serialize, Eq, PartialEq, Debug)]
#[serde(untagged)] #[serde(untagged)]
pub enum DnsReturnRecord { pub enum DnsRecordData {
A(String), A(String),
Aaaa(String), Aaaa(String),
Aname(String), Aname(String),
@ -830,6 +830,13 @@ pub enum DnsReturnRecord {
Txt(Vec<String>), Txt(Vec<String>),
} }
#[derive(Serialize, Eq, PartialEq, Debug)]
#[serde()]
pub struct DnsRecordWithTtl {
pub data: DnsRecordData,
pub ttl: u32,
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ResolveAddrArgs { pub struct ResolveAddrArgs {
@ -862,7 +869,7 @@ pub struct NameServer {
pub async fn op_dns_resolve<NP>( pub async fn op_dns_resolve<NP>(
state: Rc<RefCell<OpState>>, state: Rc<RefCell<OpState>>,
#[serde] args: ResolveAddrArgs, #[serde] args: ResolveAddrArgs,
) -> Result<Vec<DnsReturnRecord>, NetError> ) -> Result<Vec<DnsRecordWithTtl>, NetError>
where where
NP: NetPermissions + 'static, NP: NetPermissions + 'static,
{ {
@ -947,9 +954,18 @@ where
} }
_ => NetError::Dns(e), _ => NetError::Dns(e),
})? })?
.records()
.iter() .iter()
.filter_map(|rdata| rdata_to_return_record(record_type)(rdata).transpose()) .filter_map(|rec| {
.collect::<Result<Vec<DnsReturnRecord>, NetError>>() let r = format_rdata(record_type)(rec.data()).transpose();
r.map(|maybe_data| {
maybe_data.map(|data| DnsRecordWithTtl {
data,
ttl: rec.ttl(),
})
})
})
.collect::<Result<Vec<DnsRecordWithTtl>, NetError>>()
} }
#[op2(fast)] #[op2(fast)]
@ -992,22 +1008,22 @@ pub fn op_set_keepalive_inner(
resource.set_keepalive(keepalive).map_err(NetError::Map) resource.set_keepalive(keepalive).map_err(NetError::Map)
} }
fn rdata_to_return_record( fn format_rdata(
ty: RecordType, ty: RecordType,
) -> impl Fn(&RData) -> Result<Option<DnsReturnRecord>, NetError> { ) -> impl Fn(&RData) -> Result<Option<DnsRecordData>, NetError> {
use RecordType::*; use RecordType::*;
move |r: &RData| -> Result<Option<DnsReturnRecord>, NetError> { move |r: &RData| -> Result<Option<DnsRecordData>, NetError> {
let record = match ty { let record = match ty {
A => r.as_a().map(ToString::to_string).map(DnsReturnRecord::A), A => r.as_a().map(ToString::to_string).map(DnsRecordData::A),
AAAA => r AAAA => r
.as_aaaa() .as_aaaa()
.map(ToString::to_string) .map(ToString::to_string)
.map(DnsReturnRecord::Aaaa), .map(DnsRecordData::Aaaa),
ANAME => r ANAME => r
.as_aname() .as_aname()
.map(ToString::to_string) .map(ToString::to_string)
.map(DnsReturnRecord::Aname), .map(DnsRecordData::Aname),
CAA => r.as_caa().map(|caa| DnsReturnRecord::Caa { CAA => r.as_caa().map(|caa| DnsRecordData::Caa {
critical: caa.issuer_critical(), critical: caa.issuer_critical(),
tag: caa.tag().to_string(), tag: caa.tag().to_string(),
value: match caa.value() { value: match caa.value() {
@ -1034,12 +1050,12 @@ fn rdata_to_return_record(
CNAME => r CNAME => r
.as_cname() .as_cname()
.map(ToString::to_string) .map(ToString::to_string)
.map(DnsReturnRecord::Cname), .map(DnsRecordData::Cname),
MX => r.as_mx().map(|mx| DnsReturnRecord::Mx { MX => r.as_mx().map(|mx| DnsRecordData::Mx {
preference: mx.preference(), preference: mx.preference(),
exchange: mx.exchange().to_string(), exchange: mx.exchange().to_string(),
}), }),
NAPTR => r.as_naptr().map(|naptr| DnsReturnRecord::Naptr { NAPTR => r.as_naptr().map(|naptr| DnsRecordData::Naptr {
order: naptr.order(), order: naptr.order(),
preference: naptr.preference(), preference: naptr.preference(),
flags: String::from_utf8(naptr.flags().to_vec()).unwrap(), flags: String::from_utf8(naptr.flags().to_vec()).unwrap(),
@ -1047,12 +1063,9 @@ fn rdata_to_return_record(
regexp: String::from_utf8(naptr.regexp().to_vec()).unwrap(), regexp: String::from_utf8(naptr.regexp().to_vec()).unwrap(),
replacement: naptr.replacement().to_string(), replacement: naptr.replacement().to_string(),
}), }),
NS => r.as_ns().map(ToString::to_string).map(DnsReturnRecord::Ns), NS => r.as_ns().map(ToString::to_string).map(DnsRecordData::Ns),
PTR => r PTR => r.as_ptr().map(ToString::to_string).map(DnsRecordData::Ptr),
.as_ptr() SOA => r.as_soa().map(|soa| DnsRecordData::Soa {
.map(ToString::to_string)
.map(DnsReturnRecord::Ptr),
SOA => r.as_soa().map(|soa| DnsReturnRecord::Soa {
mname: soa.mname().to_string(), mname: soa.mname().to_string(),
rname: soa.rname().to_string(), rname: soa.rname().to_string(),
serial: soa.serial(), serial: soa.serial(),
@ -1061,7 +1074,7 @@ fn rdata_to_return_record(
expire: soa.expire(), expire: soa.expire(),
minimum: soa.minimum(), minimum: soa.minimum(),
}), }),
SRV => r.as_srv().map(|srv| DnsReturnRecord::Srv { SRV => r.as_srv().map(|srv| DnsRecordData::Srv {
priority: srv.priority(), priority: srv.priority(),
weight: srv.weight(), weight: srv.weight(),
port: srv.port(), port: srv.port(),
@ -1075,7 +1088,7 @@ fn rdata_to_return_record(
bytes.iter().map(|&b| b as char).collect::<String>() bytes.iter().map(|&b| b as char).collect::<String>()
}) })
.collect(); .collect();
DnsReturnRecord::Txt(texts) DnsRecordData::Txt(texts)
}), }),
_ => return Err(NetError::UnsupportedRecordType), _ => return Err(NetError::UnsupportedRecordType),
}; };
@ -1118,37 +1131,37 @@ mod tests {
#[test] #[test]
fn rdata_to_return_record_a() { fn rdata_to_return_record_a() {
let func = rdata_to_return_record(RecordType::A); let func = format_rdata(RecordType::A);
let rdata = RData::A(A(Ipv4Addr::new(127, 0, 0, 1))); let rdata = RData::A(A(Ipv4Addr::new(127, 0, 0, 1)));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::A("127.0.0.1".to_string())) Some(DnsRecordData::A("127.0.0.1".to_string()))
); );
} }
#[test] #[test]
fn rdata_to_return_record_aaaa() { fn rdata_to_return_record_aaaa() {
let func = rdata_to_return_record(RecordType::AAAA); let func = format_rdata(RecordType::AAAA);
let rdata = RData::AAAA(AAAA(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))); let rdata = RData::AAAA(AAAA(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Aaaa("::1".to_string())) Some(DnsRecordData::Aaaa("::1".to_string()))
); );
} }
#[test] #[test]
fn rdata_to_return_record_aname() { fn rdata_to_return_record_aname() {
let func = rdata_to_return_record(RecordType::ANAME); let func = format_rdata(RecordType::ANAME);
let rdata = RData::ANAME(ANAME(Name::new())); let rdata = RData::ANAME(ANAME(Name::new()));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Aname("".to_string())) Some(DnsRecordData::Aname("".to_string()))
); );
} }
#[test] #[test]
fn rdata_to_return_record_caa() { fn rdata_to_return_record_caa() {
let func = rdata_to_return_record(RecordType::CAA); let func = format_rdata(RecordType::CAA);
let rdata = RData::CAA(CAA::new_issue( let rdata = RData::CAA(CAA::new_issue(
false, false,
Some(Name::parse("example.com", None).unwrap()), Some(Name::parse("example.com", None).unwrap()),
@ -1156,7 +1169,7 @@ mod tests {
)); ));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Caa { Some(DnsRecordData::Caa {
critical: false, critical: false,
tag: "issue".to_string(), tag: "issue".to_string(),
value: "example.com; account=123456".to_string(), value: "example.com; account=123456".to_string(),
@ -1166,21 +1179,21 @@ mod tests {
#[test] #[test]
fn rdata_to_return_record_cname() { fn rdata_to_return_record_cname() {
let func = rdata_to_return_record(RecordType::CNAME); let func = format_rdata(RecordType::CNAME);
let rdata = RData::CNAME(CNAME(Name::new())); let rdata = RData::CNAME(CNAME(Name::new()));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Cname("".to_string())) Some(DnsRecordData::Cname("".to_string()))
); );
} }
#[test] #[test]
fn rdata_to_return_record_mx() { fn rdata_to_return_record_mx() {
let func = rdata_to_return_record(RecordType::MX); let func = format_rdata(RecordType::MX);
let rdata = RData::MX(MX::new(10, Name::new())); let rdata = RData::MX(MX::new(10, Name::new()));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Mx { Some(DnsRecordData::Mx {
preference: 10, preference: 10,
exchange: "".to_string() exchange: "".to_string()
}) })
@ -1189,7 +1202,7 @@ mod tests {
#[test] #[test]
fn rdata_to_return_record_naptr() { fn rdata_to_return_record_naptr() {
let func = rdata_to_return_record(RecordType::NAPTR); let func = format_rdata(RecordType::NAPTR);
let rdata = RData::NAPTR(NAPTR::new( let rdata = RData::NAPTR(NAPTR::new(
1, 1,
2, 2,
@ -1200,7 +1213,7 @@ mod tests {
)); ));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Naptr { Some(DnsRecordData::Naptr {
order: 1, order: 1,
preference: 2, preference: 2,
flags: "".to_string(), flags: "".to_string(),
@ -1213,27 +1226,27 @@ mod tests {
#[test] #[test]
fn rdata_to_return_record_ns() { fn rdata_to_return_record_ns() {
let func = rdata_to_return_record(RecordType::NS); let func = format_rdata(RecordType::NS);
let rdata = RData::NS(NS(Name::new())); let rdata = RData::NS(NS(Name::new()));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Ns("".to_string())) Some(DnsRecordData::Ns("".to_string()))
); );
} }
#[test] #[test]
fn rdata_to_return_record_ptr() { fn rdata_to_return_record_ptr() {
let func = rdata_to_return_record(RecordType::PTR); let func = format_rdata(RecordType::PTR);
let rdata = RData::PTR(PTR(Name::new())); let rdata = RData::PTR(PTR(Name::new()));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Ptr("".to_string())) Some(DnsRecordData::Ptr("".to_string()))
); );
} }
#[test] #[test]
fn rdata_to_return_record_soa() { fn rdata_to_return_record_soa() {
let func = rdata_to_return_record(RecordType::SOA); let func = format_rdata(RecordType::SOA);
let rdata = RData::SOA(SOA::new( let rdata = RData::SOA(SOA::new(
Name::new(), Name::new(),
Name::new(), Name::new(),
@ -1245,7 +1258,7 @@ mod tests {
)); ));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Soa { Some(DnsRecordData::Soa {
mname: "".to_string(), mname: "".to_string(),
rname: "".to_string(), rname: "".to_string(),
serial: 0, serial: 0,
@ -1259,11 +1272,11 @@ mod tests {
#[test] #[test]
fn rdata_to_return_record_srv() { fn rdata_to_return_record_srv() {
let func = rdata_to_return_record(RecordType::SRV); let func = format_rdata(RecordType::SRV);
let rdata = RData::SRV(SRV::new(1, 2, 3, Name::new())); let rdata = RData::SRV(SRV::new(1, 2, 3, Name::new()));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Srv { Some(DnsRecordData::Srv {
priority: 1, priority: 1,
weight: 2, weight: 2,
port: 3, port: 3,
@ -1274,7 +1287,7 @@ mod tests {
#[test] #[test]
fn rdata_to_return_record_txt() { fn rdata_to_return_record_txt() {
let func = rdata_to_return_record(RecordType::TXT); let func = format_rdata(RecordType::TXT);
let rdata = RData::TXT(TXT::from_bytes(vec![ let rdata = RData::TXT(TXT::from_bytes(vec![
"foo".as_bytes(), "foo".as_bytes(),
"bar".as_bytes(), "bar".as_bytes(),
@ -1283,7 +1296,7 @@ mod tests {
])); ]));
assert_eq!( assert_eq!(
func(&rdata).unwrap(), func(&rdata).unwrap(),
Some(DnsReturnRecord::Txt(vec![ Some(DnsRecordData::Txt(vec![
"foo".to_string(), "foo".to_string(),
"bar".to_string(), "bar".to_string(),
"£".to_string(), "£".to_string(),

View file

@ -93,7 +93,6 @@ import {
QueryReqWrap, QueryReqWrap,
} from "ext:deno_node/internal_binding/cares_wrap.ts"; } from "ext:deno_node/internal_binding/cares_wrap.ts";
import { domainToASCII } from "ext:deno_node/internal/idna.ts"; import { domainToASCII } from "ext:deno_node/internal/idna.ts";
import { notImplemented } from "ext:deno_node/_utils.ts";
function onlookup( function onlookup(
this: GetAddrInfoReqWrap, this: GetAddrInfoReqWrap,
@ -345,11 +344,6 @@ function resolver(bindingName: keyof ChannelWrapQuery) {
req.callback = callback as ResolveCallback; req.callback = callback as ResolveCallback;
req.hostname = name; req.hostname = name;
req.oncomplete = onresolve; req.oncomplete = onresolve;
if (options && (options as ResolveOptions).ttl) {
notImplemented("dns.resolve* with ttl option");
}
req.ttl = !!(options && (options as ResolveOptions).ttl); req.ttl = !!(options && (options as ResolveOptions).ttl);
const err = this._handle[bindingName](req, domainToASCII(name)); const err = this._handle[bindingName](req, domainToASCII(name));

View file

@ -37,6 +37,7 @@ import {
import { ares_strerror } from "ext:deno_node/internal_binding/ares.ts"; import { ares_strerror } from "ext:deno_node/internal_binding/ares.ts";
import { notImplemented } from "ext:deno_node/_utils.ts"; import { notImplemented } from "ext:deno_node/_utils.ts";
import { import {
op_dns_resolve,
op_net_get_ips_from_perm_token, op_net_get_ips_from_perm_token,
op_node_getaddrinfo, op_node_getaddrinfo,
} from "ext:core/ops"; } from "ext:core/ops";
@ -198,9 +199,7 @@ export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery {
this.#tries = tries; this.#tries = tries;
} }
async #query(query: string, recordType: Deno.RecordType) { async #query(query: string, recordType: Deno.RecordType, ttl?: boolean) {
// TODO(@bartlomieju): TTL logic.
let code: number; let code: number;
let ret: Awaited<ReturnType<typeof Deno.resolveDns>>; let ret: Awaited<ReturnType<typeof Deno.resolveDns>>;
@ -217,6 +216,7 @@ export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery {
query, query,
recordType, recordType,
resolveOptions, resolveOptions,
ttl,
)); ));
if (code === 0 || code === codeMap.get("EAI_NODATA")!) { if (code === 0 || code === codeMap.get("EAI_NODATA")!) {
@ -224,7 +224,7 @@ export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery {
} }
} }
} else { } else {
({ code, ret } = await this.#resolve(query, recordType)); ({ code, ret } = await this.#resolve(query, recordType, null, ttl));
} }
return { code: code!, ret: ret! }; return { code: code!, ret: ret! };
@ -234,15 +234,26 @@ export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery {
query: string, query: string,
recordType: Deno.RecordType, recordType: Deno.RecordType,
resolveOptions?: Deno.ResolveDnsOptions, resolveOptions?: Deno.ResolveDnsOptions,
ttl?: boolean,
): Promise<{ ): Promise<{
code: number; code: number;
ret: Awaited<ReturnType<typeof Deno.resolveDns>>; // deno-lint-ignore no-explicit-any
ret: any[];
}> { }> {
let ret: Awaited<ReturnType<typeof Deno.resolveDns>> = []; let ret = [];
let code = 0; let code = 0;
try { try {
ret = await Deno.resolveDns(query, recordType, resolveOptions); const res = await op_dns_resolve({
query,
recordType,
options: resolveOptions,
});
if (ttl) {
ret = res;
} else {
ret = res.map((recordWithTtl) => recordWithTtl.data);
}
} catch (e) { } catch (e) {
if (e instanceof Deno.errors.NotFound) { if (e instanceof Deno.errors.NotFound) {
code = codeMap.get("EAI_NODATA")!; code = codeMap.get("EAI_NODATA")!;
@ -363,18 +374,34 @@ export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery {
} }
queryA(req: QueryReqWrap, name: string): number { queryA(req: QueryReqWrap, name: string): number {
this.#query(name, "A").then(({ code, ret }) => { this.#query(name, "A", req.ttl).then(({ code, ret }) => {
req.oncomplete(code, ret); let recordsWithTtl;
if (req.ttl) {
recordsWithTtl = (ret as Deno.RecordWithTtl[]).map((val) => ({
address: val?.data,
ttl: val?.ttl,
}));
}
req.oncomplete(code, recordsWithTtl ?? ret);
}); });
return 0; return 0;
} }
queryAaaa(req: QueryReqWrap, name: string): number { queryAaaa(req: QueryReqWrap, name: string): number {
this.#query(name, "AAAA").then(({ code, ret }) => { this.#query(name, "AAAA", req.ttl).then(({ code, ret }) => {
const records = (ret as string[]).map((record) => compressIPv6(record)); let recordsWithTtl;
if (req.ttl) {
recordsWithTtl = (ret as Deno.RecordWithTtl[]).map((val) => ({
address: compressIPv6(val?.data as string),
ttl: val?.ttl,
}));
} else {
ret = (ret as string[]).map((record) => compressIPv6(record));
}
req.oncomplete(code, records); req.oncomplete(code, recordsWithTtl ?? ret);
}); });
return 0; return 0;

View file

@ -1,11 +1,11 @@
// Copyright 2018-2025 the Deno authors. MIT license. // Copyright 2018-2025 the Deno authors. MIT license.
// deno-lint-ignore-file no-console
import * as net from "node:net"; import * as net from "node:net";
import { assert, assertEquals } from "@std/assert"; import { assert, assertEquals } from "@std/assert";
import * as path from "@std/path"; import * as path from "@std/path";
import * as http from "node:http"; import * as http from "node:http";
import * as dns from "node:dns";
import console from "node:console";
Deno.test("[node/net] close event emits after error event - when host is not found", async () => { Deno.test("[node/net] close event emits after error event - when host is not found", async () => {
const socket = net.createConnection(27009, "doesnotexist"); const socket = net.createConnection(27009, "doesnotexist");
@ -247,3 +247,30 @@ Deno.test("[node/net] net.Server can listen on the same port immediately after i
}); });
await serverClosed.promise; await serverClosed.promise;
}); });
Deno.test("dns.resolve with ttl", async () => {
const d1 = Promise.withResolvers();
dns.resolve4("www.example.com", { ttl: true }, (err, addresses) => {
if (err) {
d1.reject(err);
} else {
d1.resolve(addresses);
}
});
// deno-lint-ignore no-explicit-any
const ret1 = await d1.promise as any[];
assert(ret1.length > 0);
assert(typeof ret1[0].ttl === "number");
const d2 = Promise.withResolvers();
dns.resolve4("www.example.com", (err, addresses) => {
if (err) {
d2.reject(err);
} else {
d2.resolve(addresses);
}
});
const ret2 = await d2.promise as string[];
assert(ret2.length > 0);
assert(typeof ret2[0] === "string");
});