diff --git a/src/bun.js/api/bun/dns.zig b/src/bun.js/api/bun/dns.zig index a9fca57010..5fd0c099c4 100644 --- a/src/bun.js/api/bun/dns.zig +++ b/src/bun.js/api/bun/dns.zig @@ -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; diff --git a/src/dns.zig b/src/dns.zig index e3bc939830..19cd19bf5c 100644 --- a/src/dns.zig +++ b/src/dns.zig @@ -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, }; diff --git a/src/dns/systemd-resolved-backend.zig b/src/dns/systemd-resolved-backend.zig new file mode 100644 index 0000000000..b8982e0a77 --- /dev/null +++ b/src/dns/systemd-resolved-backend.zig @@ -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("", 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; + }; + } +}; \ No newline at end of file diff --git a/src/dns/systemd-resolved.zig b/src/dns/systemd-resolved.zig new file mode 100644 index 0000000000..8536854e28 --- /dev/null +++ b/src/dns/systemd-resolved.zig @@ -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); + } + } +}; \ No newline at end of file diff --git a/src/dns/varlink.zig b/src/dns/varlink.zig new file mode 100644 index 0000000000..dd98ec50aa --- /dev/null +++ b/src/dns/varlink.zig @@ -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]; +} \ No newline at end of file diff --git a/src/feature_flags.zig b/src/feature_flags.zig index da55db656e..7213318d5a 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -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, diff --git a/test/js/bun/dns/systemd-resolved.test.ts b/test/js/bun/dns/systemd-resolved.test.ts new file mode 100644 index 0000000000..4bec37f353 --- /dev/null +++ b/test/js/bun/dns/systemd-resolved.test.ts @@ -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"); + }); +}); \ No newline at end of file