diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index 732d79559a..58f4aa1c96 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -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); diff --git a/packages/bun-usockets/src/context.c b/packages/bun-usockets/src/context.c index 6e2c3f3e18..b7c88f4d70 100644 --- a/packages/bun-usockets/src/context.c +++ b/packages/bun-usockets/src/context.c @@ -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; } diff --git a/packages/bun-usockets/src/crypto/openssl.c b/packages/bun-usockets/src/crypto/openssl.c index 60e1900da5..d0cd6e8c05 100644 --- a/packages/bun-usockets/src/crypto/openssl.c +++ b/packages/bun-usockets/src/crypto/openssl.c @@ -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) { diff --git a/packages/bun-usockets/src/internal/internal.h b/packages/bun-usockets/src/internal/internal.h index 7ee718e723..dff2ca30f0 100644 --- a/packages/bun-usockets/src/internal/internal.h +++ b/packages/bun-usockets/src/internal/internal.h @@ -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, diff --git a/packages/bun-usockets/src/internal/networking/bsd.h b/packages/bun-usockets/src/internal/networking/bsd.h index 699aeffa92..32b2c67a9a 100644 --- a/packages/bun-usockets/src/internal/networking/bsd.h +++ b/packages/bun-usockets/src/internal/networking/bsd.h @@ -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); diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index 0e746a0388..d185d81831 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -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))); diff --git a/packages/bun-usockets/src/socket.c b/packages/bun-usockets/src/socket.c index a4b02a7f42..e3a5f1c64e 100644 --- a/packages/bun-usockets/src/socket.c +++ b/packages/bun-usockets/src/socket.c @@ -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; } diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 3da90d798b..d10ec27927 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -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| { diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index b874d9f4f2..0385e0e144 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -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, }; } }; diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 84afd1b5e8..00c1200306 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -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() }; diff --git a/src/deps/uws/SocketContext.zig b/src/deps/uws/SocketContext.zig index 2672402e7e..d1b1d64277 100644 --- a/src/deps/uws/SocketContext.zig +++ b/src/deps/uws/SocketContext.zig @@ -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; diff --git a/src/deps/uws/socket.zig b/src/deps/uws/socket.zig index d4f81b8f77..c686437819 100644 --- a/src/deps/uws/socket.zig +++ b/src/deps/uws/socket.zig @@ -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, diff --git a/src/http/HTTPContext.zig b/src/http/HTTPContext.zig index a34020012e..82748021db 100644 --- a/src/http/HTTPContext.zig +++ b/src/http/HTTPContext.zig @@ -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; diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 16b65d6cee..16cbf3afc2 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -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}`)) { diff --git a/src/sql/mysql/js/JSMySQLConnection.zig b/src/sql/mysql/js/JSMySQLConnection.zig index 92b31c1b5d..4540514c61 100644 --- a/src/sql/mysql/js/JSMySQLConnection.zig +++ b/src/sql/mysql/js/JSMySQLConnection.zig @@ -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"); }, diff --git a/src/sql/postgres/PostgresSQLConnection.zig b/src/sql/postgres/PostgresSQLConnection.zig index e176281713..712f9b0da3 100644 --- a/src/sql/postgres/PostgresSQLConnection.zig +++ b/src/sql/postgres/PostgresSQLConnection.zig @@ -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); diff --git a/src/valkey/valkey.zig b/src/valkey/valkey.zig index dbce0e4699..b553644142 100644 --- a/src/valkey/valkey.zig +++ b/src/valkey/valkey.zig @@ -158,6 +158,8 @@ pub const Address = union(enum) { ctx, client, false, + null, + 0, )); }, } diff --git a/test/regression/issue/06888-localaddress.test.ts b/test/regression/issue/06888-localaddress.test.ts new file mode 100644 index 0000000000..6160ce2946 --- /dev/null +++ b/test/regression/issue/06888-localaddress.test.ts @@ -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(resolve => { + server.listen(0, localIPv4, () => resolve()); + }); + + const serverPort = (server.address() as net.AddressInfo).port; + + const clientPromise = new Promise((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(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(resolve => { + server.listen(0, "::1", () => resolve()); + }); + + const serverPort = (server.address() as net.AddressInfo).port; + + const clientPromise = new Promise((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(resolve => { + server.listen(0, "127.0.0.1", () => resolve()); + }); + + const serverPort = (server.address() as net.AddressInfo).port; + + const clientPromise = new Promise((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(); +});