Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ext/node): better dns.lookup compatibility #27936

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ext/net/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ deno_core::extension!(deno_net,
parameters = [ P: NetPermissions ],
ops = [
ops::op_net_accept_tcp,
ops::op_net_get_ips_from_perm_token,
ops::op_net_connect_tcp<P>,
ops::op_net_listen_tcp<P>,
ops::op_net_listen_udp<P>,
Expand Down
40 changes: 37 additions & 3 deletions ext/net/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,31 +372,65 @@ pub async fn op_net_set_multi_ttl_udp(
Ok(())
}

/// If this token is present in op_net_connect_tcp call and
/// the hostname matches with one of the resolved IPs, then
/// the permission check is performed against the original hostname.
pub struct NetPermToken {
pub hostname: String,
pub port: Option<u16>,
pub resolved_ips: Vec<String>,
}

impl deno_core::GarbageCollected for NetPermToken {}

impl NetPermToken {
/// Checks if the given address is included in the resolved IPs.
pub fn includes(&self, addr: &str) -> bool {
self.resolved_ips.iter().any(|ip| ip == addr)
}
}

#[op2]
#[serde]
pub fn op_net_get_ips_from_perm_token(
#[cppgc] token: &NetPermToken,
) -> Vec<String> {
token.resolved_ips.clone()
}

#[op2(async, stack_trace)]
#[serde]
pub async fn op_net_connect_tcp<NP>(
state: Rc<RefCell<OpState>>,
#[serde] addr: IpAddr,
#[cppgc] net_perm_token: Option<&NetPermToken>,
) -> Result<(ResourceId, IpAddr, IpAddr), NetError>
where
NP: NetPermissions + 'static,
{
op_net_connect_tcp_inner::<NP>(state, addr).await
op_net_connect_tcp_inner::<NP>(state, addr, net_perm_token).await
}

#[inline]
pub async fn op_net_connect_tcp_inner<NP>(
state: Rc<RefCell<OpState>>,
addr: IpAddr,
net_perm_token: Option<&NetPermToken>,
) -> Result<(ResourceId, IpAddr, IpAddr), NetError>
where
NP: NetPermissions + 'static,
{
{
let mut state_ = state.borrow_mut();
// If token exists and the address matches to its resolved ips,
// then we can check net permission against token.hostname, instead of addr.hostname
let hostname_to_check = match net_perm_token {
Some(token) if token.includes(&addr.hostname) => token.hostname.clone(),
_ => addr.hostname.clone(),
};
state_
.borrow_mut::<NP>()
.check_net(&(&addr.hostname, Some(addr.port)), "Deno.connect()")?;
.check_net(&(&hostname_to_check, Some(addr.port)), "Deno.connect()")?;
}

let addr = resolve_addr(&addr.hostname, addr.port)
Expand Down Expand Up @@ -1164,7 +1198,7 @@ mod tests {
};

let mut connect_fut =
op_net_connect_tcp_inner::<TestPermission>(conn_state, ip_addr)
op_net_connect_tcp_inner::<TestPermission>(conn_state, ip_addr, None)
.boxed_local();
let mut rid = None;

Expand Down
8 changes: 0 additions & 8 deletions ext/net/ops_tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,6 @@ where
n => n.to_string(),
};

{
let mut s = state.borrow_mut();
let permissions = s.borrow_mut::<NP>();
permissions
.check_net(&(&hostname, Some(0)), "Deno.startTls()")
.map_err(NetError::Permission)?;
}

let ca_certs = args
.ca_certs
.into_iter()
Expand Down
1 change: 1 addition & 0 deletions ext/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ sys_traits = { workspace = true, features = ["real", "winapi", "libc"] }
thiserror.workspace = true
tokio.workspace = true
tokio-eld = "0.2"
tower-service.workspace = true
url.workspace = true
webpki-root-certs.workspace = true
winapi.workspace = true
Expand Down
1 change: 1 addition & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ deno_core::extension!(deno_node,
ops::crypto::x509::op_node_x509_get_serial_number,
ops::crypto::x509::op_node_x509_key_usage,
ops::crypto::x509::op_node_x509_public_key,
ops::dns::op_node_getaddrinfo<P>,
ops::fs::op_node_fs_exists_sync<P>,
ops::fs::op_node_fs_exists<P>,
ops::fs::op_node_cp_sync<P>,
Expand Down
56 changes: 56 additions & 0 deletions ext/node/ops/dns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2018-2025 the Deno authors. MIT license.

use std::cell::RefCell;
use std::rc::Rc;
use std::str::FromStr;

use deno_core::op2;
use deno_core::OpState;
use deno_error::JsError;
use deno_net::ops::NetPermToken;
use deno_permissions::PermissionCheckError;
use hyper_util::client::legacy::connect::dns::GaiResolver;
use hyper_util::client::legacy::connect::dns::Name;
use tower_service::Service;

#[derive(Debug, thiserror::Error, JsError)]
pub enum GetAddrInfoError {
#[class(inherit)]
#[error(transparent)]
Permission(#[from] PermissionCheckError),
#[class(type)]
#[error("Could not resolve the hostname \"{0}\"")]
Resolution(String),
}

#[op2(async, stack_trace)]
#[cppgc]
pub async fn op_node_getaddrinfo<P>(
state: Rc<RefCell<OpState>>,
#[string] hostname: String,
port: Option<u16>,
) -> Result<NetPermToken, GetAddrInfoError>
where
P: crate::NodePermissions + 'static,
{
{
let mut state_ = state.borrow_mut();
let permissions = state_.borrow_mut::<P>();
permissions.check_net((hostname.as_str(), port), "node:dns.lookup()")?;
}

let mut resolver = GaiResolver::new();
let name = Name::from_str(&hostname)
.map_err(|_| GetAddrInfoError::Resolution(hostname.clone()))?;
let resolved_ips = resolver
.call(name)
.await
.map_err(|_| GetAddrInfoError::Resolution(hostname.clone()))?
.map(|addr| addr.ip().to_string())
.collect::<Vec<_>>();
Ok(NetPermToken {
hostname,
port,
resolved_ips,
})
}
1 change: 1 addition & 0 deletions ext/node/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pub mod blocklist;
pub mod buffer;
pub mod crypto;
pub mod dns;
pub mod fs;
pub mod http;
pub mod http2;
Expand Down
18 changes: 16 additions & 2 deletions ext/node/polyfills/dns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,25 @@ function onlookup(
this: GetAddrInfoReqWrap,
err: number | null,
addresses: string[],
netPermToken: object | undefined,
) {
if (err) {
return this.callback(dnsException(err, "getaddrinfo", this.hostname));
}

this.callback(null, addresses[0], this.family || isIP(addresses[0]));
this.callback(
null,
addresses[0],
this.family || isIP(addresses[0]),
netPermToken,
);
}

function onlookupall(
this: GetAddrInfoReqWrap,
err: number | null,
addresses: string[],
netPermToken: object | undefined,
) {
if (err) {
return this.callback(dnsException(err, "getaddrinfo", this.hostname));
Expand All @@ -127,7 +134,7 @@ function onlookupall(
};
}

this.callback(null, parsedAddresses);
this.callback(null, parsedAddresses, undefined, netPermToken);
}

type LookupCallback = (
Expand Down Expand Up @@ -189,6 +196,7 @@ export function lookup(
let family = 0;
let all = false;
let verbatim = getDefaultVerbatim();
let port = undefined;

// Parse arguments
if (hostname) {
Expand Down Expand Up @@ -230,6 +238,11 @@ export function lookup(
validateBoolean(options.verbatim, "options.verbatim");
verbatim = options.verbatim;
}

if (options?.port != null) {
validateNumber(options.port, "options.port");
port = options.port;
}
}

if (!hostname) {
Expand Down Expand Up @@ -263,6 +276,7 @@ export function lookup(
req.family = family;
req.hostname = hostname;
req.oncomplete = all ? onlookupall : onlookup;
req.port = port;

const err = getaddrinfo(
req,
Expand Down
5 changes: 5 additions & 0 deletions ext/node/polyfills/internal/dns/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export interface LookupOptions {
hints?: number | undefined;
all?: boolean | undefined;
verbatim?: boolean | undefined;
/**
* Deno specific extension. If port is specified, the required net permission
* for the lookup call will be reduced to single port.
*/
port?: number | undefined;
}

export interface LookupOneOptions extends LookupOptions {
Expand Down
56 changes: 34 additions & 22 deletions ext/node/polyfills/internal_binding/cares_wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@
// deno-lint-ignore-file prefer-primordials

import type { ErrnoException } from "ext:deno_node/internal/errors.ts";
import { isIPv4 } from "ext:deno_node/internal/net.ts";
import { isIPv4, isIPv6 } from "ext:deno_node/internal/net.ts";
import { codeMap } from "ext:deno_node/internal_binding/uv.ts";
import {
AsyncWrap,
providerType,
} from "ext:deno_node/internal_binding/async_wrap.ts";
import { ares_strerror } from "ext:deno_node/internal_binding/ares.ts";
import { notImplemented } from "ext:deno_node/_utils.ts";
import {
op_net_get_ips_from_perm_token,
op_node_getaddrinfo,
} from "ext:core/ops";

interface LookupAddress {
address: string;
Expand All @@ -45,6 +49,7 @@ interface LookupAddress {
export class GetAddrInfoReqWrap extends AsyncWrap {
family!: number;
hostname!: string;
port: number | undefined;

callback!: (
err: ErrnoException | null,
Expand All @@ -53,7 +58,11 @@ export class GetAddrInfoReqWrap extends AsyncWrap {
) => void;
resolve!: (addressOrAddresses: LookupAddress | LookupAddress[]) => void;
reject!: (err: ErrnoException | null) => void;
oncomplete!: (err: number | null, addresses: string[]) => void;
oncomplete!: (
err: number | null,
addresses: string[],
netPermToken: object | undefined,
) => void;

constructor() {
super(providerType.GETADDRINFOREQWRAP);
Expand All @@ -67,30 +76,27 @@ export function getaddrinfo(
_hints: number,
verbatim: boolean,
): number {
const addresses: string[] = [];
let addresses: string[] = [];

// TODO(cmorten): use hints
// REF: https://nodejs.org/api/dns.html#dns_supported_getaddrinfo_flags

const recordTypes: ("A" | "AAAA")[] = [];

if (family === 0 || family === 4) {
recordTypes.push("A");
}
if (family === 0 || family === 6) {
recordTypes.push("AAAA");
}

(async () => {
await Promise.allSettled(
recordTypes.map((recordType) =>
Deno.resolveDns(hostname, recordType).then((records) => {
records.forEach((record) => addresses.push(record));
})
),
);

const error = addresses.length ? 0 : codeMap.get("EAI_NODATA")!;
let error = 0;
let netPermToken: object | undefined;
try {
netPermToken = await op_node_getaddrinfo(hostname, req.port || undefined);
addresses.push(...op_net_get_ips_from_perm_token(netPermToken));
if (addresses.length === 0) {
error = codeMap.get("EAI_NODATA")!;
}
} catch (e) {
if (e instanceof Deno.errors.NotCapable) {
error = codeMap.get("EPERM")!;
} else {
error = codeMap.get("EAI_NODATA")!;
}
}

// TODO(cmorten): needs work
// REF: https://github.com/nodejs/node/blob/master/src/cares_wrap.cc#L1444
Expand All @@ -106,7 +112,13 @@ export function getaddrinfo(
});
}

req.oncomplete(error, addresses);
if (family === 4) {
addresses = addresses.filter((addr) => isIPv4(addr));
} else if (family === 6) {
addresses = addresses.filter((addr) => isIPv6(addr));
}

req.oncomplete(error, addresses, netPermToken);
})();

return 0;
Expand Down
Loading
Loading