/*
 *  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 Heiko Hund <heiko.hund@sophos.com>
 *
 *  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 (see the file COPYING included with this
 *  distribution); if not, write to the Free Software Foundation, Inc.,
 *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


#include "service.h"

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <userenv.h>
#include <accctrl.h>
#include <aclapi.h>
#include <stdio.h>
#include <sddl.h>
#include <shellapi.h>

#include "openvpn-msg.h"
#include "validate.h"
#include "block_dns.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;
static HANDLE exit_event = NULL;
static settings_t settings;
static HANDLE rdns_semaphore = NULL;
#define RDNS_TIMEOUT 600  /* seconds to wait for the semaphore */


openvpn_service_t interactive_service = {
  interactive,
  TEXT(PACKAGE_NAME "ServiceInteractive"),
  TEXT(PACKAGE_NAME " Interactive Service"),
  TEXT(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,
  block_dns,
  _undo_type_max
} undo_type_t;
typedef list_item_t* undo_lists_t[_undo_type_max];


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)
{
  int 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
   */
  _snwprintf (buf, _countof(buf), L"0x%08x\n0x%08x\n%s", 0, pid, msg);
  buf[_countof(buf) - 1] = '\0';

  WritePipeAsync (pipe, buf, 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, wcslen (result) * 2, count, events);
#ifdef UNICODE
  MsgToEventLog (MSG_FLAGS_ERROR, result);
#else
  MsgToEventLog (MSG_FLAGS_ERROR, "%S", result);
#endif

  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);
}


static VOID
ReturnOpenvpnOutput (HANDLE pipe, HANDLE ovpn_output, DWORD count, LPHANDLE events)
{
  WCHAR *wide_output = NULL;
  CHAR output[512];
  DWORD size;

  ReadFile (ovpn_output, output, sizeof (output), &size, NULL);
  if (size == 0)
    return;

  wide_output = malloc ((size) * sizeof (WCHAR));
  if (wide_output)
    {
      MultiByteToWideChar (CP_UTF8, 0, output, size, wide_output, size);
      wide_output[size - 1] = 0;
    }

  ReturnError (pipe, ERROR_OPENVPN_STARTUP, wide_output, count, events);
  free (wide_output);
}

/*
 * 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
 */
static BOOL
ValidateOptions (HANDLE pipe, const WCHAR *workdir, const WCHAR *options)
{
    WCHAR **argv;
    int argc;
    WCHAR buf[256];
    BOOL ret = FALSE;
    int i;
    const WCHAR *msg1 = L"You have specified a config file location (%s relative to %s)"
                        " that requires admin approval. This error may be avoided"
                        " by adding your account to the \"%s\" group";

    const WCHAR *msg2 = L"You have specified an option (%s) that may be used"
                         " only with admin approval. This error may be avoided"
                         " by adding your account to the \"%s\" group";

    argv = CommandLineToArgvW (options, &argc);

    if (!argv)
    {
        ReturnLastError (pipe, L"CommandLineToArgvW");
        ReturnError (pipe, ERROR_STARTUP_DATA, L"Cannot validate options", 1, &exit_event);
        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))
        {
            snwprintf (buf, _countof(buf), msg1, argv[0], workdir,
                       settings.ovpn_admin_group);
            buf[_countof(buf) - 1] = L'\0';
            ReturnError (pipe, ERROR_STARTUP_DATA, buf, 1, &exit_event);
        }
        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)
            {
                snwprintf (buf, _countof(buf), msg1, argv[i+1], workdir,
                            settings.ovpn_admin_group);
                buf[_countof(buf) - 1] = L'\0';
                ReturnError (pipe, ERROR_STARTUP_DATA, buf, 1, &exit_event);
            }
            else
            {
                snwprintf (buf, _countof(buf), msg2, argv[i],
                           settings.ovpn_admin_group);
                buf[_countof(buf) - 1] = L'\0';
                ReturnError (pipe, ERROR_STARTUP_DATA, buf, 1, &exit_event);
            }
            goto out;
        }
    }

    /* all options passed */
    ret = TRUE;

out:
    if (argv)
        LocalFree (argv);
    return ret;
}

static BOOL
GetStartupData (HANDLE pipe, STARTUP_DATA *sud)
{
  size_t len;
  BOOL ret = FALSE;
  WCHAR *data = NULL;
  DWORD size, bytes, read;

  bytes = PeekNamedPipeAsync (pipe, 1, &exit_event);
  if (bytes == 0)
    {
      MsgToEventLog (M_SYSERR, TEXT("PeekNamedPipeAsync failed"));
      ReturnLastError (pipe, L"PeekNamedPipeAsync");
      goto out;
    }

  size = bytes / sizeof (*data);
  data = malloc (bytes);
  if (data == NULL)
    {
      MsgToEventLog (M_SYSERR, TEXT("malloc failed"));
      ReturnLastError (pipe, L"malloc");
      goto out;
    }

  read = ReadPipeAsync (pipe, data, bytes, 1, &exit_event);
  if (bytes != read)
  {
      MsgToEventLog (M_SYSERR, TEXT("ReadPipeAsync failed"));
      ReturnLastError (pipe, L"ReadPipeAsync");
      goto out;
  }

  if (data[size - 1] != 0)
    {
      MsgToEventLog (M_ERR, TEXT("Startup data is not NULL terminated"));
      ReturnError (pipe, ERROR_STARTUP_DATA, L"GetStartupData", 1, &exit_event);
      goto out;
    }

  sud->directory = data;
  len = wcslen (sud->directory) + 1;
  size -= len;
  if (size <= 0)
    {
      MsgToEventLog (M_ERR, TEXT("Startup data ends at working directory"));
      ReturnError (pipe, ERROR_STARTUP_DATA, L"GetStartupData", 1, &exit_event);
      goto out;
    }

  sud->options = sud->directory + len;
  len = wcslen (sud->options) + 1;
  size -= len;
  if (size <= 0)
    {
      MsgToEventLog (M_ERR, TEXT("Startup data ends at command line options"));
      ReturnError (pipe, ERROR_STARTUP_DATA, L"GetStartupData", 1, &exit_event);
      goto out;
    }

  sud->std_input = sud->options + len;
  data = NULL; /* don't free data */
  ret = TRUE;

out:
  free (data);
  return ret;
}


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;
  int n;

  typedef NETIO_STATUS WINAPI (*ConvertInterfaceAliasToLuidFn) (LPCWSTR, PNET_LUID);
  static ConvertInterfaceAliasToLuidFn ConvertInterfaceAliasToLuid = NULL;
  if (!ConvertInterfaceAliasToLuid)
    {
      HMODULE iphlpapi = GetModuleHandle (TEXT("iphlpapi.dll"));
      if (iphlpapi == NULL)
        return GetLastError ();

      ConvertInterfaceAliasToLuid = (ConvertInterfaceAliasToLuidFn) GetProcAddress (iphlpapi, "ConvertInterfaceAliasToLuid");
      if (!ConvertInterfaceAliasToLuid)
        return GetLastError ();
    }

  n = MultiByteToWideChar (CP_UTF8, 0, iface_name, -1, NULL, 0);
  wide_name = malloc (n * sizeof (WCHAR));
  MultiByteToWideChar (CP_UTF8, 0, iface_name, -1, wide_name, n);
  status = ConvertInterfaceAliasToLuid (wide_name, luid);
  free (wide_name);

  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)
{
  typedef NETIOAPI_API (*DeleteUnicastIpAddressEntryFn) (const PMIB_UNICASTIPADDRESS_ROW);
  static DeleteUnicastIpAddressEntryFn DeleteUnicastIpAddressEntry = NULL;

  if (!DeleteUnicastIpAddressEntry)
    {
      HMODULE iphlpapi = GetModuleHandle (TEXT("iphlpapi.dll"));
      if (iphlpapi == NULL)
        return GetLastError ();

      DeleteUnicastIpAddressEntry = (DeleteUnicastIpAddressEntryFn) GetProcAddress (iphlpapi, "DeleteUnicastIpAddressEntry");
      if (!DeleteUnicastIpAddressEntry)
        return GetLastError ();
    }

  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;

  typedef NETIOAPI_API (*CreateUnicastIpAddressEntryFn) (const PMIB_UNICASTIPADDRESS_ROW);
  typedef NETIOAPI_API (*InitializeUnicastIpAddressEntryFn) (PMIB_UNICASTIPADDRESS_ROW);
  static CreateUnicastIpAddressEntryFn CreateUnicastIpAddressEntry = NULL;
  static InitializeUnicastIpAddressEntryFn InitializeUnicastIpAddressEntry = NULL;

  if (!CreateUnicastIpAddressEntry || !InitializeUnicastIpAddressEntry)
    {
      HMODULE iphlpapi = GetModuleHandle (TEXT("iphlpapi.dll"));
      if (iphlpapi == NULL)
        return GetLastError ();

      CreateUnicastIpAddressEntry = (CreateUnicastIpAddressEntryFn) GetProcAddress (iphlpapi, "CreateUnicastIpAddressEntry");
      if (!CreateUnicastIpAddressEntry)
        return GetLastError ();

      InitializeUnicastIpAddressEntry = (InitializeUnicastIpAddressEntryFn) GetProcAddress (iphlpapi, "InitializeUnicastIpAddressEntry");
      if (!InitializeUnicastIpAddressEntry)
        return GetLastError ();
    }

  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)
{
  typedef NETIOAPI_API (*DeleteIpForwardEntry2Fn) (PMIB_IPFORWARD_ROW2);
  static DeleteIpForwardEntry2Fn DeleteIpForwardEntry2 = NULL;

  if (!DeleteIpForwardEntry2)
    {
      HMODULE iphlpapi = GetModuleHandle (TEXT("iphlpapi.dll"));
      if (iphlpapi == NULL)
        return GetLastError ();

      DeleteIpForwardEntry2 = (DeleteIpForwardEntry2Fn) GetProcAddress (iphlpapi, "DeleteIpForwardEntry2");
      if (!DeleteIpForwardEntry2)
        return GetLastError ();
    }

  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;

  typedef NETIOAPI_API (*CreateIpForwardEntry2Fn) (PMIB_IPFORWARD_ROW2);
  static CreateIpForwardEntry2Fn CreateIpForwardEntry2 = NULL;

  if (!CreateIpForwardEntry2)
    {
      HMODULE iphlpapi = GetModuleHandle (TEXT("iphlpapi.dll"));
      if (iphlpapi == NULL)
        return GetLastError ();

      CreateIpForwardEntry2 = (CreateIpForwardEntry2Fn) GetProcAddress (iphlpapi, "CreateIpForwardEntry2");
      if (!CreateIpForwardEntry2)
        return GetLastError ();
    }

  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)
{
  typedef NETIOAPI_API (*FlushIpNetTable2Fn) (ADDRESS_FAMILY, NET_IFINDEX);
  static FlushIpNetTable2Fn flush_fn = NULL;

  if (msg->family == AF_INET)
    return FlushIpNetTable (msg->iface.index);

  if (!flush_fn)
    {
      HMODULE iphlpapi = GetModuleHandle (TEXT("iphlpapi.dll"));
      if (iphlpapi == NULL)
        return GetLastError ();

      flush_fn = (FlushIpNetTable2Fn) GetProcAddress (iphlpapi, "FlushIpNetTable2");
      if (!flush_fn)
        {
          if (GetLastError () == ERROR_PROC_NOT_FOUND)
            return WSAEPFNOSUPPORT;
          else
            return GetLastError ();
        }
    }
  return flush_fn (msg->family, msg->iface.index);
}

static void
BlockDNSErrHandler (DWORD err, const char *msg)
{
  TCHAR buf[256];
  LPCTSTR err_str;

  if (!err) return;

  err_str = TEXT("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;
    }

#ifdef UNICODE
  MsgToEventLog (M_ERR, L"%S (status = %lu): %s", msg, err, err_str);
#else
  MsgToEventLog (M_ERR, "%s (status = %lu): %s", msg, err, err_str);
#endif

}

/* Use an always-true match_fn to get the head of the list */
static BOOL
CmpEngine (LPVOID item, LPVOID any)
{
  return TRUE;
}

static DWORD
HandleBlockDNSMessage (const block_dns_message_t *msg, undo_lists_t *lists)
{
  DWORD err = 0;
  HANDLE engine = NULL;
  LPCWSTR exe_path;

#ifdef UNICODE
  exe_path = settings.exe_path;
#else
  WCHAR wide_path[MAX_PATH];
  MultiByteToWideChar (CP_UTF8, 0, settings.exe_path, MAX_PATH, wide_path, MAX_PATH);
  exe_path = wide_path;
#endif

  if (msg->header.type == msg_add_block_dns)
    {
      err = add_block_dns_filters (&engine, msg->iface.index, exe_path, BlockDNSErrHandler);
      if (!err)
        err = AddListItem (&(*lists)[block_dns], engine);
    }
  else
    {
      engine = RemoveListItem (&(*lists)[block_dns], CmpEngine, NULL);
      if (engine)
        {
          err = delete_block_dns_filters (engine);
          engine = NULL;
        }
      else
        MsgToEventLog (M_ERR, TEXT("No previous block DNS filters to delete"));
    }

  if (err && engine)
    {
      delete_block_dns_filters (engine);
    }

  return err;
}

/*
 * 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, TEXT("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, TEXT("ExecCommand: \"%s %s\" killed after timeout"),
            argv0, cmdline);
        }
      else if (exit_code)
          MsgToEventLog (M_ERR, TEXT("ExecCommand: \"%s %s\" exited with status = %lu"),
                          argv0, cmdline, exit_code);
      else
          MsgToEventLog (M_INFO, TEXT("ExecCommand: \"%s %s\" completed"), argv0, cmdline);

      CloseHandle(pi.hProcess);
      CloseHandle(pi.hThread);
    }
  else
    {
      exit_code = GetLastError();
      MsgToEventLog (M_SYSERR, TEXT("ExecCommand: could not run \"%s %s\" :"),
                                      argv0, cmdline);
    }

  free (cmdline_dup);
  return exit_code;
}

/*
 * Entry point for register-dns thread.
 */
static DWORD WINAPI
RegisterDNS (LPVOID unused)
{
  DWORD err;
  DWORD i;
  WCHAR sys_path[MAX_PATH];
  DWORD timeout = RDNS_TIMEOUT * 1000; /* in milliseconds */

  /* default paths of net and ipconfig commands */
  WCHAR net[MAX_PATH]   = L"C:\\Windows\\system32\\net.exe";
  WCHAR ipcfg[MAX_PATH] = L"C:\\Windows\\system32\\ipconfig.exe";

  struct
    {
      WCHAR *argv0;
      WCHAR *cmdline;
      DWORD timeout;
    } cmds [] = {
                  { net,   L"net stop dnscache",     timeout },
                  { net,   L"net start dnscache",    timeout },
                  { ipcfg, L"ipconfig /flushdns",    timeout },
                  { ipcfg, L"ipconfig /registerdns", timeout },
                };
  int ncmds = sizeof (cmds) / sizeof (cmds[0]);

  HANDLE wait_handles[2] = {rdns_semaphore, exit_event};

  if(GetSystemDirectory(sys_path, MAX_PATH))
    {
      _snwprintf (net, MAX_PATH, L"%s\\%s", sys_path, L"net.exe");
      net[MAX_PATH-1] = L'\0';

      _snwprintf (ipcfg, MAX_PATH, L"%s\\%s", sys_path, L"ipconfig.exe");
      ipcfg[MAX_PATH-1] = L'\0';
    }

  if (WaitForMultipleObjects (2, wait_handles, FALSE, timeout) == WAIT_OBJECT_0)
    {
      /* Semaphore locked */
      for (i = 0; i < ncmds; ++i)
        {
          ExecCommand (cmds[i].argv0, cmds[i].cmdline, cmds[i].timeout);
        }
      err = 0;
      if ( !ReleaseSemaphore (rdns_semaphore, 1, NULL) )
        err = MsgToEventLog (M_SYSERR, TEXT("RegisterDNS: Failed to release regsiter-dns semaphore:"));
    }
  else
    {
      MsgToEventLog (M_ERR, TEXT("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;
}

static VOID
HandleMessage (HANDLE pipe, DWORD bytes, DWORD count, LPHANDLE events, undo_lists_t *lists)
{
  DWORD read;
  union {
    message_header_t header;
    address_message_t address;
    route_message_t route;
    flush_neighbors_message_t flush_neighbors;
    block_dns_message_t block_dns;
  } msg;
  ack_message_t ack = {
    .header = {
      .type = msg_acknowledgement,
      .size = sizeof (ack),
      .message_id = -1
    },
    .error_number = ERROR_MESSAGE_DATA
  };

  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_block_dns:
    case msg_del_block_dns:
      if (msg.header.size == sizeof (msg.block_dns))
        ack.error_number = HandleBlockDNSMessage (&msg.block_dns, lists);
      break;

    case msg_register_dns:
        ack.error_number = HandleRegisterDNSMessage ();
        break;

    default:
      ack.error_number = ERROR_MESSAGE_TYPE;
      MsgToEventLog (MSG_FLAGS_ERROR, TEXT("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;
  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 block_dns:
              delete_block_dns_filters (item->data);
              item->data = NULL;
              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, svc_pipe;
  PTOKEN_USER svc_user, ovpn_user;
  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;
  TCHAR ovpn_pipe_name[36];
  LPCWSTR exe_path;
  WCHAR *cmdline = NULL;
  size_t cmdline_size;
  undo_lists_t undo_lists;

  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;
  svc_user = NULL;
  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;
  ovpn_user = NULL;
  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;
    }

  /* Check user is authorized or options are white-listed */
  if (!IsAuthorizedUser (ovpn_user->User.Sid, &settings) &&
      !ValidateOptions (pipe, sud.directory, sud.options))
    {
      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 = (LPTSTR) 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 = (LPTSTR) 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(_T("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;
    }

  openvpn_sntprintf (ovpn_pipe_name, _countof (ovpn_pipe_name),
    TEXT("\\\\.\\pipe\\openvpn\\service_%lu"), 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;
    }
  openvpn_sntprintf (cmdline, cmdline_size, L"openvpn %s --msg-channel %lu",
                     sud.options, svc_pipe);

  if (!CreateEnvironmentBlock (&user_env, imp_token, FALSE))
    {
      ReturnLastError (pipe, L"CreateEnvironmentBlock");
      goto out;
    }

  startup_info.cb = sizeof (startup_info);
  startup_info.lpDesktop = L"winsta0\\default";
  startup_info.dwFlags = STARTF_USESTDHANDLES;
  startup_info.hStdInput = stdin_read;
  startup_info.hStdOutput = stdout_write;
  startup_info.hStdError = stdout_write;

#ifdef UNICODE
  exe_path = settings.exe_path;
#else
  WCHAR wide_path[MAX_PATH];
  MultiByteToWideChar (CP_UTF8, 0, settings.exe_path, MAX_PATH, wide_path, MAX_PATH);
  exe_path = wide_path;
#endif

  // 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, strlen (input), &written, NULL);
      free (input);
    }

  while (TRUE)
    {
      DWORD bytes = PeekNamedPipeAsync (ovpn_pipe, 1, &exit_event);
      if (bytes == 0)
        break;

      HandleMessage (ovpn_pipe, 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];
      int len = _snwprintf (buf, _countof (buf),
                         L"OpenVPN exited with error: exit code = %lu", exit_code);
      buf[_countof (buf) - 1] =  L'\0';
      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)
{
  HANDLE pipe = NULL;
  PACL old_dacl, new_dacl;
  PSECURITY_DESCRIPTOR sd;
  static EXPLICIT_ACCESS ea[2];
  static BOOL initialized = FALSE;
  DWORD flags = PIPE_ACCESS_DUPLEX | WRITE_DAC | FILE_FLAG_OVERLAPPED;

  if (!initialized)
    {
      PSID everyone, anonymous;

      ConvertStringSidToSid (TEXT("S-1-1-0"), &everyone);
      ConvertStringSidToSid (TEXT("S-1-5-7"), &anonymous);

      ea[0].grfAccessPermissions = FILE_GENERIC_WRITE;
      ea[0].grfAccessMode = GRANT_ACCESS;
      ea[0].grfInheritance = NO_INHERITANCE;
      ea[0].Trustee.pMultipleTrustee = NULL;
      ea[0].Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
      ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
      ea[0].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN;
      ea[0].Trustee.ptstrName = (LPTSTR) everyone;

      ea[1].grfAccessPermissions = 0;
      ea[1].grfAccessMode = REVOKE_ACCESS;
      ea[1].grfInheritance = NO_INHERITANCE;
      ea[1].Trustee.pMultipleTrustee = NULL;
      ea[1].Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
      ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
      ea[1].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN;
      ea[1].Trustee.ptstrName = (LPTSTR) anonymous;

      flags |= FILE_FLAG_FIRST_PIPE_INSTANCE;
      initialized = TRUE;
    }

  pipe = CreateNamedPipe (TEXT("\\\\.\\pipe\\openvpn\\service"), flags,
                PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE,
                PIPE_UNLIMITED_INSTANCES, 1024, 1024, 0, NULL);
  if (pipe == INVALID_HANDLE_VALUE)
    {
      MsgToEventLog (M_SYSERR, TEXT("Could not create named pipe"));
      return INVALID_HANDLE_VALUE;
    }

  if (GetSecurityInfo (pipe, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,
                        NULL, NULL, &old_dacl, NULL, &sd) != ERROR_SUCCESS)
    {
      MsgToEventLog (M_SYSERR, TEXT("Could not get pipe security info"));
      return CloseHandleEx (&pipe);
    }

  if (SetEntriesInAcl (2, ea, old_dacl, &new_dacl) != ERROR_SUCCESS)
    {
      MsgToEventLog (M_SYSERR, TEXT("Could not set entries in new acl"));
      return CloseHandleEx (&pipe);
    }

  if (SetSecurityInfo (pipe, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,
                        NULL, NULL, new_dacl, NULL) != ERROR_SUCCESS)
    {
      MsgToEventLog (M_SYSERR, TEXT("Could not set pipe security info"));
      return CloseHandleEx (&pipe);
    }

  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);
}


VOID WINAPI
ServiceStartInteractive (DWORD dwArgc, LPTSTR *lpszArgv)
{
  HANDLE pipe, io_event = NULL;
  OVERLAPPED overlapped;
  DWORD error = NO_ERROR;
  list_item_t *threads = NULL;
  PHANDLE handles = NULL;
  DWORD handle_count;
  BOOL CmpHandle (LPVOID item, LPVOID hnd) { return item == hnd; }

  service = RegisterServiceCtrlHandlerEx (interactive_service.name, ServiceCtrlInteractive, &status);
  if (!service)
    return;

  status.dwServiceType = SERVICE_WIN32_SHARE_PROCESS;
  status.dwCurrentState = SERVICE_START_PENDING;
  status.dwServiceSpecificExitCode = NO_ERROR;
  status.dwWin32ExitCode = NO_ERROR;
  status.dwWaitHint = 3000;
  ReportStatusToSCMgr (service, &status);

  /* 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, TEXT("Could not create event"));
      goto out;
    }

  rdns_semaphore = CreateSemaphoreW (NULL, 1, 1, NULL);
  if (!rdns_semaphore)
  {
      error = MsgToEventLog (M_SYSERR, TEXT("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, TEXT("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, TEXT("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);
}