Merge pull request #100 from lucannez64/fix/host-dns

Use the host DNS resolver in Darling
This commit is contained in:
Kalah authored and GitHub committed 2026-10-19 19:06:55 +02:00
commit 6865e1eb6e
8 files changed
+209 -7

No files matched your search

+3 -3
View File
@@ -1,3 +1,3 @@
# Unified-diff context uses a single space for an empty source line.
# Do not report that patch syntax as trailing source whitespace.
patches/*.patch whitespace=-blank-at-eol
# Unified-diff context adds a space before source tabs and empty lines.
# Do not report that patch syntax as source whitespace errors.
patches/*.patch whitespace=-blank-at-eol,-space-before-tab
+13
View File
@@ -90,6 +90,19 @@ append-only output and tightening an existing public log to mode 0600. The
runner test uses a fake `darling` executable to verify loopback fixtures and
private-prefix-only shutdown/cleanup.
With the pinned Darling sources available, run the host resolver regression:
```sh
python3 -m unittest discover -s tests -p 'test_host_dns.py' -v
```
It compiles the actual patched vchroot implementation in its Linux test mode
and reads temporary resolver files through absolute and relative host symlinks.
It checks atomic file replacement, symlink retargeting, guest DNS overrides and
guest absolute-link scope. A CMake fixture verifies that both install modes
replace an older Google resolver file with the host link. No Darling session or
DNS traffic is started.
## CoreText font-list and AppKit resize regressions
After preparing the source and configuring the runtime tools:
+25
View File
@@ -192,6 +192,31 @@ observation, without waiting for a match:
python3 tools/close-league-client-in-game.py --once --dry-run
```
### Riot reports NO INTERNET FOUND while the host is online
Older base runtimes ship Google DNS servers (`8.8.8.8` and `8.8.4.4`), which
some mobile and managed networks block. The base resolver now links to the
host's `/etc/resolv.conf` through `/Volumes/SystemRoot`, including when the host
file is a symlink into `/run` for systemd-resolved or NetworkManager. File and
symlink replacements remain visible without copying the resolver on launch.
Exit Riot and League, then run `./scripts/update.sh` to rebuild and install both
the base link and the host symlink handling correction. Existing prefix DNS
overrides in `$DPREFIX/private/etc/resolv.conf` (normally
`~/.darling/private/etc/resolv.conf`) remain in effect; remove a previous manual
override with Darling stopped if you want to follow the host instead. A custom
prefix `/etc` directory can also shadow the base `/etc -> private/etc` link.
Check resolution inside the guest with:
```sh
darling shell curl --noproxy '*' https://clientconfig.rpg.riotgames.com/
```
An HTTP response establishes that resolution succeeded even if that endpoint
rejects the request. This change follows the host resolver configuration;
already-running clients may cache DNS settings until restarted.
### Downloads repeatedly fail with an IPv6 CONNECT_TO error
The bundled downloader was observed retrying CDN chunks with:
+13 -2
View File
@@ -11230,10 +11230,21 @@ index 072a6e2..c804100 100755
echo "No instances running now"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8077b7e..0556080 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -236,7 +236,12 @@ InstallSymlink(/Volumes/SystemRoot/usr/share/zoneinfo ${CMAKE_INSTALL_PREFIX}/li
@@ -204,8 +204,9 @@
InstallSymlink(private/etc ${CMAKE_INSTALL_PREFIX}/libexec/darling/etc)
InstallSymlink(private/var ${CMAKE_INSTALL_PREFIX}/libexec/darling/var)
-install(FILES etc/resolv.conf
- DESTINATION libexec/darling/private/etc)
+# Follow the host resolver, including local DNS proxies and network changes.
+# Prefix-local resolver overrides still take precedence over this base symlink.
+InstallSymlink(/Volumes/SystemRoot/etc/resolv.conf ${CMAKE_INSTALL_PREFIX}/libexec/darling/private/etc/resolv.conf)
InstallSymlink(/Volumes/SystemRoot/etc/machine-id ${CMAKE_INSTALL_PREFIX}/libexec/darling/private/etc/machine-id)
InstallSymlink(/Volumes/SystemRoot/etc/nsswitch.conf ${CMAKE_INSTALL_PREFIX}/libexec/darling/private/etc/nsswitch.conf)
@@ -236,7 +237,12 @@
install(DIRECTORY DESTINATION libexec/darling/Library/Preferences)
if(NOT DEBIAN_PACKAGING)
+20
View File
@@ -6180,3 +6180,23 @@ index 0000000..eb90f43
+ return true;
+}
+#endif
diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchroot_userspace.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchroot_userspace.c
--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchroot_userspace.c
+++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchroot_userspace.c
@@ -421,12 +421,14 @@
ctxt->follow = true;
ctxt->symlink_depth++;
- // resolve absolute symlinks within the prefix
+ // Keep absolute host symlinks outside the prefix after crossing
+ // /Volumes/SystemRoot (e.g. /etc/resolv.conf -> /run/...).
+ // Resolve guest symlinks within the prefix
//
// however, DON'T resolve `/proc` symlinks within the prefix.
// anything under `/proc` is from Linux's `/proc` since we bind-mount it, so we can special-case them because we want those to be Linux-root relative.
// see https://github.com/darlinghq/darling/issues/1052 for the issue this works around.
- if (link[0] == '/' && !(strncmp(link, "/proc/", 6) == 0 || (rv == 5 && strncmp(link, "/proc", 5) == 0))) {
+ if (link[0] == '/' && ctxt->current_root_len > 0 && !(strncmp(link, "/proc/", 6) == 0 || (rv == 5 && strncmp(link, "/proc", 5) == 0))) {
ctxt->current_root = prefix_path;
ctxt->current_root_len = prefix_path_len;
}
+133
View File
@@ -0,0 +1,133 @@
"""Exercise the base resolver install and actual vchroot code without DNS traffic."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
import test_runtime_compatibility as sources
@unittest.skipUnless(shutil.which('cc') and shutil.which('patch'), 'C compiler and patch required')
class HostDNSChecks(unittest.TestCase):
patched = sources.CompatibilityChecks.patched
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix='host-dns-')
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.prefix = self.root / 'guest'
(self.prefix / 'private/etc').mkdir(parents=True)
(self.prefix / 'Volumes/SystemRoot').mkdir(parents=True)
(self.prefix / 'etc').symlink_to('private/etc')
self.resolver = self.prefix / 'private/etc/resolv.conf'
def build_vchroot(self):
path = 'darling/src/libsystem_kernel/emulation/'
source = self.patched('xnu', path + 'src/linux_premigration/vchroot_userspace.c')
header = self.root / 'darling/emulation/linux_premigration/vchroot_expand.h'
header.parent.mkdir(parents=True)
header.write_text(self.patched('xnu', path + 'include/linux_premigration/vchroot_expand.h'))
# Use upstream's Linux TEST mode, replacing only its CLI and RPC setup.
fixture = r'''
#define _GNU_SOURCE
#define TEST
#include <stdint.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#define LC_C_LOCALE 0
#define strncasecmp_l(a,b,n,l) strncasecmp(a,b,n)
#define __simple_printf printf
#define __simple_abort abort
static int dserver_rpc_vchroot_path(char *p, unsigned long n, uint64_t *len) { abort(); }
#define main upstream_main
'''
fixture += source + r'''
#undef main
int main(int argc, const char **argv) {
if (argc != 3) return 2;
int fd = open(argv[1], O_RDONLY | O_DIRECTORY);
if (fd < 0 || __darling_vchroot(fd)) return 3;
close(fd);
struct vchroot_expand_args args = {.dfd = -100, .flags = VCHROOT_FOLLOW};
strcpy(args.path, argv[2]);
int result = vchroot_expand(&args);
if (result) return 4;
FILE *file = fopen(args.path, "r");
if (!file) return 5;
char buf[4096];
while (fgets(buf, sizeof(buf), file)) fputs(buf, stdout);
return fclose(file) != 0;
}
'''
file = self.root / 'check.c'
file.write_text(fixture)
self.binary = self.root / 'check'
result = subprocess.run(['cc', '-std=gnu11', '-I', str(self.root), str(file),
'-o', str(self.binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
def read_guest(self, path='/etc/resolv.conf'):
result = subprocess.run([str(self.binary), str(self.prefix), path],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout
def test_host_symlink_chains_updates_and_guest_override(self):
self.build_vchroot()
host = self.root / 'host'
host.mkdir()
data = host / 'stub-resolv.conf'
data.write_text('nameserver 127.0.0.53\nsearch tether.example\n')
self.resolver.symlink_to('/Volumes/SystemRoot' + str(data))
self.assertEqual(self.read_guest(), data.read_text())
self.resolver.unlink()
# An absolute host link followed by a relative one models resolved and
# NetworkManager layouts. Never write to the real host resolver.
(host / 'relative').symlink_to(data.name)
(host / 'resolv.conf').symlink_to(host / 'relative')
self.resolver.symlink_to('/Volumes/SystemRoot' + str(host / 'resolv.conf'))
self.assertEqual(self.read_guest(), data.read_text())
# Network managers replace files atomically; a bind mount or snapshot
# would keep reading the old resolver inode.
replacement = host / 'next'
replacement.write_text('nameserver 192.0.2.53\n')
os.replace(replacement, data)
self.assertEqual(self.read_guest(), data.read_text())
other = host / 'other'
other.write_text('nameserver 192.0.2.54\n')
(host / 'resolv.conf').unlink()
(host / 'resolv.conf').symlink_to(other)
self.assertEqual(self.read_guest(), other.read_text())
self.resolver.unlink()
self.resolver.write_text('nameserver 127.0.0.1\n')
self.assertEqual(self.read_guest(), self.resolver.read_text())
# Absolute links originating inside the guest must retain guest scope.
(self.prefix / 'guest-link').symlink_to('/private/etc/resolv.conf')
self.assertEqual(self.read_guest('/guest-link'), self.resolver.read_text())
@unittest.skipUnless(shutil.which('cmake'), 'CMake required')
def test_base_install_replaces_google_file_with_host_link(self):
cmake = self.patched('darling', 'CMakeLists.txt')
start = cmake.index('# Follow the host resolver')
end = cmake.index('\n', cmake.index('InstallSymlink(', start))
macro = self.patched('darling', 'cmake/InstallSymlink.cmake')
project = self.root / 'project'
project.mkdir()
(project / 'CMakeLists.txt').write_text(
'cmake_minimum_required(VERSION 3.16)\nproject(dns NONE)\n' + macro + '\n' + cmake[start:end] + '\n')
base = self.root / 'base'
target = base / 'libexec/darling/private/etc/resolv.conf'
target.parent.mkdir(parents=True)
for packaging in ('OFF', 'ON'):
target.unlink(missing_ok=True)
target.write_text('nameserver 8.8.8.8\nnameserver 8.8.4.4\n')
build = self.root / ('build-' + packaging)
subprocess.run(['cmake', '-S', str(project), '-B', str(build),
'-DCMAKE_INSTALL_PREFIX=' + str(base),
'-DBINARY_PACKAGING_MODE=' + packaging], check=True, capture_output=True)
subprocess.run(['cmake', '--install', str(build)], check=True, capture_output=True)
self.assertTrue(target.is_symlink())
self.assertEqual(os.readlink(target), '/Volumes/SystemRoot/etc/resolv.conf')
+1 -1
View File
@@ -38,7 +38,7 @@ class CompatibilityChecks(unittest.TestCase):
patch = patch_text.split(marker, 1)[1].split('\ndiff --git ', 1)[0]
with tempfile.TemporaryDirectory() as folder:
target = Path(folder) / path
target.parent.mkdir(parents=True)
target.parent.mkdir(parents=True, exist_ok=True)
if original is not None:
target.write_bytes(original)
subprocess.run(['patch', '--silent', '-p1', '-d', folder], input=marker + patch + ("" if patch.endswith("\n") else "\n"),
+1 -1
View File
@@ -274,7 +274,7 @@
"revision": "fa29287aa2f0115271e091f1031f53c9e024005d",
"repository": "https://github.com/darlinghq/darling-xnu.git",
"patch": "patches/xnu.patch",
"modified_files": 64,
"modified_files": 65,
"new_files": [
"darling/src/libsystem_kernel/emulation/include/common/cpu_topology.h",
"darling/src/libsystem_kernel/emulation/include/common/private_mutex.h",