diff --git a/configure.ac b/configure.ac index 0ef0b6f0..4d08d608 100644 --- a/configure.ac +++ b/configure.ac @@ -7,6 +7,7 @@ AC_CONFIG_SRCDIR([src/main.c]) AM_INIT_AUTOMAKE([foreign subdir-objects]) # Checks for programs. +AC_CANONICAL_HOST AC_PROG_CC AC_PROG_MKDIR_P AC_PROG_SED @@ -180,6 +181,22 @@ LIBS="${OPENSSL_LIBS} ${LIBS}" CPPFLAGS="${save_openssl_CPPFLAGS}" LIBS="${save_openssl_LIBS}" +# Link macOS DNS frameworks and expose a feature macro for SCDynamicStore support. +AC_MSG_CHECKING([for macOS SystemConfiguration framework]) +case "$host_os" in + darwin*) + AC_DEFINE([HAVE_SYSTEMCONFIGURATION], [1], + [Define to 1 on macOS for SCDynamicStore DNS support]) + LIBS="$LIBS -framework SystemConfiguration -framework CoreFoundation" + AC_MSG_RESULT([yes]) + ;; + *) + AC_DEFINE([HAVE_SYSTEMCONFIGURATION], [0], + [Define to 0 when macOS SystemConfiguration is unavailable]) + AC_MSG_RESULT([no]) + ;; +esac + # Specialised checks for particular features. # When cross compile, don't run the tests. AC_ARG_WITH([rt_dst], diff --git a/src/ipv4.c b/src/ipv4.c index e3f16851..0f86b268 100644 --- a/src/ipv4.c +++ b/src/ipv4.c @@ -37,6 +37,11 @@ #include #include +#if HAVE_SYSTEMCONFIGURATION +#include +#include +#endif + #define IPV4_GET_ROUTE_BUFFER_CHUNK_SIZE 65536 #define SHOW_ROUTE_BUFFER_SIZE 128 @@ -1436,3 +1441,139 @@ int ipv4_del_nameservers_from_resolv_conf(struct tunnel *tunnel) strerror(errno)); return ret; } + +#if HAVE_SYSTEMCONFIGURATION +/* + * macOS DNS via SCDynamicStore + * + * macOS ignores /etc/resolv.conf — DNS is managed by configd through + * SCDynamicStore. We register a supplemental resolver under our own key. + * + * When dns_suffix is available (e.g. "corp.com"): + * Split-DNS — only *.corp.com queries go to the VPN nameservers. + * configd automatically adds "corp.com" to the default resolver's + * search domain list, so "ping host" expands to "host.corp.com". + * Non-matching queries (google.com) keep using normal DNS. + * + * When dns_suffix is NOT available but DNS servers are provided: + * Full-tunnel DNS — SupplementalMatchDomains is set to "" (empty + * string = match all domains) so every query goes to the VPN. + * Note: configd does not add search domains in this mode, so short + * hostname expansion will not work. This is a macOS limitation. + * + * Cleanup is a single key removal. If the process crashes, only the + * supplemental entry is orphaned — normal DNS continues to work since + * the primary service is never modified. + */ + +#define OPENFORTIVPN_DNS_KEY \ + CFSTR("State:/Network/Service/openfortivpn/DNS") + +int ipv4_set_dns_scf(struct tunnel *tunnel) +{ + SCDynamicStoreRef store = NULL; + CFMutableDictionaryRef dns_dict = NULL; + CFMutableArrayRef servers = NULL; + int ret = 1; + + store = SCDynamicStoreCreate(NULL, CFSTR("openfortivpn"), NULL, NULL); + if (!store) { + log_warn("SCDynamicStore could not be opened.\n"); + return 1; + } + + dns_dict = CFDictionaryCreateMutable(NULL, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + servers = CFArrayCreateMutable(NULL, 2, &kCFTypeArrayCallBacks); + if (!dns_dict || !servers) + goto out; + + if (tunnel->ipv4.ns1_addr.s_addr) { + CFStringRef s = CFStringCreateWithCString(NULL, + inet_ntoa(tunnel->ipv4.ns1_addr), + kCFStringEncodingASCII); + if (s) { + CFArrayAppendValue(servers, s); + CFRelease(s); + } + } + if (tunnel->ipv4.ns2_addr.s_addr) { + CFStringRef s = CFStringCreateWithCString(NULL, + inet_ntoa(tunnel->ipv4.ns2_addr), + kCFStringEncodingASCII); + if (s) { + CFArrayAppendValue(servers, s); + CFRelease(s); + } + } + if (CFArrayGetCount(servers) == 0) { + log_debug("No DNS servers to configure.\n"); + ret = 0; + goto out; + } + CFDictionarySetValue(dns_dict, CFSTR("ServerAddresses"), servers); + + /* + * Split-DNS vs Full-tunnel: + * suffix present → SupplementalMatchDomains: ["corp.com"] + * suffix absent → SupplementalMatchDomains: [""] + */ + CFStringRef match_domain; + if (tunnel->ipv4.dns_suffix) + match_domain = CFStringCreateWithCString(NULL, + tunnel->ipv4.dns_suffix, kCFStringEncodingUTF8); + else + match_domain = CFStringCreateCopy(NULL, CFSTR("")); + + if (match_domain) { + CFArrayRef match_domains = CFArrayCreate(NULL, + (const void **)&match_domain, 1, + &kCFTypeArrayCallBacks); + if (match_domains) { + CFDictionarySetValue(dns_dict, + CFSTR("SupplementalMatchDomains"), + match_domains); + CFRelease(match_domains); + } + CFRelease(match_domain); + } + + if (SCDynamicStoreSetValue(store, OPENFORTIVPN_DNS_KEY, dns_dict)) { + if (tunnel->ipv4.dns_suffix) + log_info("Split-DNS for '%s' written to macOS System " + "Configuration.\n", + tunnel->ipv4.dns_suffix); + else + log_info("Full-tunnel DNS written to macOS System " + "Configuration.\n"); + ret = 0; + } else { + log_warn("Could not write DNS to SCDynamicStore.\n"); + } + +out: + if (servers) + CFRelease(servers); + if (dns_dict) + CFRelease(dns_dict); + if (store) + CFRelease(store); + return ret; +} + +int ipv4_clear_dns_scf(void) +{ + SCDynamicStoreRef store; + + store = SCDynamicStoreCreate(NULL, CFSTR("openfortivpn"), NULL, NULL); + if (!store) { + log_warn("SCDynamicStore could not be opened.\n"); + return 1; + } + + SCDynamicStoreRemoveValue(store, OPENFORTIVPN_DNS_KEY); + CFRelease(store); + return 0; +} +#endif diff --git a/src/ipv4.h b/src/ipv4.h index d8337b01..a1169ba6 100644 --- a/src/ipv4.h +++ b/src/ipv4.h @@ -94,4 +94,11 @@ int ipv4_restore_routes(struct tunnel *tunnel); int ipv4_add_nameservers_to_resolv_conf(struct tunnel *tunnel); int ipv4_del_nameservers_from_resolv_conf(struct tunnel *tunnel); +#if HAVE_SYSTEMCONFIGURATION + +int ipv4_set_dns_scf(struct tunnel *tunnel); +int ipv4_clear_dns_scf(void); + +#endif + #endif diff --git a/src/tunnel.c b/src/tunnel.c index 48ec1960..2681fa5e 100644 --- a/src/tunnel.c +++ b/src/tunnel.c @@ -135,6 +135,9 @@ static int on_ppp_if_up(struct tunnel *tunnel) if (tunnel->config->set_dns) { log_info("Adding VPN nameservers...\n"); ipv4_add_nameservers_to_resolv_conf(tunnel); + #if HAVE_SYSTEMCONFIGURATION + ipv4_set_dns_scf(tunnel); + #endif } log_info("Tunnel is up and running.\n"); @@ -162,6 +165,9 @@ static int on_ppp_if_down(struct tunnel *tunnel) if (tunnel->config->set_dns) { log_info("Removing VPN nameservers...\n"); ipv4_del_nameservers_from_resolv_conf(tunnel); + #if HAVE_SYSTEMCONFIGURATION + ipv4_clear_dns_scf(); + #endif } return 0;