“wip”

This commit is contained in:
Claude Bot
2025-09-08 05:13:04 +00:00
parent 73f0594704
commit 8ce601b171
7 changed files with 1400 additions and 1 deletions

View File

@@ -127,6 +127,352 @@ 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: std.ArrayList(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 = std.ArrayList(u8).init(allocator),
.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
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.appendSlice(data) catch return;
// Look for null terminator
const null_pos = std.mem.indexOf(u8, this.read_buffer.items, "\x00") orelse return;
const response = this.read_buffer.items[0..null_pos];
log("Response: {s}", .{response[0..@min(200, response.len)]});
// Process the response
if (this.current_request) |request| {
// 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;
this.read_buffer.replaceRange(0, null_pos + 1, &.{}) catch {};
this.processNextRequest();
return;
};
// Check for error in response
if (parsed.get("error")) |error_obj| {
_ = error_obj;
log("Error in response", .{});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, request);
this.current_request = null;
this.read_buffer.replaceRange(0, null_pos + 1, &.{}) catch {};
this.processNextRequest();
return;
}
// Get addresses from response - systemd-resolved returns them in "parameters.addresses"
const params = parsed.get("parameters") orelse {
log("No parameters in response", .{});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, request);
this.current_request = null;
this.read_buffer.replaceRange(0, null_pos + 1, &.{}) catch {};
this.processNextRequest();
return;
};
const addresses = params.get("addresses") orelse {
log("No addresses in response", .{});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, request);
this.current_request = null;
this.read_buffer.replaceRange(0, null_pos + 1, &.{}) catch {};
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.asArray() orelse {
log("Addresses is not an array", .{});
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.PROTO), null, request);
this.current_request = null;
this.read_buffer.replaceRange(0, null_pos + 1, &.{}) catch {};
this.processNextRequest();
return;
};
// TODO: Actually parse the addresses_array and create proper addrinfo structures
// For now, just log the count
log("Successfully resolved via systemd-resolved with {} addresses", .{addresses_array.array.items.len});
// Convert to backend result and callback
request.backend = .{ .libc = .{ .success = result_list } };
GetAddrInfoRequest.getAddrInfoAsyncCallback(0, null, request);
this.current_request = null;
}
// Remove processed data
this.read_buffer.replaceRange(0, null_pos + 1, &.{}) catch {};
// Process next request
this.processNextRequest();
}
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.prettyErrorln("[SystemdResolved] lookup called for: {s}", .{query.name});
Output.flush();
const force_systemd = bun.getRuntimeFeatureFlag(.BUN_DNS_FORCE_SYSTEMD_RESOLVED);
if (!force_systemd and !isAvailable()) {
Output.prettyErrorln("[SystemdResolved] Not available, falling back to LibC", .{});
Output.flush();
return LibC.lookup(this, query, globalThis);
}
// Log that we're using systemd-resolved
Output.prettyErrorln("[SystemdResolved] USING SYSTEMD-RESOLVED for DNS resolution of {s}", .{query.name});
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 {
Output.prettyErrorln("[SystemdResolved] Failed to create connection, falling back", .{});
// 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 {
Output.prettyErrorln("[SystemdResolved] Failed to send request, falling back", .{});
// 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());
Output.prettyErrorln("[SystemdResolved] Request sent via systemd-resolved for {s}", .{query.name});
return promise_value;
}
};
const LibC = struct {
pub fn lookup(this: *Resolver, query_init: GetAddrInfo, globalThis: *jsc.JSGlobalObject) jsc.JSValue {
if (Environment.isWindows) {
@@ -731,6 +1077,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 +1152,30 @@ 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| {
Output.prettyErrorln("[SystemdResolved] run() - NOT running in task thread, should connect async for: {s}", .{query.name});
// 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 => {
Output.prettyErrorln("[SystemdResolved] then() - processing systemd-resolved results", .{});
return;
},
.libc => {},
else => return,
}
switch (this.backend.libc) {
.success => |result| {
const any = GetAddrInfo.Result.Any{ .list = result };
@@ -2769,8 +3134,10 @@ pub const Resolver = struct {
pub fn doLookup(this: *Resolver, name: []const u8, port: u16, options: GetAddrInfo.Options, globalThis: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue {
var opts = options;
var backend = opts.backend;
Output.prettyErrorln("[DNS] doLookup called with backend: {s}", .{@tagName(backend)});
const normalized = normalizeDNSName(name, &backend);
opts.backend = backend;
Output.prettyErrorln("[DNS] Backend after normalization: {s}", .{@tagName(backend)});
const query = GetAddrInfo{
.options = opts,
.port = port,
@@ -2784,6 +3151,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 +3879,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,
vm: *jsc.VirtualMachine,
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(vm: *jsc.VirtualMachine) !*SystemdResolvedConnection {
const allocator = vm.allocator;
const this = try allocator.create(SystemdResolvedConnection);
this.* = .{
.vm = vm,
.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.vm.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.vm.uwsLoop(), @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.vm.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,251 @@
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,
vm: *jsc.VirtualMachine,
var global_instance: ?*SystemdResolved = null;
pub fn init(vm: *jsc.VirtualMachine) !*SystemdResolved {
if (global_instance) |instance| {
return instance;
}
const allocator = vm.allocator;
var this = try allocator.create(SystemdResolved);
this.* = .{
.vm = vm,
};
if (SystemdResolvedBackend.SystemdResolvedConnection.isAvailable()) {
this.connection = try SystemdResolvedBackend.SystemdResolvedConnection.init(vm);
}
global_instance = this;
return this;
}
pub fn deinit(this: *SystemdResolved) void {
if (this.connection) |conn| {
conn.deinit();
}
this.vm.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 systemd = global_instance orelse blk: {
const instance = init(vm) 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);
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);
this.vm.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);
this.vm.allocator.destroy(request);
return promise_value;
};
this.requestSent(globalThis.bunVM());
return promise_value;
}
const CallbackContext = struct {
request: *GetAddrInfoRequest,
globalThis: *jsc.JSGlobalObject,
resolver: *dns.Resolver,
};
fn onResolveComplete(
req: *SystemdResolvedBackend.SystemdResolvedConnection.Request,
result: ?*SystemdResolvedBackend.SystemdResolvedConnection.ResolveResult,
err: ?*SystemdResolvedBackend.SystemdResolvedConnection.ResolveError,
) void {
const context = @as(*CallbackContext, @ptrCast(@alignCast(req.context)));
const request = context.request;
const globalThis = context.globalThis;
_ = context.resolver;
const allocator = globalThis.allocator();
defer {
allocator.free(req.name);
allocator.destroy(req);
allocator.destroy(context);
}
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;
GetAddrInfoRequest.getAddrInfoAsyncCallback(errno, null, request);
return;
}
if (result) |res| {
defer res.deinit(allocator);
if (res.addresses.len == 0) {
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOENT), null, request);
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);
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOMEM), null, request);
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);
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOMEM), null, request);
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);
GetAddrInfoRequest.getAddrInfoAsyncCallback(@intFromEnum(std.posix.E.NOMEM), null, request);
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;
}
}
GetAddrInfoRequest.getAddrInfoAsyncCallback(0, head, request);
} else {
GetAddrInfoRequest.getAddrInfoAsyncCallback(-1, null, request);
}
}
};

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