mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
Add support for localAddress and localPort in TCP connections (#23464)
## Summary This PR implements support for `localAddress` and `localPort` options in TCP connections, allowing users to bind outgoing connections to a specific local IP address and port. This addresses issue #6888 and implements Node.js-compatible behavior for these options. ## Changes ### C Layer (uSockets) - **`bsd.c`**: Modified `bsd_create_connect_socket()` to accept a `local_addr` parameter and call `bind()` before `connect()` when a local address is specified - **`context.c`**: Updated `us_socket_context_connect()` and `start_connections()` to parse and pass local address parameters through the connection flow - **`libusockets.h`**: Updated public API signatures to include `local_host` and `local_port` parameters - **`internal.h`**: Added `local_host` and `local_port` fields to `us_connecting_socket_t` structure - **`openssl.c`**: Updated SSL connection function to match the new signature ### Zig Layer - **`SocketContext.zig`**: Updated `connect()` method to accept and pass through `local_host` and `local_port` parameters - **`socket.zig`**: Modified `connectAnon()` to handle local address binding, including IPv6 bracket removal and proper memory management - **`Handlers.zig`**: Added `localAddress` and `localPort` fields to `SocketConfig` and implemented parsing from JavaScript options - **`Listener.zig`**: Updated connection structures to store and pass local binding information - **`socket.zig` (bun.js/api/bun)**: Modified `doConnect()` to extract and pass local address options - Updated all other call sites (HTTP, MySQL, PostgreSQL, Valkey) to pass `null, 0` for backward compatibility ### JavaScript Layer - **`net.ts`**: Enabled `localAddress` and `localPort` support by passing these options to `doConnect()` and removing TODO comments ### Tests - **`06888-localaddress.test.ts`**: Added comprehensive tests covering: - IPv4 local address binding - IPv4 local address and port binding - IPv6 local address binding (loopback) - Backward compatibility (connections without local address) ## Test Results All tests pass successfully: ``` ✓ TCP socket can bind to localAddress - IPv4 ✓ TCP socket can bind to localAddress and localPort - IPv4 ✓ TCP socket can bind to localAddress - IPv6 loopback ✓ TCP socket without localAddress works normally 4 pass, 0 fail ``` ## API Usage ```typescript import net from "net"; // Connect with a specific local address const client = net.createConnection({ host: "example.com", port: 80, localAddress: "192.168.1.100", // Bind to this local IP localPort: 0, // Let system assign port (optional) }); ``` ## Implementation Details The implementation follows the same flow as Node.js: 1. JavaScript options are parsed in `Handlers.zig` 2. Local address/port are stored in the connection configuration 3. The Zig layer processes and passes them to the C layer 4. The C layer parses the local address and calls `bind()` before `connect()` 5. Both IPv4 and IPv6 addresses are supported Memory management is handled properly throughout the stack, with appropriate allocation/deallocation at each layer. Closes #6888 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1467,12 +1467,39 @@ static int is_loopback(struct sockaddr_storage *sockaddr) {
|
||||
}
|
||||
#endif
|
||||
|
||||
LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket(struct sockaddr_storage *addr, int options) {
|
||||
LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket(struct sockaddr_storage *addr, int options, struct sockaddr_storage *local_addr) {
|
||||
LIBUS_SOCKET_DESCRIPTOR fd = bsd_create_socket(addr->ss_family, SOCK_STREAM, 0, NULL);
|
||||
if (fd == LIBUS_SOCKET_ERROR) {
|
||||
return LIBUS_SOCKET_ERROR;
|
||||
}
|
||||
|
||||
// Bind to local address if specified
|
||||
if (local_addr != NULL) {
|
||||
socklen_t addr_len;
|
||||
if (local_addr->ss_family == AF_INET) {
|
||||
addr_len = sizeof(struct sockaddr_in);
|
||||
} else if (local_addr->ss_family == AF_INET6) {
|
||||
addr_len = sizeof(struct sockaddr_in6);
|
||||
} else {
|
||||
bsd_close_socket(fd);
|
||||
#ifdef _WIN32
|
||||
WSASetLastError(WSAEAFNOSUPPORT);
|
||||
#endif
|
||||
errno = EAFNOSUPPORT;
|
||||
return LIBUS_SOCKET_ERROR;
|
||||
}
|
||||
|
||||
int bind_result;
|
||||
do {
|
||||
bind_result = bind(fd, (struct sockaddr *)local_addr, addr_len);
|
||||
} while (IS_EINTR(bind_result));
|
||||
|
||||
if (bind_result != 0) {
|
||||
bsd_close_socket(fd);
|
||||
return LIBUS_SOCKET_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
win32_set_nonblocking(fd);
|
||||
|
||||
|
||||
@@ -430,8 +430,8 @@ struct us_listen_socket_t *us_socket_context_listen_unix(int ssl, struct us_sock
|
||||
return ls;
|
||||
}
|
||||
|
||||
struct us_socket_t* us_socket_context_connect_resolved_dns(struct us_socket_context_t *context, struct sockaddr_storage* addr, int options, int socket_ext_size) {
|
||||
LIBUS_SOCKET_DESCRIPTOR connect_socket_fd = bsd_create_connect_socket(addr, options);
|
||||
struct us_socket_t* us_socket_context_connect_resolved_dns(struct us_socket_context_t *context, struct sockaddr_storage* addr, int options, int socket_ext_size, struct sockaddr_storage* local_addr) {
|
||||
LIBUS_SOCKET_DESCRIPTOR connect_socket_fd = bsd_create_connect_socket(addr, options, local_addr);
|
||||
if (connect_socket_fd == LIBUS_SOCKET_ERROR) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -501,20 +501,39 @@ static bool try_parse_ip(const char *ip_str, int port, struct sockaddr_storage *
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, const char *host, int port, int options, int socket_ext_size, int* has_dns_resolved) {
|
||||
/* Helper function to parse local address for binding */
|
||||
static struct sockaddr_storage *parse_local_address(const char *local_host, unsigned short local_port, struct sockaddr_storage *storage) {
|
||||
if (local_host == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (try_parse_ip(local_host, local_port, storage)) {
|
||||
return storage;
|
||||
}
|
||||
errno = EINVAL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, const char *host, int port, const char *local_host, unsigned short local_port, int options, int socket_ext_size, int* has_dns_resolved) {
|
||||
#ifndef LIBUS_NO_SSL
|
||||
if (ssl == 1) {
|
||||
return us_internal_ssl_socket_context_connect((struct us_internal_ssl_socket_context_t *) context, host, port, options, socket_ext_size, has_dns_resolved);
|
||||
return us_internal_ssl_socket_context_connect((struct us_internal_ssl_socket_context_t *) context, host, port, local_host, local_port, options, socket_ext_size, has_dns_resolved);
|
||||
}
|
||||
#endif
|
||||
|
||||
struct us_loop_t* loop = us_socket_context_loop(ssl, context);
|
||||
|
||||
// Parse local address if provided
|
||||
struct sockaddr_storage local_addr_storage;
|
||||
struct sockaddr_storage *local_addr = parse_local_address(local_host, local_port, &local_addr_storage);
|
||||
if (local_host != NULL && local_addr == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// fast path for IP addresses in text form
|
||||
struct sockaddr_storage addr;
|
||||
if (try_parse_ip(host, port, &addr)) {
|
||||
*has_dns_resolved = 1;
|
||||
return us_socket_context_connect_resolved_dns(context, &addr, options, socket_ext_size);
|
||||
return us_socket_context_connect_resolved_dns(context, &addr, options, socket_ext_size, local_addr);
|
||||
}
|
||||
|
||||
struct addrinfo_request* ai_req;
|
||||
@@ -534,7 +553,7 @@ void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, co
|
||||
struct sockaddr_storage addr;
|
||||
init_addr_with_port(&entries->info, port, &addr);
|
||||
*has_dns_resolved = 1;
|
||||
struct us_socket_t *s = us_socket_context_connect_resolved_dns(context, &addr, options, socket_ext_size);
|
||||
struct us_socket_t *s = us_socket_context_connect_resolved_dns(context, &addr, options, socket_ext_size, local_addr);
|
||||
Bun__addrinfo_freeRequest(ai_req, s == NULL);
|
||||
return s;
|
||||
}
|
||||
@@ -548,6 +567,9 @@ void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, co
|
||||
c->long_timeout = 255;
|
||||
c->pending_resolve_callback = 1;
|
||||
c->port = port;
|
||||
// Duplicate local_host string so it persists after Zig frees its buffer
|
||||
c->local_host = local_host ? strdup(local_host) : NULL;
|
||||
c->local_port = local_port;
|
||||
us_internal_socket_context_link_connecting_socket(ssl, context, c);
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -562,11 +584,15 @@ void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, co
|
||||
}
|
||||
|
||||
int start_connections(struct us_connecting_socket_t *c, int count) {
|
||||
// Parse local address if provided
|
||||
struct sockaddr_storage local_addr_storage;
|
||||
struct sockaddr_storage *local_addr = parse_local_address(c->local_host, c->local_port, &local_addr_storage);
|
||||
|
||||
int opened = 0;
|
||||
for (; c->addrinfo_head != NULL && opened < count; c->addrinfo_head = c->addrinfo_head->ai_next) {
|
||||
struct sockaddr_storage addr;
|
||||
init_addr_with_port(c->addrinfo_head, c->port, &addr);
|
||||
LIBUS_SOCKET_DESCRIPTOR connect_socket_fd = bsd_create_connect_socket(&addr, c->options);
|
||||
LIBUS_SOCKET_DESCRIPTOR connect_socket_fd = bsd_create_connect_socket(&addr, c->options, local_addr);
|
||||
if (connect_socket_fd == LIBUS_SOCKET_ERROR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1608,9 +1608,9 @@ static void us_internal_zero_ssl_data_for_connected_socket_before_onopen(struct
|
||||
// TODO does this need more changes?
|
||||
struct us_socket_t *us_internal_ssl_socket_context_connect(
|
||||
struct us_internal_ssl_socket_context_t *context, const char *host,
|
||||
int port, int options, int socket_ext_size, int* is_connecting) {
|
||||
int port, const char *local_host, unsigned short local_port, int options, int socket_ext_size, int* is_connecting) {
|
||||
struct us_internal_ssl_socket_t *s = (struct us_internal_ssl_socket_t *)us_socket_context_connect(
|
||||
2, &context->sc, host, port, options,
|
||||
2, &context->sc, host, port, local_host, local_port, options,
|
||||
sizeof(struct us_internal_ssl_socket_t) - sizeof(struct us_socket_t) +
|
||||
socket_ext_size, is_connecting);
|
||||
if (*is_connecting && s) {
|
||||
|
||||
@@ -202,6 +202,9 @@ struct us_connecting_socket_t {
|
||||
// this is used to track pending connecting sockets in the context
|
||||
struct us_connecting_socket_t* next_pending;
|
||||
struct us_connecting_socket_t* prev_pending;
|
||||
// local address binding
|
||||
const char *local_host;
|
||||
unsigned short local_port;
|
||||
};
|
||||
|
||||
struct us_wrapped_socket_context_t {
|
||||
@@ -410,7 +413,7 @@ struct us_listen_socket_t *us_internal_ssl_socket_context_listen_unix(
|
||||
|
||||
struct us_socket_t *us_internal_ssl_socket_context_connect(
|
||||
us_internal_ssl_socket_context_r context, const char *host,
|
||||
int port, int options, int socket_ext_size, int* is_resolved);
|
||||
int port, const char *local_host, unsigned short local_port, int options, int socket_ext_size, int* is_resolved);
|
||||
|
||||
struct us_socket_t *us_internal_ssl_socket_context_connect_unix(
|
||||
us_internal_ssl_socket_context_r context, const char *server_path,
|
||||
|
||||
@@ -228,7 +228,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op
|
||||
int bsd_connect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd, const char *host, int port);
|
||||
int bsd_disconnect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd);
|
||||
|
||||
LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket(struct sockaddr_storage *addr, int options);
|
||||
LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket(struct sockaddr_storage *addr, int options, struct sockaddr_storage *local_addr);
|
||||
|
||||
LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket_unix(const char *server_path, size_t pathlen, int options);
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ void us_listen_socket_close(int ssl, struct us_listen_socket_t *ls) nonnull_fn_d
|
||||
per the happy eyeballs algorithm
|
||||
*/
|
||||
void *us_socket_context_connect(int ssl, struct us_socket_context_t * nonnull_arg context,
|
||||
const char *host, int port, int options, int socket_ext_size, int *is_connecting) __attribute__((nonnull(2)));
|
||||
const char *host, int port, const char *local_host, unsigned short local_port, int options, int socket_ext_size, int *is_connecting) __attribute__((nonnull(2)));
|
||||
|
||||
struct us_socket_t *us_socket_context_connect_unix(int ssl, us_socket_context_r context,
|
||||
const char *server_path, size_t pathlen, int options, int socket_ext_size) __attribute__((nonnull(2)));
|
||||
|
||||
@@ -142,6 +142,12 @@ void us_connecting_socket_free(int ssl, struct us_connecting_socket_t *c) {
|
||||
// instead, we move it to a close list and free it after the iteration
|
||||
us_internal_socket_context_unlink_connecting_socket(ssl, c->context, c);
|
||||
|
||||
// Free duplicated local_host string if present
|
||||
if (c->local_host) {
|
||||
free((void*)c->local_host);
|
||||
c->local_host = NULL;
|
||||
}
|
||||
|
||||
c->next = c->context->loop->data.closed_connecting_head;
|
||||
c->context->loop->data.closed_connecting_head = c;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ pub fn NewSocket(comptime ssl: bool) type {
|
||||
this.socket_context.?,
|
||||
this,
|
||||
this.flags.allow_half_open,
|
||||
c.local_host,
|
||||
@as(i32, @intCast(c.local_port)),
|
||||
);
|
||||
},
|
||||
.unix => |u| {
|
||||
|
||||
@@ -217,6 +217,8 @@ pub const SocketConfig = struct {
|
||||
allowHalfOpen: bool = false,
|
||||
reusePort: bool = false,
|
||||
ipv6Only: bool = false,
|
||||
localAddress: ?jsc.ZigString.Slice = null,
|
||||
localPort: ?u16 = null,
|
||||
|
||||
pub fn socketFlags(this: *const SocketConfig) i32 {
|
||||
var flags: i32 = if (this.exclusive)
|
||||
@@ -249,6 +251,11 @@ pub const SocketConfig = struct {
|
||||
var ssl: ?SSLConfig = null;
|
||||
var default_data = JSValue.zero;
|
||||
|
||||
// Parse localAddress and localPort for bind address
|
||||
var localAddress: ?jsc.ZigString.Slice = null;
|
||||
errdefer if (localAddress) |la| la.deinit();
|
||||
var localPort: ?u16 = null;
|
||||
|
||||
if (try opts.getTruthy(globalObject, "tls")) |tls| {
|
||||
if (!tls.isBoolean()) {
|
||||
ssl = try SSLConfig.fromJS(vm, globalObject, tls);
|
||||
@@ -301,6 +308,21 @@ pub const SocketConfig = struct {
|
||||
ipv6Only = ipv6_only;
|
||||
}
|
||||
|
||||
if (try opts.getStringish(globalObject, "localAddress")) |local_addr| {
|
||||
defer local_addr.deref();
|
||||
localAddress = try local_addr.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator);
|
||||
}
|
||||
|
||||
if (try opts.get(globalObject, "localPort")) |local_port_value| {
|
||||
if (!local_port_value.isEmptyOrUndefinedOrNull()) {
|
||||
const local_port_i32 = try local_port_value.coerceToInt32(globalObject);
|
||||
if (local_port_i32 < 0 or local_port_i32 > 65535) {
|
||||
return globalObject.throwInvalidArguments("Expected \"localPort\" to be a number between 0 and 65535", .{});
|
||||
}
|
||||
localPort = @intCast(local_port_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if (try opts.getStringish(globalObject, "hostname") orelse try opts.getStringish(globalObject, "host")) |hostname| {
|
||||
defer hostname.deref();
|
||||
|
||||
@@ -366,6 +388,8 @@ pub const SocketConfig = struct {
|
||||
.allowHalfOpen = allowHalfOpen,
|
||||
.reusePort = reusePort,
|
||||
.ipv6Only = ipv6Only,
|
||||
.localAddress = localAddress,
|
||||
.localPort = localPort,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,6 +40,8 @@ pub const UnixOrHost = union(enum) {
|
||||
host: struct {
|
||||
host: []const u8,
|
||||
port: u16,
|
||||
local_host: ?[]const u8 = null,
|
||||
local_port: u16 = 0,
|
||||
},
|
||||
fd: bun.FileDescriptor,
|
||||
|
||||
@@ -55,6 +57,8 @@ pub const UnixOrHost = union(enum) {
|
||||
.host = .{
|
||||
.host = bun.handleOom(bun.default_allocator.dupe(u8, h.host)),
|
||||
.port = this.host.port,
|
||||
.local_host = if (h.local_host) |lh| bun.handleOom(bun.default_allocator.dupe(u8, lh)) else null,
|
||||
.local_port = h.local_port,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -69,6 +73,9 @@ pub const UnixOrHost = union(enum) {
|
||||
},
|
||||
.host => |h| {
|
||||
bun.default_allocator.free(h.host);
|
||||
if (h.local_host) |lh| {
|
||||
bun.default_allocator.free(lh);
|
||||
}
|
||||
},
|
||||
.fd => {}, // this is an integer
|
||||
}
|
||||
@@ -584,7 +591,12 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock
|
||||
}
|
||||
}
|
||||
if (port) |_| {
|
||||
break :blk .{ .host = .{ .host = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice(), .port = port.? } };
|
||||
break :blk .{ .host = .{
|
||||
.host = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice(),
|
||||
.port = port.?,
|
||||
.local_host = if (socket_config.localAddress) |la| bun.handleOom(la.cloneIfNeeded(bun.default_allocator)).slice() else null,
|
||||
.local_port = socket_config.localPort orelse 0,
|
||||
} };
|
||||
}
|
||||
|
||||
break :blk .{ .unix = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice() };
|
||||
|
||||
@@ -196,8 +196,8 @@ pub const SocketContext = opaque {
|
||||
return c.us_socket_context_adopt_socket(@intFromBool(ssl), this, s, ext_size);
|
||||
}
|
||||
|
||||
pub fn connect(this: *SocketContext, ssl: bool, host: [*:0]const u8, port: i32, options: i32, socket_ext_size: i32, has_dns_resolved: *i32) ?*anyopaque {
|
||||
return c.us_socket_context_connect(@intFromBool(ssl), this, host, port, options, socket_ext_size, has_dns_resolved);
|
||||
pub fn connect(this: *SocketContext, ssl: bool, host: [*:0]const u8, port: i32, local_host: ?[*:0]const u8, local_port: i32, options: i32, socket_ext_size: i32, has_dns_resolved: *i32) ?*anyopaque {
|
||||
return c.us_socket_context_connect(@intFromBool(ssl), this, host, port, local_host, local_port, options, socket_ext_size, has_dns_resolved);
|
||||
}
|
||||
|
||||
pub fn connectUnix(this: *SocketContext, ssl: bool, path: [:0]const u8, options: i32, socket_ext_size: i32) ?*us_socket_t {
|
||||
@@ -254,7 +254,7 @@ pub const c = struct {
|
||||
pub extern fn us_create_child_socket_context(ssl: i32, context: ?*SocketContext, context_ext_size: i32) ?*SocketContext;
|
||||
pub extern fn us_socket_context_adopt_socket(ssl: i32, context: *SocketContext, s: *us_socket_t, ext_size: i32) ?*us_socket_t;
|
||||
pub extern fn us_socket_context_close(ssl: i32, ctx: *anyopaque) void;
|
||||
pub extern fn us_socket_context_connect(ssl: i32, context: *SocketContext, host: [*:0]const u8, port: i32, options: i32, socket_ext_size: i32, has_dns_resolved: *i32) ?*anyopaque;
|
||||
pub extern fn us_socket_context_connect(ssl: i32, context: *SocketContext, host: [*:0]const u8, port: i32, local_host: ?[*:0]const u8, local_port: i32, options: i32, socket_ext_size: i32, has_dns_resolved: *i32) ?*anyopaque;
|
||||
pub extern fn us_socket_context_connect_unix(ssl: i32, context: *SocketContext, path: [*:0]const u8, pathlen: usize, options: i32, socket_ext_size: i32) ?*us_socket_t;
|
||||
pub extern fn us_socket_context_ext(ssl: i32, context: *SocketContext) ?*anyopaque;
|
||||
pub extern fn us_socket_context_free(ssl: i32, context: *SocketContext) void;
|
||||
|
||||
@@ -507,7 +507,7 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type {
|
||||
comptime socket_field_name: []const u8,
|
||||
allowHalfOpen: bool,
|
||||
) !*Context {
|
||||
const this_socket = try connectAnon(host, port, socket_ctx, ctx, allowHalfOpen);
|
||||
const this_socket = try connectAnon(host, port, socket_ctx, ctx, allowHalfOpen, null, 0);
|
||||
@field(ctx, socket_field_name) = this_socket;
|
||||
return ctx;
|
||||
}
|
||||
@@ -597,9 +597,11 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type {
|
||||
socket_ctx: *SocketContext,
|
||||
ptr: *anyopaque,
|
||||
allowHalfOpen: bool,
|
||||
local_host: ?[]const u8,
|
||||
local_port: i32,
|
||||
) !ThisSocket {
|
||||
debug("connect({s}, {d})", .{ raw_host, port });
|
||||
var stack_fallback = std.heap.stackFallback(1024, bun.default_allocator);
|
||||
var stack_fallback = std.heap.stackFallback(2048, bun.default_allocator);
|
||||
var allocator = stack_fallback.get();
|
||||
|
||||
// remove brackets from IPv6 addresses, as getaddrinfo doesn't understand them
|
||||
@@ -611,11 +613,28 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type {
|
||||
const host = bun.handleOom(allocator.dupeZ(u8, clean_host));
|
||||
defer allocator.free(host);
|
||||
|
||||
// Handle local_host parameter - also remove brackets from IPv6 addresses
|
||||
var local_host_z: ?[*:0]const u8 = null;
|
||||
var local_host_buf: ?[]u8 = null;
|
||||
defer if (local_host_buf) |buf| allocator.free(buf);
|
||||
|
||||
if (local_host) |lh| {
|
||||
const clean_local_host = if (lh.len > 1 and lh[0] == '[' and lh[lh.len - 1] == ']')
|
||||
lh[1 .. lh.len - 1]
|
||||
else
|
||||
lh;
|
||||
const lh_z = bun.handleOom(allocator.dupeZ(u8, clean_local_host));
|
||||
local_host_buf = lh_z;
|
||||
local_host_z = lh_z.ptr;
|
||||
}
|
||||
|
||||
var did_dns_resolve: i32 = 0;
|
||||
const socket_ptr = socket_ctx.connect(
|
||||
is_ssl,
|
||||
host.ptr,
|
||||
port,
|
||||
local_host_z,
|
||||
local_port,
|
||||
if (allowHalfOpen) uws.LIBUS_SOCKET_ALLOW_HALF_OPEN else 0,
|
||||
@sizeOf(*anyopaque),
|
||||
&did_dns_resolve,
|
||||
|
||||
@@ -473,6 +473,8 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
|
||||
this.us_socket_context,
|
||||
ActiveSocket.init(client).ptr(),
|
||||
false,
|
||||
null,
|
||||
0,
|
||||
);
|
||||
client.allow_retry = false;
|
||||
return socket;
|
||||
|
||||
@@ -653,6 +653,8 @@ function kConnectTcp(self, addressType, req, address, port) {
|
||||
ipv6Only: addressType === 6,
|
||||
allowHalfOpen: self.allowHalfOpen,
|
||||
tls: req.tls,
|
||||
localAddress: req.localAddress,
|
||||
localPort: req.localPort,
|
||||
data: { self, req },
|
||||
socket: self[khandlers],
|
||||
});
|
||||
@@ -1718,13 +1720,9 @@ function internalConnect(self, options, address, port, addressType, localAddress
|
||||
if (localAddress || localPort) {
|
||||
if (addressType === 4) {
|
||||
localAddress ||= "0.0.0.0";
|
||||
// TODO:
|
||||
// err = self._handle.bind(localAddress, localPort);
|
||||
} else {
|
||||
// addressType === 6
|
||||
localAddress ||= "::";
|
||||
// TODO:
|
||||
// err = self._handle.bind6(localAddress, localPort, flags);
|
||||
}
|
||||
$debug(
|
||||
"connect: binding to localAddress: %s and localPort: %d (addressType: %d)",
|
||||
@@ -1732,13 +1730,6 @@ function internalConnect(self, options, address, port, addressType, localAddress
|
||||
localPort,
|
||||
addressType,
|
||||
);
|
||||
|
||||
err = checkBindError(err, localPort, self._handle);
|
||||
if (err) {
|
||||
const ex = new ExceptionWithHostPort(err, "bind", localAddress, localPort);
|
||||
self.destroy(ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//TLS
|
||||
@@ -1846,13 +1837,9 @@ function internalConnectMultiple(context, canceled?) {
|
||||
if (localPort) {
|
||||
if (addressType === 4) {
|
||||
localAddress = DEFAULT_IPV4_ADDR;
|
||||
// TODO:
|
||||
// err = self._handle.bind(localAddress, localPort);
|
||||
} else {
|
||||
// addressType === 6
|
||||
localAddress = DEFAULT_IPV6_ADDR;
|
||||
// TODO:
|
||||
// err = self._handle.bind6(localAddress, localPort, flags);
|
||||
}
|
||||
|
||||
$debug(
|
||||
@@ -1861,13 +1848,6 @@ function internalConnectMultiple(context, canceled?) {
|
||||
localPort,
|
||||
addressType,
|
||||
);
|
||||
|
||||
err = checkBindError(err, localPort, self._handle);
|
||||
if (err) {
|
||||
ArrayPrototypePush.$call(context.errors, new ExceptionWithHostPort(err, "bind", localAddress, localPort));
|
||||
internalConnectMultiple(context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.blockList?.check(address, `ipv${addressType}`)) {
|
||||
|
||||
@@ -478,7 +478,7 @@ pub fn createInstance(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFra
|
||||
});
|
||||
} else {
|
||||
ptr.#connection.setSocket(.{
|
||||
.SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false) catch |err| {
|
||||
.SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false, null, 0) catch |err| {
|
||||
ptr.deref();
|
||||
return globalObject.throwError(err, "failed to connect to mysql");
|
||||
},
|
||||
|
||||
@@ -750,7 +750,7 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS
|
||||
};
|
||||
} else {
|
||||
ptr.socket = .{
|
||||
.SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false) catch |err| {
|
||||
.SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false, null, 0) catch |err| {
|
||||
tls_config.deinit();
|
||||
if (tls_ctx) |tls| {
|
||||
tls.deinit(true);
|
||||
|
||||
@@ -158,6 +158,8 @@ pub const Address = union(enum) {
|
||||
ctx,
|
||||
client,
|
||||
false,
|
||||
null,
|
||||
0,
|
||||
));
|
||||
},
|
||||
}
|
||||
|
||||
200
test/regression/issue/06888-localaddress.test.ts
Normal file
200
test/regression/issue/06888-localaddress.test.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import * as net from "net";
|
||||
import * as os from "os";
|
||||
|
||||
test("TCP socket can bind to localAddress - IPv4", async () => {
|
||||
// Get a local IPv4 address
|
||||
const interfaces = os.networkInterfaces();
|
||||
let localIPv4: string | undefined;
|
||||
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
const iface = interfaces[name];
|
||||
if (!iface) continue;
|
||||
|
||||
for (const addr of iface) {
|
||||
if (addr.family === "IPv4" && !addr.internal) {
|
||||
localIPv4 = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (localIPv4) break;
|
||||
}
|
||||
|
||||
// Skip test if no non-loopback IPv4 address found
|
||||
if (!localIPv4) {
|
||||
console.log("No non-loopback IPv4 address found, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
const server = net.createServer(socket => {
|
||||
const remoteAddr = socket.remoteAddress;
|
||||
socket.end(`Connected from ${remoteAddr}`);
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
server.listen(0, localIPv4, () => resolve());
|
||||
});
|
||||
|
||||
const serverPort = (server.address() as net.AddressInfo).port;
|
||||
|
||||
const clientPromise = new Promise<string>((resolve, reject) => {
|
||||
const client = net.createConnection({
|
||||
host: localIPv4,
|
||||
port: serverPort,
|
||||
localAddress: localIPv4,
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
expect(client.localAddress).toBe(localIPv4);
|
||||
});
|
||||
|
||||
let data = "";
|
||||
client.on("data", chunk => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
client.on("end", () => {
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
client.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const response = await clientPromise;
|
||||
expect(response).toContain("Connected from");
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
test("TCP socket can bind to localAddress and localPort - IPv4", async () => {
|
||||
const server = net.createServer(socket => {
|
||||
const remoteAddr = socket.remoteAddress;
|
||||
const remotePort = socket.remotePort;
|
||||
socket.end(`Connected from ${remoteAddr}:${remotePort}`);
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
|
||||
const serverPort = (server.address() as net.AddressInfo).port;
|
||||
|
||||
// Use a local port of 0 to let the system assign one
|
||||
const clientPromise = new Promise<{ data: string; localPort: number }>((resolve, reject) => {
|
||||
const client = net.createConnection({
|
||||
host: "127.0.0.1",
|
||||
port: serverPort,
|
||||
localAddress: "127.0.0.1",
|
||||
localPort: 0,
|
||||
});
|
||||
|
||||
let localPort: number;
|
||||
client.on("connect", () => {
|
||||
expect(client.localAddress).toBe("127.0.0.1");
|
||||
localPort = client.localPort!;
|
||||
expect(localPort).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
let data = "";
|
||||
client.on("data", chunk => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
client.on("end", () => {
|
||||
resolve({ data, localPort });
|
||||
});
|
||||
|
||||
client.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const { data, localPort } = await clientPromise;
|
||||
expect(data).toContain("Connected from");
|
||||
expect(data).toContain(`:${localPort}`);
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
test("TCP socket can bind to localAddress - IPv6 loopback", async () => {
|
||||
const server = net.createServer(socket => {
|
||||
const remoteAddr = socket.remoteAddress;
|
||||
socket.end(`Connected from ${remoteAddr}`);
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
server.listen(0, "::1", () => resolve());
|
||||
});
|
||||
|
||||
const serverPort = (server.address() as net.AddressInfo).port;
|
||||
|
||||
const clientPromise = new Promise<string>((resolve, reject) => {
|
||||
const client = net.createConnection({
|
||||
host: "::1",
|
||||
port: serverPort,
|
||||
localAddress: "::1",
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
// IPv6 addresses might be normalized differently
|
||||
expect(client.localAddress).toContain(":");
|
||||
});
|
||||
|
||||
let data = "";
|
||||
client.on("data", chunk => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
client.on("end", () => {
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
client.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const response = await clientPromise;
|
||||
expect(response).toContain("Connected from");
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
test("TCP socket without localAddress works normally", async () => {
|
||||
const server = net.createServer(socket => {
|
||||
socket.end("Hello");
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
|
||||
const serverPort = (server.address() as net.AddressInfo).port;
|
||||
|
||||
const clientPromise = new Promise<string>((resolve, reject) => {
|
||||
const client = net.createConnection({
|
||||
host: "127.0.0.1",
|
||||
port: serverPort,
|
||||
});
|
||||
|
||||
let data = "";
|
||||
client.on("data", chunk => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
client.on("end", () => {
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
client.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const response = await clientPromise;
|
||||
expect(response).toBe("Hello");
|
||||
|
||||
server.close();
|
||||
});
|
||||
Reference in New Issue
Block a user