Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
d0aeda72a0 WIP 2025-09-08 22:10:32 +00:00
Claude Bot
c912690e5b Fix systemd-resolved DNS backend to keep process alive
The main issue was that DNS callbacks were being invoked directly from the socket handler context, which wasn't on the JS thread. This caused the poll ref management to not work correctly, leading to the process exiting before DNS requests completed.

Changes:
- Schedule DNS callbacks to run on the JS thread using WorkTask
- Store result data in CallbackContext and process it on JS thread
- This ensures proper poll ref cleanup and keeps the process alive during async DNS operations
- All DNS callbacks now properly call requestCompleted() which manages the resolver's timer

The fix follows the same pattern used by other DNS backends like LibInfo, ensuring callbacks happen in the correct thread context for proper resource management.

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-08 11:25:59 +00:00
Claude Bot
8ce601b171 “wip” 2025-09-08 05:13:04 +00:00
7 changed files with 1611 additions and 2 deletions

View File

@@ -127,6 +127,481 @@ const LibInfo = struct {
}
};
const SystemdResolvedConnection = struct {
const SOCKET_PATH = "/run/systemd/resolve/io.systemd.Resolve";
const log = Output.scoped(.SystemdResolved, .visible);
socket: ?uws.NewSocketHandler(false) = null,
socket_context: ?*uws.SocketContext = null,
vm: *jsc.VirtualMachine,
read_buffer: bun.collections.OffsetList(u8),
write_buffer: std.ArrayList(u8),
// Queue of pending requests
pending_requests: std.ArrayList(*GetAddrInfoRequest),
current_request: ?*GetAddrInfoRequest = null,
flags: packed struct {
connected: bool = false,
connecting: bool = false,
has_backpressure: bool = false,
} = .{},
pub fn init(vm: *jsc.VirtualMachine) !*SystemdResolvedConnection {
const allocator = vm.allocator;
const this = try allocator.create(SystemdResolvedConnection);
this.* = .{
.vm = vm,
.read_buffer = .{},
.write_buffer = std.ArrayList(u8).init(allocator),
.pending_requests = std.ArrayList(*GetAddrInfoRequest).init(allocator),
};
return this;
}
pub fn connect(this: *SystemdResolvedConnection) !void {
if (this.flags.connected or this.flags.connecting) return;
log("Connecting to systemd-resolved at {s}", .{SOCKET_PATH});
this.flags.connecting = true;
const ctx = this.socket_context orelse brk: {
log("Creating new socket context", .{});
const ctx_ = uws.SocketContext.createNoSSLContext(this.vm.uwsLoop(), @sizeOf(*SystemdResolvedConnection)) orelse {
log("Failed to create socket context", .{});
this.flags.connecting = false;
return error.SocketContextFailed;
};
uws.NewSocketHandler(false).configure(ctx_, true, *SystemdResolvedConnection, SocketHandler(false));
this.socket_context = ctx_;
break :brk ctx_;
};
log("Attempting to connect to Unix socket", .{});
this.socket = uws.NewSocketHandler(false).connectUnixAnon(
SOCKET_PATH,
ctx,
this,
false,
) catch |err| {
log("Failed to connect to Unix socket: {}", .{err});
this.flags.connecting = false;
return err;
};
log("Socket connection initiated", .{});
}
pub fn sendRequest(this: *SystemdResolvedConnection, request: *GetAddrInfoRequest) !void {
try this.pending_requests.append(request);
if (!this.flags.connected) {
if (!this.flags.connecting) {
try this.connect();
}
return;
}
this.processNextRequest();
}
fn processNextRequest(this: *SystemdResolvedConnection) void {
if (this.current_request != null) return;
if (this.pending_requests.items.len == 0) return;
if (this.flags.has_backpressure) return;
const request = this.pending_requests.orderedRemove(0);
this.current_request = request;
const query = switch (request.backend) {
.systemd_resolved => |q| q,
else => unreachable,
};
const family: i32 = switch (query.options.family) {
.unspecified => 0, // AF_UNSPEC
.inet => 2, // AF_INET
.inet6 => 10, // AF_INET6
.unix => 0, // Unix sockets not relevant for DNS, use AF_UNSPEC
};
// Build Varlink request
this.write_buffer.clearRetainingCapacity();
const writer = this.write_buffer.writer();
std.fmt.format(writer,
\\{{"method":"io.systemd.Resolve.ResolveHostname","parameters":{{"name":"{s}","family":{d}}}}}
, .{ query.name, family }) catch return;
this.write_buffer.append(0) catch return; // null terminator
log("Sending request for domain: {s} (family={})", .{query.name, family});
this.flushData();
}
fn flushData(this: *SystemdResolvedConnection) void {
if (this.flags.has_backpressure) return;
if (this.socket == null) return;
const data = this.write_buffer.items;
if (data.len == 0) return;
const wrote = this.socket.?.write(data);
this.flags.has_backpressure = wrote < data.len;
if (wrote > 0) {
this.write_buffer.replaceRange(0, @intCast(wrote), &.{}) catch {};
}
}
fn processResponse(this: *SystemdResolvedConnection, data: []const u8) void {
// Append to read buffer
_ = this.read_buffer.len();
this.read_buffer.write(this.vm.allocator, data) catch return;
Output.prettyErrorln("[SystemdResolved] Received {} bytes, buffer has {} bytes total", .{data.len, this.read_buffer.len()});
// Process all complete responses in the buffer
while (true) {
const remaining = this.read_buffer.remaining();
const null_pos = std.mem.indexOf(u8, remaining, "\x00") orelse break;
const response = remaining[0..null_pos];
Output.prettyErrorln("[SystemdResolved] Got complete response (len={}): {s}", .{response.len, response[0..@min(100, response.len)]});
// Consume this response from the buffer (including the null terminator)
defer {
const bytes_to_consume = @as(u32, @intCast(null_pos + 1));
this.read_buffer.consume(bytes_to_consume);
}
// Process the response
if (this.current_request) |request| {
const query_name = switch (request.backend) {
.systemd_resolved => |q| q.name,
else => "unknown",
};
log("Processing response for: {s}", .{query_name});
// Parse JSON response using bun.json
var json_source = bun.logger.Source.initPathString("systemd-resolved", response);
var temp_log = bun.logger.Log.init(this.vm.allocator);
defer temp_log.deinit();
const parsed = bun.json.parseUTF8(&json_source, &temp_log, this.vm.allocator) catch |err| {
log("Failed to parse JSON: {}", .{err});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.PROTO), null, request);
this.current_request = null;
// Buffer will be consumed by the defer statement
this.processNextRequest();
return;
};
// Check for error in response
if (parsed.asProperty("error")) |error_prop| {
_ = error_prop;
log("Error in response", .{});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, request);
this.current_request = null;
// Buffer will be consumed by the defer statement
this.processNextRequest();
return;
}
// Get addresses from response - systemd-resolved returns them in "parameters.addresses"
const params = parsed.asProperty("parameters") orelse {
log("No parameters in response", .{});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, request);
this.current_request = null;
// Buffer will be consumed by the defer statement
this.processNextRequest();
return;
};
const addresses = params.expr.asProperty("addresses") orelse {
log("No addresses in response", .{});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, request);
this.current_request = null;
// Buffer will be consumed by the defer statement
this.processNextRequest();
return;
};
// Convert addresses array to addrinfo format
var result_list = GetAddrInfo.Result.List.init(this.vm.allocator);
errdefer result_list.deinit();
// Check if addresses is an array
const addresses_array = addresses.expr.asArray() orelse {
log("Addresses is not an array", .{});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.PROTO), null, request);
this.current_request = null;
// Buffer will be consumed by the defer statement
this.processNextRequest();
return;
};
// Parse each address in the array
var addr_iter = addresses_array;
while (addr_iter.next()) |addr_item| {
// Get the family (AF_INET = 2, AF_INET6 = 10)
const family_prop = addr_item.asProperty("family") orelse continue;
const family = @as(c_int, @intFromFloat(family_prop.expr.asNumber() orelse 0));
if (family != std.posix.AF.INET and family != std.posix.AF.INET6) {
continue;
}
// Get the address bytes array
const address_prop = addr_item.asProperty("address") orelse continue;
const address_array = address_prop.expr.asArray() orelse continue;
// Get TTL if present (default to 0)
var ttl: i32 = 0;
if (addr_item.asProperty("ttl")) |ttl_prop| {
ttl = @as(i32, @intFromFloat(ttl_prop.expr.asNumber() orelse 0));
}
// Create GetAddrInfo.Result object
var result_entry: GetAddrInfo.Result = undefined;
result_entry.ttl = ttl;
if (family == std.posix.AF.INET) {
// IPv4 address
var sockaddr_in: std.posix.sockaddr.in = std.mem.zeroes(std.posix.sockaddr.in);
sockaddr_in.family = std.posix.AF.INET;
sockaddr_in.port = 0; // Port will be set later based on request
// Convert byte array to IPv4 address
var addr_bytes: [4]u8 = undefined;
var byte_index: usize = 0;
var byte_iter = address_array;
while (byte_iter.next()) |byte_item| : (byte_index += 1) {
if (byte_index >= 4) break;
addr_bytes[byte_index] = @as(u8, @intFromFloat(byte_item.asNumber() orelse 0));
}
if (byte_index == 4) {
sockaddr_in.addr = @as(u32, addr_bytes[0]) |
(@as(u32, addr_bytes[1]) << 8) |
(@as(u32, addr_bytes[2]) << 16) |
(@as(u32, addr_bytes[3]) << 24);
}
// Create std.net.Address with IPv4
result_entry.address = std.net.Address{ .in = std.net.Ip4Address{ .sa = sockaddr_in } };
} else if (family == std.posix.AF.INET6) {
// IPv6 address
var sockaddr_in6: std.posix.sockaddr.in6 = std.mem.zeroes(std.posix.sockaddr.in6);
sockaddr_in6.family = std.posix.AF.INET6;
sockaddr_in6.port = 0; // Port will be set later based on request
// Convert byte array to IPv6 address
var byte_index: usize = 0;
var byte_iter = address_array;
while (byte_iter.next()) |byte_item| : (byte_index += 1) {
if (byte_index >= 16) break;
sockaddr_in6.addr[byte_index] = @as(u8, @intFromFloat(byte_item.asNumber() orelse 0));
}
// Create std.net.Address with IPv6
result_entry.address = std.net.Address{ .in6 = std.net.Ip6Address{ .sa = sockaddr_in6 } };
} else {
// Unsupported family
continue;
}
// Append to result list
result_list.append(result_entry) catch {
result_list.deinit();
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOMEM), null, request);
this.current_request = null;
// Buffer will be consumed by the defer statement
this.processNextRequest();
return;
};
}
// Check if we got any addresses
if (result_list.items.len == 0) {
result_list.deinit();
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, request);
this.current_request = null;
// Buffer will be consumed by the defer statement
this.processNextRequest();
return;
}
log("Successfully resolved via systemd-resolved with {} addresses", .{result_list.items.len});
if (result_list.items.len > 0) {
const first_addr = result_list.items[0].address;
if (first_addr.any.family == std.posix.AF.INET) {
var buf: [48]u8 = undefined;
const addr_str = std.fmt.bufPrint(&buf, "{}", .{first_addr.in.sa.addr}) catch "?";
log("First IPv4 address: {s}", .{addr_str});
} else if (first_addr.any.family == std.posix.AF.INET6) {
var buf: [48]u8 = undefined;
const addr_str = std.fmt.bufPrint(&buf, "{any}", .{first_addr.in6.sa.addr}) catch "?";
log("First IPv6 address: {s}", .{addr_str});
}
}
// Store result and notify completion
request.backend = .{ .libc = .{ .success = result_list } };
// Don't clear current_request here - let it be cleared after buffer is cleaned up
// Call the callback to process the result
if (request.resolver_for_caching) |resolver| {
if (request.cache.pending_cache) {
// drainPendingHostNative will destroy the request
resolver.drainPendingHostNative(request.cache.pos_in_pending, request.head.globalThis, 0, .{ .list = result_list });
return;
}
}
// No cache, call onCompleteNative directly with the result list
// Save head before destroying request
var head = request.head;
bun.default_allocator.destroy(request);
head.onCompleteNative(.{ .list = result_list });
// Clear current_request and process next
this.current_request = null;
this.processNextRequest();
} else {
}
}
}
fn SocketHandler(comptime ssl: bool) type {
return struct {
const SocketType = uws.NewSocketHandler(ssl);
pub fn onOpen(this: *SystemdResolvedConnection, socket: SocketType) void {
log("Connected to systemd-resolved", .{});
this.socket = socket;
this.flags.connected = true;
this.flags.connecting = false;
this.processNextRequest();
}
pub fn onClose(this: *SystemdResolvedConnection, socket: SocketType, _: i32, _: ?*anyopaque) void {
_ = socket;
log("Disconnected from systemd-resolved", .{});
this.flags.connected = false;
this.flags.connecting = false;
// Fail current request
if (this.current_request) |request| {
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.CONNREFUSED), null, request);
this.current_request = null;
}
}
pub fn onEnd(this: *SystemdResolvedConnection, socket: SocketType) void {
onClose(this, socket, 0, null);
}
pub fn onConnectError(this: *SystemdResolvedConnection, socket: SocketType, _: i32) void {
onClose(this, socket, 0, null);
}
pub fn onTimeout(this: *SystemdResolvedConnection, socket: SocketType) void {
_ = socket;
if (this.current_request) |request| {
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.TIMEDOUT), null, request);
this.current_request = null;
}
}
pub fn onData(this: *SystemdResolvedConnection, socket: SocketType, data: []const u8) void {
_ = socket;
this.processResponse(data);
}
pub fn onWritable(this: *SystemdResolvedConnection, socket: SocketType) void {
_ = socket;
this.flags.has_backpressure = false;
this.flushData();
}
pub const onHandshake = null;
};
}
};
const SystemdResolved = struct {
const SOCKET_PATH = "/run/systemd/resolve/io.systemd.Resolve";
var global_connection: ?*SystemdResolvedConnection = null;
pub fn isAvailable() bool {
if (comptime !Environment.isLinux) return false;
const stat = std.fs.cwd().statFile(SOCKET_PATH) catch return false;
return stat.kind == .unix_domain_socket;
}
pub fn lookup(this: *Resolver, query: GetAddrInfo, globalThis: *jsc.JSGlobalObject) jsc.JSValue {
Output.flush();
const force_systemd = bun.getRuntimeFeatureFlag(.BUN_DNS_FORCE_SYSTEMD_RESOLVED);
if (!force_systemd and !isAvailable()) {
Output.flush();
return LibC.lookup(this, query, globalThis);
}
// Log that we're using systemd-resolved
Output.flush();
const key = GetAddrInfoRequest.PendingCacheKey.init(query);
var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_native);
if (cache == .inflight) {
var dns_lookup = bun.handleOom(DNSLookup.init(this, globalThis, globalThis.allocator()));
cache.inflight.append(dns_lookup);
return dns_lookup.promise.value();
}
var request = GetAddrInfoRequest.init(
cache,
.{ .systemd_resolved = query.clone() },
this,
query,
globalThis,
"pending_host_cache_native",
) catch |err| bun.handleOom(err);
const promise_value = request.head.promise.value();
// Get or create the singleton connection
const connection = global_connection orelse brk: {
const conn = SystemdResolvedConnection.init(globalThis.bunVM()) catch {
// Fall back to libc
request.backend = .{ .libc = .{ .query = query.clone() } };
var task = GetAddrInfoRequest.Task.createOnJSThread(this.vm.allocator, globalThis, request) catch |err2| bun.handleOom(err2);
task.schedule();
this.requestSent(globalThis.bunVM());
return promise_value;
};
global_connection = conn;
break :brk conn;
};
// Send request through systemd-resolved connection
connection.sendRequest(request) catch {
// Fall back to libc
request.backend = .{ .libc = .{ .query = query.clone() } };
var task = GetAddrInfoRequest.Task.createOnJSThread(this.vm.allocator, globalThis, request) catch |err| bun.handleOom(err);
task.schedule();
this.requestSent(globalThis.bunVM());
return promise_value;
};
this.requestSent(globalThis.bunVM());
return promise_value;
}
};
const LibC = struct {
pub fn lookup(this: *Resolver, query_init: GetAddrInfo, globalThis: *jsc.JSGlobalObject) jsc.JSValue {
if (Environment.isWindows) {
@@ -731,6 +1206,7 @@ pub const GetAddrInfoRequest = struct {
pub const Backend = union(enum) {
c_ares: void,
libinfo: GetAddrInfoRequest.Backend.LibInfo,
systemd_resolved: GetAddrInfo,
libc: if (Environment.isWindows)
struct {
uv: libuv.uv_getaddrinfo_t = undefined,
@@ -805,12 +1281,28 @@ pub const GetAddrInfoRequest = struct {
pub const onMachportChange = Backend.LibInfo.onMachportChange;
pub fn run(this: *GetAddrInfoRequest, task: *Task) void {
this.backend.libc.run();
switch (this.backend) {
.systemd_resolved => |query| {
// The actual connection happens in lookup(), not here
// For now fall back to libc
this.backend = .{ .libc = .{ .query = query } };
this.backend.libc.run();
},
.libc => this.backend.libc.run(),
else => {},
}
task.onFinish();
}
pub fn then(this: *GetAddrInfoRequest, _: *jsc.JSGlobalObject) void {
log("then", .{});
switch (this.backend) {
.systemd_resolved => {
return;
},
.libc => {},
else => return,
}
switch (this.backend.libc) {
.success => |result| {
const any = GetAddrInfo.Result.Any{ .list = result };
@@ -1965,7 +2457,6 @@ pub const Resolver = struct {
pub fn fromStringOrDie(order: []const u8) Order {
return fromString(order) orelse {
Output.prettyErrorln("<r><red>error<r><d>:<r> Invalid DNS result order.", .{});
Global.exit(1);
};
}
@@ -2784,6 +3275,10 @@ pub const Resolver = struct {
.system => switch (comptime Environment.os) {
.mac => LibInfo.lookup(this, query, globalThis),
.windows => LibUVBackend.lookup(this, query, globalThis),
.linux => if (bun.getRuntimeFeatureFlag(.BUN_DNS_FORCE_SYSTEMD_RESOLVED) or SystemdResolved.isAvailable())
SystemdResolved.lookup(this, query, globalThis)
else
LibC.lookup(this, query, globalThis),
else => LibC.lookup(this, query, globalThis),
},
};
@@ -3508,6 +4003,7 @@ const Async = bun.Async;
const Environment = bun.Environment;
const Global = bun.Global;
const Output = bun.Output;
const uws = bun.uws;
const c_ares = bun.c_ares;
const default_allocator = bun.default_allocator;
const strings = bun.strings;

View File

@@ -288,6 +288,7 @@ pub const GetAddrInfo = struct {
pub const default: GetAddrInfo.Backend = switch (bun.Environment.os) {
.mac, .windows => .system,
.linux => .system, // Use system backend on Linux to support systemd-resolved
else => .c_ares,
};

View File

@@ -0,0 +1,440 @@
const std = @import("std");
const bun = @import("bun");
const strings = bun.strings;
const Output = bun.Output;
const uws = bun.uws;
const jsc = bun.jsc;
const Environment = bun.Environment;
const log = Output.scoped(.SystemdResolved, false);
pub const SystemdResolvedConnection = struct {
const SOCKET_PATH = "/run/systemd/resolve/io.systemd.Resolve";
const VARLINK_METHOD = "io.systemd.Resolve.ResolveHostname";
socket: ?uws.SocketTCP = null,
socket_context: ?*uws.SocketContext = null,
event_loop: jsc.EventLoopHandle,
read_buffer: bun.MutableString,
write_buffer: bun.MutableString,
current_request: ?*Request = null,
request_queue: std.ArrayList(*Request),
flags: packed struct {
connected: bool = false,
connecting: bool = false,
has_backpressure: bool = false,
closed: bool = false,
} = .{},
pub const Request = struct {
id: u64,
name: []const u8,
family: ?i32,
flags: ?i32,
callback: *const fn (*Request, ?*ResolveResult, ?*ResolveError) void,
context: *anyopaque,
next: ?*Request = null,
};
pub const ResolveResult = struct {
addresses: []ResolvedAddress,
name: []const u8,
flags: i32,
pub fn deinit(this: *ResolveResult, allocator: std.mem.Allocator) void {
allocator.free(this.addresses);
allocator.free(this.name);
}
};
pub const ResolvedAddress = struct {
ifindex: ?i32,
family: i32,
address: []const u8,
};
pub const ResolveError = struct {
code: []const u8,
message: []const u8,
pub fn deinit(this: *ResolveError, allocator: std.mem.Allocator) void {
allocator.free(this.code);
allocator.free(this.message);
}
};
var next_request_id: std.atomic.Value(u64) = std.atomic.Value(u64).init(1);
pub fn init(event_loop: jsc.EventLoopHandle) !*SystemdResolvedConnection {
const allocator = event_loop.allocator();
const this = try allocator.create(SystemdResolvedConnection);
this.* = .{
.event_loop = event_loop,
.read_buffer = try bun.MutableString.initEmpty(allocator, 4096),
.write_buffer = try bun.MutableString.initEmpty(allocator, 4096),
.request_queue = std.ArrayList(*Request).init(allocator),
};
return this;
}
pub fn deinit(this: *SystemdResolvedConnection) void {
const allocator = this.event_loop.allocator();
if (this.socket) |socket| {
socket.close();
}
this.read_buffer.deinit();
this.write_buffer.deinit();
this.request_queue.deinit();
allocator.destroy(this);
}
pub fn isAvailable() bool {
if (comptime !Environment.isLinux) return false;
const stat = std.fs.cwd().statFile(SOCKET_PATH) catch return false;
return stat.kind == .unix_domain_socket;
}
pub fn connect(this: *SystemdResolvedConnection) !void {
if (this.flags.connected or this.flags.connecting) {
return;
}
this.flags.connecting = true;
const ctx = this.socket_context orelse brk: {
const ctx_ = uws.SocketContext.createNoSSLContext(this.event_loop.loop(), @sizeOf(*SystemdResolvedConnection)).?;
uws.NewSocketHandler(false).configure(ctx_, true, *SystemdResolvedConnection, SocketHandler(false));
this.socket_context = ctx_;
break :brk ctx_;
};
this.socket = try uws.SocketTCP.connectUnixAnon(
SOCKET_PATH,
ctx,
this,
);
}
pub fn resolveHostname(
this: *SystemdResolvedConnection,
name: []const u8,
family: ?i32,
flags: ?i32,
callback: *const fn (*Request, ?*ResolveResult, ?*ResolveError) void,
context: *anyopaque,
) !void {
const allocator = this.vm.allocator;
const request = try allocator.create(Request);
request.* = .{
.id = next_request_id.fetchAdd(1, .monotonic),
.name = try allocator.dupe(u8, name),
.family = family,
.flags = flags,
.callback = callback,
.context = context,
};
try this.request_queue.append(request);
if (!this.flags.connected) {
try this.connect();
} else {
try this.sendNextRequest();
}
}
fn sendNextRequest(this: *SystemdResolvedConnection) !void {
if (this.current_request != null) return;
if (this.request_queue.items.len == 0) return;
if (this.flags.has_backpressure) return;
const request = this.request_queue.orderedRemove(0);
this.current_request = request;
this.write_buffer.reset();
var writer = this.write_buffer.writer();
try std.json.stringify(.{
.method = VARLINK_METHOD,
.parameters = .{
.name = request.name,
.family = request.family,
.flags = request.flags,
},
}, .{}, writer);
try writer.writeByte(0);
this.flushData();
}
fn flushData(this: *SystemdResolvedConnection) void {
if (this.flags.has_backpressure) return;
const chunk = this.write_buffer.list.items;
if (chunk.len == 0) return;
if (this.socket) |socket| {
const wrote = socket.write(chunk);
this.flags.has_backpressure = wrote < chunk.len;
if (wrote > 0) {
_ = this.write_buffer.list.orderedRemove(0);
_ = this.write_buffer.list.resize(@intCast(chunk.len - wrote)) catch {};
}
}
}
fn processResponse(this: *SystemdResolvedConnection, data: []const u8) void {
defer {
const remaining = data[this.processResponseInternal(data)..];
if (remaining.len > 0) {
this.processResponse(remaining);
}
}
}
fn processResponseInternal(this: *SystemdResolvedConnection, data: []const u8) usize {
const allocator = this.event_loop.allocator();
var null_pos: ?usize = null;
for (data, 0..) |byte, i| {
if (byte == 0) {
null_pos = i;
break;
}
}
if (null_pos == null) {
this.read_buffer.appendSlice(data) catch {};
return data.len;
}
const message_data = if (this.read_buffer.list.items.len > 0) blk: {
this.read_buffer.appendSlice(data[0..null_pos.?]) catch {};
break :blk this.read_buffer.list.items;
} else data[0..null_pos.?];
defer this.read_buffer.reset();
const request = this.current_request orelse return null_pos.? + 1;
this.current_request = null;
const json_source = bun.logger.Source.initPathString("<varlink>", message_data);
var temp_log = bun.logger.Log.init(allocator);
defer temp_log.deinit();
const json = bun.json.parseUTF8(&json_source, &temp_log, allocator) catch |err| {
log("Failed to parse JSON response: {s}", .{@errorName(err)});
var error_result = ResolveError{
.code = "PARSE_ERROR",
.message = try allocator.dupe(u8, "Failed to parse response"),
};
request.callback(request, null, &error_result);
return null_pos.? + 1;
};
if (json.data == .e_object) {
const obj = json.data.e_object;
if (obj.get("error")) |error_val| {
if (error_val.data == .e_string) {
var error_result = ResolveError{
.code = try allocator.dupe(u8, error_val.data.e_string.data),
.message = try allocator.dupe(u8, error_val.data.e_string.data),
};
request.callback(request, null, &error_result);
return null_pos.? + 1;
}
}
if (obj.get("parameters")) |params| {
if (params.data == .e_object) {
const params_obj = params.data.e_object;
var addresses = std.ArrayList(ResolvedAddress).init(allocator);
defer addresses.deinit();
if (params_obj.get("addresses")) |addresses_val| {
if (addresses_val.data == .e_array) {
for (addresses_val.data.e_array.slice()) |addr_val| {
if (addr_val.data == .e_object) {
const addr_obj = addr_val.data.e_object;
var resolved_addr = ResolvedAddress{
.ifindex = null,
.family = 0,
.address = "",
};
if (addr_obj.get("ifindex")) |ifindex_val| {
if (ifindex_val.data == .e_number) {
resolved_addr.ifindex = @intFromFloat(ifindex_val.data.e_number.value);
}
}
if (addr_obj.get("family")) |family_val| {
if (family_val.data == .e_number) {
resolved_addr.family = @intFromFloat(family_val.data.e_number.value);
}
}
if (addr_obj.get("address")) |address_val| {
if (address_val.data == .e_array) {
var addr_bytes = try allocator.alloc(u8, address_val.data.e_array.len());
for (address_val.data.e_array.slice(), 0..) |byte_val, i| {
if (byte_val.data == .e_number) {
addr_bytes[i] = @intFromFloat(byte_val.data.e_number.value);
}
}
if (resolved_addr.family == std.posix.AF.INET) {
var buf: [16]u8 = undefined;
const addr_str = std.fmt.bufPrint(&buf, "{d}.{d}.{d}.{d}", .{
addr_bytes[0],
addr_bytes[1],
addr_bytes[2],
addr_bytes[3],
}) catch "";
resolved_addr.address = try allocator.dupe(u8, addr_str);
} else if (resolved_addr.family == std.posix.AF.INET6) {
var buf: [46]u8 = undefined;
const addr_in6 = @as(*align(1) const std.posix.sockaddr.in6, @ptrCast(addr_bytes.ptr));
const addr_str = std.fmt.bufPrint(&buf, "{}", .{addr_in6.addr}) catch "";
resolved_addr.address = try allocator.dupe(u8, addr_str);
}
}
}
try addresses.append(resolved_addr);
}
}
}
}
var result = ResolveResult{
.addresses = try addresses.toOwnedSlice(),
.name = "",
.flags = 0,
};
if (params_obj.get("name")) |name_val| {
if (name_val.data == .e_string) {
result.name = try allocator.dupe(u8, name_val.data.e_string.data);
}
}
if (params_obj.get("flags")) |flags_val| {
if (flags_val.data == .e_number) {
result.flags = @intFromFloat(flags_val.data.e_number.value);
}
}
request.callback(request, &result, null);
return null_pos.? + 1;
}
}
}
var error_result = ResolveError{
.code = "UNKNOWN_ERROR",
.message = try allocator.dupe(u8, "Unknown response format"),
};
request.callback(request, null, &error_result);
return null_pos.? + 1;
}
pub fn SocketHandler(comptime ssl: bool) type {
return struct {
const SocketType = if (ssl) uws.SocketTLS else uws.SocketTCP;
fn _socket(s: SocketType) uws.SocketTCP {
return s;
}
pub fn onOpen(this: *SystemdResolvedConnection, socket: SocketType) void {
log("SystemdResolved connection opened", .{});
this.socket = _socket(socket);
this.flags.connected = true;
this.flags.connecting = false;
this.sendNextRequest() catch |err| {
log("Failed to send request: {s}", .{@errorName(err)});
};
}
pub fn onClose(this: *SystemdResolvedConnection, socket: SocketType, _: i32, _: ?*anyopaque) void {
_ = socket;
log("SystemdResolved connection closed", .{});
this.flags.connected = false;
this.flags.connecting = false;
this.flags.closed = true;
if (this.current_request) |request| {
var error_result = ResolveError{
.code = "CONNECTION_CLOSED",
.message = this.vm.allocator.dupe(u8, "Connection closed") catch "",
};
request.callback(request, null, &error_result);
this.current_request = null;
}
}
pub fn onEnd(this: *SystemdResolvedConnection, socket: SocketType) void {
this.onClose(socket, 0, null);
}
pub fn onConnectError(this: *SystemdResolvedConnection, socket: SocketType, _: i32) void {
log("SystemdResolved connection error", .{});
this.onClose(socket, 0, null);
}
pub fn onTimeout(this: *SystemdResolvedConnection, socket: SocketType) void {
_ = socket;
log("SystemdResolved connection timeout", .{});
if (this.current_request) |request| {
var error_result = ResolveError{
.code = "TIMEOUT",
.message = this.vm.allocator.dupe(u8, "Request timeout") catch "",
};
request.callback(request, null, &error_result);
this.current_request = null;
}
}
pub fn onData(this: *SystemdResolvedConnection, socket: SocketType, data: []const u8) void {
_ = socket;
this.processResponse(data);
this.sendNextRequest() catch |err| {
log("Failed to send next request: {s}", .{@errorName(err)});
};
}
pub fn onWritable(this: *SystemdResolvedConnection, socket: SocketType) void {
_ = socket;
this.flags.has_backpressure = false;
this.flushData();
if (this.write_buffer.list.items.len == 0) {
this.sendNextRequest() catch |err| {
log("Failed to send request on writable: {s}", .{@errorName(err)});
};
}
}
pub const onHandshake = null;
};
}
};

View File

@@ -0,0 +1,337 @@
const std = @import("std");
const bun = @import("bun");
const dns = bun.dns;
const jsc = bun.jsc;
const Environment = bun.Environment;
const Output = bun.Output;
const strings = bun.strings;
const Async = bun.Async;
const SystemdResolvedBackend = @import("systemd-resolved-backend.zig");
const log = Output.scoped(.SystemdResolved, false);
const GetAddrInfoRequest = dns.GetAddrInfoRequest;
const DNSLookup = dns.DNSLookup;
pub const SystemdResolved = struct {
connection: ?*SystemdResolvedBackend.SystemdResolvedConnection = null,
event_loop: jsc.EventLoopHandle,
var global_instance: ?*SystemdResolved = null;
pub fn init(event_loop: jsc.EventLoopHandle) !*SystemdResolved {
if (global_instance) |instance| {
return instance;
}
const allocator = event_loop.allocator();
var this = try allocator.create(SystemdResolved);
this.* = .{
.event_loop = event_loop,
};
if (SystemdResolvedBackend.SystemdResolvedConnection.isAvailable()) {
this.connection = try SystemdResolvedBackend.SystemdResolvedConnection.init(event_loop);
}
global_instance = this;
return this;
}
pub fn deinit(this: *SystemdResolved) void {
if (this.connection) |conn| {
conn.deinit();
}
this.event_loop.allocator().destroy(this);
global_instance = null;
}
pub fn isAvailable() bool {
return SystemdResolvedBackend.SystemdResolvedConnection.isAvailable();
}
pub fn lookup(this: *dns.Resolver, query: dns.GetAddrInfo, globalThis: *jsc.JSGlobalObject) jsc.JSValue {
if (comptime !Environment.isLinux) {
return dns.LibC.lookup(this, query, globalThis);
}
if (!isAvailable()) {
return dns.LibC.lookup(this, query, globalThis);
}
const vm = globalThis.bunVM();
const event_loop = jsc.EventLoopHandle.init(vm);
const systemd = global_instance orelse blk: {
const instance = init(event_loop) catch {
return dns.LibC.lookup(this, query, globalThis);
};
break :blk instance;
};
const connection = systemd.connection orelse {
return dns.LibC.lookup(this, query, globalThis);
};
const key = GetAddrInfoRequest.PendingCacheKey.init(query);
var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_native);
if (cache == .inflight) {
var dns_lookup = bun.handleOom(DNSLookup.init(this, globalThis, globalThis.allocator()));
cache.inflight.append(dns_lookup);
return dns_lookup.promise.value();
}
var request = GetAddrInfoRequest.init(
cache,
.{ .systemd_resolved = undefined },
this,
query,
globalThis,
"pending_host_cache_native",
) catch |err| bun.handleOom(err);
log("Created GetAddrInfoRequest, calling requestSent", .{});
const promise_value = request.head.promise.value();
const callback_context = globalThis.allocator().create(CallbackContext) catch |err| {
bun.handleOom(err);
request.head.promise.rejectTask(globalThis, globalThis.createErrorInstance("Out of memory", .{}));
if (request.cache.pending_cache) this.pending_host_cache_native.used.set(request.cache.pos_in_pending);
event_loop.allocator().destroy(request);
return promise_value;
};
callback_context.* = .{
.request = request,
.globalThis = globalThis,
.resolver = this,
};
const family: ?i32 = switch (query.options.family) {
.unspecified => null,
.ipv4 => std.posix.AF.INET,
.ipv6 => std.posix.AF.INET6,
};
connection.resolveHostname(
query.name,
family,
null,
onResolveComplete,
callback_context,
) catch |err| {
log("Failed to send DNS request: {s}", .{@errorName(err)});
globalThis.allocator().destroy(callback_context);
request.head.promise.rejectTask(globalThis, globalThis.createErrorInstance("DNS request failed: {s}", .{@errorName(err)}));
if (request.cache.pending_cache) this.pending_host_cache_native.used.set(request.cache.pos_in_pending);
event_loop.allocator().destroy(request);
return promise_value;
};
this.requestSent(globalThis.bunVM());
return promise_value;
}
const CallbackContext = struct {
request: *GetAddrInfoRequest,
globalThis: *jsc.JSGlobalObject,
resolver: *dns.Resolver,
// Task to schedule callback on JS thread
pub const Task = bun.jsc.WorkTask(CallbackContext);
pub fn run(this: *CallbackContext, task: *Task) void {
// This runs on the JS thread - safe to call getAddrInfoAsyncCallback
if (this.errno != 0) {
GetAddrInfoRequest.getAddrInfoAsyncCallback(this.errno, null, this.request);
} else if (this.addrinfo) |info| {
GetAddrInfoRequest.getAddrInfoAsyncCallback(0, info, this.request);
} else {
GetAddrInfoRequest.getAddrInfoAsyncCallback(-1, null, this.request);
}
// Clean up
const allocator = this.globalThis.allocator();
allocator.destroy(this);
task.onFinish();
}
// Store result data for task
errno: i32 = 0,
addrinfo: ?*std.c.addrinfo = null,
};
fn onResolveComplete(
req: *SystemdResolvedBackend.SystemdResolvedConnection.Request,
result: ?*SystemdResolvedBackend.SystemdResolvedConnection.ResolveResult,
err: ?*SystemdResolvedBackend.SystemdResolvedConnection.ResolveError,
) void {
const context = @as(*CallbackContext, @ptrCast(@alignCast(req.context)));
const globalThis = context.globalThis;
const allocator = globalThis.allocator();
defer {
allocator.free(req.name);
allocator.destroy(req);
}
if (err) |error_info| {
defer error_info.deinit(allocator);
const errno: i32 = if (strings.eqlComptime(error_info.code, "NoSuchResourceRecord"))
@intFromEnum(std.posix.E.NOENT)
else if (strings.eqlComptime(error_info.code, "QueryTimedOut"))
@intFromEnum(std.posix.E.TIMEDOUT)
else if (strings.eqlComptime(error_info.code, "NetworkDown"))
@intFromEnum(std.posix.E.NETDOWN)
else
-1;
context.errno = errno;
// Schedule callback on JS thread
var task = CallbackContext.Task.createOnJSThread(allocator, globalThis, context) catch |e| {
bun.handleOom(e);
GetAddrInfoRequest.getAddrInfoAsyncCallback(errno, null, context.request);
allocator.destroy(context);
return;
};
task.schedule();
return;
}
if (result) |res| {
defer res.deinit(allocator);
if (res.addresses.len == 0) {
context.errno = @intFromEnum(std.posix.E.NOENT);
// Schedule callback on JS thread
var task = CallbackContext.Task.createOnJSThread(allocator, globalThis, context) catch |e| {
bun.handleOom(e);
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, context.request);
allocator.destroy(context);
return;
};
task.schedule();
return;
}
var head: ?*std.c.addrinfo = null;
var tail: ?*std.c.addrinfo = null;
for (res.addresses) |addr| {
const ai = allocator.create(std.c.addrinfo) catch {
if (head) |h| std.c.freeaddrinfo(h);
context.errno = @intFromEnum(std.posix.E.NOMEM);
// Schedule callback on JS thread
var task = CallbackContext.Task.createOnJSThread(allocator, globalThis, context) catch |e2| {
bun.handleOom(e2);
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOMEM), null, context.request);
allocator.destroy(context);
return;
};
task.schedule();
return;
};
ai.* = std.mem.zeroes(std.c.addrinfo);
ai.ai_family = addr.family;
ai.ai_socktype = std.posix.SOCK.STREAM;
ai.ai_protocol = std.posix.IPPROTO.TCP;
if (addr.family == std.posix.AF.INET) {
const sockaddr = allocator.create(std.posix.sockaddr.in) catch {
allocator.destroy(ai);
if (head) |h| std.c.freeaddrinfo(h);
context.errno = @intFromEnum(std.posix.E.NOMEM);
// Schedule callback on JS thread
var task = CallbackContext.Task.createOnJSThread(allocator, globalThis, context) catch |e2| {
bun.handleOom(e2);
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOMEM), null, context.request);
allocator.destroy(context);
return;
};
task.schedule();
return;
};
sockaddr.* = std.mem.zeroes(std.posix.sockaddr.in);
sockaddr.family = std.posix.AF.INET;
sockaddr.port = bun.std.mem.bigToNative(u16, request.head.port);
var parts = std.mem.tokenize(u8, addr.address, ".");
var i: usize = 0;
while (parts.next()) |part| : (i += 1) {
if (i >= 4) break;
const byte = std.fmt.parseInt(u8, part, 10) catch 0;
sockaddr.addr.s_addr |= @as(u32, byte) << @intCast(i * 8);
}
ai.ai_addr = @ptrCast(sockaddr);
ai.ai_addrlen = @sizeOf(std.posix.sockaddr.in);
} else if (addr.family == std.posix.AF.INET6) {
const sockaddr = allocator.create(std.posix.sockaddr.in6) catch {
allocator.destroy(ai);
if (head) |h| std.c.freeaddrinfo(h);
context.errno = @intFromEnum(std.posix.E.NOMEM);
// Schedule callback on JS thread
var task = CallbackContext.Task.createOnJSThread(allocator, globalThis, context) catch |e2| {
bun.handleOom(e2);
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOMEM), null, context.request);
allocator.destroy(context);
return;
};
task.schedule();
return;
};
sockaddr.* = std.mem.zeroes(std.posix.sockaddr.in6);
sockaddr.family = std.posix.AF.INET6;
sockaddr.port = bun.std.mem.bigToNative(u16, request.head.port);
_ = std.net.Ip6Address.parse(addr.address, 0) catch {};
ai.ai_addr = @ptrCast(sockaddr);
ai.ai_addrlen = @sizeOf(std.posix.sockaddr.in6);
}
if (head == null) {
head = ai;
tail = ai;
} else {
tail.?.ai_next = ai;
tail = ai;
}
}
context.addrinfo = head;
// Schedule callback on JS thread
var task = CallbackContext.Task.createOnJSThread(allocator, globalThis, context) catch |e| {
bun.handleOom(e);
if (head) |h| std.c.freeaddrinfo(h);
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOMEM), null, context.request);
allocator.destroy(context);
return;
};
task.schedule();
} else {
context.errno = -1;
// Schedule callback on JS thread
var task = CallbackContext.Task.createOnJSThread(allocator, globalThis, context) catch |e| {
bun.handleOom(e);
GetAddrInfoRequest.getAddrInfoAsyncCallback(-1, null, context.request);
allocator.destroy(context);
return;
};
task.schedule();
}
}
};

138
src/dns/varlink.zig Normal file
View File

@@ -0,0 +1,138 @@
const std = @import("std");
const bun = @import("bun");
const strings = bun.strings;
const Output = bun.Output;
const log = Output.scoped(.Varlink, true);
pub fn resolveHostnameSync(allocator: std.mem.Allocator, hostname: []const u8, family: ?i32) ![]std.c.addrinfo {
const socket_path = "/run/systemd/resolve/io.systemd.Resolve";
// Connect to socket
const sock = try std.posix.socket(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0);
defer std.posix.close(sock);
var addr = std.posix.sockaddr.un{
.family = std.posix.AF.UNIX,
.path = undefined,
};
@memset(&addr.path, 0);
@memcpy(addr.path[0..socket_path.len], socket_path);
try std.posix.connect(sock, @ptrCast(&addr), @sizeOf(@TypeOf(addr)));
// Create Varlink request
var request_buf: [4096]u8 = undefined;
const request = try std.fmt.bufPrint(&request_buf,
\\{{"method":"io.systemd.Resolve.ResolveHostname","parameters":{{"name":"{s}","family":{?d}}}}}
, .{ hostname, family });
// Send null-terminated request
var send_buf: [4097]u8 = undefined;
@memcpy(send_buf[0..request.len], request);
send_buf[request.len] = 0;
_ = try std.posix.send(sock, send_buf[0..request.len + 1], 0);
// Read response
var response_buf: [8192]u8 = undefined;
const bytes_read = try std.posix.recv(sock, &response_buf, 0);
// Find null terminator
var null_pos: ?usize = null;
for (response_buf[0..bytes_read], 0..) |byte, i| {
if (byte == 0) {
null_pos = i;
break;
}
}
if (null_pos == null) {
return error.InvalidResponse;
}
// Parse JSON response
const response = response_buf[0..null_pos.?];
log("Varlink response: {s}", .{response});
// Simple JSON parsing for addresses
// Look for "addresses":[
const addresses_marker = "\"addresses\":[";
const addresses_start = std.mem.indexOf(u8, response, addresses_marker) orelse return error.NoAddresses;
const addresses_data = response[addresses_start + addresses_marker.len..];
// Find the end of addresses array
const addresses_end = std.mem.indexOf(u8, addresses_data, "]") orelse return error.InvalidJSON;
const addresses_json = addresses_data[0..addresses_end];
// Count addresses (crude but works)
var addr_count: usize = 0;
var iter = std.mem.tokenize(u8, addresses_json, "{}");
while (iter.next()) |_| {
addr_count += 1;
}
if (addr_count == 0) {
return error.NoAddresses;
}
// Allocate result array
var results = try allocator.alloc(std.c.addrinfo, addr_count);
var result_idx: usize = 0;
// Parse each address
iter = std.mem.tokenize(u8, addresses_json, "{}");
while (iter.next()) |addr_obj| {
// Look for "family":2 (IPv4) or "family":10 (IPv6)
const family_marker = "\"family\":";
const family_pos = std.mem.indexOf(u8, addr_obj, family_marker) orelse continue;
const family_str = addr_obj[family_pos + family_marker.len..];
const family_val = std.fmt.parseInt(i32, family_str[0..1], 10) catch continue;
// Look for "address":[
const addr_marker = "\"address\":[";
const addr_pos = std.mem.indexOf(u8, addr_obj, addr_marker) orelse continue;
const addr_data = addr_obj[addr_pos + addr_marker.len..];
const addr_end = std.mem.indexOf(u8, addr_data, "]") orelse continue;
const addr_bytes_str = addr_data[0..addr_end];
// Parse address bytes
if (family_val == 2) { // IPv4
var sockaddr = try allocator.create(std.posix.sockaddr.in);
sockaddr.* = std.mem.zeroes(std.posix.sockaddr.in);
sockaddr.family = std.posix.AF.INET;
sockaddr.port = 0;
// Parse bytes like "23,220,75,245"
var byte_iter = std.mem.tokenize(u8, addr_bytes_str, ",");
var byte_idx: usize = 0;
var addr_value: u32 = 0;
while (byte_iter.next()) |byte_str| : (byte_idx += 1) {
if (byte_idx >= 4) break;
const byte_val = std.fmt.parseInt(u8, byte_str, 10) catch continue;
addr_value |= @as(u32, byte_val) << @intCast(byte_idx * 8);
}
sockaddr.addr.s_addr = addr_value;
results[result_idx] = std.mem.zeroes(std.c.addrinfo);
results[result_idx].family = std.posix.AF.INET;
results[result_idx].socktype = std.posix.SOCK.STREAM;
results[result_idx].protocol = std.posix.IPPROTO.TCP;
results[result_idx].addr = @ptrCast(sockaddr);
results[result_idx].addrlen = @sizeOf(std.posix.sockaddr.in);
if (result_idx > 0) {
results[result_idx - 1].next = &results[result_idx];
}
result_idx += 1;
}
}
if (result_idx == 0) {
allocator.free(results);
return error.NoValidAddresses;
}
return results[0..result_idx];
}

View File

@@ -14,6 +14,7 @@ pub const RuntimeFeatureFlag = enum {
BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER,
BUN_FEATURE_FLAG_DISABLE_DNS_CACHE,
BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO,
BUN_DNS_FORCE_SYSTEMD_RESOLVED,
BUN_FEATURE_FLAG_DISABLE_INSTALL_INDEX,
BUN_FEATURE_FLAG_DISABLE_IO_POOL,
BUN_FEATURE_FLAG_DISABLE_IPV4,

View File

@@ -0,0 +1,196 @@
import { test, expect, describe } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { existsSync } from "fs";
describe("systemd-resolved DNS backend", () => {
const SOCKET_PATH = "/run/systemd/resolve/io.systemd.Resolve";
const isAvailable = existsSync(SOCKET_PATH);
test.skipIf(!isAvailable)("should use systemd-resolved when available", async () => {
// Create a test script that uses DNS
const testScript = `
import dns from "dns/promises";
const result = await dns.lookup("example.com");
console.log(JSON.stringify(result));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", testScript],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const result = JSON.parse(stdout.trim());
expect(result).toHaveProperty("address");
expect(result).toHaveProperty("family");
});
test.skipIf(!isAvailable)("should resolve IPv4 addresses", async () => {
const testScript = `
import dns from "dns/promises";
const result = await dns.lookup("google.com", { family: 4 });
console.log(JSON.stringify(result));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", testScript],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const result = JSON.parse(stdout.trim());
expect(result).toHaveProperty("address");
expect(result.family).toBe(4);
});
test.skipIf(!isAvailable)("should resolve IPv6 addresses", async () => {
const testScript = `
import dns from "dns/promises";
const result = await dns.lookup("google.com", { family: 6 });
console.log(JSON.stringify(result));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", testScript],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const result = JSON.parse(stdout.trim());
expect(result).toHaveProperty("address");
expect(result.family).toBe(6);
});
test.skipIf(!isAvailable)("should handle multiple addresses", async () => {
const testScript = `
import dns from "dns/promises";
const result = await dns.lookup("google.com", { all: true });
console.log(JSON.stringify(result));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", testScript],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const result = JSON.parse(stdout.trim());
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
for (const entry of result) {
expect(entry).toHaveProperty("address");
expect(entry).toHaveProperty("family");
expect([4, 6]).toContain(entry.family);
}
});
test.skipIf(!isAvailable)("should handle DNS errors gracefully", async () => {
const testScript = `
import dns from "dns/promises";
try {
await dns.lookup("this-domain-definitely-does-not-exist-12345.example");
console.log("SHOULD_NOT_REACH");
} catch (err) {
console.log(JSON.stringify({ error: err.code }));
}
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", testScript],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
expect(exitCode).toBe(0);
const result = JSON.parse(stdout.trim());
expect(result.error).toBeDefined();
});
test("should fall back to libc when systemd-resolved is not available", async () => {
if (isAvailable) {
// If systemd-resolved is available, we can't test the fallback
return;
}
const testScript = `
import dns from "dns/promises";
const result = await dns.lookup("example.com");
console.log(JSON.stringify(result));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", testScript],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
const result = JSON.parse(stdout.trim());
expect(result).toHaveProperty("address");
expect(result).toHaveProperty("family");
});
});