/* * OpenVPN -- An application to securely tunnel IP networks * over a single TCP/UDP port, with support for SSL/TLS-based * session authentication and key exchange, * packet encryption, packet authentication, and * packet compression. * * Copyright (C) 2012-2025 Heiko Hund * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see . */ #include "service.h" #include #include #include #include #include #include #include #include #include #include #include #include "openvpn-msg.h" #include "validate.h" #include "wfp_block.h" #define IO_TIMEOUT 2000 /*ms*/ #define ERROR_OPENVPN_STARTUP 0x20000000 #define ERROR_STARTUP_DATA 0x20000001 #define ERROR_MESSAGE_DATA 0x20000002 #define ERROR_MESSAGE_TYPE 0x20000003 static SERVICE_STATUS_HANDLE service; static SERVICE_STATUS status = { .dwServiceType = SERVICE_WIN32_SHARE_PROCESS }; static HANDLE exit_event = NULL; static settings_t settings; static HANDLE rdns_semaphore = NULL; #define RDNS_TIMEOUT 600 /* seconds to wait for the semaphore */ #define TUN_IOCTL_REGISTER_RINGS \ CTL_CODE(51820U, 0x970U, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA) openvpn_service_t interactive_service = { interactive, _L(PACKAGE_NAME) L"ServiceInteractive", _L(PACKAGE_NAME) L" Interactive Service", SERVICE_DEPENDENCIES, SERVICE_AUTO_START }; typedef struct { WCHAR *directory; WCHAR *options; WCHAR *std_input; } STARTUP_DATA; /* Datatype for linked lists */ typedef struct _list_item { struct _list_item *next; LPVOID data; } list_item_t; /* Datatypes for undo information */ typedef enum { address, route, wfp_block, undo_dns4, undo_dns6, undo_nrpt, undo_domains, undo_wins, _undo_type_max } undo_type_t; typedef list_item_t *undo_lists_t[_undo_type_max]; typedef struct { HANDLE engine; int index; int metric_v4; int metric_v6; } wfp_block_data_t; typedef struct { char itf_name[256]; PWSTR domains; } dns_domains_undo_data_t; typedef union { message_header_t header; address_message_t address; route_message_t route; flush_neighbors_message_t flush_neighbors; wfp_block_message_t wfp_block; dns_cfg_message_t dns; nrpt_dns_cfg_message_t nrpt_dns; enable_dhcp_message_t dhcp; set_mtu_message_t mtu; wins_cfg_message_t wins; create_adapter_message_t create_adapter; } pipe_message_t; typedef struct { CHAR addresses[NRPT_ADDR_NUM * NRPT_ADDR_SIZE]; WCHAR domains[512]; /* MULTI_SZ string */ DWORD domains_size; /* bytes in domains */ } nrpt_exclude_data_t; static DWORD AddListItem(list_item_t **pfirst, LPVOID data) { list_item_t *new_item = malloc(sizeof(list_item_t)); if (new_item == NULL) { return ERROR_OUTOFMEMORY; } new_item->next = *pfirst; new_item->data = data; *pfirst = new_item; return NO_ERROR; } typedef BOOL (*match_fn_t)(LPVOID item, LPVOID ctx); static LPVOID RemoveListItem(list_item_t **pfirst, match_fn_t match, LPVOID ctx) { LPVOID data = NULL; list_item_t **pnext; for (pnext = pfirst; *pnext; pnext = &(*pnext)->next) { list_item_t *item = *pnext; if (!match(item->data, ctx)) { continue; } /* Found item, remove from the list and free memory */ *pnext = item->next; data = item->data; free(item); break; } return data; } static HANDLE CloseHandleEx(LPHANDLE handle) { if (handle && *handle && *handle != INVALID_HANDLE_VALUE) { CloseHandle(*handle); *handle = INVALID_HANDLE_VALUE; } return INVALID_HANDLE_VALUE; } static HANDLE InitOverlapped(LPOVERLAPPED overlapped) { ZeroMemory(overlapped, sizeof(OVERLAPPED)); overlapped->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); return overlapped->hEvent; } static BOOL ResetOverlapped(LPOVERLAPPED overlapped) { HANDLE io_event = overlapped->hEvent; if (!ResetEvent(io_event)) { return FALSE; } ZeroMemory(overlapped, sizeof(OVERLAPPED)); overlapped->hEvent = io_event; return TRUE; } typedef enum { peek, read, write } async_op_t; static DWORD AsyncPipeOp(async_op_t op, HANDLE pipe, LPVOID buffer, DWORD size, DWORD count, LPHANDLE events) { DWORD i; BOOL success; HANDLE io_event; DWORD res, bytes = 0; OVERLAPPED overlapped; LPHANDLE handles = NULL; io_event = InitOverlapped(&overlapped); if (!io_event) { goto out; } handles = malloc((count + 1) * sizeof(HANDLE)); if (!handles) { goto out; } if (op == write) { success = WriteFile(pipe, buffer, size, NULL, &overlapped); } else { success = ReadFile(pipe, buffer, size, NULL, &overlapped); } if (!success && GetLastError() != ERROR_IO_PENDING && GetLastError() != ERROR_MORE_DATA) { goto out; } handles[0] = io_event; for (i = 0; i < count; i++) { handles[i + 1] = events[i]; } res = WaitForMultipleObjects(count + 1, handles, FALSE, op == peek ? INFINITE : IO_TIMEOUT); if (res != WAIT_OBJECT_0) { CancelIo(pipe); goto out; } if (op == peek) { PeekNamedPipe(pipe, NULL, 0, NULL, &bytes, NULL); } else { GetOverlappedResult(pipe, &overlapped, &bytes, TRUE); } out: CloseHandleEx(&io_event); free(handles); return bytes; } static DWORD PeekNamedPipeAsync(HANDLE pipe, DWORD count, LPHANDLE events) { return AsyncPipeOp(peek, pipe, NULL, 0, count, events); } static DWORD ReadPipeAsync(HANDLE pipe, LPVOID buffer, DWORD size, DWORD count, LPHANDLE events) { return AsyncPipeOp(read, pipe, buffer, size, count, events); } static DWORD WritePipeAsync(HANDLE pipe, LPVOID data, DWORD size, DWORD count, LPHANDLE events) { return AsyncPipeOp(write, pipe, data, size, count, events); } static VOID ReturnProcessId(HANDLE pipe, DWORD pid, DWORD count, LPHANDLE events) { const WCHAR msg[] = L"Process ID"; WCHAR buf[22 + _countof(msg)]; /* 10 chars each for error and PID and 2 for line breaks */ /* * Same format as error messages (3 line string) with error = 0 in * 0x%08x format, PID on line 2 and a description "Process ID" on line 3 */ swprintf(buf, _countof(buf), L"0x%08x\n0x%08x\n%ls", 0, pid, msg); WritePipeAsync(pipe, buf, (DWORD)(wcslen(buf) * 2), count, events); } static VOID ReturnError(HANDLE pipe, DWORD error, LPCWSTR func, DWORD count, LPHANDLE events) { DWORD result_len; LPWSTR result = L"0xffffffff\nFormatMessage failed\nCould not return result"; DWORD_PTR args[] = { (DWORD_PTR)error, (DWORD_PTR)func, (DWORD_PTR) "" }; if (error != ERROR_OPENVPN_STARTUP) { FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS, 0, error, 0, (LPWSTR)&args[2], 0, NULL); } result_len = FormatMessageW( FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_ARGUMENT_ARRAY, L"0x%1!08x!\n%2!s!\n%3!s!", 0, 0, (LPWSTR)&result, 0, (va_list *)args); WritePipeAsync(pipe, result, (DWORD)(wcslen(result) * 2), count, events); MsgToEventLog(MSG_FLAGS_ERROR, result); if (error != ERROR_OPENVPN_STARTUP) { LocalFree((LPVOID)args[2]); } if (result_len) { LocalFree(result); } } static VOID ReturnLastError(HANDLE pipe, LPCWSTR func) { ReturnError(pipe, GetLastError(), func, 1, &exit_event); } /* * Validate options against a white list. Also check the config_file is * inside the config_dir. The white list is defined in validate.c * Returns true on success, false on error with reason set in errmsg. */ static BOOL ValidateOptions(HANDLE pipe, const WCHAR *workdir, const WCHAR *options, WCHAR *errmsg, DWORD capacity) { WCHAR **argv; int argc; BOOL ret = FALSE; int i; const WCHAR *msg1 = L"You have specified a config file location (%ls relative to %ls)" L" that requires admin approval. This error may be avoided" L" by adding your account to the \"%ls\" group"; const WCHAR *msg2 = L"You have specified an option (%ls) that may be used" L" only with admin approval. This error may be avoided" L" by adding your account to the \"%ls\" group"; argv = CommandLineToArgvW(options, &argc); if (!argv) { swprintf(errmsg, capacity, L"Cannot validate options: CommandLineToArgvW failed with error = 0x%08x", GetLastError()); goto out; } /* Note: argv[0] is the first option */ if (argc < 1) /* no options */ { ret = TRUE; goto out; } /* * If only one argument, it is the config file */ if (argc == 1) { WCHAR *argv_tmp[2] = { L"--config", argv[0] }; if (!CheckOption(workdir, 2, argv_tmp, &settings)) { swprintf(errmsg, capacity, msg1, argv[0], workdir, settings.ovpn_admin_group); } goto out; } for (i = 0; i < argc; ++i) { if (!IsOption(argv[i])) { continue; } if (!CheckOption(workdir, argc - i, &argv[i], &settings)) { if (wcscmp(L"--config", argv[i]) == 0 && argc - i > 1) { swprintf(errmsg, capacity, msg1, argv[i + 1], workdir, settings.ovpn_admin_group); } else { swprintf(errmsg, capacity, msg2, argv[i], settings.ovpn_admin_group); } goto out; } } /* all options passed */ ret = TRUE; out: if (argv) { LocalFree(argv); } return ret; } static BOOL GetStartupData(HANDLE pipe, STARTUP_DATA *sud) { size_t size, len; WCHAR *data = NULL; DWORD bytes, read; bytes = PeekNamedPipeAsync(pipe, 1, &exit_event); if (bytes == 0) { MsgToEventLog(M_SYSERR, L"PeekNamedPipeAsync failed"); ReturnLastError(pipe, L"PeekNamedPipeAsync"); goto err; } size = bytes / sizeof(*data); if (size == 0) { MsgToEventLog(M_SYSERR, L"malformed startup data: 1 byte received"); ReturnError(pipe, ERROR_STARTUP_DATA, L"GetStartupData", 1, &exit_event); goto err; } data = malloc(bytes); if (data == NULL) { MsgToEventLog(M_SYSERR, L"malloc failed"); ReturnLastError(pipe, L"malloc"); goto err; } read = ReadPipeAsync(pipe, data, bytes, 1, &exit_event); if (bytes != read) { MsgToEventLog(M_SYSERR, L"ReadPipeAsync failed"); ReturnLastError(pipe, L"ReadPipeAsync"); goto err; } if (data[size - 1] != 0) { MsgToEventLog(M_ERR, L"Startup data is not NULL terminated"); ReturnError(pipe, ERROR_STARTUP_DATA, L"GetStartupData", 1, &exit_event); goto err; } sud->directory = data; len = wcslen(sud->directory) + 1; size -= len; if (size <= 0) { MsgToEventLog(M_ERR, L"Startup data ends at working directory"); ReturnError(pipe, ERROR_STARTUP_DATA, L"GetStartupData", 1, &exit_event); goto err; } sud->options = sud->directory + len; len = wcslen(sud->options) + 1; size -= len; if (size <= 0) { MsgToEventLog(M_ERR, L"Startup data ends at command line options"); ReturnError(pipe, ERROR_STARTUP_DATA, L"GetStartupData", 1, &exit_event); goto err; } sud->std_input = sud->options + len; return TRUE; err: sud->directory = NULL; /* caller must not free() */ free(data); return FALSE; } static VOID FreeStartupData(STARTUP_DATA *sud) { free(sud->directory); } static SOCKADDR_INET sockaddr_inet(short family, inet_address_t *addr) { SOCKADDR_INET sa_inet; ZeroMemory(&sa_inet, sizeof(sa_inet)); sa_inet.si_family = family; if (family == AF_INET) { sa_inet.Ipv4.sin_addr = addr->ipv4; } else if (family == AF_INET6) { sa_inet.Ipv6.sin6_addr = addr->ipv6; } return sa_inet; } static DWORD InterfaceLuid(const char *iface_name, PNET_LUID luid) { NETIO_STATUS status; LPWSTR wide_name = utf8to16(iface_name); if (wide_name) { status = ConvertInterfaceAliasToLuid(wide_name, luid); free(wide_name); } else { status = ERROR_OUTOFMEMORY; } return status; } static BOOL CmpAddress(LPVOID item, LPVOID address) { return memcmp(item, address, sizeof(MIB_UNICASTIPADDRESS_ROW)) == 0 ? TRUE : FALSE; } static DWORD DeleteAddress(PMIB_UNICASTIPADDRESS_ROW addr_row) { return DeleteUnicastIpAddressEntry(addr_row); } static DWORD HandleAddressMessage(address_message_t *msg, undo_lists_t *lists) { DWORD err; PMIB_UNICASTIPADDRESS_ROW addr_row; BOOL add = msg->header.type == msg_add_address; addr_row = malloc(sizeof(*addr_row)); if (addr_row == NULL) { return ERROR_OUTOFMEMORY; } InitializeUnicastIpAddressEntry(addr_row); addr_row->Address = sockaddr_inet(msg->family, &msg->address); addr_row->OnLinkPrefixLength = (UINT8)msg->prefix_len; if (msg->iface.index != -1) { addr_row->InterfaceIndex = msg->iface.index; } else { NET_LUID luid; err = InterfaceLuid(msg->iface.name, &luid); if (err) { goto out; } addr_row->InterfaceLuid = luid; } if (add) { err = CreateUnicastIpAddressEntry(addr_row); if (err) { goto out; } err = AddListItem(&(*lists)[address], addr_row); if (err) { DeleteAddress(addr_row); } } else { err = DeleteAddress(addr_row); if (err) { goto out; } free(RemoveListItem(&(*lists)[address], CmpAddress, addr_row)); } out: if (!add || err) { free(addr_row); } return err; } static BOOL CmpRoute(LPVOID item, LPVOID route) { return memcmp(item, route, sizeof(MIB_IPFORWARD_ROW2)) == 0 ? TRUE : FALSE; } static DWORD DeleteRoute(PMIB_IPFORWARD_ROW2 fwd_row) { return DeleteIpForwardEntry2(fwd_row); } static DWORD HandleRouteMessage(route_message_t *msg, undo_lists_t *lists) { DWORD err; PMIB_IPFORWARD_ROW2 fwd_row; BOOL add = msg->header.type == msg_add_route; fwd_row = malloc(sizeof(*fwd_row)); if (fwd_row == NULL) { return ERROR_OUTOFMEMORY; } ZeroMemory(fwd_row, sizeof(*fwd_row)); fwd_row->ValidLifetime = 0xffffffff; fwd_row->PreferredLifetime = 0xffffffff; fwd_row->Protocol = MIB_IPPROTO_NETMGMT; fwd_row->Metric = msg->metric; fwd_row->DestinationPrefix.Prefix = sockaddr_inet(msg->family, &msg->prefix); fwd_row->DestinationPrefix.PrefixLength = (UINT8)msg->prefix_len; fwd_row->NextHop = sockaddr_inet(msg->family, &msg->gateway); if (msg->iface.index != -1) { fwd_row->InterfaceIndex = msg->iface.index; } else if (strlen(msg->iface.name)) { NET_LUID luid; err = InterfaceLuid(msg->iface.name, &luid); if (err) { goto out; } fwd_row->InterfaceLuid = luid; } if (add) { err = CreateIpForwardEntry2(fwd_row); if (err) { goto out; } err = AddListItem(&(*lists)[route], fwd_row); if (err) { DeleteRoute(fwd_row); } } else { err = DeleteRoute(fwd_row); if (err) { goto out; } free(RemoveListItem(&(*lists)[route], CmpRoute, fwd_row)); } out: if (!add || err) { free(fwd_row); } return err; } static DWORD HandleFlushNeighborsMessage(flush_neighbors_message_t *msg) { if (msg->family == AF_INET) { return FlushIpNetTable(msg->iface.index); } return FlushIpNetTable2(msg->family, msg->iface.index); } static void BlockDNSErrHandler(DWORD err, const char *msg) { WCHAR buf[256]; LPCWSTR err_str; if (!err) { return; } err_str = L"Unknown Win32 Error"; if (FormatMessage(FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY, NULL, err, 0, buf, sizeof(buf), NULL)) { err_str = buf; } MsgToEventLog(M_ERR, L"%hs (status = %lu): %ls", msg, err, err_str); } /* Use an always-true match_fn to get the head of the list */ static BOOL CmpAny(LPVOID item, LPVOID any) { return TRUE; } static DWORD DeleteWfpBlock(const wfp_block_message_t *msg, undo_lists_t *lists) { DWORD err = 0; wfp_block_data_t *block_data = RemoveListItem(&(*lists)[wfp_block], CmpAny, NULL); if (block_data) { err = delete_wfp_block_filters(block_data->engine); if (block_data->metric_v4 >= 0) { set_interface_metric(msg->iface.index, AF_INET, block_data->metric_v4); } if (block_data->metric_v6 >= 0) { set_interface_metric(msg->iface.index, AF_INET6, block_data->metric_v6); } free(block_data); } else { MsgToEventLog(M_ERR, L"No previous block filters to delete"); } return err; } static DWORD AddWfpBlock(const wfp_block_message_t *msg, undo_lists_t *lists) { DWORD err = 0; wfp_block_data_t *block_data = NULL; HANDLE engine = NULL; LPCWSTR exe_path; BOOL dns_only; exe_path = settings.exe_path; dns_only = (msg->flags == wfp_block_dns); err = add_wfp_block_filters(&engine, msg->iface.index, exe_path, BlockDNSErrHandler, dns_only); if (!err) { block_data = malloc(sizeof(wfp_block_data_t)); if (!block_data) { err = ERROR_OUTOFMEMORY; goto out; } block_data->engine = engine; block_data->index = msg->iface.index; int is_auto = 0; block_data->metric_v4 = get_interface_metric(msg->iface.index, AF_INET, &is_auto); if (is_auto) { block_data->metric_v4 = 0; } block_data->metric_v6 = get_interface_metric(msg->iface.index, AF_INET6, &is_auto); if (is_auto) { block_data->metric_v6 = 0; } err = AddListItem(&(*lists)[wfp_block], block_data); if (!err) { err = set_interface_metric(msg->iface.index, AF_INET, WFP_BLOCK_IFACE_METRIC); if (!err) { /* for IPv6, we intentionally ignore errors, because * otherwise block-dns activation will fail if a user or * admin has disabled IPv6 on the tun/tap/dco interface * (if OpenVPN wants IPv6 ifconfig, we'll fail there) */ set_interface_metric(msg->iface.index, AF_INET6, WFP_BLOCK_IFACE_METRIC); } if (err) { /* delete the filters, remove undo item and free interface data */ DeleteWfpBlock(msg, lists); engine = NULL; } } } out: if (err && engine) { delete_wfp_block_filters(engine); free(block_data); } return err; } static DWORD HandleWfpBlockMessage(const wfp_block_message_t *msg, undo_lists_t *lists) { if (msg->header.type == msg_add_wfp_block) { return AddWfpBlock(msg, lists); } else { return DeleteWfpBlock(msg, lists); } } /* * Execute a command and return its exit code. If timeout > 0, terminate * the process if still running after timeout milliseconds. In that case * the return value is the windows error code WAIT_TIMEOUT = 0x102 */ static DWORD ExecCommand(const WCHAR *argv0, const WCHAR *cmdline, DWORD timeout) { DWORD exit_code; STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD proc_flags = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT; WCHAR *cmdline_dup = NULL; ZeroMemory(&si, sizeof(si)); ZeroMemory(&pi, sizeof(pi)); si.cb = sizeof(si); /* CreateProcess needs a modifiable cmdline: make a copy */ cmdline_dup = _wcsdup(cmdline); if (cmdline_dup && CreateProcessW(argv0, cmdline_dup, NULL, NULL, FALSE, proc_flags, NULL, NULL, &si, &pi)) { WaitForSingleObject(pi.hProcess, timeout ? timeout : INFINITE); if (!GetExitCodeProcess(pi.hProcess, &exit_code)) { MsgToEventLog(M_SYSERR, L"ExecCommand: Error getting exit_code:"); exit_code = GetLastError(); } else if (exit_code == STILL_ACTIVE) { exit_code = WAIT_TIMEOUT; /* Windows error code 0x102 */ /* kill without impunity */ TerminateProcess(pi.hProcess, exit_code); MsgToEventLog(M_ERR, L"ExecCommand: \"%ls %ls\" killed after timeout", argv0, cmdline); } else if (exit_code) { MsgToEventLog(M_ERR, L"ExecCommand: \"%ls %ls\" exited with status = %lu", argv0, cmdline, exit_code); } else { MsgToEventLog(M_INFO, L"ExecCommand: \"%ls %ls\" completed", argv0, cmdline); } CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { exit_code = GetLastError(); MsgToEventLog(M_SYSERR, L"ExecCommand: could not run \"%ls %ls\" :", argv0, cmdline); } free(cmdline_dup); return exit_code; } /* * Entry point for register-dns thread. */ static DWORD WINAPI RegisterDNS(LPVOID unused) { DWORD err; size_t i; DWORD timeout = RDNS_TIMEOUT * 1000; /* in milliseconds */ /* path of ipconfig command */ WCHAR ipcfg[MAX_PATH]; struct { WCHAR *argv0; WCHAR *cmdline; DWORD timeout; } cmds[] = { { ipcfg, L"ipconfig /flushdns", timeout }, { ipcfg, L"ipconfig /registerdns", timeout }, }; HANDLE wait_handles[2] = { rdns_semaphore, exit_event }; swprintf(ipcfg, MAX_PATH, L"%ls\\%ls", get_win_sys_path(), L"ipconfig.exe"); if (WaitForMultipleObjects(2, wait_handles, FALSE, timeout) == WAIT_OBJECT_0) { /* Semaphore locked */ for (i = 0; i < _countof(cmds); ++i) { ExecCommand(cmds[i].argv0, cmds[i].cmdline, cmds[i].timeout); } err = 0; if (!ReleaseSemaphore(rdns_semaphore, 1, NULL)) { err = MsgToEventLog(M_SYSERR, L"RegisterDNS: Failed to release regsiter-dns semaphore:"); } } else { MsgToEventLog(M_ERR, L"RegisterDNS: Failed to lock register-dns semaphore"); err = ERROR_SEM_TIMEOUT; /* Windows error code 0x79 */ } return err; } static DWORD HandleRegisterDNSMessage(void) { DWORD err; HANDLE thread = NULL; /* Delegate this job to a sub-thread */ thread = CreateThread(NULL, 0, RegisterDNS, NULL, 0, NULL); /* * We don't add these thread handles to the undo list -- the thread and * processes it spawns are all supposed to terminate or timeout by themselves. */ if (thread) { err = 0; CloseHandle(thread); } else { err = GetLastError(); } return err; } /** * Run the command: netsh interface ip $action wins $if_name [static] $addr * @param action "delete", "add" or "set" * @param if_name "name_of_interface" * @param addr IPv4 address as a string * * If addr is null and action = "delete" all addresses are deleted. * if action = "set" then "static" is added before $addr */ static DWORD netsh_wins_cmd(const wchar_t *action, const wchar_t *if_name, const wchar_t *addr) { DWORD err = 0; int timeout = 30000; /* in msec */ wchar_t argv0[MAX_PATH]; wchar_t *cmdline = NULL; const wchar_t *addr_static = (wcscmp(action, L"set") == 0) ? L"static" : L""; if (!addr) { if (wcscmp(action, L"delete") == 0) { addr = L"all"; } else /* nothing to do -- return success*/ { goto out; } } /* Path of netsh */ swprintf(argv0, _countof(argv0), L"%ls\\%ls", get_win_sys_path(), L"netsh.exe"); /* cmd template: * netsh interface ip $action wins $if_name $static $addr */ const wchar_t *fmt = L"netsh interface ip %ls wins \"%ls\" %ls %ls"; /* max cmdline length in wchars -- include room for worst case and some */ size_t ncmdline = wcslen(fmt) + wcslen(if_name) + wcslen(action) + wcslen(addr) + wcslen(addr_static) + 32 + 1; cmdline = malloc(ncmdline * sizeof(wchar_t)); if (!cmdline) { err = ERROR_OUTOFMEMORY; goto out; } swprintf(cmdline, ncmdline, fmt, action, if_name, addr_static, addr); err = ExecCommand(argv0, cmdline, timeout); out: free(cmdline); return err; } static BOOL CmpWString(LPVOID item, LPVOID str) { return (wcscmp(item, str) == 0) ? TRUE : FALSE; } /** * Signal the DNS resolver (and others potentially) to reload the * group policy (DNS) settings on 32 bit Windows systems * * @return BOOL to indicate if the reload was initiated */ static BOOL ApplyGpolSettings32(void) { typedef NTSTATUS(__stdcall * publish_fn_t)(DWORD StateNameLo, DWORD StateNameHi, DWORD TypeId, DWORD Buffer, DWORD Length, DWORD ExplicitScope); publish_fn_t RtlPublishWnfStateData; const DWORD WNF_GPOL_SYSTEM_CHANGES_HI = 0x0D891E2A; const DWORD WNF_GPOL_SYSTEM_CHANGES_LO = 0xA3BC0875; HMODULE ntdll = LoadLibraryA("ntdll.dll"); if (ntdll == NULL) { return FALSE; } RtlPublishWnfStateData = (publish_fn_t)GetProcAddress(ntdll, "RtlPublishWnfStateData"); if (RtlPublishWnfStateData == NULL) { return FALSE; } if (RtlPublishWnfStateData(WNF_GPOL_SYSTEM_CHANGES_LO, WNF_GPOL_SYSTEM_CHANGES_HI, 0, 0, 0, 0) != ERROR_SUCCESS) { return FALSE; } return TRUE; } /** * Signal the DNS resolver (and others potentially) to reload the * group policy (DNS) settings on 64 bit Windows systems * * @return BOOL to indicate if the reload was initiated */ static BOOL ApplyGpolSettings64(void) { typedef NTSTATUS (*publish_fn_t)(INT64 StateName, INT64 TypeId, INT64 Buffer, unsigned int Length, INT64 ExplicitScope); publish_fn_t RtlPublishWnfStateData; const INT64 WNF_GPOL_SYSTEM_CHANGES = 0x0D891E2AA3BC0875; HMODULE ntdll = LoadLibraryA("ntdll.dll"); if (ntdll == NULL) { return FALSE; } RtlPublishWnfStateData = (publish_fn_t)GetProcAddress(ntdll, "RtlPublishWnfStateData"); if (RtlPublishWnfStateData == NULL) { return FALSE; } if (RtlPublishWnfStateData(WNF_GPOL_SYSTEM_CHANGES, 0, 0, 0, 0) != ERROR_SUCCESS) { return FALSE; } return TRUE; } /** * Signal the DNS resolver (and others potentially) to reload the group policy (DNS) settings * * @return BOOL to indicate if the reload was initiated */ static BOOL ApplyGpolSettings(void) { SYSTEM_INFO si; GetSystemInfo(&si); const BOOL win_32bit = si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL; return win_32bit ? ApplyGpolSettings32() : ApplyGpolSettings64(); } /** * Signal the DNS resolver to reload its settings * * @param apply_gpol BOOL reload setting from group policy hives as well * * @return BOOL to indicate if the reload was initiated */ static BOOL ApplyDnsSettings(BOOL apply_gpol) { BOOL res = FALSE; SC_HANDLE scm = NULL; SC_HANDLE dnssvc = NULL; if (apply_gpol && ApplyGpolSettings() == FALSE) { MsgToEventLog(M_ERR, L"%S: sending GPOL notification failed", __func__); } scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (scm == NULL) { MsgToEventLog(M_ERR, L"%S: OpenSCManager call failed (%lu)", __func__, GetLastError()); goto out; } dnssvc = OpenServiceA(scm, "Dnscache", SERVICE_PAUSE_CONTINUE); if (dnssvc == NULL) { MsgToEventLog(M_ERR, L"%S: OpenService call failed (%lu)", __func__, GetLastError()); goto out; } SERVICE_STATUS status; if (ControlService(dnssvc, SERVICE_CONTROL_PARAMCHANGE, &status) == 0) { MsgToEventLog(M_ERR, L"%S: ControlService call failed (%lu)", __func__, GetLastError()); goto out; } res = TRUE; out: if (dnssvc) { CloseServiceHandle(dnssvc); } if (scm) { CloseServiceHandle(scm); } return res; } /** * Get the string interface UUID (with braces) for an interface alias name * * @param itf_name the interface alias name * @param str pointer to the buffer the wide UUID is returned in * @param len size of the str buffer in characters * * @return NO_ERROR on success, or the Windows error code for the failure */ static DWORD InterfaceIdString(PCSTR itf_name, PWSTR str, size_t len) { DWORD err; GUID guid; NET_LUID luid; PWSTR iid_str = NULL; err = InterfaceLuid(itf_name, &luid); if (err) { MsgToEventLog(M_ERR, L"%S: failed to convert itf alias '%s'", __func__, itf_name); goto out; } err = ConvertInterfaceLuidToGuid(&luid, &guid); if (err) { MsgToEventLog(M_ERR, L"%S: Failed to convert itf '%s' LUID", __func__, itf_name); goto out; } if (StringFromIID(&guid, &iid_str) != S_OK) { MsgToEventLog(M_ERR, L"%S: Failed to convert itf '%s' IID", __func__, itf_name); err = ERROR_OUTOFMEMORY; goto out; } if (wcslen(iid_str) + 1 > len) { err = ERROR_INVALID_PARAMETER; goto out; } wcsncpy(str, iid_str, len); out: if (iid_str) { CoTaskMemFree(iid_str); } return err; } /** * Check for a valid search list in a certain key of the registry * * Valid means that a string value "SearchList" exists and that it * contains one or more domains. We only check if the string contains * a valid domain name character, but the main point is to prevent letting * pass whitespace-only lists, so that check is good enough for that * purpose. * * @param key HKEY in which to check for a valid search list * * @return BOOL to indicate if a valid search list has been found */ static BOOL HasValidSearchList(HKEY key) { char data[64]; DWORD size = sizeof(data); LSTATUS err = RegGetValueA(key, NULL, "SearchList", RRF_RT_REG_SZ, NULL, (PBYTE)data, &size); if (!err || err == ERROR_MORE_DATA) { data[sizeof(data) - 1] = '\0'; for (int i = 0; i < strlen(data); ++i) { if (isalnum(data[i]) || data[i] == '-' || data[i] == '.') { return TRUE; } } } return FALSE; } /** * Find the registry key for storing the DNS domains for the VPN interface * * @param itf_name PCSTR that contains the alias name of the interface the domains * are related to. If this is NULL the interface probing is skipped. * @param gpol PBOOL to indicate if the key returned is the group policy hive * @param key PHKEY in which the found registry key is returned in * * @return BOOL to indicate if a search list is already present at the location. * If the key returned is INVALID_HANDLE_VALUE, this indicates an * unrecoverable error. * * The correct location to add them is where a non-empty "SearchList" value exists, * or in the interface configuration itself. However, the system-wide and then the * group policy search lists overrule the previous one respectively, so we need to * probe to find the effective list. */ static BOOL GetDnsSearchListKey(PCSTR itf_name, PBOOL gpol, PHKEY key) { LSTATUS err; *gpol = FALSE; /* Try the group policy search list */ err = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Microsoft\\Windows NT\\DNSClient", 0, KEY_ALL_ACCESS, key); if (!err) { if (HasValidSearchList(*key)) { *gpol = TRUE; return TRUE; } RegCloseKey(*key); } /* Try the system-wide search list */ err = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "System\\CurrentControlSet\\Services\\TCPIP\\Parameters", 0, KEY_ALL_ACCESS, key); if (!err) { if (HasValidSearchList(*key)) { return TRUE; } RegCloseKey(*key); } if (itf_name) { /* Always return the VPN interface key (if it exists) */ WCHAR iid[64]; DWORD iid_err = InterfaceIdString(itf_name, iid, _countof(iid)); if (!iid_err) { HKEY itfs; err = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "System\\CurrentControlSet\\Services\\TCPIP\\Parameters\\Interfaces", 0, KEY_ALL_ACCESS, &itfs); if (!err) { err = RegOpenKeyExW(itfs, iid, 0, KEY_ALL_ACCESS, key); RegCloseKey(itfs); if (!err) { return FALSE; /* No need to preserve the VPN itf search list */ } } } } *key = INVALID_HANDLE_VALUE; return FALSE; } /** * Check if a initial list had already been created * * @param key HKEY of the registry subkey to search in * * @return BOOL to indicate if the initial list is already present under key */ static BOOL InitialSearchListExists(HKEY key) { LSTATUS err; err = RegGetValueA(key, NULL, "InitialSearchList", RRF_RT_REG_SZ, NULL, NULL, NULL); if (err) { if (err == ERROR_FILE_NOT_FOUND) { return FALSE; } MsgToEventLog(M_ERR, L"%S: failed to get InitialSearchList (%lu)", __func__, err); } return TRUE; } #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wconversion" #endif /** * Prepare DNS domain "SearchList" registry value, so additional * VPN domains can be added and its original state can be restored * in case the system cannot clean up regularly. * * @param key registry subkey to store the list in * @param list string of comma separated domains to use as the list * * @return boolean to indicate whether the list was stored successfully */ static BOOL StoreInitialDnsSearchList(HKEY key, PCWSTR list) { if (!list || wcslen(list) == 0) { MsgToEventLog(M_ERR, L"%S: empty search list", __func__); return FALSE; } if (InitialSearchListExists(key)) { /* Initial list had already been stored */ return TRUE; } DWORD size = (wcslen(list) + 1) * sizeof(*list); LSTATUS err = RegSetValueExW(key, L"InitialSearchList", 0, REG_SZ, (PBYTE)list, size); if (err) { MsgToEventLog(M_ERR, L"%S: failed to set InitialSearchList value (%lu)", __func__, err); return FALSE; } return TRUE; } /** * Append domain suffixes to an existing search list * * @param key HKEY the list is stored at * @param have_list BOOL to indicate if a search list already exists * @param domains domain suffixes as comma separated string * * @return BOOL to indicate success or failure */ static BOOL AddDnsSearchDomains(HKEY key, BOOL have_list, PCWSTR domains) { LSTATUS err; WCHAR list[2048] = { 0 }; DWORD size = sizeof(list); if (have_list) { err = RegGetValueW(key, NULL, L"SearchList", RRF_RT_REG_SZ, NULL, list, &size); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not get SearchList from registry (%lu)", __func__, err); return FALSE; } if (!StoreInitialDnsSearchList(key, list)) { return FALSE; } size_t listlen = (size / sizeof(list[0])) - 1; /* returned size is in bytes */ size_t domlen = wcslen(domains); if (listlen + domlen + 2 > _countof(list)) { MsgToEventLog(M_SYSERR, L"%S: not enough space in list for search domains (len=%lu)", __func__, domlen); return FALSE; } /* Append to end of the search list */ PWSTR pos = list + listlen; *pos = ','; wcsncpy(pos + 1, domains, domlen + 1); } else { wcsncpy(list, domains, wcslen(domains) + 1); } size = (wcslen(list) + 1) * sizeof(list[0]); err = RegSetValueExW(key, L"SearchList", 0, REG_SZ, (PBYTE)list, size); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not set SearchList to registry (%lu)", __func__, err); return FALSE; } return TRUE; } /** * Reset the DNS search list to its original value * * Looks for a "InitialSearchList" value as the one to reset to. * If it doesn't exist, doesn't reset anything, as there was no * SearchList in the first place. * * @param key HKEY of the location in the registry to reset * * @return BOOL to indicate if something was reset */ static BOOL ResetDnsSearchDomains(HKEY key) { LSTATUS err; BOOL ret = FALSE; WCHAR list[2048]; DWORD size = sizeof(list); err = RegGetValueW(key, NULL, L"InitialSearchList", RRF_RT_REG_SZ, NULL, list, &size); if (err) { if (err != ERROR_FILE_NOT_FOUND) { MsgToEventLog(M_SYSERR, L"%S: could not get InitialSearchList from registry (%lu)", __func__, err); } goto out; } size = (wcslen(list) + 1) * sizeof(list[0]); err = RegSetValueExW(key, L"SearchList", 0, REG_SZ, (PBYTE)list, size); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not set SearchList in registry (%lu)", __func__, err); goto out; } RegDeleteValueA(key, "InitialSearchList"); ret = TRUE; out: return ret; } /** * Remove domain suffixes from an existing search list * * @param key HKEY the list is stored at * @param domains domain suffixes to remove as comma separated string */ static void RemoveDnsSearchDomains(HKEY key, PCWSTR domains) { LSTATUS err; WCHAR list[2048]; DWORD size = sizeof(list); err = RegGetValueW(key, NULL, L"SearchList", RRF_RT_REG_SZ, NULL, list, &size); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not get SearchList from registry (%lu)", __func__, err); return; } PWSTR dst = wcsstr(list, domains); if (!dst) { MsgToEventLog(M_ERR, L"%S: could not find domains in search list", __func__); return; } /* Cut out domains from list */ size_t domlen = wcslen(domains); PCWSTR src = dst + domlen; /* Also remove the leading comma, if there is one */ dst = dst > list ? dst - 1 : dst; wmemmove(dst, src, domlen); size_t list_len = wcslen(list); if (list_len) { /* Now check if the shortened list equals the initial search list */ WCHAR initial[2048]; size = sizeof(initial); err = RegGetValueW(key, NULL, L"InitialSearchList", RRF_RT_REG_SZ, NULL, initial, &size); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not get InitialSearchList from registry (%lu)", __func__, err); return; } /* If the search list is back to its initial state reset it */ if (wcsncmp(list, initial, wcslen(list)) == 0) { ResetDnsSearchDomains(key); return; } } size = (list_len + 1) * sizeof(list[0]); err = RegSetValueExW(key, L"SearchList", 0, REG_SZ, (PBYTE)list, size); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not set SearchList in registry (%lu)", __func__, err); } } /** * Removes DNS domains from a search list they were previously added to * * @param undo_data pointer to dns_domains_undo_data_t */ static void UndoDnsSearchDomains(dns_domains_undo_data_t *undo_data) { BOOL gpol; HKEY dns_searchlist_key; GetDnsSearchListKey(undo_data->itf_name, &gpol, &dns_searchlist_key); if (dns_searchlist_key != INVALID_HANDLE_VALUE) { RemoveDnsSearchDomains(dns_searchlist_key, undo_data->domains); RegCloseKey(dns_searchlist_key); ApplyDnsSettings(gpol); free(undo_data->domains); undo_data->domains = NULL; } } /** * Add or remove DNS search domains * * @param itf_name alias name of the interface the domains are set for * @param domains a comma separated list of domain name suffixes * @param gpol PBOOL to indicate if group policy values were modified * @param lists pointer to the undo lists * * @return NO_ERROR on success, an error status code otherwise * * If a SearchList is present in the registry already, the domains are added * to that list. Otherwise the domains are added to the VPN interface specific list. * A group policy search list takes precedence over a system-wide list, and that one * itself takes precedence over interface specific ones. * * This function will remove previously set domains if the domains parameter * is NULL or empty. * * The gpol value is only valid if the function returns no error. In the error * case nothing is changed. */ static DWORD SetDnsSearchDomains(PCSTR itf_name, PCSTR domains, PBOOL gpol, undo_lists_t *lists) { DWORD err = ERROR_OUTOFMEMORY; HKEY list_key; BOOL have_list = GetDnsSearchListKey(itf_name, gpol, &list_key); if (list_key == INVALID_HANDLE_VALUE) { MsgToEventLog(M_SYSERR, L"%S: could not get search list registry key", __func__); return ERROR_FILE_NOT_FOUND; } /* Remove previously installed search domains */ dns_domains_undo_data_t *undo_data = RemoveListItem(&(*lists)[undo_domains], CmpAny, NULL); if (undo_data) { RemoveDnsSearchDomains(list_key, undo_data->domains); free(undo_data->domains); free(undo_data); undo_data = NULL; } /* If there are search domains, add them */ if (domains && *domains) { wchar_t *wide_domains = utf8to16(domains); /* utf8 to wide-char */ if (!wide_domains) { goto out; } undo_data = malloc(sizeof(*undo_data)); if (!undo_data) { free(wide_domains); wide_domains = NULL; goto out; } strncpy(undo_data->itf_name, itf_name, sizeof(undo_data->itf_name)); undo_data->domains = wide_domains; if (AddDnsSearchDomains(list_key, have_list, wide_domains) == FALSE || AddListItem(&(*lists)[undo_domains], undo_data) != NO_ERROR) { RemoveDnsSearchDomains(list_key, wide_domains); free(wide_domains); free(undo_data); undo_data = NULL; goto out; } } err = NO_ERROR; out: RegCloseKey(list_key); return err; } /** * Return the interfaces registry key for the specified address family * * @param family the internet address family to open the key for * @param key PHKEY to return the key in * @return BOOL to indicate success or failure */ static BOOL GetInterfacesKey(short family, PHKEY key) { PCSTR itfs_key = family == AF_INET6 ? "SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces" : "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"; LSTATUS err = RegOpenKeyExA(HKEY_LOCAL_MACHINE, itfs_key, 0, KEY_ALL_ACCESS, key); if (err) { *key = INVALID_HANDLE_VALUE; MsgToEventLog(M_SYSERR, L"%S: could not open interfaces registry key for family %d (%lu)", __func__, family, err); } return err ? FALSE : TRUE; } /** * Set the DNS name servers in a registry interface configuration * * @param itf_id the interface id to set the servers for * @param family internet address family to set the servers for * @param value the value to set the name servers to * * @return DWORD NO_ERROR on success, a Windows error code otherwise */ static DWORD SetNameServersValue(PCWSTR itf_id, short family, PCSTR value) { DWORD err; HKEY itfs; if (!GetInterfacesKey(family, &itfs)) { return ERROR_FILE_NOT_FOUND; } HKEY itf = INVALID_HANDLE_VALUE; err = RegOpenKeyExW(itfs, itf_id, 0, KEY_ALL_ACCESS, &itf); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not open interface key for %s family %d (%lu)", __func__, itf_id, family, err); goto out; } err = RegSetValueExA(itf, "NameServer", 0, REG_SZ, (PBYTE)value, strlen(value) + 1); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not set name servers '%S' for %s family %d (%lu)", __func__, value, itf_id, family, err); } out: if (itf != INVALID_HANDLE_VALUE) { RegCloseKey(itf); } if (itfs != INVALID_HANDLE_VALUE) { RegCloseKey(itfs); } return err; } /** * Set the DNS name servers in a registry interface configuration * * @param itf_id the interface id to set the servers for * @param family internet address family to set the servers for * @param addrs comma separated list of name server addresses * * @return DWORD NO_ERROR on success, a Windows error code otherwise */ static DWORD SetNameServers(PCWSTR itf_id, short family, PCSTR addrs) { return SetNameServersValue(itf_id, family, addrs); } /** * Delete all DNS name servers from a registry interface configuration * * @param itf_id the interface id to clear the servers for * @param family internet address family to clear the servers for * * @return DWORD NO_ERROR on success, a Windows error code otherwise */ static DWORD ResetNameServers(PCWSTR itf_id, short family) { return SetNameServersValue(itf_id, family, ""); } static DWORD HandleDNSConfigMessage(const dns_cfg_message_t *msg, undo_lists_t *lists) { DWORD err = 0; undo_type_t undo_type = (msg->family == AF_INET6) ? undo_dns4 : undo_dns6; int addr_len = msg->addr_len; /* sanity check */ const size_t max_addrs = _countof(msg->addr); if (addr_len > max_addrs) { addr_len = max_addrs; } if (!msg->iface.name[0]) /* interface name is required */ { return ERROR_MESSAGE_DATA; } /* use a non-const reference with limited scope to enforce null-termination of strings from * client */ { dns_cfg_message_t *msgptr = (dns_cfg_message_t *)msg; msgptr->iface.name[_countof(msg->iface.name) - 1] = '\0'; msgptr->domains[_countof(msg->domains) - 1] = '\0'; } WCHAR iid[64]; err = InterfaceIdString(msg->iface.name, iid, _countof(iid)); if (err) { return err; } /* We delete all current addresses before adding any * OR if the message type is del_dns_cfg */ if (addr_len > 0 || msg->header.type == msg_del_dns_cfg) { err = ResetNameServers(iid, msg->family); if (err) { return err; } free(RemoveListItem(&(*lists)[undo_type], CmpAny, iid)); } if (msg->header.type == msg_del_dns_cfg) { BOOL gpol = FALSE; if (msg->domains[0]) { /* setting an empty domain list removes any previous value */ err = SetDnsSearchDomains(msg->iface.name, NULL, &gpol, lists); } ApplyDnsSettings(gpol); return err; /* job done */ } if (msg->addr_len > 0) { /* prepare the comma separated address list */ /* cannot use max_addrs here as that is not considered compile * time constant by all compilers and constexpr is C23 */ CHAR addrs[_countof(msg->addr) * 64]; /* 64 is enough for one IPv4/6 address */ size_t offset = 0; for (int i = 0; i < addr_len; ++i) { if (i != 0) { addrs[offset++] = ','; } if (msg->family == AF_INET6) { RtlIpv6AddressToStringA(&msg->addr[i].ipv6, addrs + offset); } else { RtlIpv4AddressToStringA(&msg->addr[i].ipv4, addrs + offset); } offset += strlen(addrs); } err = SetNameServers(iid, msg->family, addrs); if (err) { return err; } wchar_t *tmp_iid = _wcsdup(iid); if (!tmp_iid || AddListItem(&(*lists)[undo_type], tmp_iid)) { free(tmp_iid); ResetNameServers(iid, msg->family); return ERROR_OUTOFMEMORY; } } BOOL gpol = FALSE; if (msg->domains[0]) { err = SetDnsSearchDomains(msg->iface.name, msg->domains, &gpol, lists); } ApplyDnsSettings(gpol); return err; } /** * Checks if DHCP is enabled for an interface * * @param key HKEY of the interface to check for * * @return BOOL set to TRUE if DHCP is enabled, or FALSE if * disabled or an error occurred */ static BOOL IsDhcpEnabled(HKEY key) { DWORD dhcp; DWORD size = sizeof(dhcp); LSTATUS err; err = RegGetValueA(key, NULL, "EnableDHCP", RRF_RT_REG_DWORD, NULL, (PBYTE)&dhcp, &size); if (err != NO_ERROR) { MsgToEventLog(M_SYSERR, L"%S: Could not read DHCP status (%lu)", __func__, err); return FALSE; } return dhcp ? TRUE : FALSE; } /** * Set name servers from a NRPT address list * * @param itf_id the VPN interface ID to set the name servers for * @param addresses the list of NRPT addresses * * @return LSTATUS NO_ERROR in case of success, a Windows error code otherwise */ static LSTATUS SetNameServerAddresses(PWSTR itf_id, const nrpt_address_t *addresses) { const short families[] = { AF_INET, AF_INET6 }; for (int i = 0; i < _countof(families); i++) { short family = families[i]; /* Create a comma sparated list of addresses of this family */ int offset = 0; char addr_list[NRPT_ADDR_SIZE * NRPT_ADDR_NUM]; for (int j = 0; j < NRPT_ADDR_NUM && addresses[j][0]; j++) { if ((family == AF_INET6 && strchr(addresses[j], ':') == NULL) || (family == AF_INET && strchr(addresses[j], ':') != NULL)) { /* Address family doesn't match, skip this one */ continue; } if (offset) { addr_list[offset++] = ','; } strcpy(addr_list + offset, addresses[j]); offset += strlen(addresses[j]); } if (offset == 0) { /* No address for this family to set */ continue; } /* Set name server addresses */ LSTATUS err = SetNameServers(itf_id, family, addr_list); if (err) { return err; } } return NO_ERROR; } /** * Get DNS server IPv4 addresses of an interface * * @param itf_key registry key of the IPv4 interface data * @param addrs pointer to the buffer addresses are returned in * @param size pointer to the size of the buffer, contains the * size of the addresses on return * * @return LSTATUS NO_ERROR on success, a Windows error code otherwise */ static LSTATUS GetItfDnsServersV4(HKEY itf_key, PSTR addrs, PDWORD size) { addrs[*size - 1] = '\0'; LSTATUS err; DWORD s = *size; err = RegGetValueA(itf_key, NULL, "NameServer", RRF_RT_REG_SZ, NULL, (PBYTE)addrs, &s); if (err && err != ERROR_FILE_NOT_FOUND) { *size = 0; return err; } /* Try DHCP addresses if we don't have some already */ if (!strchr(addrs, '.') && IsDhcpEnabled(itf_key)) { s = *size; RegGetValueA(itf_key, NULL, "DhcpNameServer", RRF_RT_REG_SZ, NULL, (PBYTE)addrs, &s); if (err) { *size = 0; return err; } } if (strchr(addrs, '.')) { *size = s; return NO_ERROR; } *size = 0; return ERROR_FILE_NOT_FOUND; } /** * Get DNS server IPv6 addresses of an interface * * @param itf_key registry key of the IPv6 interface data * @param addrs pointer to the buffer addresses are returned in * @param size pointer to the size of the buffer * * @return LSTATUS NO_ERROR on success, a Windows error code otherwise */ static LSTATUS GetItfDnsServersV6(HKEY itf_key, PSTR addrs, PDWORD size) { addrs[*size - 1] = '\0'; LSTATUS err; DWORD s = *size; err = RegGetValueA(itf_key, NULL, "NameServer", RRF_RT_REG_SZ, NULL, (PBYTE)addrs, &s); if (err && err != ERROR_FILE_NOT_FOUND) { *size = 0; return err; } /* Try DHCP addresses if we don't have some already */ if (!strchr(addrs, ':') && IsDhcpEnabled(itf_key)) { IN6_ADDR in_addrs[8]; DWORD in_addrs_size = sizeof(in_addrs); err = RegGetValueA(itf_key, NULL, "Dhcpv6DNSServers", RRF_RT_REG_BINARY, NULL, (PBYTE)in_addrs, &in_addrs_size); if (err) { *size = 0; return err; } s = *size; PSTR pos = addrs; size_t in_addrs_read = in_addrs_size / sizeof(IN6_ADDR); for (size_t i = 0; i < in_addrs_read; ++i) { if (i != 0) { /* Add separator */ *pos++ = ','; s--; } if (inet_ntop(AF_INET6, &in_addrs[i], pos, s) != NULL) { *size = 0; return ERROR_MORE_DATA; } size_t addr_len = strlen(pos); pos += addr_len; s -= addr_len; } s = strlen(addrs) + 1; } if (strchr(addrs, ':')) { *size = s; return NO_ERROR; } *size = 0; return ERROR_FILE_NOT_FOUND; } /** * Check if a domain is contained in a comma separated list of domains * * @param list Comma separated list of domains * @param domain Domain string to search for * @param len Length of the domain string, excluding the '\0' * * @return TRUE when the domain was found in the list, FALSE otherwise. */ static BOOL ListContainsDomain(PCWSTR list, PCWSTR domain, size_t len) { PCWSTR match = list; while (TRUE) { match = wcsstr(match, domain); if (!match) { /* Domain has not matched */ break; } if ((match == list || *(match - 1) == ',') && (*(match + len) == ',' || *(match + len) == '\0')) { /* Domain has matched fully */ return TRUE; } match += len; } return FALSE; } /** * Return interface specific domain suffix(es) * * The \p domains paramter will be set to a MULTI_SZ domains string. * In case of an error or if no domains are found for the interface * \p size is set to 0 and the contents of \p domains are invalid. * Note that the domains could have been set by DHCP or manually. * Note that domains are ignored if they match a pushed search domain. * * @param itf HKEY of the interface to read from * @param search_domains optional list of search domains * @param domains PWSTR buffer to return the domain(s) in * @param size pointer to size of the domains buffer in bytes. Will be * set to the size of the string returned, including * the terminating zeros or 0. * * @return LSTATUS NO_ERROR if the domain suffix(es) were read successfully, * ERROR_FILE_NOT_FOUND if no domain was found for the interface, * ERROR_MORE_DATA if the list did not fit into the buffer, * any other error indicates an error while reading from the registry. */ static LSTATUS GetItfDnsDomains(HKEY itf, PCWSTR search_domains, PWSTR domains, PDWORD size) { if (domains == NULL || size == 0) { return ERROR_INVALID_PARAMETER; } LSTATUS err = ERROR_FILE_NOT_FOUND; const DWORD buf_size = *size; const size_t one_glyph = sizeof(*domains); PWSTR values[] = { L"SearchList", L"Domain", L"DhcpDomainSearchList", L"DhcpDomain", NULL }; for (int i = 0; values[i]; i++) { *size = buf_size; err = RegGetValueW(itf, NULL, values[i], RRF_RT_REG_SZ, NULL, (PBYTE)domains, size); if (!err && *size > one_glyph && wcschr(domains, '.')) { /* * Found domain(s), now convert them: * - prefix each domain with a dot * - convert comma separated list to MULTI_SZ */ PWCHAR pos = domains; const DWORD buf_len = buf_size / one_glyph; while (TRUE) { /* Terminate the domain at the next comma */ PWCHAR comma = wcschr(pos, ','); if (comma) { *comma = '\0'; } /* Ignore itf domains which match a pushed search domain */ size_t domain_len = wcslen(pos); if (ListContainsDomain(search_domains, pos, domain_len)) { if (comma) { pos = comma + 1; continue; } else { /* This was the last domain */ *pos = '\0'; *size += one_glyph; return wcslen(domains) ? NO_ERROR : ERROR_FILE_NOT_FOUND; } } /* Check for enough space to convert this domain */ domain_len += 1; /* leading dot */ size_t converted_size = pos - domains; size_t domain_size = domain_len * one_glyph; size_t extra_size = 2 * one_glyph; if (converted_size + domain_size + extra_size > buf_size) { /* Domain doesn't fit, bad luck if it's the first one */ *pos = '\0'; *size = converted_size == 0 ? 0 : *size + 1; return ERROR_MORE_DATA; } /* Prefix domain at pos with the dot */ memmove(pos + 1, pos, buf_size - converted_size - one_glyph); domains[buf_len - 1] = '\0'; *pos = '.'; *size += one_glyph; if (!comma) { /* Conversion is done */ *(pos + domain_len) = '\0'; *size += one_glyph; return NO_ERROR; } pos = comma + 1; } } } *size = 0; return err; } /** * Check if an interface is connected and up * * @param iid_str the interface GUID as string * * @return TRUE if the interface is connected and up, FALSE otherwise or in * case an error happened */ static BOOL IsInterfaceConnected(PWSTR iid_str) { GUID iid; BOOL res = FALSE; MIB_IF_ROW2 itf_row; /* Get GUID from string */ if (IIDFromString(iid_str, &iid) != S_OK) { MsgToEventLog(M_SYSERR, L"%S: could not convert interface %s GUID string", __func__, iid_str); goto out; } /* Get LUID from GUID */ if (ConvertInterfaceGuidToLuid(&iid, &itf_row.InterfaceLuid) != NO_ERROR) { goto out; } /* Look up interface status */ if (GetIfEntry2(&itf_row) != NO_ERROR) { MsgToEventLog(M_SYSERR, L"%S: could not get interface %s status", __func__, iid_str); goto out; } if (itf_row.MediaConnectState == MediaConnectStateConnected && itf_row.OperStatus == IfOperStatusUp) { res = TRUE; } out: return res; } /** * Collect interface DNS settings to be used in excluding NRPT rules. This is * needed so that local DNS keeps working even when a catch all NRPT rule is * installed by a VPN connection. * * @param search_domains optional list of search domains * @param data pointer to the data structures the values are returned in * @param data_size number of exclude data structures pointed to */ static void GetNrptExcludeData(PCWSTR search_domains, nrpt_exclude_data_t *data, size_t data_size) { HKEY v4_itfs = INVALID_HANDLE_VALUE; HKEY v6_itfs = INVALID_HANDLE_VALUE; if (!GetInterfacesKey(AF_INET, &v4_itfs) || !GetInterfacesKey(AF_INET6, &v6_itfs)) { goto out; } size_t i = 0; DWORD enum_index = 0; while (i < data_size) { WCHAR itf_guid[MAX_PATH]; DWORD itf_guid_len = _countof(itf_guid); LSTATUS err = RegEnumKeyExW(v4_itfs, enum_index++, itf_guid, &itf_guid_len, NULL, NULL, NULL, NULL); if (err) { if (err != ERROR_NO_MORE_ITEMS) { MsgToEventLog(M_SYSERR, L"%S: could not enumerate interfaces (%lu)", __func__, err); } goto out; } /* Ignore interfaces that are not connected or disabled */ if (!IsInterfaceConnected(itf_guid)) { continue; } HKEY v4_itf; if (RegOpenKeyExW(v4_itfs, itf_guid, 0, KEY_READ, &v4_itf) != NO_ERROR) { MsgToEventLog(M_SYSERR, L"%S: could not open interface %s v4 registry key", __func__, itf_guid); goto out; } /* Get the DNS domain(s) for exclude routing */ data[i].domains_size = sizeof(data[0].domains); memset(data[i].domains, 0, data[i].domains_size); err = GetItfDnsDomains(v4_itf, search_domains, data[i].domains, &data[i].domains_size); if (err) { if (err != ERROR_FILE_NOT_FOUND) { MsgToEventLog(M_SYSERR, L"%S: could not read interface %s domain suffix", __func__, itf_guid); } goto next_itf; } /* Get the IPv4 DNS servers */ DWORD v4_addrs_size = sizeof(data[0].addresses); err = GetItfDnsServersV4(v4_itf, data[i].addresses, &v4_addrs_size); if (err && err != ERROR_FILE_NOT_FOUND) { MsgToEventLog(M_SYSERR, L"%S: could not read interface %s v4 name servers (%ld)", __func__, itf_guid, err); goto next_itf; } /* Get the IPv6 DNS servers, if there's space left */ PSTR v6_addrs = data[i].addresses + v4_addrs_size; DWORD v6_addrs_size = sizeof(data[0].addresses) - v4_addrs_size; if (v6_addrs_size > NRPT_ADDR_SIZE) { HKEY v6_itf; if (RegOpenKeyExW(v6_itfs, itf_guid, 0, KEY_READ, &v6_itf) != NO_ERROR) { MsgToEventLog(M_SYSERR, L"%S: could not open interface %s v6 registry key", __func__, itf_guid); goto next_itf; } err = GetItfDnsServersV6(v6_itf, v6_addrs, &v6_addrs_size); RegCloseKey(v6_itf); if (err && err != ERROR_FILE_NOT_FOUND) { MsgToEventLog(M_SYSERR, L"%S: could not read interface %s v6 name servers (%ld)", __func__, itf_guid, err); goto next_itf; } } if (v4_addrs_size || v6_addrs_size) { /* Replace delimiters with semicolons, as required by NRPT */ for (int j = 0; j < sizeof(data[0].addresses) && data[i].addresses[j]; j++) { if (data[i].addresses[j] == ',' || data[i].addresses[j] == ' ') { data[i].addresses[j] = ';'; } } ++i; } next_itf: RegCloseKey(v4_itf); } out: RegCloseKey(v6_itfs); RegCloseKey(v4_itfs); } /** * Set a NRPT rule (subkey) and its values in the registry * * @param nrpt_key NRPT registry key handle * @param subkey subkey string to create * @param address name server address string * @param domains domains to resolve by this server as MULTI_SZ * @param dom_size size of domains in bytes including the terminators * @param dnssec boolean to determine if DNSSEC is to be enabled * * @return NO_ERROR on success, or Windows error code */ static DWORD SetNrptRule(HKEY nrpt_key, PCWSTR subkey, PCSTR address, PCWSTR domains, DWORD dom_size, BOOL dnssec) { /* Create rule subkey */ DWORD err = NO_ERROR; HKEY rule_key; err = RegCreateKeyExW(nrpt_key, subkey, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &rule_key, NULL); if (err) { return err; } /* Set name(s) for DNS routing */ err = RegSetValueExW(rule_key, L"Name", 0, REG_MULTI_SZ, (PBYTE)domains, dom_size); if (err) { goto out; } /* Set DNS Server address */ err = RegSetValueExA(rule_key, "GenericDNSServers", 0, REG_SZ, (PBYTE)address, strlen(address) + 1); if (err) { goto out; } DWORD reg_val; /* Set DNSSEC if required */ if (dnssec) { reg_val = 1; err = RegSetValueExA(rule_key, "DNSSECValidationRequired", 0, REG_DWORD, (PBYTE)®_val, sizeof(reg_val)); if (err) { goto out; } reg_val = 0; err = RegSetValueExA(rule_key, "DNSSECQueryIPSECRequired", 0, REG_DWORD, (PBYTE)®_val, sizeof(reg_val)); if (err) { goto out; } reg_val = 0; err = RegSetValueExA(rule_key, "DNSSECQueryIPSECEncryption", 0, REG_DWORD, (PBYTE)®_val, sizeof(reg_val)); if (err) { goto out; } } /* Set NRPT config options */ reg_val = dnssec ? 0x0000000A : 0x00000008; err = RegSetValueExA(rule_key, "ConfigOptions", 0, REG_DWORD, (const PBYTE)®_val, sizeof(reg_val)); if (err) { goto out; } /* Mandatory NRPT version */ reg_val = 2; err = RegSetValueExA(rule_key, "Version", 0, REG_DWORD, (const PBYTE)®_val, sizeof(reg_val)); if (err) { goto out; } out: if (err) { RegDeleteKeyW(nrpt_key, subkey); } RegCloseKey(rule_key); return err; } /** * Set NRPT exclude rules to accompany a catch all rule. This is done so that * local resolution of names is not interfered with in case the VPN resolves * all names. * * @param nrpt_key the registry key to set the rules under * @param ovpn_pid the PID of the openvpn process * @param search_domains optional list of search domains */ static void SetNrptExcludeRules(HKEY nrpt_key, DWORD ovpn_pid, PCWSTR search_domains) { nrpt_exclude_data_t data[8]; /* data from up to 8 interfaces */ memset(data, 0, sizeof(data)); GetNrptExcludeData(search_domains, data, _countof(data)); unsigned n = 0; for (int i = 0; i < _countof(data); ++i) { nrpt_exclude_data_t *d = &data[i]; if (d->domains_size == 0) { break; } DWORD err; WCHAR subkey[48]; swprintf(subkey, _countof(subkey), L"OpenVPNDNSRoutingX-%02x-%lu", ++n, ovpn_pid); err = SetNrptRule(nrpt_key, subkey, d->addresses, d->domains, d->domains_size, FALSE); if (err) { MsgToEventLog(M_ERR, L"%S: failed to set rule %s (%lu)", __func__, subkey, err); } } } /** * Set NRPT rules for a openvpn process * * @param nrpt_key the registry key to set the rules under * @param addresses name server addresses * @param domains optional list of split routing domains * @param search_domains optional list of search domains * @param dnssec boolean whether DNSSEC is to be used * @param ovpn_pid the PID of the openvpn process * * @return NO_ERROR on success, or a Windows error code */ static DWORD SetNrptRules(HKEY nrpt_key, const nrpt_address_t *addresses, const char *domains, const char *search_domains, BOOL dnssec, DWORD ovpn_pid) { DWORD err = NO_ERROR; PWSTR wide_domains = L".\0"; /* DNS route everything by default */ DWORD dom_size = 6; /* Prepare DNS routing domains / split DNS */ if (domains[0]) { size_t domains_len = strlen(domains); dom_size = domains_len + 2; /* len + the trailing NULs */ wide_domains = utf8to16_size(domains, dom_size); dom_size *= sizeof(*wide_domains); if (!wide_domains) { return ERROR_OUTOFMEMORY; } /* Make a MULTI_SZ from a comma separated list */ for (size_t i = 0; i < domains_len; ++i) { if (wide_domains[i] == ',') { wide_domains[i] = 0; } } } else { PWSTR wide_search_domains; wide_search_domains = utf8to16(search_domains); if (!wide_search_domains) { return ERROR_OUTOFMEMORY; } SetNrptExcludeRules(nrpt_key, ovpn_pid, wide_search_domains); free(wide_search_domains); } /* Create address string list */ CHAR addr_list[NRPT_ADDR_NUM * NRPT_ADDR_SIZE]; PSTR pos = addr_list; for (int i = 0; i < NRPT_ADDR_NUM && addresses[i][0]; ++i) { if (i != 0) { *pos++ = ';'; } strcpy(pos, addresses[i]); pos += strlen(pos); } WCHAR subkey[MAX_PATH]; swprintf(subkey, _countof(subkey), L"OpenVPNDNSRouting-%lu", ovpn_pid); err = SetNrptRule(nrpt_key, subkey, addr_list, wide_domains, dom_size, dnssec); if (err) { MsgToEventLog(M_ERR, L"%S: failed to set rule %s (%lu)", __func__, subkey, err); } if (domains[0]) { free(wide_domains); } return err; } #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif /** * Return the registry key where NRPT rules are stored * * @param key pointer to the HKEY it is returned in * @param gpol pointer to BOOL the use of GPOL hive is returned in * * @return NO_ERROR on success, or a Windows error code */ static LSTATUS OpenNrptBaseKey(PHKEY key, PBOOL gpol) { /* * Registry keys Name Service Policy Table (NRPT) rules can be stored at. * When the group policy key exists, NRPT rules must be placed there. * It is created when NRPT rules are pushed via group policy and it * remains in the registry even if the last GP-NRPT rule is deleted. */ static PCSTR gpol_key = "SOFTWARE\\Policies\\Microsoft\\Windows NT\\DNSClient\\DnsPolicyConfig"; static PCSTR sys_key = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig"; HKEY nrpt; *gpol = TRUE; LSTATUS err = RegOpenKeyExA(HKEY_LOCAL_MACHINE, gpol_key, 0, KEY_ALL_ACCESS, &nrpt); if (err == ERROR_FILE_NOT_FOUND) { *gpol = FALSE; err = RegCreateKeyExA(HKEY_LOCAL_MACHINE, sys_key, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &nrpt, NULL); if (err) { nrpt = INVALID_HANDLE_VALUE; } } *key = nrpt; return err; } /** * Delete OpenVPN NRPT rules from the registry * * If the pid parameter is 0 all NRPT rules added by OpenVPN are deleted. * In all other cases only rules matching the pid are deleted. * * @param pid PID of the process to delete the rules for or 0 * @param gpol * * @return BOOL to indicate if rules were deleted */ static BOOL DeleteNrptRules(DWORD pid, PBOOL gpol) { HKEY key; LSTATUS err = OpenNrptBaseKey(&key, gpol); if (err) { MsgToEventLog(M_SYSERR, L"%S: could not open NRPT base key (%lu)", __func__, err); return FALSE; } /* PID suffix string to compare against later */ WCHAR pid_str[16]; size_t pidlen = 0; if (pid) { swprintf(pid_str, _countof(pid_str), L"-%lu", pid); pidlen = wcslen(pid_str); } int deleted = 0; DWORD enum_index = 0; while (TRUE) { WCHAR name[MAX_PATH]; DWORD namelen = _countof(name); err = RegEnumKeyExW(key, enum_index++, name, &namelen, NULL, NULL, NULL, NULL); if (err) { if (err != ERROR_NO_MORE_ITEMS) { MsgToEventLog(M_SYSERR, L"%S: could not enumerate NRPT rules (%lu)", __func__, err); } break; } /* Keep rule if name doesn't match */ if (wcsncmp(name, L"OpenVPNDNSRouting", 17) != 0 || (pid && wcsncmp(name + namelen - pidlen, pid_str, pidlen) != 0)) { continue; } if (RegDeleteKeyW(key, name) == NO_ERROR) { enum_index--; deleted++; } } RegCloseKey(key); return deleted ? TRUE : FALSE; } /** * Delete a process' NRPT rules and apply the reduced set of rules * * @param ovpn_pid OpenVPN process id to delete rules for */ static void UndoNrptRules(DWORD ovpn_pid) { BOOL gpol; if (DeleteNrptRules(ovpn_pid, &gpol)) { ApplyDnsSettings(gpol); } } /** * Add Name Resolution Policy Table (NRPT) rules as documented in * https://msdn.microsoft.com/en-us/library/ff957356.aspx for DNS name * resolution, as well as DNS search domain(s), if given. * * @param msg config messages sent by the openvpn process * @param ovpn_pid process id of the sending openvpn process * @param lists undo lists for this process * * @return NO_ERROR on success, or a Windows error code */ static DWORD HandleDNSConfigNrptMessage(const nrpt_dns_cfg_message_t *msg, DWORD ovpn_pid, undo_lists_t *lists) { /* * Use a non-const reference with limited scope to * enforce null-termination of strings from client */ { nrpt_dns_cfg_message_t *msgptr = (nrpt_dns_cfg_message_t *)msg; msgptr->iface.name[_countof(msg->iface.name) - 1] = '\0'; msgptr->search_domains[_countof(msg->search_domains) - 1] = '\0'; msgptr->resolve_domains[_countof(msg->resolve_domains) - 1] = '\0'; for (size_t i = 0; i < NRPT_ADDR_NUM; ++i) { msgptr->addresses[i][_countof(msg->addresses[0]) - 1] = '\0'; } } /* Make sure we have the VPN interface name */ if (msg->iface.name[0] == 0) { return ERROR_MESSAGE_DATA; } /* Some sanity checks on the add message data */ if (msg->header.type == msg_add_nrpt_cfg) { /* At least one name server address is set */ if (msg->addresses[0][0] == 0) { return ERROR_MESSAGE_DATA; } /* Resolve domains are double zero terminated (MULTI_SZ) */ const char *rdom = msg->resolve_domains; size_t rdom_size = sizeof(msg->resolve_domains); size_t rdom_len = strlen(rdom); if (rdom_len && (rdom_len + 1 >= rdom_size || rdom[rdom_len + 2] != 0)) { return ERROR_MESSAGE_DATA; } } BOOL gpol_nrpt = FALSE; BOOL gpol_list = FALSE; WCHAR iid[64]; DWORD iid_err = InterfaceIdString(msg->iface.name, iid, _countof(iid)); if (iid_err) { return iid_err; } /* Delete previously set values for this instance first, if any */ PDWORD undo_pid = RemoveListItem(&(*lists)[undo_nrpt], CmpAny, NULL); if (undo_pid) { if (*undo_pid != ovpn_pid) { MsgToEventLog(M_INFO, L"%S: PID stored for undo doesn't match: %lu vs %lu. " "This is likely an error. Cleaning up anyway.", __func__, *undo_pid, ovpn_pid); } DeleteNrptRules(*undo_pid, &gpol_nrpt); free(undo_pid); ResetNameServers(iid, AF_INET); ResetNameServers(iid, AF_INET6); } SetDnsSearchDomains(msg->iface.name, NULL, &gpol_list, lists); if (msg->header.type == msg_del_nrpt_cfg) { ApplyDnsSettings(gpol_nrpt || gpol_list); return NO_ERROR; /* Done dealing with del message */ } HKEY key; LSTATUS err = OpenNrptBaseKey(&key, &gpol_nrpt); if (err) { goto out; } /* Add undo information first in case there's no heap left */ PDWORD pid = malloc(sizeof(ovpn_pid)); if (!pid) { err = ERROR_OUTOFMEMORY; goto out; } *pid = ovpn_pid; if (AddListItem(&(*lists)[undo_nrpt], pid)) { err = ERROR_OUTOFMEMORY; free(pid); goto out; } /* Set NRPT rules */ BOOL dnssec = (msg->flags & nrpt_dnssec) != 0; err = SetNrptRules(key, msg->addresses, msg->resolve_domains, msg->search_domains, dnssec, ovpn_pid); if (err) { goto out; } /* Set name servers */ err = SetNameServerAddresses(iid, msg->addresses); if (err) { goto out; } /* Set search domains, if any */ if (msg->search_domains[0]) { err = SetDnsSearchDomains(msg->iface.name, msg->search_domains, &gpol_list, lists); } ApplyDnsSettings(gpol_nrpt || gpol_list); out: return err; } static DWORD HandleWINSConfigMessage(const wins_cfg_message_t *msg, undo_lists_t *lists) { DWORD err = 0; wchar_t addr[16]; /* large enough to hold string representation of an ipv4 */ int addr_len = msg->addr_len; /* sanity check */ if (addr_len > _countof(msg->addr)) { addr_len = _countof(msg->addr); } if (!msg->iface.name[0]) /* interface name is required */ { return ERROR_MESSAGE_DATA; } /* use a non-const reference with limited scope to enforce null-termination of strings from * client */ { wins_cfg_message_t *msgptr = (wins_cfg_message_t *)msg; msgptr->iface.name[_countof(msg->iface.name) - 1] = '\0'; } wchar_t *wide_name = utf8to16(msg->iface.name); /* utf8 to wide-char */ if (!wide_name) { return ERROR_OUTOFMEMORY; } /* We delete all current addresses before adding any * OR if the message type is del_wins_cfg */ if (addr_len > 0 || msg->header.type == msg_del_wins_cfg) { err = netsh_wins_cmd(L"delete", wide_name, NULL); if (err) { goto out; } free(RemoveListItem(&(*lists)[undo_wins], CmpWString, wide_name)); } if (msg->header.type == msg_del_wins_cfg) { goto out; /* job done */ } for (int i = 0; i < addr_len; ++i) { RtlIpv4AddressToStringW(&msg->addr[i].ipv4, addr); err = netsh_wins_cmd(i == 0 ? L"set" : L"add", wide_name, addr); if (i == 0 && err) { goto out; } /* We do not check for duplicate addresses, so any error in adding * additional addresses is ignored. */ } err = 0; if (addr_len > 0) { wchar_t *tmp_name = _wcsdup(wide_name); if (!tmp_name || AddListItem(&(*lists)[undo_wins], tmp_name)) { free(tmp_name); netsh_wins_cmd(L"delete", wide_name, NULL); err = ERROR_OUTOFMEMORY; goto out; } } out: free(wide_name); return err; } static DWORD HandleEnableDHCPMessage(const enable_dhcp_message_t *dhcp) { DWORD err = 0; DWORD timeout = 5000; /* in milli seconds */ wchar_t argv0[MAX_PATH]; /* Path of netsh */ swprintf(argv0, _countof(argv0), L"%ls\\%ls", get_win_sys_path(), L"netsh.exe"); /* cmd template: * netsh interface ipv4 set address name=$if_index source=dhcp */ const wchar_t *fmt = L"netsh interface ipv4 set address name=\"%d\" source=dhcp"; /* max cmdline length in wchars -- include room for if index: * 10 chars for 32 bit int in decimal and +1 for NUL */ size_t ncmdline = wcslen(fmt) + 10 + 1; wchar_t *cmdline = malloc(ncmdline * sizeof(wchar_t)); if (!cmdline) { err = ERROR_OUTOFMEMORY; return err; } swprintf(cmdline, ncmdline, fmt, dhcp->iface.index); err = ExecCommand(argv0, cmdline, timeout); /* Note: This could fail if dhcp is already enabled, so the caller * may not want to treat errors as FATAL. */ free(cmdline); return err; } static DWORD HandleMTUMessage(const set_mtu_message_t *mtu) { DWORD err = 0; MIB_IPINTERFACE_ROW ipiface; InitializeIpInterfaceEntry(&ipiface); ipiface.Family = mtu->family; ipiface.InterfaceIndex = mtu->iface.index; err = GetIpInterfaceEntry(&ipiface); if (err != NO_ERROR) { return err; } if (mtu->family == AF_INET) { ipiface.SitePrefixLength = 0; } ipiface.NlMtu = mtu->mtu; err = SetIpInterfaceEntry(&ipiface); return err; } /** * Creates a VPN adapter of the specified type by invoking tapctl.exe. * * @param msg Adapter creation request specifying the type. * * @return NO_ERROR on success, otherwise a Windows error code. */ static DWORD HandleCreateAdapterMessage(const create_adapter_message_t *msg) { const WCHAR *hwid; switch (msg->adapter_type) { case ADAPTER_TYPE_DCO: hwid = L"ovpn-dco"; break; case ADAPTER_TYPE_TAP: hwid = L"root\\tap0901"; break; default: return ERROR_INVALID_PARAMETER; } WCHAR cmd[MAX_PATH]; WCHAR args[MAX_PATH]; if (swprintf_s(cmd, _countof(cmd), L"%s\\tapctl.exe", settings.bin_dir) < 0) { return ERROR_BUFFER_OVERFLOW; } if (swprintf_s(args, _countof(args), L"tapctl create --hwid %s", hwid) < 0) { return ERROR_BUFFER_OVERFLOW; } return ExecCommand(cmd, args, 10000); } static VOID HandleMessage(HANDLE pipe, PPROCESS_INFORMATION proc_info, DWORD bytes, DWORD count, LPHANDLE events, undo_lists_t *lists) { pipe_message_t msg; ack_message_t ack = { .header = { .type = msg_acknowledgement, .size = sizeof(ack), .message_id = -1 }, .error_number = ERROR_MESSAGE_DATA }; DWORD read = ReadPipeAsync(pipe, &msg, bytes, count, events); if (read != bytes || read < sizeof(msg.header) || read != msg.header.size) { goto out; } ack.header.message_id = msg.header.message_id; switch (msg.header.type) { case msg_add_address: case msg_del_address: if (msg.header.size == sizeof(msg.address)) { ack.error_number = HandleAddressMessage(&msg.address, lists); } break; case msg_add_route: case msg_del_route: if (msg.header.size == sizeof(msg.route)) { ack.error_number = HandleRouteMessage(&msg.route, lists); } break; case msg_flush_neighbors: if (msg.header.size == sizeof(msg.flush_neighbors)) { ack.error_number = HandleFlushNeighborsMessage(&msg.flush_neighbors); } break; case msg_add_wfp_block: case msg_del_wfp_block: if (msg.header.size == sizeof(msg.wfp_block)) { ack.error_number = HandleWfpBlockMessage(&msg.wfp_block, lists); } break; case msg_register_dns: ack.error_number = HandleRegisterDNSMessage(); break; case msg_add_dns_cfg: case msg_del_dns_cfg: ack.error_number = HandleDNSConfigMessage(&msg.dns, lists); break; case msg_add_nrpt_cfg: case msg_del_nrpt_cfg: { DWORD ovpn_pid = proc_info->dwProcessId; ack.error_number = HandleDNSConfigNrptMessage(&msg.nrpt_dns, ovpn_pid, lists); } break; case msg_add_wins_cfg: case msg_del_wins_cfg: ack.error_number = HandleWINSConfigMessage(&msg.wins, lists); break; case msg_enable_dhcp: if (msg.header.size == sizeof(msg.dhcp)) { ack.error_number = HandleEnableDHCPMessage(&msg.dhcp); } break; case msg_set_mtu: if (msg.header.size == sizeof(msg.mtu)) { ack.error_number = HandleMTUMessage(&msg.mtu); } break; case msg_create_adapter: if (msg.header.size == sizeof(msg.create_adapter)) { ack.error_number = HandleCreateAdapterMessage(&msg.create_adapter); } break; default: ack.error_number = ERROR_MESSAGE_TYPE; MsgToEventLog(MSG_FLAGS_ERROR, L"Unknown message type %d", msg.header.type); break; } out: WritePipeAsync(pipe, &ack, sizeof(ack), count, events); } static VOID Undo(undo_lists_t *lists) { undo_type_t type; wfp_block_data_t *interface_data; for (type = 0; type < _undo_type_max; type++) { list_item_t **pnext = &(*lists)[type]; while (*pnext) { list_item_t *item = *pnext; switch (type) { case address: DeleteAddress(item->data); break; case route: DeleteRoute(item->data); break; case undo_dns4: ResetNameServers(item->data, AF_INET); break; case undo_dns6: ResetNameServers(item->data, AF_INET6); break; case undo_nrpt: UndoNrptRules(*(PDWORD)item->data); break; case undo_domains: UndoDnsSearchDomains(item->data); break; case undo_wins: netsh_wins_cmd(L"delete", item->data, NULL); break; case wfp_block: interface_data = (wfp_block_data_t *)(item->data); delete_wfp_block_filters(interface_data->engine); if (interface_data->metric_v4 >= 0) { set_interface_metric(interface_data->index, AF_INET, interface_data->metric_v4); } if (interface_data->metric_v6 >= 0) { set_interface_metric(interface_data->index, AF_INET6, interface_data->metric_v6); } break; case _undo_type_max: /* unreachable */ break; } /* Remove from the list and free memory */ *pnext = item->next; free(item->data); free(item); } } } static DWORD WINAPI RunOpenvpn(LPVOID p) { HANDLE pipe = p; HANDLE ovpn_pipe = NULL, svc_pipe = NULL; PTOKEN_USER svc_user = NULL, ovpn_user = NULL; HANDLE svc_token = NULL, imp_token = NULL, pri_token = NULL; HANDLE stdin_read = NULL, stdin_write = NULL; HANDLE stdout_write = NULL; DWORD pipe_mode, len, exit_code = 0; STARTUP_DATA sud = { 0, 0, 0 }; STARTUPINFOW startup_info; PROCESS_INFORMATION proc_info; LPVOID user_env = NULL; WCHAR ovpn_pipe_name[256]; /* The entire pipe name string can be up to 256 characters long according to MSDN. */ LPCWSTR exe_path; WCHAR *cmdline = NULL; size_t cmdline_size; undo_lists_t undo_lists; WCHAR errmsg[512] = L""; SECURITY_ATTRIBUTES inheritable = { .nLength = sizeof(inheritable), .lpSecurityDescriptor = NULL, .bInheritHandle = TRUE }; PACL ovpn_dacl; EXPLICIT_ACCESS ea[2]; SECURITY_DESCRIPTOR ovpn_sd; SECURITY_ATTRIBUTES ovpn_sa = { .nLength = sizeof(ovpn_sa), .lpSecurityDescriptor = &ovpn_sd, .bInheritHandle = FALSE }; ZeroMemory(&ea, sizeof(ea)); ZeroMemory(&startup_info, sizeof(startup_info)); ZeroMemory(&undo_lists, sizeof(undo_lists)); ZeroMemory(&proc_info, sizeof(proc_info)); if (!GetStartupData(pipe, &sud)) { goto out; } if (!InitializeSecurityDescriptor(&ovpn_sd, SECURITY_DESCRIPTOR_REVISION)) { ReturnLastError(pipe, L"InitializeSecurityDescriptor"); goto out; } /* Get SID of user the service is running under */ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &svc_token)) { ReturnLastError(pipe, L"OpenProcessToken"); goto out; } len = 0; while (!GetTokenInformation(svc_token, TokenUser, svc_user, len, &len)) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { ReturnLastError(pipe, L"GetTokenInformation (service token)"); goto out; } free(svc_user); svc_user = malloc(len); if (svc_user == NULL) { ReturnLastError(pipe, L"malloc (service token user)"); goto out; } } if (!IsValidSid(svc_user->User.Sid)) { ReturnLastError(pipe, L"IsValidSid (service token user)"); goto out; } if (!ImpersonateNamedPipeClient(pipe)) { ReturnLastError(pipe, L"ImpersonateNamedPipeClient"); goto out; } if (!OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &imp_token)) { ReturnLastError(pipe, L"OpenThreadToken"); goto out; } len = 0; while (!GetTokenInformation(imp_token, TokenUser, ovpn_user, len, &len)) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { ReturnLastError(pipe, L"GetTokenInformation (impersonation token)"); goto out; } free(ovpn_user); ovpn_user = malloc(len); if (ovpn_user == NULL) { ReturnLastError(pipe, L"malloc (impersonation token user)"); goto out; } } if (!IsValidSid(ovpn_user->User.Sid)) { ReturnLastError(pipe, L"IsValidSid (impersonation token user)"); goto out; } /* * Only authorized users are allowed to use any command line options or * have the config file in locations other than the global config directory. * * Check options are white-listed and config is in the global directory * OR user is authorized to run any config. */ if (!ValidateOptions(pipe, sud.directory, sud.options, errmsg, _countof(errmsg)) && !IsAuthorizedUser(ovpn_user->User.Sid, imp_token, settings.ovpn_admin_group, settings.ovpn_service_user)) { ReturnError(pipe, ERROR_STARTUP_DATA, errmsg, 1, &exit_event); goto out; } /* OpenVPN process DACL entry for access by service and user */ ea[0].grfAccessPermissions = SPECIFIC_RIGHTS_ALL | STANDARD_RIGHTS_ALL; ea[0].grfAccessMode = SET_ACCESS; ea[0].grfInheritance = NO_INHERITANCE; ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[0].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN; ea[0].Trustee.ptstrName = (LPWSTR)svc_user->User.Sid; ea[1].grfAccessPermissions = READ_CONTROL | SYNCHRONIZE | PROCESS_VM_READ | SYNCHRONIZE | PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION; ea[1].grfAccessMode = SET_ACCESS; ea[1].grfInheritance = NO_INHERITANCE; ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[1].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN; ea[1].Trustee.ptstrName = (LPWSTR)ovpn_user->User.Sid; /* Set owner and DACL of OpenVPN security descriptor */ if (!SetSecurityDescriptorOwner(&ovpn_sd, svc_user->User.Sid, FALSE)) { ReturnLastError(pipe, L"SetSecurityDescriptorOwner"); goto out; } if (SetEntriesInAcl(2, ea, NULL, &ovpn_dacl) != ERROR_SUCCESS) { ReturnLastError(pipe, L"SetEntriesInAcl"); goto out; } if (!SetSecurityDescriptorDacl(&ovpn_sd, TRUE, ovpn_dacl, FALSE)) { ReturnLastError(pipe, L"SetSecurityDescriptorDacl"); goto out; } /* Create primary token from impersonation token */ if (!DuplicateTokenEx(imp_token, TOKEN_ALL_ACCESS, NULL, 0, TokenPrimary, &pri_token)) { ReturnLastError(pipe, L"DuplicateTokenEx"); goto out; } /* use /dev/null for stdout of openvpn (client should use --log for output) */ stdout_write = CreateFile(_L("NUL"), GENERIC_WRITE, FILE_SHARE_WRITE, &inheritable, OPEN_EXISTING, 0, NULL); if (stdout_write == INVALID_HANDLE_VALUE) { ReturnLastError(pipe, L"CreateFile for stdout"); goto out; } if (!CreatePipe(&stdin_read, &stdin_write, &inheritable, 0) || !SetHandleInformation(stdin_write, HANDLE_FLAG_INHERIT, 0)) { ReturnLastError(pipe, L"CreatePipe"); goto out; } swprintf(ovpn_pipe_name, _countof(ovpn_pipe_name), L"\\\\.\\pipe\\" _L(PACKAGE) L"%ls\\service_%lu", service_instance, GetCurrentThreadId()); ovpn_pipe = CreateNamedPipe( ovpn_pipe_name, PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 128, 128, 0, NULL); if (ovpn_pipe == INVALID_HANDLE_VALUE) { ReturnLastError(pipe, L"CreateNamedPipe"); goto out; } svc_pipe = CreateFile(ovpn_pipe_name, GENERIC_READ | GENERIC_WRITE, 0, &inheritable, OPEN_EXISTING, 0, NULL); if (svc_pipe == INVALID_HANDLE_VALUE) { ReturnLastError(pipe, L"CreateFile"); goto out; } pipe_mode = PIPE_READMODE_MESSAGE; if (!SetNamedPipeHandleState(svc_pipe, &pipe_mode, NULL, NULL)) { ReturnLastError(pipe, L"SetNamedPipeHandleState"); goto out; } cmdline_size = wcslen(sud.options) + 128; cmdline = malloc(cmdline_size * sizeof(*cmdline)); if (cmdline == NULL) { ReturnLastError(pipe, L"malloc"); goto out; } /* there seem to be no common printf specifier that works on all * mingw/msvc platforms without trickery, so convert to void* and use * PRIuPTR to print that as best compromise */ swprintf(cmdline, cmdline_size, L"openvpn %ls --msg-channel %" PRIuPTR, sud.options, (uintptr_t)svc_pipe); if (!CreateEnvironmentBlock(&user_env, imp_token, FALSE)) { ReturnLastError(pipe, L"CreateEnvironmentBlock"); goto out; } startup_info.cb = sizeof(startup_info); startup_info.dwFlags = STARTF_USESTDHANDLES; startup_info.hStdInput = stdin_read; startup_info.hStdOutput = stdout_write; startup_info.hStdError = stdout_write; exe_path = settings.exe_path; /* TODO: make sure HKCU is correct or call LoadUserProfile() */ if (!CreateProcessAsUserW(pri_token, exe_path, cmdline, &ovpn_sa, NULL, TRUE, settings.priority | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, user_env, sud.directory, &startup_info, &proc_info)) { ReturnLastError(pipe, L"CreateProcessAsUser"); goto out; } if (!RevertToSelf()) { TerminateProcess(proc_info.hProcess, 1); ReturnLastError(pipe, L"RevertToSelf"); goto out; } ReturnProcessId(pipe, proc_info.dwProcessId, 1, &exit_event); CloseHandleEx(&stdout_write); CloseHandleEx(&stdin_read); CloseHandleEx(&svc_pipe); DWORD input_size = WideCharToMultiByte(CP_UTF8, 0, sud.std_input, -1, NULL, 0, NULL, NULL); LPSTR input = NULL; if (input_size && (input = malloc(input_size))) { DWORD written; WideCharToMultiByte(CP_UTF8, 0, sud.std_input, -1, input, input_size, NULL, NULL); WriteFile(stdin_write, input, (DWORD)strlen(input), &written, NULL); free(input); } while (TRUE) { DWORD bytes = PeekNamedPipeAsync(ovpn_pipe, 1, &exit_event); if (bytes == 0) { break; } if (bytes > sizeof(pipe_message_t)) { /* process at the other side of the pipe is misbehaving, shut it down */ MsgToEventLog( MSG_FLAGS_ERROR, L"OpenVPN process sent too large payload length to the pipe (%lu bytes), it will be terminated", bytes); break; } HandleMessage(ovpn_pipe, &proc_info, bytes, 1, &exit_event, &undo_lists); } WaitForSingleObject(proc_info.hProcess, IO_TIMEOUT); GetExitCodeProcess(proc_info.hProcess, &exit_code); if (exit_code == STILL_ACTIVE) { TerminateProcess(proc_info.hProcess, 1); } else if (exit_code != 0) { WCHAR buf[256]; swprintf(buf, _countof(buf), L"OpenVPN exited with error: exit code = %lu", exit_code); ReturnError(pipe, ERROR_OPENVPN_STARTUP, buf, 1, &exit_event); } Undo(&undo_lists); out: FlushFileBuffers(pipe); DisconnectNamedPipe(pipe); free(ovpn_user); free(svc_user); free(cmdline); DestroyEnvironmentBlock(user_env); FreeStartupData(&sud); CloseHandleEx(&proc_info.hProcess); CloseHandleEx(&proc_info.hThread); CloseHandleEx(&stdin_read); CloseHandleEx(&stdin_write); CloseHandleEx(&stdout_write); CloseHandleEx(&svc_token); CloseHandleEx(&imp_token); CloseHandleEx(&pri_token); CloseHandleEx(&ovpn_pipe); CloseHandleEx(&svc_pipe); CloseHandleEx(&pipe); return 0; } static DWORD WINAPI ServiceCtrlInteractive(DWORD ctrl_code, DWORD event, LPVOID data, LPVOID ctx) { SERVICE_STATUS *status = ctx; switch (ctrl_code) { case SERVICE_CONTROL_STOP: status->dwCurrentState = SERVICE_STOP_PENDING; ReportStatusToSCMgr(service, status); if (exit_event) { SetEvent(exit_event); } return NO_ERROR; case SERVICE_CONTROL_INTERROGATE: return NO_ERROR; default: return ERROR_CALL_NOT_IMPLEMENTED; } } static HANDLE CreateClientPipeInstance(VOID) { /* * allow all access for local system * deny FILE_CREATE_PIPE_INSTANCE for everyone * allow read/write for authenticated users * deny all access to anonymous */ const WCHAR *sddlString = L"D:(A;OICI;GA;;;S-1-5-18)(D;OICI;0x4;;;S-1-1-0)(A;OICI;GRGW;;;S-1-5-11)(D;;GA;;;S-1-5-7)"; PSECURITY_DESCRIPTOR sd = NULL; if (!ConvertStringSecurityDescriptorToSecurityDescriptor(sddlString, SDDL_REVISION_1, &sd, NULL)) { MsgToEventLog(M_SYSERR, L"ConvertStringSecurityDescriptorToSecurityDescriptor failed."); return INVALID_HANDLE_VALUE; } /* Set up SECURITY_ATTRIBUTES */ SECURITY_ATTRIBUTES sa = { 0 }; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = sd; sa.bInheritHandle = FALSE; DWORD flags = PIPE_ACCESS_DUPLEX | WRITE_DAC | FILE_FLAG_OVERLAPPED; static BOOL first = TRUE; if (first) { flags |= FILE_FLAG_FIRST_PIPE_INSTANCE; first = FALSE; } WCHAR pipe_name[256]; /* The entire pipe name string can be up to 256 characters long according to MSDN. */ swprintf(pipe_name, _countof(pipe_name), L"\\\\.\\pipe\\" _L(PACKAGE) L"%ls\\service", service_instance); HANDLE pipe = CreateNamedPipe( pipe_name, flags, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_REJECT_REMOTE_CLIENTS, PIPE_UNLIMITED_INSTANCES, 1024, 1024, 0, &sa); LocalFree(sd); if (pipe == INVALID_HANDLE_VALUE) { MsgToEventLog(M_SYSERR, L"Could not create named pipe"); return INVALID_HANDLE_VALUE; } return pipe; } static DWORD UpdateWaitHandles(LPHANDLE *handles_ptr, LPDWORD count, HANDLE io_event, HANDLE exit_event, list_item_t *threads) { static DWORD size = 10; static LPHANDLE handles = NULL; DWORD pos = 0; if (handles == NULL) { handles = malloc(size * sizeof(HANDLE)); *handles_ptr = handles; if (handles == NULL) { return ERROR_OUTOFMEMORY; } } handles[pos++] = io_event; if (!threads) { handles[pos++] = exit_event; } while (threads) { if (pos == size) { LPHANDLE tmp; size += 10; tmp = realloc(handles, size * sizeof(HANDLE)); if (tmp == NULL) { size -= 10; *count = pos; return ERROR_OUTOFMEMORY; } handles = tmp; *handles_ptr = handles; } handles[pos++] = threads->data; threads = threads->next; } *count = pos; return NO_ERROR; } static VOID FreeWaitHandles(LPHANDLE h) { free(h); } static BOOL CmpHandle(LPVOID item, LPVOID hnd) { return item == hnd; } VOID WINAPI ServiceStartInteractiveOwn(DWORD dwArgc, LPWSTR *lpszArgv) { status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; ServiceStartInteractive(dwArgc, lpszArgv); } /** * Clean up remains of previous sessions in registry. These remains can * happen with unclean shutdowns or crashes and would interfere with * normal operation of the system with and without active tunnels. */ static void CleanupRegistry(void) { BOOL changed = FALSE; /* Clean up leftover NRPT rules */ BOOL gpol_nrpt; changed = DeleteNrptRules(0, &gpol_nrpt); /* Clean up leftover DNS search list fragments */ HKEY key; BOOL gpol_list; GetDnsSearchListKey(NULL, &gpol_list, &key); if (key != INVALID_HANDLE_VALUE) { if (ResetDnsSearchDomains(key)) { changed = TRUE; } RegCloseKey(key); } if (changed) { ApplyDnsSettings(gpol_nrpt || gpol_list); } } VOID WINAPI ServiceStartInteractive(DWORD dwArgc, LPWSTR *lpszArgv) { HANDLE pipe, io_event = NULL; OVERLAPPED overlapped; DWORD error = NO_ERROR; list_item_t *threads = NULL; PHANDLE handles = NULL; DWORD handle_count; service = RegisterServiceCtrlHandlerEx(interactive_service.name, ServiceCtrlInteractive, &status); if (!service) { return; } status.dwCurrentState = SERVICE_START_PENDING; status.dwServiceSpecificExitCode = NO_ERROR; status.dwWin32ExitCode = NO_ERROR; status.dwWaitHint = 3000; ReportStatusToSCMgr(service, &status); /* Clean up potentially left over registry values */ CleanupRegistry(); /* Read info from registry in key HKLM\SOFTWARE\OpenVPN */ error = GetOpenvpnSettings(&settings); if (error != ERROR_SUCCESS) { goto out; } io_event = InitOverlapped(&overlapped); exit_event = CreateEvent(NULL, TRUE, FALSE, NULL); if (!exit_event || !io_event) { error = MsgToEventLog(M_SYSERR, L"Could not create event"); goto out; } rdns_semaphore = CreateSemaphoreW(NULL, 1, 1, NULL); if (!rdns_semaphore) { error = MsgToEventLog(M_SYSERR, L"Could not create semaphore for register-dns"); goto out; } error = UpdateWaitHandles(&handles, &handle_count, io_event, exit_event, threads); if (error != NO_ERROR) { goto out; } pipe = CreateClientPipeInstance(); if (pipe == INVALID_HANDLE_VALUE) { goto out; } status.dwCurrentState = SERVICE_RUNNING; status.dwWaitHint = 0; ReportStatusToSCMgr(service, &status); while (TRUE) { if (ConnectNamedPipe(pipe, &overlapped) == FALSE && GetLastError() != ERROR_PIPE_CONNECTED && GetLastError() != ERROR_IO_PENDING) { MsgToEventLog(M_SYSERR, L"Could not connect pipe"); break; } error = WaitForMultipleObjects(handle_count, handles, FALSE, INFINITE); if (error == WAIT_OBJECT_0) { /* Client connected, spawn a worker thread for it */ HANDLE next_pipe = CreateClientPipeInstance(); HANDLE thread = CreateThread(NULL, 0, RunOpenvpn, pipe, CREATE_SUSPENDED, NULL); if (thread) { error = AddListItem(&threads, thread); if (!error) { error = UpdateWaitHandles(&handles, &handle_count, io_event, exit_event, threads); } if (error) { ReturnError(pipe, error, L"Insufficient resources to service new clients", 1, &exit_event); /* Update wait handles again after removing the last worker thread */ RemoveListItem(&threads, CmpHandle, thread); UpdateWaitHandles(&handles, &handle_count, io_event, exit_event, threads); TerminateThread(thread, 1); CloseHandleEx(&thread); CloseHandleEx(&pipe); } else { ResumeThread(thread); } } else { CloseHandleEx(&pipe); } ResetOverlapped(&overlapped); pipe = next_pipe; } else { CancelIo(pipe); if (error == WAIT_FAILED) { MsgToEventLog(M_SYSERR, L"WaitForMultipleObjects failed"); SetEvent(exit_event); /* Give some time for worker threads to exit and then terminate */ Sleep(1000); break; } if (!threads) { /* exit event signaled */ CloseHandleEx(&pipe); ResetEvent(exit_event); error = NO_ERROR; break; } /* Worker thread ended */ HANDLE thread = RemoveListItem(&threads, CmpHandle, handles[error]); UpdateWaitHandles(&handles, &handle_count, io_event, exit_event, threads); CloseHandleEx(&thread); } } out: FreeWaitHandles(handles); CloseHandleEx(&io_event); CloseHandleEx(&exit_event); CloseHandleEx(&rdns_semaphore); status.dwCurrentState = SERVICE_STOPPED; status.dwWin32ExitCode = error; ReportStatusToSCMgr(service, &status); }