Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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],
Expand Down
141 changes: 141 additions & 0 deletions src/ipv4.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
#include <stdint.h>
#include <string.h>

#if HAVE_SYSTEMCONFIGURATION
#include <SystemConfiguration/SystemConfiguration.h>
#include <CoreFoundation/CoreFoundation.h>
#endif

#define IPV4_GET_ROUTE_BUFFER_CHUNK_SIZE 65536
#define SHOW_ROUTE_BUFFER_SIZE 128

Expand Down Expand Up @@ -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
7 changes: 7 additions & 0 deletions src/ipv4.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions src/tunnel.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down