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:
robobun
2025-10-11 20:54:30 -07:00
committed by GitHub
parent 01924e9993
commit 5bdc32265d
18 changed files with 346 additions and 43 deletions

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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)));

View File

@@ -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;
}

View File

@@ -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| {

View File

@@ -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,
};
}
};

View File

@@ -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() };

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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}`)) {

View File

@@ -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");
},

View File

@@ -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);

View File

@@ -158,6 +158,8 @@ pub const Address = union(enum) {
ctx,
client,
false,
null,
0,
));
},
}

View 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();
});