Skip to content
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
110 changes: 97 additions & 13 deletions packages/bun-usockets/src/bsd.c
Original file line number Diff line number Diff line change
Expand Up @@ -1275,18 +1275,95 @@ int bsd_socket_listen_error_is_benign(LIBUS_SOCKET_DESCRIPTOR fd) {
#endif
}

static socklen_t bsd_inet_addr_length(const struct sockaddr_storage *addr) {
return addr->ss_family == AF_INET6 ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in);
}

/* One address as the chain that getaddrinfo(3) answers with, in the frame of the caller. */
struct bsd_literal_addrinfo {
struct addrinfo info;
struct sockaddr_storage addr;
};

/* getaddrinfo(3) for a bind host. A host that Node's net.isIP() takes is bound as it is written: its chain is
* *literal, with no resolver call. getaddrinfo(3) refuses a zone name on an address that is not link-local.
* bsd_bind_freeaddrinfo does not free that chain. */
static int bsd_bind_getaddrinfo(const char *host, int port, const struct addrinfo *hints, struct bsd_literal_addrinfo *literal, struct addrinfo **result) {
if (host && Bun__parseBindAddress(host, (uint16_t) port, &literal->addr)) {
memset(&literal->info, 0, sizeof(struct addrinfo));
literal->info.ai_family = literal->addr.ss_family;
literal->info.ai_socktype = hints->ai_socktype;
literal->info.ai_addr = (struct sockaddr *) &literal->addr;
literal->info.ai_addrlen = bsd_inet_addr_length(&literal->addr);
*result = &literal->info;
return 0;
}

char port_string[16];
snprintf(port_string, 16, "%d", port);
return getaddrinfo(host, port_string, hints, result);
}

static void bsd_bind_freeaddrinfo(struct addrinfo *result, const struct bsd_literal_addrinfo *literal) {
if (result != &literal->info) {
freeaddrinfo(result);
}
}

/* bsd_bind_listen_fd for a sockaddr. */
static LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_addr(LIBUS_SOCKET_DESCRIPTOR listenFd, const struct sockaddr_storage *addr, int options, int *error) {
if (bsd_set_reuse(listenFd, options) != 0) {
*error = LIBUS_ERR;
return LIBUS_SOCKET_ERROR;
}

#if defined(SO_REUSEADDR) && !_WIN32
int one = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
#endif

#ifdef IPV6_V6ONLY
if (addr->ss_family == AF_INET6) {
int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &enabled, sizeof(enabled)) != 0) {
*error = LIBUS_ERR;
return LIBUS_SOCKET_ERROR;
}
}
#endif

if (us_internal_bind_and_listen(listenFd, (struct sockaddr *) addr, bsd_inet_addr_length(addr), 512, error)) {
return LIBUS_SOCKET_ERROR;
}

return listenFd;
}

/* bsd_create_listen_socket for the address of a host that net.isIP() takes. */
static LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket_addr(const struct sockaddr_storage *addr, int options, int *error) {
LIBUS_SOCKET_DESCRIPTOR listenFd = bsd_create_socket(addr->ss_family, SOCK_STREAM, 0, error);
if (listenFd == LIBUS_SOCKET_ERROR) {
return LIBUS_SOCKET_ERROR;
}

if (bsd_bind_listen_addr(listenFd, addr, options, error) == LIBUS_SOCKET_ERROR) {
bsd_close_socket(listenFd);
return LIBUS_SOCKET_ERROR;
}

return listenFd;
}

LIBUS_SOCKET_DESCRIPTOR bsd_create_bound_socket(const char *host, int port, int options, int *out_port, int *error) {
bsd_winsock_ensure();
struct addrinfo hints, *result;
struct bsd_literal_addrinfo literal;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

char port_string[16];
snprintf(port_string, 16, "%d", port);

int gai = getaddrinfo(host, port_string, &hints, &result);
int gai = bsd_bind_getaddrinfo(host, port, &hints, &literal, &result);
if (gai != 0) {
#ifdef _WIN32
*error = gai;
Expand Down Expand Up @@ -1330,7 +1407,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_bound_socket(const char *host, int port, int
break;
}
}
freeaddrinfo(result);
bsd_bind_freeaddrinfo(result, &literal);
if (fd == LIBUS_SOCKET_ERROR) {
return LIBUS_SOCKET_ERROR;
}
Expand Down Expand Up @@ -1359,6 +1436,10 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_bound_socket(const char *host, int port, int

LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int options, int* error) {
bsd_winsock_ensure();
struct sockaddr_storage literal;
if (host && Bun__parseBindAddress(host, (uint16_t) port, &literal)) {
return bsd_create_listen_socket_addr(&literal, options, error);
}
struct addrinfo hints, *result;
memset(&hints, 0, sizeof(struct addrinfo));

Expand Down Expand Up @@ -1753,16 +1834,14 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op
}

struct addrinfo hints, *result;
struct bsd_literal_addrinfo literal;
memset(&hints, 0, sizeof(struct addrinfo));

hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;

char port_string[16];
snprintf(port_string, 16, "%d", port);

int gai_result = getaddrinfo(host, port_string, &hints, &result);
int gai_result = bsd_bind_getaddrinfo(host, port, &hints, &literal, &result);
if (gai_result != 0) {
if (err != NULL) {
*err = -gai_result;
Expand All @@ -1787,7 +1866,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op
}

if (listenFd == LIBUS_SOCKET_ERROR) {
freeaddrinfo(result);
bsd_bind_freeaddrinfo(result, &literal);
return LIBUS_SOCKET_ERROR;
}

Expand All @@ -1800,14 +1879,19 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op
#endif
}
bsd_close_socket(listenFd);
freeaddrinfo(result);
bsd_bind_freeaddrinfo(result, &literal);
return LIBUS_SOCKET_ERROR;
}

#ifdef IPV6_V6ONLY
if (listenAddr->ai_family == AF_INET6) {
int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &enabled, sizeof(enabled)) != 0) {
if (err != NULL) {
*err = LIBUS_ERR;
}
bsd_close_socket(listenFd);
bsd_bind_freeaddrinfo(result, &literal);
return LIBUS_SOCKET_ERROR;
}
}
Expand All @@ -1825,11 +1909,11 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op
#endif
}
bsd_close_socket(listenFd);
freeaddrinfo(result);
bsd_bind_freeaddrinfo(result, &literal);
return LIBUS_SOCKET_ERROR;
}

freeaddrinfo(result);
bsd_bind_freeaddrinfo(result, &literal);
if (err != NULL) {
*err = 0;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ void *sni_find(void *sni, const char *hostname);
extern int Bun__addrinfo_get(struct us_loop_t* loop, const char* host, uint16_t port, struct addrinfo_request** ptr);
/* Fills *out when host is a numeric address (incl. inet_aton shorthand and %zone); 0 when it is a name. */
extern int Bun__parseIpAddress(const char* host, uint16_t port, struct sockaddr_storage* out);
/* Fills *out when Node's net.isIP() takes host (a %zone in its grammar included); 0 when the resolver reads it. */
extern int Bun__parseBindAddress(const char* host, uint16_t port, struct sockaddr_storage* out);
extern int Bun__addrinfo_set(struct addrinfo_request* ptr, struct us_connecting_socket_t* socket);
extern int Bun__addrinfo_cancel(struct addrinfo_request* ptr, struct us_connecting_socket_t* socket);
extern void Bun__addrinfo_freeRequest(struct addrinfo_request* addrinfo_req, int error);
Expand Down
4 changes: 4 additions & 0 deletions src/bun_core/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,10 @@ impl Display for URLFormatter<'_> {
)?;

if let Some(hostname) = self.hostname {
let hostname = match self.proto {
URLProto::Http | URLProto::Https => crate::ip_address::without_ipv6_zone(hostname),
URLProto::Unix | URLProto::Abstract => hostname,
};
let needs_brackets =
hostname[0] != b'[' && crate::ip_address::is_ipv6_address(hostname);
if needs_brackets {
Expand Down
8 changes: 8 additions & 0 deletions src/bun_core/ip_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,11 @@ pub fn strip_ipv6_brackets(host: &[u8]) -> &[u8] {
}
host
}

/// A URL has no zone id: `fe80::1%eth0` and `[fe80::1%eth0]` are the host `fe80::1` there, whatever the zone is. Any other host comes back as it is.
pub fn without_ipv6_zone(host: &[u8]) -> &[u8] {
match crate::strings::split_once_char(strip_ipv6_brackets(host), b'%') {
Some((address, _)) if is_ipv6_address(address) => address,
_ => host,
}
}
36 changes: 10 additions & 26 deletions src/runtime/node/node_cluster_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,32 +573,16 @@ pub(crate) fn cluster_raw_bind<'a>(
}
addr_z[..addr_bytes.len()].copy_from_slice(addr_bytes);

use bun_cares_sys::ares_inet_pton;
// SAFETY: `ss` is a zeroed `sockaddr_storage`, sized and aligned for every sockaddr type;
// `addr_z` is NUL-terminated and the address out-param is inside `ss`.
let parsed = unsafe {
if family == libc::AF_INET6 {
let sin6: &mut libc::sockaddr_in6 =
&mut *(&raw mut ss).cast::<libc::sockaddr_in6>();
sin6.sin6_family = libc::AF_INET6 as libc::sa_family_t;
sin6.sin6_port = (port as u16).to_be();
ares_inet_pton(
libc::AF_INET6,
addr_z.as_ptr().cast(),
(&raw mut sin6.sin6_addr).cast(),
) == 1
} else {
let sin: &mut libc::sockaddr_in =
&mut *(&raw mut ss).cast::<libc::sockaddr_in>();
sin.sin_family = libc::AF_INET as libc::sa_family_t;
sin.sin_port = (port as u16).to_be();
ares_inet_pton(
libc::AF_INET,
addr_z.as_ptr().cast(),
(&raw mut sin.sin_addr).cast(),
) == 1
}
};
// As the handle of Node.js (`uv_ip4_addr` / `uv_ip6_addr`): EINVAL for an address of the other family, which the resolver of macOS maps (`127.0.0.1` to `::ffff:127.0.0.1`).
let parsed =
match crate::socket::socket_address::parse_bind_address(addr_bytes, port as u16) {
Some(literal) if literal.family() == family => {
ss = literal.into_storage();
true
}
Some(_) => return Ok(BindResult::Errno(-(libc::EINVAL))),
None => false,
};
if !parsed {
let mut hints: libc::addrinfo = bun_core::ffi::zeroed();
hints.ai_family = family;
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/ServerConfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1338,7 +1338,7 @@ impl ServerConfig {
Address::Tcp {
hostname: Some(hostname),
..
} => hostname.as_bytes(),
} => bun_core::ip_address::without_ipv6_zone(hostname.as_bytes()),
Address::Tcp { hostname: None, .. } | Address::Unix(_) => b"0.0.0.0",
};

Expand Down
50 changes: 41 additions & 9 deletions src/runtime/socket/SocketAddress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,7 +972,7 @@ pub(crate) unsafe extern "C" fn Bun__parseIpAddress(
}

/// A `%zone` is an interface name (`en0`) everywhere but Windows, where it is already the index.
fn scope_index(zone: &[u8]) -> u32 {
pub(crate) fn scope_index(zone: &[u8]) -> u32 {
#[cfg(not(windows))]
{
let mut buf = [0u8; 64];
Expand All @@ -985,14 +985,14 @@ fn scope_index(zone: &[u8]) -> u32 {
}
}
}
let mut idx: u32 = 0;
for &b in zone {
if !b.is_ascii_digit() {
return 0;
}
idx = idx.wrapping_mul(10).wrapping_add((b - b'0') as u32);
}
idx
zone.iter()
.try_fold(0u32, |idx, &b| {
if !b.is_ascii_digit() {
return None;
}
idx.checked_mul(10)?.checked_add(u32::from(b - b'0'))
})
.unwrap_or(0)
}

#[cfg(windows)]
Expand Down Expand Up @@ -1022,3 +1022,35 @@ pub(crate) mod inet {
pub(crate) use bun_sys::posix::AF::{INET as AF_INET, INET6 as AF_INET6};
pub(crate) type socklen_t = super::ares::ares_socklen_t;
}

/// The address of a bind or listen host that Node.js's `net.isIP()` takes, a `%zone` in its grammar included. Any other host stays with the resolver, as in Node.js. The zone is read as libuv's `uv_ip6_addr` reads it (https://github.com/nodejs/node/blob/v26.3.0/deps/uv/src/uv-common.c#L288-L293), and `scope_index` also reads a zone of digits as an index off Windows, because `dns.lookup()` here prints a scoped address as `%<index>`.
pub(crate) fn parse_bind_address(host: &[u8], port: u16) -> Option<bun_sys::net::Address> {
if let Some(ip) = bun_core::ip_address::parse_strict(host) {
return Some(bun_sys::net::Address::from_ip(ip, port));
}
let ip = bun_core::ip_address::parse_zoned_ipv6_host(host)?;
let (_, zone) = strings::split_once_char(host, b'%')?;
let mut address = bun_sys::net::Address::from_ip(core::net::IpAddr::V6(ip), port);
address.set_scope_id(scope_index(zone));
Some(address)
}

/// [`parse_bind_address`] for uSockets' bind and listen constructors: 1 and `out` filled, or 0 when the resolver reads `host`; `host` must be NUL-terminated and `out` writable.
#[unsafe(no_mangle)]
pub(crate) unsafe extern "C" fn Bun__parseBindAddress(
host: *const core::ffi::c_char,
port: u16,
out: *mut bun_sys::posix::sockaddr_storage,
) -> c_int {
if host.is_null() || out.is_null() {
return 0;
}
// SAFETY: caller contract — `host` is a NUL-terminated C string.
let bytes = unsafe { core::ffi::CStr::from_ptr(host) }.to_bytes();
let Some(address) = parse_bind_address(bytes, port) else {
return 0;
};
// SAFETY: caller contract — `out` is a writable `sockaddr_storage`.
unsafe { *out = address.into_storage() };
1
}
5 changes: 4 additions & 1 deletion src/runtime/socket/udp_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2015,7 +2015,10 @@ pub(crate) fn dgram_bind_fd(fd: i32, address: &[u8], port: i32, flags: i32) -> b
bun_sys::Tag::bind2,
));
};
let addr = Address::from_ip(ip, port);
let mut addr = Address::from_ip(ip, port);
if let Some((_, zone)) = bun_core::strings::split_once_char(address, b'%') {
addr.set_scope_id(crate::socket::socket_address::scope_index(zone));
}
let socklen = addr.socklen() as libc::socklen_t;
let storage = addr.into_storage();

Expand Down
19 changes: 19 additions & 0 deletions test/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1960,6 +1960,25 @@ export function isIPv6() {
return isIP("IPv6");
}

/**
* The name of the interface that has `::1` ("lo", "lo0"), or undefined when this host has no IPv6 loopback address.
* It is read from the interfaces and not from a bind, for the tests that are about a bind.
*/
export function ipv6LoopbackInterface(): string | undefined {
return Object.entries(os.networkInterfaces()).find(([, addresses]) =>
addresses?.some(({ address }) => address === "::1"),
)?.[0];
}

/**
* The zones to bind `::1%<zone>` with: the name of the loopback interface, which is no zone on Windows, and a name
* that no interface has, which is no zone. Skip the test when `ipv6LoopbackInterface()` is undefined.
*/
export function ipv6LoopbackZones(): string[] {
const loopback = ipv6LoopbackInterface();
return [...(isWindows || loopback === undefined ? [] : [loopback]), "nosuch0"];
}

export function isIPv4() {
return isIP("IPv4");
}
Expand Down
Loading
Loading