mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 00:48:55 +00:00
Compare commits
3 Commits
dylan/test
...
claude/sys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0aeda72a0 | ||
|
|
c912690e5b | ||
|
|
8ce601b171 |
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
440
src/dns/systemd-resolved-backend.zig
Normal file
440
src/dns/systemd-resolved-backend.zig
Normal 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;
|
||||
};
|
||||
}
|
||||
};
|
||||
337
src/dns/systemd-resolved.zig
Normal file
337
src/dns/systemd-resolved.zig
Normal 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
138
src/dns/varlink.zig
Normal 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];
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
196
test/js/bun/dns/systemd-resolved.test.ts
Normal file
196
test/js/bun/dns/systemd-resolved.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user