diff --git a/.gitattributes b/.gitattributes index b3b62262bbd3..c5f421723535 100644 --- a/.gitattributes +++ b/.gitattributes @@ -28,6 +28,7 @@ test/aot/parallel/the-head-of-the-text-of-a-function-is-laid-as-bun-prints-it/** # Compared line by line with what a runner prints, which ends a line with \n on every system. test/cli/format/*/expected.txt text eol=lf test/cli/lint/conformance/expected*.txt text eol=lf +test/js/node/dns/dns-simulation-c-ares.txt text eol=lf # Embedded in the executable, which splits them at \n. src/lint/linter/config/presets.txt text eol=lf src/lint/linter/deprecated.txt text eol=lf diff --git a/.github/workflows/update-cares.yml b/.github/workflows/update-cares.yml deleted file mode 100644 index 0c33d7afc6e5..000000000000 --- a/.github/workflows/update-cares.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: Update c-ares - -permissions: - contents: read - -on: - schedule: - - cron: "0 4 * * 0" - workflow_dispatch: - -jobs: - check-update: - if: github.repository == 'oven-sh/bun' - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Check c-ares version - id: check-version - run: | - set -euo pipefail - - DEP_FILE=scripts/build/deps/cares.ts - CURRENT_VERSION=$(sed -nE 's/^const CARES_COMMIT = "([0-9a-f]{40})";$/\1/p' "$DEP_FILE") - - if [ -z "$CURRENT_VERSION" ]; then - echo "Error: Could not find CARES_COMMIT in $DEP_FILE" - exit 1 - fi - - # Validate that it looks like a git hash - if ! [[ $CURRENT_VERSION =~ ^[0-9a-f]{40}$ ]]; then - echo "Error: Invalid git hash format in $DEP_FILE" - echo "Found: $CURRENT_VERSION" - echo "Expected: 40 character hexadecimal string" - exit 1 - fi - - echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT - - LATEST_RELEASE=$(curl -sL https://api.github.com/repos/c-ares/c-ares/releases/latest) - if [ -z "$LATEST_RELEASE" ]; then - echo "Error: Failed to fetch latest release from GitHub API" - exit 1 - fi - - LATEST_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tag_name') - if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then - echo "Error: Could not extract tag name from GitHub API response" - exit 1 - fi - - LATEST_TAG_SHA=$(curl -sL "https://api.github.com/repos/c-ares/c-ares/git/refs/tags/$LATEST_TAG" | jq -r '.object.sha') - if [ -z "$LATEST_TAG_SHA" ] || [ "$LATEST_TAG_SHA" = "null" ]; then - echo "Error: Could not fetch SHA for tag $LATEST_TAG" - exit 1 - fi - LATEST_SHA=$(curl -sL "https://api.github.com/repos/c-ares/c-ares/git/tags/$LATEST_TAG_SHA" | jq -r '.object.sha') - if [ -z "$LATEST_SHA" ] || [ "$LATEST_SHA" = "null" ]; then - echo "Error: Could not fetch SHA for tag $LATEST_TAG @ $LATEST_TAG_SHA" - exit 1 - fi - - if ! [[ $LATEST_SHA =~ ^[0-9a-f]{40}$ ]]; then - echo "Error: Invalid SHA format received from GitHub" - echo "Found: $LATEST_SHA" - echo "Expected: 40 character hexadecimal string" - exit 1 - fi - - echo "latest=$LATEST_SHA" >> $GITHUB_OUTPUT - echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT - - - name: Update version if needed - if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest - env: - LATEST: ${{ steps.check-version.outputs.latest }} - run: | - set -euo pipefail - sed -i -E 's/^(const CARES_COMMIT = ")[0-9a-f]{40}(";)$/\1'"$LATEST"'\2/' scripts/build/deps/cares.ts - - - name: Create Pull Request - if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - add-paths: | - scripts/build/deps/cares.ts - commit-message: "deps: update c-ares to ${{ steps.check-version.outputs.tag }} (${{ steps.check-version.outputs.latest }})" - title: "deps: update c-ares to ${{ steps.check-version.outputs.tag }}" - delete-branch: true - branch: deps/update-cares - body: | - ## What does this PR do? - - Updates c-ares to version ${{ steps.check-version.outputs.tag }} - - Compare: https://github.com/c-ares/c-ares/compare/${{ steps.check-version.outputs.current }}...${{ steps.check-version.outputs.latest }} - - Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-cares.yml) diff --git a/CLAUDE.md b/CLAUDE.md index 8feccff501bc..6916fe6efa65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,7 +147,7 @@ The Rust side is a Cargo workspace of ~200 crates rooted at `Cargo.toml`. The ke #### Vendored Dependencies (`vendor/`) -Third-party native libraries are vendored locally and can be read from disk (not git submodules): boringssl (TLS/crypto), brotli, cares (async DNS), highway (SIMD), libdeflate, libuv (Windows event loop), lolhtml (HTML rewriter), lshpack (HTTP/2 HPACK), lsqpack + lsquic (HTTP/3), mimalloc (allocator), nodejs (headers), picohttpparser, rust-argon2 (argon2 for `Bun.password`, Rust), tinycc (FFI JIT, fork: oven-sh/tinycc), WebKit (JavaScriptCore), zlib (zlib-ng), zstd. Build configuration for these is in `scripts/build/deps/*.ts`. +Third-party native libraries are vendored locally and can be read from disk (not git submodules): boringssl (TLS/crypto), brotli, highway (SIMD), libdeflate, libuv (Windows event loop), lolhtml (HTML rewriter), lshpack (HTTP/2 HPACK), lsqpack + lsquic (HTTP/3), mimalloc (allocator), nodejs (headers), picohttpparser, rust-argon2 (argon2 for `Bun.password`, Rust), tinycc (FFI JIT, fork: oven-sh/tinycc), WebKit (JavaScriptCore), zlib (zlib-ng), zstd. Build configuration for these is in `scripts/build/deps/*.ts`. ### JavaScript Class Implementation (C++) diff --git a/Cargo.lock b/Cargo.lock index 93d0ec1d51cc..345642a784e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,9 +295,7 @@ version = "0.0.0" dependencies = [ "bun_alloc", "bun_boringssl_sys", - "bun_cares_sys", "bun_core", - "bun_sys", ] [[package]] @@ -425,18 +423,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "bun_cares_sys" -version = "0.0.0" -dependencies = [ - "bun_alloc", - "bun_core", - "bun_libuv_sys", - "bun_opaque", - "libc", - "strum", -] - [[package]] name = "bun_clap" version = "0.0.0" @@ -599,8 +585,10 @@ dependencies = [ name = "bun_dns" version = "0.0.0" dependencies = [ - "bun_cares_sys", + "bstr", "bun_core", + "bun_dns_resolver", + "bun_libuv_sys", "bun_sys", "bun_windows_sys", "bun_wyhash", @@ -608,6 +596,14 @@ dependencies = [ "strum", ] +[[package]] +name = "bun_dns_resolver" +version = "0.0.0" +dependencies = [ + "bun_core", + "thiserror", +] + [[package]] name = "bun_dotenv" version = "0.0.0" @@ -1612,7 +1608,6 @@ dependencies = [ "bun_bundler", "bun_bundler_jsc", "bun_bunfig", - "bun_cares_sys", "bun_clap", "bun_collections", "bun_core", @@ -1621,6 +1616,7 @@ dependencies = [ "bun_css", "bun_css_jsc", "bun_dns", + "bun_dns_resolver", "bun_dotenv", "bun_errno", "bun_event_loop", diff --git a/Cargo.toml b/Cargo.toml index 700ef89cb5d0..04aaf8cd7493 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,13 +53,13 @@ members = [ "src/threading", "src/valkey", "src/watcher", - "src/cares_sys", "src/clap", "src/clap_macros", "src/crash_handler", "src/csrf", "src/css_jsc", "src/dns", + "src/dns_resolver", "src/dotenv", "src/event_loop", "src/executable", @@ -434,13 +434,13 @@ bun_tar = { path = "src/tar" } bun_threading = { path = "src/threading" } bun_valkey = { path = "src/valkey" } bun_watcher = { path = "src/watcher" } -bun_cares_sys = { path = "src/cares_sys" } bun_clap = { path = "src/clap" } bun_clap_macros = { path = "src/clap_macros" } bun_crash_handler = { path = "src/crash_handler" } bun_csrf = { path = "src/csrf" } bun_css_jsc = { path = "src/css_jsc" } bun_dns = { path = "src/dns" } +bun_dns_resolver = { path = "src/dns_resolver" } bun_dotenv = { path = "src/dotenv" } bun_event_loop = { path = "src/event_loop" } bun_exe_format = { path = "src/exe_format" } diff --git a/LICENSE.md b/LICENSE.md index 6abe6776e993..384d93e57d57 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -34,7 +34,7 @@ Bun statically links these libraries: | [`tinycc`](https://github.com/tinycc/tinycc) | LGPL v2.1 | | [`uSockets`](https://github.com/uNetworking/uSockets) | Apache 2.0 | | [`zlib-ng`](https://github.com/zlib-ng/zlib-ng) | zlib | -| [`c-ares`](https://github.com/c-ares/c-ares) | MIT licensed | +| [`c-ares`](https://github.com/c-ares/c-ares) (not linked: `src/dns_resolver` and `src/bun_core/ip_address.rs` are derived from parts of it) | MIT licensed | | [`libicu`](https://github.com/unicode-org/icu) 78 | [license here](https://github.com/unicode-org/icu/blob/main/icu4c/LICENSE) | | [`libbase64`](https://github.com/aklomp/base64/blob/master/LICENSE) | BSD 2-Clause | | [`libuv`](https://github.com/libuv/libuv) (on Windows) | MIT | diff --git a/docs/project/license.mdx b/docs/project/license.mdx index 3e7404e936b0..44144f0ac6e9 100644 --- a/docs/project/license.mdx +++ b/docs/project/license.mdx @@ -39,7 +39,7 @@ Bun statically links these libraries: | [`tinycc`](https://github.com/tinycc/tinycc) | LGPL v2.1 | | [`uSockets`](https://github.com/uNetworking/uSockets) | Apache 2.0 | | [`zlib-ng`](https://github.com/zlib-ng/zlib-ng) | zlib | -| [`c-ares`](https://github.com/c-ares/c-ares) | MIT licensed | +| [`c-ares`](https://github.com/c-ares/c-ares) (not linked: `src/dns_resolver` and `src/bun_core/ip_address.rs` are derived from parts of it) | MIT licensed | | [`libicu`](https://github.com/unicode-org/icu) 78 | [ICU license](https://github.com/unicode-org/icu/blob/main/icu4c/LICENSE) | | [`libbase64`](https://github.com/aklomp/base64/blob/master/LICENSE) | BSD 2-Clause | | [`libuv`](https://github.com/libuv/libuv) (on Windows) | MIT | diff --git a/docs/runtime/networking/dns.mdx b/docs/runtime/networking/dns.mdx index 5a5888ed9040..5f35c827affc 100644 --- a/docs/runtime/networking/dns.mdx +++ b/docs/runtime/networking/dns.mdx @@ -25,7 +25,7 @@ dns.prefetch("bun.com", 443); `Bun.dns.lookup()` accepts a `backend` option that selects the resolver implementation: -- `"c-ares"`: the [c-ares](https://c-ares.org/) asynchronous resolver. It reads `/etc/resolv.conf` directly, so it does not consult NSS modules such as `systemd-resolved`. This backend is the default for `Bun.dns.lookup()` on Linux. +- `"c-ares"`: Bun's own asynchronous resolver, which behaves like the [c-ares](https://c-ares.org/) library it replaced. It reads `/etc/resolv.conf` directly, so it does not consult NSS modules such as `systemd-resolved`. This backend is the default for `Bun.dns.lookup()` on Linux. - `"system"`: the platform's own resolver (the non-blocking system API on macOS, `getaddrinfo` on a thread pool everywhere else). This backend is the default on macOS, Windows, and Android. - `"getaddrinfo"` (alias `"libc"`): the POSIX `getaddrinfo(3)` function on a thread pool. @@ -35,7 +35,7 @@ import { dns } from "bun"; const [{ address }] = await dns.lookup("example.com", { backend: "system" }); ``` -`node:dns.lookup()` always uses the `"system"` backend to match Node.js, whose `dns.lookup()` is documented as calling `getaddrinfo(3)`. The `node:dns.resolve*()` functions use c-ares, as they do in Node.js. +`node:dns.lookup()` always uses the `"system"` backend to match Node.js, whose `dns.lookup()` is documented as calling `getaddrinfo(3)`. The `node:dns.resolve*()` functions send DNS queries themselves with the resolver behind the `"c-ares"` backend. Node.js uses c-ares for them. ## DNS caching in Bun diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 8279a41e75f7..a8e22b4ce488 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2549,7 +2549,7 @@ declare module "bun" { * ## DNS resolver client * * Bun supports three DNS resolvers: - * - `c-ares` - Uses the c-ares library to perform DNS resolution. This is the default on Linux. + * - `c-ares` - Bun's own asynchronous resolver. It keeps the name of the c-ares library, which it replaced and whose behavior it follows. This is the default on Linux. * - `system` - Uses the system's non-blocking DNS resolver API if available, falls back to `getaddrinfo`. This is the default on macOS and the same as `getaddrinfo` on Linux. * - `getaddrinfo` - Uses the POSIX standard `getaddrinfo` function. Causes performance issues under concurrent loads. * @@ -2578,8 +2578,7 @@ declare module "bun" { * The DNS resolver implementation to use * * Defaults to `"c-ares"` on Linux and `"system"` on macOS. This default - * may change in a future version of Bun if c-ares is not reliable - * enough. + * may change in a future version of Bun. * * On macOS, `system` uses the builtin macOS [non-blocking DNS * resolution @@ -2604,8 +2603,7 @@ declare module "bun" { * `"system"` uses the same API underneath (except non-blocking). * * On Windows, libuv's non-blocking DNS resolver is used by default, and - * when specifying backends "system", "libc", or "getaddrinfo". The c-ares - * backend isn't supported on Windows. + * when specifying backends "system", "libc", or "getaddrinfo". */ backend?: "libc" | "c-ares" | "system" | "getaddrinfo"; }, diff --git a/scripts/build/bun.ts b/scripts/build/bun.ts index cd3061801d5d..2b0391609f37 100644 --- a/scripts/build/bun.ts +++ b/scripts/build/bun.ts @@ -109,6 +109,7 @@ function systemLibs(cfg: Config): string[] { "userenv.lib", "dbghelp.lib", "crypt32.lib", + "iphlpapi.lib", // libuv, and the DNS servers of the adapters "wsock32.lib", // ws2_32 + wsock32 — wsock32 has TransmitFile (sendfile equiv) "ws2_32.lib", "delayimp.lib", // required for /delayload: in release diff --git a/scripts/build/deps/README.md b/scripts/build/deps/README.md index b5e33cc258cc..1747e7457409 100644 --- a/scripts/build/deps/README.md +++ b/scripts/build/deps/README.md @@ -146,7 +146,6 @@ export const mydep: Dependency = { - **mimalloc.ts** — direct build, single unity TU compiled as C++ - **tinycc.ts** — direct build with a build-time codegen tool - **zlib.ts** — direct build with per-source SIMD `-m` flags + `.h.in` substitution -- **cares.ts** — direct build with hand-written per-target config.h - **boringssl.ts** — direct build with NASM assembly (win-x64) and a large gen/ manifest; `forbidUndefined` (with libuv.ts) keeps a dep that bun points at mimalloc from calling libc's allocator behind its back - **sqlite.ts** — direct build, in-tree source (lives in `src/`, not `vendor/`) - **libuv.ts** — `enabled: cfg => cfg.windows` for a platform-only dep diff --git a/scripts/build/deps/cares.ts b/scripts/build/deps/cares.ts deleted file mode 100644 index 3ab129ea660c..000000000000 --- a/scripts/build/deps/cares.ts +++ /dev/null @@ -1,297 +0,0 @@ -/** - * c-ares — async DNS resolver. Backs node:dns and the Happy Eyeballs logic - * in bun's HTTP client. Async is the point — libc's getaddrinfo blocks. - * - * DirectBuild: cmake's configure step runs ~130 try_compile probes to fill - * ares_config.h (libc function/header presence + the exact prototypes of - * recv/send/recvfrom for the casts in ares_socket.c). We pin those answers - * per target instead. - */ - -import type { Config } from "../config.ts"; -import type { Dependency } from "../source.ts"; -import { depBuildDir } from "../source.ts"; - -const CARES_COMMIT = "0b88066f79ac9745d6819899f1794df91b73c873"; - -// prettier-ignore -const SOURCES = [ - "ares_addrinfo2hostent", "ares_addrinfo_localhost", "ares_android", - "ares_cancel", "ares_close_sockets", "ares_conn", "ares_cookie", "ares_data", - "ares_destroy", "ares_free_hostent", "ares_free_string", "ares_freeaddrinfo", - "ares_getaddrinfo", "ares_getenv", "ares_gethostbyaddr", "ares_gethostbyname", - "ares_getnameinfo", "ares_hosts_file", "ares_init", "ares_library_init", - "ares_metrics", "ares_options", "ares_parse_into_addrinfo", "ares_process", - "ares_qcache", "ares_query", "ares_search", "ares_send", - "ares_set_socket_functions", "ares_socket", "ares_sortaddrinfo", - "ares_strerror", "ares_sysconfig", "ares_sysconfig_files", "ares_sysconfig_mac", - "ares_sysconfig_win", "ares_timeout", "ares_update_servers", "ares_version", - "inet_net_pton", "inet_ntop", "windows_port", - "dsa/ares_array", "dsa/ares_htable", "dsa/ares_htable_asvp", - "dsa/ares_htable_dict", "dsa/ares_htable_strvp", "dsa/ares_htable_szvp", - "dsa/ares_htable_vpstr", "dsa/ares_htable_vpvp", "dsa/ares_llist", - "dsa/ares_slist", - "event/ares_event_configchg", "event/ares_event_epoll", - "event/ares_event_kqueue", "event/ares_event_poll", "event/ares_event_select", - "event/ares_event_thread", "event/ares_event_wake_pipe", "event/ares_event_win32", - "legacy/ares_create_query", "legacy/ares_expand_name", "legacy/ares_expand_string", - "legacy/ares_fds", "legacy/ares_getsock", "legacy/ares_parse_a_reply", - "legacy/ares_parse_aaaa_reply", "legacy/ares_parse_caa_reply", - "legacy/ares_parse_mx_reply", "legacy/ares_parse_naptr_reply", - "legacy/ares_parse_ns_reply", "legacy/ares_parse_ptr_reply", - "legacy/ares_parse_soa_reply", "legacy/ares_parse_srv_reply", - "legacy/ares_parse_txt_reply", "legacy/ares_parse_uri_reply", - "record/ares_dns_mapping", "record/ares_dns_multistring", "record/ares_dns_name", - "record/ares_dns_parse", "record/ares_dns_record", "record/ares_dns_write", - "str/ares_buf", "str/ares_str", "str/ares_strsplit", - "util/ares_iface_ips", "util/ares_threads", "util/ares_timeval", - "util/ares_math", "util/ares_rand", "util/ares_uri", -]; - -export const cares: Dependency = { - name: "cares", - versionMacro: "C_ARES", - - source: () => ({ - kind: "github-archive", - repo: "c-ares/c-ares", - commit: CARES_COMMIT, - }), - - build: cfg => ({ - kind: "direct", - pic: true, - sources: SOURCES.map(s => `src/lib/${s}.c`), - includes: ["include", "src/lib", "src/lib/include"], - defines: { - HAVE_CONFIG_H: 1, - CARES_BUILDING_LIBRARY: true, - ...(cfg.windows && { - CARES_STATICLIB: true, - WIN32_LEAN_AND_MEAN: true, - _CRT_SECURE_NO_DEPRECATE: true, - _CRT_NONSTDC_NO_DEPRECATE: true, - }), - ...(cfg.linux && { - _GNU_SOURCE: true, - _POSIX_C_SOURCE: 200809, - _XOPEN_SOURCE: 700, - }), - ...(cfg.darwin && { _DARWIN_C_SOURCE: true }), - ...(cfg.freebsd && { __BSD_VISIBLE: 1 }), - }, - // _WIN32_WINNT must be the hex LITERAL 0x0602, not its decimal value — - // sdkddkver.h derives NTDDI_VERSION via token paste (`ver##0000`), so - // `1538##0000` would yield 15380000 instead of 0x06020000. Can't go - // through DirectBuild.defines (numbers emit decimal). - ...(cfg.windows && { cflags: ["-D_WIN32_WINNT=0x0602"] }), - headers: { - "ares_config.h": configH(cfg), - "ares_build.h": buildH(cfg), - }, - }), - - // ares_build.h is generated into the build dir; consumers (bun's bindings, - // node:dns) include it via → "ares_build.h". - provides: cfg => ({ - libs: [], - includes: ["include", depBuildDir(cfg, "cares")], - }), -}; - -// ─────────────────────────────────────────────────────────────────────────── -// ares_build.h — public, included by ares.h. Sets the socklen_t / ssize_t -// typedefs and pulls in the right system socket header. -// ─────────────────────────────────────────────────────────────────────────── - -function buildH(cfg: Config): string { - if (cfg.windows) { - return `#ifndef __CARES_BUILD_H -#define __CARES_BUILD_H -#define CARES_TYPEOF_ARES_SOCKLEN_T int -#define CARES_TYPEOF_ARES_SSIZE_T __int64 -#define CARES_HAVE_WINDOWS_H -#define CARES_HAVE_WS2TCPIP_H -#define CARES_HAVE_WINSOCK2_H -#include -#include -#include -#endif -`; - } - return `#ifndef __CARES_BUILD_H -#define __CARES_BUILD_H -#define CARES_TYPEOF_ARES_SOCKLEN_T socklen_t -#define CARES_TYPEOF_ARES_SSIZE_T ssize_t -#define CARES_HAVE_SYS_TYPES_H -#define CARES_HAVE_SYS_SOCKET_H -#define CARES_HAVE_SYS_SELECT_H -#define CARES_HAVE_ARPA_NAMESER_H -#define CARES_HAVE_ARPA_NAMESER_COMPAT_H -#include -#include -#include -#endif -`; -} - -// ─────────────────────────────────────────────────────────────────────────── -// ares_config.h — private, included by every .c via ares_setup.h. -// HAVE_* booleans + the recv/send prototype types c-ares -// uses to cast its socket calls. -// ─────────────────────────────────────────────────────────────────────────── - -const def1 = (names: string[]) => names.map(n => `#define ${n} 1`).join("\n"); - -// prettier-ignore -const ALWAYS = def1([ - "HAVE_ASSERT_H", "HAVE_ERRNO_H", "HAVE_FCNTL_H", "HAVE_INTTYPES_H", - "HAVE_LIMITS_H", "HAVE_MEMORY_H", "HAVE_SIGNAL_H", "HAVE_STDBOOL_H", - "HAVE_STDINT_H", "HAVE_STDLIB_H", "HAVE_STRING_H", "HAVE_TIME_H", - "HAVE_SYS_STAT_H", "HAVE_SYS_TYPES_H", - "HAVE_AF_INET6", "HAVE_PF_INET6", "HAVE_LONGLONG", - "HAVE_CONNECT", "HAVE_FCNTL", "HAVE_FREEADDRINFO", "HAVE_GETADDRINFO", - "HAVE_GETENV", "HAVE_GETHOSTNAME", "HAVE_GETNAMEINFO", "HAVE_RECV", - "HAVE_RECVFROM", "HAVE_SEND", "HAVE_SENDTO", "HAVE_SETSOCKOPT", "HAVE_SOCKET", - "HAVE_STRDUP", "HAVE_STRNLEN", "HAVE_STAT", - "HAVE_STRUCT_ADDRINFO", "HAVE_STRUCT_IN6_ADDR", "HAVE_STRUCT_SOCKADDR_IN6", - "HAVE_STRUCT_SOCKADDR_STORAGE", "HAVE_STRUCT_TIMEVAL", - "HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID", - "CARES_THREADS", -]); - -// prettier-ignore -const POSIX = def1([ - "HAVE_ARPA_INET_H", "HAVE_ARPA_NAMESER_H", "HAVE_ARPA_NAMESER_COMPAT_H", - "HAVE_DLFCN_H", "HAVE_IFADDRS_H", "HAVE_NETDB_H", "HAVE_NETINET_IN_H", - "HAVE_NETINET_TCP_H", "HAVE_NET_IF_H", "HAVE_POLL_H", "HAVE_PTHREAD_H", - "HAVE_STRINGS_H", "HAVE_SYS_IOCTL_H", "HAVE_SYS_PARAM_H", "HAVE_SYS_SELECT_H", - "HAVE_SYS_SOCKET_H", "HAVE_SYS_TIME_H", "HAVE_SYS_UIO_H", "HAVE_UNISTD_H", - "HAVE_CLOCK_GETTIME_MONOTONIC", "HAVE_FCNTL_O_NONBLOCK", - "HAVE_GETIFADDRS", "HAVE_GETTIMEOFDAY", "HAVE_IF_INDEXTONAME", - "HAVE_IF_NAMETOINDEX", "HAVE_INET_NTOP", "HAVE_INET_PTON", - "HAVE_IOCTL", "HAVE_IOCTL_FIONBIO", "HAVE_IOCTL_SIOCGIFADDR", "HAVE_MEMMEM", - "HAVE_PIPE", "HAVE_POLL", "HAVE_STRCASECMP", "HAVE_STRNCASECMP", "HAVE_WRITEV", -]); - -// prettier-ignore -const LINUX = def1([ - "HAVE_MALLOC_H", "HAVE_SYS_EPOLL_H", "HAVE_SYS_RANDOM_H", - "HAVE_EPOLL", "HAVE_GETRANDOM", - "HAVE_MSG_NOSIGNAL", "HAVE_PIPE2", -]); - -// glibc + musl have these; bionic only has the non-_r variants. -// prettier-ignore -const LINUX_NETDB_R = def1([ - "HAVE_GETSERVBYPORT_R", "HAVE_GETSERVBYNAME_R", -]); - -// Android: enables ares_sysconfig's net.dns1..4 fallback when JNI is unavailable. -// prettier-ignore -const ANDROID = def1([ - "HAVE___SYSTEM_PROPERTY_GET", -]); - -// prettier-ignore -const DARWIN = def1([ - "HAVE_SYS_EVENT_H", "HAVE_SYS_SOCKIO_H", - "HAVE_KQUEUE", "HAVE_ARC4RANDOM_BUF", -]); - -// prettier-ignore -const FREEBSD = def1([ - "HAVE_SYS_EVENT_H", "HAVE_SYS_RANDOM_H", "HAVE_SYS_SOCKIO_H", - "HAVE_KQUEUE", "HAVE_ARC4RANDOM_BUF", "HAVE_GETRANDOM", - "HAVE_MSG_NOSIGNAL", "HAVE_PIPE2", - "HAVE_GETSERVBYPORT_R", "HAVE_GETSERVBYNAME_R", -]); - -// prettier-ignore -const WINDOWS = def1([ - "HAVE_IPHLPAPI_H", "HAVE_MSWSOCK_H", "HAVE_NETIOAPI_H", "HAVE_WINDOWS_H", - "HAVE_WINSOCK2_H", "HAVE_WS2IPDEF_H", "HAVE_WS2TCPIP_H", "HAVE_IO_H", - "HAVE_CLOSESOCKET", "HAVE_CONVERTINTERFACEINDEXTOLUID", - "HAVE_CONVERTINTERFACELUIDTONAMEA", "HAVE_GETBESTROUTE2", - "HAVE_IF_INDEXTONAME", "HAVE_IF_NAMETOINDEX", "HAVE_INET_NTOP", "HAVE_INET_PTON", - "HAVE_IOCTLSOCKET", "HAVE_IOCTLSOCKET_FIONBIO", - "HAVE_NOTIFYIPINTERFACECHANGE", "HAVE_REGISTERWAITFORSINGLEOBJECT", - "HAVE__STRDUP", -]); - -// recv/send/recvfrom prototypes — c-ares casts through these to call the -// platform socket API generically. POSIX is uniform; winsock uses SOCKET + -// int lengths and char* buffers. -const POSIX_SOCKET_TYPES = ` -#define GETHOSTNAME_TYPE_ARG2 size_t -#define GETSERVBYPORT_R_ARGS 6 -#define GETSERVBYNAME_R_ARGS 6 -#define RECVFROM_TYPE_ARG1 int -#define RECVFROM_TYPE_ARG2 void * -#define RECVFROM_TYPE_ARG2_IS_VOID 0 -#define RECVFROM_TYPE_ARG3 size_t -#define RECVFROM_TYPE_ARG4 int -#define RECVFROM_TYPE_ARG5 struct sockaddr * -#define RECVFROM_TYPE_ARG5_IS_VOID 0 -#define RECVFROM_TYPE_ARG6 socklen_t * -#define RECVFROM_TYPE_ARG6_IS_VOID 0 -#define RECVFROM_TYPE_RETV ssize_t -#define RECV_TYPE_ARG1 int -#define RECV_TYPE_ARG2 void * -#define RECV_TYPE_ARG3 size_t -#define RECV_TYPE_ARG4 int -#define RECV_TYPE_RETV ssize_t -#define SEND_TYPE_ARG1 int -#define SEND_TYPE_ARG2 const void * -#define SEND_TYPE_ARG3 size_t -#define SEND_TYPE_ARG4 int -#define SEND_TYPE_RETV ssize_t -#define CARES_RANDOM_FILE "/dev/urandom" -`; - -const WINDOWS_SOCKET_TYPES = ` -#define GETHOSTNAME_TYPE_ARG2 int -#define RECVFROM_TYPE_ARG1 SOCKET -#define RECVFROM_TYPE_ARG2 char * -#define RECVFROM_TYPE_ARG2_IS_VOID 0 -#define RECVFROM_TYPE_ARG3 int -#define RECVFROM_TYPE_ARG4 int -#define RECVFROM_TYPE_ARG5 struct sockaddr * -#define RECVFROM_TYPE_ARG5_IS_VOID 0 -#define RECVFROM_TYPE_ARG6 int * -#define RECVFROM_TYPE_ARG6_IS_VOID 0 -#define RECVFROM_TYPE_RETV int -#define RECV_TYPE_ARG1 SOCKET -#define RECV_TYPE_ARG2 char * -#define RECV_TYPE_ARG3 int -#define RECV_TYPE_ARG4 int -#define RECV_TYPE_RETV int -#define SEND_TYPE_ARG1 SOCKET -#define SEND_TYPE_ARG2 const char * -#define SEND_TYPE_ARG3 int -#define SEND_TYPE_ARG4 int -#define SEND_TYPE_RETV int -`; - -function configH(cfg: Config): string { - let platform: string; - let types: string; - if (cfg.windows) { - platform = WINDOWS; - types = WINDOWS_SOCKET_TYPES; - } else if (cfg.darwin) { - platform = `${POSIX}\n${DARWIN}`; - types = POSIX_SOCKET_TYPES; - } else if (cfg.freebsd) { - platform = `${POSIX}\n${FREEBSD}`; - types = POSIX_SOCKET_TYPES; - } else { - const abiExtra = cfg.abi === "android" ? ANDROID : LINUX_NETDB_R; - platform = `${POSIX}\n${LINUX}\n${abiExtra}`; - types = POSIX_SOCKET_TYPES; - } - return `/* Generated by scripts/build/deps/cares.ts for ${cfg.os}-${cfg.arch} */ -${ALWAYS} -${platform} -${types}`; -} diff --git a/scripts/build/deps/index.ts b/scripts/build/deps/index.ts index b2a63ca571fe..ba4cbd13c311 100644 --- a/scripts/build/deps/index.ts +++ b/scripts/build/deps/index.ts @@ -13,7 +13,6 @@ import type { Dependency } from "../source.ts"; import { boringssl } from "./boringssl.ts"; import { brotli } from "./brotli.ts"; -import { cares } from "./cares.ts"; import { highway } from "./highway.ts"; import { libdeflate } from "./libdeflate.ts"; import { libjpegTurbo } from "./libjpeg-turbo.ts"; @@ -52,7 +51,6 @@ export const allDeps: readonly Dependency[] = [ // Image codecs. libjpegTurbo, libwebp, - cares, highway, libuv, lolhtml, @@ -76,7 +74,6 @@ export const allDeps: readonly Dependency[] = [ export { boringssl, brotli, - cares, highway, libdeflate, libjpegTurbo, diff --git a/scripts/build/deps/mimalloc.ts b/scripts/build/deps/mimalloc.ts index e29f4a10ab5d..544a7edd4583 100644 --- a/scripts/build/deps/mimalloc.ts +++ b/scripts/build/deps/mimalloc.ts @@ -110,7 +110,7 @@ export const mimalloc: Dependency = { // otherwise sit on the uCRT heap are pointed at mimalloc one // by one instead (ICU and libuv in bun_runtime::bin_entry's // use_mimalloc_in_dependencies, BoringSSL via the hooks in - // boringssl.ts, c-ares via ares_library_init_mem). + // boringssl.ts). const override = cfg.linux && !cfg.asan; const defines: Record = { diff --git a/scripts/build/flags.ts b/scripts/build/flags.ts index 36af7c7601f6..834bcae1a5b1 100644 --- a/scripts/build/flags.ts +++ b/scripts/build/flags.ts @@ -668,7 +668,7 @@ export const globalFlags: Flag[] = [ // EnableSplitLTOUnit flag or lld dies with "inconsistent LTO Unit // splitting". -fwhole-program-vtables (above) defaults the split ON for // C++ on ELF targets but it's a cxx-only flag, so the C modules (zlib, - // c-ares, mimalloc, boringssl's .c files, ...) would disagree. Force it + // mimalloc, boringssl's .c files, ...) would disagree. Force it // OFF everywhere instead of on: split LTO units shunt every module's // vtables + type metadata into a merged regular-LTO half — a serial // merged module, which is exactly what ThinLTO is supposed to avoid. diff --git a/scripts/build/source.ts b/scripts/build/source.ts index b24d24faa919..0d06d782370e 100644 --- a/scripts/build/source.ts +++ b/scripts/build/source.ts @@ -470,7 +470,6 @@ export type DepName = | "WebKit" | "boringssl" | "brotli" - | "cares" | "highway" | "libdeflate" | "libjpeg-turbo" diff --git a/src/boringssl/Cargo.toml b/src/boringssl/Cargo.toml index 1cb4291a769d..8ad144010785 100644 --- a/src/boringssl/Cargo.toml +++ b/src/boringssl/Cargo.toml @@ -12,6 +12,4 @@ workspace = true [dependencies] bun_alloc.workspace = true bun_core.workspace = true -bun_sys.workspace = true bun_boringssl_sys.workspace = true -bun_cares_sys.workspace = true diff --git a/src/boringssl/lib.rs b/src/boringssl/lib.rs index 1ec10aefa934..292cb64aa9b7 100644 --- a/src/boringssl/lib.rs +++ b/src/boringssl/lib.rs @@ -9,7 +9,7 @@ use core::ptr; use std::cell::Cell; pub use bun_boringssl_sys as boring; -use bun_cares_sys as c_ares; +use bun_core::ip_address::AddressText; use bun_core::strings; /// BoringSSL's translated C API @@ -162,35 +162,19 @@ pub(crate) unsafe extern "C" fn OPENSSL_system_free(ptr: *mut c_void) { unsafe { bun_alloc::default_alloc::free(ptr) } } -pub use bun_sys::posix::INET6_ADDRSTRLEN; - -// Canonical cross-platform AF_* surface — handles the Windows ws2def.h split. -use bun_sys::posix::AF::{INET as AF_INET, INET6 as AF_INET6}; - /// Node.js's `canonicalizeIP`: the canonical text of the address that libuv's `uv_inet_pton` reads in `addr_str`, without the zone id of an IPv6 address. `None` for any other text. -pub fn canonicalize_ip<'a>( - addr_str: &[u8], - out_ip: &'a mut [u8; INET6_ADDRSTRLEN + 1], -) -> Option<&'a [u8]> { - match bun_core::ip_address::parse_uv_inet_pton(addr_str)? { - IpAddr::V4(ip) => canonical_ip_octets(&ip.octets(), out_ip), - IpAddr::V6(ip) => canonical_ip_octets(&ip.octets(), out_ip), - } +pub fn canonicalize_ip(addr_str: &[u8]) -> Option { + bun_core::ip_address::parse_uv_inet_pton(addr_str).map(bun_core::ip_address::format) } /// Canonical text form of raw IP address octets (4 or 16 bytes), e.g. `::1`. -fn canonical_ip_octets<'a>( - octets: &[u8], - out_ip: &'a mut [u8; INET6_ADDRSTRLEN + 1], -) -> Option<&'a [u8]> { - let af: c_int = match octets.len() { - 4 => AF_INET, - 16 => AF_INET6, - _ => return None, - }; - // SAFETY: `octets` is readable for its checked 4/16-byte length and - // `out_ip` is INET6_ADDRSTRLEN + 1 bytes — `ntop`'s requirements. - unsafe { c_ares::ntop(af, octets.as_ptr().cast(), &mut out_ip[..]) } +fn canonical_ip_octets(octets: &[u8]) -> Option { + if let Ok(v4) = <[u8; 4]>::try_from(octets) { + return Some(bun_core::ip_address::format_v4(v4)); + } + <[u8; 16]>::try_from(octets) + .ok() + .map(bun_core::ip_address::format_v6) } /// Strips a single trailing `.` (the DNS root label). Mirrors Node.js @@ -428,7 +412,7 @@ fn is_hostname(host: &[u8]) -> bool { } pub fn check_x509_server_identity(x509: &mut boring::X509, hostname: &[u8]) -> bool { - // As in Node.js, a host is an IP address only as typed, and only in the strict form of `net.isIP`: `ares_inet_pton` reads "127.1" as 127.1.0.0 and takes "1.2.3.4/8". + // As in Node.js, a host is an IP address only as typed, and only in the strict form of `net.isIP`: `parse_ares` reads "127.1" as 127.1.0.0. let typed = unfqdn(hostname); let mut host_ip = bun_core::ip_address::parse_strict(typed); let ascii_hostname; @@ -720,15 +704,14 @@ struct CanonicalIpList<'a>(Option<&'a boring::GeneralNames>); impl core::fmt::Display for CanonicalIpList<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let Some(names) = self.0 else { return Ok(()) }; - let mut buf = [0u8; INET6_ADDRSTRLEN + 1]; let mut first = true; for entry in names.subject_alt_names() { if let boring::SubjectAltName::Ip(octets) = entry { - if let Some(ip) = canonical_ip_octets(octets, &mut buf) { + if let Some(ip) = canonical_ip_octets(octets) { if !core::mem::take(&mut first) { f.write_str(", ")?; } - write!(f, "{}", NameBytes(ip))?; + write!(f, "{}", NameBytes(&ip))?; } } } diff --git a/src/bun_core/env_var.rs b/src/bun_core/env_var.rs index 1485b6e52176..952450921f46 100644 --- a/src/bun_core/env_var.rs +++ b/src/bun_core/env_var.rs @@ -187,9 +187,13 @@ new!(pub GITHUB_SERVER_URL: string, "GITHUB_SERVER_URL", {}); new!(pub GITHUB_SHA: string, "GITHUB_SHA", {}); new!(pub GITHUB_WORKSPACE: string, "GITHUB_WORKSPACE", {}); platform_specific_new!(pub HOME: string, posix = "HOME", windows = "USERPROFILE", {}); +// hostname(7): the path of a file of host name aliases. +new!(pub HOSTALIASES: string, "HOSTALIASES", {}); new!(pub HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET: string, "HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET", {}); new!(pub IS_BUN_AUTO_UPDATE: boolean, "IS_BUN_AUTO_UPDATE", { default: false }); new!(pub JENKINS_URL: string, "JENKINS_URL", {}); +// resolv.conf(5): overrides the search list. +new!(pub LOCALDOMAIN: string, "LOCALDOMAIN", {}); // Dump mimalloc statistics at the end of the process. Note that this is not the same as // `MIMALLOC_VERBOSE`, documented here: https://microsoft.github.io/mimalloc/environment.html new!(pub MI_VERBOSE: boolean, "MI_VERBOSE", { default: false }); @@ -223,6 +227,8 @@ new!(pub npm_package_json: string, "npm_package_json", {}); new!(pub PATH: string, "PATH", {}); platform_specific_new!(pub PWD: string, posix = "PWD", windows = None, {}); new!(pub REPL_ID: boolean, "REPL_ID", { default: false }); +// resolv.conf(5): more `options`. +new!(pub RES_OPTIONS: string, "RES_OPTIONS", {}); new!(pub RUNNER_DEBUG: boolean, "RUNNER_DEBUG", { default: false }); platform_specific_new!(pub SDKROOT: string, posix = "SDKROOT", windows = None, {}); platform_specific_new!(pub SHELL: string, posix = "SHELL", windows = None, {}); diff --git a/src/bun_core/fmt.rs b/src/bun_core/fmt.rs index 2fb725f3a083..1ca8a8296c70 100644 --- a/src/bun_core/fmt.rs +++ b/src/bun_core/fmt.rs @@ -3458,7 +3458,7 @@ pub fn int_as_bytes(buf: &mut [u8], value: T) -> &[u8] { /// returning a [`CStr`] borrowing the head of `buf`. /// /// Used when handing an integer to a C API that wants a `*const c_char` -/// service/port string (e.g. `getaddrinfo`, `ares_getaddrinfo`). 21 bytes +/// service/port string (e.g. `getaddrinfo`). 21 bytes /// covers `u64::MAX` (20 digits) + NUL; `u16`/`u32` callers widen via `as u64`. #[inline] pub fn itoa_z(buf: &mut [u8; 21], n: u64) -> &core::ffi::CStr { diff --git a/src/bun_core/ip_address.rs b/src/bun_core/ip_address.rs index 3a2e9765d2f2..b9f148565881 100644 --- a/src/bun_core/ip_address.rs +++ b/src/bun_core/ip_address.rs @@ -1,3 +1,21 @@ +// `format_v6` and the `parse_ares*` functions are derived from c-ares's inet_ntop.c and inet_net_pton.c: +// +// Copyright (c) 2012 by Gilles Chehade +// Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC") +// Copyright (c) 1996-1999 by Internet Software Consortium. +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +// OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + //! IP-address parsing shared by URL/host handling and the DNS backends. #[cfg(not(windows))] @@ -31,7 +49,7 @@ pub fn is_ipv6_address(input: &[u8]) -> bool { parse_strict_v6(input).is_some() } -/// A dotted quad or an IPv6 address and nothing else, the same on every platform. This is `core::net`'s parser and not `ares_inet_pton`, which is `inet_net_pton` underneath: it also takes `10` (as 10.0.0.0), `127.1` (as 127.1.0.0), `0x7f000001`, zero-padded octets, a trailing `/bits`, and stops at a NUL. +/// A dotted quad or an IPv6 address and nothing else, the same on every platform. This is `core::net`'s parser and not [`parse_ares`], which also takes `10` (as 10.0.0.0), `127.1` (as 127.1.0.0), `0x7f000001`, zero-padded octets, and stops at a NUL. #[inline(never)] pub fn parse_strict(input: &[u8]) -> Option { // The longest literal is "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255", so a longer host is never scanned. @@ -125,3 +143,291 @@ pub fn strip_ipv6_brackets(host: &[u8]) -> &[u8] { } host } + +/// The text of one address, without an allocation. Derefs to its bytes. +#[derive(Clone, Copy)] +pub struct AddressText { + /// "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255" is the longest. + buf: [u8; 45], + len: u8, +} + +impl AddressText { + const EMPTY: Self = Self { + buf: [0; 45], + len: 0, + }; + + fn push(&mut self, byte: u8) { + if let Some(slot) = self.buf.get_mut(usize::from(self.len)) { + *slot = byte; + self.len += 1; + } + } + + fn push_decimal(&mut self, octet: u8) { + if octet >= 100 { + self.push(b'0' + octet / 100); + } + if octet >= 10 { + self.push(b'0' + octet / 10 % 10); + } + self.push(b'0' + octet % 10); + } + + fn push_hex(&mut self, word: u16) { + let skipped = (word.leading_zeros() / 4).min(3); + for nibble in skipped..4 { + let digit = (word >> (12 - 4 * nibble)) as u8 & 0xf; + self.push(if digit < 10 { + b'0' + digit + } else { + b'a' + digit - 10 + }); + } + } + + fn push_v4(&mut self, octets: [u8; 4]) { + for (i, octet) in octets.into_iter().enumerate() { + if i != 0 { + self.push(b'.'); + } + self.push_decimal(octet); + } + } +} + +impl core::ops::Deref for AddressText { + type Target = [u8]; + fn deref(&self) -> &[u8] { + self.buf.get(..usize::from(self.len)).unwrap_or(&[]) + } +} + +/// `inet_ntop(AF_INET)`. +pub fn format_v4(octets: [u8; 4]) -> AddressText { + let mut text = AddressText::EMPTY; + text.push_v4(octets); + text +} + +/// `inet_ntop(AF_INET6)` as in BIND, libuv and c-ares. Unlike `Ipv6Addr`'s `Display`, `::1.2.3.4` keeps its dotted quad. +pub fn format_v6(octets: [u8; 16]) -> AddressText { + let words = Ipv6Addr::from(octets).segments(); + + // The longest run of zero words, the first of equals, if it is two or more. + let (mut best_base, mut best_len) = (0usize, 0usize); + let (mut run_base, mut run_len) = (0usize, 0usize); + for (i, &word) in words.iter().enumerate() { + if word != 0 { + run_len = 0; + continue; + } + if run_len == 0 { + run_base = i; + } + run_len += 1; + if run_len > best_len { + (best_base, best_len) = (run_base, run_len); + } + } + if best_len < 2 { + best_len = 0; + } + + let mut text = AddressText::EMPTY; + for (i, &word) in words.iter().enumerate() { + if best_len != 0 && (best_base..best_base + best_len).contains(&i) { + if i == best_base { + text.push(b':'); + } + continue; + } + if i != 0 { + text.push(b':'); + } + let [.., a, b, c, d] = octets; + let [.., fifth, _, _] = words; + if i == 6 + && best_len != 0 + && best_base == 0 + && (best_len == 6 || (best_len == 5 && fifth == 0xffff)) + { + text.push_v4([a, b, c, d]); + return text; + } + text.push_hex(word); + } + if best_len != 0 && best_base + best_len == words.len() { + text.push(b':'); + } + text +} + +pub fn format(ip: IpAddr) -> AddressText { + match ip { + IpAddr::V4(ip) => format_v4(ip.octets()), + IpAddr::V6(ip) => format_v6(ip.octets()), + } +} + +/// The text is a C string to the `parse_ares_*` parsers: it ends at its first NUL. +fn c_char_at(src: &[u8], index: usize) -> u8 { + src.get(index).copied().unwrap_or(0) +} + +/// c-ares's `ares_inet_pton(AF_INET)`, which reads more than a dotted quad: one to four octets with the rest zero (`10`, `127.1` is 127.1.0.0), leading zeros (`010` is ten) and `0x` followed by up to eight hex digits. +pub fn parse_ares_v4(src: &[u8]) -> Option<[u8; 4]> { + let mut dst = [0u8; 4]; + let mut written = 0usize; + let mut at = 0usize; + let mut next = || { + at += 1; + c_char_at(src, at - 1) + }; + let mut put = |octet: u8| { + *dst.get_mut(written)? = octet; + written += 1; + Some(()) + }; + + let mut ch = next(); + if ch == b'0' + && matches!(c_char_at(src, 1), b'x' | b'X') + && c_char_at(src, 2).is_ascii_hexdigit() + { + next(); + let mut pending: Option = None; + loop { + ch = next(); + let Some(digit) = (ch as char).to_digit(16) else { + break; + }; + let digit = digit as u8; + pending = match pending { + None => Some(digit), + Some(high) => { + put(high << 4 | digit)?; + None + } + }; + } + if let Some(high) = pending { + put(high << 4)?; + } + } else if ch.is_ascii_digit() { + loop { + let mut octet = 0u32; + loop { + octet = octet * 10 + u32::from(ch - b'0'); + if octet > 255 { + return None; + } + ch = next(); + if !ch.is_ascii_digit() { + break; + } + } + put(octet as u8)?; + if ch == 0 || ch == b'/' { + break; + } + if ch != b'.' { + return None; + } + ch = next(); + if !ch.is_ascii_digit() { + return None; + } + } + } else { + return None; + } + (ch == 0).then_some(dst) +} + +/// c-ares's `ares_inet_pton(AF_INET6)`. No zone id. A dotted quad at the end is read by [`parse_ares_v4`], so `::1.2` is `::1.2.0.0`. +pub fn parse_ares_v6(src: &[u8]) -> Option<[u8; 16]> { + let mut octets = [0u8; 16]; + let mut written = 0usize; + let mut elided_at: Option = None; + let mut at = 0usize; + + if c_char_at(src, 0) == b':' { + if c_char_at(src, 1) != b':' { + return None; + } + at = 1; + } + let mut group_start = at; + let mut digits = 0u32; + let mut word = 0u16; + fn put_word(octets: &mut [u8; 16], written: &mut usize, word: u16) -> Option<()> { + octets + .get_mut(*written..*written + 2)? + .copy_from_slice(&word.to_be_bytes()); + *written += 2; + Some(()) + } + + loop { + let ch = c_char_at(src, at); + at += 1; + if ch == 0 { + break; + } + if let Some(digit) = (ch as char).to_digit(16) { + if digits >= 4 { + return None; + } + word = word << 4 | digit as u16; + digits += 1; + continue; + } + if ch == b':' { + group_start = at; + if digits == 0 { + if elided_at.is_some() { + return None; + } + elided_at = Some(written); + continue; + } + if c_char_at(src, at) == 0 { + return None; + } + put_word(&mut octets, &mut written, word)?; + (digits, word) = (0, 0); + continue; + } + if ch == b'.' && written + 4 <= octets.len() { + let quad = parse_ares_v4(src.get(group_start..)?)?; + octets.get_mut(written..written + 4)?.copy_from_slice(&quad); + written += 4; + digits = 0; + break; + } + return None; + } + if digits != 0 { + put_word(&mut octets, &mut written, word)?; + } + if let Some(elided_at) = elided_at { + if written == octets.len() { + return None; + } + let tail = written - elided_at; + octets.copy_within(elided_at..written, 16 - tail); + octets.get_mut(elided_at..16 - tail)?.fill(0); + written = octets.len(); + } + (written == octets.len()).then_some(octets) +} + +/// [`parse_ares_v4`], else [`parse_ares_v6`]. +pub fn parse_ares(src: &[u8]) -> Option { + if let Some(v4) = parse_ares_v4(src) { + return Some(IpAddr::from(v4)); + } + parse_ares_v6(src).map(IpAddr::from) +} diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs index 48b48fa3547c..b5c0fbb1aad8 100644 --- a/src/bun_core/lib.rs +++ b/src/bun_core/lib.rs @@ -682,7 +682,7 @@ pub const unsafe fn container_of(field: *const F, offset: usize) -> *mut P /// Recover a typed `&mut T` from a C-callback's opaque user-data pointer. /// /// This is the canonical spelling for the ubiquitous trampoline pattern where -/// a C library (c-ares, uWS, libuv, lol-html, BoringSSL, …) round- +/// a C library (uWS, libuv, lol-html, BoringSSL, …) round- /// trips a Rust object through a `void *user_data` slot and hands it back to /// an `extern "C" fn` thunk. Earlier ports open-coded this as /// `unsafe { &mut *ctx.cast::() }` at every site; centralising it here diff --git a/src/cares_sys/Cargo.toml b/src/cares_sys/Cargo.toml deleted file mode 100644 index 2dbedb44a465..000000000000 --- a/src/cares_sys/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "bun_cares_sys" -version.workspace = true -edition.workspace = true - -[lib] -path = "lib.rs" - -[lints] -workspace = true - -[dependencies] -bun_opaque.workspace = true -strum.workspace = true -libc.workspace = true -bun_alloc.workspace = true -bun_core.workspace = true - -[target.'cfg(windows)'.dependencies] -bun_libuv_sys.workspace = true diff --git a/src/cares_sys/c_ares.rs b/src/cares_sys/c_ares.rs deleted file mode 100644 index fe5b10505168..000000000000 --- a/src/cares_sys/c_ares.rs +++ /dev/null @@ -1,1908 +0,0 @@ -#![allow( - non_camel_case_types, - non_snake_case, - non_upper_case_globals, - clippy::missing_safety_doc -)] - -#[cfg(windows)] -use core::ffi::c_short; -use core::ffi::{c_char, c_int, c_uint, c_ushort, c_void}; -use core::ptr; - -#[cfg(windows)] -use crate::winsock::{sockaddr, sockaddr_in, sockaddr_in6, sockaddr_storage, socklen_t}; -#[cfg(not(windows))] -use libc::{sockaddr, sockaddr_in, sockaddr_in6, sockaddr_storage, socklen_t}; - -pub type ares_socklen_t = socklen_t; - -#[cfg(windows)] -pub type ares_socket_t = usize; // Windows `SOCKET` is `UINT_PTR` (integer, not a pointer). -#[cfg(not(windows))] -pub type ares_socket_t = c_int; - -type ares_sock_state_cb = Option; - -bun_opaque::opaque_ffi! { - /// Nomicon opaque-FFI pattern. `UnsafeCell` makes the type `!Freeze` so a - /// shared reference does not assert immutability of the C-owned state. - pub struct struct_apattern; -} - -/// Only the address families c-ares -/// actually uses. Kept local so this `*_sys` crate stays leaf-level -/// (no dependency on `bun_sys`). Canonical: `bun_sys::posix::AF`. -pub mod AF { - use core::ffi::c_int; - // `libc` does not expose AF_* on Windows MSVC; ws2def.h values are inlined - // there. Non-Windows targets keep the platform `libc` constants — `AF_INET6` - // is NOT portable (10 on Linux, 30 on macOS/BSD). - #[cfg(windows)] - pub const INET: c_int = 2; - #[cfg(windows)] - pub const INET6: c_int = 23; - #[cfg(not(windows))] - pub const INET: c_int = libc::AF_INET; - #[cfg(not(windows))] - pub const INET6: c_int = libc::AF_INET6; -} - -/// `EAI_*` getaddrinfo error codes. The `libc` crate is missing -/// `EAI_ADDRFAMILY` and the glibc-only async-getaddrinfo extensions, so we -/// hardcode those raw values from ``. -#[cfg(not(windows))] -#[derive(Copy, Clone, Eq, PartialEq)] -pub struct EAI(c_int); - -#[cfg(not(windows))] -impl EAI { - #[inline] - pub(crate) const fn from_raw(rc: i32) -> Self { - Self(rc as c_int) - } - - #[cfg(target_os = "linux")] - pub(crate) const ADDRFAMILY: Self = Self(-9); - #[cfg(not(target_os = "linux"))] - pub(crate) const ADDRFAMILY: Self = Self(1); - - pub(crate) const AGAIN: Self = Self(libc::EAI_AGAIN); - pub(crate) const BADFLAGS: Self = Self(libc::EAI_BADFLAGS); - pub(crate) const FAIL: Self = Self(libc::EAI_FAIL); - pub(crate) const FAMILY: Self = Self(libc::EAI_FAMILY); - pub(crate) const MEMORY: Self = Self(libc::EAI_MEMORY); - // RFC 3493 dropped EAI_NODATA; FreeBSD's only exposes it under - // __BSD_VISIBLE (historical value 7) and the libc crate omits it entirely. - #[cfg(not(any(target_os = "freebsd", target_os = "dragonfly")))] - pub(crate) const NODATA: Self = Self(libc::EAI_NODATA); - #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] - pub(crate) const NODATA: Self = Self(7); - pub(crate) const NONAME: Self = Self(libc::EAI_NONAME); - pub(crate) const SERVICE: Self = Self(libc::EAI_SERVICE); - #[cfg(any(target_os = "linux", target_os = "android"))] - pub(crate) const SOCKTYPE: Self = Self(libc::EAI_SOCKTYPE); - pub(crate) const SYSTEM: Self = Self(libc::EAI_SYSTEM); - - // glibc-only `getaddrinfo_a` / IDN extensions (absent on musl, bionic). - #[cfg(all(target_os = "linux", target_env = "gnu"))] - pub(crate) const INPROGRESS: Self = Self(-100); - #[cfg(all(target_os = "linux", target_env = "gnu"))] - pub(crate) const CANCELED: Self = Self(-101); - #[cfg(all(target_os = "linux", target_env = "gnu"))] - pub(crate) const NOTCANCELED: Self = Self(-102); - #[cfg(all(target_os = "linux", target_env = "gnu"))] - pub(crate) const ALLDONE: Self = Self(-103); - #[cfg(all(target_os = "linux", target_env = "gnu"))] - pub(crate) const IDN_ENCODE: Self = Self(-105); -} - -#[repr(i32)] -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum NSClass { - /// Cookie. - ns_c_invalid = 0, - /// Internet. - ns_c_in = 1, - /// unallocated/unsupported. - ns_c_2 = 2, - /// MIT Chaos-net. - ns_c_chaos = 3, - /// MIT Hesiod. - ns_c_hs = 4, - /// Query class values which do not appear in resource records - /// for prereq. sections in update requests - ns_c_none = 254, - /// Wildcard match. - ns_c_any = 255, - ns_c_max = 65536, -} - -// Values are only ever -// constructed in Rust and passed *to* C, so a plain repr(i32) enum is sound. -// If c-ares ever returns an NSType, this must become a transparent newtype. -#[repr(i32)] -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum NSType { - /// Cookie. - ns_t_invalid = 0, - /// Host address. - ns_t_a = 1, - /// Authoritative server. - ns_t_ns = 2, - /// Mail destination. - ns_t_md = 3, - /// Mail forwarder. - ns_t_mf = 4, - /// Canonical name. - ns_t_cname = 5, - /// Start of authority zone. - ns_t_soa = 6, - /// Mailbox domain name. - ns_t_mb = 7, - /// Mail group member. - ns_t_mg = 8, - /// Mail rename name. - ns_t_mr = 9, - /// Null resource record. - ns_t_null = 10, - /// Well known service. - ns_t_wks = 11, - /// Domain name pointer. - ns_t_ptr = 12, - /// Host information. - ns_t_hinfo = 13, - /// Mailbox information. - ns_t_minfo = 14, - /// Mail routing information. - ns_t_mx = 15, - /// Text strings. - ns_t_txt = 16, - /// Responsible person. - ns_t_rp = 17, - /// AFS cell database. - ns_t_afsdb = 18, - /// X_25 calling address. - ns_t_x25 = 19, - /// ISDN calling address. - ns_t_isdn = 20, - /// Router. - ns_t_rt = 21, - /// NSAP address. - ns_t_nsap = 22, - /// Reverse NSAP lookup (deprecated). - ns_t_nsap_ptr = 23, - /// Security signature. - ns_t_sig = 24, - /// Security key. - ns_t_key = 25, - /// X.400 mail mapping. - ns_t_px = 26, - /// Geographical position (withdrawn). - ns_t_gpos = 27, - /// Ip6 Address. - ns_t_aaaa = 28, - /// Location Information. - ns_t_loc = 29, - /// Next domain (security). - ns_t_nxt = 30, - /// Endpoint identifier. - ns_t_eid = 31, - /// Nimrod Locator. - ns_t_nimloc = 32, - /// Server Selection. - ns_t_srv = 33, - /// ATM Address - ns_t_atma = 34, - /// Naming Authority PoinTeR - ns_t_naptr = 35, - /// Key Exchange - ns_t_kx = 36, - /// Certification record - ns_t_cert = 37, - /// IPv6 address (deprecates AAAA) - ns_t_a6 = 38, - /// Non-terminal DNAME (for IPv6) - ns_t_dname = 39, - /// Kitchen sink (experimentatl) - ns_t_sink = 40, - /// EDNS0 option (meta-RR) - ns_t_opt = 41, - /// Address prefix list (RFC3123) - ns_t_apl = 42, - /// Delegation Signer (RFC4034) - ns_t_ds = 43, - /// SSH Key Fingerprint (RFC4255) - ns_t_sshfp = 44, - /// Resource Record Signature (RFC4034) - ns_t_rrsig = 46, - /// Next Secure (RFC4034) - ns_t_nsec = 47, - /// DNS Public Key (RFC4034) - ns_t_dnskey = 48, - /// Transaction key - ns_t_tkey = 249, - /// Transaction signature. - ns_t_tsig = 250, - /// Incremental zone transfer. - ns_t_ixfr = 251, - /// Transfer zone of authority. - ns_t_axfr = 252, - /// Transfer mailbox records. - ns_t_mailb = 253, - /// Transfer mail agent records. - ns_t_maila = 254, - /// Wildcard match. - ns_t_any = 255, - /// Uniform Resource Identifier (RFC7553) - ns_t_uri = 256, - /// Certification Authority Authorization. - ns_t_caa = 257, - ns_t_max = 65536, -} - -#[repr(C)] -#[derive(Copy, Clone, Default)] -pub struct struct_ares_server_failover_options { - pub(crate) retry_chance: c_ushort, - pub(crate) retry_delay: usize, -} - -type ares_evsys_t = c_uint; - -#[repr(C)] -pub struct Options { - pub(crate) flags: c_int, - pub(crate) timeout: c_int, - pub(crate) tries: c_int, - pub(crate) ndots: c_int, - pub(crate) udp_port: c_ushort, - pub(crate) tcp_port: c_ushort, - pub(crate) socket_send_buffer_size: c_int, - pub(crate) socket_receive_buffer_size: c_int, - pub(crate) servers: *mut in_addr, - pub(crate) nservers: c_int, - pub(crate) domains: *mut *mut c_char, - pub(crate) ndomains: c_int, - pub(crate) lookups: *mut c_char, - pub(crate) sock_state_cb: ares_sock_state_cb, - pub(crate) sock_state_cb_data: *mut c_void, - pub(crate) sortlist: *mut struct_apattern, - pub(crate) nsort: c_int, - pub(crate) ednspsz: c_int, - pub(crate) resolvconf_path: *mut c_char, - pub(crate) hosts_path: *mut c_char, - pub(crate) udp_max_queries: c_int, - pub(crate) maxtimeout: c_int, - pub(crate) qcache_max_ttl: c_uint, - pub(crate) evsys: ares_evsys_t, - pub(crate) server_failover_opts: struct_ares_server_failover_options, -} - -// SAFETY: `#[repr(C)]` POD — every field is an integer, raw pointer, or -// `Option`; all-zero is the documented "no options set" state -// passed to `ares_init_options` (S021). -unsafe impl bun_core::ffi::Zeroable for Options {} -impl Default for Options { - fn default() -> Self { - bun_core::ffi::zeroed() - } -} - -// hostent in glibc uses int for h_addrtype and h_length, whereas hostent in -// winsock2.h uses short. -#[cfg(windows)] -type hostent_int = c_short; -#[cfg(not(windows))] -type hostent_int = c_int; - -#[repr(C)] -pub struct struct_hostent { - pub h_name: *mut c_char, - pub h_aliases: *mut *mut c_char, // NUL-terminated array of NUL-terminated strings - pub h_addrtype: hostent_int, - pub h_length: hostent_int, - pub h_addr_list: *mut *mut c_char, // NUL-terminated array -} - -// ─── callback-wrapper note ───────────────────────────────────────────────── -// Rust cannot take a fn pointer as a const generic on -// stable, so the wrappers below are expressed as a trait: the implementing -// type provides the callback as a trait method, and the `extern "C"` thunk is -// monomorphized per `T: Trait`. -// ────────────────────────────────────────────────────────────────────────── - -pub trait HostentHandler: Sized { - fn on_hostent(&mut self, status: Option, timeouts: i32, results: *mut struct_hostent); -} - -impl struct_hostent { - // toJSResponse alias deleted — lives in bun_runtime::dns_jsc (extension trait). - - pub(crate) unsafe extern "C" fn host_callback_wrapper( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - hostent: *mut struct_hostent, - ) { - // SAFETY: ctx was passed as `*mut T` to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - if status != ARES_SUCCESS { - this.on_hostent(Error::get(status), timeouts, ptr::null_mut()); - return; - } - this.on_hostent(None, timeouts, hostent); - } - - // One `extern "C"` thunk per lookup name. - pub unsafe extern "C" fn callback_wrapper_cname( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - buffer: *mut u8, - buffer_length: c_int, - ) { - // SAFETY: ctx was passed as *mut T to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - if status != ARES_SUCCESS { - this.on_hostent(Error::get(status), timeouts, ptr::null_mut()); - return; - } - let mut start: *mut struct_hostent = ptr::null_mut(); - let mut addrttls = [struct_ares_addrttl::default(); 256]; - let mut naddrttls: i32 = 256; - // SAFETY: c-ares FFI; pointers are valid stack/null per contract. - let result = unsafe { - ares_parse_a_reply( - buffer, - buffer_length, - &raw mut start, - addrttls.as_mut_ptr(), - &raw mut naddrttls, - ) - }; - if result != ARES_SUCCESS { - this.on_hostent(Error::get(result), timeouts, ptr::null_mut()); - return; - } - this.on_hostent(None, timeouts, start); - } - - pub unsafe extern "C" fn callback_wrapper_ns( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - buffer: *mut u8, - buffer_length: c_int, - ) { - // SAFETY: ctx was passed as *mut T to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - if status != ARES_SUCCESS { - this.on_hostent(Error::get(status), timeouts, ptr::null_mut()); - return; - } - let mut start: *mut struct_hostent = ptr::null_mut(); - // SAFETY: c-ares FFI; pointers are valid stack/null per contract. - let result = unsafe { ares_parse_ns_reply(buffer, buffer_length, &raw mut start) }; - if result != ARES_SUCCESS { - this.on_hostent(Error::get(result), timeouts, ptr::null_mut()); - return; - } - this.on_hostent(None, timeouts, start); - } - - pub unsafe extern "C" fn callback_wrapper_ptr( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - buffer: *mut u8, - buffer_length: c_int, - ) { - // SAFETY: ctx was passed as *mut T to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - if status != ARES_SUCCESS { - this.on_hostent(Error::get(status), timeouts, ptr::null_mut()); - return; - } - let mut start: *mut struct_hostent = ptr::null_mut(); - // SAFETY: c-ares FFI; pointers are valid stack/null per contract. - let result = unsafe { - ares_parse_ptr_reply( - buffer, - buffer_length, - ptr::null(), - 0, - AF::INET, - &raw mut start, - ) - }; - if result != ARES_SUCCESS { - this.on_hostent(Error::get(result), timeouts, ptr::null_mut()); - return; - } - this.on_hostent(None, timeouts, start); - } - - /// FFI destroy — frees a c-ares-allocated hostent. - pub unsafe fn destroy(this: *mut struct_hostent) { - // SAFETY: caller guarantees `this` was allocated by c-ares (or is null). - unsafe { ares_free_hostent(this) }; - } -} - -pub struct hostent_with_ttls { - pub hostent: *mut struct_hostent, - pub ttls: [c_int; 256], -} - -impl Default for hostent_with_ttls { - fn default() -> Self { - Self { - hostent: ptr::null_mut(), - ttls: [-1; 256], - } - } -} - -pub trait HostentWithTtlsHandler: Sized { - /// `hostent_with_ttls::parse_a` or `parse_aaaa` — selects the c-ares reply - /// parser for [`hostent_with_ttls::callback_wrapper`]. - const PARSE: fn(&[u8]) -> Result, Error>; - - fn on_hostent_with_ttls( - &mut self, - status: Option, - timeouts: i32, - results: Option>, - ); -} - -impl hostent_with_ttls { - // toJSResponse alias deleted — lives in bun_runtime::dns_jsc. - - pub unsafe extern "C" fn callback_wrapper( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - buffer: *mut u8, - buffer_length: c_int, - ) { - // SAFETY: ctx was passed as *mut T to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - if status != ARES_SUCCESS { - this.on_hostent_with_ttls(Error::get(status), timeouts, None); - return; - } - // SAFETY: c-ares passes the reply buffer it owns; valid for `buffer_length` bytes. - let buffer = unsafe { - core::slice::from_raw_parts(buffer, usize::try_from(buffer_length).unwrap_or(0)) - }; - match (T::PARSE)(buffer) { - Ok(result) => this.on_hostent_with_ttls(None, timeouts, Some(result)), - Err(err) => this.on_hostent_with_ttls(Some(err), timeouts, None), - } - } - - pub fn parse_a(buffer: &[u8]) -> Result, Error> { - let mut start: *mut struct_hostent = ptr::null_mut(); - let mut addrttls = [struct_ares_addrttl::default(); 256]; - let mut naddrttls: c_int = 256; - // SAFETY: c-ares FFI; `buffer` is a valid slice; out-params are valid - // stack pointers per contract. - let result = unsafe { - ares_parse_a_reply( - buffer.as_ptr(), - c_int::try_from(buffer.len()).unwrap_or(c_int::MAX), - &raw mut start, - addrttls.as_mut_ptr(), - &raw mut naddrttls, - ) - }; - if result != ARES_SUCCESS { - return Err(Error::get(result).unwrap()); - } - let mut with_ttls = Box::new(hostent_with_ttls::default()); - with_ttls.hostent = start; - for (i, ttl) in addrttls[..usize::try_from(naddrttls).expect("int cast")] - .iter() - .enumerate() - { - with_ttls.ttls[i] = ttl.ttl; - } - Ok(with_ttls) - } - - pub fn parse_aaaa(buffer: &[u8]) -> Result, Error> { - let mut start: *mut struct_hostent = ptr::null_mut(); - let mut addr6ttls = [struct_ares_addr6ttl::default(); 256]; - let mut naddr6ttls: c_int = 256; - // SAFETY: c-ares FFI; `buffer` is a valid slice; out-params are valid - // stack pointers per contract. - let result = unsafe { - ares_parse_aaaa_reply( - buffer.as_ptr(), - c_int::try_from(buffer.len()).unwrap_or(c_int::MAX), - &raw mut start, - addr6ttls.as_mut_ptr(), - &raw mut naddr6ttls, - ) - }; - if result != ARES_SUCCESS { - return Err(Error::get(result).unwrap()); - } - let mut with_ttls = Box::new(hostent_with_ttls::default()); - with_ttls.hostent = start; - for (i, ttl) in addr6ttls[..usize::try_from(naddr6ttls).expect("int cast")] - .iter() - .enumerate() - { - with_ttls.ttls[i] = ttl.ttl; - } - Ok(with_ttls) - } -} - -impl Drop for hostent_with_ttls { - fn drop(&mut self) { - // SAFETY: hostent was allocated by ares_parse_*_reply (or null). - unsafe { ares_free_hostent(self.hostent) }; - } -} - -#[repr(C)] -pub struct struct_nameinfo { - pub node: *mut u8, - pub service: *mut u8, -} - -pub trait NameinfoHandler: Sized { - fn on_nameinfo(&mut self, status: Option, timeouts: i32, info: Option); -} - -impl struct_nameinfo { - // toJSResponse alias deleted — lives in bun_runtime::dns_jsc. - - pub(crate) unsafe extern "C" fn callback_wrapper( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - node: *mut u8, - service: *mut u8, - ) { - // SAFETY: ctx was passed as *mut T to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - if status != ARES_SUCCESS { - this.on_nameinfo(Error::get(status), timeouts, None); - return; - } - this.on_nameinfo(None, timeouts, Some(struct_nameinfo { node, service })); - } -} - -bun_opaque::opaque_ffi! { pub struct struct_Channeldata; } - -#[repr(C)] -pub struct AddrInfo_cname { - pub ttl: c_int, - pub alias: *mut u8, - pub name: *mut u8, - pub next: *mut AddrInfo_cname, -} - -#[repr(C)] -pub struct AddrInfo_node { - pub ttl: c_int, - pub flags: c_int, - pub family: c_int, - pub socktype: c_int, - pub protocol: c_int, - pub addrlen: ares_socklen_t, - pub addr: *mut sockaddr, - pub next: *mut AddrInfo_node, -} - -impl AddrInfo_node { - pub fn count(&self) -> u32 { - let mut len: u32 = 0; - let mut node: *const AddrInfo_node = self; - while !node.is_null() { - len += 1; - // SAFETY: node is non-null per loop condition; chain owned by c-ares. - node = unsafe { (*node).next }; - } - len - } -} - -#[repr(C)] -pub struct AddrInfo { - pub cnames_: *mut AddrInfo_cname, - pub node: *mut AddrInfo_node, - pub name_: *mut c_char, -} - -pub trait AddrInfoHandler: Sized { - fn on_addr_info(&mut self, status: Option, timeouts: i32, results: *mut AddrInfo); -} - -impl AddrInfo { - // toJSArray alias deleted — lives in bun_runtime::dns_jsc. - - // Consumers walk `cnames_` / `node` pointer chains directly. - - pub(crate) unsafe extern "C" fn callback_wrapper( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - addr_info: *mut AddrInfo, - ) { - // SAFETY: ctx was passed as *mut T to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - this.on_addr_info(Error::get(status), timeouts, addr_info); - } - - /// FFI destroy — frees a c-ares-allocated addrinfo chain. - pub unsafe fn destroy(this: *mut AddrInfo) { - // SAFETY: caller guarantees `this` was allocated by c-ares (or is null). - unsafe { ares_freeaddrinfo(this) }; - } -} - -#[repr(C)] -#[derive(Copy, Clone, Default)] -pub struct AddrInfo_hints { - pub ai_flags: c_int, - pub ai_family: c_int, - pub ai_socktype: c_int, - pub ai_protocol: c_int, -} -bun_core::impl_zeroable!(AddrInfo_hints); - -#[derive(Copy, Clone, Default)] -pub struct ChannelOptions { - pub timeout: Option, - pub tries: Option, -} - -bun_opaque::opaque_ffi! { - /// Opaque c-ares channel handle. `UnsafeCell` makes the type `!Freeze` so a - /// `&Channel` does not assert immutability of the C-owned state (c-ares - /// mutates the channel on every dispatch/process call). - pub struct Channel; -} -// Load-bearing: `ares_cancel`/`ares_process_fd` are declared `safe fn(&mut Channel)` -// on the basis that re-entrant callbacks re-deriving `&mut Channel` from a raw -// pointer cannot conflict because `Channel` claims zero bytes. If this type ever -// gains a non-ZST field, those signatures must revert to `unsafe fn(*mut Channel)`. -const _: () = assert!(core::mem::size_of::() == 0); - -/// Implemented by the type that owns a `*mut Channel` and receives socket- -/// state callbacks. -/// -/// R-2: methods take `&self`. The c-ares `sock_state_cb` re-enters the -/// container while a `&self` borrow may already be live in `on_dns_poll`; -/// the implementor routes mutation through interior mutability. -pub trait ChannelContainer: Sized { - fn on_dns_socket_state(&self, socket: ares_socket_t, readable: bool, writable: bool); - fn set_channel(&self, channel: *mut Channel); -} - -/// Trait for `Channel::resolve`: ties a lookup-name string to its NSType and -/// the `extern "C"` parse-thunk used as the ares_callback. -/// The dns_jsc consumer impls it per (T, record-type). -pub trait ResolveHandler: Sized { - const LOOKUP_NAME: &'static [u8]; - const NS_TYPE: NSType; - /// The `ares_callback`-compatible thunk that parses the reply and forwards - /// to `Self`. - const RAW_CALLBACK: unsafe extern "C" fn(*mut c_void, c_int, c_int, *mut u8, c_int); -} - -/// Copy `src` into the caller-owned stack `buf`, NUL-terminate, and return a -/// `*const c_char` suitable for c-ares FFI. Truncates silently at -/// `buf.len() - 1`; callers that must reject overlong input do so before -/// calling. The buffer lives in the caller's frame so the returned pointer is -/// valid for the FFI call that follows. -#[inline] -fn copy_nul_terminated(buf: &mut [u8], src: &[u8]) -> *const c_char { - let len = src.len().min(buf.len() - 1); - buf[..len].copy_from_slice(&src[..len]); - buf[len] = 0; - buf.as_ptr().cast::() -} - -impl Channel { - pub fn init(this: &C, options: ChannelOptions) -> Option { - let mut channel: *mut Channel = ptr::null_mut(); - - library_init(); - - unsafe extern "C" fn on_sock_state( - ctx: *mut c_void, - socket: ares_socket_t, - readable: c_int, - writable: c_int, - ) { - // SAFETY: `ctx` is the `&C` registered below; `on_dns_socket_state` - // takes `&self` (R-2) so the shared borrow is sufficient. - let container = unsafe { &*ctx.cast_const().cast::() }; - container.on_dns_socket_state(socket, readable != 0, writable != 0); - } - - let mut opts = Options { - // Android note: c-ares can't auto-discover servers (no /etc/resolv.conf, - // no JNI), so it falls back to 127.0.0.1 and queries time out. We do - // NOT set ARES_FLAG_NO_DFLT_SVR here — that makes init fail with - // ENOSERVER, which breaks dns.setServers() (it needs an initialized - // channel to call ares_set_servers_ports). Letting the 127.0.0.1 - // default stand means setServers() works as the documented workaround. - flags: ARES_FLAG_NOCHECKRESP, - sock_state_cb: Some(on_sock_state::), - // R-2: `*mut` spelling is signature-only (c-ares stores a `void*`); the - // callback derefs as shared (`&*const`) and the implementor mutates via - // interior mutability. - sock_state_cb_data: std::ptr::from_ref::(this).cast_mut().cast::(), - timeout: options.timeout.unwrap_or(-1), - tries: options.tries.unwrap_or(4), - ..Default::default() - }; - - let optmask: c_int = - ARES_OPT_FLAGS | ARES_OPT_TIMEOUTMS | ARES_OPT_SOCK_STATE_CB | ARES_OPT_TRIES; - - // c-ares creates its sockets with ws2_32 directly, and libuv initializes Winsock lazily. - #[cfg(windows)] - bun_libuv_sys::uv__winsock_ensure(); - // SAFETY: c-ares FFI; opts/channel are valid stack pointers. - let rc = unsafe { ares_init_options(&raw mut channel, &raw mut opts, optmask) }; - if let Some(err) = Error::get(rc) { - // Don't `ares_library_cleanup()` here: `library_init()` is `run_once!`, so - // tearing down the library on a per-channel failure would leave every later - // `Channel::init()` running against an uninitialized c-ares. - return Some(err); - } - - this.set_channel(channel); - None - } - - /// FFI destroy — `ares_destroy`. - pub unsafe fn destroy(this: *mut Channel) { - // SAFETY: caller guarantees `this` is a live channel returned by `ares_init_options`. - unsafe { ares_destroy(this) }; - } - - /// See c-ares `ares_getaddrinfo` documentation. - pub fn get_addr_info( - &mut self, - host: &[u8], - port: u16, - hints: &[AddrInfo_hints], - ctx: &mut T, - ) { - let mut host_buf = [0u8; 1024]; - let mut port_buf = [0u8; 21]; - let host_ptr = copy_nul_terminated(&mut host_buf, host); - - let port_ptr: *const c_char = if port > 0 { - bun_core::fmt::itoa_z(&mut port_buf, port as u64).as_ptr() - } else { - ptr::null() - }; - - let mut hints_buf = [AddrInfo_hints::default(); 3]; - for (i, hint) in hints[..hints.len().min(2)].iter().enumerate() { - hints_buf[i] = *hint; - } - let hints_: *const AddrInfo_hints = if !hints.is_empty() { - hints_buf.as_ptr() - } else { - ptr::null() - }; - // SAFETY: c-ares FFI; host/port/hints are NUL-terminated stack buffers or null; ctx outlives the channel. - unsafe { - ares_getaddrinfo( - self, - host_ptr, - port_ptr, - hints_, - AddrInfo::callback_wrapper::, - std::ptr::from_mut::(ctx).cast::(), - ); - } - } - - pub fn resolve(&mut self, name: &[u8], ctx: &mut T) { - if name.len() >= 1023 - || bun_core::strings::contains_char(name, 0) - || (name.is_empty() && !(T::LOOKUP_NAME == b"ns" || T::LOOKUP_NAME == b"soa")) - { - // SAFETY: thunk handles ARES_EBADNAME path. - unsafe { - (T::RAW_CALLBACK)( - std::ptr::from_mut::(ctx).cast::(), - ARES_EBADNAME, - 0, - ptr::null_mut(), - 0, - ) - }; - return; - } - - let mut name_buf = [0u8; 1024]; - let name_ptr = copy_nul_terminated(&mut name_buf, name); - - // SAFETY: c-ares FFI; name_ptr is a NUL-terminated stack buffer; ctx outlives the channel. - unsafe { - ares_query( - self, - name_ptr, - NSClass::ns_c_in, - T::NS_TYPE, - Some(T::RAW_CALLBACK), - std::ptr::from_mut::(ctx).cast::(), - ); - } - } - - pub fn get_host_by_addr(&mut self, ip_addr: &[u8], ctx: &mut T) { - // "0000:0000:0000:0000:0000:ffff:192.168.100.228".length = 45 - const BUF_SIZE: usize = 46; - let mut addr_buf = [0u8; BUF_SIZE]; - let addr_ptr: *const c_char = 'brk: { - if ip_addr.is_empty() || ip_addr.len() >= BUF_SIZE { - break 'brk ptr::null(); - } - copy_nul_terminated(&mut addr_buf, ip_addr) - }; - - // https://c-ares.org/ares_inet_pton.html - // https://github.com/c-ares/c-ares/blob/7f3262312f246556d8c1bdd8ccc1844847f42787/src/lib/ares_gethostbyaddr.c#L71-L72 - // `ares_inet_pton` allows passing raw bytes as `dst`, - // which can avoid the use of `struct_in_addr` to reduce extra bytes. - let mut addr = [0u8; 16]; - if !addr_ptr.is_null() { - // SAFETY: c-ares FFI; addr_ptr is a NUL-terminated stack buffer, addr is 16-byte stack scratch. - if unsafe { ares_inet_pton(AF::INET, addr_ptr, addr.as_mut_ptr().cast::()) } > 0 - { - // SAFETY: c-ares FFI; addr holds a 4-byte in_addr written by ares_inet_pton; ctx outlives the channel. - unsafe { - ares_gethostbyaddr( - self, - addr.as_ptr().cast::(), - 4, - AF::INET, - Some(struct_hostent::host_callback_wrapper::), - std::ptr::from_mut::(ctx).cast::(), - ); - } - return; - // SAFETY: c-ares FFI; addr_ptr is a NUL-terminated stack buffer, addr is 16-byte stack scratch. - } else if unsafe { - ares_inet_pton(AF::INET6, addr_ptr, addr.as_mut_ptr().cast::()) - } > 0 - { - // SAFETY: c-ares FFI; addr holds a 16-byte in6_addr written by ares_inet_pton; ctx outlives the channel. - unsafe { - ares_gethostbyaddr( - self, - addr.as_ptr().cast::(), - 16, - AF::INET6, - Some(struct_hostent::host_callback_wrapper::), - std::ptr::from_mut::(ctx).cast::(), - ); - } - return; - } - } - ctx.on_hostent(Some(Error::ENOTIMP), 0, ptr::null_mut()); - } - - /// https://c-ares.org/ares_getnameinfo.html - pub fn get_name_info(&mut self, sa: &sockaddr_storage, ctx: &mut T) { - let salen: ares_socklen_t = if sa.ss_family == AF::INET as _ { - core::mem::size_of::() as ares_socklen_t - } else { - core::mem::size_of::() as ares_socklen_t - }; - // SAFETY: c-ares FFI; sa is a valid sockaddr of size `salen`; ctx outlives the channel. - unsafe { - ares_getnameinfo( - self, - std::ptr::from_ref::(sa).cast::(), - salen, - // node returns ENOTFOUND for addresses like 255.255.255.255:80 - // So, it requires setting the ARES_NI_NAMEREQD flag - ARES_NI_NAMEREQD | ARES_NI_LOOKUPHOST | ARES_NI_LOOKUPSERVICE, - Some(struct_nameinfo::callback_wrapper::), - std::ptr::from_mut::(ctx).cast::(), - ); - } - } - - #[inline] - pub fn process(&mut self, fd: ares_socket_t, readable: bool, writable: bool) { - ares_process_fd( - self, - if readable { fd } else { ARES_SOCKET_BAD }, - if writable { fd } else { ARES_SOCKET_BAD }, - ); - } -} - -fn library_init() { - bun_core::run_once! {{ - // SAFETY: c-ares FFI; mimalloc fn pointers have C ABI matching ares_library_init_mem's contract. - let rc = unsafe { - ares_library_init_mem( - ARES_LIB_INIT_ALL, - Some(bun_alloc::mimalloc::mi_malloc), - Some(bun_alloc::mimalloc::mi_free), - Some(bun_alloc::mimalloc::mi_realloc), - ) - }; - if rc != ARES_SUCCESS { - panic!("ares_library_init_mem failed: {}", rc); - } - }} -} - -pub(crate) type ares_callback = - Option; -pub(crate) type ares_host_callback = - Option; -pub(crate) type ares_nameinfo_callback = - Option; -pub(crate) type ares_addrinfo_callback = - unsafe extern "C" fn(*mut c_void, c_int, c_int, *mut AddrInfo); - -unsafe extern "C" { - fn ares_library_init_mem( - flags: c_int, - amalloc: Option *mut c_void>, - afree: Option, - arealloc: Option *mut c_void>, - ) -> c_int; - pub fn ares_init_options( - channelptr: *mut *mut Channel, - options: *mut Options, - optmask: c_int, - ) -> c_int; - pub fn ares_destroy(channel: *mut Channel); - // Opaque handle by exclusive reference only — `Channel` is `!Freeze`/`!Sync` - // (UnsafeCell + PhantomData<*mut u8>). Note: `ares_cancel`/`ares_process_fd` - // synchronously invoke stored completion callbacks which may re-enter the - // resolver and re-derive a `&mut Channel` from a raw pointer; this is sound - // because `Channel` is a ZST (`UnsafeCell<[u8;0]>`), so `&mut Channel` - // claims zero bytes and overlapping `&mut` do not conflict under Stacked - // Borrows — the borrow checker does NOT gate the raw-pointer callbacks. - pub safe fn ares_cancel(channel: &mut Channel); - pub safe fn ares_set_local_ip4(channel: &mut Channel, local_ip: c_uint); - pub fn ares_set_local_ip6(channel: *mut Channel, local_ip6: *const u8); - pub fn ares_getaddrinfo( - channel: *mut Channel, - node: *const c_char, - service: *const c_char, - hints: *const AddrInfo_hints, - callback: ares_addrinfo_callback, - arg: *mut c_void, - ); - pub fn ares_freeaddrinfo(ai: *mut AddrInfo); -} - -unsafe extern "C" { - pub fn ares_query( - channel: *mut Channel, - name: *const c_char, - dnsclass: NSClass, - type_: NSType, - callback: ares_callback, - arg: *mut c_void, - ); - pub fn ares_gethostbyaddr( - channel: *mut Channel, - addr: *const c_void, - addrlen: c_int, - family: c_int, - callback: ares_host_callback, - arg: *mut c_void, - ); - pub fn ares_getnameinfo( - channel: *mut Channel, - sa: *const sockaddr, - salen: ares_socklen_t, - flags: c_int, - callback: ares_nameinfo_callback, - arg: *mut c_void, - ); - // Opaque handle by exclusive reference + scalars only. - pub safe fn ares_process_fd( - channel: &mut Channel, - read_fd: ares_socket_t, - write_fd: ares_socket_t, - ); - // Pure read of opaque `!Sync` handle. - pub safe fn ares_queue_active_queries(channel: &Channel) -> usize; -} - -#[repr(C)] -#[derive(Copy, Clone)] -union union_unnamed_2 { - _S6_u8: [u8; 16], -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct struct_ares_in6_addr { - _S6_un: union_unnamed_2, -} - -#[repr(C)] -#[derive(Copy, Clone, Default)] -pub struct struct_ares_addrttl { - pub(crate) ipaddr: u32, - pub(crate) ttl: c_int, -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct struct_ares_addr6ttl { - pub ip6addr: struct_ares_in6_addr, - pub(crate) ttl: c_int, -} - -bun_core::impl_zeroable!(struct_ares_addr6ttl); -impl Default for struct_ares_addr6ttl { - #[inline] - fn default() -> Self { - bun_core::ffi::zeroed() - } -} - -// ────────────────────────────────────────────────────────────────────────── -// Generic reply-record plumbing. The six `struct_ares_{caa,srv,mx,txt,naptr,soa}_reply` -// types share an identical c-ares callback thunk modulo (reply type, parse fn), -// and all six `ares_parse_*_reply` externs share the signature -// `(abuf *const u8, alen c_int, out *mut *mut R) -> c_int`. Collapsed from six -// copy-pasted `callback_wrapper` bodies + six per-type handler traits. -// ────────────────────────────────────────────────────────────────────────── - -/// A c-ares reply record whose parser has the canonical 3-arg signature. -pub trait AresReply: Sized { - /// The matching `ares_parse_*_reply` extern. - const PARSE: unsafe extern "C" fn(*const u8, c_int, *mut *mut Self) -> c_int; -} - -/// Receiver for a parsed `R` reply (replaces the per-type `*Handler` traits). -pub trait ReplyHandler: Sized { - fn on_reply(&mut self, status: Option, timeouts: i32, results: *mut R); -} - -/// Generic `ares_callback` thunk. Monomorphized per `(R, T)` to a concrete -/// `unsafe extern "C" fn`, so the fn-pointer ABI passed to `ares_query` is -/// identical to the former hand-rolled `callback_wrapper`s. -pub unsafe extern "C" fn ares_reply_callback>( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - buffer: *mut u8, - buffer_length: c_int, -) { - // SAFETY: ctx was passed as *mut T to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - if status != ARES_SUCCESS { - this.on_reply(Error::get(status), timeouts, ptr::null_mut()); - return; - } - let mut start: *mut R = ptr::null_mut(); - // SAFETY: c-ares FFI; pointers are valid stack/null per contract. - let result = unsafe { (R::PARSE)(buffer, buffer_length, &raw mut start) }; - if result != ARES_SUCCESS { - this.on_reply(Error::get(result), timeouts, ptr::null_mut()); - return; - } - this.on_reply(None, timeouts, start); -} - -#[repr(C)] -pub struct struct_ares_caa_reply { - pub next: *mut struct_ares_caa_reply, - pub critical: c_int, - pub(crate) property: *mut u8, - pub(crate) plength: usize, - pub(crate) value: *mut u8, - pub(crate) length: usize, -} - -impl AresReply for struct_ares_caa_reply { - const PARSE: unsafe extern "C" fn(*const u8, c_int, *mut *mut Self) -> c_int = - ares_parse_caa_reply; -} - -impl struct_ares_caa_reply { - /// Safe view of the c-ares-owned property tag bytes. - #[inline] - pub fn property_bytes(&self) -> &[u8] { - if self.property.is_null() { - &[] - } else { - // SAFETY: c-ares allocates `property` as a contiguous buffer of - // `plength` bytes that lives until `ares_free_data` is called on the - // list head; the `&self` borrow is shorter than that. c-ares never - // sets a non-zero length with a null pointer. - unsafe { core::slice::from_raw_parts(self.property, self.plength) } - } - } - /// Safe view of the c-ares-owned value bytes. - #[inline] - pub fn value_bytes(&self) -> &[u8] { - if self.value.is_null() { - &[] - } else { - // SAFETY: same invariant as `property_bytes` — `value` points to - // `length` bytes owned by the reply node for `&self`'s lifetime. - unsafe { core::slice::from_raw_parts(self.value, self.length) } - } - } -} - -#[repr(C)] -pub struct struct_ares_srv_reply { - pub next: *mut struct_ares_srv_reply, - pub host: *mut u8, - pub priority: c_ushort, - pub weight: c_ushort, - pub port: c_ushort, -} - -impl AresReply for struct_ares_srv_reply { - const PARSE: unsafe extern "C" fn(*const u8, c_int, *mut *mut Self) -> c_int = - ares_parse_srv_reply; -} - -#[repr(C)] -pub struct struct_ares_mx_reply { - pub next: *mut struct_ares_mx_reply, - pub host: *mut u8, - pub priority: c_ushort, -} - -impl AresReply for struct_ares_mx_reply { - const PARSE: unsafe extern "C" fn(*const u8, c_int, *mut *mut Self) -> c_int = - ares_parse_mx_reply; -} - -#[repr(C)] -pub struct struct_ares_txt_reply { - pub next: *mut struct_ares_txt_reply, - pub(crate) txt: *mut u8, - pub(crate) length: usize, -} - -impl AresReply for struct_ares_txt_reply { - const PARSE: unsafe extern "C" fn(*const u8, c_int, *mut *mut Self) -> c_int = - ares_parse_txt_reply; -} - -impl struct_ares_txt_reply { - /// Safe view of the c-ares-owned TXT record bytes. - #[inline] - pub fn txt_bytes(&self) -> &[u8] { - if self.txt.is_null() { - &[] - } else { - // SAFETY: c-ares allocates `txt` as `length` bytes that live until - // `ares_free_data` on the list head; `&self` is the shorter borrow. - unsafe { core::slice::from_raw_parts(self.txt, self.length) } - } - } -} - -#[repr(C)] -pub struct struct_ares_naptr_reply { - pub next: *mut struct_ares_naptr_reply, - pub flags: *mut u8, - pub service: *mut u8, - pub regexp: *mut u8, - pub replacement: *mut u8, - pub order: c_ushort, - pub preference: c_ushort, -} - -impl AresReply for struct_ares_naptr_reply { - const PARSE: unsafe extern "C" fn(*const u8, c_int, *mut *mut Self) -> c_int = - ares_parse_naptr_reply; -} - -#[repr(C)] -pub struct struct_ares_soa_reply { - pub nsname: *mut u8, - pub hostmaster: *mut u8, - pub serial: c_uint, - pub refresh: c_uint, - pub retry: c_uint, - pub expire: c_uint, - pub minttl: c_uint, -} - -impl AresReply for struct_ares_soa_reply { - const PARSE: unsafe extern "C" fn(*const u8, c_int, *mut *mut Self) -> c_int = - ares_parse_soa_reply; -} - -pub struct struct_any_reply { - pub a_reply: Option>, - pub aaaa_reply: Option>, - pub mx_reply: *mut struct_ares_mx_reply, - pub ns_reply: *mut struct_hostent, - pub txt_reply: *mut struct_ares_txt_reply, - pub srv_reply: *mut struct_ares_srv_reply, - pub ptr_reply: *mut struct_hostent, - pub naptr_reply: *mut struct_ares_naptr_reply, - pub soa_reply: *mut struct_ares_soa_reply, - pub caa_reply: *mut struct_ares_caa_reply, -} - -impl Default for struct_any_reply { - fn default() -> Self { - Self { - a_reply: None, - aaaa_reply: None, - mx_reply: ptr::null_mut(), - ns_reply: ptr::null_mut(), - txt_reply: ptr::null_mut(), - srv_reply: ptr::null_mut(), - ptr_reply: ptr::null_mut(), - naptr_reply: ptr::null_mut(), - soa_reply: ptr::null_mut(), - caa_reply: ptr::null_mut(), - } - } -} - -pub trait AnyHandler: Sized { - fn on_any( - &mut self, - status: Option, - timeouts: i32, - results: Option>, - ); -} - -impl struct_any_reply { - // toJSResponse / toJS aliases deleted. - - pub unsafe extern "C" fn callback_wrapper( - ctx: *mut c_void, - status: c_int, - timeouts: c_int, - buffer: *mut u8, - buffer_length: c_int, - ) { - // SAFETY: ctx was passed as *mut T to the ares call that registered this thunk. - let this = unsafe { bun_core::callback_ctx::(ctx) }; - if status != ARES_SUCCESS { - this.on_any(Error::get(status), timeouts, None); - return; - } - // SAFETY: c-ares guarantees `buffer` is non-null and readable for - // `buffer_length` bytes on a successful callback. - let buffer = unsafe { - core::slice::from_raw_parts(buffer, usize::try_from(buffer_length).unwrap_or(0)) - }; - match Self::parse(buffer) { - Ok(reply) => this.on_any(None, timeouts, Some(reply)), - Err(err) => this.on_any(Some(err), timeouts, None), - } - } - - /// Parse a DNS `ANY` reply buffer into a heap-allocated aggregate. Returns - /// the last per-record parse error if no record type parsed successfully. - pub(crate) fn parse(buffer: &[u8]) -> Result, Error> { - let mut any_success = false; - let mut last_error: Option = None; - let mut reply = Box::new(struct_any_reply::default()); - let abuf = buffer.as_ptr(); - let alen = c_int::try_from(buffer.len()).unwrap_or(c_int::MAX); - - match hostent_with_ttls::parse_a(buffer) { - Ok(result) => { - reply.a_reply = Some(result); - any_success = true; - } - Err(err) => last_error = Some(err as c_int), - } - - match hostent_with_ttls::parse_aaaa(buffer) { - Ok(result) => { - reply.aaaa_reply = Some(result); - any_success = true; - } - Err(err) => last_error = Some(err as c_int), - } - - // SAFETY: c-ares FFI; `abuf[..alen]` is a valid slice; out-params are - // valid stack pointers per contract. - let mut result = unsafe { ares_parse_mx_reply(abuf, alen, &raw mut reply.mx_reply) }; - if result == ARES_SUCCESS { - any_success = true; - } else { - last_error = Some(result); - } - - // SAFETY: see `ares_parse_mx_reply` call above. - result = unsafe { ares_parse_ns_reply(abuf, alen, &raw mut reply.ns_reply) }; - if result == ARES_SUCCESS { - any_success = true; - } else { - last_error = Some(result); - } - - // SAFETY: see `ares_parse_mx_reply` call above. - result = unsafe { ares_parse_txt_reply(abuf, alen, &raw mut reply.txt_reply) }; - if result == ARES_SUCCESS { - any_success = true; - } else { - last_error = Some(result); - } - - // SAFETY: see `ares_parse_mx_reply` call above. - result = unsafe { ares_parse_srv_reply(abuf, alen, &raw mut reply.srv_reply) }; - if result == ARES_SUCCESS { - any_success = true; - } else { - last_error = Some(result); - } - - // SAFETY: see `ares_parse_mx_reply` call above. - result = unsafe { - ares_parse_ptr_reply( - abuf, - alen, - ptr::null(), - 0, - AF::INET, - &raw mut reply.ptr_reply, - ) - }; - if result == ARES_SUCCESS { - any_success = true; - } else { - last_error = Some(result); - } - - // SAFETY: see `ares_parse_mx_reply` call above. - result = unsafe { ares_parse_naptr_reply(abuf, alen, &raw mut reply.naptr_reply) }; - if result == ARES_SUCCESS { - any_success = true; - } else { - last_error = Some(result); - } - - // SAFETY: see `ares_parse_mx_reply` call above. - result = unsafe { ares_parse_soa_reply(abuf, alen, &raw mut reply.soa_reply) }; - if result == ARES_SUCCESS { - any_success = true; - } else { - last_error = Some(result); - } - - // SAFETY: see `ares_parse_mx_reply` call above. - result = unsafe { ares_parse_caa_reply(abuf, alen, &raw mut reply.caa_reply) }; - if result == ARES_SUCCESS { - any_success = true; - } else { - last_error = Some(result); - } - - if !any_success { - return Err(Error::get(last_error.unwrap()).unwrap()); - } - Ok(reply) - } -} - -impl Drop for struct_any_reply { - fn drop(&mut self) { - // a_reply / aaaa_reply are Box; their Drop frees the - // inner hostent via ares_free_hostent. - // SAFETY: each field is either null or a c-ares allocation matching its free fn. - unsafe { - if !self.mx_reply.is_null() { - ares_free_data(self.mx_reply.cast::()); - } - if !self.ns_reply.is_null() { - ares_free_hostent(self.ns_reply); - } - if !self.txt_reply.is_null() { - ares_free_data(self.txt_reply.cast::()); - } - if !self.srv_reply.is_null() { - ares_free_data(self.srv_reply.cast::()); - } - if !self.ptr_reply.is_null() { - ares_free_hostent(self.ptr_reply); - } - if !self.naptr_reply.is_null() { - ares_free_data(self.naptr_reply.cast::()); - } - if !self.soa_reply.is_null() { - ares_free_data(self.soa_reply.cast::()); - } - if !self.caa_reply.is_null() { - ares_free_data(self.caa_reply.cast::()); - } - } - } -} - -unsafe extern "C" { - pub fn ares_parse_a_reply( - abuf: *const u8, - alen: c_int, - host: *mut *mut struct_hostent, - addrttls: *mut struct_ares_addrttl, - naddrttls: *mut c_int, - ) -> c_int; - pub fn ares_parse_aaaa_reply( - abuf: *const u8, - alen: c_int, - host: *mut *mut struct_hostent, - addrttls: *mut struct_ares_addr6ttl, - naddrttls: *mut c_int, - ) -> c_int; - pub fn ares_parse_caa_reply( - abuf: *const u8, - alen: c_int, - caa_out: *mut *mut struct_ares_caa_reply, - ) -> c_int; - pub fn ares_parse_ptr_reply( - abuf: *const u8, - alen: c_int, - addr: *const c_void, - addrlen: c_int, - family: c_int, - host: *mut *mut struct_hostent, - ) -> c_int; - pub fn ares_parse_ns_reply( - abuf: *const u8, - alen: c_int, - host: *mut *mut struct_hostent, - ) -> c_int; - pub fn ares_parse_srv_reply( - abuf: *const u8, - alen: c_int, - srv_out: *mut *mut struct_ares_srv_reply, - ) -> c_int; - pub fn ares_parse_mx_reply( - abuf: *const u8, - alen: c_int, - mx_out: *mut *mut struct_ares_mx_reply, - ) -> c_int; - pub fn ares_parse_txt_reply( - abuf: *const u8, - alen: c_int, - txt_out: *mut *mut struct_ares_txt_reply, - ) -> c_int; - pub fn ares_parse_naptr_reply( - abuf: *const u8, - alen: c_int, - naptr_out: *mut *mut struct_ares_naptr_reply, - ) -> c_int; - pub fn ares_parse_soa_reply( - abuf: *const u8, - alen: c_int, - soa_out: *mut *mut struct_ares_soa_reply, - ) -> c_int; - pub fn ares_free_hostent(host: *mut struct_hostent); - pub fn ares_free_data(dataptr: *mut c_void); -} - -#[repr(C)] -#[derive(Copy, Clone)] -union union_unnamed_4 { - addr4: in_addr, - addr6: struct_ares_in6_addr, -} - -#[repr(C)] -pub struct struct_ares_addr_port_node { - pub next: *mut struct_ares_addr_port_node, - pub family: c_int, - addr: union_unnamed_4, - pub udp_port: c_int, - pub tcp_port: c_int, -} -// SAFETY: `#[repr(C)]` POD — raw ptr, `c_int`s, and a byte-array union. -// All-zero is the state callers fill before `ares_inet_pton` (S021). -unsafe impl bun_core::ffi::Zeroable for struct_ares_addr_port_node {} - -impl struct_ares_addr_port_node { - /// Type-erased pointer to the in_addr/in6_addr union, for `ares_inet_ntop`. - /// The union field stays private (active arm depends on `family`), but - /// callers need its address to round-trip through c-ares' presentation - /// converters. - #[inline] - pub fn addr_ptr(&self) -> *const c_void { - ptr::addr_of!(self.addr).cast::() - } - /// Mutable counterpart of `addr_ptr` for `ares_inet_pton` to fill. - #[inline] - pub fn addr_mut_ptr(&mut self) -> *mut c_void { - ptr::addr_of_mut!(self.addr).cast::() - } -} - -unsafe extern "C" { - pub fn ares_set_servers_ports( - channel: *mut Channel, - servers: *mut struct_ares_addr_port_node, - ) -> c_int; - pub fn ares_get_servers_ports( - channel: *mut Channel, - servers: *mut *mut struct_ares_addr_port_node, - ) -> c_int; - /// https://c-ares.org/docs/ares_inet_ntop.html - pub fn ares_inet_ntop( - af: c_int, - src: *const c_void, - dst: *mut u8, - size: ares_socklen_t, - ) -> *const c_char; - /// https://c-ares.org/docs/ares_inet_pton.html - /// - /// ## Returns - /// - `1` if `src` was valid for the specified address family - /// - `0` if `src` was not parseable in the specified address family - /// - `-1` if some system error occurred. `errno` will have been set. - pub fn ares_inet_pton(af: c_int, src: *const c_char, dst: *mut c_void) -> c_int; -} - -pub const ARES_SUCCESS: c_int = 0; -pub(crate) const ARES_ENODATA: c_int = 1; -const ARES_EFORMERR: c_int = 2; -const ARES_ESERVFAIL: c_int = 3; -pub(crate) const ARES_ENOTFOUND: c_int = 4; -const ARES_ENOTIMP: c_int = 5; -const ARES_EREFUSED: c_int = 6; -const ARES_EBADQUERY: c_int = 7; -pub(crate) const ARES_EBADNAME: c_int = 8; -const ARES_EBADFAMILY: c_int = 9; -const ARES_EBADRESP: c_int = 10; -const ARES_ECONNREFUSED: c_int = 11; -const ARES_ETIMEOUT: c_int = 12; -const ARES_EOF: c_int = 13; -const ARES_EFILE: c_int = 14; -const ARES_ENOMEM: c_int = 15; -pub const ARES_EDESTRUCTION: c_int = 16; -const ARES_EBADSTR: c_int = 17; -const ARES_EBADFLAGS: c_int = 18; -pub(crate) const ARES_ENONAME: c_int = 19; -const ARES_EBADHINTS: c_int = 20; -const ARES_ENOTINITIALIZED: c_int = 21; -const ARES_ELOADIPHLPAPI: c_int = 22; -const ARES_EADDRGETNETWORKPARAMS: c_int = 23; -const ARES_ECANCELLED: c_int = 24; -const ARES_ESERVICE: c_int = 25; -pub(crate) const ARES_ENOSERVER: c_int = 26; - -#[repr(i32)] -#[derive(Copy, Clone, Eq, PartialEq, Debug, strum::IntoStaticStr)] -pub enum Error { - ENODATA = ARES_ENODATA, - EFORMERR = ARES_EFORMERR, - ESERVFAIL = ARES_ESERVFAIL, - ENOTFOUND = ARES_ENOTFOUND, - ENOTIMP = ARES_ENOTIMP, - EREFUSED = ARES_EREFUSED, - EBADQUERY = ARES_EBADQUERY, - EBADNAME = ARES_EBADNAME, - EBADFAMILY = ARES_EBADFAMILY, - EBADRESP = ARES_EBADRESP, - ECONNREFUSED = ARES_ECONNREFUSED, - ETIMEOUT = ARES_ETIMEOUT, - EOF = ARES_EOF, - EFILE = ARES_EFILE, - ENOMEM = ARES_ENOMEM, - EDESTRUCTION = ARES_EDESTRUCTION, - EBADSTR = ARES_EBADSTR, - EBADFLAGS = ARES_EBADFLAGS, - ENONAME = ARES_ENONAME, - EBADHINTS = ARES_EBADHINTS, - ENOTINITIALIZED = ARES_ENOTINITIALIZED, - ELOADIPHLPAPI = ARES_ELOADIPHLPAPI, - EADDRGETNETWORKPARAMS = ARES_EADDRGETNETWORKPARAMS, - ECANCELLED = ARES_ECANCELLED, - ESERVICE = ARES_ESERVICE, - ENOSERVER = ARES_ENOSERVER, - /// getaddrinfo(3) `EAI_AGAIN`. Not a c-ares status: [`Error::get`] never - /// produces it, and its errno comes from [`Error::errno`]. - EAI_AGAIN = ARES_ENOSERVER + 1, -} - -/// libuv's `UV_EAI_AGAIN`, the `errno` Node reports for getaddrinfo `EAI_AGAIN`. -const UV_EAI_AGAIN: i32 = -3001; - -impl Error { - // Deferred / toDeferred / toJSWithSyscall / toJSWithSyscallAndHostname - // aliases deleted — live in bun_runtime::dns_jsc (extension trait). - - pub fn init_eai(rc: i32) -> Option { - #[cfg(windows)] - { - use bun_libuv_sys as libuv; - // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/errors.js#L807-L815 - if rc == libuv::UV_EAI_NODATA || rc == libuv::UV_EAI_NONAME { - return Some(Error::ENOTFOUND); - } - // TODO: revisit this - return match rc { - 0 => None, - libuv::UV_EAI_AGAIN => Some(Error::EAI_AGAIN), - libuv::UV_EAI_ADDRFAMILY => Some(Error::EBADFAMILY), - libuv::UV_EAI_BADFLAGS => Some(Error::EBADFLAGS), - libuv::UV_EAI_BADHINTS => Some(Error::EBADHINTS), - libuv::UV_EAI_CANCELED => Some(Error::ECANCELLED), - libuv::UV_EAI_FAIL => Some(Error::ENOTFOUND), - libuv::UV_EAI_FAMILY => Some(Error::EBADFAMILY), - libuv::UV_EAI_MEMORY => Some(Error::ENOMEM), - libuv::UV_EAI_NODATA => Some(Error::ENODATA), - libuv::UV_EAI_NONAME => Some(Error::ENONAME), - libuv::UV_EAI_OVERFLOW => Some(Error::ENOMEM), - libuv::UV_EAI_PROTOCOL => Some(Error::EBADQUERY), - libuv::UV_EAI_SERVICE => Some(Error::ESERVICE), - libuv::UV_EAI_SOCKTYPE => Some(Error::ECONNREFUSED), - _ => Some(Error::ENOTFOUND), // UV_ENOENT and non documented errors - }; - } - - #[cfg(not(windows))] - { - let eai = EAI::from_raw(rc); - - // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/errors.js#L807-L815 - if eai == EAI::NODATA || eai == EAI::NONAME { - return Some(Error::ENOTFOUND); - } - - #[cfg(any(target_os = "linux", target_os = "android"))] - if eai == EAI::SOCKTYPE { - return Some(Error::ECONNREFUSED); - } - - // glibc-only async getaddrinfo_a / IDN extensions; absent on - // musl and bionic. - #[cfg(all(target_os = "linux", target_env = "gnu"))] - match eai { - EAI::IDN_ENCODE => return Some(Error::EBADSTR), - EAI::ALLDONE => return Some(Error::ENOTFOUND), - EAI::INPROGRESS => return Some(Error::ETIMEOUT), - EAI::CANCELED => return Some(Error::ECANCELLED), - EAI::NOTCANCELED => return Some(Error::ECANCELLED), - _ => {} - } - - if rc == 0 { - return None; - } - match eai { - EAI::ADDRFAMILY => Some(Error::EBADFAMILY), - EAI::AGAIN => Some(Error::EAI_AGAIN), - EAI::BADFLAGS => Some(Error::EBADFLAGS), // Invalid hints - EAI::FAIL => Some(Error::EBADRESP), - EAI::FAMILY => Some(Error::EBADFAMILY), - EAI::MEMORY => Some(Error::ENOMEM), - EAI::SERVICE => Some(Error::ESERVICE), - EAI::SYSTEM => Some(Error::ESERVFAIL), - // Any EAI code not mapped above is reported as "not implemented". - _ => Some(Error::ENOTIMP), - } - } - } - - /// This platform's raw getaddrinfo(3) status for the `EAI_*` code named `name`. - pub fn eai_raw_by_name(name: &[u8]) -> Option { - #[cfg(windows)] - { - use bun_libuv_sys as libuv; - match name { - b"EAI_AGAIN" => Some(libuv::UV_EAI_AGAIN), - b"EAI_FAIL" => Some(libuv::UV_EAI_FAIL), - b"EAI_NONAME" => Some(libuv::UV_EAI_NONAME), - _ => None, - } - } - #[cfg(not(windows))] - { - match name { - b"EAI_AGAIN" => Some(EAI::AGAIN.0), - b"EAI_FAIL" => Some(EAI::FAIL.0), - b"EAI_NONAME" => Some(EAI::NONAME.0), - _ => None, - } - } - } - - /// The JS-visible `errno` for this status. - pub fn errno(self) -> i32 { - match self { - Error::EAI_AGAIN => UV_EAI_AGAIN, - _ => self as i32, - } - } - - pub fn code(self) -> &'static str { - match self { - Error::ENODATA => "DNS_ENODATA", - Error::EFORMERR => "DNS_EFORMERR", - Error::ESERVFAIL => "DNS_ESERVFAIL", - Error::ENOTFOUND => "DNS_ENOTFOUND", - Error::ENOTIMP => "DNS_ENOTIMP", - Error::EREFUSED => "DNS_EREFUSED", - Error::EBADQUERY => "DNS_EBADQUERY", - Error::EBADNAME => "DNS_ENOTFOUND", - Error::EBADFAMILY => "DNS_EBADFAMILY", - Error::EBADRESP => "DNS_EBADRESP", - Error::ECONNREFUSED => "DNS_ECONNREFUSED", - Error::ETIMEOUT => "DNS_ETIMEOUT", - Error::EOF => "DNS_EOF", - Error::EFILE => "DNS_EFILE", - Error::ENOMEM => "DNS_ENOMEM", - Error::EDESTRUCTION => "DNS_EDESTRUCTION", - Error::EBADSTR => "DNS_EBADSTR", - Error::EBADFLAGS => "DNS_EBADFLAGS", - Error::ENONAME => "DNS_ENOTFOUND", - Error::EBADHINTS => "DNS_EBADHINTS", - Error::ENOTINITIALIZED => "DNS_ENOTINITIALIZED", - Error::ELOADIPHLPAPI => "DNS_ELOADIPHLPAPI", - Error::EADDRGETNETWORKPARAMS => "DNS_EADDRGETNETWORKPARAMS", - Error::ECANCELLED => "DNS_ECANCELLED", - Error::ESERVICE => "DNS_ESERVICE", - Error::ENOSERVER => "DNS_ENOSERVER", - Error::EAI_AGAIN => "DNS_EAI_AGAIN", - } - } - - pub fn label(self) -> &'static str { - match self { - Error::ENODATA => "No data record of requested type", - Error::EFORMERR => "Malformed DNS query", - Error::ESERVFAIL => "Server failed to complete the DNS operation", - Error::ENOTFOUND => "Domain name not found", - Error::ENOTIMP => "DNS resolver does not implement requested operation", - Error::EREFUSED => "DNS operation refused", - Error::EBADQUERY => "Misformatted DNS query", - Error::EBADNAME => "Misformatted domain name", - Error::EBADFAMILY => "Misformatted DNS query (family)", - Error::EBADRESP => "Misformatted DNS reply", - Error::ECONNREFUSED => "Could not contact DNS servers", - Error::ETIMEOUT => "Timeout while contacting DNS servers", - Error::EOF => "End of file", - Error::EFILE => "Error reading file", - Error::ENOMEM => "Out of memory", - Error::EDESTRUCTION => "Channel is being destroyed", - Error::EBADSTR => "Misformatted string", - Error::EBADFLAGS => "Illegal flags specified", - Error::ENONAME => "Given hostname is not numeric", - Error::EBADHINTS => "Illegal hints flags specified", - Error::ENOTINITIALIZED => "Library initialization not yet performed", - Error::ELOADIPHLPAPI => "ELOADIPHLPAPI TODO WHAT DOES THIS MEAN", - Error::EADDRGETNETWORKPARAMS => "EADDRGETNETWORKPARAMS", - Error::ECANCELLED => "DNS query cancelled", - Error::ESERVICE => "Service not available", - Error::ENOSERVER => "No DNS servers were configured", - Error::EAI_AGAIN => "Temporary failure in name resolution", - } - } - - pub fn get(rc: i32) -> Option { - // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/errors.js#L807-L815 - if rc == ARES_ENODATA || rc == ARES_ENONAME { - return Self::get(ARES_ENOTFOUND); - } - - if rc == 0 { - return None; - } - // c-ares returns positive ARES_* codes; Node's wrapper sometimes negates. - // `unsigned_abs` avoids the i32::MIN overflow that `.abs()` would hit. - let n = rc.unsigned_abs(); - assert!( - (1..=ARES_ENOSERVER as u32).contains(&n), - "c-ares status {rc} out of range", - ); - // SAFETY: `n` is in `1..=ARES_ENOSERVER`; `Error` is `#[repr(i32)]` with - // contiguous discriminants `1..=ARES_ENOSERVER + 1`, so every value in - // the asserted range is a valid discriminant. - Some(unsafe { core::mem::transmute::(n as i32) }) - } -} - -pub(crate) const ARES_FLAG_NOCHECKRESP: c_int = 1 << 7; -pub(crate) const ARES_OPT_FLAGS: c_int = 1 << 0; -pub(crate) const ARES_OPT_TRIES: c_int = 1 << 2; -pub(crate) const ARES_OPT_SOCK_STATE_CB: c_int = 1 << 9; -pub(crate) const ARES_OPT_TIMEOUTMS: c_int = 1 << 13; -pub(crate) const ARES_NI_NAMEREQD: c_int = 1 << 2; -pub(crate) const ARES_NI_LOOKUPHOST: c_int = 1 << 8; -pub(crate) const ARES_NI_LOOKUPSERVICE: c_int = 1 << 9; - -pub(crate) const ARES_LIB_INIT_WIN32: c_int = 1 << 0; -pub(crate) const ARES_LIB_INIT_ALL: c_int = ARES_LIB_INIT_WIN32; - -#[cfg(windows)] -pub const ARES_SOCKET_BAD: ares_socket_t = usize::MAX; // INVALID_SOCKET -#[cfg(not(windows))] -pub const ARES_SOCKET_BAD: ares_socket_t = -1; - -/// Creates a sockaddr structure from an address, port. -/// -/// # Parameters -/// - `addr`: A byte slice representing the IP address. -/// - `port`: A 16-bit unsigned integer representing the port number. -/// - `sa`: A pointer to a sockaddr structure where the result will be stored. -/// -/// # Returns -/// -/// This function returns 0 on success. -pub fn get_sockaddr(addr: &[u8], port: u16, sa: &mut sockaddr_storage) -> c_int { - const BUF_SIZE: usize = 128; - - let mut buf = [0u8; BUF_SIZE]; - if addr.is_empty() || addr.len() >= BUF_SIZE { - return -1; - } - let addr_ptr = copy_nul_terminated(&mut buf, addr); - - { - // SAFETY: caller-provided sockaddr storage; reinterpreting as sockaddr_in. - let in_: &mut sockaddr_in = - unsafe { &mut *std::ptr::from_mut::(sa).cast::() }; - // SAFETY: c-ares FFI; `addr_ptr` is a NUL-terminated stack buffer, dst is `sin_addr` storage. - if unsafe { ares_inet_pton(AF::INET, addr_ptr, (&raw mut in_.sin_addr).cast::()) } - == 1 - { - in_.sin_family = AF::INET as _; - in_.sin_port = port.to_be(); - return 0; - } - } - let mut octets = [0u8; 16]; - // SAFETY: c-ares FFI; `addr_ptr` is a NUL-terminated stack buffer, dst is a - // 16-byte `in6_addr`-sized buffer. - if unsafe { ares_inet_pton(AF::INET6, addr_ptr, (&raw mut octets).cast::()) } != 1 { - return -1; - } - - if octets[..12] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff] { - // IPv4-mapped `::ffff:a.b.c.d`: store as AF_INET. The consumer is - // `ares_getnameinfo`, which (unlike the OS getnameinfo Node uses) has - // no v4-mapped handling and would issue an ip6.arpa PTR query that - // never resolves. - // SAFETY: caller-provided sockaddr storage; reinterpreting as sockaddr_in. - let in_: &mut sockaddr_in = - unsafe { &mut *std::ptr::from_mut::(sa).cast::() }; - in_.sin_family = AF::INET as _; - in_.sin_port = port.to_be(); - in_.sin_addr.s_addr = u32::from_ne_bytes([octets[12], octets[13], octets[14], octets[15]]); - return 0; - } - - // SAFETY: caller-provided sockaddr storage; reinterpreting as sockaddr_in6. - let in6: &mut sockaddr_in6 = - unsafe { &mut *std::ptr::from_mut::(sa).cast::() }; - in6.sin6_family = AF::INET6 as _; - in6.sin6_port = port.to_be(); - in6.sin6_addr.s6_addr = octets; - 0 -} - -/// The C `struct in_addr` (4-byte IPv4 address), as c-ares' `ares_options.servers` -/// and the `ares_addr_node`/`ares_addr_port_node` unions declare it. -#[repr(C)] -#[derive(Copy, Clone)] -pub struct in_addr { - pub s_addr: u32, -} diff --git a/src/cares_sys/lib.rs b/src/cares_sys/lib.rs deleted file mode 100644 index 8d8f1840164a..000000000000 --- a/src/cares_sys/lib.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![allow(non_camel_case_types)] -#![warn(unused_must_use)] - -#[path = "c_ares.rs"] -pub mod c_ares_draft; - -/// Winsock typedefs not provided by `libc` on `x86_64-pc-windows-msvc`. -#[cfg(windows)] -pub mod winsock { - use core::ffi::c_int; - pub(crate) type socklen_t = c_int; // ws2tcpip.h: `typedef int socklen_t;` - // Same nominal type as `bun_sys::posix::sockaddr*`. - pub(crate) use bun_libuv_sys::{sockaddr, sockaddr_in, sockaddr_in6, sockaddr_storage}; -} - -/// `c_ares` and `c_ares_draft` resolve to the same module. -pub use c_ares_draft as c_ares; - -// Crate-root re-exports for callers that reference `bun_cares_sys::ares_inet_*` -// directly (e.g. `bun_boringssl`). -pub use c_ares::{ares_inet_ntop, ares_inet_pton}; - -/// Thin wrapper over `ares_inet_ntop`: writes the textual address into `dst` -/// and returns the slice up to (excluding) the trailing NUL on success. -/// `dst[len] == 0` is guaranteed on `Some`, so callers needing a C string can -/// rely on it. -/// -/// # Safety -/// `src` must point to a valid `in_addr` (af == AF_INET) or `in6_addr` -/// (af == AF_INET6). -#[inline] -pub unsafe fn ntop( - af: core::ffi::c_int, - src: *const core::ffi::c_void, - dst: &mut [u8], -) -> Option<&[u8]> { - // SAFETY: caller contract guarantees `src` points to a valid `in_addr` / - // `in6_addr` matching `af`; `dst` is a Rust slice so `dst.as_mut_ptr()` is - // valid for `dst.len()` writes, and `ares_inet_ntop` writes at most `size` - // bytes (including the trailing NUL) per c-ares docs. - if unsafe { - c_ares::ares_inet_ntop( - af, - src, - dst.as_mut_ptr(), - dst.len() as c_ares::ares_socklen_t, - ) - } - .is_null() - { - return None; - } - Some(bun_core::ffi::slice_to_nul(dst)) -} diff --git a/src/collections/hive_array.rs b/src/collections/hive_array.rs index afef6bf8aa41..911563d3f1ce 100644 --- a/src/collections/hive_array.rs +++ b/src/collections/hive_array.rs @@ -81,19 +81,6 @@ impl HiveBitSet { self.masks[w].set(self.masks[w].get() & !(1usize << (index % WORD_BITS))); } - #[inline] - pub fn find_first_set(&self) -> Option { - let mut i = 0; - while i < Self::NUM_WORDS { - let m = self.masks[i].get(); - if m != 0 { - return Some(i * WORD_BITS + m.trailing_zeros() as usize); - } - i += 1; - } - None - } - #[inline] pub fn find_first_unset(&self) -> Option { let mut i = 0; @@ -469,7 +456,7 @@ impl Drop for HiveSlot<'_, T, CAPACITY> { /// extracts the value. Single-owner (no `Clone`), so [`DerefMut`] is sound. /// /// For pools whose tokens cross an opaque round-trip as a slot *index* (e.g. -/// the c-ares callback context in `dns_jsc`), store +/// the pending getaddrinfo requests in `dns_jsc`), store /// [`HiveArray::index_of`] and recover via [`HiveArray::box_at`]. pub struct HiveBox<'a, T, const CAPACITY: usize> { slot: NonNull, @@ -1071,7 +1058,7 @@ mod tests { #[test] fn hive_bitset_iteration() { let a = HiveArray::::init(); - assert_eq!(a.used.find_first_set(), None); + assert_eq!(a.used.iter_set().next(), None); assert_eq!(a.used.find_first_unset(), Some(0)); // Claim slots 0..3, then free 0 and 2 so the set is {1, 3}. @@ -1084,7 +1071,6 @@ mod tests { // SAFETY: only token for its slot. assert!(unsafe { a.put_raw(s2.as_ptr()) }); - assert_eq!(a.used.find_first_set(), Some(1)); assert_eq!(a.used.find_first_unset(), Some(0)); let mut it = a.used.iter_set(); @@ -1106,7 +1092,7 @@ mod tests { unsafe { HiveArray::init_in_place(a.as_mut_ptr()); let a = &*a.as_ptr(); - assert_eq!(a.used.find_first_set(), None); + assert_eq!(a.used.iter_set().next(), None); let p = a.get_init(7).unwrap(); assert_eq!(*p.as_ptr(), 7); // `p` is the only token for its slot; already inside the outer diff --git a/src/dns/Cargo.toml b/src/dns/Cargo.toml index 53e84e1ca7fa..e18a789c58ca 100644 --- a/src/dns/Cargo.toml +++ b/src/dns/Cargo.toml @@ -10,12 +10,14 @@ path = "lib.rs" workspace = true [dependencies] +bstr.workspace = true strum.workspace = true libc.workspace = true bun_core.workspace = true bun_sys.workspace = true -bun_cares_sys.workspace = true +bun_dns_resolver.workspace = true bun_wyhash.workspace = true [target.'cfg(windows)'.dependencies] +bun_libuv_sys.workspace = true bun_windows_sys.workspace = true diff --git a/src/dns/eai.rs b/src/dns/eai.rs new file mode 100644 index 000000000000..992754989f18 --- /dev/null +++ b/src/dns/eai.rs @@ -0,0 +1,117 @@ +//! getaddrinfo(3) statuses as the errors `node:dns` reports. + +use bun_dns_resolver::Error; + +/// `EAI_*`. The `libc` crate has no `EAI_ADDRFAMILY` and none of glibc's +/// `getaddrinfo_a` extensions, so those are the values of ``. +#[cfg(not(windows))] +mod eai { + use core::ffi::c_int; + + #[cfg(target_os = "linux")] + pub(super) const ADDRFAMILY: c_int = -9; + #[cfg(not(target_os = "linux"))] + pub(super) const ADDRFAMILY: c_int = 1; + + // RFC 3493 dropped EAI_NODATA; FreeBSD's only exposes it under + // __BSD_VISIBLE (historical value 7) and the libc crate omits it entirely. + #[cfg(not(any(target_os = "freebsd", target_os = "dragonfly")))] + pub(super) const NODATA: c_int = libc::EAI_NODATA; + #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] + pub(super) const NODATA: c_int = 7; + + // glibc only (absent on musl, bionic). + #[cfg(all(target_os = "linux", target_env = "gnu"))] + pub(super) const INPROGRESS: c_int = -100; + #[cfg(all(target_os = "linux", target_env = "gnu"))] + pub(super) const CANCELED: c_int = -101; + #[cfg(all(target_os = "linux", target_env = "gnu"))] + pub(super) const NOTCANCELED: c_int = -102; + #[cfg(all(target_os = "linux", target_env = "gnu"))] + pub(super) const ALLDONE: c_int = -103; + #[cfg(all(target_os = "linux", target_env = "gnu"))] + pub(super) const IDN_ENCODE: c_int = -105; +} + +/// `None` for success. On Windows `rc` is libuv's `UV_EAI_*`. +pub fn error_from_eai(rc: i32) -> Option { + #[cfg(windows)] + { + use bun_libuv_sys as libuv; + // TODO: revisit this + match rc { + 0 => None, + // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/errors.js#L807-L815 + libuv::UV_EAI_NODATA | libuv::UV_EAI_NONAME => Some(Error::ENOTFOUND), + libuv::UV_EAI_AGAIN => Some(Error::EAI_AGAIN), + libuv::UV_EAI_ADDRFAMILY => Some(Error::EBADFAMILY), + libuv::UV_EAI_BADFLAGS => Some(Error::EBADFLAGS), + libuv::UV_EAI_BADHINTS => Some(Error::EBADHINTS), + libuv::UV_EAI_CANCELED => Some(Error::ECANCELLED), + libuv::UV_EAI_FAIL => Some(Error::ENOTFOUND), + libuv::UV_EAI_FAMILY => Some(Error::EBADFAMILY), + libuv::UV_EAI_MEMORY => Some(Error::ENOMEM), + libuv::UV_EAI_OVERFLOW => Some(Error::ENOMEM), + libuv::UV_EAI_PROTOCOL => Some(Error::EBADQUERY), + libuv::UV_EAI_SERVICE => Some(Error::ESERVICE), + libuv::UV_EAI_SOCKTYPE => Some(Error::ECONNREFUSED), + _ => Some(Error::ENOTFOUND), // UV_ENOENT and non documented errors + } + } + + #[cfg(not(windows))] + { + #[cfg(any(target_os = "linux", target_os = "android"))] + if rc == libc::EAI_SOCKTYPE { + return Some(Error::ECONNREFUSED); + } + + #[cfg(all(target_os = "linux", target_env = "gnu"))] + match rc { + eai::IDN_ENCODE => return Some(Error::EBADSTR), + eai::ALLDONE => return Some(Error::ENOTFOUND), + eai::INPROGRESS => return Some(Error::ETIMEOUT), + eai::CANCELED | eai::NOTCANCELED => return Some(Error::ECANCELLED), + _ => {} + } + + match rc { + 0 => None, + // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/errors.js#L807-L815 + eai::NODATA | libc::EAI_NONAME => Some(Error::ENOTFOUND), + eai::ADDRFAMILY => Some(Error::EBADFAMILY), + libc::EAI_AGAIN => Some(Error::EAI_AGAIN), + libc::EAI_BADFLAGS => Some(Error::EBADFLAGS), // Invalid hints + libc::EAI_FAIL => Some(Error::EBADRESP), + libc::EAI_FAMILY => Some(Error::EBADFAMILY), + libc::EAI_MEMORY => Some(Error::ENOMEM), + libc::EAI_SERVICE => Some(Error::ESERVICE), + libc::EAI_SYSTEM => Some(Error::ESERVFAIL), + // Any EAI code not mapped above is reported as "not implemented". + _ => Some(Error::ENOTIMP), + } + } +} + +/// This platform's raw getaddrinfo(3) status for the `EAI_*` code named `name`. +pub fn eai_raw_by_name(name: &[u8]) -> Option { + #[cfg(windows)] + { + use bun_libuv_sys as libuv; + match name { + b"EAI_AGAIN" => Some(libuv::UV_EAI_AGAIN), + b"EAI_FAIL" => Some(libuv::UV_EAI_FAIL), + b"EAI_NONAME" => Some(libuv::UV_EAI_NONAME), + _ => None, + } + } + #[cfg(not(windows))] + { + match name { + b"EAI_AGAIN" => Some(libc::EAI_AGAIN), + b"EAI_FAIL" => Some(libc::EAI_FAIL), + b"EAI_NONAME" => Some(libc::EAI_NONAME), + _ => None, + } + } +} diff --git a/src/dns/lib.rs b/src/dns/lib.rs index aac68528dcf6..4fa3bb1d8c20 100644 --- a/src/dns/lib.rs +++ b/src/dns/lib.rs @@ -1,7 +1,10 @@ #![warn(unused_must_use)] use core::ffi::c_int; -use std::io::Write as _; + +mod eai; +pub use bun_dns_resolver::Error; +pub use eai::{eai_raw_by_name, error_from_eai}; use bun_core::String as BunString; // `bun_wyhash` exports `Wyhash11` (iterative init/update/final_ surface). @@ -72,17 +75,6 @@ impl GetAddrInfo { } } - pub fn to_cares(&self) -> bun_cares_sys::c_ares_draft::AddrInfo_hints { - let mut hints: bun_cares_sys::c_ares_draft::AddrInfo_hints = bun_core::ffi::zeroed(); - - hints.ai_family = self.options.family.to_libc(); - hints.ai_socktype = self.options.socktype.to_libc(); - hints.ai_protocol = self.options.protocol.to_libc(); - hints.ai_flags = self.options.flags; - - hints - } - pub fn hash(&self) -> u64 { let mut hasher = Wyhash::init(0); hasher.update(&self.port.to_ne_bytes()); @@ -273,8 +265,7 @@ impl Backend { Backend::System } - // Android: c-ares can't discover nameservers (no /etc/resolv.conf, - // no JNI for ares_library_init_android). bionic getaddrinfo proxies + // Android: there is no /etc/resolv.conf to find the nameservers in. bionic getaddrinfo proxies // through netd which knows the real resolvers. #[cfg(all(not(any(target_os = "macos", windows)), target_os = "android"))] pub const fn default() -> Backend { @@ -358,36 +349,21 @@ pub fn address_to_string(address: &Address) -> BunString { match address.family() { sock::AF_INET => { let v4 = address.as_in4().unwrap(); // family() just checked - let bytes: [u8; 4] = v4.sin_addr.s_addr.to_ne_bytes(); - BunString::create_format(format_args!( - "{}.{}.{}.{}", - bytes[0], bytes[1], bytes[2], bytes[3] - )) + let octets = v4.sin_addr.s_addr.to_ne_bytes(); + BunString::clone_latin1(&bun_core::ip_address::format_v4(octets)) } sock::AF_INET6 => { let v6 = address.as_in6().unwrap(); // family() just checked - // Render the bare address directly via ares_inet_ntop, then - // re-append the `%scope_id` suffix for nonzero scope (e.g. fe80:: - // link-local from getaddrinfo) — ntop only sees the 16 raw addr - // bytes and cannot emit it itself. - let mut buf = [0u8; 64]; // >= INET6_ADDRSTRLEN (46) + "%4294967295" (11) - // SAFETY: sin6_addr is a valid in6_addr; buf len fits INET6_ADDRSTRLEN. - let n = match unsafe { - bun_cares_sys::ntop(sock::AF_INET6, (&raw const v6.sin6_addr).cast(), &mut buf) - } { - Some(s) => s.len(), - None => return BunString::EMPTY, - }; - let len = if v6.sin6_scope_id != 0 { - let mut cursor = &mut buf[n..]; - let before = cursor.len(); - // 64 - 46 = 18 > len("%4294967295") = 11, cannot truncate. - let _ = write!(cursor, "%{}", v6.sin6_scope_id); - n + (before - cursor.len()) - } else { - n - }; - BunString::clone_latin1(&buf[..len]) + let text = bun_core::ip_address::format_v6(v6.sin6_addr.s6_addr); + // A link-local address from getaddrinfo (fe80::) has a scope. + if v6.sin6_scope_id == 0 { + return BunString::clone_latin1(&text); + } + BunString::create_format(format_args!( + "{}%{}", + bstr::BStr::new(&*text), + v6.sin6_scope_id + )) } sock::AF_UNIX => { // Unix sockets exist on every target Bun ships (Windows 10 rs4+ @@ -459,7 +435,7 @@ impl Order { } /// A numeric address, or a host name within the RFC 1035 limits made of the bytes -/// c-ares allows in one (`ares_is_hostnamech`) or of non-ASCII bytes (UTF-8 mDNS names). +/// `bun_dns_resolver` allows in one (`is_hostname_byte`) or of non-ASCII bytes (UTF-8 mDNS names). pub fn is_valid_hostname(name: &[u8]) -> bool { fn is_hostname_byte(b: u8) -> bool { b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'/' | b'*') || !b.is_ascii() @@ -479,7 +455,7 @@ pub fn is_valid_hostname(name: &[u8]) -> bool { } /// The process-wide DNS -/// cache lives in `bun_runtime` (it owns libinfo/libuv worker threads + JSC +/// cache lives in `bun_runtime` (it owns dns_sd/libuv worker threads + JSC /// stat counters). Lower-tier crates (`bun_http`, `bun_install`) reach it via /// the link-time `Bun__addrinfo_*` family — same mechanism usockets C uses — /// rather than a `bun_runtime` crate dep, which would cycle. diff --git a/src/dns_resolver/Cargo.toml b/src/dns_resolver/Cargo.toml new file mode 100644 index 000000000000..bc6d6cafd42a --- /dev/null +++ b/src/dns_resolver/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bun_dns_resolver" +version.workspace = true +edition.workspace = true + +[lib] +path = "lib.rs" + +[lints] +workspace = true + +[dependencies] +bun_core.workspace = true +thiserror.workspace = true diff --git a/src/dns_resolver/addr_info.rs b/src/dns_resolver/addr_info.rs new file mode 100644 index 000000000000..7b399ad99401 --- /dev/null +++ b/src/dns_resolver/addr_info.rs @@ -0,0 +1,128 @@ +//! Addresses and aliases collected from answers and the hosts file. + +use core::net::IpAddr; + +use crate::message::{Message, Name, RData, class, rr_type}; +use crate::{Error, Result}; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum Family { + Unspecified, + V4, + V6, +} + +impl Family { + pub(crate) fn of(address: &IpAddr) -> Family { + match address { + IpAddr::V4(_) => Family::V4, + IpAddr::V6(_) => Family::V6, + } + } + + pub(crate) fn includes(self, other: Family) -> bool { + self == Family::Unspecified || self == other + } +} + +/// A CNAME record. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Alias { + pub(crate) ttl: i32, + pub(crate) owner: Name, + pub(crate) target: Name, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Node { + pub ttl: i32, + pub address: IpAddr, + pub port: u16, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct AddrInfo { + pub(crate) name: Option, + pub(crate) aliases: Vec, + pub nodes: Vec, +} + +fn ttl_as_int(ttl: u32) -> i32 { + i32::try_from(ttl).unwrap_or(i32::MAX) +} + +/// What to make of a response with CNAME records and no address. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum AliasesAlone { + AreAnAnswer, + AreNoData, +} + +impl AddrInfo { + pub(crate) fn has_family(&self, family: Family) -> bool { + self.nodes + .iter() + .any(|node| Family::of(&node.address) == family) + } + + pub(crate) fn push(&mut self, address: IpAddr, port: u16, ttl: u32) { + self.nodes.push(Node { + ttl: ttl_as_int(ttl), + address, + port, + }); + } + + /// Adds the CNAME records of `response`'s answer section, and the addresses of the type that + /// its question asks for. + pub(crate) fn add_response( + &mut self, + response: &Message, + aliases_alone: AliasesAlone, + port: u16, + ) -> Result<()> { + let qtype = response.question.qtype; + let is_asked_for = |rr_type| qtype == rr_type || qtype == rr_type::ANY; + let mut hostname = &response.question.name; + let mut found = AddrInfo::default(); + + for rr in &response.answer { + if rr.class != class::IN { + continue; + } + match &rr.data { + RData::Cname(target) => { + hostname = target; + found.aliases.push(Alias { + ttl: ttl_as_int(rr.ttl), + owner: rr.name.clone(), + target: target.clone(), + }); + } + RData::A(address) if is_asked_for(rr_type::A) => { + found.push(IpAddr::from(*address), port, rr.ttl); + } + RData::Aaaa(address) if is_asked_for(rr_type::AAAA) => { + found.push(IpAddr::from(*address), port, rr.ttl); + } + _ => {} + } + } + if found.nodes.is_empty() + && (found.aliases.is_empty() || aliases_alone == AliasesAlone::AreNoData) + { + return Err(Error::ENODATA); + } + + let is_same_name = self + .name + .as_ref() + .is_some_and(|name| name.eq_ignore_ascii_case(hostname)); + if !is_same_name { + self.name = Some(hostname.clone()); + } + self.nodes.append(&mut found.nodes); + self.aliases.append(&mut found.aliases); + Ok(()) + } +} diff --git a/src/dns_resolver/channel/connection.rs b/src/dns_resolver/channel/connection.rs new file mode 100644 index 000000000000..c5792d5eedb3 --- /dev/null +++ b/src/dns_resolver/channel/connection.rs @@ -0,0 +1,246 @@ +//! The sockets of a channel: one TCP connection to a server at most, and as many UDP ones as +//! timeouts have retired. + +use std::collections::BTreeSet; + +use super::server::ServerId; +use super::{Channel, Serial}; +use crate::io::{IoError, Socket, Time, Transport}; +use crate::{Error, Result}; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub(super) struct ConnectionId(u64); + +pub(super) struct Connection { + socket: Socket, + pub(super) server: ServerId, + pub(super) transport: Transport, + /// Nothing has been written yet to a TCP Fast Open socket, so the first write is what connects + /// it. + pub(super) fast_open_pending: bool, + pub(super) connected: bool, + /// A query timed out here. Packets may be getting dropped, so new queries leave from another + /// port. + pub(super) retired: bool, + /// `writable` as [`crate::io::Io::watch`] last heard it. `None`: it has not been called yet. + watching_writable: Option, + /// Messages, each after its length in 16 bits, whatever the transport. + pub(super) received: Vec, + /// In the same form. + unsent: Vec, + /// Sent and not answered, each after its `Query::send_order`: oldest first. + pub(super) queries: BTreeSet<(u64, Serial)>, +} + +const LARGEST_MESSAGE: usize = 65535; + +impl Channel { + pub(super) fn connection_of_socket(&self, socket: Socket) -> Option { + let mut connections = self.connections.iter(); + let (id, _) = connections.find(|(_, connection)| connection.socket == socket)?; + Some(*id) + } + + fn watch(&mut self, id: ConnectionId, writable: bool) { + let Some(connection) = self.connections.get_mut(&id) else { + return; + }; + if connection.watching_writable != Some(writable) { + connection.watching_writable = Some(writable); + self.io.watch(connection.socket, writable); + } + } + + pub(super) fn open_connection( + &mut self, + server_id: ServerId, + transport: Transport, + ) -> Result { + let server = self.server(server_id).ok_or(Error::ECONNREFUSED)?; + let address = server.socket_address(transport); + let local = if address.is_ipv4() { + self.local_v4.map(Into::into) + } else { + // A link-local server is reached from the link-local address of its interface. + let local_v6 = self.local_v6.filter(|_| server.config.scope_id == 0); + local_v6.map(Into::into) + }; + let opened = self + .io + .open(transport, address, local) + .map_err(|_| Error::ECONNREFUSED)?; + + self.last_connection_id += 1; + let id = ConnectionId(self.last_connection_id); + self.connections.insert( + id, + Connection { + socket: opened.socket, + server: server_id, + transport, + fast_open_pending: opened.fast_open, + connected: false, + retired: false, + watching_writable: None, + received: Vec::new(), + unsent: Vec::new(), + queries: BTreeSet::new(), + }, + ); + if let Some(server) = self.server_mut(server_id) { + match transport { + Transport::Udp => server.connections.push_front(id), + Transport::Tcp => { + server.connections.push_back(id); + server.tcp_connection = Some(id); + } + } + } + // A readable event before the first Fast Open write would read a socket that has no peer + // yet. + if !opened.fast_open { + self.watch(id, transport == Transport::Tcp); + } + Ok(id) + } + + /// Queues `message` on the connection and sends what can be sent now. + pub(super) fn write_query(&mut self, id: ConnectionId, message: &[u8]) -> Result<()> { + let connection = (self.connections.get_mut(&id)).ok_or(Error::ECONNREFUSED)?; + let length = u16::try_from(message.len()).map_err(|_| Error::EBADQUERY)?; + connection.unsent.extend_from_slice(&length.to_be_bytes()); + connection.unsent.extend_from_slice(message); + self.flush(id) + } + + pub(super) fn flush(&mut self, id: ConnectionId) -> Result<()> { + let connection = (self.connections.get_mut(&id)).ok_or(Error::ECONNREFUSED)?; + let is_tcp = connection.transport == Transport::Tcp; + let fast_open = connection.fast_open_pending; + while !connection.unsent.is_empty() { + if is_tcp && !connection.connected && !connection.fast_open_pending { + break; + } + connection.fast_open_pending = false; + let (prefix, data) = match connection.unsent.as_slice() { + all if is_tcp => (0, all), + [high, low, rest @ ..] => { + let length = usize::from(u16::from_be_bytes([*high, *low])); + (2, rest.get(..length).ok_or(Error::EFORMERR)?) + } + _ => return Err(Error::EFORMERR), + }; + let written = match self.io.send(connection.socket, data) { + Ok(written) => written, + Err(IoError::WouldBlock) => break, + Err(IoError::Failed) => return Err(Error::ECONNREFUSED), + }; + let consumed = (prefix + written).min(connection.unsent.len()); + connection.unsent.drain(..consumed); + // One datagram a message. A stream took all it could in one call. + if is_tcp { + break; + } + } + // Only a writable event says that a Fast Open socket has connected. A datagram that would + // block is left to time out. + let writable = fast_open || (is_tcp && !connection.unsent.is_empty()); + self.watch(id, writable); + Ok(()) + } + + /// Moves what the socket has into `received`. True if the socket then failed. + pub(super) fn read_socket(&mut self, id: ConnectionId) -> bool { + let server = (self.connections.get(&id)).and_then(|it| self.server(it.server)); + let expected = server.map(|server| (server.config.address, server.config.udp_port)); + let mut failed = false; + let mut buffer = core::mem::take(&mut self.read_buffer); + buffer.resize(LARGEST_MESSAGE, 0); + while let Some(connection) = self.connections.get_mut(&id) { + let received = match connection.transport { + Transport::Tcp => self.io.recv(connection.socket, &mut buffer), + // The socket is connected, so the system has normally dropped what others send + // already. + Transport::Udp => match self.io.recv_from(connection.socket, &mut buffer) { + // A port of 0: the system did not say. As in c-ares, what is queued behind + // a stranger's datagram waits for the next event. + Ok((_, from)) + if expected.is_some_and(|(address, port)| { + address != from.ip() || (from.port() != 0 && from.port() != port) + }) => + { + Err(IoError::WouldBlock) + } + Ok((count, _)) => Ok(count), + Err(error) => Err(error), + }, + }; + let count = match received { + Ok(count) => count, + Err(IoError::WouldBlock) => break, + Err(IoError::Failed) => { + failed = true; + break; + } + }; + connection.connected = true; + let data = buffer.get(..count).unwrap_or_default(); + if connection.transport == Transport::Udp { + let length = data.len() as u16; + connection.received.extend_from_slice(&length.to_be_bytes()); + } + connection.received.extend_from_slice(data); + if connection.transport == Transport::Tcp && count < buffer.len() { + break; + } + } + self.read_buffer = buffer; + failed + } + + /// Closes a connection whose server is to blame. + pub(super) fn connection_failed(&mut self, now: Time, id: ConnectionId, error: Error) { + let Some(connection) = self.connections.get(&id) else { + return; + }; + // Counted first, so that the queries are unlikely to go back to this server. + let server = connection.server; + self.count_server_failure(now, server); + self.close_connection(now, id, Some(error)); + } + + /// Its queries go out again on another connection, or end if they are out of tries: with + /// `reason`, if it is closed for a fault. + pub(super) fn close_connection(&mut self, now: Time, id: ConnectionId, reason: Option) { + let Some(connection) = self.connections.remove(&id) else { + return; + }; + if let Some(server) = self.server_mut(connection.server) { + server.connections.retain(|other| *other != id); + if server.tcp_connection == Some(id) { + server.tcp_connection = None; + } + } + for (_, serial) in connection.queries { + self.requeue_query(now, serial, reason, None); + } + self.io.close(connection.socket); + } + + pub(super) fn close_idle_connections(&mut self, now: Time) { + let is_idle = |id: &&ConnectionId| { + let connection = self.connections.get(id); + connection.is_some_and(|it| it.queries.is_empty()) + }; + let idle: Vec = self + .servers + .iter() + .flat_map(|server| &server.connections) + .filter(is_idle) + .copied() + .collect(); + for id in idle { + self.close_connection(now, id, None); + } + } +} diff --git a/src/dns_resolver/channel/exchange.rs b/src/dns_resolver/channel/exchange.rs new file mode 100644 index 000000000000..25491b3273bd --- /dev/null +++ b/src/dns_resolver/channel/exchange.rs @@ -0,0 +1,427 @@ +//! Sending queries, and what happens when a socket is ready or time runs out. + +use core::cmp::Reverse; +use std::collections::VecDeque; + +use super::connection::ConnectionId; +use super::server::ServerId; +use super::{Channel, Owner, Query, SERVER_RETRY_CHANCE, Serial}; +use crate::io::{Socket, Time, Transport}; +use crate::message::{Message, RData, flag, rcode}; +use crate::{Error, Result}; + +const COOKIE_OPTION: u16 = 10; +/// RFC 7873 section 4: a client cookie of 8 bytes, then a server cookie of 8 to 32 if there is one. +const COOKIE_LEN: core::ops::RangeInclusive = 8..=40; + +/// Work put off until the loop over a connection's messages, or over the deadlines, is done, +/// because it can close that connection. +pub(super) enum Deferred { + /// To whichever server is best by then. + Send, + End(Result), +} + +pub(super) type DeferredList = VecDeque<(Serial, Deferred)>; + +impl Channel { + /// To be called when a socket is ready for what [`crate::io::Io::watch`] named. + pub fn socket_ready(&mut self, now: Time, socket: Socket, readable: bool, writable: bool) { + if writable && let Some(id) = self.connection_of_socket(socket) { + self.process_write(now, id); + } + if readable && let Some(id) = self.connection_of_socket(socket) { + self.process_read(now, id); + } + self.check_timeouts(now); + } + + /// To be called at [`Channel::next_deadline`]. + pub fn check_timeouts(&mut self, now: Time) { + let mut deferred = DeferredList::new(); + while let Some(first @ (time, _, serial)) = self.deadlines.first().copied() + && time <= now + { + if let Some((connection, server)) = self.queries.get(&serial).and_then(|it| it.sent_on) + { + if let Some(connection) = self.connections.get_mut(&connection) { + connection.retired = true; + } + self.count_server_failure(now, server); + } + self.requeue_query(now, serial, Some(Error::ETIMEOUT), Some(&mut deferred)); + // So that the loop ends even if the query is gone. + self.deadlines.remove(&first); + } + self.run_deferred(now, deferred); + self.close_idle_connections(now); + } + + fn process_write(&mut self, now: Time, id: ConnectionId) { + if let Some(connection) = self.connections.get_mut(&id) + && !connection.fast_open_pending + { + connection.connected = true; + } + if let Err(error) = self.flush(id) { + self.connection_failed(now, id, error); + } + } + + fn process_read(&mut self, now: Time, id: ConnectionId) { + let failed = self.read_socket(id); + // What the socket delivered before it failed still counts. + self.read_responses(now, id); + if failed { + self.connection_failed(now, id, Error::ECONNREFUSED); + } + } + + fn read_responses(&mut self, now: Time, id: ConnectionId) { + let mut deferred = DeferredList::new(); + let mut received = match self.connections.get_mut(&id) { + Some(connection) => core::mem::take(&mut connection.received), + None => return, + }; + let mut rest = received.as_slice(); + while let [high, low, after @ ..] = rest + && let Some((bytes, after)) = + after.split_at_checked(usize::from(u16::from_be_bytes([*high, *low]))) + { + rest = after; + if let Err(error) = self.process_response(now, id, bytes, &mut deferred) { + self.connection_failed(now, id, error); + break; + } + } + if let Some(connection) = self.connections.get_mut(&id) { + let consumed = received.len() - rest.len(); + received.drain(..consumed); + connection.received = received; + } + self.run_deferred(now, deferred); + } + + /// An error closes the connection. That is the caller's to do. + fn process_response( + &mut self, + now: Time, + id: ConnectionId, + bytes: &[u8], + deferred: &mut DeferredList, + ) -> Result<()> { + // A datagram may be empty. + if bytes.is_empty() { + return Ok(()); + } + let response = match Message::parse(bytes) { + Ok(response) => response, + Err(_) if self.is_stray_datagram(id, bytes) => return Ok(()), + Err(_) => return Err(Error::EBADRESP), + }; + + let Some(connection) = self.connections.get(&id) else { + return Ok(()); + }; + let Some(serial) = self.serial_of_id.get(&response.id).copied() else { + return Ok(()); + }; + let Some(query) = self.queries.get_mut(&serial) else { + return Ok(()); + }; + // RFC 5452 section 9.1: the same question, from the server that was asked. + if !same_question(&query.message, &response) + || query.sent_on.map(|(connection, _)| connection) != Some(id) + || has_malformed_cookie(&response) + { + return Ok(()); + } + let server = connection.server; + + if response.flags & flag::TC != 0 && connection.transport == Transport::Udp { + query.transport = Transport::Tcp; + self.defer(deferred, serial, Deferred::Send); + return Ok(()); + } + + // A response that cannot be written again is not kept. That is no reason to refuse it. + let _ = self.cache.insert(now, &query.cache_key, &response); + self.clear_server_failures(server); + self.end_query(now, Some(server), serial, Ok(response), Some(deferred)); + Ok(()) + } + + /// Whoever guesses the port can send a datagram. To end the connection with one takes the id + /// of a query on it as well. + fn is_stray_datagram(&self, id: ConnectionId, bytes: &[u8]) -> bool { + let is_udp = (self.connections.get(&id)).is_some_and(|it| it.transport == Transport::Udp); + let query = bytes + .first_chunk() + .and_then(|id| self.serial_of_id.get(&u16::from_be_bytes(*id))) + .and_then(|serial| self.queries.get(serial)); + let sent_on = query.and_then(|query| query.sent_on); + is_udp && sent_on.map(|(connection, _)| connection) != Some(id) + } + + fn remove_from_connection(&mut self, serial: Serial) { + let Some(query) = self.queries.get_mut(&serial) else { + return; + }; + if let Some(time) = query.deadline.take() { + self.deadlines + .remove(&(time, Reverse(query.send_order), serial)); + } + if let Some((connection, _)) = query.sent_on.take() + && let Some(connection) = self.connections.get_mut(&connection) + { + connection.queries.remove(&(query.send_order, serial)); + } + } + + pub(super) fn detach_query(&mut self, serial: Serial) -> Option { + self.remove_from_connection(serial); + let query = self.queries.remove(&serial)?; + self.serial_of_id.remove(&query.message.id); + Some(query) + } + + fn defer(&mut self, deferred: &mut DeferredList, serial: Serial, what: Deferred) { + self.remove_from_connection(serial); + deferred.push_back((serial, what)); + } + + /// A send that fails adds to the list rather than recursing. + fn run_deferred(&mut self, now: Time, mut deferred: DeferredList) { + while let Some((serial, what)) = deferred.pop_front() { + match what { + Deferred::Send => { + if self.queries.contains_key(&serial) { + self.send_query_deferring(now, None, serial, &mut deferred); + } + } + Deferred::End(result) => { + if let Some(query) = self.detach_query(serial) { + self.finish(now, query.owner, result); + } + } + } + } + } + + /// After a try failed, for `reason` if there is one to give: sends the query again if it has + /// tries left, else ends it with the last reason. + pub(super) fn requeue_query( + &mut self, + now: Time, + serial: Serial, + reason: Option, + deferred: Option<&mut DeferredList>, + ) { + let max_tries = self.servers.len() * self.tries; + let Some(query) = self.queries.get_mut(&serial) else { + return; + }; + let server = query.sent_on.map(|(_, server)| server); + query.last_error = reason.or(query.last_error); + query.try_count += 1; + if query.try_count >= max_tries || query.no_retries { + let error = query.last_error.unwrap_or(Error::ETIMEOUT); + return self.end_query(now, server, serial, Err(error), deferred); + } + match deferred { + Some(deferred) => self.defer(deferred, serial, Deferred::Send), + None => { + self.remove_from_connection(serial); + self.send_query(now, None, serial); + } + } + } + + fn end_query( + &mut self, + now: Time, + server: Option, + serial: Serial, + result: Result, + deferred: Option<&mut DeferredList>, + ) { + let sent_at = self.queries.get(&serial).map(|query| query.sent_at); + if let Some(server) = server.and_then(|id| self.server_mut(id)) { + server.probe_pending = false; + if let Some(sent_at) = sent_at + && let Ok(rcode::NOERROR | rcode::NXDOMAIN) = result.as_ref().map(|it| it.rcode) + { + server.record_latency(now, sent_at); + } + } + match deferred { + Some(deferred) => self.defer(deferred, serial, Deferred::End(result)), + None => { + if let Some(query) = self.detach_query(serial) { + self.finish(now, query.owner, result); + } + } + } + } + + /// To `server`, or to the best one. When this returns, the query waits for a response or is + /// gone. + pub(super) fn send_query(&mut self, now: Time, server: Option, serial: Serial) { + let mut deferred = DeferredList::new(); + self.send_query_deferring(now, server, serial, &mut deferred); + self.run_deferred(now, deferred); + } + + fn pick_server(&mut self) -> Option { + let first = self.servers.first()?; + if !self.rotate { + return Some(first.id); + } + let fewest_failures = first.consecutive_failures; + let best = self + .servers + .iter() + .take_while(|server| server.consecutive_failures == fewest_failures) + .count(); + let mut byte = [0]; + self.io.random(&mut byte); + let [byte] = byte; + self.servers + .get(usize::from(byte) % best) + .map(|server| server.id) + } + + fn connection_for(&self, server: ServerId, transport: Transport) -> Option { + let server = self.server(server)?; + if transport == Transport::Tcp { + return server.tcp_connection; + } + let newest = *server.connections.front()?; + let connection = self.connections.get(&newest)?; + (connection.transport == Transport::Udp && !connection.retired).then_some(newest) + } + + /// [`Channel::send_query`], but what a failure leads to is left on `deferred`. + fn send_query_deferring( + &mut self, + now: Time, + requested_server: Option, + serial: Serial, + deferred: &mut DeferredList, + ) { + let Some(server) = requested_server.or_else(|| self.pick_server()) else { + return self.end_query(now, None, serial, Err(Error::ENOSERVER), Some(deferred)); + }; + let Some(query) = self.queries.get(&serial) else { + return; + }; + let transport = query.transport; + // When every server has failed, a probe is the only way one that is back and sorts behind + // the best gets noticed. + let may_probe = requested_server.is_none() && query.try_count == 0; + let bytes = query.message.write(0); + + let connection = match self.connection_for(server, transport) { + Some(connection) => connection, + None => match self.open_connection(server, transport) { + Ok(connection) => connection, + Err(error) => { + self.count_server_failure(now, server); + return self.requeue_query(now, serial, Some(error), Some(deferred)); + } + }, + }; + + match bytes.and_then(|bytes| self.write_query(connection, &bytes)) { + Ok(()) => {} + Err(Error::ECONNREFUSED) => { + self.connection_failed(now, connection, Error::ECONNREFUSED); + return self.requeue_query(now, serial, Some(Error::ECONNREFUSED), Some(deferred)); + } + Err(error) => { + self.count_server_failure(now, server); + return self.requeue_query(now, serial, Some(error), Some(deferred)); + } + } + + let deadline = now.plus_ms(self.query_timeout_ms(now, server, serial)); + if let Some(query) = self.queries.get_mut(&serial) { + self.last_send_order += 1; + query.sent_at = now; + query.deadline = Some(deadline); + query.sent_on = Some((connection, server)); + query.send_order = self.last_send_order; + self.deadlines + .insert((deadline, Reverse(query.send_order), serial)); + if let Some(connection) = self.connections.get_mut(&connection) { + connection.queries.insert((query.send_order, serial)); + } + } + + if may_probe { + self.probe_failed_server(now, server, serial); + } + } + + /// Doubles with every round through the servers, less up to half at random so that clients do + /// not retry in step. + fn query_timeout_ms(&mut self, now: Time, server: ServerId, serial: Serial) -> u64 { + let (Some(server), Some(query)) = (self.server(server), self.queries.get(&serial)) else { + return 0; + }; + let base = server.timeout_ms(now, self.timeout_ms); + let rounds = query.try_count / self.servers.len().max(1); + if rounds == 0 { + return base; + } + let doubled = u32::try_from(rounds) + .ok() + .and_then(|rounds| base.checked_shl(rounds)) + .filter(|doubled| doubled >> rounds == base) + .unwrap_or(u64::MAX); + let jitter = f32::from(self.random_u16()) / f32::from(u16::MAX) * 0.5; + (doubled - (doubled as f32 * jitter) as u64).max(base) + } + + /// Now and then, sends a copy of the query just sent to `current` to a server that has failed. + fn probe_failed_server(&mut self, now: Time, current: ServerId, serial: Serial) { + if self + .servers + .last() + .is_none_or(|worst| worst.consecutive_failures == 0) + || !self.random_u16().is_multiple_of(SERVER_RETRY_CHANCE) + { + return; + } + let due = self.servers.iter_mut().find(|server| { + server.id != current + && server.consecutive_failures > 0 + && !server.probe_pending + && server.next_retry_time <= now + }); + let (Some(server), Some(query)) = (due, self.queries.get(&serial)) else { + return; + }; + server.probe_pending = true; + let server = server.id; + let message = query.message.clone(); + self.start_query(now, &message, Owner::Probe { server }); + } +} + +fn same_question(query: &Message, response: &Message) -> bool { + let (asked, echoed) = (&query.question, &response.question); + (asked.qtype, asked.qclass) == (echoed.qtype, echoed.qclass) + && asked.name.eq_ignore_ascii_case(&echoed.name) +} + +/// RFC 7873 section 5.3: dropped, though no cookie was sent. +fn has_malformed_cookie(response: &Message) -> bool { + let Some(RData::Opt { options, .. }) = response.opt().map(|rr| &rr.data) else { + return false; + }; + options + .iter() + .find(|option| option.id == COOKIE_OPTION) + .is_some_and(|cookie| !cookie.value.is_empty() && !COOKIE_LEN.contains(&cookie.value.len())) +} diff --git a/src/dns_resolver/channel/mod.rs b/src/dns_resolver/channel/mod.rs new file mode 100644 index 000000000000..0fa4925399e5 --- /dev/null +++ b/src/dns_resolver/channel/mod.rs @@ -0,0 +1,499 @@ +//! A resolver's state: its configuration, servers, connections, and the queries in flight. + +mod connection; +mod exchange; +mod query_cache; +mod server; + +use core::cmp::Reverse; +use core::net::{Ipv4Addr, Ipv6Addr}; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + +use connection::{Connection, ConnectionId}; +use query_cache::QueryCache; +pub use server::ServerConfig; +use server::{Server, ServerId}; + +use crate::addr_info::AddrInfo; +use crate::hosts::HostsFile; +use crate::io::{Io, Time, Transport}; +use crate::lookup::{AddrInfoLookup, Host, HostLookup, NameInfo}; +use crate::message::{Message, rcode}; +use crate::reply::{RecordType, Reply}; +use crate::system_config::{self, Source}; +use crate::{Error, Result}; + +/// Counts the queries of a channel. Unlike the 16 bits on the wire, never used twice. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct Serial(u64); + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct LookupId(u64); + +const DEFAULT_TIMEOUT_MS: u64 = 2000; +/// c-ares's. `node:dns` asks for Node's 4 unless it is told otherwise. +const DEFAULT_TRIES: usize = 3; +/// One query in this many is copied to a server that failed, to see whether it is back. +const SERVER_RETRY_CHANCE: u16 = 10; + +/// `None` leaves it to the system's configuration, then to the default. +#[derive(Clone, Copy, Default, Debug)] +pub struct Options { + pub timeout_ms: Option, + pub tries: Option, +} + +#[derive(Debug)] +pub enum Answer { + Reply(Reply), + AddrInfo(AddrInfo), + Host(Host), + NameInfo(NameInfo), +} + +/// Whatever number the caller knows a request by. +pub type Token = u64; + +/// The end of one request. +#[derive(Debug)] +pub struct Completion { + /// What the request was made with. + pub token: Token, + pub result: Result, +} + +/// Who is told of a query's end. +#[derive(Clone, Copy)] +pub(crate) enum Owner { + Caller { + record_type: RecordType, + token: Token, + }, + AddrInfo { + lookup: LookupId, + }, + Host { + lookup: LookupId, + }, + /// A copy of another query, sent to a server that failed. Nobody waits for it: its outcome + /// only updates the server's failure count. + Probe { + server: ServerId, + }, +} + +struct Query { + message: Message, + cache_key: Vec, + owner: Owner, + transport: Transport, + no_retries: bool, + try_count: usize, + /// Why the last try that gave a reason failed. + last_error: Option, + sent_at: Time, + /// With `send_order`, its place in `Channel::deadlines`. + deadline: Option