diff --git a/CHANGELOG.md b/CHANGELOG.md index 138151a7616..817a819bd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang +## [3.75.1](https://github.com/metalbear-co/mirrord/tree/3.75.1) - 2023-11-14 + + +### Fixed + +- Add a hook for + [gethostbyname](https://www.man7.org/linux/man-pages/man3/gethostbyname.3.html) + to allow erlang/elixir to resolve DNS. + [#2055](https://github.com/metalbear-co/mirrord/issues/2055) +- Change spammy connect log's level from info to trace. + + +### Internal + +- Documentation of `env` config pattern matching. + + ## [3.75.0](https://github.com/metalbear-co/mirrord/tree/3.75.0) - 2023-11-08 diff --git a/Cargo.lock b/Cargo.lock index a0ffe38b264..d55f02f6e0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2278,7 +2278,7 @@ dependencies = [ [[package]] name = "fileops" -version = "3.75.0" +version = "3.75.1" dependencies = [ "libc", ] @@ -3107,7 +3107,7 @@ dependencies = [ [[package]] name = "issue1317" -version = "3.75.0" +version = "3.75.1" dependencies = [ "actix-web", "env_logger", @@ -3119,7 +3119,7 @@ dependencies = [ [[package]] name = "issue1776" -version = "3.75.0" +version = "3.75.1" dependencies = [ "errno 0.3.5", "libc", @@ -3128,7 +3128,7 @@ dependencies = [ [[package]] name = "issue1776portnot53" -version = "3.75.0" +version = "3.75.1" dependencies = [ "libc", "socket2 0.5.5", @@ -3136,14 +3136,14 @@ dependencies = [ [[package]] name = "issue1899" -version = "3.75.0" +version = "3.75.1" dependencies = [ "libc", ] [[package]] name = "issue2001" -version = "3.75.0" +version = "3.75.1" dependencies = [ "libc", ] @@ -3431,7 +3431,7 @@ checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "listen_ports" -version = "3.75.0" +version = "3.75.1" [[package]] name = "local-channel" @@ -3657,7 +3657,7 @@ dependencies = [ [[package]] name = "mirrord" -version = "3.75.0" +version = "3.75.1" dependencies = [ "actix-codec", "anyhow", @@ -3702,7 +3702,7 @@ dependencies = [ [[package]] name = "mirrord-agent" -version = "3.75.0" +version = "3.75.1" dependencies = [ "actix-codec", "async-trait", @@ -3755,7 +3755,7 @@ dependencies = [ [[package]] name = "mirrord-analytics" -version = "3.75.0" +version = "3.75.1" dependencies = [ "assert-json-diff", "base64 0.21.5", @@ -3769,7 +3769,7 @@ dependencies = [ [[package]] name = "mirrord-auth" -version = "3.75.0" +version = "3.75.1" dependencies = [ "chrono", "fs4", @@ -3789,7 +3789,7 @@ dependencies = [ [[package]] name = "mirrord-config" -version = "3.75.0" +version = "3.75.1" dependencies = [ "bimap", "bitflags 2.4.1", @@ -3811,7 +3811,7 @@ dependencies = [ [[package]] name = "mirrord-config-derive" -version = "3.75.0" +version = "3.75.1" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", @@ -3821,7 +3821,7 @@ dependencies = [ [[package]] name = "mirrord-console" -version = "3.75.0" +version = "3.75.1" dependencies = [ "bincode", "drain", @@ -3837,7 +3837,7 @@ dependencies = [ [[package]] name = "mirrord-intproxy" -version = "3.75.0" +version = "3.75.1" dependencies = [ "bytes", "http-body-util", @@ -3858,7 +3858,7 @@ dependencies = [ [[package]] name = "mirrord-intproxy-protocol" -version = "3.75.0" +version = "3.75.1" dependencies = [ "bincode", "mirrord-protocol", @@ -3868,7 +3868,7 @@ dependencies = [ [[package]] name = "mirrord-kube" -version = "3.75.0" +version = "3.75.1" dependencies = [ "actix-codec", "base64 0.21.5", @@ -3895,7 +3895,7 @@ dependencies = [ [[package]] name = "mirrord-layer" -version = "3.75.0" +version = "3.75.1" dependencies = [ "actix-codec", "anyhow", @@ -3950,7 +3950,7 @@ dependencies = [ [[package]] name = "mirrord-layer-macro" -version = "3.75.0" +version = "3.75.1" dependencies = [ "proc-macro2", "quote", @@ -3959,7 +3959,7 @@ dependencies = [ [[package]] name = "mirrord-macros" -version = "3.75.0" +version = "3.75.1" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", @@ -3969,7 +3969,7 @@ dependencies = [ [[package]] name = "mirrord-operator" -version = "3.75.0" +version = "3.75.1" dependencies = [ "actix-codec", "async-trait", @@ -4003,7 +4003,7 @@ dependencies = [ [[package]] name = "mirrord-progress" -version = "3.75.0" +version = "3.75.1" dependencies = [ "enum_dispatch", "indicatif", @@ -4035,7 +4035,7 @@ dependencies = [ [[package]] name = "mirrord-sip" -version = "3.75.0" +version = "3.75.1" dependencies = [ "apple-codesign", "memchr", @@ -4359,7 +4359,7 @@ dependencies = [ [[package]] name = "outgoing" -version = "3.75.0" +version = "3.75.1" [[package]] name = "outref" @@ -5333,21 +5333,21 @@ dependencies = [ [[package]] name = "rust-bypassed-unix-socket" -version = "3.75.0" +version = "3.75.1" dependencies = [ "tokio", ] [[package]] name = "rust-e2e-fileops" -version = "3.75.0" +version = "3.75.1" dependencies = [ "libc", ] [[package]] name = "rust-unix-socket-client" -version = "3.75.0" +version = "3.75.1" dependencies = [ "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index f7d8c538d53..bbf503eea70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ resolver = "2" # latest commits on rustls suppress certificate verification [workspace.package] -version = "3.75.0" +version = "3.75.1" edition = "2021" license = "MIT" readme = "README.md" diff --git a/mirrord-schema.json b/mirrord-schema.json index 911b7fa527b..69b1411d861 100644 --- a/mirrord-schema.json +++ b/mirrord-schema.json @@ -429,12 +429,12 @@ } }, "EnvFileConfig": { - "description": "Allows the user to set or override the local process' environment variables with the ones from the remote pod.\n\nWhich environment variables to load from the remote pod are controlled by setting either [`include`](#feature-env-include) or [`exclude`](#feature-env-exclude).\n\nSee the environment variables [reference](https://mirrord.dev/docs/reference/env/) for more details.\n\n```json { \"feature\": { \"env\": { \"include\": \"DATABASE_USER;PUBLIC_ENV\", \"exclude\": \"DATABASE_PASSWORD;SECRET_ENV\", \"override\": { \"DATABASE_CONNECTION\": \"db://localhost:7777/my-db\", \"LOCAL_BEAR\": \"panda\" } } } } ```", + "description": "Allows the user to set or override the local process' environment variables with the ones from the remote pod.\n\nWhich environment variables to load from the remote pod are controlled by setting either [`include`](#feature-env-include) or [`exclude`](#feature-env-exclude).\n\nSee the environment variables [reference](https://mirrord.dev/docs/reference/env/) for more details.\n\n```json { \"feature\": { \"env\": { \"include\": \"DATABASE_USER;PUBLIC_ENV;MY_APP_*\", \"exclude\": \"DATABASE_PASSWORD;SECRET_ENV\", \"override\": { \"DATABASE_CONNECTION\": \"db://localhost:7777/my-db\", \"LOCAL_BEAR\": \"panda\" } } } } ```", "type": "object", "properties": { "exclude": { "title": "feature.env.exclude {#feature-env-exclude}", - "description": "Include the remote environment variables in the local process that are **NOT** specified by this option.\n\nSome of the variables that are excluded by default: `PATH`, `HOME`, `HOMEPATH`, `CLASSPATH`, `JAVA_EXE`, `JAVA_HOME`, `PYTHONPATH`.\n\nValue is a list separated by \";\".", + "description": "Include the remote environment variables in the local process that are **NOT** specified by this option. Variable names can be matched using `*` and `?` where `?` matches exactly one occurrence of any character and `*` matches arbitrary many (including zero) occurrences of any character.\n\nSome of the variables that are excluded by default: `PATH`, `HOME`, `HOMEPATH`, `CLASSPATH`, `JAVA_EXE`, `JAVA_HOME`, `PYTHONPATH`.\n\nCan be passed as a list or as a semicolon-delimited string (e.g. `\"VAR;OTHER_VAR\"`).", "anyOf": [ { "$ref": "#/definitions/VecOrSingle_for_String" @@ -446,7 +446,7 @@ }, "include": { "title": "feature.env.include {#feature-env-include}", - "description": "Include only these remote environment variables in the local process.\n\nValue is a list separated by \";\".\n\nSome environment variables are excluded by default (`PATH` for example), including these requires specifying them with `include`", + "description": "Include only these remote environment variables in the local process. Variable names can be matched using `*` and `?` where `?` matches exactly one occurrence of any character and `*` matches arbitrary many (including zero) occurrences of any character.\n\nCan be passed as a list or as a semicolon-delimited string (e.g. `\"VAR;OTHER_VAR\"`).\n\nSome environment variables are excluded by default (`PATH` for example), including these requires specifying them with `include`", "anyOf": [ { "$ref": "#/definitions/VecOrSingle_for_String" diff --git a/mirrord/config/src/feature/env.rs b/mirrord/config/src/feature/env.rs index 9727aeb5bac..30bff8bfca1 100644 --- a/mirrord/config/src/feature/env.rs +++ b/mirrord/config/src/feature/env.rs @@ -21,7 +21,7 @@ use crate::{ /// { /// "feature": { /// "env": { -/// "include": "DATABASE_USER;PUBLIC_ENV", +/// "include": "DATABASE_USER;PUBLIC_ENV;MY_APP_*", /// "exclude": "DATABASE_PASSWORD;SECRET_ENV", /// "override": { /// "DATABASE_CONNECTION": "db://localhost:7777/my-db", @@ -38,8 +38,10 @@ pub struct EnvConfig { /// ### feature.env.include {#feature-env-include} /// /// Include only these remote environment variables in the local process. + /// Variable names can be matched using `*` and `?` where `?` matches exactly one occurrence of + /// any character and `*` matches arbitrary many (including zero) occurrences of any character. /// - /// Value is a list separated by ";". + /// Can be passed as a list or as a semicolon-delimited string (e.g. `"VAR;OTHER_VAR"`). /// /// Some environment variables are excluded by default (`PATH` for example), including these /// requires specifying them with `include` @@ -50,11 +52,13 @@ pub struct EnvConfig { /// /// Include the remote environment variables in the local process that are **NOT** specified by /// this option. + /// Variable names can be matched using `*` and `?` where `?` matches exactly one occurrence of + /// any character and `*` matches arbitrary many (including zero) occurrences of any character. /// /// Some of the variables that are excluded by default: /// `PATH`, `HOME`, `HOMEPATH`, `CLASSPATH`, `JAVA_EXE`, `JAVA_HOME`, `PYTHONPATH`. /// - /// Value is a list separated by ";". + /// Can be passed as a list or as a semicolon-delimited string (e.g. `"VAR;OTHER_VAR"`). #[config(env = "MIRRORD_OVERRIDE_ENV_VARS_EXCLUDE")] pub exclude: Option>, diff --git a/mirrord/layer/src/error.rs b/mirrord/layer/src/error.rs index d327fe584e8..a7dcccc4bf9 100644 --- a/mirrord/layer/src/error.rs +++ b/mirrord/layer/src/error.rs @@ -2,7 +2,7 @@ use std::{env::VarError, net::SocketAddr, ptr, str::ParseBoolError}; use errno::set_errno; use ignore_codes::*; -use libc::{c_char, DIR, FILE}; +use libc::{c_char, hostent, DIR, FILE}; use mirrord_config::{config::ConfigError, feature::network::outgoing::OutgoingFilterError}; use mirrord_protocol::{ResponseError, SerializationError}; #[cfg(target_os = "macos")] @@ -311,3 +311,9 @@ impl From for LayerError { LayerError::Frida(err) } } + +impl From for *mut hostent { + fn from(_fail: HookError) -> Self { + ptr::null_mut() + } +} diff --git a/mirrord/layer/src/socket/hooks.rs b/mirrord/layer/src/socket/hooks.rs index 576aece8544..3deb7e0d241 100644 --- a/mirrord/layer/src/socket/hooks.rs +++ b/mirrord/layer/src/socket/hooks.rs @@ -4,7 +4,7 @@ use std::{os::unix::io::RawFd, sync::LazyLock}; use dashmap::DashSet; use errno::{set_errno, Errno}; -use libc::{c_char, c_int, c_void, size_t, sockaddr, socklen_t, ssize_t, EINVAL}; +use libc::{c_char, c_int, c_void, hostent, size_t, sockaddr, socklen_t, ssize_t, EINVAL}; use mirrord_layer_macro::{hook_fn, hook_guard_fn}; use super::ops::*; @@ -108,6 +108,18 @@ pub(crate) unsafe extern "C" fn gethostname_detour( .unwrap_or_bypass_with(|_| FN_GETHOSTNAME(raw_name, name_length)) } +/// Hook for `libc::gethostbyname` (you won't find this in rust's `libc` as it's been deprecated and +/// removed). +/// +/// Resolves DNS `raw_name` and allocates a `static` [`libc::hostent`] that we change the inner +/// values whenever this function is called. The address itself of `*mut hostent` has to remain the +/// same (thus why it's a `static`). +#[hook_guard_fn] +unsafe extern "C" fn gethostbyname_detour(raw_name: *const c_char) -> *mut hostent { + let rawish_name = (!raw_name.is_null()).then(|| CStr::from_ptr(raw_name)); + gethostbyname(rawish_name).unwrap_or_bypass_with(|_| FN_GETHOSTBYNAME(raw_name)) +} + #[hook_guard_fn] pub(crate) unsafe extern "C" fn accept_detour( sockfd: c_int, @@ -503,6 +515,14 @@ pub(crate) unsafe fn enable_socket_hooks(hook_manager: &mut HookManager, enabled FN_GETHOSTNAME ); + replace!( + hook_manager, + "gethostbyname", + gethostbyname_detour, + FnGethostbyname, + FN_GETHOSTBYNAME + ); + #[cfg(target_os = "linux")] { // Here we replace a function of libuv and not libc, so we pass None as the . diff --git a/mirrord/layer/src/socket/ops.rs b/mirrord/layer/src/socket/ops.rs index 38b12e593dd..ec6c4a96735 100644 --- a/mirrord/layer/src/socket/ops.rs +++ b/mirrord/layer/src/socket/ops.rs @@ -12,7 +12,8 @@ use std::{ sync::{Arc, OnceLock}, }; -use libc::{c_int, c_void, sockaddr, socklen_t}; +use errno::set_errno; +use libc::{c_int, c_void, hostent, sockaddr, socklen_t}; use mirrord_config::feature::network::incoming::IncomingMode; use mirrord_intproxy_protocol::{ ConnMetadataRequest, ConnMetadataResponse, NetProtocol, OutgoingConnectRequest, @@ -43,6 +44,31 @@ pub(super) static REMOTE_DNS_REVERSE_MAPPING: LazyLock> /// Hostname initialized from the agent with [`gethostname`]. pub(crate) static HOSTNAME: OnceLock = OnceLock::new(); +/// Globals used by `gethostbyname`. +static mut GETHOSTBYNAME_HOSTNAME: Option = None; +static mut GETHOSTBYNAME_ALIASES_STR: Option> = None; + +/// **Safety**: +/// There is a potential UB trigger here, as [`libc::hostent`] uses this as an `*mut _`, while we +/// have `*const _`. As this is being filled to fulfill the contract of a deprecated function, I +/// (alex) don't think we're going to hit this issue ever. +static mut GETHOSTBYNAME_ALIASES_PTR: Option> = None; +static mut GETHOSTBYNAME_ADDRESSES_VAL: Option> = None; +static mut GETHOSTBYNAME_ADDRESSES_PTR: Option> = None; + +/// Global static that the user will receive when calling [`gethostbyname`]. +/// +/// **Safety**: +/// Even though we fill it with some `*const _` while it expects `*mut _`, it shouldn't be a problem +/// as the user will most likely be doing a deep copy if they want to mess around with this struct. +static mut GETHOSTBYNAME_HOSTENT: hostent = hostent { + h_name: ptr::null_mut(), + h_aliases: ptr::null_mut(), + h_addrtype: 0, + h_length: 0, + h_addr_list: ptr::null_mut(), +}; + /// Helper struct for connect results where we want to hold the original errno /// when result is -1 (error) because sometimes it's not a real error (EINPROGRESS/EINTR) /// and the caller should have the original value. @@ -466,7 +492,7 @@ pub(super) fn connect( let unix_streams = crate::setup().remote_unix_streams(); - info!("in connect {:#?}", SOCKETS); + trace!("in connect {:#?}", SOCKETS); let (_, user_socket_info) = { SOCKETS @@ -869,6 +895,88 @@ fn remote_hostname_string() -> Detour { .map(Detour::Success)? } +/// Resolves a hostname and set result to static global like the original `gethostbyname` does. +/// +/// Used by erlang/elixir to resolve DNS. +/// +/// **Safety**: +/// See the [`GETHOSTBYNAME_ALIASES_PTR`] docs. If you see this function being called and some weird +/// issue is going on, assume that you might've triggered the UB. +#[tracing::instrument(level = "trace", ret)] +pub(super) fn gethostbyname(raw_name: Option<&CStr>) -> Detour<*mut hostent> { + let name: String = raw_name + .bypass(Bypass::NullNode)? + .to_str() + .map_err(|fail| { + warn!("Failed converting `name` from `CStr` with {:#?}", fail); + + Bypass::CStrConversion + })? + .into(); + + let hosts_and_ips = remote_getaddrinfo(name.clone())?; + + // We could `unwrap` here, as this would have failed on the previous conversion. + let host_name = CString::new(name)?; + + if hosts_and_ips.is_empty() { + set_errno(errno::Errno(libc::EAI_NODATA)); + return Detour::Success(ptr::null_mut()); + } + + // We need `*mut _` at the end, so `ips` has to be `mut`. + let (aliases, mut ips) = hosts_and_ips + .into_iter() + .filter_map(|(host, ip)| match ip { + // Only care about ipv4s and hosts that exist. + IpAddr::V4(ip) => { + let c_host = CString::new(host).ok()?; + Some((c_host, ip.octets())) + } + IpAddr::V6(ip) => { + trace!("ipv6 received - ignoring - {ip:?}"); + None + } + }) + .fold( + (Vec::default(), Vec::default()), + |(mut aliases, mut ips), (host, octets)| { + aliases.push(host); + ips.push(octets); + (aliases, ips) + }, + ); + + let mut aliases_ptrs: Vec<*const i8> = aliases + .iter() + .map(|alias| alias.as_ptr().cast()) + .collect::>(); + let mut ips_ptrs = ips.iter_mut().map(|ip| ip.as_mut_ptr()).collect::>(); + + // Put a null ptr to signal end of the list. + aliases_ptrs.push(ptr::null()); + ips_ptrs.push(ptr::null_mut()); + + // Need long-lived values so we can take pointers to them. + unsafe { + GETHOSTBYNAME_HOSTNAME.replace(host_name); + GETHOSTBYNAME_ALIASES_STR.replace(aliases); + GETHOSTBYNAME_ALIASES_PTR.replace(aliases_ptrs); + GETHOSTBYNAME_ADDRESSES_VAL.replace(ips); + GETHOSTBYNAME_ADDRESSES_PTR.replace(ips_ptrs); + + // Fill the `*mut hostent` that the user will interact with. + GETHOSTBYNAME_HOSTENT.h_name = GETHOSTBYNAME_HOSTNAME.as_ref().unwrap().as_ptr() as _; + GETHOSTBYNAME_HOSTENT.h_length = 4; + GETHOSTBYNAME_HOSTENT.h_addrtype = libc::AF_INET; + GETHOSTBYNAME_HOSTENT.h_aliases = GETHOSTBYNAME_ALIASES_PTR.as_ref().unwrap().as_ptr() as _; + GETHOSTBYNAME_HOSTENT.h_addr_list = + GETHOSTBYNAME_ADDRESSES_PTR.as_ref().unwrap().as_ptr() as *mut *mut libc::c_char; + } + + Detour::Success(unsafe { std::ptr::addr_of!(GETHOSTBYNAME_HOSTENT) as _ }) +} + /// Resolve hostname from remote host with caching for the result #[tracing::instrument(level = "trace")] pub(super) fn gethostname() -> Detour<&'static CString> { diff --git a/mirrord/layer/tests/apps/gethostbyname/gethostbyname.c b/mirrord/layer/tests/apps/gethostbyname/gethostbyname.c new file mode 100644 index 00000000000..f0274b9c8ef --- /dev/null +++ b/mirrord/layer/tests/apps/gethostbyname/gethostbyname.c @@ -0,0 +1,34 @@ +#include +#include +#include +#include + +void try_gethostbyname(const char name[]) { + struct hostent *result = gethostbyname(name); + + if (result) { + printf("result %p\n\t", result); + printf("h_name %s\n\t", result->h_name); + printf("h_length %i\n\t", result->h_length); + printf("h_addrtype %i\n\t", result->h_addrtype); + + for (int i = 0; result->h_addr_list[i]; i++) { + char str[INET6_ADDRSTRLEN]; + struct in_addr address = {}; + bcopy(result->h_addr_list[i], (char *)&address, sizeof(address)); + printf("h_addresses[%i] %s\n\t", i, inet_ntoa(address)); + } + + for (int i = 0; result->h_aliases[i]; i++) { + printf("h_aliases[%i] %s\n\t", i, result->h_aliases[i]); + } + } +} + +int main(int argc, char *argv[]) { + printf("test issue 2055: START\n"); + try_gethostbyname("www.mirrord.dev"); + try_gethostbyname("www.invalid.dev"); + printf("test issue 2055: SUCCESS\n"); + printf("\n"); +} diff --git a/mirrord/layer/tests/common/mod.rs b/mirrord/layer/tests/common/mod.rs index efcb732a7ed..5ec60849da5 100644 --- a/mirrord/layer/tests/common/mod.rs +++ b/mirrord/layer/tests/common/mod.rs @@ -651,6 +651,7 @@ pub enum Application { RustListenPorts, Fork, OpenFile, + CIssue2055, RustIssue2058, // For running applications with the executable and arguments determined at runtime. DynamicApp(String, Vec), @@ -787,6 +788,11 @@ impl Application { env!("CARGO_MANIFEST_DIR"), "tests/apps/open_file/out.c_test_app", ), + Application::CIssue2055 => format!( + "{}/{}", + env!("CARGO_MANIFEST_DIR"), + "tests/apps/gethostbyname/out.c_test_app", + ), Application::RustIssue2058 => String::from("tests/apps/issue2058/target/issue2058"), Application::DynamicApp(exe, _) => exe.clone(), } @@ -877,7 +883,8 @@ impl Application { | Application::Go19DirBypass | Application::Go20DirBypass | Application::RustIssue2058 - | Application::OpenFile => vec![], + | Application::OpenFile + | Application::CIssue2055 => vec![], Application::RustOutgoingUdp => ["--udp", RUST_OUTGOING_LOCAL, RUST_OUTGOING_PEERS] .into_iter() .map(Into::into) @@ -943,6 +950,7 @@ impl Application { | Application::RustListenPorts | Application::RustRecvFrom | Application::OpenFile + | Application::CIssue2055 | Application::DynamicApp(..) => unimplemented!("shouldn't get here"), Application::PythonSelfConnect => 1337, Application::RustIssue2058 => 1234, diff --git a/mirrord/layer/tests/issue2055.rs b/mirrord/layer/tests/issue2055.rs new file mode 100644 index 00000000000..79b543c978a --- /dev/null +++ b/mirrord/layer/tests/issue2055.rs @@ -0,0 +1,62 @@ +#![feature(assert_matches)] +use std::{net::IpAddr, path::PathBuf, time::Duration}; + +use mirrord_protocol::{ + dns::{DnsLookup, GetAddrInfoRequest, GetAddrInfoResponse, LookupRecord}, + ClientMessage, DaemonMessage, DnsLookupError, + ResolveErrorKindInternal::NoRecordsFound, + ResponseError, +}; +use rstest::rstest; + +mod common; +pub use common::*; + +/// Verify that issue [#2055](https://github.com/metalbear-co/mirrord/issues/2055) is fixed. +/// "DNS Issue on Elixir macOS" +#[rstest] +#[tokio::test] +#[timeout(Duration::from_secs(60))] +async fn issue_2055(dylib_path: &PathBuf) { + let application = Application::CIssue2055; + let (mut test_process, mut intproxy) = application + .start_process_with_layer(dylib_path, Default::default(), None) + .await; + + println!("Application started, waiting for `GetAddrInfoRequest`."); + + let msg = intproxy.recv().await; + let ClientMessage::GetAddrInfoRequest(GetAddrInfoRequest { node }) = msg else { + panic!("Invalid message received from layer: {msg:?}"); + }; + + intproxy + .send(DaemonMessage::GetAddrInfoResponse(GetAddrInfoResponse(Ok( + DnsLookup(vec![LookupRecord { + name: node, + ip: "93.184.216.34".parse::().unwrap(), + }]), + )))) + .await; + + let msg = intproxy.recv().await; + let ClientMessage::GetAddrInfoRequest(GetAddrInfoRequest { node: _ }) = msg else { + panic!("Invalid message received from layer: {msg:?}"); + }; + + intproxy + .send(DaemonMessage::GetAddrInfoResponse(GetAddrInfoResponse( + Err(ResponseError::DnsLookup(DnsLookupError { + kind: NoRecordsFound(3), + })), + ))) + .await; + + test_process.wait_assert_success().await; + test_process + .assert_stdout_contains("test issue 2055: START") + .await; + test_process + .assert_stdout_contains("test issue 2055: SUCCESS") + .await; +}