From e47f05f424436e5ce369b189082ba07e848b8e11 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:11:49 +0200 Subject: [PATCH 1/2] Use the host DNS resolver in Darling --- .gitattributes | 6 +- docs/TESTING.md | 13 +++ docs/TROUBLESHOOTING.md | 25 ++++++ patches/darling.patch | 15 +++- patches/xnu.patch | 20 +++++ tests/test_host_dns.py | 133 ++++++++++++++++++++++++++++ tests/test_runtime_compatibility.py | 2 +- upstream/manifest.json | 2 +- 8 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 tests/test_host_dns.py diff --git a/.gitattributes b/.gitattributes index 00c1a6e..0cd6baf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/docs/TESTING.md b/docs/TESTING.md index 17a711a..ca9bd88 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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: diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index a655648..9b96bfe 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -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 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: diff --git a/patches/darling.patch b/patches/darling.patch index a4d2178..ade6461 100644 --- a/patches/darling.patch +++ b/patches/darling.patch @@ -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) diff --git a/patches/xnu.patch b/patches/xnu.patch index efbf7e3..a02f9cd 100644 --- a/patches/xnu.patch +++ b/patches/xnu.patch @@ -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; + } diff --git a/tests/test_host_dns.py b/tests/test_host_dns.py new file mode 100644 index 0000000..9a26d07 --- /dev/null +++ b/tests/test_host_dns.py @@ -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 +#include +#include +#include +#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') diff --git a/tests/test_runtime_compatibility.py b/tests/test_runtime_compatibility.py index 741917e..1539369 100644 --- a/tests/test_runtime_compatibility.py +++ b/tests/test_runtime_compatibility.py @@ -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"), diff --git a/upstream/manifest.json b/upstream/manifest.json index 60fbbb8..331ce8f 100644 --- a/upstream/manifest.json +++ b/upstream/manifest.json @@ -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", From 5f71c82dc5b00c0f16bed0c9fc444ab76c651746 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:22:17 +0200 Subject: [PATCH 2/2] Bypass proxies when checking guest DNS --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 9b96bfe..9ecb58e 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -210,7 +210,7 @@ prefix `/etc` directory can also shadow the base `/etc -> private/etc` link. Check resolution inside the guest with: ```sh -darling shell curl https://clientconfig.rpg.riotgames.com/ +darling shell curl --noproxy '*' https://clientconfig.rpg.riotgames.com/ ``` An HTTP response establishes that resolution succeeded even if that endpoint