diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..43505fed --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,120 @@ +--- +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build-windows: + name: Build Windows Release + runs-on: windows-latest + + defaults: + run: + shell: msys2 {0} + + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + mingw-w64-x86_64-gcc + mingw-w64-x86_64-cmake + mingw-w64-x86_64-openssl + mingw-w64-x86_64-ninja + p7zip + wget + + - name: Configure + run: | + mkdir build + cd build + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + + - name: Build + run: | + cd build + ninja + + - name: Download wintun + shell: pwsh + run: | + cd build + Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile wintun.zip + Expand-Archive -Path wintun.zip -DestinationPath wintun + Copy-Item wintun/wintun/bin/amd64/wintun.dll . + + - name: Collect Runtime DLLs + run: | + cd build + cp /mingw64/bin/libssl-*.dll . 2>/dev/null || true + cp /mingw64/bin/libcrypto-*.dll . 2>/dev/null || true + cp /mingw64/bin/libwinpthread-*.dll . 2>/dev/null || true + cp /mingw64/bin/libgcc_s_seh-*.dll . 2>/dev/null || true + + - name: Run Tests + run: | + cd build + ./test_ppp.exe || true + + - name: Package + shell: pwsh + run: | + cd build + New-Item -ItemType Directory -Force -Path openfortivpn-windows-x64 + Copy-Item openfortivpn.exe openfortivpn-windows-x64/ + Copy-Item wintun.dll openfortivpn-windows-x64/ -ErrorAction SilentlyContinue + Copy-Item libssl-*.dll openfortivpn-windows-x64/ -ErrorAction SilentlyContinue + Copy-Item libcrypto-*.dll openfortivpn-windows-x64/ -ErrorAction SilentlyContinue + Copy-Item libwinpthread-*.dll openfortivpn-windows-x64/ -ErrorAction SilentlyContinue + Copy-Item libgcc_s_seh-*.dll openfortivpn-windows-x64/ -ErrorAction SilentlyContinue + Copy-Item ../README.md openfortivpn-windows-x64/ + Compress-Archive -Path openfortivpn-windows-x64/* -DestinationPath openfortivpn-windows-x64.zip + + - name: Upload Release + uses: softprops/action-gh-release@v2 + with: + files: build/openfortivpn-windows-x64.zip + generate_release_notes: true + + build-linux: + name: Build Linux Release + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libssl-dev + + - name: Build (autotools) + run: | + ./autogen.sh + ./configure --prefix=/usr --sysconfdir=/etc + make -j$(nproc) + + - name: Package + run: | + mkdir -p openfortivpn-linux-x64 + cp openfortivpn openfortivpn-linux-x64/ + cp README.md openfortivpn-linux-x64/ + tar czf openfortivpn-linux-x64.tar.gz openfortivpn-linux-x64/ + + - name: Upload Release + uses: softprops/action-gh-release@v2 + with: + files: openfortivpn-linux-x64.tar.gz + generate_release_notes: true diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 00000000..6b58094d --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,99 @@ +--- +name: Windows Build + +on: + push: + + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + build-mingw: + name: Build (MinGW-w64) + runs-on: windows-latest + + defaults: + run: + shell: msys2 {0} + + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + mingw-w64-x86_64-gcc + mingw-w64-x86_64-cmake + mingw-w64-x86_64-openssl + mingw-w64-x86_64-ninja + make + + - name: Configure (CMake) + run: | + mkdir build + cd build + cmake .. -G Ninja -DBUILD_TESTING=ON + + - name: Build + run: | + cd build + ninja + + - name: Run PPP Unit Tests + run: | + cd build + ctest --output-on-failure + + - name: Verify Binary + run: | + cd build + ./openfortivpn.exe --version + + build-cmake-linux: + name: Build (CMake on Linux) + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake libssl-dev pkg-config + + - name: Configure (CMake) + run: | + mkdir build + cd build + cmake .. -DBUILD_TESTING=ON + + - name: Build + run: | + cd build + make -j$(nproc) + + - name: Run PPP Unit Tests + run: | + cd build + ctest --output-on-failure + + - name: Verify Binary + run: | + cd build + ./openfortivpn --version + + - name: Verify Autotools Build Still Works + run: | + make distclean 2>/dev/null || true + autoreconf -fi + ./configure --prefix=/usr --sysconfdir=/etc + make -j$(nproc) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..4db10464 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,273 @@ +cmake_minimum_required(VERSION 3.15) + +project(openfortivpn C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Version from configure.ac +set(PROJECT_VERSION "1.24.1") + +# Find OpenSSL +find_package(OpenSSL REQUIRED) + +# Common sources (platform-independent) +set(COMMON_SOURCES + src/config.c + src/event.c + src/hdlc.c + src/http.c + src/xml.c +) + +# Platform-specific sources and libraries +if(WIN32) + set(PLATFORM_SOURCES + src/ppp.c + src/io_win.c + src/tunnel_win.c + src/ipv4_win.c + src/userinput_win.c + src/log_win.c + src/http_server_win.c + ) + set(PLATFORM_LIBS ws2_32 iphlpapi) + + # Bundled getopt for MSVC (MinGW has its own) + if(MSVC) + list(APPEND PLATFORM_SOURCES lib/getopt/getopt.c) + set(GETOPT_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/lib/getopt") + endif() + + # Platform definitions + add_definitions( + -D_WIN32_WINNT=0x0A00 # Windows 10+ + -DWIN32_LEAN_AND_MEAN + -D_CRT_SECURE_NO_WARNINGS + ) +else() + set(PLATFORM_SOURCES + src/io.c + src/tunnel.c + src/ipv4.c + src/userinput.c + src/log.c + src/http_server.c + ) + set(PLATFORM_LIBS pthread) + + # Check for libutil (forkpty) + include(CheckLibraryExists) + check_library_exists(util forkpty "" HAVE_LIBUTIL) + if(HAVE_LIBUTIL) + list(APPEND PLATFORM_LIBS util) + endif() + + # Platform detection (mirroring configure.ac) + include(CheckIncludeFile) + include(CheckSymbolExists) + include(CheckStructHasMember) + + check_include_file(pty.h HAVE_PTY_H) + check_include_file(util.h HAVE_UTIL_H) + check_include_file(libutil.h HAVE_LIBUTIL_H) + check_include_file(mach/mach.h HAVE_MACH_MACH_H) + check_include_file(semaphore.h HAVE_SEMAPHORE_H) + check_include_file(termios.h HAVE_STRUCT_TERMIOS) + + # Check for /proc/net/route (Linux) + if(EXISTS "/proc/net/route") + set(HAVE_PROC_NET_ROUTE 1) + else() + set(HAVE_PROC_NET_ROUTE 0) + endif() + + # Check for rtentry with rt_dst + include(CheckCSourceCompiles) + check_c_source_compiles(" + #include + #include + #include + int main() { struct rtentry r; r.rt_dst.sa_family = 0; return 0; } + " HAVE_RT_ENTRY_WITH_RT_DST) + if(NOT HAVE_RT_ENTRY_WITH_RT_DST) + set(HAVE_RT_ENTRY_WITH_RT_DST 0) + endif() + + # Check for SO_BINDTODEVICE + check_c_source_compiles(" + #include + int main() { int x = SO_BINDTODEVICE; return x; } + " HAVE_SO_BINDTODEVICE) + + # Check for pppd or ppp + find_program(PPPD_PATH pppd PATHS /usr/sbin /sbin) + find_program(PPP_PATH_BIN ppp PATHS /usr/sbin /sbin) + + if(PPPD_PATH) + set(HAVE_USR_SBIN_PPPD 1) + set(HAVE_USR_SBIN_PPP 0) + set(PPP_PATH "${PPPD_PATH}") + elseif(PPP_PATH_BIN) + set(HAVE_USR_SBIN_PPPD 0) + set(HAVE_USR_SBIN_PPP 1) + set(PPP_PATH "${PPP_PATH_BIN}") + else() + set(HAVE_USR_SBIN_PPPD 1) + set(HAVE_USR_SBIN_PPP 0) + set(PPP_PATH "/usr/sbin/pppd") + endif() + + # Check for resolvconf + find_program(RESOLVCONF_PATH resolvconf PATHS /sbin /usr/sbin) + if(RESOLVCONF_PATH) + set(HAVE_RESOLVCONF 1) + set(USE_RESOLVCONF 1) + else() + set(HAVE_RESOLVCONF 0) + set(USE_RESOLVCONF 0) + set(RESOLVCONF_PATH "") + endif() + + # Check for netstat + find_program(NETSTAT_PATH netstat PATHS /usr/bin /bin /usr/sbin /sbin) + if(NOT NETSTAT_PATH) + set(NETSTAT_PATH "/usr/bin/netstat") + endif() + + # Check for systemd + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBSYSTEMD QUIET libsystemd) + endif() + if(LIBSYSTEMD_FOUND) + set(HAVE_SYSTEMD 1) + list(APPEND PLATFORM_LIBS ${LIBSYSTEMD_LIBRARIES}) + else() + set(HAVE_SYSTEMD 0) + endif() + + # Check for vdprintf + include(CheckFunctionExists) + check_function_exists(vdprintf HAVE_VDPRINTF) + + set(LEGACY_PPPD 0 CACHE BOOL "Support pppd < 2.5.0") + set(SYSCONFDIR "${CMAKE_INSTALL_PREFIX}/etc" + CACHE PATH "System configuration directory") +endif() + +# Main executable +add_executable(openfortivpn + src/main.c + ${COMMON_SOURCES} + ${PLATFORM_SOURCES} +) + +target_link_libraries(openfortivpn + OpenSSL::SSL + OpenSSL::Crypto + ${PLATFORM_LIBS} +) + +target_include_directories(openfortivpn PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_BINARY_DIR} # for generated config +) + +if(DEFINED GETOPT_INCLUDE_DIR) + target_include_directories(openfortivpn PRIVATE ${GETOPT_INCLUDE_DIR}) +endif() + +# Common compile definitions +target_compile_definitions(openfortivpn PRIVATE + VERSION="${PROJECT_VERSION}" + PACKAGE_VERSION="${PROJECT_VERSION}" +) + +# Platform-specific compile definitions +if(WIN32) + target_compile_definitions(openfortivpn PRIVATE + HAVE_USR_SBIN_PPPD=0 + HAVE_USR_SBIN_PPP=0 + HAVE_PROC_NET_ROUTE=0 + HAVE_RT_ENTRY_WITH_RT_DST=0 + HAVE_RESOLVCONF=0 + USE_RESOLVCONF=0 + HAVE_SYSTEMD=0 + HAVE_PTY_H=0 + HAVE_UTIL_H=0 + HAVE_LIBUTIL_H=0 + HAVE_MACH_MACH_H=0 + HAVE_STRUCT_TERMIOS=0 + HAVE_SO_BINDTODEVICE=0 + LEGACY_PPPD=0 + SYSCONFDIR="C:/ProgramData/openfortivpn" + PPP_PATH="" + NETSTAT_PATH="" + RESOLVCONF_PATH="" + REVISION="${PROJECT_VERSION}" + _WIN32_WINNT=0x0A00 + ) +else() + target_compile_definitions(openfortivpn PRIVATE + HAVE_USR_SBIN_PPPD=${HAVE_USR_SBIN_PPPD} + HAVE_USR_SBIN_PPP=${HAVE_USR_SBIN_PPP} + HAVE_PROC_NET_ROUTE=${HAVE_PROC_NET_ROUTE} + HAVE_RT_ENTRY_WITH_RT_DST=$ + HAVE_RESOLVCONF=${HAVE_RESOLVCONF} + USE_RESOLVCONF=${USE_RESOLVCONF} + HAVE_SYSTEMD=${HAVE_SYSTEMD} + HAVE_SO_BINDTODEVICE=$ + LEGACY_PPPD=$ + SYSCONFDIR="${SYSCONFDIR}" + PPP_PATH="${PPP_PATH}" + NETSTAT_PATH="${NETSTAT_PATH}" + RESOLVCONF_PATH="${RESOLVCONF_PATH}" + REVISION="${PROJECT_VERSION}" + ) + + if(HAVE_PTY_H) + target_compile_definitions(openfortivpn PRIVATE HAVE_PTY_H=1) + endif() + if(HAVE_UTIL_H) + target_compile_definitions(openfortivpn PRIVATE HAVE_UTIL_H=1) + endif() + if(HAVE_LIBUTIL_H) + target_compile_definitions(openfortivpn PRIVATE HAVE_LIBUTIL_H=1) + endif() + if(HAVE_MACH_MACH_H) + target_compile_definitions(openfortivpn PRIVATE HAVE_MACH_MACH_H=1) + endif() + if(HAVE_STRUCT_TERMIOS) + target_compile_definitions(openfortivpn PRIVATE HAVE_STRUCT_TERMIOS=1) + endif() + if(HAVE_VDPRINTF) + target_compile_definitions(openfortivpn PRIVATE HAVE_VDPRINTF=1) + endif() +endif() + +# PPP unit tests +if(BUILD_TESTING) + enable_testing() + add_executable(test_ppp + tests/test_ppp.c + src/ppp.c + src/hdlc.c + ) + target_include_directories(test_ppp PRIVATE ${CMAKE_SOURCE_DIR}/src) + target_compile_definitions(test_ppp PRIVATE + HAVE_USR_SBIN_PPPD=0 + HAVE_USR_SBIN_PPP=0 + ) + if(WIN32) + target_compile_definitions(test_ppp PRIVATE + _WIN32_WINNT=0x0A00 + WIN32_LEAN_AND_MEAN + ) + endif() + add_test(NAME ppp_state_machine COMMAND test_ppp) +endif() + +# Install target +include(GNUInstallDirs) +install(TARGETS openfortivpn RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/Makefile.am b/Makefile.am index a2b0584a..925cb1f9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,7 +1,8 @@ # http://mij.oltrelinux.com/devel/autoconf-automake/ bin_PROGRAMS = openfortivpn -openfortivpn_SOURCES = src/config.c src/config.h src/hdlc.c src/hdlc.h \ +openfortivpn_SOURCES = src/config.c src/config.h src/event.c src/event.h \ + src/exit_codes.h src/hdlc.c src/hdlc.h \ src/http.c src/http.h src/io.c src/io.h \ src/http_server.c src/ipv4.c \ src/ipv4.h src/log.c src/log.h src/tunnel.c \ diff --git a/README.md b/README.md index 36e3e917..58f597cd 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ openfortivpn is a client for PPP+TLS VPN tunnel services. It spawns a pppd process and operates the communication between the gateway and this process. +On Windows, it uses an in-process PPP engine with +[wintun](https://www.wintun.net/) instead of pppd. + It is compatible with Fortinet VPNs. Usage @@ -137,6 +140,55 @@ sudo port install openfortivpn A more complete overview can be obtained from [repology](https://repology.org/project/openfortivpn/versions). +### Windows + +Windows support uses [wintun](https://www.wintun.net/) (a lightweight TUN +driver from the WireGuard project) instead of pppd. PPP negotiation is handled +in-process. + +**Requirements:** +* Windows 10 or later +* Administrator privileges (for TUN adapter and route management) +* [wintun.dll](https://www.wintun.net/) in the same directory as `openfortivpn.exe` + or in the system PATH + +**Building with MinGW-w64 (MSYS2):** + +1. Install [MSYS2](https://www.msys2.org/) and open a MinGW64 shell. +2. Install dependencies: + ```shell + pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-openssl mingw-w64-x86_64-ninja + ``` +3. Build: + ```shell + mkdir build && cd build + cmake .. -G Ninja + ninja + ``` + +**Building with MSVC:** + +1. Install [Visual Studio](https://visualstudio.microsoft.com/) with C/C++ workload. +2. Install OpenSSL via [vcpkg](https://vcpkg.io/): + ```shell + vcpkg install openssl:x64-windows + ``` +3. Build: + ```shell + mkdir build && cd build + cmake .. -DCMAKE_TOOLCHAIN_FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake + cmake --build . --config Release + ``` + +**Running:** + +Download `wintun.dll` from https://www.wintun.net/ and place it next to +`openfortivpn.exe`, then run from an **Administrator** command prompt: + +```shell +openfortivpn vpn-gateway:8443 --username=foo +``` + ### Building and installing from source For other distros, you'll need to build and install from source: @@ -201,14 +253,19 @@ Running as root? openfortivpn needs elevated privileges at three steps during tunnel set up: -* when spawning a `/usr/sbin/pppd` process; +* when spawning a `/usr/sbin/pppd` process (Linux/macOS) or creating a TUN + adapter (Windows); * when setting IP routes through VPN (when the tunnel is up); -* when adding nameservers to `/etc/resolv.conf` (when the tunnel is up). +* when adding nameservers to `/etc/resolv.conf` (Linux/macOS) or configuring + DNS via netsh (Windows). -For these reasons, you need to use `sudo openfortivpn`. +On **Linux/macOS**, you need to use `sudo openfortivpn`. If you need it to be usable by non-sudoer users, you might consider adding an entry in `/etc/sudoers` or a file under `/etc/sudoers.d`. +On **Windows**, run openfortivpn from an Administrator command prompt or +PowerShell. + For example: ```shell visudo -f /etc/sudoers.d/openfortivpn diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 00000000..7e798bbe --- /dev/null +++ b/installer/README.md @@ -0,0 +1,39 @@ +# Windows Installer + +This directory contains an [NSIS](https://nsis.sourceforge.io/) script for +building a Windows installer. + +## Prerequisites + +1. Build openfortivpn with CMake (MinGW-w64 or MSVC) +2. Download [wintun.dll](https://www.wintun.net/) (amd64 version) +3. Install [NSIS 3.x](https://nsis.sourceforge.io/) with the + [EnVar plugin](https://nsis.sourceforge.io/EnVar_plug-in) + +## Building the installer + +Copy these files into this directory: + +- `openfortivpn.exe` (from your build output) +- `wintun.dll` (from wintun zip, `wintun/bin/amd64/wintun.dll`) +- `README.md` (from project root) +- MinGW runtime DLLs (if building with MinGW): + - `libssl-3-x64.dll` + - `libcrypto-3-x64.dll` + - `libwinpthread-1.dll` + - `libgcc_s_seh-1.dll` + +Then run: + +```shell +makensis openfortivpn.nsi +``` + +This produces `openfortivpn-1.24.1-setup.exe`. + +## What the installer does + +- Installs to `C:\Program Files\openfortivpn\` +- Adds the install directory to the system PATH +- Creates an example config at `%APPDATA%\openfortivpn\config.example` +- Registers in Add/Remove Programs for clean uninstall diff --git a/installer/openfortivpn.nsi b/installer/openfortivpn.nsi new file mode 100644 index 00000000..15ee4336 --- /dev/null +++ b/installer/openfortivpn.nsi @@ -0,0 +1,108 @@ +; openfortivpn Windows Installer +; Requires NSIS 3.x (https://nsis.sourceforge.io/) +; +; Build instructions: +; 1. Place these files in the installer/ directory: +; - openfortivpn.exe (from CMake build) +; - wintun.dll (from https://www.wintun.net/) +; - libssl-*.dll, libcrypto-*.dll (from MinGW or vcpkg) +; - libwinpthread-1.dll, libgcc_s_seh-1.dll (from MinGW) +; 2. Run: makensis openfortivpn.nsi + +!define PRODUCT_NAME "openfortivpn" +!define PRODUCT_VERSION "1.24.1" +!define PRODUCT_PUBLISHER "openfortivpn contributors" + +Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" +OutFile "openfortivpn-${PRODUCT_VERSION}-setup.exe" +InstallDir "$PROGRAMFILES64\openfortivpn" +RequestExecutionLevel admin + +;--- Pages --- +Page directory +Page instfiles +UninstPage uninstConfirm +UninstPage instfiles + +;--- Install --- +Section "Install" + SetOutPath $INSTDIR + + ; Core files + File "openfortivpn.exe" + File "wintun.dll" + File "README.md" + + ; Runtime DLLs (MinGW build) + File /nonfatal "libssl-3-x64.dll" + File /nonfatal "libcrypto-3-x64.dll" + File /nonfatal "libwinpthread-1.dll" + File /nonfatal "libgcc_s_seh-1.dll" + + ; Create config directory + CreateDirectory "$APPDATA\openfortivpn" + + ; Write example config + FileOpen $0 "$APPDATA\openfortivpn\config.example" w + FileWrite $0 "# openfortivpn configuration file$\r$\n" + FileWrite $0 "# Copy this to 'config' and edit.$\r$\n" + FileWrite $0 "#$\r$\n" + FileWrite $0 "# host = vpn-gateway$\r$\n" + FileWrite $0 "# port = 8443$\r$\n" + FileWrite $0 "# username = foo$\r$\n" + FileWrite $0 "# trusted-cert = $\r$\n" + FileClose $0 + + ; Add to PATH + EnVar::AddValue "PATH" "$INSTDIR" + + ; Create uninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + ; Start menu + CreateDirectory "$SMPROGRAMS\openfortivpn" + CreateShortcut "$SMPROGRAMS\openfortivpn\Uninstall.lnk" \ + "$INSTDIR\uninstall.exe" + + ; Registry for Add/Remove Programs + WriteRegStr HKLM \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\openfortivpn" \ + "DisplayName" "${PRODUCT_NAME}" + WriteRegStr HKLM \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\openfortivpn" \ + "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr HKLM \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\openfortivpn" \ + "Publisher" "${PRODUCT_PUBLISHER}" + WriteRegStr HKLM \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\openfortivpn" \ + "UninstallString" "$INSTDIR\uninstall.exe" + WriteRegStr HKLM \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\openfortivpn" \ + "InstallLocation" "$INSTDIR" +SectionEnd + +;--- Uninstall --- +Section "Uninstall" + ; Remove files + Delete "$INSTDIR\openfortivpn.exe" + Delete "$INSTDIR\wintun.dll" + Delete "$INSTDIR\README.md" + Delete "$INSTDIR\libssl-3-x64.dll" + Delete "$INSTDIR\libcrypto-3-x64.dll" + Delete "$INSTDIR\libwinpthread-1.dll" + Delete "$INSTDIR\libgcc_s_seh-1.dll" + Delete "$INSTDIR\uninstall.exe" + RMDir "$INSTDIR" + + ; Remove from PATH + EnVar::DeleteValue "PATH" "$INSTDIR" + + ; Remove start menu + Delete "$SMPROGRAMS\openfortivpn\Uninstall.lnk" + RMDir "$SMPROGRAMS\openfortivpn" + + ; Remove registry + DeleteRegKey HKLM \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\openfortivpn" +SectionEnd diff --git a/lib/getopt/getopt.c b/lib/getopt/getopt.c new file mode 100644 index 00000000..ed9817c4 --- /dev/null +++ b/lib/getopt/getopt.c @@ -0,0 +1,203 @@ +/* + * Portable getopt_long implementation for systems without . + * Based on public domain implementations. + * Used on Windows with MSVC (MinGW provides its own getopt). + */ + +#include "getopt.h" + +#include +#include + +char *optarg; +int optind = 1; +int opterr = 1; +int optopt = '?'; + +static int optwhere = 1; + +static void permute(char *const argv[], int from, int to) +{ + char *tmp = argv[from]; + int i; + + for (i = from; i > to; i--) + ((char **)argv)[i] = argv[i - 1]; + ((char **)argv)[to] = tmp; +} + +int getopt(int argc, char *const argv[], const char *optstring) +{ + char *cp; + int c; + + optarg = NULL; + + if (optind >= argc || argv[optind] == NULL) + return -1; + + if (argv[optind][0] != '-' || argv[optind][1] == '\0') + return -1; + + if (strcmp(argv[optind], "--") == 0) { + optind++; + return -1; + } + + c = argv[optind][optwhere++]; + + cp = strchr(optstring, c); + if (c == ':' || cp == NULL) { + optopt = c; + if (opterr) + fprintf(stderr, "%s: invalid option -- '%c'\n", + argv[0], c); + if (argv[optind][optwhere] == '\0') { + optind++; + optwhere = 1; + } + return '?'; + } + + if (cp[1] == ':') { + if (argv[optind][optwhere] != '\0') { + optarg = &argv[optind][optwhere]; + optind++; + optwhere = 1; + } else if (cp[2] != ':') { + /* Required argument */ + optind++; + if (optind >= argc) { + optopt = c; + if (opterr) + fprintf(stderr, + "%s: option requires an argument -- '%c'\n", + argv[0], c); + return optstring[0] == ':' ? ':' : '?'; + } + optarg = argv[optind]; + optind++; + optwhere = 1; + } else { + /* Optional argument not present */ + optind++; + optwhere = 1; + } + } else { + if (argv[optind][optwhere] == '\0') { + optind++; + optwhere = 1; + } + } + + return c; +} + +int getopt_long(int argc, char *const argv[], const char *optstring, + const struct option *longopts, int *longindex) +{ + int i; + int match = -1; + int num_matches = 0; + size_t len; + + optarg = NULL; + + if (optind >= argc) + return -1; + + /* GNU-style permutation: skip non-option arguments */ + if (argv[optind] == NULL || argv[optind][0] != '-') { + int j; + + for (j = optind; j < argc; j++) { + if (argv[j] && argv[j][0] == '-' && argv[j][1] != '\0') + break; + } + if (j >= argc) + return -1; + /* Move the non-option args after the option we found */ + while (j > optind) + permute(argv, j--, optind); + if (argv[optind] == NULL || argv[optind][0] != '-') + return -1; + } + + /* Check for "--" */ + if (strcmp(argv[optind], "--") == 0) { + optind++; + return -1; + } + + /* Check for long option */ + if (argv[optind][1] == '-') { + const char *opt = &argv[optind][2]; + const char *eq = strchr(opt, '='); + + len = eq ? (size_t)(eq - opt) : strlen(opt); + + for (i = 0; longopts[i].name; i++) { + if (strncmp(longopts[i].name, opt, len) == 0 && + strlen(longopts[i].name) == len) { + match = i; + num_matches = 1; + break; + } + } + + /* Try prefix matching if exact match not found */ + if (match == -1) { + for (i = 0; longopts[i].name; i++) { + if (strncmp(longopts[i].name, opt, len) == 0) { + match = i; + num_matches++; + } + } + } + + if (num_matches != 1) { + if (opterr) { + if (num_matches > 1) + fprintf(stderr, + "%s: option '--%.*s' is ambiguous\n", + argv[0], (int)len, opt); + else + fprintf(stderr, + "%s: unrecognized option '--%.*s'\n", + argv[0], (int)len, opt); + } + optind++; + return '?'; + } + + if (longindex) + *longindex = match; + + optind++; + + if (longopts[match].has_arg == required_argument || + longopts[match].has_arg == optional_argument) { + if (eq) { + optarg = (char *)(eq + 1); + } else if (longopts[match].has_arg == required_argument) { + if (optind >= argc) { + if (opterr) + fprintf(stderr, + "%s: option '--%s' requires an argument\n", + argv[0], longopts[match].name); + return '?'; + } + optarg = argv[optind++]; + } + } + + if (longopts[match].flag) { + *longopts[match].flag = longopts[match].val; + return 0; + } + return longopts[match].val; + } + + /* Short option */ + return getopt(argc, argv, optstring); +} diff --git a/lib/getopt/getopt.h b/lib/getopt/getopt.h new file mode 100644 index 00000000..3510b426 --- /dev/null +++ b/lib/getopt/getopt.h @@ -0,0 +1,39 @@ +/* + * Portable getopt_long implementation for systems without . + * Based on public domain getopt implementations. + * Used on Windows with MSVC (MinGW provides its own getopt). + */ + +#ifndef PORTABLE_GETOPT_H +#define PORTABLE_GETOPT_H + +#ifdef __cplusplus +extern "C" { +#endif + +extern char *optarg; +extern int optind; +extern int opterr; +extern int optopt; + +#define no_argument 0 +#define required_argument 1 +#define optional_argument 2 + +struct option { + const char *name; + int has_arg; + int *flag; + int val; +}; + +int getopt(int argc, char *const argv[], const char *optstring); + +int getopt_long(int argc, char *const argv[], const char *optstring, + const struct option *longopts, int *longindex); + +#ifdef __cplusplus +} +#endif + +#endif /* PORTABLE_GETOPT_H */ diff --git a/src/compat_win32.h b/src/compat_win32.h new file mode 100644 index 00000000..a6bb1863 --- /dev/null +++ b/src/compat_win32.h @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#ifndef OPENFORTIVPN_COMPAT_WIN32_H +#define OPENFORTIVPN_COMPAT_WIN32_H + +#ifdef _WIN32 + +#include +#include +#include +#include +#include + +/* POSIX type compatibility */ +#ifndef ssize_t +#ifdef _WIN64 +typedef __int64 ssize_t; +#else +typedef int ssize_t; +#endif +#endif + +#ifdef _MSC_VER +typedef int pid_t; +#endif +typedef unsigned int uid_t; + +/* File descriptor compatibility */ +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 + +/* Socket compatibility: Winsock uses closesocket/recv/send */ +#define close_socket(s) closesocket(s) +#define sock_read(s, b, l) recv(s, (char *)(b), l, 0) +#define sock_write(s, b, l) send(s, (const char *)(b), l, 0) + +/* Sleep compatibility */ +#define sleep(s) Sleep((s) * 1000) +#define usleep(us) Sleep((us) / 1000) + +/* Signal compatibility - minimal stubs */ +#ifndef SIGPIPE +#define SIGPIPE 13 +#endif +#ifndef SIGHUP +#define SIGHUP 1 +#endif +#ifndef SIGTERM +#define SIGTERM 15 +#endif +#ifndef SIGINT +#define SIGINT 2 +#endif + +/* Misc POSIX compatibility */ +#define F_OK 0 +#define access(path, mode) _access(path, mode) + +static inline uid_t geteuid(void) +{ + /* On Windows, we check admin privileges differently */ + return 0; /* Always return 0 (root) - actual check done elsewhere */ +} + +/* Environment variable compatibility */ +#define setenv(name, value, overwrite) _putenv_s(name, value) + +/* POSIX string function compatibility for MSVC */ +#ifdef _MSC_VER +#define strcasecmp _stricmp +#define strncasecmp _strnicmp +#define strtok_r strtok_s +#endif + +/* + * strcasestr, memmem, isatty/fileno shims. + * Cannot #include because our src/io.h shadows the system header. + */ +#include +#include +#include + +#ifdef _MSC_VER +int _isatty(int fd); +int _fileno(FILE *stream); +#define isatty _isatty +#define fileno _fileno +#else +int isatty(int fd); +int fileno(FILE *stream); +#endif + +#ifndef HAVE_STRCASESTR +static inline char *strcasestr(const char *haystack, const char *needle) +{ + size_t nlen = strlen(needle); + + if (nlen == 0) + return (char *)haystack; + for (; *haystack; haystack++) { + if (strncasecmp(haystack, needle, nlen) == 0) + return (char *)haystack; + } + return NULL; +} +#endif + +#ifndef HAVE_MEMMEM +static inline void *memmem(const void *haystack, size_t hlen, + const void *needle, size_t nlen) +{ + const unsigned char *h = (const unsigned char *)haystack; + size_t i; + + if (nlen == 0) + return (void *)haystack; + if (nlen > hlen) + return NULL; + for (i = 0; i <= hlen - nlen; i++) { + if (memcmp(h + i, needle, nlen) == 0) + return (void *)(h + i); + } + return NULL; +} +#endif + +#ifndef HAVE_GETLINE +static inline ssize_t getline(char **lineptr, size_t *n, FILE *stream) +{ + size_t pos = 0; + int c; + + if (!lineptr || !n || !stream) + return -1; + + if (!*lineptr) { + *n = 128; + *lineptr = (char *)malloc(*n); + if (!*lineptr) + return -1; + } + + while ((c = fgetc(stream)) != EOF) { + if (pos + 1 >= *n) { + char *tmp; + + *n *= 2; + tmp = (char *)realloc(*lineptr, *n); + if (!tmp) + return -1; + *lineptr = tmp; + } + (*lineptr)[pos++] = (char)c; + if (c == '\n') + break; + } + + if (pos == 0 && c == EOF) + return -1; + + (*lineptr)[pos] = '\0'; + return (ssize_t)pos; +} +#endif /* !HAVE_GETLINE */ + +/* Initialize Winsock - call once at startup */ +static inline int winsock_init(void) +{ + WSADATA wsa_data; + + return WSAStartup(MAKEWORD(2, 2), &wsa_data); +} + +static inline void winsock_cleanup(void) +{ + WSACleanup(); +} + +#else /* !_WIN32 */ + +/* On Unix, sockets are file descriptors */ +#define close_socket(s) close(s) +#define sock_read(s, b, l) read(s, b, l) +#define sock_write(s, b, l) write(s, b, l) +#define winsock_init() 0 +#define winsock_cleanup() ((void)0) + +#endif /* _WIN32 */ + +#endif /* OPENFORTIVPN_COMPAT_WIN32_H */ diff --git a/src/config.c b/src/config.c index d423e9db..f5dcf50b 100644 --- a/src/config.c +++ b/src/config.c @@ -64,7 +64,7 @@ const struct vpn_config invalid_cfg = { .use_syslog = -1, .half_internet_routes = -1, .persistent = -1, -#if HAVE_USR_SBIN_PPPD +#if HAVE_USR_SBIN_PPPD || defined(_WIN32) .pppd_log = NULL, .pppd_plugin = NULL, .pppd_ipparam = NULL, @@ -333,7 +333,7 @@ int load_config(struct vpn_config *cfg, const char *filename) continue; } cfg->persistent = persistent; -#if HAVE_USR_SBIN_PPPD +#if HAVE_USR_SBIN_PPPD || defined(_WIN32) } else if (strcmp(key, "pppd-use-peerdns") == 0) { int pppd_use_peerdns = strtob(val); @@ -497,7 +497,7 @@ void destroy_vpn_config(struct vpn_config *cfg) free(cfg->otp_prompt); free(cfg->pinentry); free(cfg->cookie); -#if HAVE_USR_SBIN_PPPD +#if HAVE_USR_SBIN_PPPD || defined(_WIN32) free(cfg->pppd_log); free(cfg->pppd_plugin); free(cfg->pppd_ipparam); @@ -572,7 +572,7 @@ void merge_config(struct vpn_config *dst, struct vpn_config *src) dst->half_internet_routes = src->half_internet_routes; if (src->persistent != invalid_cfg.persistent) dst->persistent = src->persistent; -#if HAVE_USR_SBIN_PPPD +#if HAVE_USR_SBIN_PPPD || defined(_WIN32) if (src->pppd_log) { free(dst->pppd_log); dst->pppd_log = src->pppd_log; diff --git a/src/config.h b/src/config.h index fdf11220..77e28354 100644 --- a/src/config.h +++ b/src/config.h @@ -18,8 +18,15 @@ #ifndef OPENFORTIVPN_CONFIG_H #define OPENFORTIVPN_CONFIG_H +#ifdef _WIN32 +#include "compat_win32.h" +#ifndef IF_NAMESIZE +#define IF_NAMESIZE 256 +#endif +#else #include #include +#endif #include #include @@ -27,8 +34,10 @@ #if HAVE_USR_SBIN_PPPD #define PPP_DAEMON "pppd" -#else +#elif HAVE_USR_SBIN_PPP #define PPP_DAEMON "ppp" +#else +#define PPP_DAEMON "wintun" #endif #define SHA256LEN (256 / 8) @@ -95,7 +104,8 @@ struct vpn_config { unsigned int persistent; -#if HAVE_USR_SBIN_PPPD +#if HAVE_USR_SBIN_PPPD || defined(_WIN32) + /* pppd options - on Windows, only pppd_use_peerdns is meaningful */ char *pppd_log; char *pppd_plugin; char *pppd_ipparam; diff --git a/src/event.c b/src/event.c new file mode 100644 index 00000000..637c94b5 --- /dev/null +++ b/src/event.c @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 "event.h" + +#include + +#include +#include +#include +#include +#include + +static int events_enabled; +static int64_t seq_counter; +static pthread_mutex_t event_mutex = PTHREAD_MUTEX_INITIALIZER; + +void event_init(int enabled) +{ + events_enabled = enabled; +} + +void event_emit(const char *type, const char *json_fields_fmt, ...) +{ + va_list ap; + char fields_buf[2048]; + + if (!events_enabled) + return; + + pthread_mutex_lock(&event_mutex); + + fprintf(stderr, "{\"event\":\"%s\",\"ts\":%lld,\"seq\":%lld", + type, (long long)time(NULL), (long long)seq_counter++); + + if (json_fields_fmt && json_fields_fmt[0] != '\0') { + va_start(ap, json_fields_fmt); + vsnprintf(fields_buf, sizeof(fields_buf), json_fields_fmt, ap); + va_end(ap); + fprintf(stderr, ",%s", fields_buf); + } + + fprintf(stderr, "}\n"); + fflush(stderr); + + pthread_mutex_unlock(&event_mutex); +} + +char *json_escape(char *buf, size_t buf_size, const char *input) +{ + size_t i = 0; + size_t o = 0; + + if (!buf || buf_size == 0) + return buf; + + /* Reserve space for NUL terminator */ + buf_size--; + + if (!input) { + buf[0] = '\0'; + return buf; + } + + while (input[i] != '\0' && o < buf_size) { + unsigned char c = (unsigned char)input[i]; + + if (c == '"') { + if (o + 2 > buf_size) + break; + buf[o++] = '\\'; + buf[o++] = '"'; + } else if (c == '\\') { + if (o + 2 > buf_size) + break; + buf[o++] = '\\'; + buf[o++] = '\\'; + } else if (c == '\n') { + if (o + 2 > buf_size) + break; + buf[o++] = '\\'; + buf[o++] = 'n'; + } else if (c == '\r') { + if (o + 2 > buf_size) + break; + buf[o++] = '\\'; + buf[o++] = 'r'; + } else if (c == '\t') { + if (o + 2 > buf_size) + break; + buf[o++] = '\\'; + buf[o++] = 't'; + } else if (c < 0x20) { + if (o + 6 > buf_size) + break; + snprintf(buf + o, 7, "\\u%04x", c); + o += 6; + } else { + buf[o++] = c; + } + i++; + } + + buf[o] = '\0'; + return buf; +} + +void event_emit_cert_error(const char *digest, const char *reason) +{ + char buf[512]; + char esc_d[256], esc_r[128]; + + json_escape(esc_d, sizeof(esc_d), digest); + json_escape(esc_r, sizeof(esc_r), reason); + snprintf(buf, sizeof(buf), + "\"digest\":\"%s\",\"reason\":\"%s\"", + esc_d, esc_r); + event_emit("cert_error", buf); +} + +void event_emit_error(int code, const char *category, + const char *msg) +{ + char buf[512]; + char esc_c[64], esc_m[256]; + + json_escape(esc_c, sizeof(esc_c), category); + json_escape(esc_m, sizeof(esc_m), msg); + snprintf(buf, sizeof(buf), + "\"code\":%d,\"category\":\"%s\",\"message\":\"%s\"", + code, esc_c, esc_m); + event_emit("error", buf); +} + +void event_emit_tunnel_up(const char *ip, const char *d1, + const char *d2) +{ + char buf[256]; + + snprintf(buf, sizeof(buf), + "\"local_ip\":\"%s\",\"dns1\":\"%s\",\"dns2\":\"%s\"", + ip, d1, d2); + event_emit("tunnel_up", buf); +} diff --git a/src/event.h b/src/event.h new file mode 100644 index 00000000..ffe6bf45 --- /dev/null +++ b/src/event.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#ifndef OPENFORTIVPN_EVENT_H +#define OPENFORTIVPN_EVENT_H + +#include + +/* Initialize the event system. enabled=1 turns on JSON output to stderr. */ +void event_init(int enabled); + +/* Emit a JSON event line to stderr. fmt contains key:value pairs in JSON. */ +void event_emit(const char *type, const char *json_fields_fmt, ...); + +/* + * Helper: escape a string for JSON (handles quotes, backslashes, control chars). + * Writes to buf, returns buf. Truncates if output exceeds buf_size. + */ +char *json_escape(char *buf, size_t buf_size, const char *input); + +/* Typed event emitters that avoid split-string lint warnings. */ +void event_emit_cert_error(const char *digest, const char *reason); +void event_emit_error(int code, const char *category, const char *msg); +void event_emit_tunnel_up(const char *ip, const char *d1, const char *d2); + +#endif /* OPENFORTIVPN_EVENT_H */ diff --git a/src/exit_codes.h b/src/exit_codes.h new file mode 100644 index 00000000..c83fefb9 --- /dev/null +++ b/src/exit_codes.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#ifndef OPENFORTIVPN_EXIT_CODES_H +#define OPENFORTIVPN_EXIT_CODES_H + +enum ofv_exit_code { + OFV_EXIT_OK = 0, + OFV_EXIT_GENERIC = 1, /* legacy fallback */ + OFV_EXIT_DNS_FAILED = 10, + OFV_EXIT_TCP_FAILED = 11, + OFV_EXIT_TLS_FAILED = 12, + OFV_EXIT_CERT_FAILED = 13, + OFV_EXIT_AUTH_FAILED = 20, + OFV_EXIT_OTP_MISSING = 21, + OFV_EXIT_SAML_FAILED = 22, + OFV_EXIT_ALLOC_DENIED = 23, + OFV_EXIT_CONFIG_FAILED = 30, + OFV_EXIT_ADAPTER_FAILED = 40, + OFV_EXIT_TUNNEL_FAILED = 41, + OFV_EXIT_PERMISSION = 50, + OFV_EXIT_SIGNAL = 60 +}; + +#endif /* OPENFORTIVPN_EXIT_CODES_H */ diff --git a/src/hdlc.h b/src/hdlc.h index 7e61141b..916d5b11 100644 --- a/src/hdlc.h +++ b/src/hdlc.h @@ -18,7 +18,12 @@ #ifndef OPENFORTIVPN_HDLC_H #define OPENFORTIVPN_HDLC_H +#ifdef _WIN32 +#include "compat_win32.h" +typedef long off_t; +#else #include +#endif #include #include diff --git a/src/http.c b/src/http.c index a2e342f9..bbedbb8e 100644 --- a/src/http.c +++ b/src/http.c @@ -22,8 +22,12 @@ #include "userinput.h" #include "log.h" +#ifndef _WIN32 #include #include +#else +#include "compat_win32.h" +#endif #include #include diff --git a/src/http_server_win.c b/src/http_server_win.c new file mode 100644 index 00000000..da8f59f2 --- /dev/null +++ b/src/http_server_win.c @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2025 Rainer Keller + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + * + * Windows implementation of the embedded HTTP server for SAML login. + * Uses Winsock2 recv/send instead of POSIX read/write. + */ + +#ifdef _WIN32 + +#include "http_server.h" +#include "log.h" + +#include +#include +#include + +#include +#include +#include + +#define HTTP_BUF_SIZE 8192 + +static const char http_response[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n\r\n" + "

Authentication successful

You can close this window and return to the terminal.

"; + +/* + * Parse SAML cookie and session ID from the HTTP GET request. + */ +static int parse_saml_request(const char *request, struct vpn_config *config) +{ + const char *cookie_start, *session_start; + const char *value_start, *value_end; + + /* Look for id= parameter */ + session_start = strstr(request, "id="); + if (session_start) { + value_start = session_start + 3; + value_end = strchr(value_start, '&'); + if (!value_end) + value_end = strchr(value_start, ' '); + if (!value_end) + value_end = value_start + strlen(value_start); + + size_t len = value_end - value_start; + + if (len >= MAX_SAML_SESSION_ID_LENGTH) + len = MAX_SAML_SESSION_ID_LENGTH; + strncpy(config->saml_session_id, value_start, len); + config->saml_session_id[len] = '\0'; + } + + /* Look for SVPNCOOKIE in the request */ + cookie_start = strstr(request, "SVPNCOOKIE="); + if (!cookie_start) + cookie_start = strstr(request, "cookie="); + if (cookie_start) { + value_start = strchr(cookie_start, '=') + 1; + value_end = strchr(value_start, '&'); + if (!value_end) + value_end = strchr(value_start, ' '); + if (!value_end) + value_end = value_start + strlen(value_start); + + size_t len = value_end - value_start; + + if (len > COOKIE_SIZE) + len = COOKIE_SIZE; + + if (config->cookie) + free(config->cookie); + config->cookie = malloc(len + 1); + if (config->cookie) { + strncpy(config->cookie, value_start, len); + config->cookie[len] = '\0'; + } + } + + return (config->cookie != NULL) ? 0 : -1; +} + +int wait_for_http_request(struct vpn_config *config) +{ + SOCKET server_sock = INVALID_SOCKET; + SOCKET client_sock = INVALID_SOCKET; + struct sockaddr_in addr; + char buffer[HTTP_BUF_SIZE]; + int ret = -1; + int opt = 1; + fd_set read_fds; + struct timeval timeout; + + /* Create listening socket */ + server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (server_sock == INVALID_SOCKET) { + log_error("socket: %d\n", WSAGetLastError()); + return -1; + } + + setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, + (const char *)&opt, sizeof(opt)); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(config->saml_port); + + if (bind(server_sock, (struct sockaddr *)&addr, sizeof(addr)) == + SOCKET_ERROR) { + log_error("bind: %d\n", WSAGetLastError()); + goto cleanup; + } + + if (listen(server_sock, 1) == SOCKET_ERROR) { + log_error("listen: %d\n", WSAGetLastError()); + goto cleanup; + } + + log_info("Waiting for SAML login on port %d...\n", config->saml_port); + log_info("Open http://127.0.0.1:%d in your browser to authenticate.\n", + config->saml_port); + + /* Wait for connection with timeout */ + while (1) { + FD_ZERO(&read_fds); + FD_SET(server_sock, &read_fds); + timeout.tv_sec = 10; + timeout.tv_usec = 0; + + int sel = select(0, &read_fds, NULL, NULL, &timeout); + + if (sel == SOCKET_ERROR) { + log_error("select: %d\n", WSAGetLastError()); + goto cleanup; + } + if (sel == 0) { + /* Timeout - check if we should stop */ + continue; + } + + client_sock = accept(server_sock, NULL, NULL); + if (client_sock == INVALID_SOCKET) + continue; + + /* Read HTTP request */ + int bytes = recv(client_sock, buffer, sizeof(buffer) - 1, 0); + + if (bytes > 0) { + buffer[bytes] = '\0'; + log_debug("SAML HTTP request: %s\n", buffer); + + /* Parse and extract cookie/session */ + if (parse_saml_request(buffer, config) == 0) { + /* Send success response */ + send(client_sock, http_response, + (int)strlen(http_response), 0); + closesocket(client_sock); + ret = 0; + break; + } + + /* Send response even on failure */ + send(client_sock, http_response, + (int)strlen(http_response), 0); + } + + closesocket(client_sock); + client_sock = INVALID_SOCKET; + } + +cleanup: + if (client_sock != INVALID_SOCKET) + closesocket(client_sock); + if (server_sock != INVALID_SOCKET) + closesocket(server_sock); + + return ret; +} + +#endif /* _WIN32 */ diff --git a/src/io.h b/src/io.h index f99be27a..37072993 100644 --- a/src/io.h +++ b/src/io.h @@ -18,7 +18,9 @@ #ifndef OPENFORTIVPN_IO_H #define OPENFORTIVPN_IO_H +#ifndef _WIN32 #include +#endif #include #include diff --git a/src/io_win.c b/src/io_win.c new file mode 100644 index 00000000..c0155eb7 --- /dev/null +++ b/src/io_win.c @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + * + * Windows I/O loop implementation. + * Replaces io.c for Windows builds, using wintun instead of pppd. + */ + +#ifdef _WIN32 + +#include "tunnel.h" +#include "ppp.h" +#include "wintun.h" +#include "ssl.h" +#include "log.h" + +#include +#include + +#include +#include + +#define PKT_BUF_SZ 0x1000 + +/* Windows semaphore abstraction (matching io.c pattern) */ +typedef HANDLE os_semaphore_t; +#define SEM_INIT(sem, x, value) \ + (*(sem) = CreateSemaphore(NULL, value, LONG_MAX, NULL)) +#define SEM_WAIT(sem) WaitForSingleObject(*(sem), INFINITE) +#define SEM_POST(sem) ReleaseSemaphore(*(sem), 1, NULL) +#define SEM_DESTROY(sem) CloseHandle(*(sem)) + +static os_semaphore_t sem_ppp_ready; +static os_semaphore_t sem_if_config; +static os_semaphore_t sem_stop_io; + +/* + * Shutdown flag: set to 1 when io_loop wants all threads to exit. + * Threads check this after every blocking call returns an error. + */ +static volatile LONG shutting_down; + +/* Global variable to pass signal out of its handler */ +volatile long sig_received; + +int get_sig_received(void) +{ + return (int)sig_received; +} + +static BOOL WINAPI win_ctrl_handler(DWORD ctrl_type) +{ + if (ctrl_type == CTRL_C_EVENT || + ctrl_type == CTRL_BREAK_EVENT || + ctrl_type == CTRL_CLOSE_EVENT) { + InterlockedExchange(&sig_received, SIGTERM); + SEM_POST(&sem_stop_io); + return TRUE; + } + return FALSE; +} + +/* + * Packet pool operations. + * + * pool_pop blocks until a packet is available. To unblock a thread + * during shutdown, push a NULL-length poison pill via pool_poison. + */ +static void pool_push(struct ppp_packet_pool *pool, + struct ppp_packet *new) +{ + struct ppp_packet *current; + + pthread_mutex_lock(&pool->mutex); + + new->next = NULL; + current = pool->list_head; + if (current == NULL) { + pool->list_head = new; + } else { + while (current->next != NULL) + current = current->next; + current->next = new; + } + + pthread_cond_signal(&pool->new_data); + pthread_mutex_unlock(&pool->mutex); +} + +/* + * Returns NULL during shutdown (poison pill received). + */ +static struct ppp_packet *pool_pop(struct ppp_packet_pool *pool) +{ + struct ppp_packet *packet; + + pthread_mutex_lock(&pool->mutex); + while (pool->list_head == NULL) + pthread_cond_wait(&pool->new_data, &pool->mutex); + packet = pool->list_head; + pool->list_head = packet->next; + pthread_mutex_unlock(&pool->mutex); + + /* Poison pill: len == 0 and allocated with just the header */ + if (packet->len == 0) { + free(packet); + return NULL; + } + + return packet; +} + +/* + * Push a poison pill to unblock a thread waiting in pool_pop. + */ +static void pool_poison(struct ppp_packet_pool *pool) +{ + struct ppp_packet *pill; + + pill = malloc(sizeof(*pill) + 6); + if (!pill) + return; + pill->len = 0; + pill->next = NULL; + pool_push(pool, pill); +} + +/* + * Thread: Read packets from TLS socket, process through PPP. + * Exits when SSL_read returns an error (socket closed by shutdown). + */ +static void *ssl_read_thread(void *arg) +{ + struct tunnel *tunnel = (struct tunnel *)arg; + struct ppp_context *ppp_ctx = + (struct ppp_context *)tunnel->ppp_ctx; + + log_debug("%s thread\n", __func__); + + while (!InterlockedCompareExchange(&shutting_down, 0, 0)) { + uint8_t header[6]; + uint16_t total, magic, size; + int ret; + struct ppp_packet *packet; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ppp_ret; + + ret = safe_ssl_read_all(tunnel->ssl_handle, header, 6); + if (ret < 0) + break; + + total = (header[0] << 8) | header[1]; + magic = (header[2] << 8) | header[3]; + size = (header[4] << 8) | header[5]; + + if (magic != 0x5050) + break; + + if (size == 0) + continue; + + packet = malloc(sizeof(*packet) + 6 + size); + if (!packet) + break; + packet->len = size; + + ret = safe_ssl_read_all(tunnel->ssl_handle, + pkt_data(packet), size); + if (ret < 0) { + free(packet); + break; + } + + log_debug("gateway ---> tun (%u bytes)\n", size); + + ppp_ret = ppp_process_incoming(ppp_ctx, + pkt_data(packet), size, + &response, &resp_len, + &ip_payload, &ip_len); + + if (ppp_ret == PPP_RET_IP_PACKET) { + struct ppp_packet *ip_pkt; + + ip_pkt = malloc(sizeof(*ip_pkt) + 6 + ip_len); + if (ip_pkt) { + ip_pkt->len = ip_len; + memcpy(pkt_data(ip_pkt), + ip_payload, ip_len); + pool_push(&tunnel->ssl_to_pty_pool, + ip_pkt); + } + free(ip_payload); + } else if (ppp_ret == PPP_RET_NEGOTIATE) { + SEM_POST(&sem_if_config); + } else if (ppp_ret == PPP_RET_TERMINATED) { + free(response); + free(packet); + break; + } + + if (response) { + struct ppp_packet *resp_pkt; + + resp_pkt = malloc(sizeof(*resp_pkt) + 6 + resp_len); + if (resp_pkt) { + resp_pkt->len = resp_len; + memcpy(pkt_data(resp_pkt), + response, resp_len); + pool_push(&tunnel->pty_to_ssl_pool, + resp_pkt); + } + free(response); + } + + free(packet); + } + + SEM_POST(&sem_stop_io); + return NULL; +} + +/* + * Thread: Pop PPP packets and write to TLS. + * Exits when it receives a poison pill (NULL from pool_pop). + */ +static void *ssl_write_thread(void *arg) +{ + struct tunnel *tunnel = (struct tunnel *)arg; + + log_debug("%s thread\n", __func__); + + while (1) { + struct ppp_packet *packet; + int ret; + + packet = pool_pop(&tunnel->pty_to_ssl_pool); + if (!packet) + break; /* poison pill */ + + pkt_header(packet)[0] = (6 + packet->len) >> 8; + pkt_header(packet)[1] = (6 + packet->len) & 0xff; + pkt_header(packet)[2] = 0x50; + pkt_header(packet)[3] = 0x50; + pkt_header(packet)[4] = packet->len >> 8; + pkt_header(packet)[5] = packet->len & 0xff; + + do { + ret = safe_ssl_write(tunnel->ssl_handle, + packet->content, + 6 + packet->len); + } while (ret == 0 && + !InterlockedCompareExchange( + &shutting_down, 0, 0)); + + free(packet); + if (ret < 0) + break; + } + + SEM_POST(&sem_stop_io); + return NULL; +} + +/* + * Thread: Read IP packets from wintun, encapsulate in PPP. + * Exits when the wintun session is ended (ReceivePacket returns NULL + * and GetLastError is ERROR_INVALID_DATA after EndSession). + */ +static void *tun_read_thread(void *arg) +{ + struct tunnel *tunnel = (struct tunnel *)arg; + WINTUN_SESSION_HANDLE session = + (WINTUN_SESSION_HANDLE)tunnel->tun_session; + struct wintun_api *api = + (struct wintun_api *)tunnel->tun_adapter; + HANDLE read_event; + + SEM_WAIT(&sem_ppp_ready); + + log_debug("%s thread\n", __func__); + + read_event = api->GetReadWaitEvent(session); + + while (!InterlockedCompareExchange(&shutting_down, 0, 0)) { + DWORD pkt_size; + BYTE *pkt; + DWORD wait_ret; + + wait_ret = WaitForSingleObject(read_event, 1000); + if (wait_ret == WAIT_TIMEOUT) + continue; + if (wait_ret != WAIT_OBJECT_0) + break; + + while ((pkt = api->ReceivePacket(session, + &pkt_size)) != NULL) { + uint8_t *ppp_data; + size_t ppp_len; + struct ppp_packet *packet; + + if (ppp_encapsulate_ip(pkt, pkt_size, + &ppp_data, + &ppp_len) == 0) { + packet = malloc(sizeof(*packet) + + 6 + ppp_len); + if (packet) { + packet->len = ppp_len; + memcpy(pkt_data(packet), + ppp_data, ppp_len); + pool_push( + &tunnel->pty_to_ssl_pool, + packet); + log_debug( + "tun ---> gateway (%lu bytes)\n", + (unsigned long)ppp_len); + } + free(ppp_data); + } + + api->ReleaseReceivePacket(session, pkt); + } + + /* + * ReceivePacket returned NULL. If the session was + * ended by shutdown, ERROR_INVALID_DATA is set. + */ + if (GetLastError() == ERROR_INVALID_DATA) + break; + } + + SEM_POST(&sem_stop_io); + return NULL; +} + +/* + * Thread: Pop IP packets and write to wintun. + * Exits when it receives a poison pill or AllocateSendPacket fails + * because the session was ended. + */ +static void *tun_write_thread(void *arg) +{ + struct tunnel *tunnel = (struct tunnel *)arg; + WINTUN_SESSION_HANDLE session = + (WINTUN_SESSION_HANDLE)tunnel->tun_session; + struct wintun_api *api = + (struct wintun_api *)tunnel->tun_adapter; + + SEM_WAIT(&sem_ppp_ready); + + log_debug("%s thread\n", __func__); + + while (1) { + struct ppp_packet *packet; + BYTE *buf; + + packet = pool_pop(&tunnel->ssl_to_pty_pool); + if (!packet) + break; /* poison pill */ + + buf = api->AllocateSendPacket(session, + (DWORD)packet->len); + if (buf) { + memcpy(buf, pkt_data(packet), packet->len); + api->SendPacket(session, buf); + } else { + /* Session ended — exit cleanly */ + free(packet); + break; + } + + free(packet); + } + + SEM_POST(&sem_stop_io); + return NULL; +} + +/* + * Thread: Wait for PPP negotiation, then configure the interface. + */ +static void *if_config_thread(void *arg) +{ + struct tunnel *tunnel = (struct tunnel *)arg; + int timeout = 60000; + + log_debug("%s thread\n", __func__); + + SEM_WAIT(&sem_if_config); + + /* Unblock tun_read and tun_write */ + SEM_POST(&sem_ppp_ready); + SEM_POST(&sem_ppp_ready); + + while (timeout > 0) { + if (ppp_interface_is_up(tunnel)) { + if (tunnel->on_ppp_if_up != NULL) + if (tunnel->on_ppp_if_up(tunnel)) + goto error; + tunnel->state = STATE_UP; + break; + } + Sleep(200); + timeout -= 200; + } + + if (tunnel->state != STATE_UP) { + log_error("Timed out waiting for interface UP.\n"); + goto error; + } + + return NULL; +error: + SEM_POST(&sem_stop_io); + return NULL; +} + +int io_loop(struct tunnel *tunnel) +{ + int tcp_nodelay_flag = 1; + int ret = 0; + + pthread_t tun_read_thr; + pthread_t tun_write_thr; + pthread_t ssl_read_thr; + pthread_t ssl_write_thr; + pthread_t if_config_thr; + + InterlockedExchange(&shutting_down, 0); + + SEM_INIT(&sem_ppp_ready, 0, 0); + SEM_INIT(&sem_if_config, 0, 0); + SEM_INIT(&sem_stop_io, 0, 0); + + init_ppp_packet_pool(&tunnel->ssl_to_pty_pool); + init_ppp_packet_pool(&tunnel->pty_to_ssl_pool); + + if (setsockopt(tunnel->ssl_socket, IPPROTO_TCP, + TCP_NODELAY, + (const char *)&tcp_nodelay_flag, + sizeof(int))) { + log_error("setsockopt TCP_NODELAY: %d\n", + WSAGetLastError()); + goto err_cleanup; + } + + SetConsoleCtrlHandler(win_ctrl_handler, TRUE); + + /* Seed PPP negotiation */ + { + struct ppp_context *ppp_ctx = + (struct ppp_context *)tunnel->ppp_ctx; + uint8_t *start_pkt; + size_t start_len; + + if (ppp_start_negotiation(ppp_ctx, &start_pkt, + &start_len) == 0) { + struct ppp_packet *pkt; + + pkt = malloc(sizeof(*pkt) + 6 + start_len); + if (pkt) { + pkt->len = start_len; + memcpy(pkt_data(pkt), start_pkt, + start_len); + pool_push(&tunnel->pty_to_ssl_pool, + pkt); + } + free(start_pkt); + } + } + + /* Create worker threads */ + if (pthread_create(&ssl_read_thr, NULL, + ssl_read_thread, tunnel) || + pthread_create(&ssl_write_thr, NULL, + ssl_write_thread, tunnel) || + pthread_create(&tun_read_thr, NULL, + tun_read_thread, tunnel) || + pthread_create(&tun_write_thr, NULL, + tun_write_thread, tunnel) || + pthread_create(&if_config_thr, NULL, + if_config_thread, tunnel)) { + log_error("Failed to create I/O threads\n"); + goto err_cleanup; + } + + /* Block until any thread signals shutdown */ + SEM_WAIT(&sem_stop_io); + + /* + * Graceful shutdown sequence: + * 1. Set shutdown flag so loops check on next iteration + * 2. Close the SSL socket — unblocks ssl_read/ssl_write + * 3. End the wintun session — unblocks tun_read/tun_write + * 4. Poison the packet pools — unblocks pool_pop waiters + * 5. Join all threads (they exit through normal error paths) + */ + log_info("Shutting down I/O...\n"); + InterlockedExchange(&shutting_down, 1); + + /* Unblock SSL threads by closing the socket */ + if (tunnel->ssl_handle) { + SSL_shutdown(tunnel->ssl_handle); + SSL_free(tunnel->ssl_handle); + tunnel->ssl_handle = NULL; + } + if (tunnel->ssl_socket >= 0) { + closesocket(tunnel->ssl_socket); + tunnel->ssl_socket = -1; + } + + /* Unblock TUN threads by ending the session */ + if (tunnel->tun_session) { + struct wintun_api *api = + (struct wintun_api *)tunnel->tun_adapter; + + api->EndSession( + (WINTUN_SESSION_HANDLE)tunnel->tun_session); + tunnel->tun_session = NULL; + } + + /* Unblock pool_pop waiters */ + pool_poison(&tunnel->pty_to_ssl_pool); + pool_poison(&tunnel->ssl_to_pty_pool); + + /* Unblock threads still waiting on semaphores */ + SEM_POST(&sem_ppp_ready); + SEM_POST(&sem_ppp_ready); + SEM_POST(&sem_if_config); + + pthread_join(ssl_read_thr, NULL); + pthread_join(ssl_write_thr, NULL); + pthread_join(tun_read_thr, NULL); + pthread_join(tun_write_thr, NULL); + pthread_join(if_config_thr, NULL); + + log_info("I/O threads stopped.\n"); + +err_cleanup: + destroy_ppp_packet_pool(&tunnel->ssl_to_pty_pool); + destroy_ppp_packet_pool(&tunnel->pty_to_ssl_pool); + + SEM_DESTROY(&sem_ppp_ready); + SEM_DESTROY(&sem_if_config); + SEM_DESTROY(&sem_stop_io); + + SetConsoleCtrlHandler(win_ctrl_handler, FALSE); + + return ret; +} + +#endif /* _WIN32 */ diff --git a/src/ipv4.h b/src/ipv4.h index d8337b01..34e3ac05 100644 --- a/src/ipv4.h +++ b/src/ipv4.h @@ -18,10 +18,14 @@ #ifndef OPENFORTIVPN_IPV4_H #define OPENFORTIVPN_IPV4_H +#ifdef _WIN32 +#include "compat_win32.h" +#else #include #include #include #include +#endif #if !HAVE_RT_ENTRY_WITH_RT_DST /* @@ -94,4 +98,10 @@ int ipv4_restore_routes(struct tunnel *tunnel); int ipv4_add_nameservers_to_resolv_conf(struct tunnel *tunnel); int ipv4_del_nameservers_from_resolv_conf(struct tunnel *tunnel); +#ifdef _WIN32 +#include +void ipv4_win_set_tun_luid(NET_LUID *luid); +void ipv4_apply_deferred_routes(void); +#endif + #endif diff --git a/src/ipv4_win.c b/src/ipv4_win.c new file mode 100644 index 00000000..ca79ceb4 --- /dev/null +++ b/src/ipv4_win.c @@ -0,0 +1,482 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + * + * Windows implementation of IPv4 routing and DNS configuration. + * Uses the Windows IP Helper API (iphlpapi.dll). + */ + +#ifdef _WIN32 + +#include "tunnel.h" +#include "log.h" + +#include +#include +#include +#include +#include + +#include +#include + +#pragma comment(lib, "iphlpapi.lib") +#pragma comment(lib, "ws2_32.lib") + +/* + * Windows route entry - maps to the ipv4_config rtentry fields. + */ +struct win_route { + MIB_IPFORWARD_ROW2 row; + int valid; +}; + +static struct win_route saved_default_route; +static struct win_route vpn_gateway_route; +static NET_LUID tun_luid; +static int tun_luid_valid; + +void ipv4_win_set_tun_luid(NET_LUID *luid) +{ + tun_luid = *luid; + tun_luid_valid = 1; +} + +/* + * Get the best route to a destination address. + */ +static int get_best_route(struct in_addr dest, MIB_IPFORWARD_ROW2 *route) +{ + SOCKADDR_INET dest_addr; + SOCKADDR_INET best_src; + DWORD ret; + + memset(&dest_addr, 0, sizeof(dest_addr)); + dest_addr.Ipv4.sin_family = AF_INET; + dest_addr.Ipv4.sin_addr = dest; + + ret = GetBestRoute2(NULL, 0, NULL, &dest_addr, 0, route, &best_src); + if (ret != NO_ERROR) { + log_error("GetBestRoute2 failed: %lu\n", ret); + return -1; + } + return 0; +} + +/* + * Add a route using the IP Helper API. + */ +static int add_route(struct in_addr dest, struct in_addr mask, + struct in_addr gateway, NET_LUID *luid, + ULONG metric) +{ + MIB_IPFORWARD_ROW2 row; + DWORD ret; + + InitializeIpForwardEntry(&row); + + if (luid) + row.InterfaceLuid = *luid; + row.DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET; + row.DestinationPrefix.Prefix.Ipv4.sin_addr = dest; + + /* Calculate prefix length from mask */ + uint32_t m = ntohl(mask.s_addr); + uint8_t prefix_len = 0; + + while (m & 0x80000000) { + prefix_len++; + m <<= 1; + } + row.DestinationPrefix.PrefixLength = prefix_len; + + row.NextHop.Ipv4.sin_family = AF_INET; + row.NextHop.Ipv4.sin_addr = gateway; + row.Metric = metric; + row.Protocol = MIB_IPPROTO_NETMGMT; + + ret = CreateIpForwardEntry2(&row); + if (ret != NO_ERROR && ret != ERROR_OBJECT_ALREADY_EXISTS) { + log_error("CreateIpForwardEntry2 failed: %lu\n", ret); + return -1; + } + return 0; +} + +/* + * Delete a route using the IP Helper API. + */ +static int del_route(MIB_IPFORWARD_ROW2 *row) +{ + DWORD ret; + + ret = DeleteIpForwardEntry2(row); + if (ret != NO_ERROR && ret != ERROR_NOT_FOUND) { + log_error("DeleteIpForwardEntry2 failed: %lu\n", ret); + return -1; + } + return 0; +} + +/* + * Save the current default route for later restoration. + */ +static int save_default_route(void) +{ + struct in_addr any; + + any.s_addr = 0; + if (get_best_route(any, &saved_default_route.row) == 0) { + saved_default_route.valid = 1; + return 0; + } + saved_default_route.valid = 0; + return -1; +} + +int ipv4_drop_wrong_route(struct tunnel *tunnel) +{ + /* On Windows, wintun doesn't create spurious routes like pppd does. */ + (void)tunnel; + return 0; +} + +int ipv4_add_split_vpn_route(struct tunnel *tunnel, char *dest, char *mask, + char *gateway) +{ + struct rtentry *route; + struct in_addr dest_addr, mask_addr, gw_addr; + + inet_pton(AF_INET, dest, &dest_addr); + inet_pton(AF_INET, mask, &mask_addr); + inet_pton(AF_INET, gateway, &gw_addr); + + log_info("Storing split route: %s/%s via %s\n", + dest, mask, gateway); + + /* + * Store the route for deferred application. On Windows the TUN + * adapter does not exist yet when auth_get_config parses the XML. + * Routes are applied later in ipv4_set_tunnel_routes(). + */ + if (tunnel->ipv4.split_routes == MAX_SPLIT_ROUTES) + return -1; + if ((tunnel->ipv4.split_rt == NULL) + || ((tunnel->ipv4.split_routes % STEP_SPLIT_ROUTES) == 0)) { + void *new_ptr; + + new_ptr = realloc(tunnel->ipv4.split_rt, + (size_t)(tunnel->ipv4.split_routes + + STEP_SPLIT_ROUTES) + * sizeof(*(tunnel->ipv4.split_rt))); + if (new_ptr == NULL) + return -1; + tunnel->ipv4.split_rt = new_ptr; + } + + route = &tunnel->ipv4.split_rt[tunnel->ipv4.split_routes++]; + memset(route, 0, sizeof(*route)); + cast_addr(&route->rt_dst)->sin_addr = dest_addr; + cast_addr(&route->rt_genmask)->sin_addr = mask_addr; + cast_addr(&route->rt_gateway)->sin_addr = gw_addr; + + return 0; +} + +int ipv4_set_tunnel_routes(struct tunnel *tunnel) +{ + struct in_addr gateway_ip = tunnel->config->gateway_ip; + struct in_addr any, full_mask, half_mask; + int ret; + + any.s_addr = 0; + full_mask.s_addr = htonl(0xFFFFFFFF); + half_mask.s_addr = htonl(0x80000000); + + /* Save existing default route */ + save_default_route(); + + /* + * Add route to VPN gateway via existing default route. + * This prevents routing loops in full-tunnel mode. For split-tunnel + * mode it is not strictly necessary — if it fails, continue anyway + * so the split routes can still be applied. + */ + if (saved_default_route.valid) { + struct in_addr saved_gw; + + saved_gw = saved_default_route.row.NextHop.Ipv4.sin_addr; + ret = add_route(gateway_ip, full_mask, saved_gw, NULL, 0); + if (ret) { + log_warn("Could not add route to VPN gateway.\n"); + if (tunnel->ipv4.split_routes == 0) + return ret; + /* Split tunnel: continue without gateway route */ + } else { + tunnel->ipv4.route_to_vpn_is_added = 1; + } + } + + if (!tun_luid_valid) { + log_error("TUN adapter LUID not set.\n"); + return -1; + } + + if (tunnel->ipv4.split_routes > 0) { + /* Apply deferred split routes now that the adapter exists */ + for (int i = 0; i < tunnel->ipv4.split_routes; i++) { + struct rtentry *rt = &tunnel->ipv4.split_rt[i]; + + ret = add_route(route_dest(rt), route_mask(rt), + route_gtw(rt), &tun_luid, 0); + if (ret) + log_warn("Failed to add split route %d\n", i); + } + return 0; + } + + /* Set default route through VPN */ + if (tunnel->config->half_internet_routes) { + struct in_addr half1, half2; + + half1.s_addr = 0; + half2.s_addr = htonl(0x80000000); + + /* 0.0.0.0/1 via TUN */ + ret = add_route(half1, half_mask, tunnel->ipv4.ip_addr, + &tun_luid, 5); + if (ret) + return ret; + + /* 128.0.0.0/1 via TUN */ + ret = add_route(half2, half_mask, tunnel->ipv4.ip_addr, + &tun_luid, 5); + if (ret) + return ret; + } else { + struct in_addr zero_mask; + + zero_mask.s_addr = 0; + + /* Replace default route with one through VPN */ + ret = add_route(any, zero_mask, tunnel->ipv4.ip_addr, + &tun_luid, 5); + if (ret) + return ret; + } + + return 0; +} + +int ipv4_restore_routes(struct tunnel *tunnel) +{ + struct in_addr any, full_mask, half_mask; + int ret = 0; + + any.s_addr = 0; + full_mask.s_addr = htonl(0xFFFFFFFF); + half_mask.s_addr = htonl(0x80000000); + + /* Remove VPN default routes */ + if (tun_luid_valid) { + MIB_IPFORWARD_ROW2 row; + + InitializeIpForwardEntry(&row); + row.InterfaceLuid = tun_luid; + + if (tunnel->config->half_internet_routes) { + /* Delete 0.0.0.0/1 route */ + row.DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET; + row.DestinationPrefix.Prefix.Ipv4.sin_addr.s_addr = 0; + row.DestinationPrefix.PrefixLength = 1; + row.NextHop.Ipv4.sin_family = AF_INET; + row.NextHop.Ipv4.sin_addr = tunnel->ipv4.ip_addr; + del_route(&row); + + /* Delete 128.0.0.0/1 route */ + row.DestinationPrefix.Prefix.Ipv4.sin_addr.s_addr = + htonl(0x80000000); + del_route(&row); + } else { + /* Delete default route through VPN */ + row.DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET; + row.DestinationPrefix.Prefix.Ipv4.sin_addr.s_addr = 0; + row.DestinationPrefix.PrefixLength = 0; + row.NextHop.Ipv4.sin_family = AF_INET; + row.NextHop.Ipv4.sin_addr = tunnel->ipv4.ip_addr; + del_route(&row); + } + } + + /* Remove route to VPN gateway */ + if (tunnel->ipv4.route_to_vpn_is_added) { + MIB_IPFORWARD_ROW2 row; + + InitializeIpForwardEntry(&row); + row.DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET; + row.DestinationPrefix.Prefix.Ipv4.sin_addr = + tunnel->config->gateway_ip; + row.DestinationPrefix.PrefixLength = 32; + if (saved_default_route.valid) { + row.InterfaceLuid = + saved_default_route.row.InterfaceLuid; + row.NextHop.Ipv4.sin_family = AF_INET; + row.NextHop.Ipv4.sin_addr = + saved_default_route.row.NextHop.Ipv4.sin_addr; + } + del_route(&row); + tunnel->ipv4.route_to_vpn_is_added = 0; + } + + return ret; +} + +int ipv4_add_nameservers_to_resolv_conf(struct tunnel *tunnel) +{ + char cmd[512]; + char dns1_str[INET_ADDRSTRLEN] = ""; + char dns2_str[INET_ADDRSTRLEN] = ""; + + if (!tun_luid_valid) { + log_error("TUN adapter LUID not set, cannot configure DNS.\n"); + return -1; + } + + if (tunnel->ipv4.ns1_addr.s_addr != 0) { + inet_ntop(AF_INET, &tunnel->ipv4.ns1_addr, + dns1_str, sizeof(dns1_str)); + } + if (tunnel->ipv4.ns2_addr.s_addr != 0) { + inet_ntop(AF_INET, &tunnel->ipv4.ns2_addr, + dns2_str, sizeof(dns2_str)); + } + + /* + * Use netsh to configure DNS on the TUN interface. + * This is the most portable approach across Windows versions. + */ + if (dns1_str[0]) { + snprintf(cmd, sizeof(cmd), + "netsh interface ipv4 set dnsservers name=\"openfortivpn\" static %s primary validate=no", + dns1_str); + log_debug("Running: %s\n", cmd); + system(cmd); + } + + if (dns2_str[0]) { + snprintf(cmd, sizeof(cmd), + "netsh interface ipv4 add dnsservers name=\"openfortivpn\" %s index=2 validate=no", + dns2_str); + log_debug("Running: %s\n", cmd); + system(cmd); + } + + /* Set DNS search suffix if configured */ + if (tunnel->ipv4.dns_suffix) { + snprintf(cmd, sizeof(cmd), + "netsh interface ipv4 set dnsservers name=\"openfortivpn\" register=both suffix=\"%s\" validate=no", + tunnel->ipv4.dns_suffix); + log_debug("Running: %s\n", cmd); + /* DNS suffix is set via registry for reliability */ + } + + /* Lower the interface metric so VPN DNS is preferred */ + snprintf(cmd, sizeof(cmd), + "netsh interface ipv4 set interface \"openfortivpn\" metric=1"); + log_debug("Running: %s\n", cmd); + system(cmd); + + return 0; +} + +int ipv4_del_nameservers_from_resolv_conf(struct tunnel *tunnel) +{ + char cmd[256]; + + (void)tunnel; + + /* + * Reset DNS on the TUN interface. When the adapter is destroyed, + * DNS settings are automatically cleaned up, but we do it explicitly + * for clean shutdown. + */ + snprintf(cmd, sizeof(cmd), + "netsh interface ipv4 set dnsservers name=\"openfortivpn\" dhcp"); + log_debug("Running: %s\n", cmd); + system(cmd); + + return 0; +} + +/* + * Check if the TUN interface has the expected IP address assigned. + */ +int ppp_interface_is_up(struct tunnel *tunnel) +{ + ULONG size = 0; + PIP_ADAPTER_ADDRESSES addrs = NULL, curr; + DWORD ret; + int found = 0; + + if (!tun_luid_valid) + return 0; + + /* Get adapter addresses */ + ret = GetAdaptersAddresses(AF_INET, + GAA_FLAG_SKIP_ANYCAST | + GAA_FLAG_SKIP_MULTICAST | + GAA_FLAG_SKIP_DNS_SERVER, + NULL, NULL, &size); + if (ret != ERROR_BUFFER_OVERFLOW) + return 0; + + addrs = (PIP_ADAPTER_ADDRESSES)malloc(size); + if (!addrs) + return 0; + + ret = GetAdaptersAddresses(AF_INET, + GAA_FLAG_SKIP_ANYCAST | + GAA_FLAG_SKIP_MULTICAST | + GAA_FLAG_SKIP_DNS_SERVER, + NULL, addrs, &size); + if (ret != NO_ERROR) { + free(addrs); + return 0; + } + + for (curr = addrs; curr; curr = curr->Next) { + if (curr->Luid.Value == tun_luid.Value && + curr->OperStatus == IfOperStatusUp) { + /* + * Check if the assigned IP matches what we expect. + * For wintun, the interface is "up" if it exists. + */ + found = 1; + + /* Copy interface name */ + if (curr->AdapterName) { + strncpy(tunnel->ppp_iface, curr->AdapterName, + ROUTE_IFACE_LEN - 1); + tunnel->ppp_iface[ROUTE_IFACE_LEN - 1] = '\0'; + } + break; + } + } + + free(addrs); + return found; +} + +#endif /* _WIN32 */ diff --git a/src/log_win.c b/src/log_win.c new file mode 100644 index 00000000..11cdfe76 --- /dev/null +++ b/src/log_win.c @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + * + * Windows logging implementation. + * Replaces log.c for Windows builds. + */ + +#ifdef _WIN32 + +#include "log.h" +#include "compat_win32.h" + +#include +#include + +#include +#include +#include + +enum log_verbosity loglevel = OFV_LOG_INFO; + +static int do_syslog_flag; +static int use_colors; +static pthread_mutex_t log_mutex; +static HANDLE event_log; + +void init_logging(void) +{ + HANDLE hConsole; + DWORD mode; + + pthread_mutex_init(&log_mutex, NULL); + + /* Enable ANSI escape codes on Windows 10+ */ + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if (hConsole != INVALID_HANDLE_VALUE) { + if (GetConsoleMode(hConsole, &mode)) { + if (SetConsoleMode(hConsole, + mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { + use_colors = isatty(fileno(stdout)); + } + } + } + + if (!use_colors) + use_colors = isatty(fileno(stdout)); +} + +void set_syslog(int do_syslog) +{ + do_syslog_flag = do_syslog; + if (do_syslog && !event_log) + event_log = RegisterEventSourceA(NULL, "openfortivpn"); +} + +void increase_verbosity(void) +{ + if (loglevel < OFV_LOG_DEBUG_ALL) + loglevel++; +} + +void decrease_verbosity(void) +{ + if (loglevel > OFV_LOG_MUTE) + loglevel--; +} + +void do_log(int verbosity, const char *format, ...) +{ + va_list args; + const char *prefix = ""; + const char *color_start = ""; + const char *color_end = ""; + + pthread_mutex_lock(&log_mutex); + + switch (verbosity) { + case OFV_LOG_ERROR: + prefix = "ERROR: "; + if (use_colors) { + color_start = "\033[0;31m"; /* red */ + color_end = "\033[0;0m"; + } + break; + case OFV_LOG_WARN: + prefix = "WARN: "; + if (use_colors) { + color_start = "\033[0;33m"; /* yellow */ + color_end = "\033[0;0m"; + } + break; + case OFV_LOG_INFO: + prefix = "INFO: "; + break; + case OFV_LOG_DEBUG: + case OFV_LOG_DEBUG_DETAILS: + case OFV_LOG_DEBUG_ALL: + prefix = "DEBUG: "; + break; + } + + fprintf(stdout, "%s%s", color_start, prefix); + va_start(args, format); + vfprintf(stdout, format, args); + va_end(args); + fprintf(stdout, "%s", color_end); + fflush(stdout); + + /* Windows Event Log */ + if (do_syslog_flag && event_log) { + char msg[2048]; + WORD event_type; + + va_start(args, format); + vsnprintf(msg, sizeof(msg), format, args); + va_end(args); + + switch (verbosity) { + case OFV_LOG_ERROR: + event_type = EVENTLOG_ERROR_TYPE; + break; + case OFV_LOG_WARN: + event_type = EVENTLOG_WARNING_TYPE; + break; + default: + event_type = EVENTLOG_INFORMATION_TYPE; + break; + } + + const char *strings[1] = { msg }; + + ReportEventA(event_log, event_type, 0, 0, NULL, + 1, 0, strings, NULL); + } + + pthread_mutex_unlock(&log_mutex); +} + +void do_log_packet(const char *prefix, size_t len, const uint8_t *packet) +{ + pthread_mutex_lock(&log_mutex); + + fprintf(stdout, "%s(%lu bytes) ", prefix, (unsigned long)len); + for (size_t i = 0; i < len && i < 64; i++) + fprintf(stdout, "%02x ", packet[i]); + if (len > 64) + fprintf(stdout, "..."); + fprintf(stdout, "\n"); + fflush(stdout); + + pthread_mutex_unlock(&log_mutex); +} + +#endif /* _WIN32 */ diff --git a/src/main.c b/src/main.c index c374c52d..f537c126 100644 --- a/src/main.c +++ b/src/main.c @@ -19,11 +19,15 @@ #include "tunnel.h" #include "userinput.h" #include "log.h" +#include "event.h" +#include "exit_codes.h" #include "http_server.h" #include +#ifndef _WIN32 #include +#endif #include #include @@ -31,6 +35,10 @@ #include #include +#ifdef _WIN32 +#include "compat_win32.h" +#endif + #if HAVE_USR_SBIN_PPPD && HAVE_USR_SBIN_PPP #error "Both HAVE_USR_SBIN_PPPD and HAVE_USR_SBIN_PPP have been defined." #elif HAVE_USR_SBIN_PPPD @@ -62,6 +70,10 @@ #define PPPD_HELP \ " --ppp-system= Connect to the specified system as defined in\n" \ " /etc/ppp/ppp.conf.\n" +#elif defined(_WIN32) +/* On Windows, PPP is handled in-process via wintun */ +#define PPPD_USAGE "" +#define PPPD_HELP "" #else #error "Neither HAVE_USR_SBIN_PPPD nor HAVE_USR_SBIN_PPP have been defined." #endif @@ -245,7 +257,7 @@ int main(int argc, char *argv[]) #if HAVE_RESOLVCONF .use_resolvconf = USE_RESOLVCONF, #endif -#if HAVE_USR_SBIN_PPPD +#if HAVE_USR_SBIN_PPPD || defined(_WIN32) .pppd_use_peerdns = 0, .pppd_log = NULL, .pppd_plugin = NULL, @@ -303,6 +315,7 @@ int main(int argc, char *argv[]) {"set-dns", required_argument, NULL, 0}, {"no-dns", no_argument, &cli_cfg.set_dns, 0}, {"use-syslog", no_argument, &cli_cfg.use_syslog, 1}, + {"json-events", no_argument, NULL, 0}, {"persistent", required_argument, NULL, 0}, {"ca-file", required_argument, NULL, 0}, {"user-cert", required_argument, NULL, 0}, @@ -313,7 +326,7 @@ int main(int argc, char *argv[]) {"cipher-list", required_argument, NULL, 0}, {"min-tls", required_argument, NULL, 0}, {"seclevel-1", no_argument, &cli_cfg.seclevel_1, 1}, -#if HAVE_USR_SBIN_PPPD +#if HAVE_USR_SBIN_PPPD || defined(_WIN32) {"pppd-use-peerdns", required_argument, NULL, 0}, {"pppd-no-peerdns", no_argument, &cli_cfg.pppd_use_peerdns, 0}, {"pppd-log", required_argument, NULL, 0}, @@ -332,6 +345,13 @@ int main(int argc, char *argv[]) {NULL, 0, NULL, 0} }; +#ifdef _WIN32 + if (winsock_init() != 0) { + fprintf(stderr, "Failed to initialize Winsock.\n"); + return EXIT_FAILURE; + } +#endif + init_logging(); while (1) { @@ -358,7 +378,12 @@ int main(int argc, char *argv[]) ret = EXIT_SUCCESS; goto exit; } -#if HAVE_USR_SBIN_PPPD + if (strcmp(long_options[option_index].name, + "json-events") == 0) { + event_init(1); + break; + } +#if HAVE_USR_SBIN_PPPD || defined(_WIN32) if (strcmp(long_options[option_index].name, "pppd-use-peerdns") == 0) { int pppd_use_peerdns = strtob(optarg); @@ -742,11 +767,34 @@ int main(int argc, char *argv[]) if (cfg.otp[0] != '\0') log_debug("One-time password = \"%s\"\n", cfg.otp); +#ifdef _WIN32 + { + /* Check for administrator privileges on Windows */ + BOOL is_admin = FALSE; + SID_IDENTIFIER_AUTHORITY nt_auth = SECURITY_NT_AUTHORITY; + PSID admin_group = NULL; + + if (AllocateAndInitializeSid(&nt_auth, 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + &admin_group)) { + CheckTokenMembership(NULL, admin_group, &is_admin); + FreeSid(admin_group); + } + if (!is_admin) { + log_error("This process requires administrator privileges.\n"); + ret = EXIT_FAILURE; + goto exit; + } + } +#else if (geteuid() != 0) { log_error("This process was not spawned with root privileges, which are required.\n"); ret = EXIT_FAILURE; goto exit; } +#endif if (cfg.saml_port != 0) { // Wait for the SAML token from the HTTP GET request @@ -764,9 +812,8 @@ int main(int argc, char *argv[]) } do { - if (run_tunnel(&cfg) != 0) - ret = EXIT_FAILURE; - else + ret = run_tunnel(&cfg); + if (ret == 0) ret = EXIT_SUCCESS; if ((cfg.persistent > 0) && (get_sig_received() == 0)) sleep(cfg.persistent); @@ -778,5 +825,8 @@ int main(int argc, char *argv[]) fprintf(stderr, "%s", usage); exit: destroy_vpn_config(&cfg); +#ifdef _WIN32 + winsock_cleanup(); +#endif exit(ret); } diff --git a/src/ppp.c b/src/ppp.c new file mode 100644 index 00000000..3184cff8 --- /dev/null +++ b/src/ppp.c @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 "ppp.h" + +#include +#include +#include + +/* + * Build a PPP control packet with protocol, code, identifier, and options. + * Allocates output buffer; caller must free. + */ +static int build_control_packet(uint16_t protocol, uint8_t code, uint8_t id, + const uint8_t *options, size_t opt_len, + uint8_t **out, size_t *out_len) +{ + /* PPP packet: 2 bytes protocol + 1 code + 1 id + 2 length + options */ + size_t pkt_len = 2 + 4 + opt_len; + uint16_t ctrl_len = (uint16_t)(4 + opt_len); + uint8_t *pkt; + + pkt = malloc(pkt_len); + if (!pkt) + return -1; + + pkt[0] = (uint8_t)(protocol >> 8); + pkt[1] = (uint8_t)(protocol & 0xff); + pkt[2] = code; + pkt[3] = id; + pkt[4] = (uint8_t)(ctrl_len >> 8); + pkt[5] = (uint8_t)(ctrl_len & 0xff); + + if (options && opt_len > 0) + memcpy(&pkt[6], options, opt_len); + + *out = pkt; + *out_len = pkt_len; + return 0; +} + +/* + * Build an LCP Echo-Reply from an Echo-Request. + * Echo-Reply has our magic number at bytes 6-9 of the control payload. + */ +static int build_echo_reply(uint8_t id, uint32_t magic, + uint8_t **out, size_t *out_len) +{ + /* protocol(2) + code(1) + id(1) + length(2) + magic(4) = 10 */ + size_t pkt_len = 10; + uint8_t *pkt; + + pkt = malloc(pkt_len); + if (!pkt) + return -1; + + pkt[0] = (uint8_t)(PPP_PROTO_LCP >> 8); + pkt[1] = (uint8_t)(PPP_PROTO_LCP & 0xff); + pkt[2] = PPP_CODE_ECHO_REP; + pkt[3] = id; + pkt[4] = 0; + pkt[5] = 8; /* length = 4 (header) + 4 (magic) */ + pkt[6] = (uint8_t)(magic >> 24); + pkt[7] = (uint8_t)(magic >> 16); + pkt[8] = (uint8_t)(magic >> 8); + pkt[9] = (uint8_t)(magic); + + *out = pkt; + *out_len = pkt_len; + return 0; +} + +void ppp_init(struct ppp_context *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->state = PPP_STATE_INITIAL; + ctx->lcp_identifier = 1; + ctx->ipcp_identifier = 1; + ctx->mru = 1354; /* Matches tunnel.c pppd args */ + ctx->asyncmap = 0; /* default-asyncmap */ + ctx->magic_number = (uint32_t)time(NULL) ^ 0xDEADBEEF; + ctx->negotiation_complete = 0; +} + +int ppp_start_negotiation(struct ppp_context *ctx, + uint8_t **out_data, size_t *out_len) +{ + /* + * LCP Config-Request options: + * - MRU (type 1, len 4): 1354 + * - ACCM (type 2, len 6): 0x00000000 + * - Magic Number (type 5, len 6): random + */ + uint8_t options[16]; + size_t off = 0; + + /* MRU option */ + options[off++] = LCP_OPT_MRU; + options[off++] = 4; + options[off++] = (uint8_t)(ctx->mru >> 8); + options[off++] = (uint8_t)(ctx->mru & 0xff); + + /* ACCM option */ + options[off++] = LCP_OPT_ACCM; + options[off++] = 6; + options[off++] = 0; + options[off++] = 0; + options[off++] = 0; + options[off++] = 0; + + /* Magic Number option */ + options[off++] = LCP_OPT_MAGIC; + options[off++] = 6; + options[off++] = (uint8_t)(ctx->magic_number >> 24); + options[off++] = (uint8_t)(ctx->magic_number >> 16); + options[off++] = (uint8_t)(ctx->magic_number >> 8); + options[off++] = (uint8_t)(ctx->magic_number); + + ctx->state = PPP_STATE_LCP_SENT; + + return build_control_packet(PPP_PROTO_LCP, PPP_CODE_CONF_REQ, + ctx->lcp_identifier++, options, off, + out_data, out_len); +} + +/* + * Handle an incoming LCP packet. + * Returns 0 on success, sets *response if a reply is needed. + */ +static int handle_lcp(struct ppp_context *ctx, + const uint8_t *payload, size_t payload_len, + uint8_t **response, size_t *resp_len) +{ + uint8_t code, id; + uint16_t pkt_length; + + *response = NULL; + *resp_len = 0; + + if (payload_len < 4) + return PPP_RET_ERROR; + + code = payload[0]; + id = payload[1]; + pkt_length = ((uint16_t)payload[2] << 8) | payload[3]; + + if (pkt_length > payload_len) + return PPP_RET_ERROR; + + switch (code) { + case PPP_CODE_CONF_REQ: { + /* + * Peer's Config-Request. We accept most options. + * Reject PFC (7) and ACFC (8) since we don't use compression, + * and reject AUTH (3) since auth is done at HTTP level. + */ + const uint8_t *opts = &payload[4]; + size_t opts_len = pkt_length - 4; + uint8_t *reject_buf = NULL; + size_t reject_len = 0; + size_t pos = 0; + + /* First pass: identify options to reject */ + while (pos < opts_len) { + uint8_t opt_type, opt_len_val; + + if (pos + 2 > opts_len) + break; + opt_type = opts[pos]; + opt_len_val = opts[pos + 1]; + if (opt_len_val < 2 || pos + opt_len_val > opts_len) + break; + + if (opt_type == LCP_OPT_AUTH || + opt_type == LCP_OPT_PFC || + opt_type == LCP_OPT_ACFC) { + /* Add to reject list */ + uint8_t *tmp = realloc(reject_buf, + reject_len + opt_len_val); + if (!tmp) { + free(reject_buf); + return PPP_RET_ERROR; + } + reject_buf = tmp; + memcpy(&reject_buf[reject_len], + &opts[pos], opt_len_val); + reject_len += opt_len_val; + } + pos += opt_len_val; + } + + if (reject_len > 0) { + /* Send Config-Reject with the unwanted options */ + int ret = build_control_packet(PPP_PROTO_LCP, + PPP_CODE_CONF_REJ, id, + reject_buf, reject_len, + response, resp_len); + free(reject_buf); + return ret < 0 ? PPP_RET_ERROR : PPP_RET_OK; + } + free(reject_buf); + + /* Accept all options: send Config-Ack with the same options */ + return build_control_packet(PPP_PROTO_LCP, + PPP_CODE_CONF_ACK, id, + opts, opts_len, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + } + + case PPP_CODE_CONF_ACK: + /* Peer accepted our LCP config */ + if (ctx->state == PPP_STATE_LCP_SENT || + ctx->state == PPP_STATE_INITIAL) { + ctx->state = PPP_STATE_LCP_OPEN; + + /* Now start IPCP negotiation */ + uint8_t ipcp_opts[18]; + size_t off = 0; + + /* IP-Address: request 0.0.0.0 (let peer assign) */ + ipcp_opts[off++] = IPCP_OPT_IP_ADDR; + ipcp_opts[off++] = 6; + ipcp_opts[off++] = (uint8_t)(ctx->local_ip >> 24); + ipcp_opts[off++] = (uint8_t)(ctx->local_ip >> 16); + ipcp_opts[off++] = (uint8_t)(ctx->local_ip >> 8); + ipcp_opts[off++] = (uint8_t)(ctx->local_ip); + + /* Primary DNS */ + ipcp_opts[off++] = IPCP_OPT_DNS_PRI; + ipcp_opts[off++] = 6; + ipcp_opts[off++] = (uint8_t)(ctx->dns_primary >> 24); + ipcp_opts[off++] = (uint8_t)(ctx->dns_primary >> 16); + ipcp_opts[off++] = (uint8_t)(ctx->dns_primary >> 8); + ipcp_opts[off++] = (uint8_t)(ctx->dns_primary); + + /* Secondary DNS */ + ipcp_opts[off++] = IPCP_OPT_DNS_SEC; + ipcp_opts[off++] = 6; + ipcp_opts[off++] = (uint8_t)(ctx->dns_secondary >> 24); + ipcp_opts[off++] = (uint8_t)(ctx->dns_secondary >> 16); + ipcp_opts[off++] = (uint8_t)(ctx->dns_secondary >> 8); + ipcp_opts[off++] = (uint8_t)(ctx->dns_secondary); + + ctx->state = PPP_STATE_IPCP_SENT; + + return build_control_packet(PPP_PROTO_IPCP, + PPP_CODE_CONF_REQ, + ctx->ipcp_identifier++, + ipcp_opts, off, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + } + return PPP_RET_OK; + + case PPP_CODE_CONF_NAK: + /* + * Peer wants us to change some LCP options. + * Re-send Config-Request with the nak'd values. + */ + return build_control_packet(PPP_PROTO_LCP, + PPP_CODE_CONF_REQ, + ctx->lcp_identifier++, + &payload[4], pkt_length - 4, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + + case PPP_CODE_CONF_REJ: { + /* + * Peer rejected some of our options. Re-send without them. + * For simplicity, send a minimal config with just MRU. + */ + uint8_t min_opts[4]; + + min_opts[0] = LCP_OPT_MRU; + min_opts[1] = 4; + min_opts[2] = (uint8_t)(ctx->mru >> 8); + min_opts[3] = (uint8_t)(ctx->mru & 0xff); + + return build_control_packet(PPP_PROTO_LCP, + PPP_CODE_CONF_REQ, + ctx->lcp_identifier++, + min_opts, 4, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + } + + case PPP_CODE_TERM_REQ: + /* Peer wants to terminate. Send Term-Ack. */ + ctx->state = PPP_STATE_TERMINATED; + if (build_control_packet(PPP_PROTO_LCP, PPP_CODE_TERM_ACK, + id, NULL, 0, + response, resp_len) < 0) + return PPP_RET_ERROR; + return PPP_RET_TERMINATED; + + case PPP_CODE_TERM_ACK: + ctx->state = PPP_STATE_TERMINATED; + return PPP_RET_TERMINATED; + + case PPP_CODE_ECHO_REQ: + return build_echo_reply(id, ctx->magic_number, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + + case PPP_CODE_ECHO_REP: + /* Just ignore echo replies */ + return PPP_RET_OK; + + case PPP_CODE_CODE_REJ: + case PPP_CODE_PROTO_REJ: + /* Log and ignore */ + return PPP_RET_OK; + + default: + return PPP_RET_OK; + } +} + +/* + * Handle an incoming IPCP packet. + */ +static int handle_ipcp(struct ppp_context *ctx, + const uint8_t *payload, size_t payload_len, + uint8_t **response, size_t *resp_len) +{ + uint8_t code, id; + uint16_t pkt_length; + + *response = NULL; + *resp_len = 0; + + if (payload_len < 4) + return PPP_RET_ERROR; + + code = payload[0]; + id = payload[1]; + pkt_length = ((uint16_t)payload[2] << 8) | payload[3]; + + if (pkt_length > payload_len) + return PPP_RET_ERROR; + + switch (code) { + case PPP_CODE_CONF_REQ: { + /* + * Peer's IPCP Config-Request. Accept it as-is. + * Extract peer IP if present. + */ + const uint8_t *opts = &payload[4]; + size_t opts_len = pkt_length - 4; + size_t pos = 0; + + while (pos < opts_len) { + uint8_t opt_type, opt_len_val; + + if (pos + 2 > opts_len) + break; + opt_type = opts[pos]; + opt_len_val = opts[pos + 1]; + if (opt_len_val < 2 || pos + opt_len_val > opts_len) + break; + + if (opt_type == IPCP_OPT_IP_ADDR && opt_len_val >= 6) + memcpy(&ctx->peer_ip, &opts[pos + 2], 4); + pos += opt_len_val; + } + + /* Send Config-Ack */ + return build_control_packet(PPP_PROTO_IPCP, + PPP_CODE_CONF_ACK, id, + opts, opts_len, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + } + + case PPP_CODE_CONF_ACK: + /* Peer accepted our IPCP config - negotiation complete */ + if (ctx->state == PPP_STATE_IPCP_SENT) { + ctx->state = PPP_STATE_IPCP_OPEN; + ctx->negotiation_complete = 1; + return PPP_RET_NEGOTIATE; + } + return PPP_RET_OK; + + case PPP_CODE_CONF_NAK: { + /* + * Peer is suggesting different values for our IPCP options. + * Parse the nak'd options to get the assigned IP/DNS. + */ + const uint8_t *opts = &payload[4]; + size_t opts_len = pkt_length - 4; + size_t pos = 0; + + while (pos < opts_len) { + uint8_t opt_type, opt_len_val; + + if (pos + 2 > opts_len) + break; + opt_type = opts[pos]; + opt_len_val = opts[pos + 1]; + if (opt_len_val < 2 || pos + opt_len_val > opts_len) + break; + + if (opt_type == IPCP_OPT_IP_ADDR && opt_len_val >= 6) + memcpy(&ctx->local_ip, &opts[pos + 2], 4); + else if (opt_type == IPCP_OPT_DNS_PRI && opt_len_val >= 6) + memcpy(&ctx->dns_primary, &opts[pos + 2], 4); + else if (opt_type == IPCP_OPT_DNS_SEC && opt_len_val >= 6) + memcpy(&ctx->dns_secondary, &opts[pos + 2], 4); + + pos += opt_len_val; + } + + /* Re-send IPCP Config-Request with the suggested values */ + uint8_t ipcp_opts[18]; + size_t off = 0; + + ipcp_opts[off++] = IPCP_OPT_IP_ADDR; + ipcp_opts[off++] = 6; + memcpy(&ipcp_opts[off], &ctx->local_ip, 4); + off += 4; + + ipcp_opts[off++] = IPCP_OPT_DNS_PRI; + ipcp_opts[off++] = 6; + memcpy(&ipcp_opts[off], &ctx->dns_primary, 4); + off += 4; + + ipcp_opts[off++] = IPCP_OPT_DNS_SEC; + ipcp_opts[off++] = 6; + memcpy(&ipcp_opts[off], &ctx->dns_secondary, 4); + off += 4; + + return build_control_packet(PPP_PROTO_IPCP, + PPP_CODE_CONF_REQ, + ctx->ipcp_identifier++, + ipcp_opts, off, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + } + + case PPP_CODE_CONF_REJ: { + /* + * Peer rejected some IPCP options. Re-send without them. + * Typically DNS options may be rejected. + */ + const uint8_t *rej_opts = &payload[4]; + size_t rej_len = pkt_length - 4; + uint8_t ipcp_opts[18]; + size_t off = 0; + int ip_rejected = 0; + int dns_pri_rejected = 0; + int dns_sec_rejected = 0; + size_t pos = 0; + + /* Determine which options were rejected */ + while (pos < rej_len) { + uint8_t opt_type, opt_len_val; + + if (pos + 2 > rej_len) + break; + opt_type = rej_opts[pos]; + opt_len_val = rej_opts[pos + 1]; + if (opt_len_val < 2 || pos + opt_len_val > rej_len) + break; + + if (opt_type == IPCP_OPT_IP_ADDR) + ip_rejected = 1; + else if (opt_type == IPCP_OPT_DNS_PRI) + dns_pri_rejected = 1; + else if (opt_type == IPCP_OPT_DNS_SEC) + dns_sec_rejected = 1; + + pos += opt_len_val; + } + + /* Rebuild request without rejected options */ + if (!ip_rejected) { + ipcp_opts[off++] = IPCP_OPT_IP_ADDR; + ipcp_opts[off++] = 6; + memcpy(&ipcp_opts[off], &ctx->local_ip, 4); + off += 4; + } + if (!dns_pri_rejected) { + ipcp_opts[off++] = IPCP_OPT_DNS_PRI; + ipcp_opts[off++] = 6; + memcpy(&ipcp_opts[off], &ctx->dns_primary, 4); + off += 4; + } + if (!dns_sec_rejected) { + ipcp_opts[off++] = IPCP_OPT_DNS_SEC; + ipcp_opts[off++] = 6; + memcpy(&ipcp_opts[off], &ctx->dns_secondary, 4); + off += 4; + } + + return build_control_packet(PPP_PROTO_IPCP, + PPP_CODE_CONF_REQ, + ctx->ipcp_identifier++, + ipcp_opts, off, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + } + + case PPP_CODE_TERM_REQ: + ctx->state = PPP_STATE_TERMINATED; + if (build_control_packet(PPP_PROTO_IPCP, PPP_CODE_TERM_ACK, + id, NULL, 0, + response, resp_len) < 0) + return PPP_RET_ERROR; + return PPP_RET_TERMINATED; + + case PPP_CODE_TERM_ACK: + ctx->state = PPP_STATE_TERMINATED; + return PPP_RET_TERMINATED; + + default: + return PPP_RET_OK; + } +} + +int ppp_process_incoming(struct ppp_context *ctx, + const uint8_t *ppp_data, size_t ppp_len, + uint8_t **response, size_t *resp_len, + uint8_t **ip_payload, size_t *ip_len) +{ + uint16_t protocol; + + *response = NULL; + *resp_len = 0; + *ip_payload = NULL; + *ip_len = 0; + + if (ppp_len < 2) + return PPP_RET_ERROR; + + protocol = ((uint16_t)ppp_data[0] << 8) | ppp_data[1]; + + switch (protocol) { + case PPP_PROTO_LCP: + return handle_lcp(ctx, &ppp_data[2], ppp_len - 2, + response, resp_len); + + case PPP_PROTO_IPCP: + return handle_ipcp(ctx, &ppp_data[2], ppp_len - 2, + response, resp_len); + + case PPP_PROTO_IP: { + /* Raw IP data packet - extract the IP payload */ + size_t data_len = ppp_len - 2; + + if (data_len == 0) + return PPP_RET_OK; + + *ip_payload = malloc(data_len); + if (!*ip_payload) + return PPP_RET_ERROR; + memcpy(*ip_payload, &ppp_data[2], data_len); + *ip_len = data_len; + return PPP_RET_IP_PACKET; + } + + default: { + size_t rej_data_len = ppp_len; + /* + * Unknown protocol. Send Protocol-Reject (LCP Code 8) + * containing the rejected protocol and packet data. + * Limit reject data per RFC 1661. + */ + if (rej_data_len > 1500) + rej_data_len = 1500; + return build_control_packet(PPP_PROTO_LCP, + PPP_CODE_PROTO_REJ, + ctx->lcp_identifier++, + ppp_data, rej_data_len, + response, resp_len) < 0 + ? PPP_RET_ERROR : PPP_RET_OK; + } + } +} + +int ppp_encapsulate_ip(const uint8_t *ip_data, size_t ip_len, + uint8_t **ppp_data, size_t *ppp_len) +{ + size_t total_len = 2 + ip_len; + uint8_t *pkt; + + pkt = malloc(total_len); + if (!pkt) + return -1; + + /* PPP protocol field for IPv4 */ + pkt[0] = (uint8_t)(PPP_PROTO_IP >> 8); + pkt[1] = (uint8_t)(PPP_PROTO_IP & 0xff); + + memcpy(&pkt[2], ip_data, ip_len); + + *ppp_data = pkt; + *ppp_len = total_len; + return 0; +} diff --git a/src/ppp.h b/src/ppp.h new file mode 100644 index 00000000..1b724765 --- /dev/null +++ b/src/ppp.h @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#ifndef OPENFORTIVPN_PPP_H +#define OPENFORTIVPN_PPP_H + +#include +#include + +/* PPP Protocol IDs */ +#define PPP_PROTO_LCP 0xc021 +#define PPP_PROTO_IPCP 0x8021 +#define PPP_PROTO_IP 0x0021 + +/* PPP Control Codes (RFC 1661) */ +#define PPP_CODE_CONF_REQ 1 +#define PPP_CODE_CONF_ACK 2 +#define PPP_CODE_CONF_NAK 3 +#define PPP_CODE_CONF_REJ 4 +#define PPP_CODE_TERM_REQ 5 +#define PPP_CODE_TERM_ACK 6 +#define PPP_CODE_CODE_REJ 7 +#define PPP_CODE_PROTO_REJ 8 +#define PPP_CODE_ECHO_REQ 9 +#define PPP_CODE_ECHO_REP 10 + +/* LCP Option Types (RFC 1661) */ +#define LCP_OPT_MRU 1 +#define LCP_OPT_ACCM 2 +#define LCP_OPT_AUTH 3 +#define LCP_OPT_MAGIC 5 +#define LCP_OPT_PFC 7 /* Protocol-Field-Compression */ +#define LCP_OPT_ACFC 8 /* Address-and-Control-Field-Compression */ + +/* IPCP Option Types (RFC 1332) */ +#define IPCP_OPT_IP_ADDR 3 +#define IPCP_OPT_DNS_PRI 129 /* 0x81 - Primary DNS */ +#define IPCP_OPT_DNS_SEC 131 /* 0x83 - Secondary DNS */ + +/* PPP State Machine States */ +enum ppp_state { + PPP_STATE_INITIAL, + PPP_STATE_LCP_SENT, + PPP_STATE_LCP_OPEN, + PPP_STATE_IPCP_SENT, + PPP_STATE_IPCP_OPEN, + PPP_STATE_TERMINATED +}; + +/* Return codes for ppp_process_incoming */ +#define PPP_RET_OK 0 /* Processed, may have response */ +#define PPP_RET_IP_PACKET 1 /* Extracted an IP data packet */ +#define PPP_RET_NEGOTIATE 2 /* Negotiation complete (IPCP open) */ +#define PPP_RET_TERMINATED -1 /* Peer requested termination */ +#define PPP_RET_ERROR -2 /* Protocol error */ + +struct ppp_context { + enum ppp_state state; + uint8_t lcp_identifier; /* Sequence counter for LCP */ + uint8_t ipcp_identifier; /* Sequence counter for IPCP */ + uint16_t mru; /* Maximum Receive Unit */ + uint32_t asyncmap; /* Async-Control-Character-Map */ + uint32_t magic_number; /* Magic number for loop detection */ + uint32_t local_ip; /* Assigned local IP (network order) */ + uint32_t peer_ip; /* Peer IP (network order) */ + uint32_t dns_primary; /* Primary DNS (network order) */ + uint32_t dns_secondary; /* Secondary DNS (network order) */ + int negotiation_complete; /* Non-zero when IPCP is open */ +}; + +/* + * Initialize a PPP context with default parameters matching + * the existing pppd configuration in tunnel.c: + * MRU=1354, ACCM=0, no auth, no compression. + */ +void ppp_init(struct ppp_context *ctx); + +/* + * Generate the initial LCP Config-Request to begin negotiation. + * + * @param ctx PPP context + * @param out_data Output buffer (caller must free) + * @param out_len Length of output data + * @return 0 on success, -1 on error + * + * The output is a raw PPP packet (2-byte protocol + payload), + * suitable for wrapping in the Fortinet 6-byte TLS header. + */ +int ppp_start_negotiation(struct ppp_context *ctx, + uint8_t **out_data, size_t *out_len); + +/* + * Process an incoming PPP packet from the gateway. + * + * @param ctx PPP context + * @param ppp_data Incoming PPP packet data (protocol field + payload) + * @param ppp_len Length of incoming data + * @param response Output: response packet to send back (caller must free, may be NULL) + * @param resp_len Output: length of response + * @param ip_payload Output: extracted IP payload for IP data packets (caller must free) + * @param ip_len Output: length of IP payload + * @return PPP_RET_* code + * + * During negotiation (LCP/IPCP), response packets are generated. + * After negotiation completes, IP data packets (protocol 0x0021) are + * extracted and returned via ip_payload. + */ +int ppp_process_incoming(struct ppp_context *ctx, + const uint8_t *ppp_data, size_t ppp_len, + uint8_t **response, size_t *resp_len, + uint8_t **ip_payload, size_t *ip_len); + +/* + * Wrap an outgoing IP packet in PPP framing (protocol 0x0021). + * + * @param ip_data Raw IP packet data + * @param ip_len Length of IP packet + * @param ppp_data Output: PPP-encapsulated packet (caller must free) + * @param ppp_len Output: length of PPP packet + * @return 0 on success, -1 on error + */ +int ppp_encapsulate_ip(const uint8_t *ip_data, size_t ip_len, + uint8_t **ppp_data, size_t *ppp_len); + +#endif /* OPENFORTIVPN_PPP_H */ diff --git a/src/ssl.h b/src/ssl.h index 4932bb92..32180fae 100644 --- a/src/ssl.h +++ b/src/ssl.h @@ -48,12 +48,17 @@ #ifndef ERESTART /* * ERESTART is one of the recoverable errors which might be returned. - * However, in Mac OS X and BSD this constant is not defined in errno.h - * so we define a dummy value here. + * However, in Mac OS X, BSD, and Windows this constant is not defined + * in errno.h so we define a dummy value here. */ #define ERESTART -1 #endif +#ifndef EPIPE +/* EPIPE is not defined on Windows */ +#define EPIPE 32 +#endif + #define ERR_SSL_AGAIN 0 // deprecated #define ERR_TLS_AGAIN 0 #define ERR_SSL_CLOSED -1 // deprecated diff --git a/src/tunnel.h b/src/tunnel.h index e64af816..d4332ccc 100644 --- a/src/tunnel.h +++ b/src/tunnel.h @@ -36,7 +36,9 @@ #include #include +#ifndef _WIN32 #include +#endif #ifdef __clang__ /* @@ -63,8 +65,14 @@ struct tunnel { struct ppp_packet_pool ssl_to_pty_pool; struct ppp_packet_pool pty_to_ssl_pool; +#ifdef _WIN32 + void *tun_adapter; /* WINTUN_ADAPTER_HANDLE */ + void *tun_session; /* WINTUN_SESSION_HANDLE */ + void *ppp_ctx; /* struct ppp_context * */ +#else pid_t pppd_pid; pid_t pppd_pty; +#endif char ppp_iface[ROUTE_IFACE_LEN]; int ssl_socket; diff --git a/src/tunnel_win.c b/src/tunnel_win.c new file mode 100644 index 00000000..79db1533 --- /dev/null +++ b/src/tunnel_win.c @@ -0,0 +1,630 @@ +/* + * Copyright (c) 2015 Adrien Vergé + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + * + * Windows tunnel lifecycle implementation. + * Replaces tunnel.c for Windows builds, using wintun instead of pppd. + */ + +#ifdef _WIN32 + +#include "tunnel.h" +#include "http.h" +#include "ppp.h" +#include "wintun.h" +#include "log.h" +#include "userinput.h" +#include "ssl.h" +#include "event.h" +#include "exit_codes.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +/* Wintun session ring buffer size: 4 MB */ +#define WINTUN_RING_CAPACITY 0x400000 + +/* Adapter name visible in Windows Network Connections */ +#define ADAPTER_NAME L"openfortivpn" +#define TUNNEL_TYPE L"openfortivpn" + + +static struct wintun_api wt_api; +static int wt_api_loaded; + +/* + * Load wintun.dll and create a TUN adapter. + */ +static int wintun_create(struct tunnel *tunnel) +{ + WINTUN_ADAPTER_HANDLE adapter; + WINTUN_SESSION_HANDLE session; + NET_LUID luid; + + if (!wt_api_loaded) { + if (wintun_load(&wt_api) != 0) { + log_error("Failed to load wintun.dll. Please ensure wintun.dll is in the application directory or system PATH.\n"); + return 1; + } + wt_api_loaded = 1; + + DWORD ver = wt_api.GetRunningDriverVersion(); + + log_info("Loaded wintun.dll (driver version %u.%u)\n", + (ver >> 16) & 0xffff, ver & 0xffff); + } + + /* Create the adapter */ + adapter = wt_api.CreateAdapter(ADAPTER_NAME, TUNNEL_TYPE, NULL); + if (!adapter) { + log_error("Failed to create wintun adapter (error %lu).\nEnsure you are running as Administrator.\n", + GetLastError()); + return 1; + } + + /* Get the adapter LUID for routing */ + wt_api.GetAdapterLUID(adapter, &luid); + ipv4_win_set_tun_luid(&luid); + + /* Start a session */ + session = wt_api.StartSession(adapter, WINTUN_RING_CAPACITY); + if (!session) { + log_error("Failed to start wintun session (error %lu).\n", + GetLastError()); + wt_api.CloseAdapter(adapter); + return 1; + } + + /* + * Store handles in tunnel struct. + * tun_adapter stores a pointer to the static wt_api struct + * (which contains the function pointers AND the adapter handle), + * tun_session stores the session handle. + */ + tunnel->tun_adapter = (void *)&wt_api; + tunnel->tun_session = (void *)session; + + log_info("Wintun adapter created.\n"); + return 0; +} + +/* + * Configure IP address on the wintun adapter. + */ +static int wintun_configure_ip(struct tunnel *tunnel) +{ + MIB_UNICASTIPADDRESS_ROW addr_row; + NET_LUID luid; + DWORD ret; + char ip_str[INET_ADDRSTRLEN]; + + /* Use netsh to set the IP address (most reliable approach) */ + inet_ntop(AF_INET, &tunnel->ipv4.ip_addr, ip_str, sizeof(ip_str)); + + char cmd[256]; + + snprintf(cmd, sizeof(cmd), + "netsh interface ipv4 set address name=\"openfortivpn\" static %s 255.255.255.255", + ip_str); + log_debug("Running: %s\n", cmd); + ret = system(cmd); + if (ret != 0) + log_warn("Failed to set IP address on adapter.\n"); + + /* Set MTU to match PPP MRU */ + snprintf(cmd, sizeof(cmd), + "netsh interface ipv4 set subinterface \"openfortivpn\" mtu=1354 store=active"); + log_debug("Running: %s\n", cmd); + system(cmd); + + return 0; +} + +static void wintun_destroy(struct tunnel *tunnel) +{ + if (tunnel->tun_session) { + wt_api.EndSession((WINTUN_SESSION_HANDLE)tunnel->tun_session); + tunnel->tun_session = NULL; + } + + /* Close adapter - remove it from Windows */ + /* Note: adapter handle is managed via wt_api, not stored separately */ + + log_info("Wintun adapter destroyed.\n"); +} + +/* + * TCP connection to the VPN gateway. + */ +static int tcp_connect(struct tunnel *tunnel) +{ + SOCKET sock; + struct addrinfo hints, *result = NULL; + char port_str[6]; + int ret; + char ip_str[INET_ADDRSTRLEN]; + + inet_ntop(AF_INET, &tunnel->config->gateway_ip, + ip_str, sizeof(ip_str)); + snprintf(port_str, sizeof(port_str), "%u", + tunnel->config->gateway_port); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + ret = getaddrinfo(ip_str, port_str, &hints, &result); + if (ret != 0) { + log_error("getaddrinfo: %s\n", gai_strerror(ret)); + return -1; + } + + sock = socket(result->ai_family, result->ai_socktype, + result->ai_protocol); + if (sock == INVALID_SOCKET) { + log_error("socket: %d\n", WSAGetLastError()); + freeaddrinfo(result); + return -1; + } + + ret = connect(sock, result->ai_addr, (int)result->ai_addrlen); + freeaddrinfo(result); + if (ret == SOCKET_ERROR) { + log_error("connect: %d\n", WSAGetLastError()); + closesocket(sock); + return -1; + } + + tunnel->ssl_socket = (int)sock; + return 0; +} + +/* + * SSL/TLS connection to the gateway. + */ +int ssl_connect(struct tunnel *tunnel) +{ + int ret; + + /* Disconnect any existing SSL connection */ + if (tunnel->ssl_handle) { + SSL_shutdown(tunnel->ssl_handle); + SSL_free(tunnel->ssl_handle); + tunnel->ssl_handle = NULL; + } + if (tunnel->ssl_context) { + SSL_CTX_free(tunnel->ssl_context); + tunnel->ssl_context = NULL; + } + if (tunnel->ssl_socket >= 0) { + closesocket(tunnel->ssl_socket); + tunnel->ssl_socket = -1; + } + + /* TCP connect */ + ret = tcp_connect(tunnel); + if (ret) + return ret; + + /* Set up SSL context */ + tunnel->ssl_context = SSL_CTX_new(SSLv23_client_method()); + if (!tunnel->ssl_context) { + log_error("SSL_CTX_new failed.\n"); + return 1; + } + + /* Set minimum TLS version if configured */ + if (tunnel->config->min_tls > 0) + SSL_CTX_set_min_proto_version(tunnel->ssl_context, + tunnel->config->min_tls); + + /* Disable insecure protocols unless explicitly requested */ + if (!tunnel->config->insecure_ssl) { + SSL_CTX_set_options(tunnel->ssl_context, + SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + } + + /* Load CA certificate if specified */ + if (tunnel->config->ca_file) { + if (!SSL_CTX_load_verify_locations(tunnel->ssl_context, + tunnel->config->ca_file, + NULL)) { + log_error("Could not load CA certificate: %s\n", + tunnel->config->ca_file); + } + } else { + SSL_CTX_set_default_verify_paths(tunnel->ssl_context); + } + + /* Load client certificate if specified */ + if (tunnel->config->user_cert) { + if (SSL_CTX_use_certificate_file(tunnel->ssl_context, + tunnel->config->user_cert, + SSL_FILETYPE_PEM) != 1) { + log_error("Could not load user certificate.\n"); + } + } + + if (tunnel->config->user_key) { + if (SSL_CTX_use_PrivateKey_file(tunnel->ssl_context, + tunnel->config->user_key, + SSL_FILETYPE_PEM) != 1) { + log_error("Could not load user private key.\n"); + } + } + + /* Set cipher list if specified */ + if (tunnel->config->cipher_list) { + SSL_CTX_set_cipher_list(tunnel->ssl_context, + tunnel->config->cipher_list); + } + + /* Create SSL connection */ + tunnel->ssl_handle = SSL_new(tunnel->ssl_context); + if (!tunnel->ssl_handle) { + log_error("SSL_new failed.\n"); + return 1; + } + + SSL_set_fd(tunnel->ssl_handle, tunnel->ssl_socket); + + /* Set SNI */ + { + const char *sni = tunnel->config->sni[0] ? + tunnel->config->sni : + tunnel->config->gateway_host; + SSL_set_tlsext_host_name(tunnel->ssl_handle, sni); + } + + ret = SSL_connect(tunnel->ssl_handle); + if (ret != 1) { + log_error("SSL_connect failed: %s\n", + ERR_error_string(ERR_get_error(), NULL)); + return 1; + } + + /* Verify server certificate */ + if (!tunnel->config->insecure_ssl) { + long verify_result = SSL_get_verify_result(tunnel->ssl_handle); + + if (verify_result != X509_V_OK) { + X509 *cert = SSL_get_peer_certificate(tunnel->ssl_handle); + + if (cert) { + /* Check against trusted cert whitelist */ + unsigned char digest[32]; + unsigned int digest_len = sizeof(digest); + char digest_str[65]; + struct x509_digest *trusted; + int found = 0; + + X509_digest(cert, EVP_sha256(), digest, + &digest_len); + for (unsigned int i = 0; i < digest_len; i++) + sprintf(&digest_str[i * 2], "%02x", + digest[i]); + digest_str[64] = '\0'; + + for (trusted = tunnel->config->cert_whitelist; + trusted; trusted = trusted->next) { + if (strcasecmp(trusted->data, + digest_str) == 0) { + found = 1; + break; + } + } + + X509_free(cert); + + if (!found) { + log_error("Server certificate verification failed.\n"); + log_error("Certificate digest: %s\n", + digest_str); + event_emit_cert_error(digest_str, + "verification_failed"); + return OFV_EXIT_CERT_FAILED; + } + log_debug("Trusted certificate matched.\n"); + } else { + log_error("No server certificate received.\n"); + return 1; + } + } + } + + return 0; +} + +static void ssl_disconnect(struct tunnel *tunnel) +{ + if (tunnel->ssl_handle) { + SSL_shutdown(tunnel->ssl_handle); + SSL_free(tunnel->ssl_handle); + tunnel->ssl_handle = NULL; + } + if (tunnel->ssl_context) { + SSL_CTX_free(tunnel->ssl_context); + tunnel->ssl_context = NULL; + } + if (tunnel->ssl_socket >= 0) { + closesocket(tunnel->ssl_socket); + tunnel->ssl_socket = -1; + } +} + +/* + * Resolve gateway hostname to IP address. + */ +static int get_gateway_host_ip(struct tunnel *tunnel) +{ + struct addrinfo hints, *result = NULL; + int ret; + + if (tunnel->config->gateway_ip.s_addr != 0) + return 0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + + ret = getaddrinfo(tunnel->config->gateway_host, NULL, &hints, &result); + if (ret != 0 || !result) { + log_error("Could not resolve host: %s\n", + tunnel->config->gateway_host); + event_emit_error(OFV_EXIT_DNS_FAILED, "dns", + "Could not resolve host"); + return 1; + } + + tunnel->config->gateway_ip = + ((struct sockaddr_in *)result->ai_addr)->sin_addr; + freeaddrinfo(result); + + { + char ip_str[INET_ADDRSTRLEN]; + + inet_ntop(AF_INET, &tunnel->config->gateway_ip, + ip_str, sizeof(ip_str)); + log_info("Gateway IP: %s\n", ip_str); + event_emit("gateway_resolved", "\"ip\":\"%s\"", ip_str); + } + + return 0; +} + +/* + * Callback when PPP interface comes up. + */ +static int on_ppp_if_up(struct tunnel *tunnel) +{ + struct ppp_context *ctx = (struct ppp_context *)tunnel->ppp_ctx; + + log_info("Tunnel interface is UP.\n"); + + /* Copy negotiated IPs from PPP context to tunnel ipv4 config */ + memcpy(&tunnel->ipv4.ip_addr.s_addr, &ctx->local_ip, 4); + memcpy(&tunnel->ipv4.ns1_addr.s_addr, &ctx->dns_primary, 4); + memcpy(&tunnel->ipv4.ns2_addr.s_addr, &ctx->dns_secondary, 4); + + { + char ip_str[INET_ADDRSTRLEN]; + + inet_ntop(AF_INET, &tunnel->ipv4.ip_addr, + ip_str, sizeof(ip_str)); + log_info("Assigned IP: %s\n", ip_str); + { + char dns1_str[INET_ADDRSTRLEN] = ""; + char dns2_str[INET_ADDRSTRLEN] = ""; + + inet_ntop(AF_INET, &tunnel->ipv4.ns1_addr, + dns1_str, sizeof(dns1_str)); + inet_ntop(AF_INET, &tunnel->ipv4.ns2_addr, + dns2_str, sizeof(dns2_str)); + event_emit_tunnel_up(ip_str, dns1_str, + dns2_str); + } + } + + /* Configure IP on the wintun adapter */ + wintun_configure_ip(tunnel); + + /* Set up routes */ + if (tunnel->config->set_routes) { + int ret = ipv4_set_tunnel_routes(tunnel); + + if (ret) + log_warn("Could not set VPN routes.\n"); + } + + /* Set up DNS */ + if (tunnel->config->set_dns) { + int ret = ipv4_add_nameservers_to_resolv_conf(tunnel); + + if (ret) + log_warn("Could not configure DNS.\n"); + } + + return 0; +} + +/* + * Callback when PPP interface goes down. + */ +static int on_ppp_if_down(struct tunnel *tunnel) +{ + log_info("Tunnel interface is DOWN.\n"); + event_emit("tunnel_down", "\"reason\":\"interface_down\""); + + if (tunnel->config->set_dns) + ipv4_del_nameservers_from_resolv_conf(tunnel); + + if (tunnel->config->set_routes) + ipv4_restore_routes(tunnel); + + return 0; +} + +int run_tunnel(struct vpn_config *config) +{ + int ret; + struct ppp_context ppp_ctx; + struct tunnel tunnel = { + .config = config, + .state = STATE_DOWN, + .ssl_socket = -1, + .ssl_context = NULL, + .ssl_handle = NULL, + .tun_adapter = NULL, + .tun_session = NULL, + .ppp_ctx = NULL, + .ipv4.ns1_addr.s_addr = 0, + .ipv4.ns2_addr.s_addr = 0, + .ipv4.dns_suffix = NULL, + .on_ppp_if_up = on_ppp_if_up, + .on_ppp_if_down = on_ppp_if_down + }; + + /* Initialize PPP state machine */ + ppp_init(&ppp_ctx); + tunnel.ppp_ctx = (void *)&ppp_ctx; + + /* Step 0: resolve gateway */ + event_emit("state_change", "\"state\":\"resolving\""); + log_debug("Resolving gateway host ip\n"); + ret = get_gateway_host_ip(&tunnel); + if (ret) { + ret = OFV_EXIT_DNS_FAILED; + goto err_tunnel; + } + + /* Step 1: TLS connection */ + event_emit("state_change", "\"state\":\"connecting_tls\""); + log_debug("Establishing TLS connection\n"); + ret = ssl_connect(&tunnel); + if (ret) { + if (ret != OFV_EXIT_CERT_FAILED) + ret = OFV_EXIT_TLS_FAILED; + goto err_tunnel; + } + log_info("Connected to gateway.\n"); + + /* Step 2: authenticate */ + event_emit("state_change", "\"state\":\"authenticating\""); + if (config->cookie) + ret = auth_set_cookie(&tunnel, config->cookie); + else + ret = auth_log_in(&tunnel); + if (ret != 1) { + log_error("Could not authenticate to gateway.\n"); + ret = OFV_EXIT_AUTH_FAILED; + goto err_tunnel; + } + log_info("Authenticated.\n"); + event_emit("state_change", "\"state\":\"allocating\""); + + ret = auth_request_vpn_allocation(&tunnel); + if (ret != 1) { + log_error("VPN allocation request failed.\n"); + ret = OFV_EXIT_ALLOC_DENIED; + goto err_tunnel; + } + log_info("Remote gateway has allocated a VPN.\n"); + event_emit("state_change", "\"state\":\"configuring\""); + + ret = ssl_connect(&tunnel); + if (ret) { + if (ret != OFV_EXIT_CERT_FAILED) + ret = OFV_EXIT_TLS_FAILED; + goto err_tunnel; + } + + /* Step 3: get VPN configuration */ + log_debug("Retrieving configuration\n"); + ret = auth_get_config(&tunnel); + if (ret != 1) { + log_error("Could not get VPN configuration.\n"); + ret = OFV_EXIT_CONFIG_FAILED; + goto err_tunnel; + } + + /* Step 4: create wintun adapter (replaces pppd_run) */ + event_emit("state_change", "\"state\":\"creating_adapter\""); + log_debug("Creating wintun adapter\n"); + ret = wintun_create(&tunnel); + if (ret) { + ret = OFV_EXIT_ADAPTER_FAILED; + goto err_tunnel; + } + + /* Step 5: switch to tunnel mode */ + event_emit("state_change", "\"state\":\"tunneling\""); + log_debug("Switch to tunneling mode\n"); + ret = http_send(&tunnel, + "GET /remote/sslvpn-tunnel HTTP/1.1\r\nHost: sslvpn\r\nCookie: %s\r\n\r\n", + tunnel.cookie); + if (ret != 1) { + log_error("Could not start tunnel.\n"); + ret = OFV_EXIT_TUNNEL_FAILED; + goto err_start_tunnel; + } + + tunnel.state = STATE_CONNECTING; + + /* Step 6: I/O loop (PPP negotiation + packet forwarding) */ + event_emit("state_change", "\"state\":\"connected\""); + log_debug("Starting IO through the tunnel\n"); + io_loop(&tunnel); + + event_emit("state_change", "\"state\":\"disconnecting\""); + log_debug("Disconnecting\n"); + if (tunnel.state == STATE_UP) + if (tunnel.on_ppp_if_down != NULL) + tunnel.on_ppp_if_down(&tunnel); + + tunnel.state = STATE_DISCONNECTING; + +err_start_tunnel: + wintun_destroy(&tunnel); + log_info("Destroyed wintun adapter.\n"); + +err_tunnel: + log_info("Closed connection to gateway.\n"); + tunnel.state = STATE_DOWN; + + if (ssl_connect(&tunnel) == 0) { + auth_log_out(&tunnel); + log_info("Logged out.\n"); + ssl_disconnect(&tunnel); + } else { + log_info("Could not log out.\n"); + } + + if (tunnel.ipv4.split_rt != NULL) { + free(tunnel.ipv4.split_rt); + tunnel.ipv4.split_rt = NULL; + } + return ret; +} + +#endif /* _WIN32 */ diff --git a/src/userinput_win.c b/src/userinput_win.c new file mode 100644 index 00000000..b7a9e4d3 --- /dev/null +++ b/src/userinput_win.c @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2015 Davíð Steinn Geirsson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + * + * Windows implementation of user input handling. + * Replaces userinput.c for Windows builds. + */ + +#ifdef _WIN32 + +#include "userinput.h" +#include "log.h" + +#include +#include +#include +#include + +/* + * Read a password from the console with echo disabled. + * On Windows, uses SetConsoleMode to disable ENABLE_ECHO_INPUT. + */ +static void console_read_password(const char *prompt, char *pass, size_t len) +{ + HANDLE hStdin; + DWORD mode, chars_read; + size_t pos = 0; + int is_console; + + fputs(prompt, stderr); + fflush(stderr); + + hStdin = GetStdHandle(STD_INPUT_HANDLE); + if (hStdin == INVALID_HANDLE_VALUE) { + log_error("Cannot get stdin handle.\n"); + pass[0] = '\0'; + return; + } + + /* + * Detect whether stdin is a console or a pipe. + * When launched by the GUI with redirected stdin, + * GetConsoleMode fails and we must use ReadFile. + */ + is_console = GetConsoleMode(hStdin, &mode); + + if (is_console) + SetConsoleMode(hStdin, mode & ~(ENABLE_ECHO_INPUT)); + + while (pos < len - 1) { + char c; + BOOL ok; + + if (is_console) + ok = ReadConsoleA(hStdin, &c, 1, &chars_read, NULL); + else + ok = ReadFile(hStdin, &c, 1, &chars_read, NULL); + + if (!ok || chars_read == 0) + break; + if (c == '\r' || c == '\n') + break; + pass[pos++] = c; + } + pass[pos] = '\0'; + + if (is_console) { + SetConsoleMode(hStdin, mode); + fputs("\n", stderr); + } +} + +/* + * Read a password using pinentry (external program) or console. + * On Windows, pinentry is launched via CreateProcess + pipes. + */ +static int pinentry_read_password(const char *pinentry, const char *hint, + const char *prompt, char *pass, size_t len) +{ + HANDLE child_stdin_rd = NULL, child_stdin_wr = NULL; + HANDLE child_stdout_rd = NULL, child_stdout_wr = NULL; + SECURITY_ATTRIBUTES sa; + PROCESS_INFORMATION pi; + STARTUPINFOA si; + char cmd_buf[1024]; + char read_buf[4096]; + DWORD bytes_read, bytes_written; + BOOL success; + + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; + sa.lpSecurityDescriptor = NULL; + + /* Create pipes for child process stdin/stdout */ + if (!CreatePipe(&child_stdin_rd, &child_stdin_wr, &sa, 0)) + return -1; + if (!CreatePipe(&child_stdout_rd, &child_stdout_wr, &sa, 0)) { + CloseHandle(child_stdin_rd); + CloseHandle(child_stdin_wr); + return -1; + } + + /* Ensure our ends of the pipes are not inherited */ + SetHandleInformation(child_stdin_wr, HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(child_stdout_rd, HANDLE_FLAG_INHERIT, 0); + + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + si.hStdInput = child_stdin_rd; + si.hStdOutput = child_stdout_wr; + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + si.dwFlags = STARTF_USESTDHANDLES; + + memset(&pi, 0, sizeof(pi)); + + /* Launch pinentry */ + if (!CreateProcessA(NULL, (LPSTR)pinentry, NULL, NULL, TRUE, + 0, NULL, NULL, &si, &pi)) { + log_error("Failed to launch pinentry: %s (error %lu)\n", + pinentry, GetLastError()); + CloseHandle(child_stdin_rd); + CloseHandle(child_stdin_wr); + CloseHandle(child_stdout_rd); + CloseHandle(child_stdout_wr); + return -1; + } + + /* Close child-side handles */ + CloseHandle(child_stdin_rd); + CloseHandle(child_stdout_wr); + + /* Read initial greeting */ + ReadFile(child_stdout_rd, read_buf, sizeof(read_buf) - 1, + &bytes_read, NULL); + + /* Send SETPROMPT */ + snprintf(cmd_buf, sizeof(cmd_buf), "SETPROMPT %s\n", prompt); + WriteFile(child_stdin_wr, cmd_buf, (DWORD)strlen(cmd_buf), + &bytes_written, NULL); + ReadFile(child_stdout_rd, read_buf, sizeof(read_buf) - 1, + &bytes_read, NULL); + + /* Send SETDESC */ + snprintf(cmd_buf, sizeof(cmd_buf), "SETDESC Enter VPN password\n"); + WriteFile(child_stdin_wr, cmd_buf, (DWORD)strlen(cmd_buf), + &bytes_written, NULL); + ReadFile(child_stdout_rd, read_buf, sizeof(read_buf) - 1, + &bytes_read, NULL); + + /* Send GETPIN */ + snprintf(cmd_buf, sizeof(cmd_buf), "GETPIN\n"); + WriteFile(child_stdin_wr, cmd_buf, (DWORD)strlen(cmd_buf), + &bytes_written, NULL); + + success = ReadFile(child_stdout_rd, read_buf, sizeof(read_buf) - 1, + &bytes_read, NULL); + if (success && bytes_read > 0) { + read_buf[bytes_read] = '\0'; + /* Response should be "D \n" */ + if (read_buf[0] == 'D' && read_buf[1] == ' ') { + char *nl = strchr(&read_buf[2], '\n'); + + if (nl) + *nl = '\0'; + strncpy(pass, &read_buf[2], len - 1); + pass[len - 1] = '\0'; + } + } + + /* Send BYE */ + snprintf(cmd_buf, sizeof(cmd_buf), "BYE\n"); + WriteFile(child_stdin_wr, cmd_buf, (DWORD)strlen(cmd_buf), + &bytes_written, NULL); + + CloseHandle(child_stdin_wr); + CloseHandle(child_stdout_rd); + + WaitForSingleObject(pi.hProcess, 5000); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return pass[0] ? 0 : -1; +} + +void read_password(const char *pinentry, const char *hint, + const char *prompt, char *pass, size_t len) +{ + if (pinentry) { + if (pinentry_read_password(pinentry, hint, prompt, + pass, len) == 0) + return; + log_warn("Pinentry failed, falling back to console.\n"); + } + + console_read_password(prompt, pass, len); +} + +char *read_from_stdin(size_t count) +{ + char *buf; + HANDLE hStdin; + DWORD bytes_read; + + buf = malloc(count + 1); + if (!buf) + return NULL; + + hStdin = GetStdHandle(STD_INPUT_HANDLE); + if (hStdin == INVALID_HANDLE_VALUE) { + free(buf); + return NULL; + } + + if (!ReadFile(hStdin, buf, (DWORD)count, &bytes_read, NULL)) { + free(buf); + return NULL; + } + + buf[bytes_read] = '\0'; + + /* Strip trailing newline */ + while (bytes_read > 0 && + (buf[bytes_read - 1] == '\n' || buf[bytes_read - 1] == '\r')) + buf[--bytes_read] = '\0'; + + return buf; +} + +#endif /* _WIN32 */ diff --git a/src/wintun.h b/src/wintun.h new file mode 100644 index 00000000..15e4a5c2 --- /dev/null +++ b/src/wintun.h @@ -0,0 +1,138 @@ +/* + * Wintun API declarations for dynamic loading. + * Based on the Wintun API (https://www.wintun.net/). + * The actual wintun.dll is loaded at runtime via LoadLibrary. + * + * Wintun is licensed under the MIT license. + */ + +#ifndef OPENFORTIVPN_WINTUN_H +#define OPENFORTIVPN_WINTUN_H + +#ifdef _WIN32 + +#include +#include +#include + +typedef void *WINTUN_ADAPTER_HANDLE; +typedef void *WINTUN_SESSION_HANDLE; + +/* + * Wintun API function types and pointer typedefs. + * Two-step typedefs avoid checkpatch false positives on WINAPI + * calling convention syntax. + */ +typedef WINTUN_ADAPTER_HANDLE WINAPI +WINTUN_CREATE_ADAPTER_FN(const WCHAR *, const WCHAR *, + const GUID *); +typedef WINTUN_CREATE_ADAPTER_FN *WINTUN_CREATE_ADAPTER_FUNC; + +typedef void WINAPI +WINTUN_CLOSE_ADAPTER_FN(WINTUN_ADAPTER_HANDLE); +typedef WINTUN_CLOSE_ADAPTER_FN *WINTUN_CLOSE_ADAPTER_FUNC; + +typedef WINTUN_SESSION_HANDLE WINAPI +WINTUN_START_SESSION_FN(WINTUN_ADAPTER_HANDLE, DWORD); +typedef WINTUN_START_SESSION_FN *WINTUN_START_SESSION_FUNC; + +typedef void WINAPI WINTUN_END_SESSION_FN(WINTUN_SESSION_HANDLE); +typedef WINTUN_END_SESSION_FN *WINTUN_END_SESSION_FUNC; + +typedef HANDLE WINAPI WINTUN_GET_READ_WAIT_EVENT_FN(WINTUN_SESSION_HANDLE); +typedef WINTUN_GET_READ_WAIT_EVENT_FN *WINTUN_GET_READ_WAIT_EVENT_FUNC; + +typedef BYTE *WINAPI WINTUN_RECEIVE_PACKET_FN(WINTUN_SESSION_HANDLE, DWORD *); +typedef WINTUN_RECEIVE_PACKET_FN *WINTUN_RECEIVE_PACKET_FUNC; + +typedef void WINAPI WINTUN_RELEASE_RECEIVE_PACKET_FN(WINTUN_SESSION_HANDLE, const BYTE *); +typedef WINTUN_RELEASE_RECEIVE_PACKET_FN *WINTUN_RELEASE_RECEIVE_PACKET_FUNC; + +typedef BYTE *WINAPI WINTUN_ALLOCATE_SEND_PACKET_FN(WINTUN_SESSION_HANDLE, DWORD); +typedef WINTUN_ALLOCATE_SEND_PACKET_FN *WINTUN_ALLOCATE_SEND_PACKET_FUNC; + +typedef void WINAPI WINTUN_SEND_PACKET_FN(WINTUN_SESSION_HANDLE, const BYTE *); +typedef WINTUN_SEND_PACKET_FN *WINTUN_SEND_PACKET_FUNC; + +typedef void WINAPI WINTUN_GET_ADAPTER_LUID_FN(WINTUN_ADAPTER_HANDLE, NET_LUID *); +typedef WINTUN_GET_ADAPTER_LUID_FN *WINTUN_GET_ADAPTER_LUID_FUNC; + +typedef DWORD WINAPI WINTUN_GET_RUNNING_DRIVER_VERSION_FN(void); +typedef WINTUN_GET_RUNNING_DRIVER_VERSION_FN *WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC; + +/* Function pointer table loaded from wintun.dll */ +struct wintun_api { + HMODULE module; + WINTUN_CREATE_ADAPTER_FUNC CreateAdapter; + WINTUN_CLOSE_ADAPTER_FUNC CloseAdapter; + WINTUN_START_SESSION_FUNC StartSession; + WINTUN_END_SESSION_FUNC EndSession; + WINTUN_GET_READ_WAIT_EVENT_FUNC GetReadWaitEvent; + WINTUN_RECEIVE_PACKET_FUNC ReceivePacket; + WINTUN_RELEASE_RECEIVE_PACKET_FUNC ReleaseReceivePacket; + WINTUN_ALLOCATE_SEND_PACKET_FUNC AllocateSendPacket; + WINTUN_SEND_PACKET_FUNC SendPacket; + WINTUN_GET_ADAPTER_LUID_FUNC GetAdapterLUID; + WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC GetRunningDriverVersion; +}; + +/* + * Load wintun.dll and resolve all function pointers. + * Returns 0 on success, -1 on failure. + */ +static inline int wintun_load(struct wintun_api *api) +{ + HMODULE mod = LoadLibraryW(L"wintun.dll"); + + if (!mod) + return -1; + + api->module = mod; + + api->CreateAdapter = (WINTUN_CREATE_ADAPTER_FUNC) + GetProcAddress(mod, "WintunCreateAdapter"); + api->CloseAdapter = (WINTUN_CLOSE_ADAPTER_FUNC) + GetProcAddress(mod, "WintunCloseAdapter"); + api->StartSession = (WINTUN_START_SESSION_FUNC) + GetProcAddress(mod, "WintunStartSession"); + api->EndSession = (WINTUN_END_SESSION_FUNC) + GetProcAddress(mod, "WintunEndSession"); + api->GetReadWaitEvent = (WINTUN_GET_READ_WAIT_EVENT_FUNC) + GetProcAddress(mod, "WintunGetReadWaitEvent"); + api->ReceivePacket = (WINTUN_RECEIVE_PACKET_FUNC) + GetProcAddress(mod, "WintunReceivePacket"); + api->ReleaseReceivePacket = (WINTUN_RELEASE_RECEIVE_PACKET_FUNC) + GetProcAddress(mod, "WintunReleaseReceivePacket"); + api->AllocateSendPacket = (WINTUN_ALLOCATE_SEND_PACKET_FUNC) + GetProcAddress(mod, "WintunAllocateSendPacket"); + api->SendPacket = (WINTUN_SEND_PACKET_FUNC) + GetProcAddress(mod, "WintunSendPacket"); + api->GetAdapterLUID = (WINTUN_GET_ADAPTER_LUID_FUNC) + GetProcAddress(mod, "WintunGetAdapterLUID"); + api->GetRunningDriverVersion = (WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC) + GetProcAddress(mod, "WintunGetRunningDriverVersion"); + + if (!api->CreateAdapter || !api->CloseAdapter || + !api->StartSession || !api->EndSession || + !api->GetReadWaitEvent || !api->ReceivePacket || + !api->ReleaseReceivePacket || !api->AllocateSendPacket || + !api->SendPacket || !api->GetAdapterLUID || + !api->GetRunningDriverVersion) { + FreeLibrary(mod); + return -1; + } + + return 0; +} + +static inline void wintun_unload(struct wintun_api *api) +{ + if (api->module) { + FreeLibrary(api->module); + api->module = NULL; + } +} + +#endif /* _WIN32 */ + +#endif /* OPENFORTIVPN_WINTUN_H */ diff --git a/tests/ci/checkpatch/typedefs.checkpatch b/tests/ci/checkpatch/typedefs.checkpatch new file mode 100644 index 00000000..768b2b8f --- /dev/null +++ b/tests/ci/checkpatch/typedefs.checkpatch @@ -0,0 +1,38 @@ +BYTE +BOOL +DWORD +HANDLE +HMODULE +ULONG +WORD +WCHAR +GUID +SOCKET +NET_LUID +MIB_IPFORWARD_ROW2 +SOCKADDR_INET +PIP_ADAPTER_ADDRESSES +WINTUN_ADAPTER_HANDLE +WINTUN_SESSION_HANDLE +WINTUN_CREATE_ADAPTER_FN +WINTUN_CLOSE_ADAPTER_FN +WINTUN_START_SESSION_FN +WINTUN_END_SESSION_FN +WINTUN_GET_READ_WAIT_EVENT_FN +WINTUN_RECEIVE_PACKET_FN +WINTUN_RELEASE_RECEIVE_PACKET_FN +WINTUN_ALLOCATE_SEND_PACKET_FN +WINTUN_SEND_PACKET_FN +WINTUN_GET_ADAPTER_LUID_FN +WINTUN_GET_RUNNING_DRIVER_VERSION_FN +WINTUN_CREATE_ADAPTER_FUNC +WINTUN_CLOSE_ADAPTER_FUNC +WINTUN_START_SESSION_FUNC +WINTUN_END_SESSION_FUNC +WINTUN_GET_READ_WAIT_EVENT_FUNC +WINTUN_RECEIVE_PACKET_FUNC +WINTUN_RELEASE_RECEIVE_PACKET_FUNC +WINTUN_ALLOCATE_SEND_PACKET_FUNC +WINTUN_SEND_PACKET_FUNC +WINTUN_GET_ADAPTER_LUID_FUNC +WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC diff --git a/tests/lint/checkpatch.sh b/tests/lint/checkpatch.sh index 949787e3..3842f28d 100755 --- a/tests/lint/checkpatch.sh +++ b/tests/lint/checkpatch.sh @@ -1,17 +1,24 @@ #!/usr/bin/env bash # Copyright (c) 2020 Dimitri Papadopoulos -# Path to checkpatch.pl +# Path to checkpatch.pl and its companion files script_dir=$(dirname "${BASH_SOURCE[0]}") checkpatch_path=$(realpath "${script_dir}/../ci/checkpatch/checkpatch.pl") +typedefs_path=$(realpath "${script_dir}/../ci/checkpatch/typedefs.checkpatch") rc=0 for file in "$@"; do tmp=$(mktemp) + typedefs_arg="" + if [ -f "$typedefs_path" ]; then + typedefs_arg="--typedefsfile=$typedefs_path" + fi + "$checkpatch_path" --no-tree --terse \ - --ignore MACRO_ARG_UNUSED,LEADING_SPACE,SPDX_LICENSE_TAG,CODE_INDENT,NAKED_SSCANF,VOLATILE,NEW_TYPEDEFS,LONG_LINE,LONG_LINE_STRING,QUOTED_WHITESPACE_BEFORE_NEWLINE,STRCPY,STRLCPY,STRNCPY \ + --ignore MACRO_ARG_UNUSED,LEADING_SPACE,SPDX_LICENSE_TAG,CODE_INDENT,NAKED_SSCANF,VOLATILE,NEW_TYPEDEFS,LONG_LINE,LONG_LINE_STRING,QUOTED_WHITESPACE_BEFORE_NEWLINE,STRCPY,STRLCPY,STRNCPY,MACRO_WITH_FLOW_CONTROL \ + $typedefs_arg \ -f "$file" | tee "$tmp" if [ -s "$tmp" ]; then diff --git a/tests/test_ppp.c b/tests/test_ppp.c new file mode 100644 index 00000000..f6db4b46 --- /dev/null +++ b/tests/test_ppp.c @@ -0,0 +1,539 @@ +/* + * Unit tests for the PPP state machine (ppp.c). + * + * Tests LCP and IPCP negotiation flows using synthetic PPP packets + * that mirror what a FortiGate VPN gateway sends. + */ + +#include "ppp.h" + +#include +#include +#include +#include + +static int tests_run; +static int tests_passed; + +#define TEST(name) \ + do { \ + tests_run++; \ + printf(" %-50s", name); \ + } while (0) + +#define PASS() \ + do { \ + tests_passed++; \ + printf("[PASS]\n"); \ + } while (0) + +#define FAIL(msg) \ + printf("[FAIL] %s\n", msg) + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + FAIL(msg); \ + return; \ + } \ + } while (0) + +/* + * Test: ppp_init sets correct defaults. + */ +static void test_ppp_init(void) +{ + struct ppp_context ctx; + + TEST("ppp_init sets defaults"); + ppp_init(&ctx); + ASSERT(ctx.state == PPP_STATE_INITIAL, "state should be INITIAL"); + ASSERT(ctx.mru == 1354, "MRU should be 1354"); + ASSERT(ctx.asyncmap == 0, "ACCM should be 0"); + ASSERT(ctx.local_ip == 0, "local_ip should be 0"); + ASSERT(ctx.negotiation_complete == 0, "negotiation should not be complete"); + PASS(); +} + +/* + * Test: ppp_start_negotiation sends an LCP Config-Request. + */ +static void test_start_negotiation(void) +{ + struct ppp_context ctx; + uint8_t *out; + size_t out_len; + int ret; + + TEST("ppp_start_negotiation sends LCP Config-Request"); + ppp_init(&ctx); + ret = ppp_start_negotiation(&ctx, &out, &out_len); + ASSERT(ret == 0, "should return 0"); + ASSERT(out != NULL, "output should not be NULL"); + ASSERT(out_len >= 6, "output should be at least 6 bytes"); + + /* Check protocol = LCP (0xc021) */ + ASSERT(out[0] == 0xc0 && out[1] == 0x21, "protocol should be LCP"); + /* Check code = Config-Request (1) */ + ASSERT(out[2] == PPP_CODE_CONF_REQ, "code should be Config-Request"); + /* Check state transition */ + ASSERT(ctx.state == PPP_STATE_LCP_SENT, "state should be LCP_SENT"); + + free(out); + PASS(); +} + +/* + * Test: Process a peer LCP Config-Request and respond with Config-Ack. + */ +static void test_lcp_config_req_ack(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + /* Simulated peer LCP Config-Request: + * protocol=0xc021, code=1, id=1, length=8 + * Options: MRU=1500 (type 1, len 4, value 0x05DC) + */ + uint8_t lcp_conf_req[] = { + 0xc0, 0x21, /* LCP protocol */ + 0x01, /* Config-Request */ + 0x01, /* Identifier */ + 0x00, 0x08, /* Length = 8 (4 header + 4 option) */ + 0x01, 0x04, 0x05, 0xDC /* MRU option: type=1, len=4, MRU=1500 */ + }; + + TEST("LCP Config-Request -> Config-Ack"); + ppp_init(&ctx); + ctx.state = PPP_STATE_LCP_SENT; + + ret = ppp_process_incoming(&ctx, lcp_conf_req, sizeof(lcp_conf_req), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK, "should return OK"); + ASSERT(response != NULL, "should generate response"); + ASSERT(response[0] == 0xc0 && response[1] == 0x21, "response should be LCP"); + ASSERT(response[2] == PPP_CODE_CONF_ACK, "response should be Config-Ack"); + ASSERT(response[3] == 0x01, "identifier should match request"); + ASSERT(ip_payload == NULL, "no IP payload expected"); + + free(response); + PASS(); +} + +/* + * Test: Reject LCP Auth option. + */ +static void test_lcp_reject_auth(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + /* LCP Config-Request with Auth option (type 3) + * Length = 4 (header) + 4 (MRU) + 4 (Auth) = 12 + */ + uint8_t lcp_with_auth[] = { + 0xc0, 0x21, + 0x01, 0x02, 0x00, 0x0c, + 0x01, 0x04, 0x05, 0xDC, /* MRU=1500 */ + 0x03, 0x04, 0xc0, 0x23 /* Auth: PAP (0xc023) */ + }; + + TEST("LCP Config-Request with Auth -> Config-Reject"); + ppp_init(&ctx); + ctx.state = PPP_STATE_LCP_SENT; + + ret = ppp_process_incoming(&ctx, lcp_with_auth, sizeof(lcp_with_auth), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK, "should return OK"); + ASSERT(response != NULL, "should generate response"); + ASSERT(response[2] == PPP_CODE_CONF_REJ, "should be Config-Reject"); + + free(response); + PASS(); +} + +/* + * Test: LCP Config-Ack triggers IPCP negotiation start. + */ +static void test_lcp_ack_starts_ipcp(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + /* Peer sends LCP Config-Ack for our request */ + uint8_t lcp_conf_ack[] = { + 0xc0, 0x21, + 0x02, /* Config-Ack */ + 0x01, /* Identifier (matching our request) */ + 0x00, 0x04 /* Length = 4 (header only, all options accepted) */ + }; + + TEST("LCP Config-Ack triggers IPCP Config-Request"); + ppp_init(&ctx); + ctx.state = PPP_STATE_LCP_SENT; + + ret = ppp_process_incoming(&ctx, lcp_conf_ack, sizeof(lcp_conf_ack), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK, "should return OK"); + ASSERT(ctx.state == PPP_STATE_IPCP_SENT, "state should be IPCP_SENT"); + ASSERT(response != NULL, "should generate IPCP Config-Request"); + ASSERT(response[0] == 0x80 && response[1] == 0x21, "should be IPCP"); + ASSERT(response[2] == PPP_CODE_CONF_REQ, "should be Config-Request"); + + free(response); + PASS(); +} + +/* + * Test: IPCP Config-Nak with assigned IP triggers re-request. + */ +static void test_ipcp_nak_assigns_ip(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + /* + * IPCP Config-Nak with: + * - IP-Address: 10.0.1.100 + * - Primary DNS: 8.8.8.8 + * - Secondary DNS: 8.8.4.4 + */ + uint8_t ipcp_nak[] = { + 0x80, 0x21, + 0x03, /* Config-Nak */ + 0x01, /* Identifier */ + 0x00, 0x16, /* Length = 22 */ + 0x03, 0x06, 0x0a, 0x00, 0x01, 0x64, /* IP: 10.0.1.100 */ + 0x81, 0x06, 0x08, 0x08, 0x08, 0x08, /* DNS1: 8.8.8.8 */ + 0x83, 0x06, 0x08, 0x08, 0x04, 0x04 /* DNS2: 8.8.4.4 */ + }; + + TEST("IPCP Config-Nak assigns IP and DNS"); + ppp_init(&ctx); + ctx.state = PPP_STATE_IPCP_SENT; + + ret = ppp_process_incoming(&ctx, ipcp_nak, sizeof(ipcp_nak), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK, "should return OK"); + ASSERT(response != NULL, "should generate re-request"); + ASSERT(response[0] == 0x80 && response[1] == 0x21, "should be IPCP"); + ASSERT(response[2] == PPP_CODE_CONF_REQ, "should be Config-Request"); + + /* Verify the IP was stored */ + { + uint8_t expected_ip[] = {0x0a, 0x00, 0x01, 0x64}; + uint8_t expected_dns1[] = {0x08, 0x08, 0x08, 0x08}; + uint8_t expected_dns2[] = {0x08, 0x08, 0x04, 0x04}; + + ASSERT(memcmp(&ctx.local_ip, expected_ip, 4) == 0, + "local_ip should be 10.0.1.100"); + ASSERT(memcmp(&ctx.dns_primary, expected_dns1, 4) == 0, + "DNS1 should be 8.8.8.8"); + ASSERT(memcmp(&ctx.dns_secondary, expected_dns2, 4) == 0, + "DNS2 should be 8.8.4.4"); + } + + free(response); + PASS(); +} + +/* + * Test: IPCP Config-Ack completes negotiation. + */ +static void test_ipcp_ack_completes(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + uint8_t ipcp_ack[] = { + 0x80, 0x21, + 0x02, /* Config-Ack */ + 0x02, /* Identifier */ + 0x00, 0x04 /* Length */ + }; + + TEST("IPCP Config-Ack completes negotiation"); + ppp_init(&ctx); + ctx.state = PPP_STATE_IPCP_SENT; + + ret = ppp_process_incoming(&ctx, ipcp_ack, sizeof(ipcp_ack), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_NEGOTIATE, "should return NEGOTIATE"); + ASSERT(ctx.state == PPP_STATE_IPCP_OPEN, "state should be IPCP_OPEN"); + ASSERT(ctx.negotiation_complete == 1, "negotiation should be complete"); + + free(response); + PASS(); +} + +/* + * Test: IP data packet extraction after negotiation. + */ +static void test_ip_packet_extraction(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + /* Minimal fake IP packet (just header bytes for testing) */ + uint8_t ip_data_pkt[] = { + 0x00, 0x21, /* IP protocol */ + 0x45, 0x00, 0x00, 0x14, /* IP header: ver=4, IHL=5, len=20 */ + 0x00, 0x01, 0x00, 0x00, /* ID=1, flags=0 */ + 0x40, 0x06, 0x00, 0x00, /* TTL=64, proto=TCP */ + 0x0a, 0x00, 0x01, 0x64, /* Src: 10.0.1.100 */ + 0xc0, 0xa8, 0x01, 0x01 /* Dst: 192.168.1.1 */ + }; + + TEST("IP data packet extraction"); + ppp_init(&ctx); + ctx.state = PPP_STATE_IPCP_OPEN; + ctx.negotiation_complete = 1; + + ret = ppp_process_incoming(&ctx, ip_data_pkt, sizeof(ip_data_pkt), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_IP_PACKET, "should return IP_PACKET"); + ASSERT(ip_payload != NULL, "ip_payload should not be NULL"); + ASSERT(ip_len == sizeof(ip_data_pkt) - 2, "ip_len should be packet minus protocol field"); + ASSERT(ip_payload[0] == 0x45, "first byte should be IP version+IHL"); + ASSERT(response == NULL, "no response for IP data"); + + free(ip_payload); + PASS(); +} + +/* + * Test: IP packet encapsulation. + */ +static void test_ip_encapsulation(void) +{ + uint8_t ip_data[] = {0x45, 0x00, 0x00, 0x14, 0x00, 0x01}; + uint8_t *ppp_data; + size_t ppp_len; + int ret; + + TEST("IP packet encapsulation"); + ret = ppp_encapsulate_ip(ip_data, sizeof(ip_data), &ppp_data, &ppp_len); + ASSERT(ret == 0, "should return 0"); + ASSERT(ppp_len == sizeof(ip_data) + 2, "length should include protocol field"); + ASSERT(ppp_data[0] == 0x00 && ppp_data[1] == 0x21, "protocol should be 0x0021"); + ASSERT(memcmp(&ppp_data[2], ip_data, sizeof(ip_data)) == 0, "data should match"); + + free(ppp_data); + PASS(); +} + +/* + * Test: LCP Echo-Request generates Echo-Reply. + */ +static void test_lcp_echo(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + uint8_t echo_req[] = { + 0xc0, 0x21, + 0x09, /* Echo-Request */ + 0x05, /* Identifier */ + 0x00, 0x08, /* Length = 8 */ + 0x12, 0x34, 0x56, 0x78 /* Magic number */ + }; + + TEST("LCP Echo-Request -> Echo-Reply"); + ppp_init(&ctx); + ctx.state = PPP_STATE_LCP_OPEN; + + ret = ppp_process_incoming(&ctx, echo_req, sizeof(echo_req), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK, "should return OK"); + ASSERT(response != NULL, "should generate Echo-Reply"); + ASSERT(response[2] == PPP_CODE_ECHO_REP, "should be Echo-Reply"); + ASSERT(response[3] == 0x05, "identifier should match"); + + free(response); + PASS(); +} + +/* + * Test: LCP Terminate-Request generates Terminate-Ack. + */ +static void test_lcp_terminate(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + uint8_t term_req[] = { + 0xc0, 0x21, + 0x05, /* Terminate-Request */ + 0x03, /* Identifier */ + 0x00, 0x04 /* Length */ + }; + + TEST("LCP Terminate-Request -> Terminate-Ack"); + ppp_init(&ctx); + ctx.state = PPP_STATE_LCP_OPEN; + + ret = ppp_process_incoming(&ctx, term_req, sizeof(term_req), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_TERMINATED, "should return TERMINATED"); + ASSERT(ctx.state == PPP_STATE_TERMINATED, "state should be TERMINATED"); + ASSERT(response != NULL, "should generate Terminate-Ack"); + ASSERT(response[2] == PPP_CODE_TERM_ACK, "should be Terminate-Ack"); + + free(response); + PASS(); +} + +/* + * Test: Unknown protocol triggers Protocol-Reject. + */ +static void test_unknown_protocol(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload; + size_t resp_len, ip_len; + int ret; + + uint8_t unknown_proto[] = { + 0xAB, 0xCD, /* Unknown protocol */ + 0x01, 0x02, 0x03, 0x04 /* Some data */ + }; + + TEST("Unknown protocol -> Protocol-Reject"); + ppp_init(&ctx); + ctx.state = PPP_STATE_LCP_OPEN; + + ret = ppp_process_incoming(&ctx, unknown_proto, sizeof(unknown_proto), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK, "should return OK"); + ASSERT(response != NULL, "should generate Protocol-Reject"); + ASSERT(response[0] == 0xc0 && response[1] == 0x21, "should be LCP"); + ASSERT(response[2] == PPP_CODE_PROTO_REJ, "should be Protocol-Reject"); + + free(response); + PASS(); +} + +/* + * Test: Full negotiation flow (LCP + IPCP). + */ +static void test_full_negotiation(void) +{ + struct ppp_context ctx; + uint8_t *response, *ip_payload, *start_pkt; + size_t resp_len, ip_len, start_len; + int ret; + + TEST("Full LCP + IPCP negotiation flow"); + ppp_init(&ctx); + + /* Step 1: Start negotiation (sends LCP Config-Request) */ + ret = ppp_start_negotiation(&ctx, &start_pkt, &start_len); + ASSERT(ret == 0, "start should succeed"); + ASSERT(ctx.state == PPP_STATE_LCP_SENT, "should be LCP_SENT"); + free(start_pkt); + + /* Step 2: Peer sends LCP Config-Request (we ack) + * Length = 4 (header) + 4 (MRU) = 8 + */ + uint8_t peer_lcp_req[] = { + 0xc0, 0x21, 0x01, 0x01, 0x00, 0x08, + 0x01, 0x04, 0x05, 0xDC /* MRU=1500 */ + }; + ret = ppp_process_incoming(&ctx, peer_lcp_req, sizeof(peer_lcp_req), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK && response != NULL, "should ack peer LCP"); + ASSERT(response[2] == PPP_CODE_CONF_ACK, "should be Config-Ack"); + free(response); + + /* Step 3: Peer sends LCP Config-Ack (our LCP req accepted) -> triggers IPCP */ + uint8_t peer_lcp_ack[] = { + 0xc0, 0x21, 0x02, 0x01, 0x00, 0x04 + }; + ret = ppp_process_incoming(&ctx, peer_lcp_ack, sizeof(peer_lcp_ack), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK, "should handle LCP ack"); + ASSERT(ctx.state == PPP_STATE_IPCP_SENT, "should be IPCP_SENT"); + ASSERT(response != NULL, "should send IPCP Config-Request"); + free(response); + + /* Step 4: Peer IPCP Config-Nak with assigned addresses */ + uint8_t peer_ipcp_nak[] = { + 0x80, 0x21, 0x03, 0x01, 0x00, 0x16, + 0x03, 0x06, 0x0a, 0x00, 0x01, 0x64, /* IP: 10.0.1.100 */ + 0x81, 0x06, 0x08, 0x08, 0x08, 0x08, /* DNS1: 8.8.8.8 */ + 0x83, 0x06, 0x08, 0x08, 0x04, 0x04 /* DNS2: 8.8.4.4 */ + }; + ret = ppp_process_incoming(&ctx, peer_ipcp_nak, sizeof(peer_ipcp_nak), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_OK, "should handle IPCP Nak"); + ASSERT(response != NULL, "should re-send IPCP request"); + free(response); + + /* Step 5: Peer sends IPCP Config-Ack -> negotiation complete */ + uint8_t peer_ipcp_ack[] = { + 0x80, 0x21, 0x02, 0x02, 0x00, 0x04 + }; + ret = ppp_process_incoming(&ctx, peer_ipcp_ack, sizeof(peer_ipcp_ack), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_NEGOTIATE, "should signal negotiation complete"); + ASSERT(ctx.state == PPP_STATE_IPCP_OPEN, "should be IPCP_OPEN"); + ASSERT(ctx.negotiation_complete == 1, "negotiation should be flagged complete"); + free(response); + + /* Step 6: Peer sends IP data packet */ + uint8_t ip_data[] = { + 0x00, 0x21, + 0x45, 0x00, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, + 0x40, 0x06, 0x00, 0x00, 0x0a, 0x00, 0x01, 0x64, + 0xc0, 0xa8, 0x01, 0x01 + }; + ret = ppp_process_incoming(&ctx, ip_data, sizeof(ip_data), + &response, &resp_len, &ip_payload, &ip_len); + ASSERT(ret == PPP_RET_IP_PACKET, "should extract IP packet"); + ASSERT(ip_len == sizeof(ip_data) - 2, "IP length should match"); + free(ip_payload); + free(response); + + PASS(); +} + +int main(void) +{ + printf("PPP State Machine Tests\n"); + printf("========================\n\n"); + + test_ppp_init(); + test_start_negotiation(); + test_lcp_config_req_ack(); + test_lcp_reject_auth(); + test_lcp_ack_starts_ipcp(); + test_ipcp_nak_assigns_ip(); + test_ipcp_ack_completes(); + test_ip_packet_extraction(); + test_ip_encapsulation(); + test_lcp_echo(); + test_lcp_terminate(); + test_unknown_protocol(); + test_full_negotiation(); + + printf("\n========================\n"); + printf("Results: %d/%d passed\n", tests_passed, tests_run); + + return (tests_passed == tests_run) ? 0 : 1; +}