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
72 changes: 35 additions & 37 deletions packages/bun-usockets/src/bsd.c
Original file line number Diff line number Diff line change
Expand Up @@ -1836,59 +1836,57 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op
return listenFd;
}

int bsd_connect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd, const char *host, int port) {
bsd_winsock_ensure();
struct addrinfo hints, *result;
memset(&hints, 0, sizeof(struct addrinfo));

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

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

int gai_error = getaddrinfo(host, port_string, &hints, &result);

if (gai_error != 0) {
return gai_error;
}

if (result == NULL) {
int bsd_connect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_storage *addr) {
socklen_t addrlen;
if (addr->ss_family == AF_INET) {
addrlen = sizeof(struct sockaddr_in);
} else if (addr->ss_family == AF_INET6) {
addrlen = sizeof(struct sockaddr_in6);
} else {
/* AF_UNSPEC would dissolve the association and report success. */
#ifdef _WIN32
WSASetLastError(WSAEINVAL);
#endif
errno = EINVAL;
return -1;
}

for (struct addrinfo *rp = result; rp != NULL; rp = rp->ai_next) {
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
freeaddrinfo(result);
return 0;
}
}

freeaddrinfo(result);
return (int)LIBUS_SOCKET_ERROR;
int rc;
do {
rc = connect(fd, (const struct sockaddr *) addr, addrlen);
} while (IS_EINTR(rc));
return rc == 0 ? 0 : -1;
}

int bsd_disconnect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd) {
/* As libuv's uv__udp_disconnect: Winsock takes an address as long as the socket's own. */
#ifdef _WIN32
struct sockaddr_storage addr;
#else
struct sockaddr addr;
#endif
memset(&addr, 0, sizeof(addr));
addr.sa_family = AF_UNSPEC;
((struct sockaddr *) &addr)->sa_family = AF_UNSPEC;
#ifdef __APPLE__
addr.sa_len = sizeof(addr);
#endif

int res = connect(fd, &addr, sizeof(addr));
int res;
do {
res = connect(fd, (struct sockaddr *) &addr, sizeof(addr));
} while (IS_EINTR(res));
if (res == 0) {
return 0;
}
// EAFNOSUPPORT is harmless in this case - we just want to disconnect
if (res == 0 ||
#ifdef _WIN32
WSAGetLastError() == WSAEAFNOSUPPORT
return WSAGetLastError() == WSAEAFNOSUPPORT ? 0 : -1;
#elif defined(__APPLE__) || defined(__FreeBSD__)
/* An AF_INET6 socket answers EINVAL there, and the association is gone. */
return errno == EAFNOSUPPORT || errno == EINVAL ? 0 : -1;
#else
errno == EAFNOSUPPORT
return errno == EAFNOSUPPORT ? 0 : -1;
#endif
) {
return 0;
} else {
return -1;
}
}

static int bsd_do_connect_raw(LIBUS_SOCKET_DESCRIPTOR fd, struct sockaddr *addr, size_t namelen)
Expand Down
3 changes: 2 additions & 1 deletion packages/bun-usockets/src/internal/networking/bsd.h
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket_unix(const char *path, size_t p

/* Creates an UDP socket bound to the hostname and port */
LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int options, int *err);
int bsd_connect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd, const char *host, int port);
/* connect(2) to an AF_INET or AF_INET6 address. Returns 0, or -1 with the error in errno / WSAGetLastError(). */
int bsd_connect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_storage *addr);
int bsd_disconnect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd);

LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket(struct sockaddr_storage *addr, struct sockaddr_storage *local_addr, int options);
Expand Down
4 changes: 2 additions & 2 deletions packages/bun-usockets/src/udp.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ int us_udp_socket_set_ttl_multicast(struct us_udp_socket_t *s, int ttl) {
return bsd_socket_ttl_multicast(us_poll_fd(&s->p), ttl);
}

int us_udp_socket_connect(struct us_udp_socket_t *s, const char* host, unsigned short port) {
return bsd_connect_udp_socket(us_poll_fd((struct us_poll_t *)s), host, port);
int us_udp_socket_connect(struct us_udp_socket_t *s, const struct sockaddr_storage *addr) {
return bsd_connect_udp_socket(us_poll_fd((struct us_poll_t *)s), addr);
}

int us_udp_socket_disconnect(struct us_udp_socket_t *s) {
Expand Down
36 changes: 27 additions & 9 deletions src/js/node/dgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ interface UDPHandle {
queueCount: number;
sendQueue: (QueuedSend | undefined)[] | undefined;
sendQueueHead: number;
// Fixed by the socket type, as Node's handle is: `socket.type` is writable.
readonly family: 4 | 6;
connect: typeof handleConnect;
send: typeof handleSend;
drain: typeof handleDrain;
getSendQueueSize: typeof handleGetSendQueueSize;
Expand Down Expand Up @@ -345,6 +348,8 @@ function newHandle(type: string, lookup: LookupFunction | undefined) {
sendQueue: undefined,
// Index of the first not-yet-drained sendQueue entry (see handleDrain).
sendQueueHead: 0,
family: type === "udp6" ? 6 : 4,
connect: handleConnect,
send: handleSend,
drain: handleDrain,
getSendQueueSize: handleGetSendQueueSize,
Expand All @@ -366,6 +371,18 @@ function newHandle(type: string, lookup: LookupFunction | undefined) {
return handle;
}

// Mirrors the libuv UDP wrap's connect() and connect6(): 0 or a negative uv errno, UV_EINVAL for what is no address of the handle's family.
function handleConnect(this: UDPHandle, ip: string, port: number) {
const socket = this.socket;
if (!socket) return UV_EBADF;
return connectFn(socket, ip, port, this.family === 6);
}

// `isIP()` is 0 for text that the handle still reads as an address of its family, such as one with an empty zone.
function blockListFamily(handle: UDPHandle, ip: string) {
return `ipv${isIP(ip) || handle.family}`;
}

// Mirrors the libuv UDP wrap's send(): sentBytes + 1 on synchronous success,
// 0 when queued for the next writable event (callback fires on drain), or a
// negative uv errno / native error. Connected form omits port/address.
Expand Down Expand Up @@ -992,7 +1009,8 @@ const connectFn = $bindgen("udp_socket.bind.ts", "connect") as (
socket: $Node.UDPSocketHandle,
host: string,
port: number,
) => void;
ipv6: boolean,
) => number;

function doConnect(
ex: Error | null,
Expand All @@ -1005,15 +1023,14 @@ function doConnect(
const state = self[kStateSymbol];
if (!state.handle) return;

if (!ex && state.sendBlockList?.check(ip, `ipv${isIP(ip)}`)) {
if (!ex && state.sendBlockList?.check(ip, blockListFamily(state.handle, ip))) {
ex = $ERR_IP_BLOCKED(ip);
}

if (!ex) {
try {
connectFn(state.handle.socket!, ip, port);
} catch (e) {
ex = e as Error;
const err = state.handle.connect(ip, port);
if (err) {
ex = new ExceptionWithHostPort(err, "connect", address, port);
}
}

Expand All @@ -1033,13 +1050,14 @@ function doConnect(
process.nextTick(() => self.emit("connect"));
}

const disconnectFn = $bindgen("udp_socket.bind.ts", "disconnect") as (socket: $Node.UDPSocketHandle) => void;
const disconnectFn = $bindgen("udp_socket.bind.ts", "disconnect") as (socket: $Node.UDPSocketHandle) => number;

Socket.prototype.disconnect = function (this: $Node.DgramSocket) {
const state = this[kStateSymbol];
if (state.connectState !== CONNECT_STATE_CONNECTED) throw $ERR_SOCKET_DGRAM_NOT_CONNECTED();

disconnectFn(state.handle!.socket!);
const err = disconnectFn(state.handle!.socket!);
if (err) throw new ErrnoException(err, "connect");
state.connectState = CONNECT_STATE_DISCONNECTED;
};

Expand Down Expand Up @@ -1240,7 +1258,7 @@ function doSend(
return;
}

if (ip && state.sendBlockList?.check(ip, `ipv${isIP(ip)}`)) {
if (ip && state.sendBlockList?.check(ip, blockListFamily(state.handle, ip))) {
Comment thread
robobun marked this conversation as resolved.
Resolved
if (callback) {
process.nextTick(callback, $ERR_IP_BLOCKED(ip));
}
Expand Down
66 changes: 50 additions & 16 deletions src/runtime/socket/SocketAddress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,24 @@ const _: () = {
assert!(AF::INET6 as c_int == ares::AF::INET6);
};

/// Fills `out` with `host`:`port` when `host` is numeric (inet_aton shorthand and `%zone` included) and returns 1, or 0 when it is a name — the one parse behind uSockets' connect paths, so a literal never reaches the resolver; `host` must be NUL-terminated and `out` writable.
/// Writes `host`:`port` to `out` and returns it when `host` is numeric (inet_aton shorthand and `%zone` included), or returns `None` when it is a name. The one parse in front of a resolver, so a literal never reaches it.
#[inline(always)]
pub(crate) fn parse_ip_address<'a>(
host: &[u8],
port: u16,
out: &'a mut mem::MaybeUninit<bun_sys::posix::sockaddr_storage>,
) -> Option<&'a mut bun_sys::posix::sockaddr_storage> {
let ip = bun_core::ip_address::to_ip_address(host)?;
let mut addr = bun_sys::net::Address::from_ip(ip, port);
if ip.is_ipv6() {
if let Some(pct) = strings::index_of_char_usize(host, b'%') {
addr.set_scope_id(scope_index(&host[pct + 1..]));
}
}
Some(out.write(addr.into_storage()))
}

/// [`parse_ip_address`] for uSockets' connect paths: 1 and `out` filled, or 0 when `host` is a name; `host` must be NUL-terminated and `out` writable.
#[unsafe(no_mangle)]
pub(crate) unsafe extern "C" fn Bun__parseIpAddress(
host: *const core::ffi::c_char,
Expand All @@ -960,17 +977,34 @@ pub(crate) unsafe extern "C" fn Bun__parseIpAddress(
};
// SAFETY: caller contract — `host` is a NUL-terminated C string.
let bytes = unsafe { core::ffi::CStr::from_ptr(host) }.to_bytes();
let Some(ip) = bun_core::ip_address::to_ip_address(bytes) else {
return 0;
c_int::from(parse_ip_address(bytes, port, out).is_some())
}

/// The address that Node.js's UDP handle takes, by libuv's `uv_ip4_addr` / `uv_ip6_addr` (https://github.com/nodejs/node/blob/v26.3.0/deps/uv/src/uv-common.c#L254-L296): a literal of the handle's own family, no name and no inet_aton shorthand. Unlike libuv, a zone may also be a decimal index off Windows, which is how `dns.lookup()` here prints a link-local answer.
pub(crate) fn parse_uv_ip_address(
host: &[u8],
port: u16,
ipv6: bool,
out: &mut bun_sys::posix::sockaddr_storage,
) -> bool {
// libuv reads a C string and connects to the text in front of a NUL, which `sendBlockList` did not check.
if strings::contains_char(host, 0) {
return false;
}
let Some(ip) = bun_core::ip_address::parse_uv_inet_pton(host) else {
return false;
};
if ip.is_ipv6() != ipv6 {
return false;
}
let mut addr = bun_sys::net::Address::from_ip(ip, port);
if ip.is_ipv6() {
if let Some(pct) = strings::index_of_char_usize(bytes, b'%') {
addr.set_scope_id(scope_index(&bytes[pct + 1..]));
if ipv6 {
if let Some((_, zone)) = strings::split_once_char(host, b'%') {
addr.set_scope_id(scope_index(zone));
Comment thread
robobun marked this conversation as resolved.
Resolved
}
}
out.write(addr.into_storage());
1
*out = addr.into_storage();
true
}

/// A `%zone` is an interface name (`en0`) everywhere but Windows, where it is already the index.
Expand All @@ -987,14 +1021,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
12 changes: 5 additions & 7 deletions src/runtime/socket/udp_socket.bind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,14 @@ export const udpSocket = b.fn({
rustPath: "crate::socket::udp_socket::UDPSocket::udp_socket",
});

/** Node's `handle.connect(ip, port)`: 0 or a negative libuv error code. `ipv6` is the socket type. */
export const connect = b.fn({
args: { socket: UDPSocket, host: b.string.loose, port: b.i32 },
args: { socket: UDPSocket, host: b.string.loose.utf8, port: b.i32, ipv6: b.bool },
returns: b.i32,
capability: "js",
throws: true,
});
export const disconnect = b.fn({
args: { socket: UDPSocket },
capability: "heap",
throws: true,
});
/** Node's `handle.disconnect()`: 0 or a negative libuv error code. */
export const disconnect = b.fn({ args: { socket: UDPSocket }, returns: b.i32 });
export const bufferSize = b.fn({
args: { socket: UDPSocket, size: b.i32.wrap, isRecv: b.bool },
returns: b.i32,
Expand Down
Loading
Loading