Pull request #2: Put the open-source part of the VPN client library here

Merge in ADGUARD-CORE-LIBS/vpn-libs from feature/add_opensource_vpn_client to master

Squashed commit of the following:

commit 0b68e8bebcb5ab4ab76aa5d63267f224c5bd56f2
Author: Sergei Gunchenko <s.gunchenko@adguard.com>
Date:   Mon Aug 1 19:28:49 2022 +0300

    common/src/utils.cpp edited online with Bitbucket

commit 237afe76b1e03c8e95fd7564b100d72956598550
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 16:58:00 2022 +0300

    Maybe fix conan-upload-recipes

commit 940c4abfc8808b7e464a37302d7eca5215eda14e
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 16:57:52 2022 +0300

    Un-simplify conan export script

commit c6ee05e3642d27daadf6b1d49374b187bdeec818
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 16:51:12 2022 +0300

    Debug

commit b3956633a8ad4d6ada4559b2fc4fb0d4d09e97ea
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 16:49:27 2022 +0300

    Maybe fix upload-conan-recipes 2

commit 447f05addd243bce179d99812309c3f9bb1b7412
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 16:42:20 2022 +0300

    Maybe fix upload-conan-recipes

commit 8ff27ec486c4176676d3c186dfda13e652fb0344
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 15:39:37 2022 +0300

    Check subprocess return in conan_export.py

commit 5652549b863232f573e18c71844323e3298bf080
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 15:22:30 2022 +0300

    Fix bamboo spec

commit 28ae435a1500afd0e288c33695003f9feb59100f
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 15:20:16 2022 +0300

    Get rid of version.h, use increment_version.sh from native_libs_common

commit 01f4f4892d3718ee48e25192259239c6e95183cd
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 15:16:02 2022 +0300

    Fix conandata.yml

commit b4163a3dfdbdd5dc2817988a8325d21754165e83
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 15:10:30 2022 +0300

    Simplify conan export script

commit 03343cd1ae47a1e60fddb09e58bacf498f765991
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 14:57:26 2022 +0300

    Fix bamboo spec

commit 9ba118b74ddc41e1ccd3d7c754120e03e86699ee
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 14:56:19 2022 +0300

    Update changelog

commit ce1c14cdcf18ab1032d12555db1efaa177ef6101
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 14:55:36 2022 +0300

    Fix increment_version.sh and increment version

commit 56186e50fd51925f6fb7dc17e9d1fc7a5831a2c8
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 14:53:19 2022 +0300

    Add versioning system from native_libs_common

commit e6570a1afdda8da0afad51c94e8d1b7375739fea
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 14:20:13 2022 +0300

    Add missing includes

commit 5e708c71553f31e7998dcc5d310e1abecef44ab4
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 14:17:52 2022 +0300

    Add license

commit 09ee4698adc2514a23962f0549f9a571d40f4f96
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 13:30:12 2022 +0300

    Pull variable out of namespace ag

commit 7bfc263756c175c6ac85fd8310c387c666a5038b
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 12:25:21 2022 +0300

    Update native_libs_common to 2.0.10

commit 68025494e7c781cdfd8ee1f4ef801014c8179d67
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Mon Aug 1 12:25:05 2022 +0300

    Permanently add /Oy- and -fno-omit-frame-pointer compiler flags

... and 123 more commits
This commit is contained in:
Nikita Gorskikh
2022-08-03 13:13:43 +03:00
parent b073c9a83d
commit 72461d0994
766 changed files with 230805 additions and 30 deletions
+25
View File
@@ -0,0 +1,25 @@
---
# We'll use defaults from the LLVM style, but with 4 columns indentation.
BasedOnStyle: LLVM
ColumnLimit: 120
IndentWidth: 4
ContinuationIndentWidth: 8
---
Language: Cpp
AccessModifierOffset: -4
ConstructorInitializerIndentWidth: 8
IndentCaseLabels: false
NamespaceIndentation: None
SpaceBeforeRangeBasedForLoopColon: true
AllowAllArgumentsOnNextLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortFunctionsOnASingleLine: None
AllowShortLambdasOnASingleLine: Empty
BreakConstructorInitializers: BeforeComma
SpaceAfterCStyleCast: true
BreakBeforeBinaryOperators: NonAssignment
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
AlwaysBreakTemplateDeclarations: Yes
# LLVM 15: InsertBraces: true
---
+82
View File
@@ -0,0 +1,82 @@
Checks: '
*,
-abseil-*,
-altera-*,
-android-*,
-darwin-*,
-fuchsia-*,
-linuxkernel-*,
-llvm-*,
-llvmlibc-*,
-zircon-*,
-readability-convert-member-functions-to-static,
-modernize-use-trailing-return-type,
-cppcoreguidelines-pro-type-vararg,
-hicpp-vararg,
-hicpp-uppercase-literal-suffix,
-readability-uppercase-literal-suffix,
-cert-dcl16-c,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-pro-type-cstyle-cast,
-cppcoreguidelines-avoid-c-arrays,
-performance-noexcept-move-constructor,
-bugprone-unhandled-exception-at-new,
-bugprone-lambda-function-name,
-readability-identifier-length,
-cert-err58-cpp,
-hicpp-avoid-c-arrays,
-misc-non-private-member-variables-in-classes,
-cppcoreguidelines-non-private-member-variables-in-classes,
-google-runtime-int,
-google-default-arguments,
-google-readability-casting,
-bugprone-easily-swappable-parameters,
-cppcoreguidelines-owning-memory,
-modernize-avoid-c-arrays,
-hicpp-named-parameter,
-readability-named-parameter,
-readability-implicit-bool-conversion,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-readability-function-cognitive-complexity,
-cppcoreguidelines-pro-type-union-access,
-hicpp-signed-bitwise,
-hicpp-deprecated-headers,
-modernize-deprecated-headers,
-readability-duplicate-include,
-cert-err33-c,
'
WarningsAsErrors: '*'
CheckOptions: [
{ key: readability-identifier-naming.PublicMemberPrefix, value: '' },
{ key: readability-identifier-naming.ProtectedMemberPrefix, value: 'm_' },
{ key: readability-identifier-naming.PrivateMemberPrefix, value: 'm_' },
{ key: readability-identifier-naming.GlobalVariablePrefix, value: 'g_' },
{ key: readability-identifier-naming.MemberCase, value: lower_case },
{ key: readability-identifier-naming.VariableCase, value: lower_case },
{ key: readability-identifier-naming.GlobalVariableCase, value: lower_case },
{ key: readability-identifier-naming.FunctionCase, value: lower_case },
{ key: readability-identifier-naming.MethodCase, value: lower_case },
{ key: readability-identifier-naming.NamespaceCase, value: lower_case },
{ key: readability-identifier-naming.TemplateParameterCase, value: CamelCase },
{ key: readability-identifier-naming.StructCase, value: CamelCase },
{ key: readability-identifier-naming.ClassCase, value: CamelCase },
{ key: readability-identifier-naming.UnionCase, value: CamelCase },
{ key: readability-identifier-naming.EnumCase, value: CamelCase },
{ key: readability-identifier-naming.TypedefCase, value: CamelCase },
{ key: readability-identifier-naming.TypeAliasCase, value: CamelCase },
{ key: readability-identifier-naming.ConstantCase, value: UPPER_CASE },
{ key: readability-identifier-naming.ValueTemplateParameterCase, value: UPPER_CASE },
]
+14
View File
@@ -0,0 +1,14 @@
cmake-build-release/
cmake-build-debug/
.vscode/
.idea/
build/
# OS X
.DS_Store
# Debug files
*.dSYM
# Rust
Cargo.lock
+5
View File
@@ -0,0 +1,5 @@
# CHANGELOG
## 0.90.4
* [Feature] VpnLibs is now open-source.
+36
View File
@@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.6)
project(vpn_libs C CXX)
include(cmake/conan_bootstrap.cmake)
conan_bootstrap(SRCROOT "." CONANFILE "./conanfile.py" SCOPE_NAME agvpn)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
if (MSVC)
set(CompilerFlags CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE CMAKE_CXX_FLAGS_RELWITHDEBINFO
CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE CMAKE_C_FLAGS_RELWITHDEBINFO)
foreach (CompilerFlag ${CompilerFlags})
string(REPLACE "/MDd" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
string(REPLACE "-MDd" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
string(REPLACE "/MTd" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
string(REPLACE "-MTd" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
string(REPLACE "-MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
string(REPLACE "/RTC1" "" ${CompilerFlag} "${${CompilerFlag}}")
endforeach ()
add_compile_definitions(_WIN32_WINNT=0x0601)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_DEPRECATE)
add_compile_options(/Oy-) # -fno-omit-frame-pointer
else ()
add_compile_options(-fno-omit-frame-pointer)
endif ()
add_subdirectory(common)
add_subdirectory(net)
add_subdirectory(tcpip)
add_subdirectory(core)
enable_testing()
+63 -2
View File
@@ -1,4 +1,65 @@
# AdGuard VPN libs
This library unifies the (eventually) open-source `vpn-client` library
with closed-source parts to form the VPN client library used by AdGuard products.
A VPN client library that provides client network traffic tunnelling to an AdGuard VPN server.
## Build instructions
### Prerequisites
* CMake 3.7 or higher
* Clang 8 or higher
* Conan 1.38 or higher
### Building
If it is a clean build, export custom conan packages to the local conan repository.
See https://github.com/AdguardTeam/NativeLibsCommon/blob/master/README.md for details.
To build the main library:
```
mkdir build && cd build
cmake ..
make vpnlibs_core
```
To run tests:
```
make tests && ctest
```
To build a minimal working example application, run:
```
make standalone_client
```
Refer to `core/test/standalone_client/README.md`
## Project structure
Every subproject consists of the following directories and files:
- `include/<module_name>/` - public headers (add the `include` directory to includes only, so that include header
from other module would look like `#include "module/header.h"`)
- `include/<module_name>/internal` - not so public headers (they are not part of the public API,
but may be needed by other modules, or to extend the library)
- `src/` - source code files and private headers
- `test/` - tests and their data
- `CMakeLists.txt` - cmake build config
Root project consists of the following directories and files:
- `common/` - Set of useful general-purpose utilities
- `core/` - VPN client core module (configuration, traffic tunnelling, etc.)
- `net/` - network communication
- `tcpip/` - L3 and L4 protocols handling (the network and transport layers)
- `third-party/` - third-party libraries (this is not a subproject, so subproject's rules are not enforced)
- `CMakeLists.txt` - main cmake build config.
The public API is in `core/include/vpn.h`.
## License
Copyright (C) AdGuard Software Ltd.
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 https://www.gnu.org/licenses/.
+2
View File
@@ -4,3 +4,5 @@
!include 'run-tests.yaml'
---
!include 'run-integration-test.yaml'
---
!include 'upload-conan-recipes.yaml'
+5 -4
View File
@@ -3,7 +3,7 @@ version: 2
plan:
project-key: CL
key: VLIV
name: VpnLibs - Increment version
name: VpnLibs - Increment Version
stages:
- Default Stage:
manual: false
@@ -23,7 +23,7 @@ Increment version:
tasks:
- !include docker-clean.yaml
- checkout:
repository: vpn-libs
repository: core-libs/vpn-libs
path: vpn-libs
force-clean-build: 'true'
description: Checkout
@@ -44,7 +44,8 @@ Increment version:
git remote set-url origin ${bamboo_planRepository_repositoryUrl}
git pull
git reset
git add common/include/version.h
git add common/include/vpn/version.h
git add CHANGELOG.md
git commit -m "skipci: Automatic version increment by Bamboo"
git push
working-dir: vpn-libs
@@ -55,7 +56,7 @@ Increment version:
variables:
custom.version: 'none'
repositories:
- vpn-libs:
- core-libs/vpn-libs:
scope: global
branches:
create: manually
+7 -5
View File
@@ -2,8 +2,8 @@
version: 2
plan:
project-key: CL
key: VOIT
name: VpnLibs - Run tests
key: VLIT
name: VpnLibs - Run Integration Test
stages:
- Default Stage:
manual: false
@@ -22,12 +22,12 @@ Run integration test:
tasks:
- !include docker-clean.yaml
- checkout:
repository: vpn-libs
repository: core-libs/vpn-libs
path: vpn-libs
force-clean-build: 'true'
description: Checkout client
- checkout:
repository: vpn-libs-endpoint
repository: core-libs/vpn-libs-endpoint
path: vpn-libs-endpoint
force-clean-build: 'true'
description: Checkout server
@@ -170,7 +170,9 @@ Run integration test:
- adg-privileged-docker
artifact-subscriptions: [ ]
repositories:
- vpn-libs:
- core-libs/vpn-libs:
scope: global
- core-libs/vpn-libs-endpoint:
scope: global
branches:
create: for-pull-request
+125 -8
View File
@@ -1,18 +1,22 @@
---
version: 2
plan:
project-key: CL
key: VRT
name: VpnLibs - Run tests
key: VLRT
name: VpnLibs - Run Tests
stages:
- Default Stage:
manual: false
final: false
jobs:
- Run C++ tests
Run C++ tests:
key: JOB1
description: Runs C++ tests
- Run tests on Linux
- Run tests on Windows
- Run tests on macOS
Run tests on Linux:
key: RTL
docker:
image: adguard/core-libs:latest
volumes:
@@ -23,7 +27,7 @@ Run C++ tests:
- !include docker-clean.yaml
- checkout:
force-clean-build: 'true'
repository: vpn-libs
repository: core-libs/vpn-libs
path: vpn-libs
description: Checkout Default Repository
- script:
@@ -68,9 +72,119 @@ Run C++ tests:
requirements:
- adg-privileged-docker
artifact-subscriptions: [ ]
Run tests on Windows:
key: RTW
tasks:
- checkout:
description: Checkout Default Repository
force-clean-build: 'true'
- script:
description: Build tests
interpreter: BINSH_OR_CMDEXE
scripts:
- |-
call vcvars32
if exist build\ rmdir /s /q build || exit /b 1
mkdir build || exit /b 1
cd build || exit /b 1
cmake -DCMAKE_BUILD_TYPE=Debug ^
-DCMAKE_C_COMPILER="C:/Program Files (x86)/LLVM/bin/clang-cl.exe" ^
-DCMAKE_CXX_COMPILER="C:/Program Files (x86)/LLVM/bin/clang-cl.exe" ^
-DCMAKE_C_FLAGS_DEBUG=/MT ^
-DCMAKE_CXX_FLAGS_DEBUG=/MT ^
-G "Ninja" ^
..
ninja tests || exit /b 1
- script:
description: Run tests
interpreter: BINSH_OR_CMDEXE
scripts:
- |-
call vcvars32
cd build || exit /b 1
ctest -D ExperimentalTest --no-compress-output || exit /b 1
final-tasks:
- any-task:
plugin-key: fr.cstb.bamboo.plugins.ctest.bamboo-ctest-plugin:test
configuration:
testFilePathPattern: '**/Testing/*/*.xml'
description: Parse tests result
- script:
interpreter: SHELL
scripts:
- |-
REM Upload built binaries
conan upload -r art --all -c "*" > upload.txt 2>&1
REM Clean up local cache
conan remove -f '*'
conan remove --locks
requirements:
- system.builder.msbuild.MSBuild v15.0 (32bit)
artifact-subscriptions: [ ]
Run tests on macOS:
key: RTM
tasks:
- checkout:
description: Checkout Default Repository
force-clean-build: 'true'
- script:
description: Build tests
interpreter: SHELL
scripts:
- |-
# Clean up local cache
conan remove -f '*'
# Build tests
mkdir -p build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug -GNinja \
-DTARGET_OS=macos \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_CXX_FLAGS="-fsanitize=address -stdlib=libc++" \
-DCMAKE_C_FLAGS="-fsanitize=address" \
-DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address' \
..
ninja tests
- script:
description: Run tests
interpreter: SHELL
scripts:
- |-
# Run tests
cd build || exit 1
ctest -D ExperimentalTest --no-compress-output
final-tasks:
- any-task:
plugin-key: fr.cstb.bamboo.plugins.ctest.bamboo-ctest-plugin:test
configuration:
testFilePathPattern: '**/Testing/*/*.xml'
description: Parse tests result
- script:
interpreter: SHELL
scripts:
- |-
# Upload built
conan upload -r art --all -c '*' > upload.txt 2>&1
# Clean up local cache
conan remove -f '*'
# Conan remove locks
conan remove --locks
requirements:
- system.builder.xcode.macOS 10.14
artifact-subscriptions: [ ]
repositories:
- vpn-libs:
- core-libs/vpn-libs:
scope: global
branches:
create: for-pull-request
delete:
@@ -80,7 +194,10 @@ branches:
push-on-success: false
merge-from: VpnLibs - Run tests
link-to-jira: true
notifications: [ ]
labels: [ ]
other:
concurrent-build-plugin: system-default
+63
View File
@@ -0,0 +1,63 @@
---
version: 2
plan:
project-key: CL
key: VLUR
name: VpnLibs - Upload Conan Recipes
stages:
- Default Stage:
manual: false
final: false
jobs:
- Default Job
Default Job:
key: JOB1
docker:
image: adguard/core-libs:latest
volumes:
${bamboo.git.cache.directory}: ${bamboo.git.cache.directory}
${system.HOME}/.ssh: /root/.ssh
docker-run-arguments: [ ]
tasks:
- !include docker-clean.yaml
- checkout:
force-clean-build: 'true'
description: Checkout
- script:
interpreter: SHELL
scripts:
- |-
set -e
git remote set-url origin ${bamboo_planRepository_repositoryUrl}
git pull
git reset
conan remove -f '*'
conan remote add -i 0 art ${bamboo.conanRepoUrl} || true
python3 -m pip install -r ./scripts/requirements.txt
./scripts/conan_export.py
conan user -p "${bamboo_artifactoryPassword}" -r art "${bamboo_artifactoryUser}" > upload.txt 2>&1
conan upload -r art --all -c '*' >> upload.txt 2>&1
final-tasks:
- script:
interpreter: SHELL
scripts:
- |-
# Clean up
conan remove -f '*'
conan remove --locks
description: Clean up
requirements:
- adg-privileged-docker
artifact-subscriptions: [ ]
repositories:
- core-libs/vpn-libs:
scope: global
branches:
create: manually
delete: never
link-to-jira: true
triggers: [ ]
notifications: [ ]
labels: [ ]
other:
concurrent-build-plugin: system-default
+35
View File
@@ -0,0 +1,35 @@
include(GoogleTest)
if(NOT TARGET tests)
add_custom_target(tests)
endif(NOT TARGET tests)
# `EXPAND_GTEST` is useful if the test has a parametrized gtest case, it often makes the report
# unreadable
function(add_unit_test TEST_NAME TEST_DIR EXTRA_INCLUDES IS_GTEST EXPAND_GTEST)
set(FILE_NO_EXT ${TEST_DIR}/${TEST_NAME})
if (EXISTS "${FILE_NO_EXT}.cpp")
set(FILE_PATH ${FILE_NO_EXT}.cpp)
elseif(EXISTS "${FILE_NO_EXT}.c")
set(FILE_PATH ${FILE_NO_EXT}.c)
else()
message(FATAL_ERROR "Cannot find source file for test: ${TEST_NAME} (directory=${TEST_DIR})")
endif()
add_executable(${TEST_NAME} EXCLUDE_FROM_ALL ${FILE_PATH})
foreach(INC ${EXTRA_INCLUDES})
target_include_directories(${TEST_NAME} PRIVATE ${INC})
endforeach()
add_dependencies(tests ${TEST_NAME})
if (${IS_GTEST})
target_link_libraries(${TEST_NAME} PRIVATE CONAN_PKG::gtest)
endif()
if (${EXPAND_GTEST})
gtest_discover_tests(${TEST_NAME})
else()
add_test(${TEST_NAME} ${TEST_NAME})
endif()
endfunction()
+901
View File
@@ -0,0 +1,901 @@
# The MIT License (MIT)
# Copyright (c) 2018 JFrog
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# This file comes from: https://github.com/conan-io/cmake-conan. Please refer
# to this repository for issues and documentation.
# Its purpose is to wrap and launch Conan C/C++ Package Manager when cmake is called.
# It will take CMake current settings (os, compiler, compiler version, architecture)
# and translate them to conan settings for installing and retrieving dependencies.
# It is intended to facilitate developers building projects that have conan dependencies,
# but it is only necessary on the end-user side. It is not necessary to create conan
# packages, in fact it shouldn't be use for that. Check the project documentation.
# version: 0.16.1
include(CMakeParseArguments)
function(_get_msvc_ide_version result)
set(${result} "" PARENT_SCOPE)
if(NOT MSVC_VERSION VERSION_LESS 1400 AND MSVC_VERSION VERSION_LESS 1500)
set(${result} 8 PARENT_SCOPE)
elseif(NOT MSVC_VERSION VERSION_LESS 1500 AND MSVC_VERSION VERSION_LESS 1600)
set(${result} 9 PARENT_SCOPE)
elseif(NOT MSVC_VERSION VERSION_LESS 1600 AND MSVC_VERSION VERSION_LESS 1700)
set(${result} 10 PARENT_SCOPE)
elseif(NOT MSVC_VERSION VERSION_LESS 1700 AND MSVC_VERSION VERSION_LESS 1800)
set(${result} 11 PARENT_SCOPE)
elseif(NOT MSVC_VERSION VERSION_LESS 1800 AND MSVC_VERSION VERSION_LESS 1900)
set(${result} 12 PARENT_SCOPE)
elseif(NOT MSVC_VERSION VERSION_LESS 1900 AND MSVC_VERSION VERSION_LESS 1910)
set(${result} 14 PARENT_SCOPE)
elseif(NOT MSVC_VERSION VERSION_LESS 1910 AND MSVC_VERSION VERSION_LESS 1920)
set(${result} 15 PARENT_SCOPE)
elseif(NOT MSVC_VERSION VERSION_LESS 1920 AND MSVC_VERSION VERSION_LESS 1930)
set(${result} 16 PARENT_SCOPE)
else()
message(FATAL_ERROR "Conan: Unknown MSVC compiler version [${MSVC_VERSION}]")
endif()
endfunction()
macro(_conan_detect_build_type)
conan_parse_arguments(${ARGV})
if(ARGUMENTS_BUILD_TYPE)
set(_CONAN_SETTING_BUILD_TYPE ${ARGUMENTS_BUILD_TYPE})
elseif(CMAKE_BUILD_TYPE)
set(_CONAN_SETTING_BUILD_TYPE ${CMAKE_BUILD_TYPE})
else()
message(FATAL_ERROR "Please specify in command line CMAKE_BUILD_TYPE (-DCMAKE_BUILD_TYPE=Release)")
endif()
string(TOUPPER ${_CONAN_SETTING_BUILD_TYPE} _CONAN_SETTING_BUILD_TYPE_UPPER)
if (_CONAN_SETTING_BUILD_TYPE_UPPER STREQUAL "DEBUG")
set(_CONAN_SETTING_BUILD_TYPE "Debug")
elseif(_CONAN_SETTING_BUILD_TYPE_UPPER STREQUAL "RELEASE")
set(_CONAN_SETTING_BUILD_TYPE "Release")
elseif(_CONAN_SETTING_BUILD_TYPE_UPPER STREQUAL "RELWITHDEBINFO")
set(_CONAN_SETTING_BUILD_TYPE "RelWithDebInfo")
elseif(_CONAN_SETTING_BUILD_TYPE_UPPER STREQUAL "MINSIZEREL")
set(_CONAN_SETTING_BUILD_TYPE "MinSizeRel")
endif()
endmacro()
macro(_conan_check_system_name)
#handle -s os setting
if(CMAKE_SYSTEM_NAME AND NOT CMAKE_SYSTEM_NAME STREQUAL "Generic")
#use default conan os setting if CMAKE_SYSTEM_NAME is not defined
set(CONAN_SYSTEM_NAME ${CMAKE_SYSTEM_NAME})
if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
set(CONAN_SYSTEM_NAME Macos)
endif()
if(${CMAKE_SYSTEM_NAME} STREQUAL "QNX")
set(CONAN_SYSTEM_NAME Neutrino)
endif()
set(CONAN_SUPPORTED_PLATFORMS Windows Linux Macos Android iOS FreeBSD WindowsStore WindowsCE watchOS tvOS FreeBSD SunOS AIX Arduino Emscripten Neutrino)
list (FIND CONAN_SUPPORTED_PLATFORMS "${CONAN_SYSTEM_NAME}" _index)
if (${_index} GREATER -1)
#check if the cmake system is a conan supported one
set(_CONAN_SETTING_OS ${CONAN_SYSTEM_NAME})
else()
message(FATAL_ERROR "cmake system ${CONAN_SYSTEM_NAME} is not supported by conan. Use one of ${CONAN_SUPPORTED_PLATFORMS}")
endif()
endif()
endmacro()
macro(_conan_check_language)
get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
if (";${_languages};" MATCHES ";CXX;")
set(LANGUAGE CXX)
set(USING_CXX 1)
elseif (";${_languages};" MATCHES ";C;")
set(LANGUAGE C)
set(USING_CXX 0)
else ()
message(FATAL_ERROR "Conan: Neither C or C++ was detected as a language for the project. Unabled to detect compiler version.")
endif()
endmacro()
macro(_conan_detect_compiler)
conan_parse_arguments(${ARGV})
if(ARGUMENTS_ARCH)
set(_CONAN_SETTING_ARCH ${ARGUMENTS_ARCH})
endif()
if (${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL GNU)
# using GCC
# TODO: Handle other params
string(REPLACE "." ";" VERSION_LIST ${CMAKE_${LANGUAGE}_COMPILER_VERSION})
list(GET VERSION_LIST 0 MAJOR)
list(GET VERSION_LIST 1 MINOR)
set(COMPILER_VERSION ${MAJOR}.${MINOR})
if(${MAJOR} GREATER 4)
set(COMPILER_VERSION ${MAJOR})
endif()
set(_CONAN_SETTING_COMPILER gcc)
set(_CONAN_SETTING_COMPILER_VERSION ${COMPILER_VERSION})
if (USING_CXX)
conan_cmake_detect_unix_libcxx(_LIBCXX)
set(_CONAN_SETTING_COMPILER_LIBCXX ${_LIBCXX})
endif ()
elseif (${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL Intel)
string(REPLACE "." ";" VERSION_LIST ${CMAKE_${LANGUAGE}_COMPILER_VERSION})
list(GET VERSION_LIST 0 MAJOR)
list(GET VERSION_LIST 1 MINOR)
set(COMPILER_VERSION ${MAJOR}.${MINOR})
set(_CONAN_SETTING_COMPILER intel)
set(_CONAN_SETTING_COMPILER_VERSION ${COMPILER_VERSION})
if (USING_CXX)
conan_cmake_detect_unix_libcxx(_LIBCXX)
set(_CONAN_SETTING_COMPILER_LIBCXX ${_LIBCXX})
endif ()
elseif (${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL AppleClang)
# using AppleClang
string(REPLACE "." ";" VERSION_LIST ${CMAKE_${LANGUAGE}_COMPILER_VERSION})
list(GET VERSION_LIST 0 MAJOR)
list(GET VERSION_LIST 1 MINOR)
set(_CONAN_SETTING_COMPILER apple-clang)
set(_CONAN_SETTING_COMPILER_VERSION ${MAJOR}.${MINOR})
if (USING_CXX)
conan_cmake_detect_unix_libcxx(_LIBCXX)
set(_CONAN_SETTING_COMPILER_LIBCXX ${_LIBCXX})
endif ()
elseif (${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL Clang)
string(REPLACE "." ";" VERSION_LIST ${CMAKE_${LANGUAGE}_COMPILER_VERSION})
list(GET VERSION_LIST 0 MAJOR)
list(GET VERSION_LIST 1 MINOR)
set(_CONAN_SETTING_COMPILER clang)
set(_CONAN_SETTING_COMPILER_VERSION ${MAJOR}.${MINOR})
if(APPLE)
cmake_policy(GET CMP0025 APPLE_CLANG_POLICY)
if(NOT APPLE_CLANG_POLICY STREQUAL NEW)
message(STATUS "Conan: APPLE and Clang detected. Assuming apple-clang compiler. Set CMP0025 to avoid it")
set(_CONAN_SETTING_COMPILER apple-clang)
endif()
endif()
if(${_CONAN_SETTING_COMPILER} STREQUAL clang AND ${MAJOR} GREATER 7)
set(_CONAN_SETTING_COMPILER_VERSION ${MAJOR})
endif()
if (USING_CXX)
conan_cmake_detect_unix_libcxx(_LIBCXX)
set(_CONAN_SETTING_COMPILER_LIBCXX ${_LIBCXX})
endif ()
elseif(${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL MSVC)
set(_VISUAL "Visual Studio")
_get_msvc_ide_version(_VISUAL_VERSION)
if("${_VISUAL_VERSION}" STREQUAL "")
message(FATAL_ERROR "Conan: Visual Studio not recognized")
else()
set(_CONAN_SETTING_COMPILER ${_VISUAL})
set(_CONAN_SETTING_COMPILER_VERSION ${_VISUAL_VERSION})
endif()
if(NOT _CONAN_SETTING_ARCH)
if (MSVC_${LANGUAGE}_ARCHITECTURE_ID MATCHES "64")
set(_CONAN_SETTING_ARCH x86_64)
elseif (MSVC_${LANGUAGE}_ARCHITECTURE_ID MATCHES "^ARM")
message(STATUS "Conan: Using default ARM architecture from MSVC")
set(_CONAN_SETTING_ARCH armv6)
elseif (MSVC_${LANGUAGE}_ARCHITECTURE_ID MATCHES "86")
set(_CONAN_SETTING_ARCH x86)
else ()
message(FATAL_ERROR "Conan: Unknown MSVC architecture [${MSVC_${LANGUAGE}_ARCHITECTURE_ID}]")
endif()
endif()
conan_cmake_detect_vs_runtime(_vs_runtime ${ARGV})
message(STATUS "Conan: Detected VS runtime: ${_vs_runtime}")
set(_CONAN_SETTING_COMPILER_RUNTIME ${_vs_runtime})
if (CMAKE_GENERATOR_TOOLSET)
set(_CONAN_SETTING_COMPILER_TOOLSET ${CMAKE_VS_PLATFORM_TOOLSET})
elseif(CMAKE_VS_PLATFORM_TOOLSET AND (CMAKE_GENERATOR STREQUAL "Ninja"))
set(_CONAN_SETTING_COMPILER_TOOLSET ${CMAKE_VS_PLATFORM_TOOLSET})
endif()
else()
message(FATAL_ERROR "Conan: compiler setup not recognized")
endif()
endmacro()
function(conan_cmake_settings result)
#message(STATUS "COMPILER " ${CMAKE_CXX_COMPILER})
#message(STATUS "COMPILER " ${CMAKE_CXX_COMPILER_ID})
#message(STATUS "VERSION " ${CMAKE_CXX_COMPILER_VERSION})
#message(STATUS "FLAGS " ${CMAKE_LANG_FLAGS})
#message(STATUS "LIB ARCH " ${CMAKE_CXX_LIBRARY_ARCHITECTURE})
#message(STATUS "BUILD TYPE " ${CMAKE_BUILD_TYPE})
#message(STATUS "GENERATOR " ${CMAKE_GENERATOR})
#message(STATUS "GENERATOR WIN64 " ${CMAKE_CL_64})
message(STATUS "Conan: Automatic detection of conan settings from cmake")
conan_parse_arguments(${ARGV})
_conan_detect_build_type(${ARGV})
_conan_check_system_name()
_conan_check_language()
_conan_detect_compiler(${ARGV})
# If profile is defined it is used
if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND ARGUMENTS_DEBUG_PROFILE)
set(_APPLIED_PROFILES ${ARGUMENTS_DEBUG_PROFILE})
elseif(CMAKE_BUILD_TYPE STREQUAL "Release" AND ARGUMENTS_RELEASE_PROFILE)
set(_APPLIED_PROFILES ${ARGUMENTS_RELEASE_PROFILE})
elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo" AND ARGUMENTS_RELWITHDEBINFO_PROFILE)
set(_APPLIED_PROFILES ${ARGUMENTS_RELWITHDEBINFO_PROFILE})
elseif(CMAKE_BUILD_TYPE STREQUAL "MinSizeRel" AND ARGUMENTS_MINSIZEREL_PROFILE)
set(_APPLIED_PROFILES ${ARGUMENTS_MINSIZEREL_PROFILE})
elseif(ARGUMENTS_PROFILE)
set(_APPLIED_PROFILES ${ARGUMENTS_PROFILE})
endif()
foreach(ARG ${_APPLIED_PROFILES})
set(_SETTINGS ${_SETTINGS} -pr=${ARG})
endforeach()
foreach(ARG ${ARGUMENTS_PROFILE_BUILD})
conan_check(VERSION 1.24.0 REQUIRED DETECT_QUIET)
set(_SETTINGS ${_SETTINGS} -pr:b=${ARG})
endforeach()
if(NOT _SETTINGS OR ARGUMENTS_PROFILE_AUTO STREQUAL "ALL")
set(ARGUMENTS_PROFILE_AUTO arch build_type compiler compiler.version
compiler.runtime compiler.libcxx compiler.toolset)
endif()
# remove any manually specified settings from the autodetected settings
foreach(ARG ${ARGUMENTS_SETTINGS})
string(REGEX MATCH "[^=]*" MANUAL_SETTING "${ARG}")
message(STATUS "Conan: ${MANUAL_SETTING} was added as an argument. Not using the autodetected one.")
list(REMOVE_ITEM ARGUMENTS_PROFILE_AUTO "${MANUAL_SETTING}")
endforeach()
# Automatic from CMake
foreach(ARG ${ARGUMENTS_PROFILE_AUTO})
string(TOUPPER ${ARG} _arg_name)
string(REPLACE "." "_" _arg_name ${_arg_name})
if(_CONAN_SETTING_${_arg_name})
set(_SETTINGS ${_SETTINGS} -s ${ARG}=${_CONAN_SETTING_${_arg_name}})
endif()
endforeach()
foreach(ARG ${ARGUMENTS_SETTINGS})
set(_SETTINGS ${_SETTINGS} -s ${ARG})
endforeach()
message(STATUS "Conan: Settings= ${_SETTINGS}")
set(${result} ${_SETTINGS} PARENT_SCOPE)
endfunction()
function(conan_cmake_detect_unix_libcxx result)
# Take into account any -stdlib in compile options
get_directory_property(compile_options DIRECTORY . COMPILE_OPTIONS)
string(GENEX_STRIP "${compile_options}" compile_options)
# Take into account any _GLIBCXX_USE_CXX11_ABI in compile definitions
get_directory_property(defines DIRECTORY . COMPILE_DEFINITIONS)
string(GENEX_STRIP "${defines}" defines)
foreach(define ${defines})
if(define MATCHES "_GLIBCXX_USE_CXX11_ABI")
if(define MATCHES "^-D")
set(compile_options ${compile_options} "${define}")
else()
set(compile_options ${compile_options} "-D${define}")
endif()
endif()
endforeach()
# add additional compiler options ala cmRulePlaceholderExpander::ExpandRuleVariable
set(EXPAND_CXX_COMPILER ${CMAKE_CXX_COMPILER})
if(CMAKE_CXX_COMPILER_ARG1)
# CMake splits CXX="foo bar baz" into CMAKE_CXX_COMPILER="foo", CMAKE_CXX_COMPILER_ARG1="bar baz"
# without this, ccache, winegcc, or other wrappers might lose all their arguments
separate_arguments(SPLIT_CXX_COMPILER_ARG1 NATIVE_COMMAND ${CMAKE_CXX_COMPILER_ARG1})
list(APPEND EXPAND_CXX_COMPILER ${SPLIT_CXX_COMPILER_ARG1})
endif()
if(CMAKE_CXX_COMPILE_OPTIONS_TARGET AND CMAKE_CXX_COMPILER_TARGET)
# without --target= we may be calling the wrong underlying GCC
list(APPEND EXPAND_CXX_COMPILER "${CMAKE_CXX_COMPILE_OPTIONS_TARGET}${CMAKE_CXX_COMPILER_TARGET}")
endif()
if(CMAKE_CXX_COMPILE_OPTIONS_EXTERNAL_TOOLCHAIN AND CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN)
list(APPEND EXPAND_CXX_COMPILER "${CMAKE_CXX_COMPILE_OPTIONS_EXTERNAL_TOOLCHAIN}${CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN}")
endif()
if(CMAKE_CXX_COMPILE_OPTIONS_SYSROOT)
# without --sysroot= we may find the wrong #include <string>
if(CMAKE_SYSROOT_COMPILE)
list(APPEND EXPAND_CXX_COMPILER "${CMAKE_CXX_COMPILE_OPTIONS_SYSROOT}${CMAKE_SYSROOT_COMPILE}")
elseif(CMAKE_SYSROOT)
list(APPEND EXPAND_CXX_COMPILER "${CMAKE_CXX_COMPILE_OPTIONS_SYSROOT}${CMAKE_SYSROOT}")
endif()
endif()
separate_arguments(SPLIT_CXX_FLAGS NATIVE_COMMAND ${CMAKE_CXX_FLAGS})
if(CMAKE_OSX_SYSROOT)
set(xcode_sysroot_option "--sysroot=${CMAKE_OSX_SYSROOT}")
endif()
execute_process(
COMMAND ${CMAKE_COMMAND} -E echo "#include <string>"
COMMAND ${EXPAND_CXX_COMPILER} ${SPLIT_CXX_FLAGS} -x c++ ${xcode_sysroot_option} ${compile_options} -E -dM -
OUTPUT_VARIABLE string_defines
)
if(string_defines MATCHES "#define __GLIBCXX__")
# Allow -D_GLIBCXX_USE_CXX11_ABI=ON/OFF as argument to cmake
if(DEFINED _GLIBCXX_USE_CXX11_ABI)
if(_GLIBCXX_USE_CXX11_ABI)
set(${result} libstdc++11 PARENT_SCOPE)
return()
else()
set(${result} libstdc++ PARENT_SCOPE)
return()
endif()
endif()
if(string_defines MATCHES "#define _GLIBCXX_USE_CXX11_ABI 1\n")
set(${result} libstdc++11 PARENT_SCOPE)
else()
# Either the compiler is missing the define because it is old, and so
# it can't use the new abi, or the compiler was configured to use the
# old abi by the user or distro (e.g. devtoolset on RHEL/CentOS)
set(${result} libstdc++ PARENT_SCOPE)
endif()
else()
set(${result} libc++ PARENT_SCOPE)
endif()
endfunction()
function(conan_cmake_detect_vs_runtime result)
conan_parse_arguments(${ARGV})
if(ARGUMENTS_BUILD_TYPE)
set(build_type "${ARGUMENTS_BUILD_TYPE}")
elseif(CMAKE_BUILD_TYPE)
set(build_type "${CMAKE_BUILD_TYPE}")
else()
message(FATAL_ERROR "Please specify in command line CMAKE_BUILD_TYPE (-DCMAKE_BUILD_TYPE=Release)")
endif()
if(build_type)
string(TOUPPER "${build_type}" build_type)
endif()
set(variables CMAKE_CXX_FLAGS_${build_type} CMAKE_C_FLAGS_${build_type} CMAKE_CXX_FLAGS CMAKE_C_FLAGS)
foreach(variable ${variables})
if(NOT "${${variable}}" STREQUAL "")
string(REPLACE " " ";" flags "${${variable}}")
foreach (flag ${flags})
if("${flag}" STREQUAL "/MD" OR "${flag}" STREQUAL "/MDd" OR "${flag}" STREQUAL "/MT" OR "${flag}" STREQUAL "/MTd")
string(SUBSTRING "${flag}" 1 -1 runtime)
set(${result} "${runtime}" PARENT_SCOPE)
return()
endif()
endforeach()
endif()
endforeach()
if("${build_type}" STREQUAL "DEBUG")
set(${result} "MDd" PARENT_SCOPE)
else()
set(${result} "MD" PARENT_SCOPE)
endif()
endfunction()
function(_collect_settings result)
set(ARGUMENTS_PROFILE_AUTO arch build_type compiler compiler.version
compiler.runtime compiler.libcxx compiler.toolset)
foreach(ARG ${ARGUMENTS_PROFILE_AUTO})
string(TOUPPER ${ARG} _arg_name)
string(REPLACE "." "_" _arg_name ${_arg_name})
if(_CONAN_SETTING_${_arg_name})
set(detected_setings ${detected_setings} ${ARG}=${_CONAN_SETTING_${_arg_name}})
endif()
endforeach()
set(${result} ${detected_setings} PARENT_SCOPE)
endfunction()
function(conan_cmake_autodetect detected_settings)
_conan_detect_build_type()
_conan_check_system_name()
_conan_check_language()
_conan_detect_compiler()
_collect_settings(collected_settings)
set(${detected_settings} ${collected_settings} PARENT_SCOPE)
endfunction()
macro(conan_parse_arguments)
set(options BASIC_SETUP CMAKE_TARGETS UPDATE KEEP_RPATHS NO_LOAD NO_OUTPUT_DIRS OUTPUT_QUIET NO_IMPORTS SKIP_STD)
set(oneValueArgs CONANFILE ARCH BUILD_TYPE INSTALL_FOLDER CONAN_COMMAND)
set(multiValueArgs DEBUG_PROFILE RELEASE_PROFILE RELWITHDEBINFO_PROFILE MINSIZEREL_PROFILE
PROFILE REQUIRES OPTIONS IMPORTS SETTINGS BUILD ENV GENERATORS PROFILE_AUTO
INSTALL_ARGS CONFIGURATION_TYPES PROFILE_BUILD BUILD_REQUIRES)
cmake_parse_arguments(ARGUMENTS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
endmacro()
function(old_conan_cmake_install)
# Calls "conan install"
# Argument BUILD is equivalant to --build={missing, PkgName,...} or
# --build when argument is 'BUILD all' (which builds all packages from source)
# Argument CONAN_COMMAND, to specify the conan path, e.g. in case of running from source
# cmake does not identify conan as command, even if it is +x and it is in the path
conan_parse_arguments(${ARGV})
if(CONAN_CMAKE_MULTI)
set(ARGUMENTS_GENERATORS ${ARGUMENTS_GENERATORS} cmake_multi)
else()
set(ARGUMENTS_GENERATORS ${ARGUMENTS_GENERATORS} cmake)
endif()
set(CONAN_BUILD_POLICY "")
foreach(ARG ${ARGUMENTS_BUILD})
if(${ARG} STREQUAL "all")
set(CONAN_BUILD_POLICY ${CONAN_BUILD_POLICY} --build)
break()
else()
set(CONAN_BUILD_POLICY ${CONAN_BUILD_POLICY} --build=${ARG})
endif()
endforeach()
if(ARGUMENTS_CONAN_COMMAND)
set(CONAN_CMD ${ARGUMENTS_CONAN_COMMAND})
else()
conan_check(REQUIRED)
endif()
set(CONAN_OPTIONS "")
if(ARGUMENTS_CONANFILE)
if(IS_ABSOLUTE ${ARGUMENTS_CONANFILE})
set(CONANFILE ${ARGUMENTS_CONANFILE})
else()
set(CONANFILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARGUMENTS_CONANFILE})
endif()
else()
set(CONANFILE ".")
endif()
foreach(ARG ${ARGUMENTS_OPTIONS})
set(CONAN_OPTIONS ${CONAN_OPTIONS} -o=${ARG})
endforeach()
if(ARGUMENTS_UPDATE)
set(CONAN_INSTALL_UPDATE --update)
endif()
if(ARGUMENTS_NO_IMPORTS)
set(CONAN_INSTALL_NO_IMPORTS --no-imports)
endif()
set(CONAN_INSTALL_FOLDER "")
if(ARGUMENTS_INSTALL_FOLDER)
set(CONAN_INSTALL_FOLDER -if=${ARGUMENTS_INSTALL_FOLDER})
endif()
foreach(ARG ${ARGUMENTS_GENERATORS})
set(CONAN_GENERATORS ${CONAN_GENERATORS} -g=${ARG})
endforeach()
foreach(ARG ${ARGUMENTS_ENV})
set(CONAN_ENV_VARS ${CONAN_ENV_VARS} -e=${ARG})
endforeach()
set(conan_args install ${CONANFILE} ${settings} ${CONAN_ENV_VARS} ${CONAN_GENERATORS} ${CONAN_BUILD_POLICY} ${CONAN_INSTALL_UPDATE} ${CONAN_INSTALL_NO_IMPORTS} ${CONAN_OPTIONS} ${CONAN_INSTALL_FOLDER} ${ARGUMENTS_INSTALL_ARGS})
string (REPLACE ";" " " _conan_args "${conan_args}")
message(STATUS "Conan executing: ${CONAN_CMD} ${_conan_args}")
if(ARGUMENTS_OUTPUT_QUIET)
execute_process(COMMAND ${CONAN_CMD} ${conan_args}
RESULT_VARIABLE return_code
OUTPUT_VARIABLE conan_output
ERROR_VARIABLE conan_output
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
else()
execute_process(COMMAND ${CONAN_CMD} ${conan_args}
RESULT_VARIABLE return_code
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
endif()
if(NOT "${return_code}" STREQUAL "0")
message(FATAL_ERROR "Conan install failed='${return_code}'")
endif()
endfunction()
function(conan_cmake_install)
if(DEFINED CONAN_COMMAND)
set(CONAN_CMD ${CONAN_COMMAND})
else()
conan_check(REQUIRED)
endif()
set(installOptions UPDATE NO_IMPORTS OUTPUT_QUIET ERROR_QUIET)
set(installOneValueArgs PATH_OR_REFERENCE REFERENCE REMOTE LOCKFILE LOCKFILE_OUT LOCKFILE_NODE_ID INSTALL_FOLDER)
set(installMultiValueArgs GENERATOR BUILD ENV ENV_HOST ENV_BUILD OPTIONS_HOST OPTIONS OPTIONS_BUILD PROFILE
PROFILE_HOST PROFILE_BUILD SETTINGS SETTINGS_HOST SETTINGS_BUILD)
cmake_parse_arguments(ARGS "${installOptions}" "${installOneValueArgs}" "${installMultiValueArgs}" ${ARGN})
foreach(arg ${installOptions})
if(ARGS_${arg})
set(${arg} ${${arg}} ${ARGS_${arg}})
endif()
endforeach()
foreach(arg ${installOneValueArgs})
if(DEFINED ARGS_${arg})
if("${arg}" STREQUAL "REMOTE")
set(flag "--remote")
elseif("${arg}" STREQUAL "LOCKFILE")
set(flag "--lockfile")
elseif("${arg}" STREQUAL "LOCKFILE_OUT")
set(flag "--lockfile-out")
elseif("${arg}" STREQUAL "LOCKFILE_NODE_ID")
set(flag "--lockfile-node-id")
elseif("${arg}" STREQUAL "INSTALL_FOLDER")
set(flag "--install-folder")
endif()
set(${arg} ${${arg}} ${flag} ${ARGS_${arg}})
endif()
endforeach()
foreach(arg ${installMultiValueArgs})
if(DEFINED ARGS_${arg})
if("${arg}" STREQUAL "GENERATOR")
set(flag "--generator")
elseif("${arg}" STREQUAL "BUILD")
set(flag "--build")
elseif("${arg}" STREQUAL "ENV")
set(flag "--env")
elseif("${arg}" STREQUAL "ENV_HOST")
set(flag "--env:host")
elseif("${arg}" STREQUAL "ENV_BUILD")
set(flag "--env:build")
elseif("${arg}" STREQUAL "OPTIONS")
set(flag "--options")
elseif("${arg}" STREQUAL "OPTIONS_HOST")
set(flag "--options:host")
elseif("${arg}" STREQUAL "OPTIONS_BUILD")
set(flag "--options:build")
elseif("${arg}" STREQUAL "PROFILE")
set(flag "--profile")
elseif("${arg}" STREQUAL "PROFILE_HOST")
set(flag "--profile:host")
elseif("${arg}" STREQUAL "PROFILE_BUILD")
set(flag "--profile:build")
elseif("${arg}" STREQUAL "SETTINGS")
set(flag "--settings")
elseif("${arg}" STREQUAL "SETTINGS_HOST")
set(flag "--settings:host")
elseif("${arg}" STREQUAL "SETTINGS_BUILD")
set(flag "--settings:build")
endif()
list(LENGTH ARGS_${arg} numargs)
foreach(item ${ARGS_${arg}})
if(${item} STREQUAL "all" AND ${arg} STREQUAL "BUILD")
set(${arg} "--build")
break()
endif()
set(${arg} ${${arg}} ${flag} ${item})
endforeach()
endif()
endforeach()
if(DEFINED UPDATE)
set(UPDATE --update)
endif()
if(DEFINED NO_IMPORTS)
set(NO_IMPORTS --no-imports)
endif()
set(install_args install ${PATH_OR_REFERENCE} ${REFERENCE} ${UPDATE} ${NO_IMPORTS} ${REMOTE} ${LOCKFILE} ${LOCKFILE_OUT} ${LOCKFILE_NODE_ID} ${INSTALL_FOLDER}
${GENERATOR} ${BUILD} ${ENV} ${ENV_HOST} ${ENV_BUILD} ${OPTIONS} ${OPTIONS_HOST} ${OPTIONS_BUILD}
${PROFILE} ${PROFILE_HOST} ${PROFILE_BUILD} ${SETTINGS} ${SETTINGS_HOST} ${SETTINGS_BUILD})
string(REPLACE ";" " " _install_args "${install_args}")
message(STATUS "Conan executing: ${CONAN_CMD} ${_install_args}")
if(ARGS_OUTPUT_QUIET)
set(OUTPUT_OPT OUTPUT_QUIET)
endif()
if(ARGS_ERROR_QUIET)
set(ERROR_OPT ERROR_QUIET)
endif()
execute_process(COMMAND ${CONAN_CMD} ${install_args}
RESULT_VARIABLE return_code
${OUTPUT_OPT}
${ERROR_OPT}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
if(NOT "${return_code}" STREQUAL "0")
if (ARGS_ERROR_QUIET)
message(WARNING "Conan install failed='${return_code}'")
else()
message(FATAL_ERROR "Conan install failed='${return_code}'")
endif()
endif()
endfunction()
function(conan_cmake_setup_conanfile)
conan_parse_arguments(${ARGV})
if(ARGUMENTS_CONANFILE)
get_filename_component(_CONANFILE_NAME ${ARGUMENTS_CONANFILE} NAME)
# configure_file will make sure cmake re-runs when conanfile is updated
configure_file(${ARGUMENTS_CONANFILE} ${CMAKE_CURRENT_BINARY_DIR}/${_CONANFILE_NAME}.junk COPYONLY)
file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/${_CONANFILE_NAME}.junk)
else()
conan_cmake_generate_conanfile(ON ${ARGV})
endif()
endfunction()
function(conan_cmake_configure)
conan_cmake_generate_conanfile(OFF ${ARGV})
endfunction()
# Generate, writing in disk a conanfile.txt with the requires, options, and imports
# specified as arguments
# This will be considered as temporary file, generated in CMAKE_CURRENT_BINARY_DIR)
function(conan_cmake_generate_conanfile DEFAULT_GENERATOR)
conan_parse_arguments(${ARGV})
set(_FN "${CMAKE_CURRENT_BINARY_DIR}/conanfile.txt")
file(WRITE ${_FN} "")
if(DEFINED ARGUMENTS_REQUIRES)
file(APPEND ${_FN} "[requires]\n")
foreach(REQUIRE ${ARGUMENTS_REQUIRES})
file(APPEND ${_FN} ${REQUIRE} "\n")
endforeach()
endif()
if (DEFAULT_GENERATOR OR DEFINED ARGUMENTS_GENERATORS)
file(APPEND ${_FN} "[generators]\n")
if (DEFAULT_GENERATOR)
file(APPEND ${_FN} "cmake\n")
endif()
if (DEFINED ARGUMENTS_GENERATORS)
foreach(GENERATOR ${ARGUMENTS_GENERATORS})
file(APPEND ${_FN} ${GENERATOR} "\n")
endforeach()
endif()
endif()
if(DEFINED ARGUMENTS_BUILD_REQUIRES)
file(APPEND ${_FN} "[build_requires]\n")
foreach(BUILD_REQUIRE ${ARGUMENTS_BUILD_REQUIRES})
file(APPEND ${_FN} ${BUILD_REQUIRE} "\n")
endforeach()
endif()
if(DEFINED ARGUMENTS_IMPORTS)
file(APPEND ${_FN} "[imports]\n")
foreach(IMPORTS ${ARGUMENTS_IMPORTS})
file(APPEND ${_FN} ${IMPORTS} "\n")
endforeach()
endif()
if(DEFINED ARGUMENTS_OPTIONS)
file(APPEND ${_FN} "[options]\n")
foreach(OPTION ${ARGUMENTS_OPTIONS})
file(APPEND ${_FN} ${OPTION} "\n")
endforeach()
endif()
endfunction()
macro(conan_load_buildinfo)
if(CONAN_CMAKE_MULTI)
set(_CONANBUILDINFO conanbuildinfo_multi.cmake)
else()
set(_CONANBUILDINFO conanbuildinfo.cmake)
endif()
if(ARGUMENTS_INSTALL_FOLDER)
set(_CONANBUILDINFOFOLDER ${ARGUMENTS_INSTALL_FOLDER})
else()
set(_CONANBUILDINFOFOLDER ${CMAKE_CURRENT_BINARY_DIR})
endif()
# Checks for the existence of conanbuildinfo.cmake, and loads it
# important that it is macro, so variables defined at parent scope
if(EXISTS "${_CONANBUILDINFOFOLDER}/${_CONANBUILDINFO}")
message(STATUS "Conan: Loading ${_CONANBUILDINFO}")
include(${_CONANBUILDINFOFOLDER}/${_CONANBUILDINFO})
else()
message(FATAL_ERROR "${_CONANBUILDINFO} doesn't exist in ${CMAKE_CURRENT_BINARY_DIR}")
endif()
endmacro()
macro(conan_cmake_run)
conan_parse_arguments(${ARGV})
if(ARGUMENTS_CONFIGURATION_TYPES AND NOT CMAKE_CONFIGURATION_TYPES)
message(WARNING "CONFIGURATION_TYPES should only be specified for multi-configuration generators")
elseif(ARGUMENTS_CONFIGURATION_TYPES AND ARGUMENTS_BUILD_TYPE)
message(WARNING "CONFIGURATION_TYPES and BUILD_TYPE arguments should not be defined at the same time.")
endif()
if(CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE AND NOT CONAN_EXPORTED
AND NOT ARGUMENTS_BUILD_TYPE)
set(CONAN_CMAKE_MULTI ON)
if (NOT ARGUMENTS_CONFIGURATION_TYPES)
set(ARGUMENTS_CONFIGURATION_TYPES "Release;Debug")
endif()
message(STATUS "Conan: Using cmake-multi generator")
else()
set(CONAN_CMAKE_MULTI OFF)
endif()
if(NOT CONAN_EXPORTED)
conan_cmake_setup_conanfile(${ARGV})
if(CONAN_CMAKE_MULTI)
foreach(CMAKE_BUILD_TYPE ${ARGUMENTS_CONFIGURATION_TYPES})
set(ENV{CONAN_IMPORT_PATH} ${CMAKE_BUILD_TYPE})
conan_cmake_settings(settings ${ARGV})
old_conan_cmake_install(SETTINGS ${settings} ${ARGV})
endforeach()
set(CMAKE_BUILD_TYPE)
else()
conan_cmake_settings(settings ${ARGV})
old_conan_cmake_install(SETTINGS ${settings} ${ARGV})
endif()
endif()
if (NOT ARGUMENTS_NO_LOAD)
conan_load_buildinfo()
endif()
if(ARGUMENTS_BASIC_SETUP)
foreach(_option CMAKE_TARGETS KEEP_RPATHS NO_OUTPUT_DIRS SKIP_STD)
if(ARGUMENTS_${_option})
if(${_option} STREQUAL "CMAKE_TARGETS")
list(APPEND _setup_options "TARGETS")
else()
list(APPEND _setup_options ${_option})
endif()
endif()
endforeach()
conan_basic_setup(${_setup_options})
endif()
endmacro()
macro(conan_check)
# Checks conan availability in PATH
# Arguments REQUIRED, DETECT_QUIET and VERSION are optional
# Example usage:
# conan_check(VERSION 1.0.0 REQUIRED)
set(options REQUIRED DETECT_QUIET)
set(oneValueArgs VERSION)
cmake_parse_arguments(CONAN "${options}" "${oneValueArgs}" "" ${ARGN})
if(NOT CONAN_DETECT_QUIET)
message(STATUS "Conan: checking conan executable")
endif()
find_program(CONAN_CMD conan)
if(NOT CONAN_CMD AND CONAN_REQUIRED)
message(FATAL_ERROR "Conan executable not found! Please install conan.")
endif()
if(NOT CONAN_DETECT_QUIET)
message(STATUS "Conan: Found program ${CONAN_CMD}")
endif()
execute_process(COMMAND ${CONAN_CMD} --version
RESULT_VARIABLE return_code
OUTPUT_VARIABLE CONAN_VERSION_OUTPUT
ERROR_VARIABLE CONAN_VERSION_OUTPUT)
if(NOT "${return_code}" STREQUAL "0")
message(FATAL_ERROR "Conan --version failed='${return_code}'")
endif()
if(NOT CONAN_DETECT_QUIET)
message(STATUS "Conan: Version found ${CONAN_VERSION_OUTPUT}")
endif()
if(DEFINED CONAN_VERSION)
string(REGEX MATCH ".*Conan version ([0-9]+\\.[0-9]+\\.[0-9]+)" FOO
"${CONAN_VERSION_OUTPUT}")
if(${CMAKE_MATCH_1} VERSION_LESS ${CONAN_VERSION})
message(FATAL_ERROR "Conan outdated. Installed: ${CMAKE_MATCH_1}, \
required: ${CONAN_VERSION}. Consider updating via 'pip \
install conan==${CONAN_VERSION}'.")
endif()
endif()
endmacro()
function(conan_add_remote)
# Adds a remote
# Arguments URL and NAME are required, INDEX, COMMAND and VERIFY_SSL are optional
# Example usage:
# conan_add_remote(NAME bincrafters INDEX 1
# URL https://api.bintray.com/conan/bincrafters/public-conan
# VERIFY_SSL True)
set(oneValueArgs URL NAME INDEX COMMAND VERIFY_SSL)
cmake_parse_arguments(CONAN "" "${oneValueArgs}" "" ${ARGN})
if(DEFINED CONAN_INDEX)
set(CONAN_INDEX_ARG "-i ${CONAN_INDEX}")
endif()
if(DEFINED CONAN_COMMAND)
set(CONAN_CMD ${CONAN_COMMAND})
else()
conan_check(REQUIRED)
endif()
set(CONAN_VERIFY_SSL_ARG "True")
if(DEFINED CONAN_VERIFY_SSL)
set(CONAN_VERIFY_SSL_ARG ${CONAN_VERIFY_SSL})
endif()
message(STATUS "Conan: Adding ${CONAN_NAME} remote repository (${CONAN_URL}) verify ssl (${CONAN_VERIFY_SSL_ARG})")
execute_process(COMMAND ${CONAN_CMD} remote add ${CONAN_NAME} ${CONAN_INDEX_ARG} -f ${CONAN_URL} ${CONAN_VERIFY_SSL_ARG}
RESULT_VARIABLE return_code)
if(NOT "${return_code}" STREQUAL "0")
message(FATAL_ERROR "Conan remote failed='${return_code}'")
endif()
endfunction()
macro(conan_config_install)
# install a full configuration from a local or remote zip file
# Argument ITEM is required, arguments TYPE, SOURCE, TARGET and VERIFY_SSL are optional
# Example usage:
# conan_config_install(ITEM https://github.com/conan-io/cmake-conan.git
# TYPE git SOURCE source-folder TARGET target-folder VERIFY_SSL false)
set(oneValueArgs ITEM TYPE SOURCE TARGET VERIFY_SSL)
set(multiValueArgs ARGS)
cmake_parse_arguments(CONAN "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
find_program(CONAN_CMD conan)
if(NOT CONAN_CMD AND CONAN_REQUIRED)
message(FATAL_ERROR "Conan executable not found!")
endif()
if(DEFINED CONAN_VERIFY_SSL)
set(CONAN_VERIFY_SSL_ARG "--verify-ssl=${CONAN_VERIFY_SSL}")
endif()
if(DEFINED CONAN_TYPE)
set(CONAN_TYPE_ARG "--type=${CONAN_TYPE}")
endif()
if(DEFINED CONAN_ARGS)
set(CONAN_ARGS_ARGS "--args=\"${CONAN_ARGS}\"")
endif()
if(DEFINED CONAN_SOURCE)
set(CONAN_SOURCE_ARGS "--source-folder=${CONAN_SOURCE}")
endif()
if(DEFINED CONAN_TARGET)
set(CONAN_TARGET_ARGS "--target-folder=${CONAN_TARGET}")
endif()
set (CONAN_CONFIG_INSTALL_ARGS ${CONAN_VERIFY_SSL_ARG}
${CONAN_TYPE_ARG}
${CONAN_ARGS_ARGS}
${CONAN_SOURCE_ARGS}
${CONAN_TARGET_ARGS})
message(STATUS "Conan: Installing config from ${CONAN_ITEM}")
execute_process(COMMAND ${CONAN_CMD} config install ${CONAN_ITEM} ${CONAN_CONFIG_INSTALL_ARGS}
RESULT_VARIABLE return_code)
if(NOT "${return_code}" STREQUAL "0")
message(FATAL_ERROR "Conan config failed='${return_code}'")
endif()
endmacro()
+63
View File
@@ -0,0 +1,63 @@
function(conan_bootstrap)
cmake_parse_arguments(BS "" "SRCROOT;CONANFILE;SCOPE_NAME" "" ${ARGN})
include("${BS_SRCROOT}/cmake/conan.cmake")
if (NOT TARGET CONAN_DONE::${BS_SCOPE_NAME})
# Profile settings
if(ANDROID_ABI STREQUAL armeabi-v7a)
set(CONAN_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/${BS_SRCROOT}/conan/profiles/android-arm.jinja")
elseif(ANDROID_ABI STREQUAL arm64-v8a)
set(CONAN_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/${BS_SRCROOT}/conan/profiles/android-arm64.jinja")
elseif(ANDROID_ABI STREQUAL x86)
set(CONAN_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/${BS_SRCROOT}/conan/profiles/android-x86.jinja")
elseif(ANDROID_ABI STREQUAL x86_64)
set(CONAN_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/${BS_SRCROOT}/conan/profiles/android-x86_64.jinja")
elseif(CMAKE_SYSTEM_NAME STREQUAL Linux)
set(CONAN_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/${BS_SRCROOT}/conan/profiles/linux-clang")
elseif(WIN32)
set(CONAN_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/${BS_SRCROOT}/conan/profiles/windows-clang-cl.jinja")
elseif(APPLE)
if(NOT TARGET_OS)
set(TARGET_OS macos)
endif()
set(CONAN_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/${BS_SRCROOT}/conan/profiles/apple-${TARGET_OS}.jinja")
if(NOT CMAKE_OSX_ARCHITECTURES)
set(CMAKE_OSX_ARCHITECTURES "${CMAKE_SYSTEM_PROCESSOR}")
endif()
if(CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
list(APPEND settings arch=armv8)
else()
list(APPEND settings arch=x86_64)
endif()
else()
set(CONAN_PROFILE "default")
endif()
message("CONAN_PROFILE is ${CONAN_PROFILE}")
if(ANDROID)
conan_cmake_run(CONANFILE "${BS_CONANFILE}"
PROFILE "${CONAN_PROFILE}"
BUILD missing
OUTPUT_QUIET
PROFILE_AUTO compiler.version compiler.libcxx
SETTINGS ${settings}
ENV CMAKE_ORIGINAL_TOOLCHAIN=${CMAKE_TOOLCHAIN_FILE})
elseif(WIN32)
# don't set libcxx on windows to not to mix c++ libraries
conan_cmake_run(CONANFILE "${BS_CONANFILE}"
PROFILE "${CONAN_PROFILE}"
BUILD missing
PROFILE_AUTO compiler.version
SETTINGS ${settings})
else()
conan_cmake_run(CONANFILE "${BS_CONANFILE}"
PROFILE "${CONAN_PROFILE}"
BUILD missing
PROFILE_AUTO compiler.version compiler.libcxx
SETTINGS ${settings})
endif()
conan_define_targets()
add_library(CONAN_DONE::${BS_SCOPE_NAME} INTERFACE IMPORTED)
endif()
endfunction()
+55
View File
@@ -0,0 +1,55 @@
cmake_minimum_required(VERSION 3.0)
project(common)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
find_package(Threads REQUIRED)
if(CMAKE_USE_PTHREADS_INIT)
add_compile_options("-pthread")
endif()
set(VPN_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..)
set(COMMON_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)
set(THIRD_PARTY_DIR ${VPN_LIB_DIR}/third-party)
include(../cmake/conan_bootstrap.cmake)
conan_bootstrap(SRCROOT ".." CONANFILE "../conanfile.py" SCOPE_NAME agvpn)
set(SOURCE_FILES
${COMMON_SRC_DIR}/utils.cpp
${COMMON_SRC_DIR}/fsm.cpp
${COMMON_SRC_DIR}/fsm_validation.cpp
${COMMON_SRC_DIR}/event_loop.cpp
)
add_library(vpnlibs_common STATIC EXCLUDE_FROM_ALL ${SOURCE_FILES})
set_property(TARGET vpnlibs_common PROPERTY POSITION_INDEPENDENT_CODE ON)
if (NOT MSVC)
target_compile_options(vpnlibs_common PRIVATE -W -Wall -Wextra -Werror)
target_compile_options(vpnlibs_common PRIVATE
-Wno-unknown-warning-option -Wno-macro-redefined -Wno-unused-parameter
-Wno-missing-field-initializers -Wno-c99-designator)
endif()
target_include_directories(vpnlibs_common PUBLIC include)
target_link_libraries(vpnlibs_common ${CMAKE_THREAD_LIBS_INIT})
target_link_libraries(vpnlibs_common
CONAN_PKG::native_libs_common CONAN_PKG::libevent
CONAN_PKG::magic_enum CONAN_PKG::dns-libs)
if (MSVC)
target_link_libraries(vpnlibs_common Ws2_32 crypt32 UserEnv)
# Needed as fmt headers leak through the DNS proxy headers
target_compile_definitions(vpnlibs_common PUBLIC FMT_EXCEPTIONS=0)
endif()
enable_testing()
link_libraries(vpnlibs_common)
include(../cmake/add_unit_test.cmake)
set(TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/test)
add_unit_test(test_fsm "${TEST_DIR}" "${COMMON_SRC_DIR}" TRUE TRUE)
add_unit_test(test_event_loop "${TEST_DIR}" "${COMMON_SRC_DIR}" TRUE TRUE)
add_unit_test(test_dns_stamp "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
+184
View File
@@ -0,0 +1,184 @@
#pragma once
#include <optional>
#include <stdbool.h>
#include <stdint.h>
#include <type_traits>
#include <utility>
#include <event2/event.h>
namespace ag {
struct VpnEventLoop;
using TaskId = int64_t;
struct VpnEventLoopTask {
/** User-provided argument which will be passed in the following functions */
void *arg;
/**
* The function to be executed on the event loop.
* Will not be called if the loop is stopped before the task's turn to run.
*/
void (*action)(void *arg, TaskId task_id);
/**
* The helper function which can be used for example to deallocate some resources and
* will be executed anyway either after the `action` or after the task cancellation
* or after the event loop stop.
* May be null.
*/
void (*finalize)(void *arg);
};
/**
* Create an event loop
*/
VpnEventLoop *vpn_event_loop_create();
/**
* Destroy an event loop
*/
void vpn_event_loop_destroy(VpnEventLoop *loop);
/**
* Run an event loop on the current thread.
* Blocks until stopped, if not exited immediately with an error.
* @return 0 if successful, non-zero value otherwise
*/
int vpn_event_loop_run(VpnEventLoop *loop);
/**
* Stop an event loop.
* Basically calls `vpn_event_loop_exit` and `vpn_event_loop_finalize_exit`.
*/
void vpn_event_loop_stop(VpnEventLoop *loop);
/**
* Exit an event loop after the next iteration.
* Non-blocking, `vpn_event_loop_finalize_exit` must be called next.
* @param loop the event loop
* @param ms the amount of time after which the loop should exit
*/
void vpn_event_loop_exit(VpnEventLoop *loop, uint32_t ms);
/**
* Finalize an exitted event loop.
* Blocks until the loop is actually stopped and calls `VpnEventLoopTask#finalize`
* for each task.
* Must be called after `vpn_event_loop_exit`.
*/
void vpn_event_loop_finalize_exit(VpnEventLoop *loop);
/**
* Submit a task to be executed on the next iteration of an event loop
* @param loop the event loop
* @param task the task to be executed
* @return the assigned identifier to the task
*/
TaskId vpn_event_loop_submit(VpnEventLoop *loop, VpnEventLoopTask task);
/**
* Schedule a task to be executed after some time on an event loop
* @param loop the event loop
* @param task the task to be executed
* @param defer_ms the amount of time after which the task is fired
* @return the assigned identifier to the task
*/
TaskId vpn_event_loop_schedule(VpnEventLoop *loop, VpnEventLoopTask task, uint32_t defer_ms);
/**
* Submit a task that runs `action` to the event loop and block until it is finalized.
* Note that the task may not be executed in some circumstances (e.g. if the event
* loop is stopped before it has the chance to execute it), but it is always finalized.
* @param loop the event loop
* @param action the action to be executed
* @param arg the argument for the action
* @return whether the action has been executed
*/
bool vpn_event_loop_dispatch_sync(VpnEventLoop *loop, void (*action)(void *arg), void *arg);
/**
* Cancel a task execution
* @param loop the event loop
* @param task_id the identifier of the task
*/
void vpn_event_loop_cancel(VpnEventLoop *loop, TaskId task_id);
/**
* Get underlying event base
*/
struct event_base *vpn_event_loop_get_base(const VpnEventLoop *loop);
/**
* Check if event loop is running
*/
bool vpn_event_loop_is_active(const VpnEventLoop *loop);
class [[nodiscard]] AutoTaskId {
public:
AutoTaskId() = default;
AutoTaskId(VpnEventLoop *loop, TaskId id);
~AutoTaskId();
AutoTaskId(const AutoTaskId &) = delete;
AutoTaskId &operator=(const AutoTaskId &) = delete;
AutoTaskId(AutoTaskId &&other) noexcept;
AutoTaskId &operator=(AutoTaskId &&other) noexcept;
/**
* Cancel the task and clean up
*/
void reset();
/**
* Release the ownership of the managed task
*/
void release();
/**
* Check if contains a valid task identifier
*/
[[nodiscard]] bool has_value() const;
bool operator<(const AutoTaskId &other) const;
private:
friend AutoTaskId make_auto_id(TaskId id);
explicit AutoTaskId(TaskId id);
VpnEventLoop *m_loop = nullptr;
std::optional<TaskId> m_id;
};
/**
* Helper function to search in containers by id
*/
AutoTaskId make_auto_id(TaskId id);
/**
* Just like `vpn_event_loop_submit` but more convenient for C++
*/
AutoTaskId submit(VpnEventLoop *loop, VpnEventLoopTask task);
/**
* Just like `vpn_event_loop_schedule` but more convenient for C++
*/
AutoTaskId schedule(VpnEventLoop *loop, VpnEventLoopTask task, uint32_t defer_ms);
/**
* Just like `vpn_event_loop_dispatch_sync` but more convenient for C++
*/
template <typename Func>
bool dispatch_sync(VpnEventLoop *loop, Func &&func) {
return vpn_event_loop_dispatch_sync(
loop,
[](void *arg) {
(*(decltype(std::addressof(func))) arg)();
},
std::addressof(func));
}
} // namespace ag
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <cstdint>
#include <vector>
#include "vpn/platform.h"
namespace ag {
using FsmState = uint32_t;
using FsmEvent = int;
using FsmCondition = bool (*)(const void *, void *);
using FsmAction = void (*)(void *, void *);
struct FsmTransitionEntry {
FsmState src_state; // transition source state
FsmEvent event; // event identifier
FsmCondition condition; // if returns true, an entry is considered matched
FsmAction before_transition; // action to do before entering the target state
FsmState target_state; // transition target state
FsmAction after_transition; // action to do after entering the target state
};
using FsmTransitionTable = std::vector<FsmTransitionEntry>;
struct FsmParameters {
FsmState initial_state; // initial FSM state
FsmTransitionTable table; // transition table
void *ctx; // user context
const char *fsm_name; // FSM name for logging
const char *const *state_names; // state names table for logging
const char *const *event_names; // event names table for logging
};
class Fsm {
private:
FsmParameters m_params;
FsmState m_current_state;
bool m_recursive;
int m_id;
public:
/// Match such an entry if previous conditions failed (useful as default condition)
static constexpr auto OTHERWISE = nullptr;
/// Always match such an entry (useful as a single always matching condition)
static constexpr auto ANYWAY = nullptr;
/// Entry has no action to execute
static constexpr auto DO_NOTHING = nullptr;
/// Match such an entry in any state
static constexpr auto ANY_SOURCE_STATE = UINT32_MAX;
/// Leave such an entry in the same state
static constexpr auto SAME_TARGET_STATE = UINT32_MAX - 1;
/**
* Create an FSM instance
* @param params FSM parameters
*/
explicit Fsm(FsmParameters params);
Fsm(const Fsm &) = delete;
Fsm &operator=(const Fsm &) = delete;
Fsm(Fsm &&) noexcept = delete;
Fsm &operator=(Fsm &&) noexcept = delete;
~Fsm();
/**
* Perform FSM transition on event
* @param event event to process
* @param data event data
*/
void perform_transition(FsmEvent event, void *data);
/**
* Get current FSM state
*/
[[nodiscard]] FsmState get_state() const;
/**
* Reset FSM to initial state
*/
void reset();
static bool validate_transition_table(const FsmTransitionTable &table);
};
} // namespace ag
+169
View File
@@ -0,0 +1,169 @@
#pragma once
#ifndef _WIN32
#define AG_ERR_IS_EAGAIN(err) ((EAGAIN == (err)) || (EWOULDBLOCK == (err)))
#define AG_ENETUNREACH ENETUNREACH
#define AG_EHOSTUNREACH EHOSTUNREACH
#define AG_ENOBUFS ENOBUFS
#define AG_SHUT_RD SHUT_RD
#define AG_SHUT_WR SHUT_WR
#define AG_SHUT_RDWR SHUT_RDWR
#endif
#ifdef __linux__
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/tcp.h>
#include <netinet/in.h>
#include <pwd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <inttypes.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ag_vsnprintf vsnprintf
#ifdef ANDROID
#include <pthread.h>
#else
#include <sys/syscall.h>
#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 30
static inline pid_t gettid(void) {
return syscall(SYS_gettid);
}
#endif // __GLIBC__ == 2 && __GLIBC_MINOR__ < 30
#endif // ANDROID
#endif //__linux__
#ifdef __MACH__
#include <TargetConditionals.h>
#include <arpa/inet.h>
#include <inttypes.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <pwd.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#define ag_vsnprintf vsnprintf
#if TARGET_OS_IPHONE
#define DEFAULT_CONNECTION_MEMORY_BUFFER_SIZE (128 * 1024)
#endif // TARGET_OS_IPHONE
#endif //__MACH__
#ifdef _WIN32
#define AG_ERR_IS_EAGAIN(err) (WSAEWOULDBLOCK == (err))
#define AG_ENETUNREACH WSAENETUNREACH
#define AG_EHOSTUNREACH WSAEHOSTUNREACH
#define AG_ENOBUFS WSAENOBUFS
#define AG_SHUT_RD SD_RECEIVE
#define AG_SHUT_WR SD_SEND
#define AG_SHUT_RDWR SD_BOTH
#define NOCRYPT // don't conflict with openssl
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#undef ERROR
#undef DELETE
#undef PASSTHROUGH
#include <ws2ipdef.h>
#include <ws2tcpip.h>
// Must be included after ws2*
#include <iphlpapi.h>
typedef int sa_family_t;
#define SHUT_WR SD_SEND
#ifndef _MSC_VER
#include <unistd.h>
#else
#include <basetsd.h>
typedef SSIZE_T ssize_t;
#include <io.h>
#endif
#include <process.h>
#include <winbase.h>
#define __STDC_FORMAT_MACROS
#include <inttypes.h>
#undef max
#undef min
#define ag_vsnprintf std::vsnprintf
static inline uint32_t gettid() {
return GetCurrentThreadId();
}
#ifndef PATH_MAX
#define PATH_MAX 260
#endif
#include <malloc.h>
#include <stdio.h>
#undef P_tmpdir
#define P_tmpdir "."
#undef _P_tmpdir
#define _P_tmpdir "."
// Avoid conflicts with Windows headers
#undef X509_NAME
#undef X509_EXTENSIONS
#undef PKCS7_ISSUER_AND_SERIAL
#undef PKCS7_SIGNER_INFO
#undef OCSP_REQUEST
#undef OCSP_RESPONSE
#endif //_WIN32
#ifdef _WIN32
#define AG_EXPORT extern __declspec(dllexport)
#elif defined(__GNUC__)
#define AG_EXPORT __attribute__((visibility("default")))
#else
#define AG_EXPORT
#endif
#ifdef _WIN32
#define WIN_EXPORT AG_EXPORT
#else
#define WIN_EXPORT
#endif
#undef AG_PLATFORM
#if defined _WIN32
#define AG_PLATFORM "Windows"
#elif defined __MACH__ && TARGET_OS_IPHONE
#define AG_PLATFORM "iOS"
#elif defined __MACH__
#define AG_PLATFORM "Mac"
#elif defined __linux__ && defined ANDROID
#define AG_PLATFORM "Android"
#elif defined __linux__
#define AG_PLATFORM "Linux"
#endif
#ifndef DEFAULT_CONNECTION_MEMORY_BUFFER_SIZE
#define DEFAULT_CONNECTION_MEMORY_BUFFER_SIZE (4 * 1024 * 1024)
#endif // DEFAULT_CONNECTION_MEMORY_BUFFER_SIZE
+375
View File
@@ -0,0 +1,375 @@
#pragma once
#include <climits>
#include <cstdbool>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <string>
#include <string_view>
#include <time.h>
#include <type_traits>
#include <event2/util.h>
#include "vpn/platform.h"
namespace ag {
struct VpnError {
int code;
const char *text;
};
struct VpnConnectionStats {
uint32_t rtt_us; // RTT in microseconds
double packet_loss_ratio; // the ratio of the number of lost packets to the total number of sent packets
};
// The longest ipv6 address len (45) + brackets (2) + port delimiter (1) + maximum port length (5) + null (1)
#define SOCKADDR_STR_BUF_SIZE (INET6_ADDRSTRLEN + 8)
struct TcpFlowCtrlInfo {
size_t send_buffer_size; // free space in a connection write buffer
size_t send_window_size; // size of a connection send window
};
// Default values for `TcpFlowCtrlInfo` if there is no way to get real values
#define DEFAULT_SEND_BUFFER_SIZE (8 * 1024 * 1024)
#define DEFAULT_SEND_WINDOW_SIZE (8 * 1024 * 1024)
// For use in C interfaces. `uint32_t` to make it easier for C# bindings.
#define AG_ARRAY_OF(T) \
struct { \
T *data; \
uint32_t size; \
}
// May be owning or non-owning depending on context
using VpnStr = AG_ARRAY_OF(const char);
#define VPNSTR_INIT(c_string) \
{ c_string, (c_string) ? uint32_t(strlen(c_string)) : 0 }
typedef enum {
VDSP_PLAIN,
VDSP_DNSCRYPT,
VDSP_DOH,
VDSP_TLS,
VDSP_DOQ,
} VpnDnsStampProtocol;
typedef enum {
/** Resolver does DNSSEC validation */
VDSIP_DNSSEC = 1 << 0,
/** Resolver does not record logs */
VDSIP_NO_LOG = 1 << 1,
/** Resolver doesn't intentionally block domains */
VDSIP_NO_FILTER = 1 << 2,
} VpnDnsStampInformalProperties;
typedef AG_ARRAY_OF(uint8_t) VpnBuffer;
typedef struct {
/** Protocol */
VpnDnsStampProtocol proto;
/** IP address and/or port */
const char *server_addr;
/**
* Provider means different things depending on the stamp type
* DNSCrypt: the DNSCrypt provider name
* DOH and DOT: server's hostname
* Plain DNS: not specified
*/
const char *provider_name;
/** (For DoH) absolute URI path, such as /dns-query */
const char *path;
/** The DNSCrypt providers Ed25519 public key, as 32 raw bytes. Empty for other types. */
VpnBuffer server_public_key;
/**
* Hash is the SHA256 digest of one of the TBS certificate found in the validation chain, typically
* the certificate used to sign the resolvers certificate. Multiple hashes can be provided for seamless
* rotations.
*/
AG_ARRAY_OF(VpnBuffer) hashes;
/** Server properties */
VpnDnsStampInformalProperties properties;
} VpnDnsStamp;
/**
* Convert milliseconds to timeval structure
*/
static inline struct timeval ms_to_timeval(uint32_t ms) {
struct timeval tv {}; // NOLINT(cppcoreguidelines-pro-type-member-init)
tv.tv_sec = ms / 1000;
tv.tv_usec = (ms % 1000) * 1000;
return tv;
}
/**
* Convert timeval structure to milliseconds
*/
static inline uint64_t timeval_to_ms(struct timeval tv) {
return (uint64_t) tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
/**
* Get containing sockaddr structure size
*/
size_t sockaddr_get_size(const struct sockaddr *addr);
/**
* Check if address is any
*/
bool sockaddr_is_any(const struct sockaddr *addr);
/**
* Check if address is loopback
*/
bool sockaddr_is_loopback(const struct sockaddr *addr);
/**
* Get port in network byte order from sockaddr
* @param addr sockaddr with ip
* @return port
*/
uint16_t sockaddr_get_raw_port(const struct sockaddr *addr);
/**
* Get port in host byte order from sockaddr
* @param addr sockaddr with ip
* @return port
*/
uint16_t sockaddr_get_port(const struct sockaddr *addr);
/**
* Set port number in host byte order to sockaddr
*/
void sockaddr_set_port(struct sockaddr *addr, int port);
/**
* Get pointer to ip address
* @param addr sockaddr with ip
* @return pointer to ip address
*/
void *sockaddr_get_ip_ptr(const struct sockaddr *addr);
/**
* Get ip address size
*/
size_t sockaddr_get_ip_size(const struct sockaddr *addr);
/**
* Check if addresses are equal
*/
bool sockaddr_equals(const struct sockaddr *lh, const struct sockaddr *rh);
/**
* Convert sockaddr's IP to human-readable string (null-terminated)
* @return >0 length of successfully composed string without terminating null,
* <0 if failed
*/
ssize_t sockaddr_ip_to_str(const struct sockaddr *addr, char *buf, size_t buf_size);
/**
* Convert sockaddr to human-readable string `<IP>:<port>` (null-terminated)
* @return true in case of success, false otherwise
*/
bool sockaddr_to_str(const struct sockaddr *addr, char *buf, size_t buf_size);
/**
* Combine 2 hash codes into a single one
*/
uint64_t hash_pair_combine(uint64_t h1, uint64_t h2);
/**
* Get hash of IP address
*/
uint64_t ip_addr_hash(sa_family_t family, const void *addr);
/**
* Get hash of address:port
*/
uint64_t sockaddr_hash(const struct sockaddr *addr);
/**
* Get hash of a pair of address:port
*/
uint64_t sockaddr_pair_hash(const struct sockaddr *src, const struct sockaddr *dst);
/**
* Create sockaddr from raw buffer
* @param src buffer with ip
* @param size buffer size
* @param port port (in network order!)
* @return composed sockaddr
*/
struct sockaddr_storage sockaddr_from_raw(const uint8_t *src, size_t size, uint16_t port);
/**
* Create sockaddr_storage from sockaddr
* @param addr sockaddr
* @return composed sockaddr_storage
*/
struct sockaddr_storage sockaddr_to_storage(const struct sockaddr *addr);
/**
* Parse an IPv4 or IPv6 address, with optional port, from a string
*
* Recognized formats are:
* - [IPv6Address]:port
* - [IPv6Address]
* - IPv6Address
* - IPv4Address:port
* - IPv4Address
*
* If no port is specified, the port in the output is set to 0
*
* @param str string to parse
* @return parsed sockaddr
*/
struct sockaddr_storage sockaddr_from_str(const char *str);
/**
* Get bound sockaddr from file descriptor
* @param fd descriptor
* @return composed sockaddr
*/
struct sockaddr_storage local_sockaddr_from_fd(evutil_socket_t fd);
/**
* Get connected peer sockaddr from file descriptor
* @param fd descriptor
* @return composed sockaddr
*/
struct sockaddr_storage remote_sockaddr_from_fd(evutil_socket_t fd);
/**
* 32-bit hash of string by djb2 algorithm
*/
uint32_t str_hash32(const char *str, size_t length);
/**
* Do `strdup` if non-null, return null otherwise
*/
char *safe_strdup(const char *s);
/**
* Make `VpnError` from socket descriptor
*/
VpnError make_vpn_error_from_fd(evutil_socket_t fd);
/**
* Make `VpnError` from `errno` (or `WSAGetLastError`)
*/
VpnError make_vpn_from_socket_error(int code);
/**
* Return the amount of time, in nanoseconds, including the time the system was asleep,
* that has passed since an arbitrary point that may change between program runs.
* Suitable e.g. for determining the amount of real time between two events in a single program run.
*/
int64_t get_time_monotonic_nanos();
template <auto FUNC>
using Ftor = std::integral_constant<decltype(FUNC), FUNC>;
template <typename T, auto FUNC>
using DeclPtr = std::unique_ptr<T, Ftor<FUNC>>;
using U8View = std::basic_string_view<uint8_t>;
template <typename T>
constexpr size_t width_of() {
return sizeof(T) * CHAR_BIT;
}
std::string sockaddr_ip_to_str(const struct sockaddr *addr);
std::string sockaddr_to_str(const struct sockaddr *addr);
/** %-formatted string output. */
std::string str_format(const char *fmt, ...)
#if defined __GNUC__ && !defined __MINGW64__ \
&& !defined __MINGW32__ // disable warnings on mingw due to the unsupported "%zu" format
__attribute((format(printf, 1, 2)))
#endif
;
/**
* Encode input data to hex string
* @param data Input buffer
* @return Hex representation of input data
*/
std::string encode_to_hex(U8View data);
/**
* Return a string where all non-printable bytes from `data` are replaced with '?'.
*/
std::string escape_non_print(U8View data);
/**
* Convert a C string to std::string_view, accepting nullptr.
*/
static inline std::string_view safe_to_string_view(const char *c_str) {
return c_str ? std::string_view{c_str} : std::string_view{};
}
/** Return whether the two strings are equal ignoring case. */
bool case_equals(std::string_view a, std::string_view b);
extern "C" {
/**
* Parse an IPv4 or IPv6 address, with optional port, from a string
*
* Recognized formats are:
* - [IPv6Address]:port
* - [IPv6Address]
* - IPv6Address
* - IPv4Address:port
* - IPv4Address
*
* If no port is specified, the port in the output is set to 0
*
* @param str string to parse
* @param result (out) parsed sockaddr
*/
WIN_EXPORT void sockaddr_from_str_out(const char *str, struct sockaddr_storage *result);
/**
* Parse a DNS stamp string. The caller is responsible for freeing
* the result with `vpn_dns_stamp_free()`.
* @param stamp_str "sdns://..." string
* @param error on output, if an error occurred, contains the error description (free with `vpn_string_free()`)
* @return a parsed stamp, or NULL if an error occurred.
*/
WIN_EXPORT VpnDnsStamp *vpn_dns_stamp_from_str(const char *stamp_str, const char **error);
/**
* Free a ag_parse_dns_stamp_result pointer.
*/
WIN_EXPORT void vpn_dns_stamp_free(VpnDnsStamp *stamp);
/**
* Convert a DNS stamp to a "sdns://..." string.
* Free the returned string with `vpn_string_free()`
*/
WIN_EXPORT const char *vpn_dns_stamp_to_str(VpnDnsStamp *stamp);
/**
* Convert a DNS stamp to a string that can be used as a DNS upstream URL.
* Free the returned string with `vpn_string_free()`
*/
WIN_EXPORT const char *vpn_dns_stamp_pretty_url(VpnDnsStamp *stamp);
/**
* Convert a DNS stamp to a string that can NOT be used as a DNS upstream URL, but may be prettier.
* Free the returned string with `vpn_string_free()`
*/
WIN_EXPORT const char *vpn_dns_stamp_prettier_url(VpnDnsStamp *stamp);
/**
* Free a string allocated by VPN.
*/
WIN_EXPORT void vpn_string_free(const char *s);
} // extern "C"
} // namespace ag
+512
View File
@@ -0,0 +1,512 @@
#include <algorithm>
#include <atomic>
#include <cassert>
#include <condition_variable>
#include <list>
#include <memory>
#include <mutex>
#include <optional>
#include <event2/bufferevent.h>
#include <event2/event.h>
#include <event2/thread.h>
#include <magic_enum.hpp>
#ifdef EVTHREAD_USE_PTHREADS_IMPLEMENTED
#include <signal.h>
#endif
#include "common/logger.h"
#include "vpn/event_loop.h"
#include "vpn/platform.h"
#include "vpn/utils.h"
#define log_loop(loop_, lvl_, fmt_, ...) lvl_##log((loop_)->log, "[{}] " fmt_, (loop_)->id, ##__VA_ARGS__)
#define log_task(loop_, tid_, lvl_, fmt_, ...) \
lvl_##log((loop_)->log, "[{}/id={}] " fmt_, (loop_)->id, tid_, ##__VA_ARGS__)
namespace ag {
std::atomic<TaskId> g_next_task_id = 0;
std::atomic_int g_next_loop_id = 0;
static event_base *make_event_base();
static void run_task_queue(evutil_socket_t, short, void *arg);
static void run_deferred_task(evutil_socket_t, short, void *arg);
struct TaskInfo {
TaskId id;
VpnEventLoopTask task;
};
struct DeferredTaskCtx {
VpnEventLoop *parent_loop;
TaskId id;
};
struct DeferredTaskInfo {
TaskInfo basic;
std::unique_ptr<DeferredTaskCtx> ctx;
DeclPtr<event, &event_free> timer_event;
};
enum EventLoopState {
ELS_STOPPED,
ELS_RUNNING,
ELS_BASE_EXITED,
};
struct VpnEventLoop {
DeclPtr<event_base, &event_base_free> ev_base{make_event_base()};
mutable std::mutex guard;
std::condition_variable stop_barrier;
bool task_queue_scheduled = false;
std::list<TaskInfo> task_queue;
std::list<DeferredTaskInfo> deferred_task_queue;
EventLoopState state = ELS_STOPPED;
bool stopping_externally = false;
ag::Logger log{"EVLOOP"};
int id = g_next_loop_id++;
};
extern "C" struct evthread_lock_callbacks *evthread_get_lock_callbacks(void);
static const struct EventLoopStaticInitializer {
EventLoopStaticInitializer() {
#ifndef NDEBUG
event_enable_debug_mode();
#endif
// Check if `evthread_use_*()` was called before, because calling it twice leads to
// program termination in case Libevent's debugging checks are enabled
if (evthread_get_lock_callbacks()->lock == nullptr) {
#ifdef EVTHREAD_USE_PTHREADS_IMPLEMENTED
evthread_use_pthreads();
#endif
#ifdef EVTHREAD_USE_WINDOWS_THREADS_IMPLEMENTED
evthread_use_windows_threads();
static WSADATA wsa_data;
static const auto wsa_ret [[maybe_unused]] = WSAStartup(MAKEWORD(2, 2), &wsa_data);
assert(wsa_ret == 0);
#endif
}
#if 0
event_enable_debug_logging(EVENT_DBG_ALL);
#endif
event_set_log_callback([](int severity, const char *msg) {
static ag::Logger libevent_logger{"LIBEVENT"};
switch (severity) {
case EVENT_LOG_DEBUG:
dbglog(libevent_logger, "{}", msg);
break;
case EVENT_LOG_MSG:
infolog(libevent_logger, "{}", msg);
break;
case EVENT_LOG_WARN:
warnlog(libevent_logger, "{}", msg);
break;
case EVENT_LOG_ERR:
errlog(libevent_logger, "{}", msg);
break;
default:
dbglog(libevent_logger, "???: {}", msg);
break;
}
});
}
} ENSURE_INITIALIZED [[maybe_unused]];
VpnEventLoop *vpn_event_loop_create() {
std::unique_ptr<VpnEventLoop> loop{new VpnEventLoop{}};
if (loop->ev_base == nullptr) {
log_loop(loop, err, "Failed to create event base");
return nullptr;
}
return loop.release();
}
void vpn_event_loop_destroy(VpnEventLoop *loop) {
assert(loop == nullptr || loop->task_queue.empty());
assert(loop == nullptr || loop->deferred_task_queue.empty());
delete loop;
}
int vpn_event_loop_run(VpnEventLoop *loop) {
log_loop(loop, dbg, "...");
#ifdef __MACH__
static auto ensure_sigpipe_ignored [[maybe_unused]] = signal(SIGPIPE, SIG_IGN);
#elif defined EVTHREAD_USE_PTHREADS_IMPLEMENTED
// Block SIGPIPE
sigset_t sigset, oldset;
sigemptyset(&sigset);
sigaddset(&sigset, SIGPIPE);
pthread_sigmask(SIG_BLOCK, &sigset, &oldset);
#endif // __MACH__
loop->guard.lock();
assert(loop->state == ELS_STOPPED);
loop->state = ELS_RUNNING;
loop->guard.unlock();
log_loop(loop, dbg, "Running event base...");
int r = event_base_loop(loop->ev_base.get(), EVLOOP_NO_EXIT_ON_EMPTY);
log_loop(loop, dbg, "Exited from event base ({})", r);
loop->guard.lock();
loop->state = (loop->stopping_externally) ? ELS_BASE_EXITED : ELS_STOPPED;
loop->guard.unlock();
loop->stop_barrier.notify_one();
#if defined(EVTHREAD_USE_PTHREADS_IMPLEMENTED) && !defined(__MACH__)
// Restore SIGPIPE state
pthread_sigmask(SIG_SETMASK, &oldset, nullptr);
#endif
log_loop(loop, dbg, "Done");
return r;
}
void vpn_event_loop_stop(VpnEventLoop *loop) {
vpn_event_loop_exit(loop, 0);
vpn_event_loop_finalize_exit(loop);
}
void vpn_event_loop_exit(VpnEventLoop *loop, uint32_t ms) {
log_loop(loop, dbg, "...");
timeval tv = ms_to_timeval(ms);
event_base_loopexit(loop->ev_base.get(), &tv);
log_loop(loop, dbg, "Done");
}
void vpn_event_loop_finalize_exit(VpnEventLoop *loop) {
log_loop(loop, dbg, "...");
std::unique_lock l(loop->guard);
log_loop(loop, dbg, "Waiting until run finished (current state={})", magic_enum::enum_name(loop->state));
loop->stopping_externally = true;
loop->stop_barrier.wait(l, [loop]() -> bool {
return loop->state != ELS_RUNNING;
});
log_loop(loop, dbg, "Run finish waited");
for (TaskInfo &info : loop->task_queue) {
if (info.task.finalize != nullptr) {
log_task(loop, info.id, trace, "Finalizing");
info.task.finalize(info.task.arg);
}
}
loop->task_queue.clear();
for (DeferredTaskInfo &info : loop->deferred_task_queue) {
if (info.basic.task.finalize != nullptr) {
log_task(loop, info.basic.id, trace, "Finalizing");
info.basic.task.finalize(info.basic.task.arg);
}
}
loop->deferred_task_queue.clear();
loop->state = ELS_STOPPED;
loop->stopping_externally = false;
log_loop(loop, dbg, "Done");
}
TaskId vpn_event_loop_submit(VpnEventLoop *loop, VpnEventLoopTask task) {
loop->guard.lock();
TaskId task_id = g_next_task_id++;
switch (loop->state) {
case ELS_BASE_EXITED:
loop->guard.unlock();
log_task(loop, task_id, trace, "Finalizing immediately as loop is exited");
task.finalize(task.arg);
return -1;
case ELS_STOPPED:
case ELS_RUNNING:
// handled below
break;
}
loop->task_queue.push_back({task_id, task});
log_task(loop, task_id, trace, "Queued");
if (!loop->task_queue_scheduled) {
loop->task_queue_scheduled = true;
loop->guard.unlock();
event_base_once(loop->ev_base.get(), -1, EV_TIMEOUT, &run_task_queue, loop, nullptr);
} else {
loop->guard.unlock();
}
return task_id;
}
TaskId vpn_event_loop_schedule(VpnEventLoop *loop, VpnEventLoopTask task, uint32_t defer_ms) {
TaskId task_id = g_next_task_id++;
loop->guard.lock();
switch (loop->state) {
case ELS_BASE_EXITED:
loop->guard.unlock();
log_task(loop, task_id, trace, "Finalizing immediately as loop is exited");
task.finalize(task.arg);
return -1;
case ELS_STOPPED:
case ELS_RUNNING:
// handled below
break;
}
DeferredTaskInfo &info = loop->deferred_task_queue.emplace_back(
DeferredTaskInfo{{task_id, task}, std::make_unique<DeferredTaskCtx>(DeferredTaskCtx{loop, task_id})});
info.timer_event.reset(event_new(loop->ev_base.get(), -1, 0, &run_deferred_task, info.ctx.get()));
const struct timeval tv = ms_to_timeval(defer_ms);
event_add(info.timer_event.get(), &tv);
log_task(loop, task_id, trace, "Scheduled");
loop->guard.unlock();
return task_id;
}
bool vpn_event_loop_dispatch_sync(VpnEventLoop *loop, void (*action)(void *), void *arg) {
struct DispatchContext {
void (*action)(void *);
void *arg;
std::mutex mutex;
std::condition_variable cv;
bool executed = false;
bool finalized = false;
};
DispatchContext ctx{.action = action, .arg = arg};
vpn_event_loop_submit(loop,
{
.arg = &ctx,
.action =
[](void *arg, TaskId) {
auto *ctx = (DispatchContext *) arg;
if (ctx->action) {
ctx->action(ctx->arg);
}
std::scoped_lock l(ctx->mutex);
ctx->executed = true;
},
.finalize =
[](void *arg) {
auto *ctx = (DispatchContext *) arg;
// Hold the lock during `notify_all()` since `ctx` might be deleted after releasing
// `ctx->mutex`
std::scoped_lock l(ctx->mutex);
ctx->finalized = true;
ctx->cv.notify_all();
},
});
std::unique_lock l(ctx.mutex);
ctx.cv.wait(l, [&] {
return ctx.finalized;
});
return ctx.executed;
}
void vpn_event_loop_cancel(VpnEventLoop *loop, TaskId task_id) {
log_task(loop, task_id, trace, "...");
std::optional<TaskInfo> info;
loop->guard.lock();
for (auto i = loop->task_queue.begin(); i != loop->task_queue.end(); ++i) {
if (i->id == task_id) {
info = *i;
loop->task_queue.erase(i);
break;
}
}
if (!info.has_value()) {
for (auto i = loop->deferred_task_queue.begin(); i != loop->deferred_task_queue.end(); ++i) {
if (i->basic.id == task_id) {
info = i->basic;
loop->deferred_task_queue.erase(i);
break;
}
}
}
loop->guard.unlock();
if (!info.has_value()) {
log_task(loop, task_id, trace, "Not found");
} else if (info->task.finalize != nullptr) {
log_task(loop, task_id, trace, "Finalizing");
info->task.finalize(info->task.arg);
}
}
struct event_base *vpn_event_loop_get_base(const VpnEventLoop *loop) {
if (loop == nullptr) {
return nullptr;
}
return loop->ev_base.get();
}
bool vpn_event_loop_is_active(const VpnEventLoop *loop) {
if (loop == nullptr) {
return false;
}
std::scoped_lock l(loop->guard);
return loop->state == ELS_RUNNING && !event_base_got_exit(loop->ev_base.get())
&& !event_base_got_break(loop->ev_base.get());
}
AutoTaskId make_auto_id(TaskId id) {
return AutoTaskId{id};
}
AutoTaskId submit(VpnEventLoop *loop, VpnEventLoopTask task) {
return {loop, vpn_event_loop_submit(loop, task)};
}
AutoTaskId schedule(VpnEventLoop *loop, VpnEventLoopTask task, uint32_t defer_ms) {
return {loop, vpn_event_loop_schedule(loop, task, defer_ms)};
}
AutoTaskId::AutoTaskId(VpnEventLoop *loop, TaskId id)
: m_loop(loop)
, m_id((id >= 0) ? std::make_optional<TaskId>(id) : std::nullopt) {
}
AutoTaskId::AutoTaskId(TaskId id)
: m_id((id >= 0) ? std::make_optional<TaskId>(id) : std::nullopt) {
}
AutoTaskId::~AutoTaskId() {
this->reset();
}
AutoTaskId::AutoTaskId(AutoTaskId &&other) noexcept {
*this = std::move(other);
}
AutoTaskId &AutoTaskId::operator=(AutoTaskId &&other) noexcept {
std::swap(m_loop, other.m_loop);
std::swap(m_id, other.m_id);
return *this;
}
void AutoTaskId::reset() {
if (m_loop != nullptr && m_id.has_value()) {
vpn_event_loop_cancel(m_loop, m_id.value());
}
this->release();
}
void AutoTaskId::release() {
m_loop = nullptr;
m_id.reset();
}
bool AutoTaskId::has_value() const {
return m_id.has_value();
}
bool AutoTaskId::operator<(const AutoTaskId &other) const {
return m_id < other.m_id;
}
static event_base *make_event_base() {
DeclPtr<event_base, &event_base_free> base{event_base_new()};
if (base != nullptr && 0 != evthread_make_base_notifiable(base.get())) {
return nullptr;
}
return base.release();
}
static void run_task_queue(evutil_socket_t, short, void *arg) {
auto *loop = (VpnEventLoop *) arg;
log_loop(loop, trace, "...");
TaskId task_queue_last_id = -1;
if (std::scoped_lock l(loop->guard); !loop->task_queue.empty()) {
if (loop->task_queue.front().id <= loop->task_queue.back().id) {
task_queue_last_id = loop->task_queue.back().id;
} else {
log_loop(loop, err, "Event loop inconsistency: front task id={} is larger than last task id={}",
loop->task_queue.front().id, loop->task_queue.back().id);
task_queue_last_id = std::numeric_limits<TaskId>::max();
// FIXME: we need to investigate this but at this time just execute whole queue.
assert(0);
}
}
TaskInfo info = {};
while (true) {
{
std::scoped_lock l(loop->guard);
if (loop->task_queue.empty()) {
loop->task_queue_scheduled = false;
break;
}
if (loop->task_queue.front().id > task_queue_last_id) {
event_base_once(loop->ev_base.get(), -1, EV_TIMEOUT, &run_task_queue, loop, nullptr);
break;
}
info = loop->task_queue.front();
loop->task_queue.pop_front();
}
log_task(loop, info.id, trace, "Running");
info.task.action(info.task.arg, info.id);
if (info.task.finalize != nullptr) {
log_task(loop, info.id, trace, "Finalizing");
info.task.finalize(info.task.arg);
}
}
log_loop(loop, trace, "Done");
}
static void run_deferred_task(evutil_socket_t, short, void *arg) {
auto *ctx = (DeferredTaskCtx *) arg;
VpnEventLoop *loop = ctx->parent_loop;
std::optional<DeferredTaskInfo> info;
TaskId task_id = ctx->id;
log_task(loop, task_id, trace, "...");
{
std::scoped_lock l(loop->guard);
auto it = std::find_if(loop->deferred_task_queue.begin(), loop->deferred_task_queue.end(),
[task_id](const DeferredTaskInfo &info) -> bool {
return info.basic.id == task_id;
});
if (it != loop->deferred_task_queue.end()) {
info = std::move(*it);
loop->deferred_task_queue.erase(it);
}
}
if (info.has_value()) {
log_task(loop, task_id, trace, "Running");
info->basic.task.action(info->basic.task.arg, info->basic.id);
if (info->basic.task.finalize != nullptr) {
log_task(loop, task_id, trace, "Finalizing");
info->basic.task.finalize(info->basic.task.arg);
}
} else {
log_task(loop, task_id, trace, "Not found");
}
log_task(loop, task_id, trace, "Done");
}
} // namespace ag
+93
View File
@@ -0,0 +1,93 @@
#include <assert.h>
#include <atomic>
#include <stdlib.h>
#include "common/logger.h"
#include "vpn/fsm.h"
namespace ag {
static ag::Logger g_logger{"FSM"};
#define log_fsm(lvl_, fmt_, ...) lvl_##log(g_logger, "[{}/{}] " fmt_, m_id, m_params.fsm_name, ##__VA_ARGS__)
static std::atomic_int g_next_id = 0;
bool validate_fsm_transition_table(FsmTransitionTable table);
static const char *state_name(const FsmParameters &params, FsmState state) {
return params.state_names[state];
}
static const char *event_name(const FsmParameters &params, FsmEvent event) {
return params.event_names[event];
}
Fsm::Fsm(FsmParameters params) // NOLINT(performance-unnecessary-value-param)
: m_params{std::move(params)}
, m_current_state{m_params.initial_state}
, m_recursive{false}
, m_id{g_next_id.fetch_add(1)} {
if (!validate_transition_table(m_params.table)) {
abort();
}
}
Fsm::~Fsm() = default;
void Fsm::perform_transition(FsmEvent event, void *data) {
log_fsm(trace, "Before transition: state={} event={}", state_name(m_params, m_current_state),
event_name(m_params, event));
if (m_recursive) {
log_fsm(err, "Recursive fsm run is prohibited: state={} event={}", state_name(m_params, m_current_state),
event_name(m_params, event));
assert(0);
return;
}
m_recursive = true;
for (auto &entry : m_params.table) {
if (m_current_state != entry.src_state && entry.src_state != Fsm::ANY_SOURCE_STATE) {
continue;
}
if (event != entry.event) {
continue;
}
if (entry.condition != Fsm::ANYWAY && entry.condition != Fsm::OTHERWISE
&& !entry.condition(m_params.ctx, data)) {
continue;
}
if (entry.before_transition != Fsm::DO_NOTHING) {
entry.before_transition(m_params.ctx, data);
}
if (entry.target_state != Fsm::SAME_TARGET_STATE) {
m_current_state = entry.target_state;
}
m_recursive = false;
log_fsm(trace, "After transition: state={}", state_name(m_params, m_current_state));
if (entry.after_transition != Fsm::DO_NOTHING) {
entry.after_transition(m_params.ctx, data);
}
break;
}
m_recursive = false;
}
FsmState Fsm::get_state() const {
return m_current_state;
}
void Fsm::reset() {
m_current_state = m_params.initial_state;
m_recursive = false;
}
} // namespace ag
+73
View File
@@ -0,0 +1,73 @@
#include <cassert>
#include <unordered_map>
#include "common/logger.h"
#include "vpn/fsm.h"
#include "vpn/utils.h"
namespace std {
template <>
struct hash<ag::FsmTransitionEntry> {
size_t operator()(const ag::FsmTransitionEntry &e) const {
return ag::hash_pair_combine(std::hash<ag::FsmState>{}(e.src_state), std::hash<ag::FsmEvent>{}(e.event));
}
};
} // namespace std
namespace ag {
#ifndef NDEBUG
static inline bool operator==(const ag::FsmTransitionEntry &lh, const ag::FsmTransitionEntry &rh) {
return lh.src_state == rh.src_state && lh.event == rh.event;
}
struct EntryValidationInfo {
bool closed = false; // entry with the same (src_state, event) pair is closed by Fsm::OTHERWISE or Fsm::ANYWAY
};
bool Fsm::validate_transition_table(const FsmTransitionTable &table) {
bool result = true;
ag::Logger log{"FSM_VALIDATOR"};
std::unordered_map<FsmTransitionEntry, EntryValidationInfo> validation_table;
for (const auto &entry : table) {
if (entry.src_state == Fsm::SAME_TARGET_STATE) {
errlog(log, "Transition entry can't have SAME_STATE as source state");
result = false;
goto loop_exit;
}
if (entry.target_state == Fsm::ANY_SOURCE_STATE) {
errlog(log, "Transition entry can't have ANY_STATE as target state");
result = false;
goto loop_exit;
}
EntryValidationInfo &info = validation_table[entry];
if (info.closed) {
errlog(log, "Entry with the same (src_state, event) pair is already closed: ({}, {})", entry.src_state,
entry.event);
result = false;
goto loop_exit;
}
info.closed = entry.condition == Fsm::ANYWAY || entry.condition == Fsm::OTHERWISE;
}
loop_exit:
return result;
}
#else
bool Fsm::validate_transition_table(const FsmTransitionTable &) {
return true;
}
#endif // NDEBUG
} // namespace ag
+453
View File
@@ -0,0 +1,453 @@
#include <cctype>
#include <cstdio>
#include <cstring>
#include <string>
#include <event2/util.h>
#include "dnsstamp/dns_stamp.h"
#include "vpn/platform.h"
#include "vpn/utils.h"
namespace ag {
size_t sockaddr_get_size(const struct sockaddr *addr) {
switch (addr->sa_family) {
case AF_INET:
return sizeof(struct sockaddr_in);
case AF_INET6:
return sizeof(struct sockaddr_in6);
default:
return 0;
}
}
bool sockaddr_is_any(const struct sockaddr *addr) {
switch (addr->sa_family) {
case AF_INET:
return htonl(INADDR_ANY) == ((struct sockaddr_in *) addr)->sin_addr.s_addr;
case AF_INET6:
// in6addr_any is already in network order
return 0 == memcmp(&((struct sockaddr_in6 *) addr)->sin6_addr, &in6addr_any, sizeof(in6addr_any));
default:
return false;
}
}
bool sockaddr_is_loopback(const struct sockaddr *addr) {
switch (addr->sa_family) {
case AF_INET:
return htonl(INADDR_LOOPBACK) == ((struct sockaddr_in *) addr)->sin_addr.s_addr;
case AF_INET6:
// in6addr_loopback is already in network order
return 0 == memcmp(&((struct sockaddr_in6 *) addr)->sin6_addr, &in6addr_loopback, sizeof(in6addr_loopback));
default:
return false;
}
}
uint16_t sockaddr_get_raw_port(const struct sockaddr *addr) {
switch (addr->sa_family) {
case AF_INET:
return ((struct sockaddr_in *) addr)->sin_port;
case AF_INET6:
return ((struct sockaddr_in6 *) addr)->sin6_port;
default:
return 0;
}
}
uint16_t sockaddr_get_port(const struct sockaddr *addr) {
return ntohs(sockaddr_get_raw_port(addr));
}
void sockaddr_set_port(struct sockaddr *addr, int port) {
switch (addr->sa_family) {
case AF_INET:
((struct sockaddr_in *) addr)->sin_port = htons(port);
break;
case AF_INET6:
((struct sockaddr_in6 *) addr)->sin6_port = htons(port);
break;
}
}
void *sockaddr_get_ip_ptr(const struct sockaddr *addr) {
switch (addr->sa_family) {
case AF_INET:
return &((struct sockaddr_in *) addr)->sin_addr;
case AF_INET6:
return &((struct sockaddr_in6 *) addr)->sin6_addr;
default:
return NULL;
}
}
size_t sockaddr_get_ip_size(const struct sockaddr *addr) {
switch (addr->sa_family) {
case AF_INET:
return sizeof(((struct sockaddr_in *) addr)->sin_addr);
case AF_INET6:
return sizeof(((struct sockaddr_in6 *) addr)->sin6_addr);
default:
return 0;
}
}
bool sockaddr_equals(const struct sockaddr *lh, const struct sockaddr *rh) {
return lh->sa_family == rh->sa_family && 0 == memcmp(lh->sa_data, rh->sa_data, sockaddr_get_size(lh));
}
ssize_t sockaddr_ip_to_str(const struct sockaddr *addr, char *buf, size_t buf_size) {
const char *orig_buf = buf;
if (addr->sa_family == AF_INET6 && buf_size > 0) {
*buf++ = '[';
--buf_size;
}
if (NULL == evutil_inet_ntop(addr->sa_family, sockaddr_get_ip_ptr(addr), buf, buf_size)) {
snprintf(buf, buf_size, "__conversion error__");
return -1;
}
size_t addr_len = strlen(buf);
buf += addr_len;
buf_size -= addr_len;
if (addr->sa_family == AF_INET6 && buf_size > 0) {
*buf++ = ']';
*buf = '\0';
--buf_size;
}
return buf - orig_buf;
}
bool sockaddr_to_str(const struct sockaddr *addr, char *buf, size_t buf_size) {
ssize_t size = sockaddr_ip_to_str(addr, buf, buf_size);
if (size < 0) {
return false;
}
snprintf(&buf[size], buf_size - size, ":%d", sockaddr_get_port(addr));
return true;
}
uint64_t hash_pair_combine(uint64_t h1, uint64_t h2) {
uint64_t hash = 17;
hash = hash * 31 + h1;
hash = hash * 31 + h2;
return hash;
}
uint64_t ip_addr_hash(sa_family_t family, const void *addr) {
uint64_t hash = 0;
switch (family) {
case AF_INET:
memcpy(&hash, addr, sizeof(uint32_t));
break;
case AF_INET6: {
uint64_t ip_1; // NOLINT(cppcoreguidelines-init-variables)
uint64_t ip_2; // NOLINT(cppcoreguidelines-init-variables)
memcpy(&ip_1, addr, sizeof(ip_1));
memcpy(&ip_2, (uint8_t *) addr + sizeof(ip_1), sizeof(ip_2));
hash = hash_pair_combine(ip_1, ip_2);
break;
}
}
return hash;
}
uint64_t sockaddr_hash(const struct sockaddr *addr) {
return hash_pair_combine(ip_addr_hash(addr->sa_family, sockaddr_get_ip_ptr(addr)), sockaddr_get_port(addr));
}
uint64_t sockaddr_pair_hash(const struct sockaddr *src, const struct sockaddr *dst) {
return hash_pair_combine(sockaddr_hash(src), sockaddr_hash(dst));
}
struct sockaddr_storage sockaddr_from_raw(const uint8_t *src, size_t size, uint16_t port) {
struct sockaddr_storage result = {};
switch (size) {
case 4: {
struct sockaddr_in *sin = (struct sockaddr_in *) &result;
sin->sin_family = AF_INET;
sin->sin_port = port;
sin->sin_addr.s_addr = *(uint32_t *) src;
#ifdef SIN6_LEN
sin->sin_len = sizeof(struct sockaddr_in);
#endif
break;
}
case 16: {
struct sockaddr_in6 *sin = (struct sockaddr_in6 *) &result;
sin->sin6_family = AF_INET6;
sin->sin6_port = port;
memcpy(sin->sin6_addr.s6_addr, src, size);
#ifdef SIN6_LEN
sin->sin6_len = sizeof(struct sockaddr_in6);
#endif
break;
}
}
return result;
}
void sockaddr_from_str_out(const char *str, struct sockaddr_storage *result) {
sockaddr_storage local_result = sockaddr_from_str(str);
std::memcpy(result, &local_result, sizeof(sockaddr_storage));
}
struct sockaddr_storage sockaddr_to_storage(const struct sockaddr *addr) {
void *ip = sockaddr_get_ip_ptr(addr);
return (ip != nullptr)
? sockaddr_from_raw((uint8_t *) ip, (addr->sa_family == AF_INET) ? 4 : 16, htons(sockaddr_get_port(addr)))
: sockaddr_storage{};
}
struct sockaddr_storage sockaddr_from_str(const char *str) {
struct sockaddr_storage addr = {};
int addr_len = sizeof(addr);
if (str != NULL && 0 != evutil_parse_sockaddr_port(str, (struct sockaddr *) &addr, &addr_len)) {
addr = (struct sockaddr_storage){};
}
return addr;
}
struct sockaddr_storage local_sockaddr_from_fd(evutil_socket_t fd) {
struct sockaddr_storage addr = {};
socklen_t addrlen = sizeof(addr);
if (getsockname(fd, (struct sockaddr *) &addr, &addrlen) != 0) {
addr = (struct sockaddr_storage){};
}
return addr;
}
struct sockaddr_storage remote_sockaddr_from_fd(evutil_socket_t fd) {
struct sockaddr_storage addr = {};
socklen_t addrlen = sizeof(addr);
if (getpeername(fd, (struct sockaddr *) &addr, &addrlen) != 0) {
addr = (struct sockaddr_storage){};
}
return addr;
}
uint32_t str_hash32(const char *str, size_t length) {
uint32_t hash = 5381;
for (size_t i = 0; i < length; ++i) {
hash = (hash * 33) ^ (uint32_t) str[i];
}
return hash;
}
std::string sockaddr_ip_to_str(const struct sockaddr *addr) {
char buf[SOCKADDR_STR_BUF_SIZE];
sockaddr_ip_to_str(addr, buf, sizeof(buf));
return buf;
}
std::string sockaddr_to_str(const struct sockaddr *addr) {
char buf[SOCKADDR_STR_BUF_SIZE];
sockaddr_to_str(addr, buf, sizeof(buf));
return buf;
}
std::string str_format(const char *fmt, ...) {
int r;
std::string s;
va_list va;
va_start(va, fmt);
r = vsnprintf(NULL, 0, fmt, va);
va_end(va);
if (r < 0) {
return {};
}
s.resize(r + 1);
va_start(va, fmt);
r = vsnprintf(&s[0], s.capacity(), fmt, va);
va_end(va);
if (r < 0) {
return {};
}
s.resize(r);
return s;
}
std::string encode_to_hex(U8View data) {
const static char table[] = "0123456789abcdef";
const uint8_t *end = data.data() + data.length();
size_t out_len = data.length() * 2;
char out[out_len];
const uint8_t *in_pos = data.data();
char *out_pos = out;
while (in_pos < end) {
*out_pos++ = table[(*in_pos >> 4) & 0xf];
*out_pos++ = table[*in_pos & 0xf];
in_pos++;
}
return std::string(out, out_len);
}
char *safe_strdup(const char *s) {
return (s != nullptr) ? strdup(s) : nullptr;
}
VpnError make_vpn_error_from_fd(evutil_socket_t fd) {
return make_vpn_from_socket_error(evutil_socket_geterror(fd));
}
VpnError make_vpn_from_socket_error(int code) {
return {code, evutil_socket_error_to_string(code)};
}
std::string escape_non_print(U8View data) {
std::string s;
s.reserve(data.size());
for (uint8_t b : data) {
if (std::isprint((unsigned char) b)) {
s.push_back((char) b);
} else {
s.push_back('?');
}
}
return s;
}
int64_t get_time_monotonic_nanos() {
static constexpr int64_t NSEC_PER_SEC = 1'000'000'000;
#ifdef _WIN32
LARGE_INTEGER count; // ticks
LARGE_INTEGER freq; // ticks * second ^ -1
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&count);
count.QuadPart *= NSEC_PER_SEC;
count.QuadPart /= freq.QuadPart;
return count.QuadPart;
#else // _WIN32
timespec ts{};
#ifdef __linux__
clock_gettime(CLOCK_BOOTTIME, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
return ts.tv_sec * NSEC_PER_SEC + ts.tv_nsec;
#endif // _WIN32
}
bool case_equals(std::string_view a, std::string_view b) {
return a.size() == b.size() && 0 == evutil_ascii_strncasecmp(a.data(), b.data(), b.size());
}
static const char *marshal_str(const std::string &str) {
return str.empty() ? nullptr : strdup(str.c_str());
}
static VpnBuffer marshal_buffer(U8View v) {
VpnBuffer c_buffer;
c_buffer.size = v.size();
c_buffer.data = (uint8_t *) std::malloc(c_buffer.size);
std::memcpy((void *) c_buffer.data, v.data(), c_buffer.size);
return c_buffer;
}
VpnDnsStamp *vpn_dns_stamp_from_str(const char *stamp_str, const char **error) {
auto [stamp, stamp_err] = ag::ServerStamp::from_string(stamp_str);
if (stamp_err) {
*error = marshal_str(*stamp_err);
return nullptr;
}
auto *c_result = (VpnDnsStamp *) std::calloc(1, sizeof(VpnDnsStamp));
c_result->proto = (VpnDnsStampProtocol) stamp.proto;
c_result->path = marshal_str(stamp.path);
c_result->server_addr = marshal_str(stamp.server_addr_str);
c_result->provider_name = marshal_str(stamp.provider_name);
if (const auto &key = stamp.server_pk; !key.empty()) {
c_result->server_public_key = marshal_buffer({key.data(), key.size()});
}
if (const auto &hashes = stamp.hashes; !hashes.empty()) {
c_result->hashes = {(VpnBuffer *) std::malloc(hashes.size() * sizeof(VpnBuffer)), (uint32_t) hashes.size()};
for (size_t i = 0; i < hashes.size(); ++i) {
const auto &h = hashes[i];
c_result->hashes.data[i] = marshal_buffer({h.data(), h.size()});
}
}
c_result->properties = (VpnDnsStampInformalProperties) stamp.props;
return c_result;
}
void vpn_dns_stamp_free(VpnDnsStamp *stamp) {
if (!stamp) {
return;
}
std::free((void *) stamp->path);
std::free((void *) stamp->server_addr);
std::free((void *) stamp->provider_name);
std::free(stamp->server_public_key.data);
for (uint32_t i = 0; i < stamp->hashes.size; ++i) {
std::free(stamp->hashes.data[i].data);
}
std::free((void *) stamp->hashes.data);
std::free(stamp);
}
static ag::ServerStamp marshal_stamp(const VpnDnsStamp *c_stamp) {
ag::ServerStamp stamp{};
stamp.proto = (ag::StampProtoType) c_stamp->proto;
if (c_stamp->path) {
stamp.path = c_stamp->path;
}
if (c_stamp->server_addr) {
stamp.server_addr_str = c_stamp->server_addr;
}
if (c_stamp->provider_name) {
stamp.provider_name = c_stamp->provider_name;
}
stamp.server_pk.assign(c_stamp->server_public_key.data,
c_stamp->server_public_key.data + c_stamp->server_public_key.size);
for (size_t i = 0; i < c_stamp->hashes.size; ++i) {
const VpnBuffer &hash = c_stamp->hashes.data[i];
stamp.hashes.emplace_back(hash.data, hash.data + hash.size);
}
stamp.props = (ag::ServerInformalProperties) c_stamp->properties;
return stamp;
}
const char *vpn_dns_stamp_to_str(VpnDnsStamp *c_stamp) {
ag::ServerStamp stamp = marshal_stamp(c_stamp);
return marshal_str(stamp.str());
}
const char *vpn_dns_stamp_pretty_url(VpnDnsStamp *c_stamp) {
ag::ServerStamp stamp = marshal_stamp(c_stamp);
return marshal_str(stamp.pretty_url(false));
}
const char *vpn_dns_stamp_prettier_url(VpnDnsStamp *c_stamp) {
ag::ServerStamp stamp = marshal_stamp(c_stamp);
return marshal_str(stamp.pretty_url(true));
}
void vpn_string_free(const char *s) {
std::free((void *) s);
}
} // namespace ag
+62
View File
@@ -0,0 +1,62 @@
#include <gtest/gtest.h>
#include "vpn/utils.h"
using namespace ag;
TEST(VpnDnsStampTest, DnsStampManipulationWorks) {
const char *error = nullptr;
VpnDnsStamp *stamp = vpn_dns_stamp_from_str("asdfasdfasdfsdf", &error);
ASSERT_FALSE(stamp);
ASSERT_TRUE(error);
vpn_string_free(error);
error = nullptr;
const char *doh_str = "sdns://AgMAAAAAAAAADDk0LjE0MC4xNC4xNITK_rq-BN6tvu8PZG5zLmFkZ3VhcmQuY29tCi9kbnMtcXVlcnk";
stamp = vpn_dns_stamp_from_str(doh_str, &error);
ASSERT_TRUE(stamp);
ASSERT_FALSE(error);
ASSERT_STREQ(stamp->provider_name, "dns.adguard.com");
ASSERT_STREQ(stamp->path, "/dns-query");
ASSERT_TRUE(stamp->properties & VDSIP_DNSSEC);
ASSERT_TRUE(stamp->properties & VDSIP_NO_LOG);
ASSERT_FALSE(stamp->properties & VDSIP_NO_FILTER);
ASSERT_EQ(stamp->hashes.size, 2);
using StrPtr = DeclPtr<const char, &vpn_string_free>;
ASSERT_STREQ(StrPtr(vpn_dns_stamp_pretty_url(stamp)).get(), "https://dns.adguard.com/dns-query");
ASSERT_STREQ(StrPtr(vpn_dns_stamp_prettier_url(stamp)).get(), "https://dns.adguard.com/dns-query");
ASSERT_STREQ(StrPtr(vpn_dns_stamp_to_str(stamp)).get(), doh_str);
VpnDnsStamp orig_stamp = *stamp;
static uint8_t BYTES[] = "\xca\xfe\xba\xbe\xde\xad\xbe\xef";
VpnBuffer hash = {.data = BYTES, .size = 4};
stamp->proto = VDSP_DOQ;
stamp->hashes.data = &hash;
stamp->hashes.size = 1;
stamp->properties = VDSIP_NO_FILTER;
stamp->path = NULL;
ASSERT_STREQ(StrPtr(vpn_dns_stamp_pretty_url(stamp)).get(), "quic://dns.adguard.com");
ASSERT_STREQ(StrPtr(vpn_dns_stamp_prettier_url(stamp)).get(), "quic://dns.adguard.com");
ASSERT_STREQ(StrPtr(vpn_dns_stamp_to_str(stamp)).get(),
"sdns://BAQAAAAAAAAADDk0LjE0MC4xNC4xNATK_rq-D2Rucy5hZGd1YXJkLmNvbQ");
stamp->proto = VDSP_DNSCRYPT;
stamp->hashes.size = 0;
stamp->provider_name = "2.dnscrypt-cert.adguard";
stamp->server_public_key.data = BYTES;
stamp->server_public_key.size = 8;
ASSERT_STREQ(StrPtr(vpn_dns_stamp_pretty_url(stamp)).get(),
"sdns://AQQAAAAAAAAADDk0LjE0MC4xNC4xNAjK_rq-3q2-7xcyLmRuc2NyeXB0LWNlcnQuYWRndWFyZA");
ASSERT_STREQ(StrPtr(vpn_dns_stamp_prettier_url(stamp)).get(), "dnscrypt://2.dnscrypt-cert.adguard");
ASSERT_STREQ(StrPtr(vpn_dns_stamp_to_str(stamp)).get(),
"sdns://AQQAAAAAAAAADDk0LjE0MC4xNC4xNAjK_rq-3q2-7xcyLmRuc2NyeXB0LWNlcnQuYWRndWFyZA");
*stamp = orig_stamp;
vpn_dns_stamp_free(stamp);
}
+186
View File
@@ -0,0 +1,186 @@
#include <condition_variable>
#include <mutex>
#include <thread>
#include <gtest/gtest.h>
#include "common/logger.h"
#include "vpn/event_loop.h"
#include "vpn/utils.h"
using namespace ag;
struct TestData {
std::condition_variable action_cond_var;
std::condition_variable finalize_cond_var;
std::mutex guard;
TaskId id = -1;
bool executed = false;
bool finalized = false;
};
class EventLoopTest : public testing::Test {
public:
EventLoopTest() {
ag::Logger::set_log_level(ag::LOG_LEVEL_TRACE);
}
protected:
DeclPtr<VpnEventLoop, &vpn_event_loop_destroy> m_ev_loop{vpn_event_loop_create()};
std::thread m_loop_thread;
void run_event_loop() {
m_loop_thread = std::thread([loop = m_ev_loop.get()]() {
vpn_event_loop_run(loop);
});
}
VpnEventLoopTask make_task(TestData &data) {
return {&data,
[](void *arg, TaskId task_id) {
auto *data = (TestData *) arg;
if (task_id == data->id) {
std::unique_lock l(data->guard);
data->executed = true;
data->action_cond_var.notify_one();
}
},
[](void *arg) {
auto *data = (TestData *) arg;
std::unique_lock l(data->guard);
data->finalized = true;
data->finalize_cond_var.notify_one();
}};
}
void SetUp() override {
}
void TearDown() override {
vpn_event_loop_stop(m_ev_loop.get());
if (m_loop_thread.joinable()) {
m_loop_thread.join();
}
}
};
TEST_F(EventLoopTest, Submit) {
TestData tasks[5];
for (auto &task : tasks) {
task.id = vpn_event_loop_submit(m_ev_loop.get(), make_task(task));
}
run_event_loop();
for (size_t i = 0; i < std::size(tasks); ++i) {
std::unique_lock l(tasks[i].guard);
ASSERT_TRUE(tasks[i].action_cond_var.wait_for(l, std::chrono::seconds(3),
[t = &tasks[i]]() {
return t->executed;
}))
<< "i=" << i << "task_id=" << tasks[i].id;
ASSERT_TRUE(tasks[i].finalize_cond_var.wait_for(l, std::chrono::seconds(3),
[t = &tasks[i]]() {
return t->finalized;
}))
<< "i=" << i << "task_id=" << tasks[i].id;
}
}
TEST_F(EventLoopTest, Schedule) {
const std::chrono::milliseconds POSTPONE{1000};
TestData tasks[5];
for (auto &task : tasks) {
task.id = vpn_event_loop_schedule(m_ev_loop.get(), make_task(task), POSTPONE.count());
}
run_event_loop();
for (size_t i = 0; i < std::size(tasks); ++i) {
std::unique_lock l(tasks[i].guard);
ASSERT_TRUE(tasks[i].action_cond_var.wait_for(l, POSTPONE * 1.3,
[t = &tasks[i]]() {
return t->executed;
}))
<< "i=" << i << "task_id=" << tasks[i].id;
ASSERT_TRUE(tasks[i].finalize_cond_var.wait_for(l, POSTPONE * 1.3,
[t = &tasks[i]]() {
return t->finalized;
}))
<< "i=" << i << "task_id=" << tasks[i].id;
}
}
TEST_F(EventLoopTest, CancelSubmitted) {
TestData task = {};
task.id = vpn_event_loop_submit(m_ev_loop.get(), make_task(task));
vpn_event_loop_cancel(m_ev_loop.get(), task.id);
std::unique_lock l(task.guard);
ASSERT_FALSE(task.action_cond_var.wait_for(l, std::chrono::seconds(2), [t = &task]() {
return t->executed;
}));
ASSERT_TRUE(task.finalize_cond_var.wait_for(l, std::chrono::seconds(5), [t = &task]() {
return t->finalized;
}));
}
TEST_F(EventLoopTest, CancelScheduled) {
const std::chrono::milliseconds POSTPONE{1000};
TestData postponed_task = {};
postponed_task.id = vpn_event_loop_schedule(m_ev_loop.get(), make_task(postponed_task), POSTPONE.count());
struct CancellingTaskCtx {
VpnEventLoop *loop;
TaskId id;
};
CancellingTaskCtx ctx = {m_ev_loop.get(), postponed_task.id};
vpn_event_loop_submit(m_ev_loop.get(), {&ctx, [](void *arg, TaskId) {
auto *ctx = (CancellingTaskCtx *) arg;
vpn_event_loop_cancel(ctx->loop, ctx->id);
}});
run_event_loop();
std::unique_lock l(postponed_task.guard);
ASSERT_FALSE(postponed_task.action_cond_var.wait_for(l, POSTPONE * 1.3, [t = &postponed_task]() {
return t->executed;
}));
ASSERT_TRUE(postponed_task.finalize_cond_var.wait_for(l, POSTPONE * 1.3, [t = &postponed_task]() {
return t->finalized;
}));
}
TEST_F(EventLoopTest, CancelByStop) {
const std::chrono::milliseconds POSTPONE{1000};
TestData tasks[6];
for (size_t i = 0; i < std::size(tasks); ++i) {
if (i % 2 == 0) {
tasks[i].id = vpn_event_loop_submit(m_ev_loop.get(), make_task(tasks[i]));
} else {
tasks[i].id = vpn_event_loop_schedule(m_ev_loop.get(), make_task(tasks[i]), POSTPONE.count());
}
}
vpn_event_loop_stop(m_ev_loop.get());
for (size_t i = 0; i < std::size(tasks); ++i) {
std::unique_lock l(tasks[i].guard);
ASSERT_FALSE(tasks[i].action_cond_var.wait_for(l, POSTPONE * 1.3,
[t = &tasks[i]]() {
return t->executed;
}))
<< "i=" << i << "task_id=" << tasks[i].id;
ASSERT_TRUE(tasks[i].finalize_cond_var.wait_for(l, POSTPONE * 1.3,
[t = &tasks[i]]() {
return t->finalized;
}))
<< "i=" << i << "task_id=" << tasks[i].id;
}
}
+33
View File
@@ -0,0 +1,33 @@
#include <gtest/gtest.h>
#include "vpn/fsm.h"
#ifndef NDEBUG
using namespace ag;
static bool dummy_condition(const void *, void *) {
return true;
}
static void dummy_action(void *, void *) {
}
TEST(FSMValidation, TargetStateAnyState) {
FsmTransitionTable transition_table = {
{0, 0, dummy_condition, dummy_action, Fsm::ANY_SOURCE_STATE},
};
ASSERT_FALSE(Fsm::validate_transition_table(transition_table));
}
TEST(FSMValidation, Closed) {
FsmTransitionTable transition_table = {
{0, 0, Fsm::ANYWAY, dummy_action, 0},
{0, 0, dummy_condition, dummy_action, 0},
};
ASSERT_FALSE(Fsm::validate_transition_table(transition_table));
}
#endif // NDEBUG
+13
View File
@@ -0,0 +1,13 @@
include(default)
[settings]
arch=armv7
build_type=RelWithDebInfo
compiler=clang
compiler.libcxx=libc++
compiler.version=11
os=Android
os.api_level=21
[build_requires]
[options]
[env]
CONAN_CMAKE_TOOLCHAIN_FILE={{ os.path.join(profile_dir, "android.toolchain.cmake") }}
+13
View File
@@ -0,0 +1,13 @@
include(default)
[settings]
arch=armv8
build_type=RelWithDebInfo
compiler=clang
compiler.libcxx=libc++
compiler.version=11
os=Android
os.api_level=21
[build_requires]
[options]
[env]
CONAN_CMAKE_TOOLCHAIN_FILE={{ os.path.join(profile_dir, "android.toolchain.cmake") }}
+13
View File
@@ -0,0 +1,13 @@
include(default)
[settings]
arch=x86
build_type=RelWithDebInfo
compiler=clang
compiler.libcxx=libc++
compiler.version=11
os=Android
os.api_level=21
[build_requires]
[options]
[env]
CONAN_CMAKE_TOOLCHAIN_FILE={{ os.path.join(profile_dir, "android.toolchain.cmake") }}
+13
View File
@@ -0,0 +1,13 @@
include(default)
[settings]
arch=x86_64
build_type=RelWithDebInfo
compiler=clang
compiler.libcxx=libc++
compiler.version=11
os=Android
os.api_level=21
[build_requires]
[options]
[env]
CONAN_CMAKE_TOOLCHAIN_FILE={{ os.path.join(profile_dir, "android.toolchain.cmake") }}
+48
View File
@@ -0,0 +1,48 @@
# Get ${CMAKE_PLATFORM_INFO_DIR}/../.. and ../../.. . Need to use get_filename_component because CMAKE_PLATFORM_INFO_DIR
# may not be created yet.
get_filename_component(CMAKE_PLATFORM_INFO_DIR_PARENT "${CMAKE_PLATFORM_INFO_DIR}" DIRECTORY)
get_filename_component(CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT "${CMAKE_PLATFORM_INFO_DIR_PARENT}" DIRECTORY)
get_filename_component(CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT_PARENT "${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT}" DIRECTORY)
# Load conan build info
if(EXISTS "${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT}/conanbuildinfo.cmake")
include("${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT}/conanbuildinfo.cmake")
message("included ${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT}/conanbuildinfo.cmake")
elseif(EXISTS "${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT_PARENT}/conanbuildinfo.cmake")
include("${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT_PARENT}/conanbuildinfo.cmake")
message("included ${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT_PARENT}/conanbuildinfo.cmake")
else()
message(FATAL_ERROR "conanbuildinfo.cmake not found, make sure that conan install finished correctly.")
endif()
message("ARCH: ${CONAN_SETTINGS_ARCH}")
if(CONAN_SETTINGS_ARCH STREQUAL armv7)
set(ANDROID_ABI armeabi-v7a)
set(ANDROID_PLATFORM 19)
elseif(CONAN_SETTINGS_ARCH STREQUAL armv8)
set(ANDROID_ABI arm64-v8a)
set(ANDROID_PLATFORM 21)
elseif(CONAN_SETTINGS_ARCH STREQUAL x86_64)
set(ANDROID_ABI x86_64)
set(ANDROID_PLATFORM 21)
elseif(CONAN_SETTINGS_ARCH STREQUAL x86)
set(ANDROID_ABI x86)
set(ANDROID_PLATFORM 19)
else()
message(FATAL_ERROR "Architecture ${CONAN_SETTINGS_ARCH} is not supported")
endif()
if(EXISTS "$ENV{CMAKE_ORIGINAL_TOOLCHAIN}")
message("Including $ENV{CMAKE_ORIGINAL_TOOLCHAIN}")
include("$ENV{CMAKE_ORIGINAL_TOOLCHAIN}")
else()
message(FATAL_ERROR "Please specify valid toolchain in CMAKE_ANDROID_TOOLCHAIN environment variable")
endif()
# Use conan paths instead of NDK paths for find
set(CMAKE_FIND_ROOT_PATH /)
set(CMAKE_FIND_USE_CMAKE_PATH OFF)
# We don't need to export any symbol implicitly, and also disable C++ exceptions, and enable unwind tables.
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden -fno-exceptions -funwind-tables")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden -fno-exceptions -funwind-tables -fno-rtti")
+8
View File
@@ -0,0 +1,8 @@
include(apple.jinja)
[settings]
os=iOS
os.version=11.2
os.sdk=iphoneos
[env]
CONAN_CMAKE_TOOLCHAIN_FILE={{ os.path.join(profile_dir, "apple.toolchain.cmake") }}
IPHONEOS_DEPLOYMENT_TARGET="11.2"
@@ -0,0 +1,8 @@
include(apple.jinja)
[settings]
os=iOS
os.version=11.2
os.sdk=iphonesimulator
[env]
CONAN_CMAKE_TOOLCHAIN_FILE={{ os.path.join(profile_dir, "apple.toolchain.cmake") }}
IPHONEOS_DEPLOYMENT_TARGET="11.2"
+6
View File
@@ -0,0 +1,6 @@
include(apple.jinja)
[settings]
os.version=10.12
[env]
MACOSX_DEPLOYMENT_TARGET="10.12"
+11
View File
@@ -0,0 +1,11 @@
include(default)
[settings]
build_type=RelWithDebInfo
compiler=apple-clang
compiler.version=12.0
compiler.libcxx=libc++
[env]
CONAN_CMAKE_TOOLCHAIN_FILE={{ os.path.join(profile_dir, "apple.toolchain.cmake") }}
# Without this one cargo uses `cc` from the default toolchain which does not have system includes
# specified, and as a result quiche cannot be built
CC=clang
+75
View File
@@ -0,0 +1,75 @@
# Get ${CMAKE_PLATFORM_INFO_DIR}/../.. and ../../.. . Need to use get_filename_component because CMAKE_PLATFORM_INFO_DIR
# may not be created yet.
get_filename_component(CMAKE_PLATFORM_INFO_DIR_PARENT "${CMAKE_PLATFORM_INFO_DIR}" DIRECTORY)
get_filename_component(CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT "${CMAKE_PLATFORM_INFO_DIR_PARENT}" DIRECTORY)
get_filename_component(CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT_PARENT "${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT}" DIRECTORY)
# Verbose only on first run
if(EXISTS "${CMAKE_PLATFORM_INFO_DIR}")
set(QUIET ON)
endif()
# Load conan build info
if(EXISTS "${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT}/conanbuildinfo.cmake")
include("${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT}/conanbuildinfo.cmake")
if(NOT QUIET)
message("included ${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT}/conanbuildinfo.cmake")
endif(NOT QUIET)
elseif(EXISTS "${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT_PARENT}/conanbuildinfo.cmake")
include("${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT_PARENT}/conanbuildinfo.cmake")
if(NOT QUIET)
message("included ${CMAKE_PLATFORM_INFO_DIR_PARENT_PARENT_PARENT}/conanbuildinfo.cmake")
endif(NOT QUIET)
else()
message(FATAL_ERROR "conanbuildinfo.cmake not found, make sure that conan install finished correctly.")
endif()
if(NOT QUIET)
message("ARCH: ${CONAN_SETTINGS_ARCH}")
endif(NOT QUIET)
# Set OSX sysroot
if(CONAN_SETTINGS_OS STREQUAL "iOS")
if(CONAN_SETTINGS_OS_SDK)
set(CMAKE_OSX_SYSROOT "${CONAN_SETTINGS_OS_SDK}")
if(NOT QUIET)
message("CMAKE_OSX_SYSROOT is ${CMAKE_OSX_SYSROOT}")
endif(NOT QUIET)
else()
message(FATAL_ERROR "Please specify sdk via os.sdk (iphoneos or iphonesimulator) when compiling for iOS")
endif()
endif()
# Set system processor
if(CONAN_SETTINGS_ARCH STREQUAL "armv8")
set(CMAKE_SYSTEM_PROCESSOR arm64)
# CMake bug workaround
set(CMAKE_HOST_SYSTEM_PROCESSOR arm64)
elseif(CONAN_SETTINGS_ARCH STREQUAL "x86_64")
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# CMake bug workaround
set(CMAKE_HOST_SYSTEM_PROCESSOR x86_64)
else()
message(FATAL_ERROR "Architecture ${CONAN_SETTINGS_ARCH} is not supported")
endif()
set(CMAKE_OSX_ARCHITECTURES "${CMAKE_SYSTEM_PROCESSOR}")
set(CMAKE_CROSSCOMPILING TRUE)
foreach(arch ${CMAKE_OSX_ARCHITECTURES})
add_compile_options(-arch ${arch})
link_libraries("-arch ${arch}")
endforeach()
if(NOT QUIET)
message("CMAKE_OSX_ARCHITECTURES is ${CMAKE_OSX_ARCHITECTURES}")
endif(NOT QUIET)
# Set minimum deployment version
if(CMAKE_OSX_SYSROOT STREQUAL iphonesimulator)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.2" CACHE STRING "Minimum iOS deployment version")
elseif(CMAKE_OSX_SYSROOT STREQUAL iphoneos)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.2" CACHE STRING "Minimum iOS deployment version")
add_compile_options(-fembed-bitcode)
else()
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.12" CACHE STRING "Minimum macOS deployment version")
endif()
# We don't need to export any symbol implicitly, and also disable C++ exceptions.
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden -fno-exceptions")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden -fno-exceptions")
+11
View File
@@ -0,0 +1,11 @@
include(default)
[settings]
cppstd=17
compiler=clang
compiler.libcxx=libc++
build_type=RelWithDebInfo
[options]
[build_requires]
[env]
CC=clang
CXX=clang++
+16
View File
@@ -0,0 +1,16 @@
include(default)
[settings]
arch=x86
arch_build=x86
compiler=clang
compiler.version=9
compiler.runtime=MT
build_type=RelWithDebInfo
[options]
[build_requires]
[env]
CONAN_CMAKE_TOOLCHAIN_FILE={{ os.path.join(profile_dir, "windows.toolchain.cmake") }}
CC="C:/Program Files (x86)/LLVM/bin/clang-cl.exe"
CXX="C:/Program Files (x86)/LLVM/bin/clang-cl.exe"
CONAN_MAKE_PROGRAM=ninja
CONAN_CMAKE_GENERATOR=Ninja
+3
View File
@@ -0,0 +1,3 @@
set(CMAKE_C_FLAGS_RELWITHDEBINFO "/O2 /Ob2 /GR /MT /Z7 /EHs-c- -D_HAS_EXCEPTIONS=0 /W0")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "/O2 /Ob2 /GR /MT /Z7 /EHs-c- -D_HAS_EXCEPTIONS=0 /std:c++17 /W0")
set(CMAKE_VERBOSE_MAKEFILE TRUE CACHE BOOL "Verbose Makefile")
+16
View File
@@ -0,0 +1,16 @@
requirements:
- "magic_enum/0.7.3"
- "http_parser/2.9.4"
- "brotli/1.0.9"
- "zlib/1.2.11"
- "libevent/2.1.11@AdguardTeam/NativeLibsCommon"
- "ldns/2021-03-29@AdguardTeam/NativeLibsCommon"
- "klib/2021-04-06@AdguardTeam/NativeLibsCommon"
- "openssl/boring-2021-05-11@AdguardTeam/NativeLibsCommon"
- "quiche/0.10.0@AdguardTeam/NativeLibsCommon"
- "nghttp2/1.44.0@AdguardTeam/NativeLibsCommon"
- "dns-libs/777@AdguardTeam/NativeLibsCommon"
- "native_libs_common/2.0.10@AdguardTeam/NativeLibsCommon"
commit_hash:
"0.90.5":
hash: "8349aca40ecd6df1e8b66c508e8f6e78f3769fb6"
+88
View File
@@ -0,0 +1,88 @@
from conans import ConanFile, CMake
class VpnLibsConan(ConanFile):
name = "vpn-libs"
license = "GPL-3.0-or-later"
author = "AdguardTeam"
url = "https://github.com/AdguardTeam/VpnLibs"
description = "A VPN client library that provides client network traffic tunnelling to an AdGuard VPN server"
settings = "os", "compiler", "build_type", "arch"
options = {
"commit_hash": "ANY",
"sanitize": "ANY"
}
default_options = {
"commit_hash": None, # None means `master`
"sanitize": None, # None means none
}
generators = "cmake"
def requirements(self):
for req in self.conan_data["requirements"]:
self.requires(req)
def build_requirements(self):
self.build_requires("gtest/1.11.0")
self.build_requires("cxxopts/3.0.0")
self.build_requires("nlohmann_json/3.10.5")
def configure(self):
self.options["gtest"].build_gmock = False
self.options["dns-libs"].commit_hash = "1ce2d08acaadaf9cc730f04fc62ddd07bea8e159"
# Resolve conflict between pcre2 required from dns-libs and pcre2 required form native_libs_common
self.options["pcre2"].build_pcre2grep = False
# Commit hash should only be used with native_libs_common/777
# self.options["native_libs_common"].commit_hash = "72731a36771d550ffae8c1223e0a129fefc2384c"
def source(self):
self.run("git clone https://github.com/AdguardTeam/VpnLibs.git source_subfolder")
if self.options.commit_hash:
self.run("cd source_subfolder && git checkout %s" % self.options.commit_hash)
def build(self):
cmake = CMake(self)
cmake.definitions["CMAKE_C_FLAGS"] = ""
cmake.definitions["CMAKE_CXX_FLAGS"] = ""
# A better way to pass these was not found :(
if self.settings.os == "Linux":
if self.settings.compiler.libcxx:
cmake.definitions["CMAKE_CXX_FLAGS"] = "-stdlib=%s" % self.settings.compiler.libcxx
if self.settings.compiler.version:
cmake.definitions["CMAKE_CXX_COMPILER_VERSION"] = self.settings.compiler.version
if self.options.sanitize:
cmake.definitions["CMAKE_C_FLAGS"] += f" -fno-omit-frame-pointer -fsanitize={self.options.sanitize}"
cmake.definitions["CMAKE_CXX_FLAGS"] += f" -fno-omit-frame-pointer -fsanitize={self.options.sanitize}"
cmake.configure(source_folder="source_subfolder")
cmake.build(target="vpnlibs_common")
cmake.build(target="vpnlibs_core")
cmake.build(target="vpnlibs_net")
cmake.build(target="vpnlibs_tcpip")
def package(self):
MODULES = [
"common",
"core",
"net",
"tcpip",
]
for m in MODULES:
self.copy("*.h", dst="include", src="source_subfolder/%s/include" % m)
self.copy("*.lib", dst="lib", keep_path=False)
self.copy("*.dll", dst="bin", keep_path=False)
self.copy("*.so", dst="lib", keep_path=False)
self.copy("*.dylib", dst="lib", keep_path=False)
self.copy("*.a", dst="lib", keep_path=False)
def package_info(self):
self.cpp_info.libs = [
"vpnlibs_core",
"vpnlibs_net",
"vpnlibs_tcpip",
"vpnlibs_common",
]
+147
View File
@@ -0,0 +1,147 @@
cmake_minimum_required(VERSION 3.1)
project(core C CXX)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(VPN_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..)
set(THIRD_PARTY_DIR ${VPN_LIB_DIR}/third-party)
set(VPNCORE_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)
include(../cmake/conan_bootstrap.cmake)
conan_bootstrap(SRCROOT ".." CONANFILE "../conanfile.py" SCOPE_NAME agvpn)
set(SOURCE_FILES
${VPNCORE_SRC_DIR}/dns_proxy_accessor.cpp
${VPNCORE_SRC_DIR}/vpn_client.cpp
${VPNCORE_SRC_DIR}/vpn_manager.cpp
${VPNCORE_SRC_DIR}/vpn_fsm.cpp
${VPNCORE_SRC_DIR}/tunnel.cpp
${VPNCORE_SRC_DIR}/socks_listener.cpp
${VPNCORE_SRC_DIR}/tun_device_listener.cpp
${VPNCORE_SRC_DIR}/http2_upstream.cpp
${VPNCORE_SRC_DIR}/http3_upstream.cpp
${VPNCORE_SRC_DIR}/upstream_multiplexer.cpp
${VPNCORE_SRC_DIR}/http_udp_multiplexer.cpp
${VPNCORE_SRC_DIR}/http_icmp_multiplexer.cpp
${VPNCORE_SRC_DIR}/direct_upstream.cpp
${VPNCORE_SRC_DIR}/fake_upstream.cpp
${VPNCORE_SRC_DIR}/utils.cpp
${VPNCORE_SRC_DIR}/domain_filter.cpp
${VPNCORE_SRC_DIR}/domain_lookuper.cpp
${VPNCORE_SRC_DIR}/memory_buffer.cpp
${VPNCORE_SRC_DIR}/memfile_buffer.cpp
${VPNCORE_SRC_DIR}/single_upstream_connector.cpp
${VPNCORE_SRC_DIR}/fallbackable_upstream_connector.cpp
${VPNCORE_SRC_DIR}/icmp_manager.cpp
${VPNCORE_SRC_DIR}/dns_sniffer.cpp
${VPNCORE_SRC_DIR}/vpn_dns_resolver.cpp
${VPNCORE_SRC_DIR}/vpn_connection.cpp
)
if(NOT TARGET vpnlibs_common)
add_subdirectory(${VPN_LIB_DIR}/common ${CMAKE_BINARY_DIR}/common)
endif(NOT TARGET vpnlibs_common)
if(NOT TARGET vpnlibs_net)
add_subdirectory(${VPN_LIB_DIR}/net ${CMAKE_BINARY_DIR}/net)
endif(NOT TARGET vpnlibs_net)
if(NOT TARGET fflibs_static)
add_subdirectory(${THIRD_PARTY_DIR}/fflibs ${CMAKE_BINARY_DIR}/third-party/fflibs_static)
endif(NOT TARGET fflibs_static)
add_library(vpnlibs_core STATIC EXCLUDE_FROM_ALL ${SOURCE_FILES})
set_target_properties(vpnlibs_core PROPERTIES POSITION_INDEPENDENT_CODE ON)
if (NOT MSVC)
target_compile_options(vpnlibs_core PRIVATE -W -Wall -Wextra -Werror)
target_compile_options(vpnlibs_core PRIVATE
-Wno-unknown-warning-option -Wno-macro-redefined -Wno-unused-parameter
-Wno-missing-field-initializers -Wno-c99-designator)
else()
target_compile_definitions(vpnlibs_core PRIVATE _UNICODE UNICODE)
endif()
target_include_directories(vpnlibs_core PUBLIC include)
target_link_libraries(vpnlibs_core vpnlibs_common vpnlibs_net)
target_link_libraries(vpnlibs_core CONAN_PKG::klib CONAN_PKG::magic_enum CONAN_PKG::libevent)
target_link_libraries(vpnlibs_core CONAN_PKG::dns-libs)
target_link_libraries(vpnlibs_core fflibs_static CONAN_PKG::quiche)
if (WIN32)
target_link_libraries(vpnlibs_core BCrypt) # Quiche requirement
endif()
if(NOT TARGET vpnlibs_tcpip)
add_subdirectory(${VPN_LIB_DIR}/tcpip ${CMAKE_BINARY_DIR}/tcpip)
endif(NOT TARGET vpnlibs_tcpip)
target_link_libraries(vpnlibs_core vpnlibs_tcpip)
set(TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/test)
add_executable(test_standalone_client EXCLUDE_FROM_ALL ${TEST_DIR}/test_standalone_client.cpp)
target_link_libraries(test_standalone_client vpnlibs_core)
add_executable(standalone_client EXCLUDE_FROM_ALL ${TEST_DIR}/standalone_client/standalone_client.cpp)
target_link_libraries(standalone_client vpnlibs_core CONAN_PKG::cxxopts)
target_link_libraries(standalone_client CONAN_PKG::native_libs_common)
target_link_libraries(standalone_client CONAN_PKG::nlohmann_json)
target_link_libraries(standalone_client CONAN_PKG::magic_enum)
enable_testing()
link_libraries(CONAN_PKG::gtest)
add_library(module_vpncore_mocked STATIC EXCLUDE_FROM_ALL
${SOURCE_FILES}
${TEST_DIR}/test_mock_c.cpp
${TEST_DIR}/test_mock_vpn_client.cpp
)
if (MSVC)
target_compile_definitions(module_vpncore_mocked PRIVATE _UNICODE UNICODE)
endif (MSVC)
set(MOCKED_FUNCTIONS
locations_pinger_start
)
foreach(FN ${MOCKED_FUNCTIONS})
target_compile_definitions(module_vpncore_mocked PRIVATE ${FN}=MOCK_${FN})
endforeach(FN ${MOCKED_FUNCTIONS})
include(${VPN_LIB_DIR}/cmake/add_unit_test.cmake)
set(TEST_EXTRA_INCLUDES ${VPNCORE_SRC_DIR} ${THIRD_PARTY_DIR}/klib)
# do not link `VpnClient` tests against the mocked library as it mocks `VpnClient` also
add_unit_test(test_vpn_client_fsm "${TEST_DIR}" "" TRUE TRUE)
target_link_libraries(test_vpn_client_fsm PRIVATE vpnlibs_core)
target_include_directories(test_vpn_client_fsm PRIVATE ${TEST_EXTRA_INCLUDES})
target_link_libraries(module_vpncore_mocked vpnlibs_core)
target_include_directories(module_vpncore_mocked PUBLIC ${TEST_EXTRA_INCLUDES})
link_libraries(module_vpncore_mocked)
add_unit_test(test_domain_filter "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE FALSE)
add_unit_test(test_utils "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE FALSE)
add_unit_test(test_domain_lookuper "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE FALSE)
# for some tls messages dumps
set_source_files_properties(${TEST_DIR}/test_domain_lookuper.cpp PROPERTY COMPILE_FLAGS "${CMAKE_C_FLAGS} -Wno-narrowing")
add_unit_test(test_tunnel "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
add_unit_test(test_memory_buffer "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
add_unit_test(test_memfile_buffer "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
add_unit_test(test_upstream_multiplexer "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
add_unit_test(test_single_upstream_connector "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
add_unit_test(test_fallbackable_upstream_connector "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
add_unit_test(test_vpn_dns_resolver "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
if(NOT WIN32)
add_unit_test(test_vpn_manager_fsm ${TEST_DIR} "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
add_unit_test(test_vpn_manager_fsm_recovery ${TEST_DIR} "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
add_dependencies(tests test_standalone_client standalone_client)
endif()
configure_file(test/standalone_client/standalone_client.conf ${CMAKE_CURRENT_BINARY_DIR}/standalone_client.conf COPYONLY)
+188
View File
@@ -0,0 +1,188 @@
#pragma once
#include <cassert>
#include <cstdint>
#include "vpn/internal/utils.h"
#include "vpn/platform.h"
#include "vpn/utils.h"
#include "vpn/vpn.h"
namespace ag {
class VpnClient;
enum ClientEvent {
CLIENT_EVENT_CONNECT_REQUEST, /**< Called when new incoming connection is appeared (raised with
`ClientConnectRequest`) */
CLIENT_EVENT_CONNECTION_ACCEPTED, /**< Called when passed connection is accepted (raised with connection id) */
CLIENT_EVENT_CONNECTION_CLOSED, /**< Called when connection is closed by client (raised with connection id) */
CLIENT_EVENT_READ, /**< Called when some data needs to be sent via connection (raised with `ClientRead`) */
CLIENT_EVENT_DATA_SENT, /**< Called when some data was sent to client (raised with `ClientDataSentEvent`) */
CLIENT_EVENT_OUTPUT, /**< Called when some data from server is ready to be sent to client application (raised with
`ClientOutputEvent`) */
CLIENT_EVENT_ICMP_ECHO_REQUEST, /**< Called when ICMP echo request is received (raised with
`icmp_echo_request_event_t`) */
};
enum ClientConnectResult {
CCR_PASS, // connected successfully
CCR_DROP, // connection to destination peer timed out
CCR_REJECT, // destination peer rejected connection
CCR_UNREACH, // destination peer is unreachable
};
struct ClientConnectRequest {
uint64_t id; /**< connection identifier */
int protocol; /**< protocol */
const sockaddr *src; /**< source address */
const TunnelAddress *dst; /**< destination address */
std::string_view app_name; /**< name of application that initiated this request */
};
struct ClientRead {
uint64_t id; /**< connection identifier */
const uint8_t *data; /**< data buffer */
size_t length; /**< data length */
/**
* Operation result. Filled by caller.
* Negative value means error.
* For a TCP connection may be less than `length`, in that case the listener should slide
* buffer and try to raise the rest data if read is still enabled, or, if read is disabled,
* try it after the other side enables read.
* For a UDP connection may be equal to `length` in case packet was sent successfully, or
* 0 in case the other side can't send anymore at the moment. In the latter case, the listener
* should retry later.
*/
int result;
};
struct ClientDataSentEvent {
uint64_t id; /**< connection identifier */
size_t length; /**< sent bytes number (if 0, then connection polls for send resuming) */
};
struct ClientOutputEvent {
int family; // ip family
size_t iovlen; // message vector size
const struct iovec *iov; // message vector
};
struct ClientHandler {
/**
* Event handling function
* @param arg user argument
* @param what see `ClientEvent`
* @param data event data (see `ClientEvent`)
*/
void (*func)(void *arg, ClientEvent what, void *data) = nullptr;
/** User argument */
void *arg = nullptr;
};
/**
* Client communication interface which encapsulates client-side connections management
*/
class ClientListener {
public:
enum class InitResult {
SUCCESS,
ADDR_IN_USE,
FAILURE,
};
VpnClient *vpn = nullptr;
ClientHandler handler = {};
ClientListener() = default;
virtual ~ClientListener() = default;
ClientListener(const ClientListener &) = delete;
ClientListener &operator=(const ClientListener &) = delete;
ClientListener(ClientListener &&) noexcept = delete;
ClientListener &operator=(ClientListener &&) noexcept = delete;
/**
* Initialize client listener (MUST be called if overridden)
* If initialization is unsuccessful, the object is left in the same state it was in before calling init()
* @param vpn vpn instance
* @param handler client events handler
* @return true if initialized successfully, false otherwise
*/
virtual InitResult init(VpnClient *vpn, ClientHandler handler) {
this->vpn = vpn;
this->handler = handler;
return InitResult::SUCCESS;
}
/**
* Deinitialize client listener
* The object is returned to the state it was in before calling init()
*/
virtual void deinit() = 0;
/**
* Complete request for connection raised with `CLIENT_EVENT_CONNECT_REQUEST`
* @param id connection id
* @param result connect result
*/
virtual void complete_connect_request(uint64_t id, ClientConnectResult result) = 0;
/**
* Close connection
* @param id connection id
* @param graceful true if connection should be closed in a graceful way
* @param async true if connection should be closed with a context switch
*/
virtual void close_connection(uint64_t id, bool graceful, bool async) = 0;
/**
* Send data through connection
* @param id connection id
* @param data data to send
* @param length data length
* @return number of consumed bytes (< 0 in case of error)
*/
virtual ssize_t send(uint64_t id, const uint8_t *data, size_t length) = 0;
/**
* Notify client of server sent some data
* @param id connection id
* @param n number of sent bytes
*/
virtual void consume(uint64_t id, size_t n) = 0;
/**
* Get flow control info for connection
* @param id connection id
*/
virtual TcpFlowCtrlInfo flow_control_info(uint64_t id) = 0;
/**
* Enable/disable read operations
* @param id connection id
* @param on true -> enable / false -> disable
*/
virtual void turn_read(uint64_t id, bool on) = 0;
/**
* Pass data packets received from a client application to the client listener in case it
* doesn't listen for incoming data by itself
* @param packets packets
* @return 0 on success, non-zero value otherwise
*/
virtual int process_client_packets(VpnPackets packets) {
(void) packets;
assert(0);
return -1;
}
/**
* Process an ICMP reply
*/
virtual void process_icmp_reply(const IcmpEchoReply &reply) {
}
};
} // namespace ag
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <cstdlib>
#include <optional>
#include <string>
#include "vpn/internal/utils.h"
namespace ag {
struct BufferPeekResult {
std::optional<std::string> err; // nullopt if successful, some error description otherwise
U8View data; // peeked data chunk
};
class DataBuffer {
public:
DataBuffer() = default;
virtual ~DataBuffer() = default;
/**
* Initialize buffer
* @return nullopt if successful, some error description otherwise
*/
virtual std::optional<std::string> init() = 0;
/**
* Get current buffer size
*/
virtual size_t size() const = 0;
/**
* Push data chunk in buffer
* @return nullopt if successful, some error description otherwise
*/
virtual std::optional<std::string> push(U8View data) = 0;
virtual std::optional<std::string> push(std::vector<uint8_t> data) = 0;
/**
* Peek data chunk from buffer (may be less than the buffer size).
* The next call of `peek` returns the same chunk, call to `drain` to advance the buffer.
*/
virtual BufferPeekResult peek() = 0;
/**
* Remove data from buffer
* @param length data length to remove
*/
virtual void drain(size_t length) = 0;
};
} // namespace ag
@@ -0,0 +1,65 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <variant>
#include <vector>
#include "vpn/event_loop.h"
#include "vpn/internal/utils.h"
namespace ag {
class DnsProxy;
void delete_dnsproxy(DnsProxy *p);
class DnsProxyAccessor {
public:
struct Parameters {
/// The DNS resolver URL (see `upstream_options::address` in the DNS libs for the syntax details)
std::string resolver_address;
/// The address which the outbound proxy for the DNS proxy is listening on
sockaddr_storage socks_listener_address = {};
/// Certificate verification handler
CertVerifyHandler cert_verify_handler = {};
/// Whether IPv6 is available
bool ipv6_available = true;
};
explicit DnsProxyAccessor(Parameters p);
~DnsProxyAccessor() = default;
DnsProxyAccessor(const DnsProxyAccessor &) = delete;
DnsProxyAccessor &operator=(const DnsProxyAccessor &) = delete;
DnsProxyAccessor(DnsProxyAccessor &&) = delete;
DnsProxyAccessor &operator=(DnsProxyAccessor &&) = delete;
/**
* Start the DNS proxy
* @param timeout queries expiration time
*/
bool start(std::chrono::milliseconds timeout);
/**
* Stop the DNS proxy
*/
void stop();
/**
* Get a listener address by the given protocol
*/
[[nodiscard]] const sockaddr_storage &get_listen_address(int proto) const;
private:
DeclPtr<ag::DnsProxy, &ag::delete_dnsproxy> m_dns_proxy;
Parameters m_parameters = {};
sockaddr_storage m_dns_proxy_udp_listen_address = {};
sockaddr_storage m_dns_proxy_tcp_listen_address = {};
ag::Logger m_log{"DNS_PROXY_ACCESSOR"};
};
} // namespace ag
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "common/logger.h"
#include "vpn/internal/domain_filter.h"
#include "vpn/utils.h"
namespace ag {
struct DnsSnifferParameters {
/// Needed to store IP addresses which potentially target the excluded hosts
DomainFilter *filter = nullptr;
};
class DnsSniffer {
public:
DnsSniffer() = default;
~DnsSniffer() = default;
DnsSniffer(const DnsSniffer &) = delete;
DnsSniffer &operator=(const DnsSniffer &) = delete;
DnsSniffer(DnsSniffer &&) = delete;
DnsSniffer &operator=(DnsSniffer &&) = delete;
void init(const DnsSnifferParameters &parameters);
/**
* Process an intercepted DNS reply.
* @param data UDP payload.
* @param library_request Must be `true` if the request was made by this library.
*/
void on_intercepted_dns_reply(U8View data, bool library_request);
private:
DnsSnifferParameters m_parameters = {};
ag::Logger m_log{"DNS_SNIFFER"};
};
} // namespace ag
+114
View File
@@ -0,0 +1,114 @@
#pragma once
#include <bitset>
#include <chrono>
#include <string>
#include <string_view>
#include <unordered_set>
#include <variant>
#include <vector>
#include "common/cache.h"
#include "common/logger.h"
#include "vpn/internal/utils.h"
#include "vpn/vpn.h"
namespace ag {
enum DomainFilterValidationStatus {
DFVS_OK,
DFVS_MALFORMED,
};
enum DomainFilterMatchStatus {
/// Connection is not matched
DFMS_DEFAULT,
/// Connection targets the excluded host
DFMS_EXCLUSION,
/// Connection is not matched, but it potentially targets the excluded host
DFMS_SUSPECT_EXCLUSION,
};
class DomainFilter {
public:
static constexpr size_t DEFAULT_CACHE_SIZE = 512;
static constexpr std::chrono::seconds DEFAULT_TAG_TTL{60};
DomainFilter();
~DomainFilter();
DomainFilter(const DomainFilter &) = delete;
DomainFilter &operator=(const DomainFilter &) = delete;
DomainFilter(DomainFilter &&) = delete;
DomainFilter &operator=(DomainFilter &&) = delete;
static DomainFilterValidationStatus validate_entry(const std::string &entry);
/**
* Update current filtering settings
* @param vpn VPN client
* @param mode the VPN mode
* @return true if updated successfully, false otherwise
*/
bool update_exclusions(VpnMode mode, std::string_view exclusions);
/**
* Match domain name against exclusion list. Logic of matching is explained in
* `VpnSettings.exclusions` description.
* @note Always returns some value (not `DFMS_NONE`) as we are sure what to do with the
* connection with known domain name.
* @param domain domain name
*/
[[nodiscard]] DomainFilterMatchStatus match_domain(std::string_view domain) const;
/**
* Match address tag against exclusion list. Logic of matching is explained in
* `VpnSettings.exclusions` description.
* @param tag address tag
*/
[[nodiscard]] DomainFilterMatchStatus match_tag(const SockAddrTag &tag) const;
/**
* Add resolved tag in cache
* @param tag address tag
* @param domain domain name
*/
void add_resolved_tag(SockAddrTag tag, std::string domain);
/**
* Add an IP address which is suspected to belong to an exclusion
* @param addr the IP address
* @param ttl record TTL
*/
void add_exclusion_suspect(const sockaddr_storage &addr, std::chrono::seconds ttl);
/**
* Get list of the DNS-resolvable exclusions
*/
[[nodiscard]] std::vector<std::string_view> get_resolvable_exclusions() const;
/**
* Get the current VPN mode
*/
[[nodiscard]] VpnMode get_mode() const;
private:
enum MatchFlags : uint32_t;
using MatchFlagsSet = std::bitset<width_of<MatchFlags>()>;
struct DomainEntryInfo {
std::string text;
MatchFlagsSet flags;
};
using ParseResult = std::variant<sockaddr_storage, DomainEntryInfo, DomainFilterValidationStatus>;
VpnMode m_mode = VPN_MODE_GENERAL;
std::unordered_map<std::string, MatchFlagsSet> m_domains; // key - domain name / value - set of `MatchFlags`
std::unordered_set<sockaddr_storage> m_addresses;
ag::LruTimeoutCache<ag::SockAddrTag, std::string> m_resolved_tags{DEFAULT_CACHE_SIZE, DEFAULT_TAG_TTL};
ag::LruTimeoutCache<sockaddr_storage, uint8_t> m_exclusion_suspects{DEFAULT_CACHE_SIZE, DEFAULT_TAG_TTL};
ag::Logger m_log{"DOMAIN_FILTER"};
static ParseResult parse_entry(const std::string &entry);
};
} // namespace ag
@@ -0,0 +1,47 @@
#pragma once
#include <memory>
#include <string>
#include "common/logger.h"
#include "vpn/utils.h"
namespace ag {
enum DomainLookuperStatus {
DLUS_NOTFOUND, // gave up looking for a domain name
DLUS_FOUND, // found a domain name
DLUS_PASS, // pass current packet, check the next one
};
struct DomainLookuperResult {
DomainLookuperStatus status = DLUS_NOTFOUND;
std::string domain;
};
enum DomainLookuperPacketDirection {
DLUPD_OUTGOING, // from client to server
DLUPD_INCOMING, // from server to client
};
class DomainLookuper {
public:
DomainLookuper();
~DomainLookuper();
DomainLookuper(const DomainLookuper &) = delete;
DomainLookuper &operator=(const DomainLookuper &) = delete;
DomainLookuper(DomainLookuper &&) noexcept = delete;
DomainLookuper &operator=(DomainLookuper &&) noexcept = delete;
DomainLookuperResult proceed(DomainLookuperPacketDirection dir, const uint8_t *data, size_t length);
void reset();
private:
struct Context;
std::unique_ptr<Context> m_context;
};
} // namespace ag
@@ -0,0 +1,68 @@
#pragma once
#include <memory>
#include <variant>
#include "vpn/event_loop.h"
#include "vpn/internal/server_upstream.h"
namespace ag {
class VpnClient;
using EndpointConnectorResult = std::variant<
/// Successfully connected upstream
std::unique_ptr<ServerUpstream>,
/// Disconnected for some reason (`code` may be `VPN_EC_NOERROR` in case
/// it is disconnected gracefully)
VpnError>;
struct EndpointConnectorHandler {
void (*func)(void *arg, EndpointConnectorResult result) = nullptr;
void *arg = nullptr;
};
struct EndpointConnectorParameters {
/// An event loop for operation
VpnEventLoop *ev_loop = nullptr;
/// Parent VPN client
VpnClient *vpn_client = nullptr;
/// Upstream handler which will be set to the upstream in case of successful connection
SeverHandler upstream_handler = {};
/// Connector handler
EndpointConnectorHandler connector_handler = {};
};
/**
* The endpoint connector is intended to encapsulate the process of establishing
* a connection to the endpoint
*/
class EndpointConnector {
public:
const EndpointConnectorParameters PARAMETERS = {};
explicit EndpointConnector(const EndpointConnectorParameters &parameters)
: PARAMETERS(parameters) {
}
virtual ~EndpointConnector() = default;
EndpointConnector(const EndpointConnector &) = delete;
EndpointConnector &operator=(const EndpointConnector &) = delete;
EndpointConnector(EndpointConnector &&) = delete;
EndpointConnector &operator=(EndpointConnector &&) = delete;
/**
* Initiate the connection
* @param timeout_ms the procedure timeout
* @return some error if failed to start
*/
virtual VpnError connect(uint32_t timeout_ms) = 0;
/**
* Interrupt the connection procedure
*/
virtual void disconnect() = 0;
};
} // namespace ag
+104
View File
@@ -0,0 +1,104 @@
#pragma once
#include <chrono>
#include <map>
#include <memory>
#include <optional>
#include <event2/event.h>
#include "common/logger.h"
#include "vpn/event_loop.h"
#include "vpn/internal/utils.h"
#include "vpn/utils.h"
namespace ag {
struct IcmpRequestKey {
uint16_t id = 0;
static IcmpRequestKey make(const IcmpEchoRequest &request);
static IcmpRequestKey make(const IcmpEchoReply &reply);
bool operator<(const IcmpRequestKey &other) const;
};
struct IcmpEchoRequestKey : public IcmpRequestKey {
uint16_t seqno = 0;
static IcmpEchoRequestKey make(const IcmpEchoRequest &request);
static IcmpEchoRequestKey make(const IcmpEchoReply &reply);
bool operator<(const IcmpEchoRequestKey &other) const;
};
enum IcmpManagerMessageStatus {
/** A message should be passed further to the destination */
IM_MSGS_PASS,
/** A message should be dropped */
IM_MSGS_DROP,
};
struct IcmpManagerHandler {
/** Raised when an ICMP reply should be sent to the client */
void (*on_reply_ready)(void *arg, const IcmpEchoReply &reply);
/** User context */
void *arg;
};
class IcmpManager {
public:
struct Parameters {
/** Event loop for operation */
VpnEventLoop *ev_loop;
/** ICMP request timeout */
std::optional<std::chrono::milliseconds> request_timeout;
};
IcmpManager();
~IcmpManager() = default;
IcmpManager(IcmpManager &&) = default;
IcmpManager &operator=(IcmpManager &&) = default;
IcmpManager(const IcmpManager &) = delete;
IcmpManager &operator=(const IcmpManager &) = delete;
/**
* Initialize the ICMP manager
* @return true if successful
*/
bool init(Parameters parameters, IcmpManagerHandler handler);
/**
* Deinitialize the ICMP manager
*/
void deinit();
/**
* Register a request received from client
* @return see `IcmpManagerMessageStatus`
*/
IcmpManagerMessageStatus register_request(const IcmpEchoRequest &request);
/**
* Register a reply received from remote host
* @param reply the reply (may be modified)
* @return see `IcmpManagerMessageStatus`
*/
IcmpManagerMessageStatus register_reply(IcmpEchoReply &reply);
private:
struct RequestInfo;
static void request_info_delete(RequestInfo *i);
using RequestInfoPtr = DeclPtr<RequestInfo, &request_info_delete>;
std::map<IcmpRequestKey, RequestInfoPtr> m_requests;
Parameters m_parameters = {};
IcmpManagerHandler m_handler = {};
DeclPtr<event, &event_free> m_timer;
ag::Logger m_log{"ICMP"};
int m_id;
static void timer_callback(evutil_socket_t, short, void *arg);
};
} // namespace ag
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
namespace ag {
class IdGenerator {
public:
explicit IdGenerator(size_t step = 1)
: m_step(step) {
}
size_t get() {
size_t id = m_next_id;
m_next_id += m_step;
return id;
}
void reset() {
m_next_id = 1;
}
private:
size_t m_step;
size_t m_next_id = 1;
};
} // namespace ag
+203
View File
@@ -0,0 +1,203 @@
#pragma once
#include <cstdint>
#include "vpn/internal/icmp_manager.h"
#include "vpn/internal/utils.h"
#include "vpn/vpn.h"
namespace ag {
class VpnClient;
enum ServerEvent {
SERVER_EVENT_SESSION_OPENED, /**< Called when session with server is successfully established (raised with null) */
SERVER_EVENT_SESSION_CLOSED, /**< Called when session with server is gracefully closed by server (raised with null)
*/
SERVER_EVENT_CONNECTION_OPENED, /**< Called when connection to peer is successfully opened (raised with connection
id) */
SERVER_EVENT_CONNECTION_CLOSED, /**< Called when connection is gracefully closed by peer (raised with connection id)
*/
SERVER_EVENT_READ, /**< Called when some data needs to be sent via connection (raised with `ServerReadEvent`) */
SERVER_EVENT_DATA_SENT, /**< Called when some data was sent to client (raised with `ServerDataSentEvent`) */
SERVER_EVENT_HEALTH_CHECK_RESULT, /**< Called when a health check result is ready (raised with `VpnError`) */
SERVER_EVENT_GET_AVAILABLE_TO_SEND, /**< Called when the upstream wants to know available size for sending (raised
with `ServerAvailableToSendEvent`) */
SERVER_EVENT_ERROR, /**< Called when some error happened on server side (raised with `ServerError`) */
SERVER_EVENT_ECHO_REPLY, /**< Called when ICMP echo reply is received (raised with `icmp_echo_reply_t`) */
};
struct ServerReadEvent {
uint64_t id; /**< connection id */
const uint8_t *data; /**< data from server */
size_t length; /**< data length */
int result; /**< (filled by handler) operation result */
};
struct ServerDataSentEvent {
uint64_t id; /**< connection id */
size_t length; /**< sent bytes number (if 0, then connection polls for send resuming) */
};
struct ServerAvailableToSendEvent {
uint64_t id; /**< connection id */
size_t length; /**< (filled by handler) number of available to send bytes */
};
struct ServerError {
uint64_t id; /**< connection id (if `NON_ID`, then event relates to the whole session, no a connection) */
VpnError error;
};
struct SeverHandler {
/**
* Event handling function
* @param arg user argument
* @param what see `ServerEvent`
* @param data event data (see `ServerEvent`)
*/
void (*func)(void *arg, ServerEvent what, void *data) = nullptr;
/** User argument */
void *arg = nullptr;
};
/**
* Server communication interface which encapsulates server-side connections management
*/
class ServerUpstream {
public:
const std::optional<VpnUpstreamProtocolConfig> PROTOCOL_CONFIG;
VpnClient *vpn = nullptr;
SeverHandler handler = {};
int id;
explicit ServerUpstream(int id, std::optional<VpnUpstreamProtocolConfig> protocol_config = std::nullopt)
: PROTOCOL_CONFIG(protocol_config)
, id(id) {
}
virtual ~ServerUpstream() = default;
ServerUpstream(const ServerUpstream &) = delete;
ServerUpstream &operator=(const ServerUpstream &) = delete;
ServerUpstream(ServerUpstream &&) noexcept = delete;
ServerUpstream &operator=(ServerUpstream &&) noexcept = delete;
/**
* Initialize server upstream (MUST be called if overridden)
* @param vpn vpn instance
* @param handler server events handler
* @return true if initialized successfully, false otherwise
*/
virtual bool init(VpnClient *vpn, SeverHandler handler) {
this->vpn = vpn;
this->handler = handler;
return true;
}
/**
* Deinitialize server upstream
*/
virtual void deinit() = 0;
/**
* Open session with server. Result will be raised asynchronously with
* `SERVER_EVENT_SESSION_OPENED` in case of success, or with `SERVER_EVENT_ERROR` in case of
* error.
* @param timeout_ms timeout of operation (if 0, the value from upstream settings will be used)
* @return true if operation started successfully, false otherwise
*/
virtual bool open_session(uint32_t timeout_ms = 0) = 0;
/**
* Close session with server
*/
virtual void close_session() = 0;
/**
* Create connection to peer. Result will be raised asynchronously with
* `SERVER_EVENT_CONNECTION_OPENED` in case of success, or with `SERVER_EVENT_ERROR` in case of
* error.
* @param addr source and destination address pair
* @param proto connection protocol
* @param app_name name of the application that initiated this connection (optional)
* @return connection id in case of success, NON_ID in case of error
*/
virtual uint64_t open_connection(const TunnelAddressPair *addr, int proto, std::string_view app_name) = 0;
/**
* Close connection to peer
* @param id connection id
* @param graceful true if connection should be closed in a graceful way
* @param async true if connection should be closed with a context switch
*/
virtual void close_connection(uint64_t id, bool graceful, bool async) = 0;
/**
* Send data through connection
* @param id connection id
* @param data data to send
* @param length data length
* @return number of consumed bytes (< 0 in case of error)
*/
virtual ssize_t send(uint64_t id, const uint8_t *data, size_t length) = 0;
/**
* Notify server of client sent some data
* @param id connection id
* @param n number of sent bytes
*/
virtual void consume(uint64_t id, size_t length) = 0;
/**
* Get free space in write buffer
* @param id connection id
*/
virtual size_t available_to_send(uint64_t id) = 0;
/**
* Enable/disable read operations
* @param id connection id
* @param info flow control info
*/
virtual void update_flow_control(uint64_t id, TcpFlowCtrlInfo info) = 0;
/**
* Perform health check procedure
* @return VPN_EC_NOERROR in case of success, some error code otherwise
*/
virtual VpnError do_health_check() = 0;
/**
* Get statistics of the connection to endpoint
*/
[[nodiscard]] virtual VpnConnectionStats get_connection_stats() const = 0;
/**
* Process an ICMP echo request received from client
*/
virtual void on_icmp_request(IcmpEchoRequestEvent &event) = 0;
/**
* Get the connection protocol
*/
[[nodiscard]] VpnUpstreamProtocol get_protocol() const {
return this->PROTOCOL_CONFIG->type;
}
/**
* Handle a system sleep event. The system is going to sleep after this function returns.
*/
virtual void handle_sleep() {
// Default no-op
}
/**
* Handler a system wake up event.
*/
virtual void handle_wake() {
// Default no-op
}
};
} // namespace ag
+90
View File
@@ -0,0 +1,90 @@
#pragma once
#include <chrono>
#include <memory>
#include <optional>
#include <unordered_map>
#include <event2/event.h>
#include <khash.h>
#include "common/logger.h"
#include "vpn/internal/client_listener.h"
#include "vpn/internal/dns_sniffer.h"
#include "vpn/internal/icmp_manager.h"
#include "vpn/internal/server_upstream.h"
#include "vpn/internal/utils.h"
#include "vpn/internal/vpn_connection.h"
#include "vpn/internal/vpn_dns_resolver.h"
#include "vpn/utils.h"
#include "vpn/vpn.h"
namespace ag {
KHASH_MAP_INIT_INT64(connections_by_id, VpnConnection *);
struct VpnConnections {
khash_t(connections_by_id) *by_client_id = nullptr;
khash_t(connections_by_id) *by_server_id = nullptr;
};
struct DnsResolveWaiter {
uint64_t conn_client_id = NON_ID;
bool failures[magic_enum::enum_count<dns_utils::RecordType>()] = {};
};
struct Tunnel {
static constexpr std::chrono::seconds EXCLUSIONS_RESOLVE_PERIOD{60 * 60};
VpnConnections connections = {};
VpnClient *vpn = nullptr;
IcmpManager icmp_manager;
ag::Logger log{"TUNNEL"};
int id;
bool endpoint_upstream_connected = false;
std::unique_ptr<VpnDnsResolver> dns_resolver;
std::unordered_map<VpnDnsResolveId, DnsResolveWaiter> dns_resolve_waiters;
ag::AutoTaskId repeat_exclusions_resolve_task;
std::unique_ptr<ServerUpstream> fake_upstream;
DnsSniffer dns_sniffer;
Tunnel();
~Tunnel();
Tunnel(const Tunnel &) = delete;
Tunnel &operator=(const Tunnel &) = delete;
Tunnel(Tunnel &&) noexcept = delete;
Tunnel &operator=(Tunnel &&) noexcept = delete;
bool init(VpnClient *vpn);
void deinit();
void upstream_handler(ServerUpstream *upstream, ServerEvent what, void *data);
void listener_handler(ClientListener *listener, ClientEvent what, void *data);
void complete_connect_request(uint64_t id, std::optional<VpnConnectAction> action);
void reset_connections(int uid);
void reset_connection(uint64_t client_id);
void on_before_endpoint_disconnect(ServerUpstream *upstream);
void on_after_endpoint_disconnect(ServerUpstream *upstream);
void on_exclusions_updated();
/**
* @param request_result The connection request result
* @param only_app_initiated_dns If true and the connection is not forcibly redirected (`VPN_CA_FORCE_REDIRECT`),
* all the non-app-initiated DNS queries
* (`vpn_network_manager_check_app_request_domain`) will be dropped on this connection
* @return Some value if connection should definitely be routed to some upstream.
* Otherwise, if upstream can be changed in the future (e.g. if the destination address
* is ip, we can realize that the connection should not have been routed to the
* VPN endpoint, but to the host directly), none is returned.
*/
std::optional<VpnConnectAction> finalize_connect_action(
ConnectRequestResult &request_result, bool only_app_initiated_dns) const;
static void on_icmp_reply_ready(void *arg, const IcmpEchoReply &reply);
};
} // namespace ag
+230
View File
@@ -0,0 +1,230 @@
#pragma once
#include <codecvt>
#include <cstring>
#include <locale>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#include <event2/event.h>
#include <magic_enum.hpp>
#include "net/tcp_socket.h"
#include "net/udp_socket.h"
#include "vpn/platform.h"
#include "vpn/utils.h"
#include "vpn/vpn.h"
#define CONN_BUFFER_FILE_NAME_FMT "cbuf-%" PRIu64 "-%" PRIu64 ".dat"
namespace ag {
static constexpr uint64_t NON_ID = UINT64_MAX;
static constexpr size_t HTTP_OK_STATUS = 200;
static constexpr size_t HTTP_AUTH_REQUIRED_STATUS = 407;
static constexpr char HTTP_AUTH_REQUIRED_MSG[] = "Authorization Required";
struct ConnectRequestResult {
uint64_t id = NON_ID;
// nullopt means we aren't completely sure if a connection should be redirected
std::optional<ag::VpnConnectAction> action;
std::string appname;
int uid = 0;
[[nodiscard]] std::string to_string() const {
return str_format("ID=%" PRIu64 " action=%s appname=%s", this->id,
magic_enum::enum_name(this->action.value_or(ag::VPN_CA_DEFAULT)).data(), this->appname.c_str());
}
};
struct NamePort {
std::string name;
int port = 0;
};
inline bool operator==(const NamePort &lh, const NamePort &rh) {
return lh.port == rh.port && lh.name == rh.name;
}
inline bool operator!=(const NamePort &lh, const NamePort &rh) {
return !(lh == rh);
}
using TunnelAddress = std::variant<std::monostate, sockaddr_storage, NamePort>;
struct TunnelAddressPair {
sockaddr_storage src = {};
TunnelAddress dst = {};
TunnelAddressPair() = default;
TunnelAddressPair(const sockaddr *s, const TunnelAddress *d)
: src(ag::sockaddr_to_storage(s))
, dst(*d) {
}
TunnelAddressPair(const sockaddr *s, const sockaddr *d)
: src(ag::sockaddr_to_storage(s))
, dst(ag::sockaddr_to_storage(d)) {
}
};
inline bool operator==(const TunnelAddressPair &lh, const TunnelAddressPair &rh) {
if (0 != memcmp(&lh.src, &rh.src, sizeof(lh.src))) {
return false;
}
if (lh.dst.index() != rh.dst.index()) {
return false;
}
if (const sockaddr_storage *ld = std::get_if<sockaddr_storage>(&lh.dst),
*rd = std::get_if<sockaddr_storage>(&rh.dst);
ld && rd) {
return sockaddr_equals((sockaddr *) ld, (sockaddr *) rd);
}
if (const NamePort *ld = std::get_if<NamePort>(&lh.dst), *rd = std::get_if<NamePort>(&rh.dst); ld && rd) {
return *ld == *rd;
}
return false;
}
inline bool operator!=(const TunnelAddressPair &lh, const TunnelAddressPair &rh) {
return !(lh == rh);
}
static const TunnelAddress HEALTH_CHECK_HOST(NamePort{"_check", 0});
std::string tunnel_addr_to_str(const TunnelAddress *addr);
/**
* Get pointer value and null it
*/
template <typename T, typename = std::enable_if_t<std::is_pointer<T>::value>>
T load_and_null(T &x) {
return std::exchange(x, nullptr);
}
using TcpSocketPtr = ag::DeclPtr<TcpSocket, &tcp_socket_destroy>;
using UdpSocketPtr = ag::DeclPtr<UdpSocket, &udp_socket_destroy>;
using EventPtr = ag::DeclPtr<event, &event_free>;
struct SockAddrTag {
sockaddr_storage addr = {};
std::string appname;
};
inline bool operator==(const SockAddrTag &lh, const SockAddrTag &rh) {
return ag::sockaddr_equals((sockaddr *) &lh.addr, (sockaddr *) &rh.addr) && lh.appname == rh.appname;
}
/**
* Check if string starts with prefix
*/
static inline constexpr bool starts_with(std::string_view str, std::string_view prefix) {
return str.substr(0, prefix.length()) == prefix;
}
/**
* Contruct full path for connection buffer file
* @param base_path directory path
* @param id connection id
*/
std::string make_buffer_file_path(const char *base_path, uint64_t id);
/**
* Remove connection buffer files which had not been removed at the end of VPN run for some reason
* @param base_path directory path to scan
*/
void clean_up_buffer_files(const char *base_path);
static inline std::string safe_path_name(const char *str) {
return str;
}
static inline std::string safe_path_name(const wchar_t *str) {
return std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(str);
}
ag::VpnUpstreamConfig vpn_upstream_config_clone(const ag::VpnUpstreamConfig *src);
void vpn_upstream_config_destroy(ag::VpnUpstreamConfig *config);
template <std::size_t N, std::size_t... IS>
constexpr std::array<const char *, N> cpp_to_cstr_array(
const std::array<std::string_view, N> &arr, std::index_sequence<IS...>) {
return {{arr[IS].data()...}};
}
template <std::size_t N, std::size_t... IS>
constexpr std::array<const char *, N> cpp_to_cstr_array(const std::array<std::string_view, N> &arr) {
return cpp_to_cstr_array(arr, std::make_index_sequence<N>());
}
template <typename E, std::size_t N = magic_enum::enum_count<E>()>
constexpr std::array<const char *, N> make_enum_names_array() {
return cpp_to_cstr_array<N>(magic_enum::enum_names<E>());
}
void log_headers(const ag::Logger &log, uint64_t stream_id, const HttpHeaders *headers, const char *msg);
ag::VpnError bad_http_response_to_connect_error(const HttpHeaders *response);
HttpHeaders make_http_connect_request(
HttpVersion version, const TunnelAddress *dst_addr, std::string_view app_name, std::string_view creds);
std::string make_credentials(std::string_view username, std::string_view password);
using SslPtr = ag::DeclPtr<SSL, SSL_free>;
std::variant<SslPtr, std::string> make_ssl(
int (*verification_callback)(X509_STORE_CTX *, void *), void *arg, ag::U8View alpn_protos, const char *sni);
} // namespace ag
inline bool operator==(const sockaddr_storage &lh, const sockaddr_storage &rh) {
return ag::sockaddr_equals((sockaddr *) &lh, (sockaddr *) &rh);
}
inline bool operator!=(const sockaddr_storage &lh, const sockaddr_storage &rh) {
return !(lh == rh);
}
namespace std {
template <>
struct hash<sockaddr_storage> {
size_t operator()(const sockaddr_storage &k) const {
return ag::sockaddr_hash((sockaddr *) &k);
}
};
template <>
struct hash<ag::TunnelAddress> {
size_t operator()(const ag::TunnelAddress &addr) const {
size_t hash = 0;
if (const sockaddr_storage *a = std::get_if<sockaddr_storage>(&addr); a != nullptr) {
hash = ag::sockaddr_hash((sockaddr *) a);
} else if (const ag::NamePort *np = std::get_if<ag::NamePort>(&addr); np != nullptr) {
hash = ag::hash_pair_combine(ag::str_hash32(np->name.c_str(), np->name.length()), np->port);
}
return hash;
}
};
template <>
struct hash<ag::TunnelAddressPair> {
size_t operator()(const ag::TunnelAddressPair &addr) const {
return ag::hash_pair_combine(ag::sockaddr_hash((sockaddr *) &addr.src), hash<ag::TunnelAddress>{}(addr.dst));
}
};
template <>
struct hash<ag::SockAddrTag> {
size_t operator()(const ag::SockAddrTag &k) const {
return ag::hash_pair_combine(ag::sockaddr_hash((sockaddr *) &k.addr), std::hash<std::string>()(k.appname));
}
};
} // namespace std
+158
View File
@@ -0,0 +1,158 @@
#pragma once
#include <chrono>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "common/logger.h"
#include "net/locations_pinger.h"
#include "net/network_manager.h"
#include "net/utils.h"
#include "vpn/event_loop.h"
#include "vpn/fsm.h"
#include "vpn/internal/client_listener.h"
#include "vpn/internal/data_buffer.h"
#include "vpn/internal/dns_proxy_accessor.h"
#include "vpn/internal/domain_filter.h"
#include "vpn/internal/domain_lookuper.h"
#include "vpn/internal/endpoint_connector.h"
#include "vpn/internal/id_generator.h"
#include "vpn/internal/server_upstream.h"
#include "vpn/internal/tunnel.h"
#include "vpn/internal/utils.h"
#include "vpn/vpn.h"
namespace ag {
namespace vpn_client {
enum Event {
EVENT_PROTECT_SOCKET, /** Raised when socket needs to be protected (raised with `socket_protect_event_t`) */
EVENT_VERIFY_CERTIFICATE, /** Raised when VPN needs to verify certificate (raised with
`VpnVerifyCertificateEvent`) */
EVENT_CONNECTED, /** Raised when VPN client connected to endpoint gracefully (raised with null) */
EVENT_DISCONNECTED, /** Raised when VPN client disconnected from endpoint gracefully (raised with null) */
EVENT_OUTPUT, /** Raised when some data from server is ready to be sent to client application (raised with
`VpnClientOutputEvent`) */
EVENT_CONNECT_REQUEST, /** Raised when new incoming connection is appeared (raised with
`VpnConnectRequestEvent`) */
EVENT_ERROR, /** Raised when something went wrong (raised with `VpnError`) */
EVENT_DNS_UPSTREAM_UNAVAILABLE, /** Raised if a health check on the configured DNS upstream is failed (raised with
null) */
};
struct Handler {
void (*func)(void *arg, Event what, void *data);
void *arg;
};
struct Parameters {
VpnEventLoop *ev_loop = nullptr;
evdns_base *dns_base = nullptr;
VpnNetworkManager *network_manager = nullptr;
Handler handler = {};
CertVerifyHandler cert_verify_handler = {};
};
struct EndpointConnectionConfig {
VpnUpstreamProtocolConfig main_protocol;
VpnUpstreamFallbackConfig fallback;
const VpnEndpoint *endpoint;
std::chrono::milliseconds timeout{VPN_DEFAULT_ENDPOINT_UPSTREAM_TIMEOUT_MS};
std::string username;
std::string password;
std::chrono::milliseconds endpoint_pinging_period{VPN_DEFAULT_ENDPOINT_PINGING_PERIOD_MS};
};
static constexpr const char *LOG_NAME = "VPNCLIENT";
} // namespace vpn_client
class VpnClient {
public:
VpnClient() = delete;
VpnClient(const VpnClient &) = delete;
VpnClient(VpnClient &&) = delete;
VpnClient &operator=(const VpnClient &) = delete;
VpnClient &operator=(VpnClient &&) = delete;
explicit VpnClient(vpn_client::Parameters parameters);
~VpnClient();
VpnError init(const VpnSettings *settings);
VpnError connect(vpn_client::EndpointConnectionConfig config, uint32_t timeout_ms = 0);
VpnError listen(std::unique_ptr<ClientListener> listener, const VpnListenerConfig *config, bool ipv6_available);
void disconnect();
void finalize_disconnect();
void deinit();
void process_client_packets(VpnPackets packets);
std::optional<VpnConnectAction> finalize_connect_action(
ConnectRequestResult &request_result, bool only_app_initiated_dns) const;
void complete_connect_request(uint64_t id, std::optional<VpnConnectAction> action);
void reject_connect_request(uint64_t id);
void update_exclusions(VpnMode mode, std::string_view exclusions);
void reset_connections(int uid);
void reset_connection(uint64_t id);
void update_parameters(vpn_client::Parameters parameters);
void do_health_check();
void do_dns_upstream_health_check();
void handle_sleep();
void handle_wake();
VpnConnectionStats get_connection_stats() const;
[[nodiscard]] std::unique_ptr<DataBuffer> make_buffer(uint64_t id) const;
[[nodiscard]] bool may_send_icmp_request() const;
[[nodiscard]] static int next_upstream_id();
Fsm fsm;
std::unique_ptr<Tunnel> tunnel = std::make_unique<Tunnel>(); // tunnel connections manager
vpn_client::Parameters parameters = {};
VpnListenerConfig listener_config = {}; // common listener configuration
vpn_client::EndpointConnectionConfig upstream_config = {}; // upstream configuration
bool quic_enabled = false;
bool kill_switch_on = false;
bool ipv6_available = false;
std::unique_ptr<ServerUpstream> endpoint_upstream; // upstream for connections routed through vpn
std::unique_ptr<ServerUpstream> bypass_upstream; // upstream for bypassed connections
std::unique_ptr<ClientListener> client_listener; // client listener
std::unique_ptr<ClientListener> dns_proxy_listener; // client listener
IdGenerator listener_conn_id_generator{}; // connection id generator for client-side connections
IdGenerator upstream_conn_id_generator{}; // connection id generator for server-side connections
std::unique_ptr<DnsProxyAccessor> dns_proxy; // DNS proxy wrapper
DomainFilter domain_filter; // decides if connection should be bypassed over VPN
std::set<ag::AutoTaskId> deferred_tasks;
std::unique_ptr<EndpointConnector> endpoint_connector; // connects to endpoint using given upstream(s)
std::optional<std::string> tmp_files_base_path; // directory where some temporary files will be stored
size_t conn_memory_buffer_threshold =
0; // connection in-memory buffer size exceeding which causes storing incoming data in a file
size_t max_conn_buffer_file_size = 0; // maximum size of file of a connection data buffer
ag::Logger log{vpn_client::LOG_NAME}; // logger
int id = 0;
std::optional<VpnError> pending_error;
sockaddr_storage socks_listener_address{}; // The address the SOCKS listener is bound to
};
} // namespace ag
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include <bitset>
#include <cstdint>
#include "vpn/event_loop.h"
#include "vpn/internal/domain_lookuper.h"
#include "vpn/internal/utils.h"
#include "vpn/utils.h"
namespace ag {
enum VpnConnectionState {
/// Waiting until an application gives connect result
CONNS_WAITING_ACTION,
/// Waiting for the target domain name resolve result
CONNS_WAITING_RESOLVE,
/// Waiting for server side response for connection open request
CONNS_WAITING_RESPONSE,
/// Waiting for server side response while migrating to another upstream
CONNS_WAITING_RESPONSE_MIGRATING,
/// Waiting for connection accept on the client side
CONNS_WAITING_ACCEPT,
/// Complete state of a normal data exchange
CONNS_CONNECTED,
/// Established connection waiting for migration completion
CONNS_CONNECTED_MIGRATING,
};
enum VpnConnectionFlags {
/// Set until the first packet from a client is received
CONNF_FIRST_PACKET,
/// Connection is routed to the target host directly unconditionally
CONNF_FORCIBLY_BYPASSED,
/// Connection is routed through the VPN endpoint unconditionally
CONNF_FORCIBLY_REDIRECTED,
/// Trying to find the destination host name to check if the connection should be excluded
CONNF_LOOKINGUP_DOMAIN,
/// Session with the endpoint is already terminated for some reason
/// (no need to wait for server side close event)
CONNF_SESSION_CLOSED,
/// Connection is potentially targets the domain which is excluded
CONNF_SUSPECT_EXCLUSION,
/// Connection is established via the fake upstream to check if the host name is in exclusions
CONNF_FAKE_CONNECTION,
/// Connection traffic is plain DNS data
CONNF_PLAIN_DNS_CONNECTION,
/// Drop all the DNS queries except those which resolve domains of the requests
/// made by an application
CONNF_DROP_NON_APP_DNS_QUERIES,
/// Connection is routed through the local DNS proxy
CONNF_ROUTE_TO_DNS_PROXY,
};
enum PacketDirection {
PD_OUTGOING,
PD_INCOMING,
};
class ClientListener;
class ServerUpstream;
struct VpnConnection {
uint64_t client_id = NON_ID;
uint64_t server_id = NON_ID;
ClientListener *listener = nullptr;
ServerUpstream *upstream = nullptr;
VpnConnectionState state = CONNS_WAITING_ACTION;
TunnelAddressPair addr;
int proto = 0;
std::bitset<width_of<VpnConnectionFlags>()> flags;
int uid = 0;
std::string app_name;
ag::AutoTaskId complete_connect_request_task;
size_t incoming_bytes = 0;
size_t outgoing_bytes = 0;
static VpnConnection *make(uint64_t client_id, TunnelAddressPair addr, int proto);
VpnConnection(const VpnConnection &) = delete;
VpnConnection(VpnConnection &&) = delete;
VpnConnection &operator=(const VpnConnection &) = delete;
VpnConnection &operator=(VpnConnection &&) = delete;
VpnConnection() = default;
virtual ~VpnConnection() = default;
[[nodiscard]] SockAddrTag make_tag() const;
};
struct UdpVpnConnection : public VpnConnection {
bool check_dns_queries_completed(PacketDirection dir);
void count_dns_message(PacketDirection type);
private:
uint32_t m_dns_query_counter = 0;
[[nodiscard]] bool are_dns_queries_completed() const;
};
struct TcpVpnConnection : public VpnConnection {
DomainLookuper domain_lookuper;
uint64_t migrating_client_id = NON_ID;
};
} // namespace ag
@@ -0,0 +1,165 @@
#pragma once
#include <array>
#include <bitset>
#include <map>
#include <optional>
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>
#include <magic_enum.hpp>
#include "common/logger.h"
#include "net/dns_utils.h"
#include "vpn/event_loop.h"
#include "vpn/internal/client_listener.h"
#include "vpn/utils.h"
namespace ag {
using VpnDnsResolveId = uint32_t;
enum VpnDnsResolverQueue {
/// Contains items whose resolving can be delayed
VDRQ_BACKGROUND,
/// Contains items that should be resolved as soon as possible
VDRQ_FOREGROUND,
};
/// Successfully resolved
struct VpnDnsResolverSuccess {
sockaddr_storage addr;
};
/// Failed to resolve a domain for some reason
struct VpnDnsResolverFailure {
dns_utils::RecordType record_type;
};
using VpnDnsResolverResult = std::variant<VpnDnsResolverSuccess, VpnDnsResolverFailure>;
/**
* This class is intended to make plain DNS requests through VPN endpoint to resolve
* the provided domains.
*/
class VpnDnsResolver : public ClientListener {
public:
/// Prevent UDP stream flooding
static constexpr size_t MAX_PARALLEL_BACKGROUND_RESOLVES = 32;
using RecordTypeSet = std::bitset<magic_enum::enum_count<dns_utils::RecordType>()>;
struct ResultHandler {
/// Will be raised for each of the record type passed to `resolve()`
void (*func)(void *arg, VpnDnsResolveId id, VpnDnsResolverResult result);
void *arg;
};
VpnDnsResolver() = default;
~VpnDnsResolver() override = default;
VpnDnsResolver(const VpnDnsResolver &) = delete;
VpnDnsResolver &operator=(const VpnDnsResolver &) = delete;
VpnDnsResolver(VpnDnsResolver &&) = delete;
VpnDnsResolver &operator=(VpnDnsResolver &&) = delete;
void deinit() override;
/**
* Set the IPV6 availability
*/
void set_ipv6_availability(bool available);
/**
* Start the domain name resolving procedure
* @param name the name to be resolved
* @param record_types record types to resolve
* @param result_handler the handler which is called after a result is ready
* @return some ID if started successfully
*/
std::optional<VpnDnsResolveId> resolve(VpnDnsResolverQueue queue, std::string name,
RecordTypeSet record_types = 1 << dns_utils::RT_A | 1 << dns_utils::RT_AAAA,
ResultHandler result_handler = {});
/**
* Stop the specified resolving procedure silently
*/
void cancel(VpnDnsResolveId id);
/**
* Stop all running resolving procedures.
* May raise some callbacks.
*/
void stop_resolving();
private:
struct Resolve {
std::string name;
RecordTypeSet record_types;
ResultHandler handler = {};
};
struct BootstrapState {
struct Connection {
std::array<std::optional<uint16_t>, 2> queries;
};
std::unordered_map<uint64_t, Connection> connections;
ag::AutoTaskId timeout_task;
};
struct ResolveState {
struct Query {
VpnDnsResolveId id;
dns_utils::RecordType record_type;
ResultHandler result_handler;
};
uint64_t connection_id = NON_ID;
bool is_open = false;
std::unordered_map<uint16_t, Query> queries;
ag::AutoTaskId timeout_task;
};
using State = std::variant<std::monostate, BootstrapState, ResolveState>;
using Queue = std::map<VpnDnsResolveId, Resolve>;
bool m_ipv6_available = false;
std::optional<sockaddr_storage> m_dns_resolver_address;
VpnDnsResolveId next_id = 0;
std::array<Queue, magic_enum::enum_count<VpnDnsResolverQueue>()> queues;
State state;
std::vector<uint64_t> accepting_connections;
ag::AutoTaskId deferred_accept_task;
std::vector<uint64_t> closing_connections;
ag::AutoTaskId deferred_close_task;
ag::AutoTaskId deferred_resolve_task;
uint16_t next_connection_port = 1;
bool stopping = false;
ag::Logger log{"VPN_DNS_RESOLVER"};
void complete_connect_request(uint64_t id, ClientConnectResult result) override;
void close_connection(uint64_t id, bool graceful, bool async) override;
ssize_t send(uint64_t id, const uint8_t *data, size_t length) override;
void consume(uint64_t id, size_t n) override;
TcpFlowCtrlInfo flow_control_info(uint64_t id) override;
void turn_read(uint64_t id, bool on) override;
int process_client_packets(VpnPackets packets) override;
void accept_pending_connection(uint64_t);
std::optional<std::pair<uint16_t, std::vector<uint8_t>>> make_request(bool is_aaaa, std::string_view name) const;
std::optional<uint16_t> send_request(bool is_aaaa, uint64_t conn_id, std::string_view name);
std::array<std::optional<uint16_t>, 2> send_request(
uint64_t conn_id, std::string_view name, RecordTypeSet record_types);
void resolve_pending_domains();
void resolve_queue(VpnDnsResolverQueue queue);
sockaddr_storage make_source_address();
static void raise_result(ResultHandler h, VpnDnsResolveId id, VpnDnsResolverResult result);
static void on_bootstrap_timeout(void *arg, TaskId);
static void on_resolve_timeout(void *arg, TaskId);
};
} // namespace ag
+164
View File
@@ -0,0 +1,164 @@
#pragma once
#include <cassert>
#include <cstdint>
#include <cstring>
#include <optional>
#include "vpn/platform.h"
#include "vpn/utils.h"
namespace ag {
namespace wire_utils {
static constexpr size_t IPV4_ADDR_SIZE = 4;
static constexpr size_t IPV6_ADDR_SIZE = 16;
static constexpr size_t PADDED_IP_SIZE = IPV6_ADDR_SIZE;
static constexpr size_t IPV4_6_SIZE_DIFF = IPV6_ADDR_SIZE - IPV4_ADDR_SIZE;
class Writer {
public:
explicit Writer(U8View buffer)
: m_buffer(buffer) {
}
~Writer() = default;
Writer(const Writer &) = delete;
Writer &operator=(const Writer &) = delete;
Writer(Writer &&) = delete;
Writer &operator=(Writer &&) = delete;
void put_u8(uint8_t val) {
assert(m_buffer.size() >= sizeof(val));
memcpy((void *) m_buffer.data(), &val, sizeof(val));
m_buffer.remove_prefix(sizeof(val));
}
void put_u16(uint16_t val) {
val = htons(val);
assert(m_buffer.size() >= sizeof(val));
memcpy((void *) m_buffer.data(), &val, sizeof(val));
m_buffer.remove_prefix(sizeof(val));
}
void put_u32(uint32_t val) {
assert(m_buffer.size() >= sizeof(val));
val = htonl(val);
memcpy((void *) m_buffer.data(), &val, sizeof(val));
m_buffer.remove_prefix(sizeof(val));
}
void put_data(U8View d) {
assert(m_buffer.size() >= d.size());
memcpy((void *) m_buffer.data(), d.data(), d.size());
m_buffer.remove_prefix(d.size());
}
void put_ip(const sockaddr *addr) {
size_t addr_size = (addr->sa_family == AF_INET) ? IPV4_ADDR_SIZE : IPV6_ADDR_SIZE;
this->put_data({(uint8_t *) sockaddr_get_ip_ptr(addr), addr_size});
}
void put_ip_padded(const sockaddr *addr) {
if (addr->sa_family == AF_INET) {
// empty PADDING for ipv4
static constexpr uint8_t PADDING[IPV4_6_SIZE_DIFF] = {};
this->put_data({PADDING, std::size(PADDING)});
}
this->put_ip(addr);
}
private:
U8View m_buffer;
};
class Reader {
public:
explicit Reader(U8View buffer)
: m_buffer(buffer) {
}
void drain(size_t n) {
m_buffer.remove_prefix(std::min(n, m_buffer.size()));
}
std::optional<uint8_t> get_u8() {
uint8_t val; // NOLINT(cppcoreguidelines-init-variables)
if (m_buffer.size() < sizeof(val)) {
return std::nullopt;
}
memcpy(&val, m_buffer.data(), sizeof(val));
m_buffer.remove_prefix(sizeof(val));
return val;
}
std::optional<uint16_t> get_u16() {
uint16_t val; // NOLINT(cppcoreguidelines-init-variables)
if (m_buffer.size() < sizeof(val)) {
return std::nullopt;
}
memcpy(&val, m_buffer.data(), sizeof(val));
m_buffer.remove_prefix(sizeof(val));
return ntohs(val);
}
std::optional<uint32_t> get_u32() {
uint32_t val; // NOLINT(cppcoreguidelines-init-variables)
if (m_buffer.size() < sizeof(val)) {
return std::nullopt;
}
memcpy(&val, m_buffer.data(), sizeof(val));
m_buffer.remove_prefix(sizeof(val));
return ntohl(val);
}
std::optional<sockaddr_storage> get_ip(int family) {
size_t ip_size = (family == AF_INET) ? IPV4_ADDR_SIZE : IPV6_ADDR_SIZE;
if (m_buffer.size() < ip_size) {
return std::nullopt;
}
sockaddr_storage addr = sockaddr_from_raw(m_buffer.data(), ip_size, 0);
m_buffer.remove_prefix(ip_size);
return addr;
}
std::optional<sockaddr_storage> get_ip_padded() {
if (m_buffer.size() < PADDED_IP_SIZE) {
return std::nullopt;
}
int family = AF_INET6;
bool is_ipv4 = std::all_of(m_buffer.begin(), std::next(m_buffer.begin(), IPV4_6_SIZE_DIFF), [](int i) -> bool {
return i == 0;
}) && 0 != memcmp(m_buffer.data(), &in6addr_loopback, IPV6_ADDR_SIZE);
if (is_ipv4) {
m_buffer.remove_prefix(IPV4_6_SIZE_DIFF);
family = AF_INET;
}
return this->get_ip(family);
}
std::optional<U8View> get_bytes(size_t size) {
if (m_buffer.size() < size) {
return std::nullopt;
}
U8View data = m_buffer.substr(0, size);
m_buffer.remove_prefix(size);
return data;
}
[[nodiscard]] U8View get_buffer() const {
return m_buffer;
}
private:
U8View m_buffer;
};
} // namespace wire_utils
} // namespace ag
+666
View File
@@ -0,0 +1,666 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "vpn/platform.h" // Unbreak Windows build
#include <event2/buffer.h>
#include <openssl/x509.h>
#include "common/logger.h"
#include "net/utils.h"
#include "vpn/event_loop.h"
#include "vpn/utils.h"
namespace ag {
enum {
VPN_DEFAULT_ENDPOINT_UPSTREAM_TIMEOUT_MS = 30 * 1000, // for VPN endpoint connection
VPN_DEFAULT_TCP_TIMEOUT_MS = 10 * 60 * 1000, // for bypassed server-side and client-side TCP connections
VPN_DEFAULT_UDP_TIMEOUT_MS = 30 * 1000, // for bypassed and redirected server-side and client-side UDP connections
VPN_DEFAULT_MAX_CONN_BUFFER_FILE_SIZE = 4 * 1024 * 1024,
VPN_DEFAULT_CONN_MEMORY_BUFFER_THRESHOLD = DEFAULT_CONNECTION_MEMORY_BUFFER_SIZE,
VPN_DEFAULT_ENDPOINT_PINGING_PERIOD_MS = 60 * 1000,
VPN_DEFAULT_RECOVERY_LOCATION_UPDATE_PERIOD_MS = 70 * 1000,
VPN_DEFAULT_INITIAL_RECOVERY_INTERVAL_MS = 1 * 1000,
VPN_DEFAULT_CONNECT_ATTEMPTS_NUM = 5,
VPN_DEFAULT_FALLBACK_CONNECT_DELAY_MS = 1 * 1000,
VPN_DEFAULT_POSTPONEMENT_WINDOW_MS =
3 * 1000, // how long after recovery starts connections are postponed instead of bypassed
};
static const float VPN_DEFAULT_RECOVERY_BACKOFF_RATE = 1.3f;
typedef enum {
VPN_EC_NOERROR, // Depending on context may mean successful operation status (if returned from `vpn_connect`)
// or an endpoint session is closed without any error (if raised with `VPN_EVENT_STATE_CHANGED`)
VPN_EC_ERROR, // General code for the errors not described below
VPN_EC_INVALID_SETTINGS, // Settings passed for an operation are invalid
VPN_EC_ADDR_IN_USE, // Operation failed because the specified address was in use
VPN_EC_INVALID_STATE, // VPN client instance is in invalid state for the requested operation
VPN_EC_AUTH_REQUIRED, // Authorization error (in case user credentials are invalid or expired)
VPN_EC_LOCATION_UNAVAILABLE, // None of the endpoints in a location are available
VPN_EC_EVENT_LOOP_FAILURE, // Failed to start the IO event loop, or it unexpectedly terminated
} VpnErrorCode;
typedef struct Vpn Vpn;
typedef void VpnListener;
/**
* TUN device handler configuration
*/
typedef struct {
/**
* File descriptor of TUN device. If -1, the client handler expects that application will
* provide data packet from a client application via `vpn_process_client_packet` and send
* data from a server in `VPN_EVENT_CLIENT_OUTPUT` handler. Otherwise it listens the descriptor
* by itself.
*/
evutil_socket_t fd;
/** Maximum transfer unit for TCP protocol (if 0, `DEFAULT_MTU_SIZE` will be used) */
uint32_t mtu_size;
/** Pcap file name */
const char *pcap_filename;
} VpnTunListenerConfig;
/**
* SOCKS listener configuration
*/
typedef struct {
/**
* Address to listen on for SOCKS5 traffic (if not set, `127.0.0.1` will be used).
* Recognized formats are:
* - [IPv6Address]:port
* - [IPv6Address]
* - IPv6Address
* - IPv4Address:port
* - IPv4Address
* If port is 0 or not specified, it will be chosen automatically.
*/
struct sockaddr_storage listen_address;
/**
* If set, require this username to connect.
* Must be set if `listen_address` is not a loopback address or if `password` is set.
*/
const char *username;
/**
* If set, require this password to connect.
* Must be set if `username` is set.
*/
const char *password;
} VpnSocksListenerConfig;
/**
* Common settings for all types of listeners.
*/
typedef struct {
/** Socket operations timeout (if 0, `VPN_DEFAULT_TCP_TIMEOUT_MS` will be used) */
uint32_t timeout_ms;
/**
* Optional.
* If configured, the library will intercept and route plain DNS queries to a DNS resolver.
* One of the following kinds:
* 8.8.8.8:53 -- plain DNS
* tcp://8.8.8.8:53 -- plain DNS over TCP
* tls://1.1.1.1 -- DNS-over-TLS
* https://dns.adguard.com/dns-query -- DNS-over-HTTPS
* sdns://... -- DNS stamp (see https://dnscrypt.info/stamps-specifications)
* quic://dns.adguard.com:8853 -- DNS-over-QUIC
*/
const char *dns_upstream;
} VpnListenerConfig;
/**
* Communication protocols between a VPN client and an endpoint
*/
typedef enum {
VPN_UP_HTTP2,
VPN_UP_HTTP3,
} VpnUpstreamProtocol;
typedef struct {
/** Number of parallel HTTP2 sessions. If 0, default value will be assigned. */
uint32_t connections_num;
} VpnHttp2UpstreamConfig;
typedef struct {
/** QUIC protocol version. If 0, default version will be used */
uint32_t quic_version;
} VpnHttp3UpstreamConfig;
typedef struct {
/**
* If a session with selected endpoint is lost, the library tries to recover it using exponential
* backoff intervals between attempts. This field defines the rate of increasing of the interval.
* If less then 1, `VPN_DEFAULT_RECOVERY_BACKOFF_RATE` will be assigned.
*/
float backoff_rate;
/**
* In case session recovery takes too long the library re-select an endpoint by pinging
* the location. This field defines the period between updates.
* If 0, `VPN_DEFAULT_RECOVERY_LOCATION_UPDATE_PERIOD_MS` will be assigned.
*/
uint32_t location_update_period_ms;
} VpnUpstreamSessionRecoverySettings;
typedef struct {
/** VPN endpoint communication protocol */
VpnUpstreamProtocol type;
union {
VpnHttp2UpstreamConfig http2;
VpnHttp3UpstreamConfig http3;
};
} VpnUpstreamProtocolConfig;
typedef struct {
/** By default falling back is disabled */
bool enabled;
/**
* The delay period after which the library begins to connect using the fallback protocol.
* The connection procedure by the main protocol does not stop in this case.
* The connection to the endpoint will eventually be through the protocol that established
* it faster.
* If 0, the default value (`VPN_DEFAULT_FALLBACK_CONNECT_DELAY_MS`) will be assigned.
*/
uint32_t connect_delay_ms;
/** Fall back upstream protocol configuration */
VpnUpstreamProtocolConfig protocol;
} VpnUpstreamFallbackConfig;
/**
* VPN client's server-side interface configuration
*/
typedef struct {
/** Protocol-specific configuration */
VpnUpstreamProtocolConfig protocol;
/**
* A location to connect to. An endpoint is selected by the location ping algorithm,
* see `locations_pinger_t` for details.
*/
VpnLocation location;
/**
* Time out value for VPN endpoints addresses ping operation.
* If 0, `DEFAULT_PING_TIMEOUT_MS` will be assigned.
*/
uint32_t location_ping_timeout_ms;
/**
* Socket operations timeout. If 0, `VPN_DEFAULT_ENDPOINT_UPSTREAM_TIMEOUT_MS` will be assigned.
* Used for IO operations and server session establishing during `vpn_connect`.
*/
uint32_t timeout_ms;
/**
* The library periodically pings a VPN endpoint. This is needed for an endpoint to increase its
* confidence in the connected client legitimacy, and for the library to update connectivity
* status.
* This field contains the period value of the pinging.
* If 0, `VPN_DEFAULT_ENDPOINT_PINGING_PERIOD_MS` will be assigned.
*/
uint32_t endpoint_pinging_period_ms;
/** Username for authorization */
const char *username;
/** Password for authorization */
const char *password;
/** Session recovery settings */
VpnUpstreamSessionRecoverySettings recovery;
/** Fall back configuration */
VpnUpstreamFallbackConfig fallback;
} VpnUpstreamConfig;
/**
* VPN client application events identifiers
*/
typedef enum {
VPN_EVENT_PROTECT_SOCKET, /** Raised when socket needs to be protected (raised with `socket_protect_event_t`) */
VPN_EVENT_VERIFY_CERTIFICATE, /** Raised when VPN needs to verify certificate (raised with
`VpnVerifyCertificateEvent`) */
VPN_EVENT_STATE_CHANGED, /** Raised when VPN endpoint session state is changed (raised with `VpnStateChangedEvent`)
*/
VPN_EVENT_CLIENT_OUTPUT, /** Raised when some data from server is ready to be sent to client application (raised
with `VpnClientOutputEvent`) */
VPN_EVENT_CONNECT_REQUEST, /** Raised when new incoming connection is appeared (raised with
`VpnConnectRequestEvent`) */
VPN_EVENT_ENDPOINT_CONNECTION_STATS, /** Raised when requested (with `vpn_request_endpoint_connection_stats`)
connection statistics is ready (raised with
`VpnEndpointConnectionStatsEvent`) */
VPN_EVENT_DNS_UPSTREAM_UNAVAILABLE, /** Raised if a health check on the configured DNS upstream is failed (raised
with `VpnDnsUpstreamUnavailableEvent`) */
} VpnEvent;
typedef struct {
X509_STORE_CTX *ctx; // SSL context to verify
int result; // FILLED BY HANDLER: operation result (0 in case of success)
} VpnVerifyCertificateEvent;
typedef enum {
/**
* Idle state. VPN client is disconnected and waiting for call to `vpn_connect`.
*/
VPN_SS_DISCONNECTED,
/**
* VPN client is connecting to an endpoint after call to `vpn_connect`.
* If the connection fails, returns to to `VPN_SS_DISCONNECTED` state,
* otherwise to `VPN_SS_CONNECTED` state.
*/
VPN_SS_CONNECTING,
/**
* VPN client is connected to an endpoint.
* If connection to an endpoint is lost with a fatal error, state changes to `VPN_SS_DISCONNECTED`.
* Otherwise, if the connection is lost with a non-fatal error, state changes to `VPN_SS_WAITING_RECOVERY`.
*/
VPN_SS_CONNECTED,
/**
* VPN client is waiting for the next connection recovery attempt.
* If connection to an endpoint is lost with a fatal error, state changes to `VPN_SS_DISCONNECTED`.
* If the connection recovery is initiated successfully, state changes to `VPN_SS_RECOVERING`.
* Otherwise, stays in this state and waits for the next attempt.
*/
VPN_SS_WAITING_RECOVERY,
/**
* VPN client is re-connecting to an endpoint.
* If connection to an endpoint is lost with a fatal error, state changes to `VPN_SS_DISCONNECTED`.
* If the connection recovery succeeds, state changes to `VPN_SS_CONNECTED`.
* Otherwise, returns to `VPN_SS_WAITING_RECOVERY` and waits for the next attempt.
*/
VPN_SS_RECOVERING,
} VpnSessionState;
typedef struct {
ag::VpnError error; // error caused disconnect
uint32_t time_to_next_ms; // time to next recovery attempt
} VpnWaitingRecoveryInfo;
typedef struct {
const VpnEndpoint *endpoint; // the endpoint to which the library is connected
VpnUpstreamProtocol protocol; // the protocol used for this connection
} VpnConnectedInfo;
typedef struct {
const char *location_id; // identifier of the location the VPN client is connected to
VpnSessionState state; // connection state
union {
VpnWaitingRecoveryInfo waiting_recovery_info; // valid if `state` is `VPN_SS_WAITING_RECOVERY`
VpnConnectedInfo connected_info; // valid if `state` is `VPN_SS_CONNECTED`
ag::VpnError error; // valid for any other state
};
} VpnStateChangedEvent;
typedef struct {
int family; // ip family
struct {
size_t chunks_num; // message vector size
const struct iovec *chunks; // message vector
} packet; // note, that it's a single packet which should be sent in one piece
} VpnClientOutputEvent;
typedef enum {
VPN_AT_ADDR, // address contains socket address
VPN_AT_HOST, // address contains host name + port
} VpnAddressType;
typedef struct {
ag::VpnStr name;
uint32_t port;
} VpnHostPort;
typedef struct {
VpnAddressType type;
union {
struct sockaddr_storage addr;
VpnHostPort host;
};
} VpnAddress;
typedef struct {
uint64_t id; // connection id
int proto; // connection protocol
const struct sockaddr *src; // source address of connection
const VpnAddress *dst; // destination address of connection
const char *app_name; // application that initiated the request
} VpnConnectRequestEvent;
typedef struct {
ag::VpnError error; // some error in case the statistics could not be collected
VpnUpstreamProtocol protocol; // endpoint connection protocol
ag::VpnConnectionStats stats; // endpoint connection statistics
} VpnEndpointConnectionStatsEvent;
typedef struct {
const char *upstream; // the upstream address (for now, the same as `VpnListenerConfig.dns_upstream`)
} VpnDnsUpstreamUnavailableEvent;
typedef struct {
void (*func)(void *arg, VpnEvent what, void *data);
void *arg;
} VpnHandler;
/**
* VPN client user-side connections actions
*/
typedef enum {
/**
* Route this connection according to the VPN mode (`VpnMode`) and exclusions
* (`VpnSettings.exclusions`)
*/
VPN_CA_DEFAULT,
/**
* Route this connection to the destination host directly _unconditionally_
*/
VPN_CA_FORCE_BYPASS,
/**
* Route this connection through the VPN endpoint _unconditionally_
*/
VPN_CA_FORCE_REDIRECT,
} VpnConnectAction;
/**
* Defines how client's traffic goes to the target host
*/
typedef enum {
/**
* Route through a VPN endpoint all connections except ones which destinations are in exclusions
*/
VPN_MODE_GENERAL,
/**
* Route through a VPN endpoint only the connections which destinations are in exclusions
*/
VPN_MODE_SELECTIVE,
} VpnMode;
/**
* List of VPN client settings
*/
typedef struct {
VpnHandler handler;
/** The VPN mode (see `VpnMode`) */
VpnMode mode;
/**
* Whitespace-separated list of the domains and addresses which should be routed in a special
* manner
*
* Supported syntax:
*
* `entry1[ entry2]...`
*
* - the list consists of entries which are separated by whitespaces
* - each entry contains:
* - domain name
* - if starts with "*.", any subdomain of the domain will be matched including
* www-subdomain, but not the domain itself (e.g., `*.example.com` will match
* `sub.example.com`, `sub.sub.example.com`, `www.example.com`, but not `example.com`)
* - if starts with "www." or it's just a domain name, the domain itself and its
* www-subdomain will be matched (e.g. `example.com` and `www.example.com` will
* match `example.com` `www.example.com`, but not `sub.example.com`)
* - ip address
* - recognized formats are:
* - [IPv6Address]:port
* - [IPv6Address]
* - IPv6Address
* - IPv4Address:port
* - IPv4Address
* - if port is not specified, any port will be matched
*
* examples:
* - example.org 1.2.3.4
* - 1.1.1.1:1 [feed::beef] www.example.com
* - [deaf::beef]:12 *.example.com
*/
ag::VpnStr exclusions;
/**
* Path to directory where some temporary files (like connection buffers) will be stored.
* If null, temporary files won't be used at all.
*/
const char *tmp_files_base_path;
/**
* Connection in-memory buffer size exceeding which causes storing incoming data in a file.
* If `tmp_files_base_path` is null, takes no effect. Otherwise, if equals 0,
* `VPN_DEFAULT_CONN_MEMORY_BUFFER_THRESHOLD` will be used.
*/
int conn_memory_buffer_threshold;
/**
* Maximum size of file of a connection data buffer.
* If `tmp_files_base_path` is null, takes no effect. Otherwise, if equals 0,
* `VPN_DEFAULT_MAX_CONN_BUFFER_FILE_SIZE` will be used.
*/
int max_conn_buffer_file_size;
/**
* By default QUIC connections are dropped (with the exceptions described below),
* set to true to alter it.
*
* QUIC connections are not dropped in the following cases:
* 1) Connection is bypassed by the application (i.e. it returned `VPN_CA_FORCE_BYPASS` in
* `vpn_complete_connect_request`)
* 2) Connection is redirected to the VPN endpoint forcibly (i.e. the application returned
* `VPN_CA_FORCE_REDIRECT` in `vpn_complete_connect_request`)
*/
bool quic_enabled;
/**
* When disabled, all connection requests are routed directly to target hosts in case session
* to VPN endpoint is lost and the library fails to recover it. This helps not to break
* an Internet connection if user has poor connectivity to an endpoint.
*
* When enabled, incoming connection requests which should be routed through
* an endpoint will not be routed directly in that case.
*/
bool killswitch_enabled;
} VpnSettings;
/**
* Information about a connection to complete its request
*/
typedef struct {
uint64_t id; // connection id
VpnConnectAction action; // what to do with the connection (see `VpnConnectAction`)
const char *appname; // name of the application which initiated connection
int uid; // the application id
} VpnConnectionInfo;
/**
* Defines the behavior in case of the VPN endpoint connection failure during
* the connection establishing procedure
*/
typedef enum {
/// Try to establish the connection for the configured number of times.
/// Go to the `VPN_SS_DISCONNECTED` state in case of all of them failed.
VPN_CRP_SEVERAL_ATTEMPTS,
/// In case of failure go to the `VPN_SS_WAITING_RECOVERY` state and fall
/// into the connection recovery algorithm, just like if the client would
/// lose the connection while were connected
VPN_CRP_FALL_INTO_RECOVERY,
} VpnConnectRetyPolicy;
typedef struct {
/// Defines the failure handling algorithm
VpnConnectRetyPolicy policy;
union {
/// Valid for `VPN_CRP_SEVERAL_ATTEMPTS`.
/// Number of the connection attempts
/// (if <= 0, `VPN_DEFAULT_CONNECT_ATTEMPTS_NUM` will be used).
int attempts_num;
};
} VpnConnectRetryInfo;
typedef struct {
/// VPN upstream configuration
VpnUpstreamConfig upstream_config;
/// Defines the failure handling algorithm
VpnConnectRetryInfo retry_info;
} VpnConnectParameters;
extern "C" {
/**
* Open VPN client
* @param settings VPN client settings
* @return null if failed, some instance otherwise
*/
WIN_EXPORT Vpn *vpn_open(const VpnSettings *settings);
/**
* Proceed connection to a VPN server and check if communication with the server is possible
* @param vpn VPN client
* @param parameters VPN connect parameters
* @return `VpnError` filled with `VpnErrorCode`
*/
WIN_EXPORT ag::VpnError vpn_connect(Vpn *vpn, const VpnConnectParameters *parameters);
/**
* Forcibly initiate the next session recovery attempt. Do nothing if the VPN client is not between
* recovery attempts (e.g., not in `VPN_SS_WAITING_RECOVERY` state).
* @param vpn VPN client
*/
WIN_EXPORT void vpn_force_reconnect(Vpn *vpn);
/**
* Start listening for client connections.
* MUST be called after `vpn_connect`.
* Please note that it may not be acceptable to run several instances simultaneously for some
* listener configurations. It's up to an application to ensure that only a single instance is
* listening at the moment or several listening instances have the compatible configurations.
* @param vpn VPN client
* @param listener The specific listener to listen with.
* See `vpn_create_tun_listener()`, `vpn_create_socks_listener()`.
* This function always takes ownership of the listener, regardless of the return value.
* @param config The config common to all listener types.
*/
WIN_EXPORT ag::VpnError vpn_listen(Vpn *vpn, VpnListener *listener, const VpnListenerConfig *config);
/**
* Stop VPN client.
* MUST be called if either `vpn_connect` or `vpn_listen` was called previously.
* MUST NOT be called in event handlers without a context switch (i.e., it's not safe to call
* `vpn_stop` directly, for example, in disconnected event handler).
* @param vpn VPN client
*/
WIN_EXPORT void vpn_stop(Vpn *vpn);
/**
* Close VPN client
* @param vpn VPN client
*/
WIN_EXPORT void vpn_close(Vpn *vpn);
/**
* Return the address that the SOCKS listener is bound to.
* The returned address is valid only if this function is called
* after `vpn_listen()` has completed successfully with a result
* of `vpn_create_socks_listener()` as an argument.
*/
WIN_EXPORT sockaddr_storage vpn_get_socks_listener_address(Vpn *vpn);
/**
* Clone common listener config.
* @return cloned config (must be freed by caller via `vpn_listener_config_destroy()`)
*/
WIN_EXPORT VpnListenerConfig vpn_listener_config_clone(const VpnListenerConfig *config);
/** Destroy cloned common listener config. */
WIN_EXPORT void vpn_listener_config_destroy(VpnListenerConfig *config);
typedef AG_ARRAY_OF(evbuffer_iovec) VpnPackets;
/**
* Pass data packets received from a client application to the client listener in case it
* doesn't listen for incoming data by itself
* @param vpn VPN client
* @param packets packets
*/
void vpn_process_client_packets(Vpn *vpn, VpnPackets packets);
/**
* Complete connect request according to action.
* MAY be called from `VPN_EVENT_CONNECT_REQUEST` handler without a context switch.
* @param vpn VPN client
* @param info connection info
*/
WIN_EXPORT void vpn_complete_connect_request(Vpn *vpn, const VpnConnectionInfo *info);
/**
* Update the VPN exclusion settings. This also resets all client connections without restarting vpn client.
* @param vpn VPN client
* @param mode the VPN mode
* @param exclusions the exclusions list (see `VpnSettings.exclusions`)
*/
WIN_EXPORT void vpn_update_exclusions(Vpn *vpn, VpnMode mode, ag::VpnStr exclusions);
/**
* Reset all connection with given application UID
* @param vpn VPN client
* @param uid an application UID. Use -1 to reset all connections.
*/
WIN_EXPORT void vpn_reset_connections(Vpn *vpn, int uid);
/**
* Notify the instance of a network change.
* @param vpn VPN client
* @param network_loss_suspected true if it's possible that the network through which the library
* was connected to an endpoint was lost
*/
void vpn_notify_network_change(Vpn *vpn, bool network_loss_suspected);
/**
* Request the instance for an endpoint connection statistics.
* The statistics is raised with `VPN_EVENT_ENDPOINT_CONNECTION_STATS` event.
*/
WIN_EXPORT void vpn_request_endpoint_connection_stats(Vpn *vpn);
/**
* Notify the instance that the system is going to sleep.
* The completion handler will be called when the instance is ready for sleeping.
* The completion handler will always be called, even if the the instance is destroyed
* after receiving the notification.
* @param vpn VPN client
* @param completion_handler ready callback, must NOT be NULL
* @param arg ready callback argument
*/
void vpn_notify_sleep(Vpn *vpn, void (*completion_handler)(void *arg), void *arg);
/**
* Notify the instance that the system has just waked up from sleep.
* @param vpn VPN client
*/
void vpn_notify_wake(Vpn *vpn);
typedef enum {
VPN_EVS_OK, // an entry is valid
VPN_EVS_MALFORMED, // an entry is not valid
} VpnExclusionValidationStatus;
/**
* Check if a string is a valid exclusion
* @param text string to check
* @return see `VpnExclusionValidationStatus`
*/
WIN_EXPORT VpnExclusionValidationStatus vpn_validate_exclusion(const char *text);
using VpnDnsUpstreamValidationStatus = enum {
VPN_DUVS_OK, // an upstream is valid
VPN_DUVS_MALFORMED, // an upstream is not valid
};
/**
* Check if a string as a valid DNS resolver address
* @param address the DNS resolver address (see `VpnListenerConfig#dns_upstream`'s description for details)
* @return see `VpnDnsUpstreamValidationStatus`
*/
WIN_EXPORT VpnDnsUpstreamValidationStatus vpn_validate_dns_upstream(const char *address);
/**
* Return the event loop that the VPN instance is running in.
* If the event loop has not been started yet or has already been stopped, return NULL.
*/
WIN_EXPORT ag::VpnEventLoop *vpn_get_event_loop(Vpn *vpn);
/**
* If connected, mark the current endpoint unavailable and start recovery, otherwise do nothing.
*/
WIN_EXPORT void vpn_abandon_current_endpoint(Vpn *vpn);
/** Create a TUN interface listener. */
VpnListener *vpn_create_tun_listener(Vpn *vpn, const VpnTunListenerConfig *config);
/** Create a SOCKS listener. */
WIN_EXPORT VpnListener *vpn_create_socks_listener(Vpn *vpn, const VpnSocksListenerConfig *config);
} // extern "C"
} // namespace ag
+543
View File
@@ -0,0 +1,543 @@
#include "direct_upstream.h"
#include <event2/util.h>
#include "common/net_utils.h"
#include "net/dns_manager.h"
#include "net/utils.h"
#include "vpn/internal/vpn_client.h"
#include "vpn/utils.h"
#define log_upstream(ups_, lvl_, fmt_, ...) lvl_##log((ups_)->m_log, "[{}] " fmt_, (ups_)->id, ##__VA_ARGS__)
#define log_conn(ups_, cid_, lvl_, fmt_, ...) \
lvl_##log((ups_)->m_log, "[{}] [R:{}] " fmt_, (ups_)->id, (uint64_t) (cid_), ##__VA_ARGS__)
namespace ag {
struct SocketContext {
DirectUpstream *upstream = nullptr;
uint64_t conn_id = NON_ID;
};
struct IcmpSocketContext {
DirectUpstream *upstream = nullptr;
sockaddr_storage peer = {};
IcmpRequestKey key = {};
uint16_t seqno = 0;
[[nodiscard]] IcmpEchoReply make_reply_template() const {
return {this->peer, this->key.id, this->seqno};
}
};
struct IcmpRequestAttempt {
TcpSocketPtr socket;
std::unique_ptr<IcmpSocketContext> context;
};
struct DirectUpstream::IcmpRequestInfo {
std::vector<IcmpRequestAttempt> tries;
};
static constexpr uint16_t ICMP_PING_EMULATION_PORT = 443;
DirectUpstream::DirectUpstream(int id)
: ServerUpstream(id) {
}
DirectUpstream::~DirectUpstream() = default;
bool DirectUpstream::init(VpnClient *vpn, SeverHandler handler) {
if (!this->ServerUpstream::init(vpn, handler)) {
log_upstream(this, err, "Failed to initialize base upstream");
deinit();
return false;
}
return true;
}
void DirectUpstream::deinit() {
}
bool DirectUpstream::open_session(uint32_t) {
this->handler.func(this->handler.arg, SERVER_EVENT_SESSION_OPENED, nullptr);
return true;
}
void DirectUpstream::close_session() {
log_upstream(this, dbg, "...");
while (!m_tcp_connections.empty()) {
close_connection(m_tcp_connections.begin()->first, false);
}
while (!m_udp_connections.empty()) {
close_connection(m_udp_connections.begin()->first, false);
}
log_upstream(this, dbg, "Done");
}
void DirectUpstream::tcp_socket_handler(void *arg, TcpSocketEvent what, void *data) {
auto *ctx = (SocketContext *) arg;
DirectUpstream *upstream = ctx->upstream;
switch (what) {
case TCP_SOCKET_EVENT_CONNECTED: {
log_conn(upstream, ctx->conn_id, dbg, "Connected to remote host successfully");
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_CONNECTION_OPENED, &ctx->conn_id);
break;
}
case TCP_SOCKET_EVENT_READ: {
auto *sock_event = (TcpSocketReadEvent *) data;
if (sock_event->length == 0) {
log_conn(upstream, ctx->conn_id, dbg, "Got EOF from remote host");
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &ctx->conn_id);
upstream->m_tcp_connections.erase(ctx->conn_id);
} else {
log_conn(upstream, ctx->conn_id, trace, "Got {} bytes from remote host", sock_event->length);
ServerReadEvent serv_event = {ctx->conn_id, sock_event->data, sock_event->length, 0};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_READ, &serv_event);
if (serv_event.result >= 0) {
sock_event->processed = serv_event.result;
} else {
upstream->close_connection(ctx->conn_id, false);
}
}
break;
}
case TCP_SOCKET_EVENT_SENT: {
const TcpSocketSentEvent *sock_event = (TcpSocketSentEvent *) data;
ServerDataSentEvent serv_event = {ctx->conn_id, sock_event->bytes};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_DATA_SENT, &serv_event);
break;
}
case TCP_SOCKET_EVENT_ERROR: {
const VpnError *sock_event = (VpnError *) data;
log_conn(upstream, ctx->conn_id, dbg, "Error event on socket: {} ({})", sock_event->text, sock_event->code);
ServerError serv_event = {ctx->conn_id, *sock_event};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &serv_event);
upstream->m_tcp_connections.erase(ctx->conn_id);
break;
}
case TCP_SOCKET_EVENT_WRITE_FLUSH: {
// do nothing
break;
}
case TCP_SOCKET_EVENT_PROTECT: {
vpn_client::Handler *vpn_handler = &upstream->vpn->parameters.handler;
vpn_handler->func(vpn_handler->arg, vpn_client::EVENT_PROTECT_SOCKET, data);
break;
}
}
}
void DirectUpstream::udp_socket_handler(void *arg, UdpSocketEvent what, void *data) {
auto *ctx = (SocketContext *) arg;
DirectUpstream *upstream = ctx->upstream;
switch (what) {
case UDP_SOCKET_EVENT_PROTECT: {
vpn_client::Handler *vpn_handler = &upstream->vpn->parameters.handler;
vpn_handler->func(vpn_handler->arg, vpn_client::EVENT_PROTECT_SOCKET, data);
break;
}
case UDP_SOCKET_EVENT_TIMEOUT: {
ServerError event = {
ctx->conn_id, {ag::utils::AG_ETIMEDOUT, evutil_socket_error_to_string(ag::utils::AG_ETIMEDOUT)}};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &event);
upstream->m_udp_connections.erase(ctx->conn_id);
break;
}
case UDP_SOCKET_EVENT_READ: {
auto *sock_event = (UdpSocketReadEvent *) data;
auto it = upstream->m_udp_connections.find(ctx->conn_id);
if (it == upstream->m_udp_connections.end()) {
log_conn(upstream, ctx->conn_id, dbg, "Read on closed connection");
sock_event->closed = true;
break;
}
if (!it->second.read_enabled) {
log_conn(upstream, ctx->conn_id, dbg, "Dropping packet as read disabled ({} bytes)", sock_event->length);
break;
}
ServerReadEvent event = {ctx->conn_id, sock_event->data, sock_event->length, 0};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_READ, &event);
break;
}
}
}
uint64_t DirectUpstream::open_tcp_connection(const TunnelAddressPair *addr) {
uint64_t id = this->vpn->upstream_conn_id_generator.get();
std::unique_ptr<SocketContext> ctx = std::make_unique<SocketContext>(SocketContext{this, id});
TcpSocketParameters params = {
.ev_loop = this->vpn->parameters.ev_loop,
.handler = {tcp_socket_handler, ctx.get()},
.timeout_ms = VPN_DEFAULT_TCP_TIMEOUT_MS,
.socket_manager = this->vpn->parameters.network_manager->socket,
};
if (this->vpn->tmp_files_base_path.has_value()) {
params.read_threshold = this->vpn->conn_memory_buffer_threshold;
}
TcpSocketPtr sock{tcp_socket_create(&params)};
if (sock == nullptr) {
return NON_ID;
}
TcpSocketConnectParameters param = {};
if (const sockaddr_storage *dst = std::get_if<sockaddr_storage>(&addr->dst); dst != nullptr) {
param = {TCP_SOCKET_CB_ADDR, .by_addr = {(sockaddr *) dst}};
} else if (const NamePort *dst = std::get_if<NamePort>(&addr->dst); dst != nullptr) {
param = {TCP_SOCKET_CB_HOSTNAME, .by_name = {this->vpn->parameters.dns_base, dst->name.c_str(), dst->port}};
} else {
log_upstream(this, err, "Empty destination address");
assert(0);
return NON_ID;
}
if (0 != tcp_socket_connect(sock.get(), &param).code) {
return NON_ID;
}
TcpConnection *conn = &m_tcp_connections[id];
conn->sock_ctx = std::move(ctx);
conn->socket = std::move(sock);
return id;
}
uint64_t DirectUpstream::open_udp_connection(const TunnelAddressPair *addr) {
uint64_t id = this->vpn->upstream_conn_id_generator.get();
std::unique_ptr<SocketContext> ctx = std::make_unique<SocketContext>(SocketContext{this, id});
UdpSocketParameters params = {this->vpn->parameters.ev_loop, {udp_socket_handler, ctx.get()},
VPN_DEFAULT_UDP_TIMEOUT_MS, *std::get_if<sockaddr_storage>(&addr->dst),
this->vpn->parameters.network_manager->socket};
UdpSocketPtr socket{udp_socket_create(&params)};
if (socket == nullptr) {
log_upstream(this, err, "Failed to create socket");
return NON_ID;
}
UdpConnection *conn = &m_udp_connections[id];
conn->sock_ctx = std::move(ctx);
conn->socket = std::move(socket);
conn->open_task_id = ag::submit(vpn->parameters.ev_loop,
{
new SocketContext{this, id},
[](void *arg, TaskId) {
auto *ctx = (SocketContext *) arg;
DirectUpstream *upstream = ctx->upstream;
if (auto i = upstream->m_udp_connections.find(ctx->conn_id);
i != upstream->m_udp_connections.end()) {
upstream->handler.func(
upstream->handler.arg, SERVER_EVENT_CONNECTION_OPENED, &ctx->conn_id);
UdpConnection *conn = &i->second;
conn->open_task_id.release();
}
},
[](void *arg) {
delete (SocketContext *) arg;
},
});
return id;
}
uint64_t DirectUpstream::open_connection(const TunnelAddressPair *addr, int proto, std::string_view app_name) {
(void) app_name;
uint64_t id = NON_ID;
if (proto == IPPROTO_TCP) {
id = open_tcp_connection(addr);
} else if (proto == IPPROTO_UDP) {
id = open_udp_connection(addr);
} else {
log_upstream(this, err, "Unknown protocol: {}", proto);
assert(0);
}
return id;
}
void DirectUpstream::close_connection(uint64_t id, bool graceful) {
if (m_tcp_connections.count(id) == 0 && m_udp_connections.count(id) == 0) {
// already raised in EOF event
return;
}
log_conn(this, id, dbg, "Closing");
if (auto i = m_tcp_connections.find(id); i != m_tcp_connections.end()) {
TcpConnection *conn = &i->second;
if (!graceful && conn->socket != nullptr) {
tcp_socket_set_rst(conn->socket.get());
}
m_tcp_connections.erase(i);
} else if (auto i = m_udp_connections.find(id); i != m_udp_connections.end()) {
m_udp_connections.erase(i);
}
this->handler.func(this->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &id);
}
void DirectUpstream::close_connection(uint64_t id, bool graceful, bool async) {
if (!async) {
close_connection(id, graceful);
return;
}
struct CloseCtx {
DirectUpstream *upstream;
uint64_t id;
bool graceful;
};
Connection *conn = nullptr;
if (auto i = m_tcp_connections.find(id); i != m_tcp_connections.end()) {
conn = &i->second;
} else if (auto i = m_udp_connections.find(id); i != m_udp_connections.end()) {
conn = &i->second;
} else {
this->handler.func(this->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &id);
return;
}
conn->close_task_id = ag::submit(vpn->parameters.ev_loop,
{
new CloseCtx{this, id, graceful},
[](void *arg, TaskId) {
auto *ctx = (CloseCtx *) arg;
ctx->upstream->close_connection(ctx->id, ctx->graceful);
},
[](void *arg) {
delete (CloseCtx *) arg;
},
});
}
ssize_t DirectUpstream::send(uint64_t id, const uint8_t *data, size_t length) {
VpnError error = {};
if (auto i = m_tcp_connections.find(id); i != m_tcp_connections.end()) {
TcpConnection *conn = &i->second;
error = tcp_socket_write(conn->socket.get(), data, length);
} else if (auto i = m_udp_connections.find(id); i != m_udp_connections.end()) {
UdpConnection *conn = &i->second;
error = udp_socket_write(conn->socket.get(), data, length);
} else {
log_conn(this, id, dbg, "Not found");
}
if (error.code == 0) {
return length;
}
log_conn(this, id, dbg, "Failed to send data: {} ({})", safe_to_string_view(error.text), error.code);
return -1;
}
void DirectUpstream::consume(uint64_t id, size_t length) {
// do nothing
}
size_t DirectUpstream::available_to_send(uint64_t id) {
if (auto i = m_tcp_connections.find(id); i != m_tcp_connections.cend()) {
const TcpConnection *conn = &i->second;
return tcp_socket_available_to_write(conn->socket.get());
}
if (auto i = m_udp_connections.find(id); i != m_udp_connections.end()) {
return UDP_MAX_DATAGRAM_SIZE;
}
return 0;
}
void DirectUpstream::update_flow_control(uint64_t id, TcpFlowCtrlInfo info) {
auto i = m_tcp_connections.find(id);
if (i != m_tcp_connections.end()) {
TcpConnection *conn = &i->second;
tcp_socket_set_read_enabled(conn->socket.get(), info.send_buffer_size > 0);
return;
}
auto j = m_udp_connections.find(id);
if (j != m_udp_connections.end()) {
UdpConnection *conn = &j->second;
conn->read_enabled = info.send_buffer_size > 0;
}
}
VpnError DirectUpstream::do_health_check() {
assert(0);
return {VPN_EC_ERROR, "Not implemented"};
}
VpnConnectionStats DirectUpstream::get_connection_stats() const {
assert(0);
return {};
}
void DirectUpstream::on_icmp_request(IcmpEchoRequestEvent &event) {
auto ctx = std::make_unique<IcmpSocketContext>(IcmpSocketContext{
.upstream = this,
.peer = event.request.peer,
.key = IcmpRequestKey::make(event.request),
.seqno = event.request.seqno,
});
TcpSocketParameters params = {
.ev_loop = this->vpn->parameters.ev_loop,
.handler = {icmp_socket_handler, ctx.get()},
.timeout_ms = VPN_DEFAULT_TCP_TIMEOUT_MS,
.socket_manager = this->vpn->parameters.network_manager->socket,
};
TcpSocketPtr sock{tcp_socket_create(&params)};
if (sock == nullptr) {
event.result = -1;
return;
}
sockaddr_storage peer = event.request.peer;
sockaddr_set_port((sockaddr *) &peer, ICMP_PING_EMULATION_PORT);
TcpSocketConnectParameters param = {TCP_SOCKET_CB_ADDR, .by_addr = {(sockaddr *) &peer}};
if (0 != tcp_socket_connect(sock.get(), &param).code) {
event.result = -1;
return;
}
auto &info = m_icmp_requests[ctx->key];
if (info == nullptr) {
info = std::make_unique<IcmpRequestInfo>();
}
info->tries.emplace_back(IcmpRequestAttempt{std::move(sock), std::move(ctx)});
}
void DirectUpstream::cancel_icmp_request(const IcmpRequestKey &key, uint16_t seqno) {
auto request_it = m_icmp_requests.find(key);
if (request_it == m_icmp_requests.end()) {
log_upstream(this, trace, "Request is not found: id={} seqno={}", key.id, seqno);
return;
}
auto &info = *request_it->second;
auto try_it = std::find_if(info.tries.begin(), info.tries.end(), [seqno](const IcmpRequestAttempt &i) {
return i.context->seqno == seqno;
});
if (try_it == info.tries.end()) {
log_upstream(this, trace, "Request try is not found: id={} seqno={}", key.id, seqno);
return;
}
info.tries.erase(try_it);
if (info.tries.empty()) {
m_icmp_requests.erase(request_it);
}
}
static void update_reply_on_error_v4(IcmpEchoReply &reply, int code) {
switch (code) {
case AG_EHOSTUNREACH:
reply.type = ICMP_MT_DESTINATION_UNREACHABLE;
reply.code = ICMP_DUC_HOST_UNREACH;
break;
case AG_ENETUNREACH:
reply.type = ICMP_MT_DESTINATION_UNREACHABLE;
reply.code = ICMP_DUC_NET_UNREACH;
break;
case ag::utils::AG_ETIMEDOUT:
reply.type = ICMP_MT_TIME_EXCEEDED;
reply.code = ICMP_TEC_TTL;
break;
default:
reply.type = ICMP_MT_DROP;
break;
}
}
static void update_reply_on_error_v6(IcmpEchoReply &reply, int code) {
switch (code) {
case AG_EHOSTUNREACH:
reply.type = ICMPV6_MT_DESTINATION_UNREACHABLE;
reply.code = ICMPV6_DUC_ADDRESS_UNREACH;
break;
case AG_ENETUNREACH:
reply.type = ICMPV6_MT_DESTINATION_UNREACHABLE;
reply.code = ICMPV6_DUC_NO_ROUTE;
break;
case ag::utils::AG_ETIMEDOUT:
reply.type = ICMPV6_MT_TIME_EXCEEDED;
reply.code = ICMPV6_TEC_HOP;
break;
default:
reply.type = ICMP_MT_DROP;
break;
}
}
void DirectUpstream::icmp_socket_handler(void *arg, TcpSocketEvent what, void *data) {
auto *ctx = (IcmpSocketContext *) arg;
DirectUpstream *self = ctx->upstream;
std::optional<IcmpEchoReply> reply;
switch (what) {
case TCP_SOCKET_EVENT_CONNECTED:
reply = ctx->make_reply_template();
reply->type = (ctx->peer.ss_family == AF_INET) ? ICMP_MT_ECHO_REPLY : ICMPV6_MT_ECHO_REPLY;
break;
case TCP_SOCKET_EVENT_ERROR:
reply = ctx->make_reply_template();
if (const VpnError *error = (VpnError *) data; reply->peer.ss_family == AF_INET) {
update_reply_on_error_v4(reply.value(), error->code);
} else {
update_reply_on_error_v6(reply.value(), error->code);
}
break;
case TCP_SOCKET_EVENT_PROTECT: {
vpn_client::Handler *vpn_handler = &self->vpn->parameters.handler;
vpn_handler->func(vpn_handler->arg, vpn_client::EVENT_PROTECT_SOCKET, data);
break;
}
case TCP_SOCKET_EVENT_READ:
case TCP_SOCKET_EVENT_SENT:
case TCP_SOCKET_EVENT_WRITE_FLUSH:
log_upstream(self, dbg, "Unexpected event: {}", magic_enum::enum_name(what));
assert(0);
reply = ctx->make_reply_template();
if (reply->peer.ss_family == AF_INET) {
update_reply_on_error_v4(reply.value(), ag::utils::AG_ECONNREFUSED);
} else {
update_reply_on_error_v6(reply.value(), ag::utils::AG_ECONNREFUSED);
}
break;
}
if (reply.has_value()) {
self->handler.func(self->handler.arg, SERVER_EVENT_ECHO_REPLY, &reply);
self->cancel_icmp_request(ctx->key, ctx->seqno);
}
}
} // namespace ag
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <chrono>
#include <memory>
#include <unordered_map>
#include <vector>
#include "common/logger.h"
#include "net/tcp_socket.h"
#include "net/udp_socket.h"
#include "vpn/internal/server_upstream.h"
namespace ag {
struct SocketContext;
class DirectUpstream : public ServerUpstream {
public:
explicit DirectUpstream(int id);
~DirectUpstream() override;
DirectUpstream(const DirectUpstream &) = delete;
DirectUpstream(DirectUpstream &&) = delete;
DirectUpstream operator=(const DirectUpstream &) = delete;
DirectUpstream operator=(DirectUpstream &&) = delete;
private:
struct Connection {
std::unique_ptr<SocketContext> sock_ctx;
ag::AutoTaskId close_task_id;
};
struct TcpConnection : public Connection {
TcpSocketPtr socket;
};
struct UdpConnection : public Connection {
UdpSocketPtr socket;
bool read_enabled = false;
ag::AutoTaskId open_task_id;
};
struct IcmpRequestInfo;
std::unordered_map<uint64_t, TcpConnection> m_tcp_connections;
std::unordered_map<uint64_t, UdpConnection> m_udp_connections;
std::map<IcmpRequestKey, std::unique_ptr<IcmpRequestInfo>> m_icmp_requests;
ag::Logger m_log{"DIRECT_UPSTREAM"};
bool init(VpnClient *vpn, SeverHandler handler) override;
void deinit() override;
bool open_session(uint32_t timeout_ms) override;
void close_session() override;
uint64_t open_connection(const TunnelAddressPair *addr, int proto, std::string_view app_name) override;
void close_connection(uint64_t id, bool graceful, bool async) override;
ssize_t send(uint64_t id, const uint8_t *data, size_t length) override;
void consume(uint64_t id, size_t length) override;
size_t available_to_send(uint64_t id) override;
void update_flow_control(uint64_t id, TcpFlowCtrlInfo info) override;
VpnError do_health_check() override;
[[nodiscard]] VpnConnectionStats get_connection_stats() const override;
void on_icmp_request(IcmpEchoRequestEvent &event) override;
static void tcp_socket_handler(void *arg, TcpSocketEvent what, void *data);
static void udp_socket_handler(void *arg, UdpSocketEvent what, void *data);
static void icmp_socket_handler(void *arg, TcpSocketEvent what, void *data);
uint64_t open_tcp_connection(const TunnelAddressPair *addr);
uint64_t open_udp_connection(const TunnelAddressPair *addr);
void close_connection(uint64_t id, bool graceful);
void cancel_icmp_request(const IcmpRequestKey &key, uint16_t seqno);
};
} // namespace ag
+142
View File
@@ -0,0 +1,142 @@
#include "vpn/internal/dns_proxy_accessor.h"
// including it after the VPN client's headers leads to conflicts
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#define NOCRYPT
#undef gettid
#include "proxy/dnsproxy.h"
#include "common/logger.h"
#define log_accessor(r_, lvl_, fmt_, ...) lvl_##log((r_)->m_log, fmt_, ##__VA_ARGS__)
using namespace std::chrono;
namespace ag {
static constexpr std::string_view BOOTSTRAP_ADDRESSES[] = {
"94.140.14.140:53",
};
static DnsProxySettings make_dns_proxy_settings(const DnsProxyAccessor::Parameters &parameters, milliseconds timeout) {
DnsProxySettings settings = DnsProxySettings::get_default();
settings.upstreams = {{
.address = parameters.resolver_address,
.bootstrap = {std::begin(BOOTSTRAP_ADDRESSES), std::end(BOOTSTRAP_ADDRESSES)},
.timeout = timeout,
.id = 0,
}};
settings.fallbacks = {};
settings.listeners = {
{
.address = "127.0.0.1",
.port = 0,
.protocol = ag::utils::TP_UDP,
},
{
.address = "127.0.0.1",
.port = 0,
.protocol = ag::utils::TP_TCP,
},
};
settings.outbound_proxy = {{
.protocol = OutboundProxyProtocol::SOCKS5_UDP,
.address = sockaddr_ip_to_str((sockaddr *) &parameters.socks_listener_address),
.port = sockaddr_get_port((sockaddr *) &parameters.socks_listener_address),
.auth_info = std::nullopt,
.trust_any_certificate = false,
.ignore_if_unavailable = false,
}};
settings.ipv6_available = parameters.ipv6_available;
return settings;
}
void delete_dnsproxy(DnsProxy *p) {
delete p;
}
DnsProxyAccessor::DnsProxyAccessor(Parameters p)
: m_dns_proxy(new DnsProxy())
, m_parameters(std::move(p)) {
}
bool DnsProxyAccessor::start(std::chrono::milliseconds timeout) {
auto [ok, msg] = m_dns_proxy->init(make_dns_proxy_settings(m_parameters, timeout),
{
.on_request_processed = nullptr,
.on_certificate_verification =
[this](CertificateVerificationEvent e) -> std::optional<std::string> {
const unsigned char *d = e.certificate.data();
DeclPtr<X509_STORE_CTX, &X509_STORE_CTX_free> store{X509_STORE_CTX_new()};
DeclPtr<X509, &X509_free> cert{
d2i_X509(nullptr, (const unsigned char **) &d, (long) e.certificate.size())};
X509_STORE_CTX_set_cert(store.get(), cert.get());
STACK_OF(X509) *chain = sk_X509_new_null();
for (const std::vector<uint8_t> &c : e.chain) {
d = c.data();
sk_X509_push(chain, d2i_X509(nullptr, (const unsigned char **) &d, (long) c.size()));
}
X509_STORE_CTX_set_chain(store.get(), chain);
int verify_result = m_parameters.cert_verify_handler.func(
// server name and ip are already verified by the DNS proxy
nullptr, nullptr, store.get(), m_parameters.cert_verify_handler.arg);
sk_X509_pop_free(chain, &X509_free);
return (verify_result > 0) ? std::nullopt : std::make_optional("Verification failed");
},
});
if (!ok) {
if (msg.has_value()) {
log_accessor(this, err, "Failed to initialize DNS proxy: {}", *msg);
}
return false;
}
if (msg.has_value()) {
log_accessor(this, warn, "DNS proxy initialization warning: {}", *msg);
}
const auto &settings = m_dns_proxy->get_settings();
for (const auto &listener : settings.listeners) {
switch (listener.protocol) {
case utils::TP_UDP:
m_dns_proxy_udp_listen_address = sockaddr_from_str(listener.address.c_str());
sockaddr_set_port((sockaddr *) &m_dns_proxy_udp_listen_address, listener.port);
break;
case utils::TP_TCP:
m_dns_proxy_tcp_listen_address = sockaddr_from_str(listener.address.c_str());
sockaddr_set_port((sockaddr *) &m_dns_proxy_tcp_listen_address, listener.port);
break;
}
}
if (m_dns_proxy_udp_listen_address.ss_family == AF_UNSPEC
|| m_dns_proxy_tcp_listen_address.ss_family == AF_UNSPEC) {
log_accessor(this, err, "DNS proxy is not listening for queries over {}",
(m_dns_proxy_udp_listen_address.ss_family == AF_UNSPEC) ? "UDP" : "TCP");
return false;
}
return true;
}
void DnsProxyAccessor::stop() {
if (m_dns_proxy != nullptr) {
m_dns_proxy->deinit();
}
}
const sockaddr_storage &DnsProxyAccessor::get_listen_address(int proto) const {
return (proto == IPPROTO_UDP) ? m_dns_proxy_udp_listen_address : m_dns_proxy_tcp_listen_address;
}
} // namespace ag
+43
View File
@@ -0,0 +1,43 @@
#include "vpn/internal/dns_sniffer.h"
#include "net/dns_utils.h"
#include "vpn/internal/tunnel.h"
#define log_sniffer(p_, lvl_, fmt_, ...) lvl_##log((p_)->m_log, fmt_, ##__VA_ARGS__)
namespace ag {
void DnsSniffer::init(const DnsSnifferParameters &p) {
m_parameters = p;
}
void DnsSniffer::on_intercepted_dns_reply(U8View data, bool library_request) {
dns_utils::DecodeResult r = dns_utils::decode_packet(data);
if (const auto *e = std::get_if<dns_utils::Error>(&r); e != nullptr) {
log_sniffer(this, trace, "Failed to parse reply: {}", e->description);
return;
}
const auto *answer = std::get_if<dns_utils::DecodedReply>(&r);
if (answer == nullptr) {
return;
}
bool found_exclusion = false;
for (const std::string &name : answer->names) {
found_exclusion = DFMS_EXCLUSION == m_parameters.filter->match_domain(name);
if (found_exclusion) {
log_sniffer(this, dbg, "Domain name ({}) is excluded, adding its addresses as suspects", name);
break;
}
}
if (found_exclusion) {
for (const dns_utils::AnswerAddress &addr : answer->addresses) {
m_parameters.filter->add_exclusion_suspect(sockaddr_from_raw(addr.ip.data(), addr.ip.size(), 0),
library_request ? std::max(addr.ttl, Tunnel::EXCLUSIONS_RESOLVE_PERIOD) : addr.ttl);
}
}
}
} // namespace ag
+182
View File
@@ -0,0 +1,182 @@
#include "vpn/internal/domain_filter.h"
#include "vpn/internal/utils.h"
#include "vpn/utils.h"
#define log_filter(f_, lvl_, fmt_, ...) lvl_##log((f_)->m_log, fmt_, ##__VA_ARGS__)
namespace ag {
static constexpr std::string_view WWW_PREFIX = "www.";
static constexpr std::string_view WILDCARD_PREFIX = "*.";
enum DomainFilter::MatchFlags : uint32_t {
DFMM_EXACT, // match the domain itself and www. + the domain
DFMM_SUBDOMAINS, // match only subdomains and www. + the domain, but not the domain itself
};
DomainFilter::DomainFilter() = default;
DomainFilter::~DomainFilter() = default;
DomainFilterValidationStatus DomainFilter::validate_entry(const std::string &entry) {
ParseResult result = parse_entry(entry);
if (const auto *status = std::get_if<DomainFilterValidationStatus>(&result);
status != nullptr && *status != DFVS_OK) {
return *status;
}
return DFVS_OK;
}
DomainFilter::ParseResult DomainFilter::parse_entry(const std::string &entry) {
if (sockaddr_storage addr = sockaddr_from_str(entry.c_str()); addr.ss_family != AF_UNSPEC) {
return addr;
}
if (entry.npos == entry.find_first_of(":/")) {
MatchFlagsSet match_flags;
std::string_view prepared_entry = entry;
if (starts_with(prepared_entry, WILDCARD_PREFIX)) {
prepared_entry.remove_prefix(WILDCARD_PREFIX.length());
match_flags.set(DFMM_SUBDOMAINS);
} else {
match_flags.set(DFMM_EXACT);
if (starts_with(prepared_entry, WWW_PREFIX)) {
prepared_entry.remove_prefix(WWW_PREFIX.length());
}
}
return DomainEntryInfo{std::string(prepared_entry), match_flags};
}
return DFVS_MALFORMED;
}
bool DomainFilter::update_exclusions(VpnMode mode_, std::string_view exclusions) {
m_mode = mode_;
m_domains.clear();
m_addresses.clear();
m_exclusion_suspects.clear();
auto add_entry = [this](const std::string &entry) {
ParseResult result = parse_entry(entry);
if (const auto *addr = std::get_if<sockaddr_storage>(&result); addr != nullptr) {
log_filter(this, trace, "Entry added in address table: {}", entry);
m_addresses.insert(*addr);
} else if (auto *domain_info = std::get_if<DomainEntryInfo>(&result); domain_info != nullptr) {
log_filter(this, trace, "Entry added in domain table: {}", domain_info->text);
m_domains[std::move(domain_info->text)] |= domain_info->flags;
} else {
auto status = std::get<DomainFilterValidationStatus>(result);
log_filter(this, warn, "Malformed entry detected in exceptions list: {} ({})", entry,
magic_enum::enum_name(status));
}
};
std::string buffer;
for (char ch : exclusions) {
if (!isspace(ch)) {
buffer.push_back(ch);
} else if (!buffer.empty()) {
add_entry(buffer);
buffer.clear();
}
}
if (!buffer.empty()) {
add_entry(buffer);
}
return true;
}
DomainFilterMatchStatus DomainFilter::match_domain(std::string_view domain) const {
bool www_prefixed = starts_with(domain, WWW_PREFIX);
std::string seek = !www_prefixed ? std::string(domain) : std::string(domain.substr(WWW_PREFIX.length()));
while (true) {
auto i = m_domains.find(seek);
if (i != m_domains.end()) {
MatchFlagsSet flags = i->second;
bool found = (flags.test(DFMM_EXACT)
&& (seek.length() == domain.length()
|| (www_prefixed && seek.length() + WWW_PREFIX.length() == domain.length())))
|| (flags.test(DFMM_SUBDOMAINS) && (seek.length() < domain.length()));
if (found) {
log_filter(this, dbg, "Matched domain: {}", seek);
return DFMS_EXCLUSION;
}
}
size_t next_dot = seek.find('.');
if (next_dot == std::string::npos || next_dot + 1 == seek.length()) {
break;
}
seek.erase(0, next_dot + 1);
}
return DFMS_DEFAULT;
}
DomainFilterMatchStatus DomainFilter::match_tag(const SockAddrTag &tag) const {
DomainFilterMatchStatus result = DFMS_DEFAULT;
char addr_str[SOCKADDR_STR_BUF_SIZE];
if (m_log.is_enabled(ag::LOG_LEVEL_DEBUG)) {
sockaddr_to_str((sockaddr *) &tag.addr, addr_str, sizeof(addr_str));
}
sockaddr_storage addr_no_port = tag.addr;
sockaddr_set_port((sockaddr *) &addr_no_port, 0);
bool found = m_addresses.end() != m_addresses.find(tag.addr);
if (!found) {
found = m_addresses.end() != m_addresses.find(addr_no_port);
}
if (found) {
log_filter(this, trace, "Address matched against exclusion list: {}", addr_str);
result = DFMS_EXCLUSION;
} else if (auto domain = m_resolved_tags.get(tag)) {
log_filter(this, dbg, "Cache hit: {}#{} -> {}", addr_str, tag.appname, *domain);
result = match_domain(*domain);
} else if (m_exclusion_suspects.get(addr_no_port)) {
result = DFMS_SUSPECT_EXCLUSION;
}
return result;
}
void DomainFilter::add_resolved_tag(SockAddrTag tag, std::string domain) {
log_filter(this, dbg, "{}#{} -> {}", sockaddr_to_str((sockaddr *) &tag.addr), tag.appname, domain);
m_resolved_tags.insert(std::move(tag), std::move(domain));
}
void DomainFilter::add_exclusion_suspect(const sockaddr_storage &addr, std::chrono::seconds ttl) {
log_filter(this, dbg, "{} / {}", sockaddr_ip_to_str((sockaddr *) &addr), (int64_t) ttl.count());
m_exclusion_suspects.insert(addr, 0, ttl);
}
std::vector<std::string_view> DomainFilter::get_resolvable_exclusions() const {
std::vector<std::string_view> names;
for (const auto &[name, flags] : m_domains) {
if (flags.test(DFMM_EXACT)) {
names.emplace_back(name);
}
}
return names;
}
VpnMode DomainFilter::get_mode() const {
return m_mode;
}
} // namespace ag
+231
View File
@@ -0,0 +1,231 @@
#include "vpn/internal/domain_lookuper.h"
#include <cassert>
#include <memory>
#include <string_view>
#include <vector>
#include "net/tls.h"
#include "vpn/internal/utils.h"
namespace ag {
struct Parser { // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
virtual ~Parser() = default;
virtual DomainLookuperResult parse(DomainLookuperPacketDirection dir, std::vector<uint8_t> *buffer) = 0;
};
struct TlsParser : public Parser { // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
enum State {
TPS_IDLE,
TPS_SERVER_HELLO,
TPS_CERT,
};
State state = TPS_IDLE;
TlsReader reader = {};
size_t buffer_offset = 0;
~TlsParser() override = default;
[[nodiscard]] DomainLookuperResult parse_cert_to_lookuper_result(TlsParseResult r) const {
switch (r) {
case TLS_RMORE:
return {DLUS_PASS};
case TLS_RCERT:
return {DLUS_FOUND, this->reader.x509_subject_common_name};
case TLS_RSERV_HELLO:
return {DLUS_PASS};
default:
return {DLUS_NOTFOUND};
}
}
DomainLookuperResult parse(DomainLookuperPacketDirection dir, std::vector<uint8_t> *buffer) override {
switch (this->state) {
case TPS_IDLE: {
if (dir != DLUPD_OUTGOING) {
break; // not TLS
}
tls_input(&this->reader, buffer->data(), buffer->size());
this->buffer_offset = 0;
TlsParseResult r = tls_parse(&this->reader);
switch (r) {
case TLS_RCLIENT_HELLO:
r = tls_parse(&this->reader);
if (r == TLS_RCLIENT_HELLO_SNI && !this->reader.tls_hostname.empty()) {
DomainLookuperResult result = {
DLUS_FOUND, {this->reader.tls_hostname.data(), this->reader.tls_hostname.size()}};
buffer->clear();
return result;
}
this->state = TPS_SERVER_HELLO;
buffer->clear();
[[fallthrough]];
case TLS_RMORE:
return {DLUS_PASS};
default:
break;
}
break;
}
case TPS_SERVER_HELLO: {
if (dir == DLUPD_OUTGOING) {
return {DLUS_PASS};
}
this->reader = {};
tls_input(&this->reader, buffer->data(), buffer->size());
TlsParseResult r = tls_parse(&this->reader);
switch (r) {
case TLS_RSERV_HELLO:
this->state = TPS_CERT;
break;
case TLS_RMORE:
default: // not a server hello
return {DLUS_PASS};
}
this->buffer_offset = this->reader.in.data() - buffer->data();
r = tls_parse(&this->reader);
if (r != TLS_RDONE) {
return parse_cert_to_lookuper_result(r);
}
[[fallthrough]];
}
case TPS_CERT: {
if (dir == DLUPD_OUTGOING) {
return {DLUS_PASS};
}
tls_input(&this->reader, buffer->data() + this->buffer_offset, buffer->size() - this->buffer_offset);
TlsParseResult r = tls_parse(&this->reader);
return parse_cert_to_lookuper_result(r);
}
}
return {DLUS_NOTFOUND};
}
};
struct HttpParser : public Parser {
~HttpParser() override = default;
DomainLookuperResult parse(DomainLookuperPacketDirection, std::vector<uint8_t> *buffer) override {
static constexpr size_t MIN_METHOD_LENGTH = 3;
static constexpr size_t MAX_METHOD_LENGTH = 32;
// check method
size_t i;
for (i = 0; i < buffer->size(); ++i) {
int ch = (*buffer)[i];
if (isspace((unsigned char) ch)) {
break;
}
if (!isalpha((unsigned char) ch) || i == MAX_METHOD_LENGTH) {
return {DLUS_NOTFOUND};
}
}
if (i < MIN_METHOD_LENGTH) {
return {DLUS_NOTFOUND};
}
static constexpr std::string_view HOST_MARKER = "Host:";
std::string_view seek = {(char *) buffer->data() + i, buffer->size() - i};
if (size_t host_header_pos, host_start, host_end; seek.npos != (host_header_pos = seek.find(HOST_MARKER))
&& host_header_pos + HOST_MARKER.length() < seek.length()
&& seek.npos != (host_start = seek.find_first_not_of(" \t", host_header_pos + HOST_MARKER.length()))
&& seek.npos != (host_end = seek.find_first_of("\r\n", host_start)) && host_start < host_end) {
return {DLUS_FOUND, {seek.data() + host_start, seek.data() + host_end}};
}
if (size_t uri_start; // try extract host from uri
seek.npos != (uri_start = seek.find_first_not_of(" \t")) && seek[uri_start] != '/') {
static constexpr std::string_view SCHEME_MARKER = "://";
size_t host_start = seek.find(SCHEME_MARKER, uri_start);
if (host_start == seek.npos) {
host_start = uri_start;
} else if (host_start + SCHEME_MARKER.length() >= seek.length()) {
return {DLUS_NOTFOUND};
} else {
host_start += SCHEME_MARKER.length();
}
size_t host_end = seek.find_first_of(":/ \t", host_start);
if (host_end != seek.npos) {
return {DLUS_FOUND, {seek.data() + host_start, seek.data() + host_end}};
}
}
return {DLUS_NOTFOUND};
}
};
struct ParserFactory {
size_t idx = 0;
using ParserProducer = std::unique_ptr<Parser> (*)();
static constexpr ParserProducer PRODUCE_TABLE[] = {
[]() -> std::unique_ptr<Parser> {
return std::make_unique<TlsParser>();
},
[]() -> std::unique_ptr<Parser> {
return std::make_unique<HttpParser>();
},
};
std::unique_ptr<Parser> produce() {
static constexpr size_t TABLE_SIZE = std::size(PRODUCE_TABLE);
if (this->idx >= TABLE_SIZE) {
return nullptr;
}
return PRODUCE_TABLE[this->idx++]();
}
};
struct DomainLookuper::Context {
ParserFactory factory = {};
std::unique_ptr<Parser> current_parser = factory.produce();
std::vector<uint8_t> buffer;
DomainLookuperResult parse(DomainLookuperPacketDirection dir, const uint8_t *data, size_t length);
};
DomainLookuperResult DomainLookuper::Context::parse(
DomainLookuperPacketDirection dir, const uint8_t *data, size_t length) {
this->buffer.insert(this->buffer.end(), data, data + length);
while (this->current_parser != nullptr) {
DomainLookuperResult r = this->current_parser->parse(dir, &this->buffer);
switch (r.status) {
case DLUS_FOUND:
case DLUS_PASS:
return r;
case DLUS_NOTFOUND:
this->current_parser = this->factory.produce();
break;
}
}
return {DLUS_NOTFOUND, ""};
}
DomainLookuperResult DomainLookuper::proceed(DomainLookuperPacketDirection dir, const uint8_t *data, size_t length) {
if (m_context == nullptr) {
m_context = std::make_unique<DomainLookuper::Context>();
}
DomainLookuperResult r = m_context->parse(dir, data, length);
return r;
}
void DomainLookuper::reset() {
m_context.reset();
}
DomainLookuper::DomainLookuper() = default;
DomainLookuper::~DomainLookuper() = default;
} // namespace ag
+122
View File
@@ -0,0 +1,122 @@
#include "fake_upstream.h"
#include "vpn/internal/vpn_client.h"
namespace ag {
FakeUpstream::FakeUpstream(int id)
: ServerUpstream(id) {
}
FakeUpstream::~FakeUpstream() = default;
bool FakeUpstream::init(VpnClient *vpn, SeverHandler handler) {
if (!this->ServerUpstream::init(vpn, handler)) {
deinit();
return false;
}
// `open_session` is not being used
m_session_open = true;
return true;
}
void FakeUpstream::deinit() {
}
bool FakeUpstream::open_session(uint32_t) {
assert(0);
return false;
}
void FakeUpstream::close_session() {
std::unordered_set<uint64_t> connections;
connections.reserve(m_opening_connections.size() + m_closing_connections.size());
connections.insert(m_opening_connections.begin(), m_opening_connections.end());
connections.insert(m_closing_connections.begin(), m_closing_connections.end());
for (uint64_t conn : connections) {
this->close_connection(conn, true, false);
}
m_opening_connections.clear();
m_closing_connections.clear();
m_async_task.reset();
m_session_open = false;
}
uint64_t FakeUpstream::open_connection(const TunnelAddressPair *, int, std::string_view) {
if (!m_session_open) {
return NON_ID;
}
uint64_t id = this->vpn->upstream_conn_id_generator.get();
m_opening_connections.insert(id);
if (!m_async_task.has_value()) {
m_async_task = ag::submit(this->vpn->parameters.ev_loop, {this, on_async_task});
}
return id;
}
void FakeUpstream::close_connection(uint64_t id, bool, bool async) {
m_opening_connections.erase(id);
if (!async) {
this->handler.func(this->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &id);
} else if (m_session_open) {
m_closing_connections.insert(id);
if (!m_async_task.has_value()) {
m_async_task = ag::submit(this->vpn->parameters.ev_loop, {this, on_async_task});
}
}
}
ssize_t FakeUpstream::send(uint64_t, const uint8_t *, size_t) {
assert(0);
return -1;
}
void FakeUpstream::consume(uint64_t, size_t) {
assert(0);
}
size_t FakeUpstream::available_to_send(uint64_t) {
assert(0);
return 0;
}
void FakeUpstream::update_flow_control(uint64_t, TcpFlowCtrlInfo) {
// can be called from tunnel
}
VpnError FakeUpstream::do_health_check() {
assert(0);
return {VPN_EC_ERROR, "Internal error"};
}
VpnConnectionStats FakeUpstream::get_connection_stats() const {
assert(0);
return {};
}
void FakeUpstream::on_icmp_request(IcmpEchoRequestEvent &) {
assert(0);
}
void FakeUpstream::on_async_task(void *arg, TaskId) {
auto *self = (FakeUpstream *) arg;
self->m_async_task.release();
std::unordered_set<uint64_t> connections;
connections.swap(self->m_opening_connections);
for (uint64_t id : connections) {
self->handler.func(self->handler.arg, SERVER_EVENT_CONNECTION_OPENED, &id);
}
connections.clear();
connections.swap(self->m_closing_connections);
for (uint64_t id : connections) {
self->close_connection(id, false, false);
}
}
} // namespace ag
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <unordered_set>
#include "vpn/internal/server_upstream.h"
namespace ag {
class FakeUpstream : public ServerUpstream {
public:
explicit FakeUpstream(int id);
~FakeUpstream() override;
FakeUpstream(const FakeUpstream &) = delete;
FakeUpstream &operator=(const FakeUpstream &) = delete;
FakeUpstream(FakeUpstream &&) = delete;
FakeUpstream &operator=(FakeUpstream &&) = delete;
private:
std::unordered_set<uint64_t> m_opening_connections;
std::unordered_set<uint64_t> m_closing_connections;
ag::AutoTaskId m_async_task;
bool m_session_open = false;
bool init(VpnClient *vpn, SeverHandler handler) override;
void deinit() override;
bool open_session(uint32_t timeout_ms = 0) override;
void close_session() override;
uint64_t open_connection(const TunnelAddressPair *addr, int proto, std::string_view app_name) override;
void close_connection(uint64_t id, bool graceful, bool async) override;
ssize_t send(uint64_t id, const uint8_t *data, size_t length) override;
void consume(uint64_t id, size_t length) override;
size_t available_to_send(uint64_t id) override;
void update_flow_control(uint64_t id, TcpFlowCtrlInfo info) override;
VpnError do_health_check() override;
[[nodiscard]] VpnConnectionStats get_connection_stats() const override;
void on_icmp_request(IcmpEchoRequestEvent &event) override;
static void on_async_task(void *, TaskId);
};
} // namespace ag
@@ -0,0 +1,167 @@
#include "fallbackable_upstream_connector.h"
#include <atomic>
#include "single_upstream_connector.h"
#define log_connector(con_, lvl_, fmt_, ...) lvl_##log((con_)->m_log, "[{}] " fmt_, (con_)->m_id, ##__VA_ARGS__)
using namespace std::chrono;
namespace ag {
static std::atomic<int> next_connector_id = 0;
FallbackableUpstreamConnector::FallbackableUpstreamConnector(const EndpointConnectorParameters &parameters,
std::unique_ptr<ServerUpstream> main, std::unique_ptr<ServerUpstream> fallback, Milliseconds fallback_delay)
: EndpointConnector(parameters)
, m_main({
std::make_unique<SingleUpstreamConnector>(
this->make_connector_parameters({&main_connector_handler, this}), std::move(main)),
})
, m_fallback({
std::make_unique<SingleUpstreamConnector>(
this->make_connector_parameters({&fallback_connector_handler, this}), std::move(fallback)),
fallback_delay,
})
, m_id(next_connector_id.fetch_add(1, std::memory_order_relaxed)) {
}
VpnError FallbackableUpstreamConnector::connect(uint32_t timeout_ms) {
m_connect_timeout = Milliseconds(timeout_ms);
m_main.start_ts = steady_clock::now();
if (VpnError error = m_main.connector->connect(timeout_ms); error.code != VPN_EC_NOERROR) {
log_connector(this, dbg, "Failed to start connect to main upstream, trying fallback immediately");
this->handle_connect_result(m_main.connector.get(), error);
} else {
m_fallback.delay_task = ag::schedule(this->PARAMETERS.ev_loop,
{
this,
[](void *arg, TaskId) {
auto *self = (FallbackableUpstreamConnector *) arg;
self->m_fallback.delay_task.release();
self->start_fallback_connection();
},
},
m_fallback.delay.count());
}
return {};
}
void FallbackableUpstreamConnector::disconnect() {
if (!m_main.has_result && m_main.connector != nullptr) {
m_main.connector->disconnect();
}
m_main.result_task.reset();
if (m_fallback.tried && !m_fallback.has_result && m_fallback.connector != nullptr) {
m_fallback.connector->disconnect();
}
m_fallback.delay_task.reset();
m_fallback.result_task.reset();
}
EndpointConnectorParameters FallbackableUpstreamConnector::make_connector_parameters(EndpointConnectorHandler h) const {
return {
this->PARAMETERS.ev_loop,
this->PARAMETERS.vpn_client,
this->PARAMETERS.upstream_handler,
h,
};
}
void FallbackableUpstreamConnector::handle_connect_result(
const EndpointConnector *connector, EndpointConnectorResult result) {
bool need_raise = false;
if (std::holds_alternative<std::unique_ptr<ServerUpstream>>(result)) {
log_connector(this, dbg, "Got successful result from {} connector",
(m_main.connector.get() == connector) ? "main" : "m_fallback");
EndpointConnector *another_connector =
(m_main.connector.get() == connector) ? m_main.connector.get() : m_fallback.connector.get();
another_connector->disconnect();
need_raise = true;
}
if (m_main.connector.get() == connector) {
m_main.connector.reset();
m_main.has_result = true;
m_main.result_task.release();
} else {
m_fallback.connector.reset();
m_fallback.has_result = true;
m_fallback.result_task.release();
}
if (m_main.has_result && m_fallback.has_result) {
log_connector(this, dbg, "Both endpoint connectors failed");
need_raise = true;
}
if (need_raise) {
EndpointConnectorHandler h = this->PARAMETERS.connector_handler;
h.func(h.arg, std::move(result));
m_main = {};
m_fallback = {};
} else if (!m_fallback.tried) {
log_connector(this, dbg, "Main protocol failed, trying fallback immediately");
m_fallback.delay_task = ag::submit(this->PARAMETERS.ev_loop,
{
this,
[](void *arg, TaskId) {
auto *self = (FallbackableUpstreamConnector *) arg;
self->m_fallback.delay_task.release();
self->start_fallback_connection();
},
});
}
}
VpnEventLoopTask FallbackableUpstreamConnector::make_deferred_handle_task(
const EndpointConnector *c, EndpointConnectorResult result) const {
struct connector_result_ctx_t {
FallbackableUpstreamConnector *self;
const EndpointConnector *ready_connector;
EndpointConnectorResult result;
};
return {
new connector_result_ctx_t{(FallbackableUpstreamConnector *) this, c, std::move(result)},
[](void *arg, TaskId task_id) {
auto *ctx = (connector_result_ctx_t *) arg;
FallbackableUpstreamConnector *self = ctx->self;
self->handle_connect_result(ctx->ready_connector, std::move(ctx->result));
},
[](void *arg) {
delete (connector_result_ctx_t *) arg;
},
};
}
void FallbackableUpstreamConnector::start_fallback_connection() {
assert(!m_fallback.tried);
assert(!m_fallback.has_result);
m_fallback.tried = true;
Milliseconds timeout = m_connect_timeout - duration_cast<Milliseconds>(steady_clock::now() - m_main.start_ts);
VpnError error = m_fallback.connector->connect(std::max(timeout, m_connect_timeout / 10).count());
if (error.code != VPN_EC_NOERROR) {
this->handle_connect_result(m_fallback.connector.get(), error);
}
}
void FallbackableUpstreamConnector::main_connector_handler(void *arg, EndpointConnectorResult result) {
auto *self = (FallbackableUpstreamConnector *) arg;
assert(!self->m_main.result_task.has_value());
self->m_main.result_task = ag::submit(
self->PARAMETERS.ev_loop, self->make_deferred_handle_task(self->m_main.connector.get(), std::move(result)));
}
void FallbackableUpstreamConnector::fallback_connector_handler(void *arg, EndpointConnectorResult result) {
auto *self = (FallbackableUpstreamConnector *) arg;
assert(!self->m_fallback.result_task.has_value());
self->m_fallback.result_task = ag::submit(self->PARAMETERS.ev_loop,
self->make_deferred_handle_task(self->m_fallback.connector.get(), std::move(result)));
}
} // namespace ag
@@ -0,0 +1,58 @@
#pragma once
#include <chrono>
#include "common/logger.h"
#include "vpn/internal/endpoint_connector.h"
namespace ag {
class FallbackableUpstreamConnector : public EndpointConnector {
public:
using Milliseconds = std::chrono::milliseconds;
FallbackableUpstreamConnector(const EndpointConnectorParameters &parameters, std::unique_ptr<ServerUpstream> main,
std::unique_ptr<ServerUpstream> fallback, Milliseconds fallback_delay);
~FallbackableUpstreamConnector() override = default;
FallbackableUpstreamConnector(const FallbackableUpstreamConnector &) = delete;
FallbackableUpstreamConnector &operator=(const FallbackableUpstreamConnector &) = delete;
FallbackableUpstreamConnector(FallbackableUpstreamConnector &&) = delete;
FallbackableUpstreamConnector &operator=(FallbackableUpstreamConnector &&) = delete;
private:
struct MainInfo {
std::unique_ptr<EndpointConnector> connector;
bool has_result = false;
std::chrono::time_point<std::chrono::steady_clock> start_ts;
ag::AutoTaskId result_task;
};
struct FallbackInfo {
std::unique_ptr<EndpointConnector> connector;
Milliseconds delay;
bool tried = false;
bool has_result = false;
ag::AutoTaskId delay_task;
ag::AutoTaskId result_task;
};
MainInfo m_main;
FallbackInfo m_fallback;
Milliseconds m_connect_timeout{0};
int m_id;
ag::Logger m_log{"FBUCONNECTOR"};
VpnError connect(uint32_t timeout_ms) override;
void disconnect() override;
[[nodiscard]] EndpointConnectorParameters make_connector_parameters(EndpointConnectorHandler h) const;
void handle_connect_result(const EndpointConnector *connector, EndpointConnectorResult result);
VpnEventLoopTask make_deferred_handle_task(const EndpointConnector *c, EndpointConnectorResult result) const;
void start_fallback_connection();
static void main_connector_handler(void *arg, EndpointConnectorResult result);
static void fallback_connector_handler(void *arg, EndpointConnectorResult result);
};
} // namespace ag
+821
View File
@@ -0,0 +1,821 @@
#include "http2_upstream.h"
#include <cassert>
#include <cstdio>
#include <string_view>
#include <vector>
#include <event2/util.h>
#include <nghttp2/nghttp2.h>
#include "common/net_utils.h"
#include "net/tls.h"
#include "net/utils.h"
#include "vpn/utils.h"
#define log_upstream(ups_, lvl_, fmt_, ...) lvl_##log((ups_)->m_log, "[{}] " fmt_, (ups_)->id, ##__VA_ARGS__)
#define log_conn(ups_, cid_, lvl_, fmt_, ...) \
lvl_##log((ups_)->m_log, "[{}] [R:{}] " fmt_, (ups_)->id, (uint64_t) (cid_), ##__VA_ARGS__)
using namespace std::chrono;
namespace ag {
static constexpr size_t HTTP2_STREAM_INITIAL_WINDOW_SIZE = 131072; // Chrome constant
enum Http2Upstream::TcpConnection::Flag : int {
TCF_READ_ENABLED, // `SERVER_EVENT_READ` can be raised
TCF_STREAM_CLOSED, // stream closed gracefully, but we're waiting until all data is sent
};
struct close_ctx_t {
Http2Upstream *upstream;
uint64_t id;
bool graceful;
};
struct CompleteCtx {
Http2Upstream *upstream;
uint64_t id;
};
Http2Upstream::Http2Upstream(
const VpnUpstreamProtocolConfig &protocol_config, int id, VpnClient *vpn, SeverHandler handler)
: MultiplexableUpstream(protocol_config, id, vpn, handler)
, m_udp_mux({this, send_connect_request_callback, send_data_callback, consume_callback})
, m_icmp_mux({this, send_connect_request_callback, send_data_callback, consume_callback})
, m_credentials(make_credentials(vpn->upstream_config.username, vpn->upstream_config.password)) {
// static logger_ptr_t ngh2_logger{ logger_open("NGH2", LOG_LEVEL_DEFAULT) };
// nghttp2_set_debug_vprintf_callback([] (const char *format, va_list args) {
// logger_vlog(ngh2_logger.get(), LOG_LEVEL_DEBUG, format, args);
// });
}
Http2Upstream::~Http2Upstream() {
// reset it manually to be sure it's deleted before others
m_session.reset();
}
int Http2Upstream::handle_read(uint64_t id, const uint8_t *data, size_t length) {
ServerReadEvent serv_event = {id, data, length, 0};
this->handler.func(this->handler.arg, SERVER_EVENT_READ, &serv_event);
if (serv_event.result < 0) {
this->close_tcp_connection(id, false);
}
return serv_event.result;
}
std::pair<uint64_t, Http2Upstream::TcpConnection *> Http2Upstream::get_conn_by_stream_id(uint32_t id) {
std::pair<uint64_t, TcpConnection *> r = {NON_ID, nullptr};
auto id_iter = m_conn_id_by_stream_id.find(id);
if (id_iter != m_conn_id_by_stream_id.end()) {
r.first = id_iter->second;
auto found = m_tcp_connections.find(id_iter->second);
if (found != m_tcp_connections.end()) {
r.second = &found->second;
}
}
return r;
}
void Http2Upstream::handle_response(const HttpHeadersEvent *http_event) {
SeverHandler *handler = &this->handler;
uint32_t stream_id = http_event->stream_id;
if (stream_id == m_udp_mux.get_stream_id()) {
m_udp_mux.handle_response(http_event->headers);
} else if (stream_id == m_icmp_mux.get_stream_id()) {
m_icmp_mux.handle_response(http_event->headers);
} else if (m_health_check_info->stream_id == stream_id) {
if (http_event->headers->status_code == HTTP_AUTH_REQUIRED_STATUS) {
m_health_check_info->error = {VPN_EC_AUTH_REQUIRED, HTTP_AUTH_REQUIRED_MSG};
} else if (http_event->headers->status_code != HTTP_OK_STATUS) {
m_health_check_info->error = {VPN_EC_ERROR, "Bad response code"};
}
} else {
auto found = get_conn_by_stream_id(stream_id);
if (found.second == nullptr) {
log_upstream(this, dbg, "Got response on closed connection: stream={}", stream_id);
assert(0);
return;
}
if (http_event->headers->status_code == 200) {
handler->func(handler->arg, SERVER_EVENT_CONNECTION_OPENED, &found.first);
} else {
TcpConnection *conn = found.second;
conn->pending_error = {found.first, bad_http_response_to_connect_error(http_event->headers)};
}
}
}
int Http2Upstream::read_out_pending_data(uint64_t id, TcpConnection *conn) {
DataBuffer *pending = conn->unread_data.get();
while (conn->flags.test(TCF_READ_ENABLED) && pending->size() > 0) {
BufferPeekResult res = pending->peek();
if (res.err.has_value()) {
log_conn(this, id, err, "Failed to read buffered data: {}", *res.err);
return -1;
}
int r = handle_read(id, res.data.data(), res.data.size());
if (r > 0) {
pending->drain(r);
} else if (r < 0) {
return r;
}
}
return 0;
}
void Http2Upstream::http_handler(void *arg, HttpEventId what, void *data) {
Http2Upstream *upstream = (Http2Upstream *) arg;
switch (what) {
case HTTP_EVENT_HEADERS: {
const HttpHeadersEvent *http_event = (HttpHeadersEvent *) data;
log_headers(upstream->m_log, http_event->stream_id, http_event->headers, "Got response from server");
upstream->handle_response(http_event);
break;
}
case HTTP_EVENT_DATA: {
HttpDataEvent *http_event = (HttpDataEvent *) data;
if ((uint32_t) http_event->stream_id == upstream->m_udp_mux.get_stream_id()) {
http_event->result = upstream->m_udp_mux.process_read_event({http_event->data, http_event->length});
break;
}
if ((uint32_t) http_event->stream_id == upstream->m_icmp_mux.get_stream_id()) {
http_event->result = upstream->m_icmp_mux.process_read_event({http_event->data, http_event->length});
break;
}
auto found = upstream->get_conn_by_stream_id(http_event->stream_id);
if (found.second == nullptr) {
log_upstream(upstream, dbg, "Got data on closed connection: stream={}", http_event->stream_id);
assert(0);
break;
}
http_event->result = 0;
TcpConnection *conn = found.second;
DataBuffer *pending = conn->unread_data.get();
if (pending != nullptr) {
http_event->result = upstream->read_out_pending_data(found.first, conn);
}
if (http_event->result < 0) {
break;
}
http_event->result = 0;
if (conn->flags.test(TCF_READ_ENABLED)) {
assert(pending == nullptr || pending->size() == 0);
http_event->result = upstream->handle_read(found.first, http_event->data, http_event->length);
}
if (http_event->result >= 0) {
if ((size_t) http_event->result < http_event->length) {
const uint8_t *unread = http_event->data + http_event->result;
size_t size = http_event->length - http_event->result;
if (pending == nullptr) {
conn->unread_data = upstream->vpn->make_buffer(found.first);
pending = conn->unread_data.get();
if (std::optional<std::string> err = pending->init(); err.has_value()) {
log_conn(upstream, found.first, err, "Failed to initialize data buffer: {}", *err);
http_event->result = -1;
break;
}
}
std::optional<std::string> err = pending->push({unread, size});
if (err.has_value()) {
log_conn(upstream, found.first, err, "Failed to put data (size={}) in buffer: {}", size, *err);
http_event->result = -1;
}
}
http_event->result = std::min(http_event->result, 0);
}
if (http_event->result >= 0 && pending != nullptr && pending->size() > 0) {
http_event->result = NGHTTP2_ERR_PAUSE;
}
break;
}
case HTTP_EVENT_DATA_FINISHED: {
// endpoint may send a couple of RST frames in response
http_session_send_data(upstream->m_session.get(), (int32_t) * (uint32_t *) data, nullptr, 0, true);
break;
}
case HTTP_EVENT_STREAM_PROCESSED: {
const HttpStreamProcessedEvent *http_event = (HttpStreamProcessedEvent *) data;
uint32_t stream_id = http_event->stream_id;
if (stream_id == upstream->m_udp_mux.get_stream_id()) {
ServerError serv_err = {0, {http_event->error_code, nghttp2_http2_strerror(http_event->error_code)}};
upstream->m_udp_mux.close(serv_err);
} else if (stream_id == upstream->m_icmp_mux.get_stream_id()) {
log_upstream(upstream, dbg, "ICMP multiplexer stream has been closed: {} ({})",
nghttp2_http2_strerror(http_event->error_code), http_event->error_code);
upstream->m_icmp_mux.close();
} else if (upstream->m_health_check_info.has_value() && upstream->m_health_check_info->stream_id == stream_id) {
if (upstream->m_health_check_info->error.code == 0 && http_event->error_code != NGHTTP2_NO_ERROR) {
upstream->m_health_check_info->error = {VPN_EC_ERROR, nghttp2_http2_strerror(http_event->error_code)};
}
upstream->handler.func(
upstream->handler.arg, SERVER_EVENT_HEALTH_CHECK_RESULT, &upstream->m_health_check_info->error);
upstream->m_health_check_info.reset();
} else {
auto found = upstream->get_conn_by_stream_id(stream_id);
if (found.second == nullptr) {
log_upstream(upstream, dbg, "Got stream processed event on closed connection: stream={}", stream_id);
assert(0);
break;
}
TcpConnection *conn = found.second;
conn->flags.set(TCF_STREAM_CLOSED);
std::optional<ServerError> err_event = conn->pending_error;
if (!err_event.has_value() && http_event->error_code != NGHTTP2_NO_ERROR) {
err_event = {found.first, {ag::utils::AG_ECONNREFUSED, nghttp2_http2_strerror(http_event->error_code)}};
}
if (err_event.has_value()) {
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &err_event.value());
upstream->clean_tcp_connection_data(found.first);
} else if (conn->unread_data == nullptr || conn->unread_data->size() == 0) {
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &found.first);
upstream->clean_tcp_connection_data(found.first);
} else {
// postpone until all data is sent to client
}
}
break;
}
case HTTP_EVENT_DATA_SENT: {
const HttpDataSentEvent *http_event = (HttpDataSentEvent *) data;
// for udp it will be reported in socket write buffer flushed event
if ((uint32_t) http_event->stream_id != upstream->m_udp_mux.get_stream_id()) {
auto found = upstream->get_conn_by_stream_id(http_event->stream_id);
if (found.second != nullptr) {
ServerDataSentEvent serv_event = {found.first, http_event->length};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_DATA_SENT, &serv_event);
}
}
break;
}
case HTTP_EVENT_GOAWAY: {
const HttpGoawayEvent *http_event = (HttpGoawayEvent *) data;
log_upstream(upstream, dbg, "HTTP_EVENT_GOAWAY {}", http_event->error_code);
if (http_event->error_code != NGHTTP2_NO_ERROR) {
ServerError serv_event = {NON_ID};
switch (http_event->error_code) {
case HTTP_ERROR_AUTH_REQUIRED:
serv_event.error = {VPN_EC_AUTH_REQUIRED, HTTP_AUTH_REQUIRED_MSG};
break;
default:
serv_event.error = {VPN_EC_ERROR, nghttp2_http2_strerror(http_event->error_code)};
break;
}
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &serv_event);
} else {
upstream->close_session_inner();
}
break;
}
case HTTP_EVENT_OUTPUT: {
const HttpOutputEvent *http_event = (HttpOutputEvent *) data;
log_upstream(upstream, trace, "Sending {} bytes to server", http_event->length);
VpnError error = tcp_socket_write(upstream->m_socket.get(), http_event->data, http_event->length);
if (error.code != 0) {
ServerError serv_event = {NON_ID, {VPN_EC_ERROR, error.text}};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &serv_event);
}
break;
}
}
}
int Http2Upstream::establish_http_session() {
assert(m_session == nullptr);
HttpSessionParams params = {
m_next_session_id++, {http_handler, this}, HTTP2_STREAM_INITIAL_WINDOW_SIZE, HTTP_VER_2_0};
m_session.reset(http_session_open(&params));
int r = 0;
if (m_session != nullptr && 0 != (r = http_session_send_settings(m_session.get()))) {
if (r < 0) {
log_upstream(this, err, "Failed to start HTTP session with endpoint: {}", nghttp2_strerror(r));
} else {
log_upstream(this, err, "Failed to start HTTP session with endpoint");
}
}
return r == 0;
}
void Http2Upstream::net_handler(void *arg, TcpSocketEvent what, void *data) {
Http2Upstream *upstream = (Http2Upstream *) arg;
upstream->m_in_handler = true;
switch (what) {
case TCP_SOCKET_EVENT_CONNECTED: {
tcp_socket_set_read_enabled(upstream->m_socket.get(), true);
tcp_socket_set_timeout(
upstream->m_socket.get(), duration_cast<milliseconds>(upstream->vpn->upstream_config.timeout).count());
log_upstream(upstream, dbg, "Established TCP connection to endpoint successfully");
log_upstream(upstream, dbg, "Initiating HTTP2 session with endpoint...");
if (upstream->establish_http_session()) {
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_SESSION_OPENED, nullptr);
} else {
ServerError serv_event = {NON_ID, {VPN_EC_ERROR, "Failed to initiate HTTP2 session"}};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &serv_event);
}
break;
}
case TCP_SOCKET_EVENT_READ: {
TcpSocketReadEvent *sock_event = (TcpSocketReadEvent *) data;
if (sock_event->length == 0) {
log_upstream(upstream, dbg, "Got EOF from endpoint");
upstream->close_session_inner();
} else {
log_upstream(upstream, trace, "Got {} bytes from endpoint", sock_event->length);
int r = http_session_input(upstream->m_session.get(), sock_event->data, sock_event->length);
if (r > 0) {
sock_event->processed = r;
if (0 == http_session_available_to_read(upstream->m_session.get(), 0)) {
tcp_socket_set_read_enabled(upstream->m_socket.get(), false);
}
} else {
log_upstream(upstream, err, "Failed to process HTTP data from server: {} ({})", nghttp2_strerror(r), r);
ServerError serv_event = {NON_ID, {VPN_EC_ERROR, nghttp2_strerror(r)}};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &serv_event);
}
}
break;
}
case TCP_SOCKET_EVENT_ERROR: {
const VpnError *sock_event = (VpnError *) data;
// while opening a session or performing a health check we can't ignore time out
if (sock_event->code == ag::utils::AG_ETIMEDOUT && upstream->m_session != nullptr
&& !upstream->m_health_check_info.has_value()) {
log_upstream(upstream, dbg, "Ignore timed out socket");
break;
}
log_upstream(upstream, dbg, "Error on HTTP session socket: {} ({})", sock_event->text, sock_event->code);
ServerError serv_event = {NON_ID, {VPN_EC_ERROR, sock_event->text}};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &serv_event);
break;
}
case TCP_SOCKET_EVENT_SENT: {
// do nothing
break;
}
case TCP_SOCKET_EVENT_WRITE_FLUSH: {
log_upstream(upstream, trace, "Write buffer flushed");
for (auto &[id, _] : upstream->m_tcp_connections) {
ServerDataSentEvent serv_event = {id, 0};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_DATA_SENT, &serv_event);
}
upstream->m_udp_mux.report_sent_bytes();
break;
}
case TCP_SOCKET_EVENT_PROTECT: {
vpn_client::Handler *vpn_handler = &upstream->vpn->parameters.handler;
vpn_handler->func(vpn_handler->arg, vpn_client::EVENT_PROTECT_SOCKET, data);
break;
}
}
upstream->m_in_handler = false;
if (upstream->m_closed) {
upstream->close_session_inner();
upstream->m_closed = false;
}
}
bool Http2Upstream::open_session(uint32_t timeout_ms) {
const vpn_client::EndpointConnectionConfig *config = &this->vpn->upstream_config;
TcpSocketParameters sock_params = {
this->vpn->parameters.ev_loop,
{net_handler, this},
(timeout_ms == 0) ? (uint32_t) duration_cast<milliseconds>(config->timeout).count() : timeout_ms,
this->vpn->parameters.network_manager->socket,
0,
#ifdef _WIN32
true,
#endif // _WIN32
};
m_socket.reset(tcp_socket_create(&sock_params));
if (m_socket == nullptr) {
log_upstream(this, err, "Failed to create socket to server");
return false;
}
static constexpr uint8_t HTTP2_ALPN[] = {2, 'h', '2'};
SslPtr ssl;
if (auto r = make_ssl(verify_callback, this, {HTTP2_ALPN, std::size(HTTP2_ALPN)}, config->endpoint->name);
std::holds_alternative<SslPtr>(r)) {
ssl = std::move(std::get<SslPtr>(r));
} else {
log_upstream(this, err, "{}", std::get<std::string>(r));
return false;
}
TcpSocketConnectParameters param = {};
if (config->endpoint->address.ss_family != AF_UNSPEC) {
param = {
.connect_by = TCP_SOCKET_CB_ADDR,
.by_addr = {(sockaddr *) &config->endpoint->address},
.ssl = ssl.release(),
};
} else {
param = {
.connect_by = TCP_SOCKET_CB_HOSTNAME,
.by_name = {this->vpn->parameters.dns_base, config->endpoint->name, DEFAULT_PORT},
.ssl = ssl.release(),
};
}
VpnError error = tcp_socket_connect(m_socket.get(), &param);
if (error.code != 0) {
log_upstream(this, err, "Failed to connect to endpoint: {} ({})", safe_to_string_view(error.text), error.code);
m_socket.reset();
}
return error.code == 0;
}
void Http2Upstream::close_session_inner() {
if (m_in_handler) {
m_closed = true;
return;
}
close_session();
this->handler.func(this->handler.arg, SERVER_EVENT_SESSION_CLOSED, nullptr);
}
void Http2Upstream::close_session() {
log_upstream(this, dbg, "...");
std::vector<uint64_t> remaining_connections;
remaining_connections.reserve(m_tcp_connections.size());
std::transform(m_tcp_connections.begin(), m_tcp_connections.end(), std::back_inserter(remaining_connections),
[](const auto &i) -> uint64_t {
return i.first;
});
for (uint64_t conn_id : remaining_connections) {
this->close_connection(conn_id, false, false);
}
if (std::optional<uint64_t> id = m_udp_mux.get_stream_id(); id.has_value()) {
http_session_reset_stream(m_session.get(), (int32_t) id.value(), NGHTTP2_CANCEL);
}
if (std::optional<uint64_t> id = m_icmp_mux.get_stream_id(); id.has_value()) {
http_session_reset_stream(m_session.get(), (int32_t) id.value(), NGHTTP2_CANCEL);
}
m_session.reset();
m_socket.reset();
m_tcp_connections.clear();
m_conn_id_by_stream_id.clear();
m_udp_mux.close({});
m_icmp_mux.close();
m_stream_id_generator.reset();
m_health_check_info.reset();
log_upstream(this, dbg, "Done");
}
std::optional<uint32_t> Http2Upstream::send_connect_request(const TunnelAddress *dst_addr, std::string_view app_name) {
if (m_session == nullptr) {
log_upstream(this, dbg, "Failed to send connect request: upstream is inactive");
return std::nullopt;
}
HttpHeaders headers = make_http_connect_request(HTTP_VER_2_0, dst_addr, app_name, m_credentials);
uint32_t stream_id = m_stream_id_generator.get();
log_headers(m_log, stream_id, &headers, "Sending connect request");
int r = http_session_send_headers(m_session.get(), (int32_t) stream_id, &headers, false);
if (r != 0) {
log_upstream(this, dbg, "Failed to send connect request: {} ({})", nghttp2_strerror(r), r);
}
return (r == 0) ? std::make_optional(stream_id) : std::nullopt;
}
bool Http2Upstream::open_connection(
uint64_t conn_id, const TunnelAddressPair *addr, int proto, std::string_view app_name) {
bool result = true;
if (proto == IPPROTO_UDP) {
result = m_udp_mux.open_connection(conn_id, addr, app_name);
} else {
std::optional<uint32_t> stream_id = send_connect_request(&addr->dst, app_name);
if (stream_id.has_value()) {
TcpConnection *conn = &m_tcp_connections[conn_id];
conn->stream_id = stream_id.value();
m_conn_id_by_stream_id[conn->stream_id] = conn_id;
} else {
result = false;
}
}
return result;
}
void Http2Upstream::on_icmp_request(IcmpEchoRequestEvent &event) {
event.result = m_icmp_mux.send_request(event.request) ? 0 : -1;
}
void Http2Upstream::close_tcp_connection(uint64_t id, bool graceful) {
log_conn(this, id, dbg, "Closing");
auto i = m_tcp_connections.find(id);
if (m_session != nullptr && i != m_tcp_connections.end()) {
TcpConnection *conn = &i->second;
if (!conn->flags.test(TCF_STREAM_CLOSED)) {
int err = graceful ? NGHTTP2_NO_ERROR : NGHTTP2_CANCEL;
http_session_reset_stream(m_session.get(), (int32_t) i->second.stream_id, err);
return; // will be cleaned up in the stream processed event
}
// resetting stream again won't have any effect
this->handler.func(this->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, (void *) &i->first);
}
clean_tcp_connection_data(id);
}
void Http2Upstream::close_connection(uint64_t id, bool graceful, bool async) {
if (m_udp_mux.check_connection(id)) {
m_udp_mux.close_connection(id, async);
return;
}
auto it = m_tcp_connections.find(id);
if (it == m_tcp_connections.end()) {
// @fixme: AG-9352
this->handler.func(this->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &id);
return;
}
if (!async) {
close_tcp_connection(id, graceful);
return;
}
TcpConnection *conn = &it->second;
conn->close_task_id = ag::submit(this->vpn->parameters.ev_loop,
{
new close_ctx_t{this, id, graceful},
[](void *arg, TaskId task_id) {
close_ctx_t *ctx = (close_ctx_t *) arg;
auto *self = ctx->upstream;
auto i = self->m_tcp_connections.find(ctx->id);
if (i == self->m_tcp_connections.end()) {
return;
}
i->second.close_task_id.release();
ctx->upstream->close_tcp_connection(ctx->id, ctx->graceful);
},
[](void *arg) {
delete (close_ctx_t *) arg;
},
});
}
ssize_t Http2Upstream::send(uint64_t id, const uint8_t *data, size_t length) {
ssize_t r = 0;
if (auto i = m_tcp_connections.find(id); i != m_tcp_connections.end()) {
TcpConnection *conn = &i->second;
if (!conn->flags.test(TCF_STREAM_CLOSED)) {
r = http_session_send_data(m_session.get(), (int32_t) conn->stream_id, data, length, false);
if (r == 0) {
r = (ssize_t) length;
} else if (r == NGHTTP2_ERR_BUFFER_ERROR) {
r = 0;
}
} else {
log_conn(this, id, err, "Trying to send data on connection with closed stream");
r = -1;
}
} else if (m_udp_mux.check_connection(id)) {
r = m_udp_mux.send(id, {data, length});
} else {
log_conn(this, id, err, "Trying to send data on already closed or inexistent connection");
r = -1;
}
if (r < 0) {
log_conn(this, id, dbg, "Failed to send data from client: {} ({})", nghttp2_strerror(r), r);
}
return r;
}
std::optional<uint32_t> Http2Upstream::get_stream_id(uint64_t id) const {
std::optional<uint32_t> stream_id;
if (m_udp_mux.check_connection(id)) {
stream_id = m_udp_mux.get_stream_id();
} else if (auto i = m_tcp_connections.find(id); i != m_tcp_connections.end()) {
stream_id = i->second.stream_id;
}
return stream_id;
}
void Http2Upstream::consume(uint64_t id, size_t length) {
log_conn(this, id, trace, "{}", length);
std::optional<uint32_t> stream_id = get_stream_id(id);
if (!stream_id.has_value()) {
log_conn(this, id, dbg, "Trying to consume on closed or inexistent connection");
return;
}
if (stream_id.value() == 0) {
// connection's stream is closed
return;
}
int r = http_session_data_consume(m_session.get(), (int32_t) stream_id.value(), length);
if (r != 0) {
log_conn(this, id, err, "Failed to consume data: {} ({})", nghttp2_strerror(r), r);
} else if (0 != http_session_available_to_read(m_session.get(), 0)) {
tcp_socket_set_read_enabled(m_socket.get(), true);
}
}
void Http2Upstream::clean_tcp_connection_data(uint64_t id) {
auto i = m_tcp_connections.find(id);
if (i != m_tcp_connections.end()) {
TcpConnection *conn = &i->second;
if (conn->unread_data != nullptr && conn->unread_data->size() > 0) {
log_conn(this, id, dbg, "Remaining unread={}", conn->unread_data->size());
}
m_conn_id_by_stream_id.erase(conn->stream_id);
m_tcp_connections.erase(i);
log_upstream(
this, dbg, "Remaining connections: {} ({})", m_tcp_connections.size(), m_conn_id_by_stream_id.size());
}
}
size_t Http2Upstream::available_to_send(uint64_t id) {
std::optional<uint32_t> stream_id = get_stream_id(id);
if (!stream_id.has_value()) {
log_conn(this, id, dbg, "Trying to get window size on closed or inexistent connection");
return 0;
}
if (stream_id.value() == 0) {
// connection's stream is closed
return 0;
}
return std::min(tcp_socket_available_to_write(m_socket.get()),
http_session_available_to_write(m_session.get(), (int32_t) stream_id.value()));
}
void Http2Upstream::complete_read(void *arg, TaskId) {
CompleteCtx *ctx = (CompleteCtx *) arg;
Http2Upstream *upstream = ctx->upstream;
auto i = upstream->m_tcp_connections.find(ctx->id);
if (i == upstream->m_tcp_connections.end()) {
return;
}
TcpConnection *conn = &i->second;
conn->complete_read_task_id.release();
int r = upstream->read_out_pending_data(ctx->id, conn);
if (r < 0) {
upstream->close_tcp_connection(ctx->id, false);
} else if (conn->unread_data->size() == 0 && conn->flags.test(TCF_STREAM_CLOSED)) {
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &ctx->id);
upstream->clean_tcp_connection_data(ctx->id);
}
}
int Http2Upstream::verify_callback(X509_STORE_CTX *store_ctx, void *arg) {
auto *self = (Http2Upstream *) arg;
return self->vpn->parameters.cert_verify_handler.func(self->vpn->upstream_config.endpoint->name,
(sockaddr *) &self->vpn->upstream_config.endpoint->address, store_ctx,
self->vpn->parameters.cert_verify_handler.arg);
}
void Http2Upstream::update_flow_control(uint64_t id, TcpFlowCtrlInfo info) {
if (auto i = m_tcp_connections.find(id); i != m_tcp_connections.end()) {
TcpConnection *conn = &i->second;
if (conn->flags.test(TCF_READ_ENABLED) != (info.send_buffer_size > 0)) {
log_conn(this, id, trace, "Read {}", info.send_buffer_size > 0 ? "on" : "off");
conn->flags.set(TCF_READ_ENABLED, info.send_buffer_size > 0);
}
if (!conn->flags.test(TCF_STREAM_CLOSED)) {
int r = http_session_set_recv_window(m_session.get(), (int32_t) conn->stream_id, info.send_window_size);
if (r != 0) {
log_conn(this, id, err, "Failed to set window: {} ({})", nghttp2_strerror(r), r);
}
}
if (conn->flags.test(TCF_READ_ENABLED) && !conn->complete_read_task_id.has_value()
&& conn->unread_data != nullptr && conn->unread_data->size() > 0) {
// we have some unread data on the connection - complete it
conn->complete_read_task_id =
ag::submit(vpn->parameters.ev_loop, {new CompleteCtx{this, id}, complete_read, [](void *arg) {
delete (CompleteCtx *) arg;
}});
}
} else if (m_udp_mux.check_connection(id)) {
m_udp_mux.set_read_enabled(id, info.send_buffer_size > 0);
}
if (info.send_buffer_size > 0 && 0 != http_session_available_to_read(m_session.get(), 0)) {
tcp_socket_set_read_enabled(m_socket.get(), true);
}
}
size_t Http2Upstream::connections_num() const {
return m_tcp_connections.size() + m_udp_mux.connections_num();
}
VpnError Http2Upstream::do_health_check() {
if (m_health_check_info.has_value()) {
log_upstream(this, dbg, "Ignoring as another health check is already in progress");
return {};
}
std::optional<uint32_t> stream_id = send_connect_request(&HEALTH_CHECK_HOST, "");
if (!stream_id.has_value()) {
return {VPN_EC_ERROR, "Failed to send health check request"};
}
m_health_check_info = {stream_id.value()};
return {};
}
VpnConnectionStats Http2Upstream::get_connection_stats() const {
return (m_socket != nullptr) ? tcp_socket_get_stats(m_socket.get()) : VpnConnectionStats{};
}
std::optional<uint64_t> Http2Upstream::send_connect_request_callback(
ServerUpstream *upstream, const TunnelAddress *dst_addr, std::string_view app_name) {
Http2Upstream *self = (Http2Upstream *) upstream;
return self->send_connect_request(dst_addr, app_name);
}
int Http2Upstream::send_data_callback(ServerUpstream *upstream, uint64_t stream_id, U8View data) {
Http2Upstream *self = (Http2Upstream *) upstream;
int r = http_session_send_data(self->m_session.get(), (int32_t) stream_id, data.data(), data.size(), false);
if (r == NGHTTP2_ERR_BUFFER_ERROR) {
log_upstream(self, dbg, "Failed to send data: {} ({})", nghttp2_strerror(r), r);
r = 0;
}
return r;
}
void Http2Upstream::consume_callback(ServerUpstream *upstream, uint64_t stream_id, size_t size) {
Http2Upstream *self = (Http2Upstream *) upstream;
int r = http_session_data_consume(self->m_session.get(), (int32_t) stream_id, size);
if (r != 0) {
log_upstream(self, dbg, "Failed to consume data: {} ({})", nghttp2_strerror(r), r);
}
}
} // namespace ag
+109
View File
@@ -0,0 +1,109 @@
#pragma once
#include <bitset>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "common/logger.h"
#include "http_icmp_multiplexer.h"
#include "http_udp_multiplexer.h"
#include "multiplexable_upstream.h"
#include "net/http_session.h"
#include "net/tcp_socket.h"
#include "vpn/internal/data_buffer.h"
#include "vpn/internal/id_generator.h"
#include "vpn/utils.h"
namespace ag {
class Http2Upstream : public MultiplexableUpstream {
public:
static constexpr int DEFAULT_PORT = 443;
Http2Upstream(const VpnUpstreamProtocolConfig &protocol_config, int id, VpnClient *vpn, SeverHandler handler);
~Http2Upstream() override;
Http2Upstream(const Http2Upstream &) = delete;
Http2Upstream &operator=(const Http2Upstream &) = delete;
Http2Upstream(Http2Upstream &&) = delete;
Http2Upstream &operator=(Http2Upstream &&) = delete;
private:
struct TcpConnection {
enum Flag : int;
uint32_t stream_id = 0;
std::bitset<width_of<Flag>()> flags;
std::unique_ptr<DataBuffer> unread_data;
ag::AutoTaskId complete_read_task_id;
std::optional<ServerError> pending_error; // to store bad HTTP response status until stream processed event
ag::AutoTaskId close_task_id;
};
struct HealthCheckInfo {
uint32_t stream_id = 0;
VpnError error = {};
};
DeclPtr<HttpSession, &http_session_close> m_session;
TcpSocketPtr m_socket;
bool m_in_handler = false;
bool m_closed = false;
std::unordered_map<uint64_t, TcpConnection> m_tcp_connections;
std::unordered_map<uint32_t, uint64_t> m_conn_id_by_stream_id;
HttpUdpMultiplexer m_udp_mux;
HttpIcmpMultiplexer m_icmp_mux;
uint64_t m_next_session_id = 0;
std::string m_credentials;
std::optional<HealthCheckInfo> m_health_check_info;
// For client initiated streams ids are odd numbers
// https://tools.ietf.org/html/rfc7540#section-5.1.1
IdGenerator m_stream_id_generator{2};
ag::Logger m_log{"H2_UPSTREAM"};
bool open_session(uint32_t timeout_ms) override;
void close_session() override;
void close_connection(uint64_t id, bool graceful, bool async) override;
ssize_t send(uint64_t id, const uint8_t *data, size_t length) override;
void consume(uint64_t id, size_t length) override;
size_t available_to_send(uint64_t id) override;
void update_flow_control(uint64_t id, TcpFlowCtrlInfo info) override;
VpnError do_health_check() override;
[[nodiscard]] VpnConnectionStats get_connection_stats() const override;
[[nodiscard]] size_t connections_num() const override;
bool open_connection(uint64_t id, const TunnelAddressPair *addr, int proto, std::string_view app_name) override;
void on_icmp_request(IcmpEchoRequestEvent &event) override;
static void http_handler(void *arg, HttpEventId what, void *data);
static void net_handler(void *arg, TcpSocketEvent what, void *data);
static void complete_read(void *arg, TaskId task_id);
static int verify_callback(X509_STORE_CTX *store_ctx, void *arg);
int establish_http_session();
/**
* @param dst_addr destination address
* @param app_name the name of the appllication that initiated this connection (may be empty)
* @return a new stream id, or nullopt if unsuccessful
*/
std::optional<uint32_t> send_connect_request(const TunnelAddress *dst_addr, std::string_view app_name);
void close_session_inner();
void clean_tcp_connection_data(uint64_t id);
int handle_read(uint64_t id, const uint8_t *data, size_t length);
void handle_response(const HttpHeadersEvent *http_event);
void close_tcp_connection(uint64_t id, bool graceful);
[[nodiscard]] std::optional<uint32_t> get_stream_id(uint64_t id) const;
std::pair<uint64_t, TcpConnection *> get_conn_by_stream_id(uint32_t id);
int read_out_pending_data(uint64_t id, TcpConnection *conn);
static std::optional<uint64_t> send_connect_request_callback(
ServerUpstream *upstream, const TunnelAddress *dst_addr, std::string_view app_name);
static int send_data_callback(ServerUpstream *upstream, uint64_t stream_id, U8View data);
static void consume_callback(ServerUpstream *upstream, uint64_t stream_id, size_t size);
};
} // namespace ag
File diff suppressed because it is too large Load Diff
+157
View File
@@ -0,0 +1,157 @@
#pragma once
#include <bitset>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "vpn/platform.h" // Because quiche.h doesn't include the required headers
#include <quiche.h>
#include "common/logger.h"
#include "http_icmp_multiplexer.h"
#include "http_udp_multiplexer.h"
#include "net/udp_socket.h"
#include "vpn/internal/data_buffer.h"
#include "vpn/internal/server_upstream.h"
#include "vpn/utils.h"
namespace ag {
class Http3Upstream : public ServerUpstream {
public:
Http3Upstream(int id, const VpnUpstreamProtocolConfig &protocol_config);
~Http3Upstream() override;
Http3Upstream(const Http3Upstream &) = delete;
Http3Upstream &operator=(const Http3Upstream &) = delete;
Http3Upstream(Http3Upstream &&) = delete;
Http3Upstream &operator=(Http3Upstream &&) = delete;
private:
enum State : int;
enum Http3ErrorCode : uint64_t;
struct SendConnectRequestResult {
std::optional<uint64_t> stream_id; // stream ID if successful
bool is_retriable = false; // true if request failed with non-fatal error
};
struct TcpConnection {
enum Flag : int;
uint64_t stream_id = 0;
std::bitset<width_of<Flag>()> flags;
/**
* `SERVER_EVENT_GET_AVAILABLE_TO_SEND` is an optimization - not a requirement,
* some of client listener implementations (particularly TUN device listener) may report
* the greater number of bytes than it can actually send.
* So this buffer is a workaround for such cases.
*/
std::unique_ptr<DataBuffer> unread_data;
std::optional<ServerError> pending_error;
size_t sent_bytes_to_notify = 0;
[[nodiscard]] bool has_unread_data() const;
};
struct RetriableTcpConnectRequest {
TunnelAddress dst_addr;
std::string app_name;
};
struct HealthCheckInfo {
std::optional<uint64_t> stream_id;
ag::AutoTaskId retry_task_id;
ag::AutoTaskId timeout_task_id;
VpnError error = {};
};
State m_state = (State) 0;
std::chrono::milliseconds m_max_idle_timeout;
UdpSocketPtr m_socket;
DeclPtr<quiche_conn, &quiche_conn_free> m_quic_conn;
DeclPtr<quiche_h3_conn, &quiche_h3_conn_free> m_h3_conn;
std::unordered_map<uint64_t, TcpConnection> m_tcp_connections;
std::unordered_map<uint64_t, uint64_t> m_tcp_conn_by_stream_id;
std::unordered_map<uint64_t, RetriableTcpConnectRequest> m_retriable_tcp_requests;
std::unordered_map<uint64_t, bool> m_closing_connections; // value is graceful flag
ag::AutoTaskId m_complete_read_task_id;
ag::AutoTaskId m_notify_sent_task_id;
ag::AutoTaskId m_close_connections_task_id;
ag::AutoTaskId m_post_receive_task_id;
ag::AutoTaskId m_flush_error_task_id;
HttpUdpMultiplexer m_udp_mux;
HttpIcmpMultiplexer m_icmp_mux;
DeclPtr<event, &event_free> m_quic_timer;
std::string m_credentials;
std::optional<HealthCheckInfo> m_health_check_info;
bool m_in_handler = false;
bool m_closed = false; // @todo: seems like it can be replaced by a separate state
ag::Logger m_log{"H3_UPSTREAM"};
/**
* A point in time when our idle timer expires.
*
* We maintain our own idle timer because Quiche doesn't check for idle timeout before sending, leading to
* erroneus idle timer reset when data is sent after a long period of inactivity, such as system sleep.
*
* We assume that idle timeout is equal to `m_max_idle_timeout` and reset the timer whenever
* we send or receive a packet. Since outdated received packets are discarded by `udp_socket`,
* idle timeout won't be reset errouneusly when receiving a stale packet after waking from sleep.
*/
std::optional<int64_t> m_idle_timeout_at_ns;
ag::AutoTaskId m_close_on_idle_task_id;
bool init(VpnClient *vpn, SeverHandler handler) override;
void deinit() override;
bool open_session(uint32_t timeout_ms) override;
void close_session() override;
uint64_t open_connection(const TunnelAddressPair *addr, int proto, std::string_view app_name) override;
void close_connection(uint64_t id, bool graceful, bool async) override;
ssize_t send(uint64_t id, const uint8_t *data, size_t length) override;
void consume(uint64_t id, size_t length) override;
size_t available_to_send(uint64_t id) override;
void update_flow_control(uint64_t id, TcpFlowCtrlInfo info) override;
VpnError do_health_check() override;
[[nodiscard]] VpnConnectionStats get_connection_stats() const override;
void on_icmp_request(IcmpEchoRequestEvent &event) override;
void handle_sleep() override;
void handle_wake() override;
static void quic_timer_callback(evutil_socket_t, short, void *arg);
static void socket_handler(void *arg, UdpSocketEvent what, void *data);
static int verify_callback(X509_STORE_CTX *store_ctx, void *arg);
bool flush_pending_quic_data();
void handle_socket_data(U8View data);
bool initiate_h3_session();
std::pair<uint64_t, TcpConnection *> get_tcp_conn_by_stream_id(uint64_t id);
void handle_h3_event(quiche_h3_event *h3_event, uint64_t stream_id);
void handle_response(uint64_t stream_id, const HttpHeaders *headers);
void close_stream(uint64_t stream_id, Http3ErrorCode error);
ssize_t read_out_h3_data(uint64_t stream_id, uint8_t *buffer, size_t cap);
void process_pending_data(uint64_t stream_id);
void close_session_inner();
SendConnectRequestResult send_connect_request(const TunnelAddress *dst_addr, std::string_view app_name);
void close_tcp_connection(uint64_t id, bool graceful);
void clean_tcp_connection_data(uint64_t id);
[[nodiscard]] bool is_health_check_stream(uint64_t stream_id) const;
[[nodiscard]] std::optional<uint64_t> get_stream_id(uint64_t id) const;
bool push_unread_data(uint64_t conn_id, TcpConnection *conn, U8View data) const;
int read_out_pending_data(uint64_t conn_id, TcpConnection *conn);
int raise_read_event(uint64_t conn_id, U8View data);
void poll_tcp_connections();
void retry_connect_requests();
static void complete_read(void *arg, TaskId task_id);
static std::optional<uint64_t> mux_send_connect_request_callback(
ServerUpstream *upstream, const TunnelAddress *dst_addr, std::string_view app_name);
static int mux_send_data_callback(ServerUpstream *upstream, uint64_t stream_id, U8View data);
static void mux_consume_callback(ServerUpstream *, uint64_t, size_t);
};
} // namespace ag
+142
View File
@@ -0,0 +1,142 @@
#include "http_icmp_multiplexer.h"
#include "vpn/internal/wire_utils.h"
namespace ag {
enum HttpIcmpMultiplexer::State : uint32_t {
MS_IDLE,
MS_ESTABLISHED,
};
/**
* Outgoing ICMP packet format (sent from us to the endpoint)
* <p>
* +----------+---------------------+-----------------+---------------+-----------+
* | ID | Destination address | Sequence number | TTL/Hop limit | Data size |
* | 2 bytes | 16 bytes | 2 bytes | 1 byte | 2 bytes |
* +----------+---------------------+-----------------+---------------+-----------+
* <p>
* Incoming ICMP packet format (sent from the endpoint to us)
* <p>
* +----------+----------------+--------+--------+-----------------+
* | ID | Source address | Type | Code | Sequence number |
* | 2 bytes | 16 bytes | 1 byte | 1 byte | 2 bytes |
* +----------+----------------+--------+--------+-----------------+
*/
static constexpr size_t ICMPPKT_ID_SIZE = 2;
static constexpr size_t ICMPPKT_ADDR_SIZE = 16;
static constexpr size_t ICMPPKT_SEQNO_SIZE = 2;
static constexpr size_t ICMPPKT_TTL_SIZE = 1;
static constexpr size_t ICMPPKT_DATA_SIZE = 2;
static constexpr size_t ICMPPKT_TYPE_SIZE = 1;
static constexpr size_t ICMPPKT_CODE_SIZE = 1;
static constexpr size_t ICMPPKT_REQ_SIZE =
ICMPPKT_ID_SIZE + ICMPPKT_ADDR_SIZE + ICMPPKT_SEQNO_SIZE + ICMPPKT_TTL_SIZE + ICMPPKT_DATA_SIZE;
static constexpr size_t ICMPPKT_REPLY_SIZE =
ICMPPKT_ID_SIZE + ICMPPKT_ADDR_SIZE + ICMPPKT_TYPE_SIZE + ICMPPKT_CODE_SIZE + ICMPPKT_SEQNO_SIZE;
HttpIcmpMultiplexer::HttpIcmpMultiplexer(HttpIcmpMultiplexerParameters parameters)
: m_params(std::move(parameters)) {
}
HttpIcmpMultiplexer::~HttpIcmpMultiplexer() = default;
void HttpIcmpMultiplexer::close() {
m_state = MS_IDLE;
m_stream_id.reset();
m_reply_buffer.clear();
}
std::optional<uint64_t> HttpIcmpMultiplexer::get_stream_id() const {
return m_stream_id;
}
bool HttpIcmpMultiplexer::send_request(const IcmpEchoRequest &request) {
ServerUpstream *upstream = m_params.parent;
switch (m_state) {
case MS_IDLE:
assert(!m_stream_id.has_value());
static const TunnelAddress ICMP_HOST(NamePort{"_icmp", 0});
m_stream_id = m_params.send_connect_request_callback(upstream, &ICMP_HOST, "_icmp");
if (!m_stream_id.has_value()) {
return false;
}
m_stream_id = m_stream_id.value();
m_state = MS_ESTABLISHED;
[[fallthrough]];
case MS_ESTABLISHED:
return this->send_request_established(request);
}
return true;
}
void HttpIcmpMultiplexer::handle_response(const HttpHeaders *) {
assert(m_state == MS_ESTABLISHED);
// if failed, `close` will be called after the stream close
}
int HttpIcmpMultiplexer::process_read_event(U8View data) {
assert(m_state == MS_ESTABLISHED);
size_t data_size = data.size();
while (!data.empty()) {
data = this->process_reply_chunk(data);
}
m_params.consume_callback(m_params.parent, m_stream_id.value(), data_size);
return 0;
}
bool HttpIcmpMultiplexer::send_request_established(const IcmpEchoRequest &request) {
uint8_t packet_buffer[ICMPPKT_REQ_SIZE];
wire_utils::Writer writer({packet_buffer, sizeof(packet_buffer)});
writer.put_u16(request.id);
writer.put_ip_padded((sockaddr *) &request.peer);
writer.put_u16(request.seqno);
writer.put_u8(request.ttl);
writer.put_u16(request.data_size);
int r = m_params.send_data_callback(m_params.parent, m_stream_id.value(), {packet_buffer, sizeof(packet_buffer)});
return r == 0;
}
U8View HttpIcmpMultiplexer::process_reply_chunk(U8View data) {
U8View raw_reply;
if (m_reply_buffer.empty() && data.size() >= ICMPPKT_REPLY_SIZE) {
raw_reply = data.substr(0, ICMPPKT_REPLY_SIZE);
data.remove_prefix(ICMPPKT_REPLY_SIZE);
} else {
m_reply_buffer.reserve(ICMPPKT_REPLY_SIZE);
size_t to_read = std::min(data.size(), ICMPPKT_REPLY_SIZE - m_reply_buffer.size());
m_reply_buffer.insert(m_reply_buffer.end(), data.begin(), std::next(data.begin(), (ssize_t) to_read));
data.remove_prefix(to_read);
if (m_reply_buffer.size() < ICMPPKT_REPLY_SIZE) {
return data;
}
assert(m_reply_buffer.size() == ICMPPKT_REPLY_SIZE);
raw_reply = {m_reply_buffer.data(), ICMPPKT_REPLY_SIZE};
}
wire_utils::Reader reader(raw_reply);
IcmpEchoReply reply = {};
reply.id = reader.get_u16().value();
reply.peer = reader.get_ip_padded().value();
reply.type = reader.get_u8().value();
reply.code = reader.get_u8().value();
reply.seqno = reader.get_u16().value();
SeverHandler handler = m_params.parent->handler;
handler.func(handler.arg, SERVER_EVENT_ECHO_REPLY, &reply);
m_reply_buffer.clear();
return data;
}
} // namespace ag
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include <optional>
#include <string_view>
#include <vector>
#include "vpn/internal/server_upstream.h"
#include "vpn/internal/vpn_client.h"
#include "vpn/platform.h"
namespace ag {
struct HttpIcmpMultiplexerParameters {
ServerUpstream *parent = nullptr;
/** @return stream id if sent successfully, none otherwise */
std::optional<uint64_t> (*send_connect_request_callback)(
ServerUpstream *upstream, const TunnelAddress *dst_addr, std::string_view app_name) = nullptr;
/** @return 0 in case of success, non-zero value otherwise */
int (*send_data_callback)(ServerUpstream *upstream, uint64_t stream_id, U8View data) = nullptr;
void (*consume_callback)(ServerUpstream *upstream, uint64_t stream_id, size_t size) = nullptr;
};
/**
* Multiplexes ICMP traffic into a single HTTP stream
*/
class HttpIcmpMultiplexer {
public:
explicit HttpIcmpMultiplexer(HttpIcmpMultiplexerParameters parameters);
~HttpIcmpMultiplexer();
HttpIcmpMultiplexer() = delete;
HttpIcmpMultiplexer(const HttpIcmpMultiplexer &) = delete;
HttpIcmpMultiplexer &operator=(const HttpIcmpMultiplexer &) = delete;
HttpIcmpMultiplexer(HttpIcmpMultiplexer &&) = delete;
HttpIcmpMultiplexer &operator=(HttpIcmpMultiplexer &&) = delete;
/**
* Close multiplexer
*/
void close();
/**
* @brief Get id of stream which is currently used for ICMP traffic
*/
[[nodiscard]] std::optional<uint64_t> get_stream_id() const;
/**
* Send an ICMP request
*/
bool send_request(const IcmpEchoRequest &request);
/**
* Process data from VPN endpoint
* @return 0 if successful
*/
int process_read_event(U8View data);
/**
* Handle response to stream creation request
*/
void handle_response(const HttpHeaders *response);
private:
enum State : uint32_t;
HttpIcmpMultiplexerParameters m_params = {};
State m_state = (State) 0;
std::optional<uint64_t> m_stream_id;
std::vector<uint8_t> m_reply_buffer;
bool send_request_established(const IcmpEchoRequest &request);
U8View process_reply_chunk(U8View data);
};
} // namespace ag
+447
View File
@@ -0,0 +1,447 @@
#include "http_udp_multiplexer.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <string_view>
#include "common/net_utils.h"
#include "vpn/internal/vpn_client.h"
#include "vpn/internal/wire_utils.h"
#include "vpn/utils.h"
#define log_mux(mux_, lvl_, fmt_, ...) \
lvl_##log((mux_)->m_log, "[{}] [SID:{}] " fmt_, (mux_)->m_id, (mux_)->m_stream_id, ##__VA_ARGS__)
#define log_conn(mux_, cid_, lvl_, fmt_, ...) \
lvl_##log((mux_)->m_log, "[{}] [SID:{}-R:{}] " fmt_, (mux_)->m_id, (mux_)->m_stream_id, (uint64_t) (cid_), \
##__VA_ARGS__)
using namespace std::chrono;
namespace ag {
struct CompleteCtx {
HttpUdpMultiplexer *multiplexer;
uint64_t id;
};
static std::atomic_int g_next_mux_id = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static std::vector<uint8_t> compose_udp_packet(
const sockaddr *src, const sockaddr *dst, std::string_view app_name, const uint8_t *data, size_t length) {
size_t full_length = UDPPKT_IN_PREFIX_SIZE + UDPPKT_APPLEN_SIZE + app_name.size() + length;
std::vector<uint8_t> packet_buffer(full_length);
wire_utils::Writer writer({packet_buffer.data(), packet_buffer.size()});
writer.put_u32(full_length - UDPPKT_LENGTH_SIZE);
writer.put_ip_padded(src);
writer.put_u16(sockaddr_get_port(src));
writer.put_ip_padded(dst);
writer.put_u16(sockaddr_get_port(dst));
app_name = app_name.substr(0, UINT8_MAX);
writer.put_u8(app_name.size());
writer.put_data({(uint8_t *) app_name.data(), app_name.size()});
writer.put_data({data, length});
return packet_buffer;
}
HttpUdpMultiplexer::HttpUdpMultiplexer(HttpUdpMultiplexerParameters parameters)
: m_params(std::move(parameters))
, m_id(g_next_mux_id++) {
}
HttpUdpMultiplexer::~HttpUdpMultiplexer() = default;
void HttpUdpMultiplexer::close(ServerError serv_err) {
ServerUpstream *upstream = m_params.parent;
std::optional<ServerError> error = m_pending_error;
if (!error.has_value() && serv_err.error.code != 0) {
error = serv_err;
}
for (auto i = m_connections.begin(); i != m_connections.end();) {
auto next = std::next(i);
uint64_t id = i->first;
if (error.has_value()) {
error->id = id;
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_ERROR, &error.value());
} else {
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &id);
}
this->clean_connection_data(id);
i = next;
}
log_mux(this, trace, "Closed: {} ({})", safe_to_string_view(serv_err.error.text), serv_err.error.code);
reset();
}
void HttpUdpMultiplexer::reset() {
m_state = MS_IDLE;
m_stream_id = 0;
m_addr_to_id.clear();
m_recv_connection = (RecvConnection){};
m_timer_event.reset();
}
void HttpUdpMultiplexer::complete_udp_connection(void *arg, TaskId) {
auto *ctx = (CompleteCtx *) arg;
HttpUdpMultiplexer *mux = ctx->multiplexer;
if (mux->m_state == MS_ESTABLISHED) {
ServerUpstream *upstream = mux->m_params.parent;
auto i = mux->m_connections.find(ctx->id);
if (i != mux->m_connections.end()) {
Connection *conn = &i->second;
conn->open_task_id.release();
mux->m_addr_to_id[conn->addr] = ctx->id;
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_CONNECTION_OPENED, &ctx->id);
}
}
}
bool HttpUdpMultiplexer::open_connection(uint64_t conn_id, const TunnelAddressPair *addr, std::string_view app_name) {
if (std::get_if<sockaddr_storage>(&addr->dst) == nullptr) {
log_conn(this, conn_id, err, "UDP connection must have socket address as destination");
assert(0);
return false;
}
auto i = m_connections.find(conn_id);
if (i != m_connections.end()) {
log_conn(this, conn_id, err, "Connection with such id already exists");
assert(0);
return false;
}
ServerUpstream *upstream = m_params.parent;
switch (m_state) {
case MS_IDLE: {
assert(m_stream_id == 0);
static const TunnelAddress UDP_HOST(NamePort{"_udp2", 0});
std::optional<uint64_t> stream_id = m_params.send_connect_request_callback(upstream, &UDP_HOST, "_udp2");
if (!stream_id.has_value()) {
return false;
}
m_stream_id = stream_id.value();
m_state = MS_ESTABLISHED;
[[fallthrough]];
}
case MS_ESTABLISHED:
Connection *conn = &m_connections[conn_id];
conn->timeout = steady_clock::now() + milliseconds(VPN_DEFAULT_UDP_TIMEOUT_MS);
conn->open_task_id = ag::submit(upstream->vpn->parameters.ev_loop,
{new CompleteCtx{this, conn_id}, complete_udp_connection, [](void *arg) {
delete (CompleteCtx *) arg;
}});
break;
}
Connection *conn = &m_connections[conn_id];
conn->addr = *addr;
conn->app_name = app_name;
return true;
}
void HttpUdpMultiplexer::close_connection(uint64_t id, bool async) {
if (!async) {
close_connection(id);
return;
}
ServerUpstream *upstream = m_params.parent;
auto i = m_connections.find(id);
if (i == m_connections.end()) {
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &id);
return;
}
struct CloseCtx {
HttpUdpMultiplexer *mux;
uint64_t id;
};
Connection *conn = &i->second;
conn->close_task_id = ag::submit(upstream->vpn->parameters.ev_loop,
{
new CloseCtx{this, id},
[](void *arg, TaskId) {
auto *ctx = (CloseCtx *) arg;
ctx->mux->close_connection(ctx->id);
},
[](void *arg) {
delete (CloseCtx *) arg;
},
});
}
void HttpUdpMultiplexer::close_connection(uint64_t id) {
if (!clean_connection_data(id)) {
return;
}
if (m_recv_connection.id == id && m_recv_connection.state == RCS_PAYLOAD) {
// closed connection is the one for which we are receiving packet,
// so drop the rest of the packet
m_recv_connection.state = RCS_DROPPING;
}
SeverHandler *handler = &m_params.parent->handler;
handler->func(handler->arg, SERVER_EVENT_CONNECTION_CLOSED, &id);
}
bool HttpUdpMultiplexer::check_connection(uint64_t id) const {
return m_connections.count(id) != 0;
}
ssize_t HttpUdpMultiplexer::send(uint64_t id, U8View data) {
assert(m_state == MS_ESTABLISHED);
auto i = m_connections.find(id);
if (i == m_connections.end()) {
return -1;
}
Connection *conn = &i->second;
const sockaddr *dst = (sockaddr *) std::get_if<sockaddr_storage>(&conn->addr.dst);
log_conn(this, id, trace, "Sending UDP packet: {}->{} len={}", sockaddr_to_str((sockaddr *) &conn->addr.src),
sockaddr_to_str(dst), data.size());
std::vector<uint8_t> packet =
compose_udp_packet((sockaddr *) &conn->addr.src, dst, conn->app_name, data.data(), data.size());
int r = m_params.send_data_callback(m_params.parent, m_stream_id, {packet.data(), packet.size()});
if (r == 0) {
conn->timeout = steady_clock::now() + milliseconds(VPN_DEFAULT_UDP_TIMEOUT_MS);
conn->sent_bytes_since_flush += data.size();
r = data.size();
}
return r;
}
HttpUdpMultiplexer::PacketInfo HttpUdpMultiplexer::read_prefix(const std::vector<uint8_t> &data) const {
assert(data.size() == UDPPKT_IN_PREFIX_SIZE);
PacketInfo info = {NON_ID, 0};
wire_utils::Reader reader({data.data(), UDPPKT_IN_PREFIX_SIZE});
uint32_t length = reader.get_u32().value();
if (length < (UDPPKT_IN_PREFIX_SIZE - UDPPKT_LENGTH_SIZE)) {
log_mux(this, dbg, "Drop packet as its length less than the prefix size ({})", length);
return info;
}
sockaddr_storage src_addr = reader.get_ip_padded().value();
sockaddr_set_port((sockaddr *) &src_addr, reader.get_u16().value());
sockaddr_storage dst_addr = reader.get_ip_padded().value();
sockaddr_set_port((sockaddr *) &dst_addr, reader.get_u16().value());
log_mux(this, trace, "Got UDP packet: {}->{} len={}", sockaddr_to_str((sockaddr *) &src_addr),
sockaddr_to_str((sockaddr *) &dst_addr), length);
if (length > MAX_UDP_IN_PACKET_LENGTH) {
log_mux(this, dbg, "Drop packet as its length more than the maximum allowed value ({})", length);
return info;
}
info.payload_length = length - (UDPPKT_IN_PREFIX_SIZE - UDPPKT_LENGTH_SIZE);
auto i = m_addr_to_id.find({(sockaddr *) &dst_addr, (sockaddr *) &src_addr});
if (i != m_addr_to_id.end()) {
info.id = i->second;
log_conn(this, info.id, trace, "Payload length: {}", info.payload_length);
} else {
log_mux(this, dbg, "Connection has already been closed or never existed: {}->{}",
sockaddr_to_str((sockaddr *) &src_addr), sockaddr_to_str((sockaddr *) &dst_addr));
}
return info;
}
int HttpUdpMultiplexer::process_read_event(U8View data) {
assert(m_state == MS_ESTABLISHED);
ServerUpstream *upstream = m_params.parent;
RecvConnection *rconn = &m_recv_connection;
size_t data_size = data.size();
while (!data.empty()) {
switch (rconn->state) {
case RCS_IDLE: {
assert(rconn->buffer.size() < UDPPKT_IN_PREFIX_SIZE);
rconn->buffer.reserve(UDPPKT_IN_PREFIX_SIZE);
size_t to_read = std::min(UDPPKT_IN_PREFIX_SIZE - rconn->buffer.size(), data.length());
rconn->buffer.insert(rconn->buffer.end(), data.data(), data.data() + to_read);
if (rconn->buffer.size() < UDPPKT_IN_PREFIX_SIZE) {
data.remove_prefix(to_read);
break;
}
assert(rconn->buffer.size() == UDPPKT_IN_PREFIX_SIZE);
PacketInfo info = read_prefix(rconn->buffer);
bool drop_packet = false;
if (info.id == NON_ID) {
// logged in `read_prefix`
drop_packet = true;
} else if (auto i = m_connections.find(info.id); i == m_connections.end()) {
log_conn(this, info.id, dbg, "No such connection in table, dropping packet");
drop_packet = true;
} else if (!i->second.read_enabled) {
log_conn(this, info.id, dbg, "Read is disabled, dropping packet");
drop_packet = true;
}
if (!drop_packet) {
rconn->state = RCS_PAYLOAD;
rconn->id = info.id;
rconn->buffer.reserve(info.payload_length);
} else {
rconn->state = RCS_DROPPING;
}
rconn->bytes_left = info.payload_length;
rconn->buffer.clear();
data.remove_prefix(to_read);
break;
}
case RCS_PAYLOAD: {
m_connections[rconn->id].timeout = steady_clock::now() + milliseconds(VPN_DEFAULT_UDP_TIMEOUT_MS);
size_t to_read = std::min(rconn->bytes_left, data.length());
rconn->buffer.insert(rconn->buffer.end(), data.data(), data.data() + to_read);
data.remove_prefix(to_read);
assert(rconn->bytes_left >= to_read);
rconn->bytes_left -= to_read;
if (rconn->bytes_left == 0) {
ServerReadEvent serv_event = {rconn->id, rconn->buffer.data(), rconn->buffer.size(), 0};
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_READ, &serv_event);
*rconn = (RecvConnection){};
}
break;
}
case RCS_DROPPING: {
size_t to_drop = std::min(rconn->bytes_left, data.length());
data.remove_prefix(to_drop);
assert(rconn->bytes_left >= to_drop);
rconn->bytes_left -= to_drop;
if (rconn->bytes_left == 0) {
*rconn = (RecvConnection){};
}
break;
}
}
}
// consume the whole packet as it must be fully processed in the loop above
m_params.consume_callback(upstream, m_stream_id, data_size);
return 0;
}
void HttpUdpMultiplexer::timer_callback(evutil_socket_t, short, void *arg) {
auto *multiplexer = (HttpUdpMultiplexer *) arg;
ServerUpstream *upstream = multiplexer->m_params.parent;
time_point<steady_clock> now = steady_clock::now();
for (auto i = multiplexer->m_connections.begin(); i != multiplexer->m_connections.end();) {
auto next = std::next(i);
Connection *conn = &i->second;
if (conn->timeout <= now) {
uint64_t id = i->first;
log_conn(multiplexer, id, trace, "Timed out");
multiplexer->clean_connection_data(id);
upstream->handler.func(upstream->handler.arg, SERVER_EVENT_CONNECTION_CLOSED, &id);
}
i = next;
}
}
void HttpUdpMultiplexer::handle_response(const HttpHeaders *response) {
assert(m_state == MS_ESTABLISHED);
if (response->status_code != 200) {
// will be raised in `close` after stream close
m_pending_error = {0, {ag::utils::AG_ECONNREFUSED, "HTTP stream creation failed"}};
return;
}
if (m_timer_event == nullptr) {
ServerUpstream *upstream = m_params.parent;
m_timer_event.reset(event_new(
vpn_event_loop_get_base(upstream->vpn->parameters.ev_loop), -1, EV_PERSIST, timer_callback, this));
struct timeval tv = ms_to_timeval(VPN_DEFAULT_UDP_TIMEOUT_MS);
event_add(m_timer_event.get(), &tv);
}
}
bool HttpUdpMultiplexer::clean_connection_data(uint64_t id) {
auto i = m_connections.find(id);
if (i == m_connections.end()) {
return false;
}
m_addr_to_id.erase(i->second.addr);
m_connections.erase(i);
log_mux(this, dbg, "Remaining connections: {}", m_connections.size());
return true;
}
std::optional<uint64_t> HttpUdpMultiplexer::get_stream_id() const {
std::optional<uint64_t> out;
switch (m_state) {
case MS_IDLE:
break;
case MS_ESTABLISHED:
out = m_stream_id;
break;
}
return out;
}
void HttpUdpMultiplexer::report_sent_bytes() {
for (auto &[id, conn] : m_connections) {
if (conn.sent_bytes_since_flush > 0) {
ServerDataSentEvent serv_event = {id, conn.sent_bytes_since_flush};
m_params.parent->handler.func(m_params.parent->handler.arg, SERVER_EVENT_DATA_SENT, &serv_event);
conn.sent_bytes_since_flush = 0;
}
}
}
void HttpUdpMultiplexer::set_read_enabled(uint64_t id, bool v) {
auto i = m_connections.find(id);
if (i != m_connections.end()) {
i->second.read_enabled = v;
}
}
size_t HttpUdpMultiplexer::connections_num() const {
return m_connections.size();
}
} // namespace ag
+187
View File
@@ -0,0 +1,187 @@
#pragma once
#include <chrono>
#include <optional>
#include <unordered_map>
#include <vector>
#include "vpn/internal/server_upstream.h"
#include "vpn/internal/vpn_client.h"
#include "vpn/platform.h"
namespace ag {
// clang-format off
/*
* Incoming UDP packet format (sent from the endpoint to us)
* <p>
* +----------+----------------+-------------+---------------------+------------------+---------+
* | Length | Source address | Source port | Destination address | Destination port | Payload |
* | 4 bytes | 16 bytes | 2 bytes | 16 bytes | 2 bytes | N bytes |
* +----------+----------------+-------------+---------------------+------------------+---------+
* <p>
* Outgoing UDP packet format (sent from us to the endpoint)
* <p>
* +----------+----------------+-------------+---------------------+------------------+------------------+----------+---------+
* | Length | Source address | Source port | Destination address | Destination port | App name len (L) | App name | Payload |
* | 4 bytes | 16 bytes | 2 bytes | 16 bytes | 2 bytes | 1 byte | L bytes | N bytes |
* +----------+----------------+-------------+---------------------+------------------+------------------+----------+---------+
*/
// clang-format on
static constexpr size_t UDPPKT_LENGTH_SIZE = 4;
static constexpr size_t UDPPKT_ADDR_SIZE = 16;
static constexpr size_t UDPPKT_PORT_SIZE = 2;
static constexpr size_t UDPPKT_IN_PREFIX_SIZE = UDPPKT_LENGTH_SIZE + 2 * (UDPPKT_ADDR_SIZE + UDPPKT_PORT_SIZE);
static constexpr size_t UDPPKT_APPLEN_SIZE = 1;
static constexpr size_t UDPPKT_APP_MAXSIZE = 255;
static constexpr size_t MAX_UDP_PAYLOAD_SIZE = 65535 - 8; // 8 bytes header
static constexpr size_t MAX_UDP_IN_PACKET_LENGTH = MAX_UDP_PAYLOAD_SIZE + UDPPKT_IN_PREFIX_SIZE - UDPPKT_LENGTH_SIZE;
struct HttpUdpMultiplexerParameters {
ServerUpstream *parent = nullptr;
/** @return stream id if sent successfully, none otherwise */
std::optional<uint64_t> (*send_connect_request_callback)(
ServerUpstream *upstream, const TunnelAddress *dst_addr, std::string_view app_name) = nullptr;
/** @return 0 in case of success, non-zero value otherwise */
int (*send_data_callback)(ServerUpstream *upstream, uint64_t stream_id, U8View data) = nullptr;
void (*consume_callback)(ServerUpstream *upstream, uint64_t stream_id, size_t size) = nullptr;
};
/**
* Multiplexes UDP traffic into a single HTTP stream
*/
class HttpUdpMultiplexer {
public:
explicit HttpUdpMultiplexer(HttpUdpMultiplexerParameters parameters);
~HttpUdpMultiplexer();
HttpUdpMultiplexer() = delete;
HttpUdpMultiplexer(const HttpUdpMultiplexer &) = delete;
HttpUdpMultiplexer &operator=(const HttpUdpMultiplexer &) = delete;
HttpUdpMultiplexer(HttpUdpMultiplexer &&) = delete;
HttpUdpMultiplexer &operator=(HttpUdpMultiplexer &&) = delete;
/**
* Reset multiplexer to idle state
*/
void reset();
/**
* Close multiplexer with error (if non-zero)
*/
void close(ServerError serv_err);
/**
* @brief Get id of stream which is currently used for UDP traffic
*/
[[nodiscard]] std::optional<uint64_t> get_stream_id() const;
/**
* Open new UDP "connection"
*/
bool open_connection(uint64_t conn_id, const TunnelAddressPair *addr, std::string_view app_name);
/**
* Close connection
*/
void close_connection(uint64_t id, bool async);
/**
* Check if multiplexer has a connection with the given identifier
* @return true if the connection exists, false otherwise
*/
[[nodiscard]] bool check_connection(uint64_t id) const;
/**
* Send data via connection
*/
ssize_t send(uint64_t id, U8View data);
/**
* Process data from VPN server
* @return 0 if successful
*/
int process_read_event(U8View data);
/**
* Handle response to stream creation request
*/
void handle_response(const HttpHeaders *response);
/**
* Raise `SERVER_EVENT_DATA_SENT` for each connection that have non-zero sent bytes counter
*/
void report_sent_bytes();
/**
* Turn on/off read events for connection.
* Incoming packets are dropped in case read events are turned off.
* @param id connection id
* @param v whether read events should be tuned on/off
*/
void set_read_enabled(uint64_t id, bool v);
/**
* Get the current number of UDP connections
*/
[[nodiscard]] size_t connections_num() const;
private:
enum MultiplexerState {
MS_IDLE,
MS_ESTABLISHED,
};
enum RecvConnectionState {
RCS_IDLE,
RCS_PAYLOAD, // receiving payload
RCS_DROPPING, // dropping data of invalid packet
};
struct Connection {
bool read_enabled = false; // if true `SERVER_EVENT_READ` can be raised
TunnelAddressPair addr;
std::string app_name;
size_t sent_bytes_since_flush = 0; // number of bytes sent since last socket write buffer flush
std::chrono::time_point<std::chrono::steady_clock> timeout;
ag::AutoTaskId open_task_id;
ag::AutoTaskId close_task_id;
};
struct RecvConnection {
RecvConnectionState state = RCS_IDLE;
uint64_t id = NON_ID;
size_t bytes_left = 0;
std::vector<uint8_t> buffer;
};
struct PacketInfo {
uint64_t id; // NON_ID in case of error
size_t payload_length; // raw payload length
};
HttpUdpMultiplexerParameters m_params = {};
MultiplexerState m_state = MS_IDLE;
uint64_t m_stream_id = 0;
RecvConnection m_recv_connection = {};
std::unordered_map<TunnelAddressPair, uint64_t> m_addr_to_id;
std::unordered_map<uint64_t, Connection> m_connections;
EventPtr m_timer_event = nullptr;
std::optional<ServerError> m_pending_error;
ag::Logger m_log{"UDP_MUX"};
int m_id;
static void complete_udp_connection(void *arg, TaskId task_id);
static void timer_callback(evutil_socket_t, short, void *arg);
[[nodiscard]] PacketInfo read_prefix(const std::vector<uint8_t> &data) const;
/**
* @return true if a connection with such id existed, false otherwise
*/
bool clean_connection_data(uint64_t id);
void close_connection(uint64_t id);
};
} // namespace ag
+188
View File
@@ -0,0 +1,188 @@
#include "vpn/internal/icmp_manager.h"
#include <atomic>
#include <chrono>
#include <vector>
#define log_req(mngr_, msg_, lvl_, fmt_, ...) \
lvl_##log((mngr_)->m_log, "[{}] [{}/{}/{}] " fmt_, (mngr_)->m_id, sockaddr_ip_to_str((sockaddr *) &(msg_).peer), \
(int) (msg_).id, (int) (msg_).seqno, ##__VA_ARGS__)
#define log_reply(mngr_, msg_, lvl_, fmt_, ...) \
lvl_##log((mngr_)->m_log, "[{}] [{}/{}/{}] " fmt_, (mngr_)->m_id, sockaddr_ip_to_str((sockaddr *) &(msg_).peer), \
(int) (msg_).id, (int) (msg_).seqno, ##__VA_ARGS__)
using namespace std::chrono;
namespace ag {
struct RequestAttempt {
uint16_t seqno;
steady_clock::time_point timeout_ts;
};
struct IcmpManager::RequestInfo {
sockaddr_storage original_peer = {};
std::vector<RequestAttempt> tries;
};
void IcmpManager::request_info_delete(RequestInfo *i) {
delete i;
}
static constexpr seconds DEFAULT_PING_REQUEST_TIMEOUT = seconds(3);
static std::atomic_int g_next_id = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
IcmpRequestKey IcmpRequestKey::make(const IcmpEchoRequest &request) {
return {request.id};
}
IcmpRequestKey IcmpRequestKey::make(const IcmpEchoReply &reply) {
return {reply.id};
}
bool IcmpRequestKey::operator<(const IcmpRequestKey &other) const {
return this->id < other.id;
}
IcmpEchoRequestKey IcmpEchoRequestKey::make(const IcmpEchoRequest &request) {
return {{request.id}, request.seqno};
}
IcmpEchoRequestKey IcmpEchoRequestKey::make(const IcmpEchoReply &reply) {
return {{reply.id}, reply.seqno};
}
bool IcmpEchoRequestKey::operator<(const IcmpEchoRequestKey &other) const {
return this->id < other.id || this->seqno < other.seqno;
}
IcmpManager::IcmpManager()
: m_id(g_next_id.fetch_add(1, std::memory_order_relaxed)) {
}
bool IcmpManager::init(Parameters p, IcmpManagerHandler h) {
m_parameters = p;
m_parameters.request_timeout = m_parameters.request_timeout.value_or(DEFAULT_PING_REQUEST_TIMEOUT);
m_handler = h;
return true;
}
void IcmpManager::deinit() {
m_timer.reset();
m_requests.clear();
}
IcmpManagerMessageStatus IcmpManager::register_request(const IcmpEchoRequest &request) {
log_req(this, request, dbg, "{}", request);
RequestInfoPtr &info = m_requests[IcmpRequestKey::make(request)];
if (info == nullptr) {
info.reset(new RequestInfo{});
info->original_peer = request.peer;
}
bool is_already_on_wire =
std::any_of(info->tries.begin(), info->tries.end(), [seqno = request.seqno](const RequestAttempt &i) {
return i.seqno == seqno;
});
if (is_already_on_wire) {
log_req(this, request, dbg, "Such request is already in progress");
return IM_MSGS_DROP;
}
info->tries.emplace_back(RequestAttempt{
request.seqno,
steady_clock::time_point(steady_clock::now() + m_parameters.request_timeout.value()),
});
if (m_timer == nullptr) {
m_timer.reset(event_new(vpn_event_loop_get_base(m_parameters.ev_loop), -1, EV_PERSIST, timer_callback, this));
const timeval tv = ms_to_timeval(m_parameters.request_timeout.value().count() / 10);
event_add(m_timer.get(), &tv);
}
return IM_MSGS_PASS;
}
IcmpManagerMessageStatus IcmpManager::register_reply(IcmpEchoReply &reply) {
log_reply(this, reply, dbg, "{}", reply);
auto request_it = m_requests.find(IcmpRequestKey::make(reply));
if (request_it == m_requests.end()) {
log_reply(this, reply, dbg, "There's no request with such ID");
return IM_MSGS_DROP;
}
RequestInfoPtr &info = request_it->second;
if (info == nullptr) {
m_requests.erase(request_it);
log_reply(this, reply, dbg, "There's no request with such ID");
assert(0);
return IM_MSGS_DROP;
}
auto try_it = info->tries.end();
if (!(reply.peer.ss_family == AF_INET && reply.type != ICMP_MT_ECHO_REPLY)
&& !(reply.peer.ss_family == AF_INET6 && reply.type != ICMPV6_MT_ECHO_REPLY)) {
try_it = std::find_if(info->tries.begin(), info->tries.end(), [seqno = reply.seqno](const RequestAttempt &i) {
return i.seqno == seqno;
});
} else if (!info->tries.empty()) {
try_it = info->tries.begin();
reply.seqno = try_it->seqno;
}
if (try_it == info->tries.end()) {
log_reply(this, reply, dbg, "There's no request with such sequence number");
return IM_MSGS_DROP;
}
assert(reply.seqno == try_it->seqno);
info->tries.erase(try_it);
if (info->tries.empty()) {
m_requests.erase(request_it);
}
return IM_MSGS_PASS;
}
static IcmpEchoReply make_reply_on_timeout(const IcmpRequestKey &key, const sockaddr_storage &peer, uint16_t seqno) {
return {
.peer = peer,
.id = key.id,
.seqno = seqno,
.type = ICMP_MT_DROP,
.code = 0,
};
}
void IcmpManager::timer_callback(evutil_socket_t, short, void *arg) {
auto *self = (IcmpManager *) arg;
steady_clock::time_point now = steady_clock::now();
for (auto i = self->m_requests.begin(); i != self->m_requests.end();) {
const IcmpRequestKey &key = i->first;
RequestInfo &info = *i->second;
for (auto j = info.tries.begin(); j != info.tries.end();) {
RequestAttempt &req_try = *j;
if (now < req_try.timeout_ts) {
++j;
continue;
}
IcmpEchoReply reply = make_reply_on_timeout(key, info.original_peer, req_try.seqno);
log_reply(self, reply, dbg, "Request has timed out");
self->m_handler.on_reply_ready(self->m_handler.arg, reply);
j = info.tries.erase(j);
}
if (info.tries.empty()) {
i = self->m_requests.erase(i);
} else {
++i;
}
}
}
} // namespace ag
+291
View File
@@ -0,0 +1,291 @@
#include "memfile_buffer.h"
#include <algorithm>
#include <cassert>
#include "memory_buffer.h"
namespace ag {
MemfileBuffer::MemfileBuffer(std::string path, size_t mem_threshold, size_t max_file_size)
: m_mem_buffer(std::make_unique<MemoryBuffer>())
, m_threshold(mem_threshold)
, m_max_file_size(max_file_size)
, m_path(std::move(path)) {
assert(!m_path.empty());
}
MemfileBuffer::~MemfileBuffer() {
fffile_close(std::exchange(m_fd, FF_BADFD));
if (!m_path.empty()) {
fffile_rm(m_path.c_str());
}
}
std::optional<std::string> MemfileBuffer::init() {
return m_mem_buffer->init();
}
size_t MemfileBuffer::size() const {
ssize_t fsize = (m_fd != FF_BADFD) ? fffile_size(m_fd) : 0;
assert(fsize >= (ssize_t) m_read_offset);
size_t stored_in_file = std::max(fsize, (ssize_t) 0) - m_read_offset;
return m_mem_buffer->size() + stored_in_file;
}
std::optional<std::string> MemfileBuffer::push(U8View data) {
data = transfer_mem2mem(data);
return write_in_file(data);
}
std::optional<std::string> MemfileBuffer::push(std::vector<uint8_t> data) {
data = transfer_mem2mem(std::move(data));
return write_in_file({data.data(), data.size()});
}
BufferPeekResult MemfileBuffer::peek() {
BufferPeekResult r = {transfer_file2mem()};
return !r.err.has_value() ? m_mem_buffer->peek() : r;
}
void MemfileBuffer::drain(size_t length) {
m_mem_buffer->drain(length);
transfer_file2mem();
}
size_t MemfileBuffer::get_free_mem_space() const {
return m_threshold - m_mem_buffer->size();
}
std::optional<std::string> MemfileBuffer::transfer_file2mem() {
if (m_fd == FF_BADFD) {
return std::nullopt;
}
size_t free_space = get_free_mem_space();
if (free_space == 0) {
return std::nullopt;
}
ssize_t fsize = fffile_size(m_fd);
if (fsize == 0) {
return std::nullopt;
}
if (fsize < 0) {
return str_format("Failed to get file size: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
assert(fsize >= (ssize_t) m_read_offset);
size_t to_read = std::min(free_space, (size_t) (fsize - m_read_offset));
if ((size_t) fsize == m_read_offset) {
return std::nullopt;
}
int r = fffile_seek(m_fd, m_read_offset, SEEK_SET);
if (r < 0) {
return str_format("Failed to set file offset: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
std::vector<uint8_t> buf;
buf.resize(to_read);
uint8_t *buf_pos = buf.data();
uint8_t *buf_end = buf_pos + buf.size();
while (buf_pos != buf_end) {
ssize_t ret = fffile_read(m_fd, buf_pos, buf_end - buf_pos);
if (ret > 0) {
buf_pos += ret;
m_read_offset += ret;
} else if (ret == 0) {
return str_format("Unexpected EOF while reading file content");
} else {
return str_format("Failed to read file content: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
}
return m_mem_buffer->push(std::move(buf));
}
std::vector<uint8_t> MemfileBuffer::transfer_mem2mem(std::vector<uint8_t> data) {
size_t free_space = get_free_mem_space();
if (free_space == 0) {
return data;
}
std::vector<uint8_t> chunk;
if (free_space >= data.size()) {
chunk.swap(data);
} else {
chunk = {data.data(), data.data() + free_space};
}
if (std::optional<std::string> err = m_mem_buffer->push(chunk); !err.has_value()) {
data.erase(data.begin(), data.begin() + chunk.size());
}
return data;
}
U8View MemfileBuffer::transfer_mem2mem(U8View data) {
size_t free_space = get_free_mem_space();
if (free_space == 0) {
return data;
}
U8View chunk;
if (free_space >= data.size()) {
chunk = data;
} else {
chunk = {data.data(), free_space};
}
if (std::optional<std::string> err = m_mem_buffer->push(chunk); !err.has_value()) {
data.remove_prefix(chunk.size());
}
return data;
}
static std::optional<std::string> transfer_file2file(fffd dst, fffd src, size_t size) {
if (size == 0) {
return std::nullopt;
}
auto *buf = (uint8_t *) malloc(size);
uint8_t *buf_pos = buf;
uint8_t *buf_end = buf + size;
while (buf_pos != buf_end) {
int r = fffile_read(src, buf_pos, buf_end - buf_pos);
if (r > 0) {
buf_pos += r;
} else if (r == 0) {
free(buf);
return str_format("Unexpected EOF while reading file content");
} else {
free(buf);
return str_format("%s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
}
buf_pos = buf;
while (buf_pos != buf_end) {
int r = fffile_write(dst, buf_pos, buf_end - buf_pos);
if (r > 0) {
buf_pos += r;
} else {
free(buf);
return str_format("%s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
}
free(buf);
return std::nullopt;
}
bool MemfileBuffer::need_to_strip_file(size_t fsize, size_t data_size) const {
return fsize + data_size > m_max_file_size
|| (m_read_offset > 0
// current position is more than 30% of the file size
&& (m_read_offset > (m_max_file_size * 3 / 10)
|| m_read_offset > (fsize * 3 / 10)
// file reached 80% of its maximum size
|| fsize > (m_max_file_size * 8 / 10)));
}
std::optional<std::string> MemfileBuffer::strip_file() {
ssize_t fsize = fffile_size(m_fd);
if (fsize < 0) {
return str_format("Failed to get file size: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
assert(fsize >= (ssize_t) m_read_offset);
std::string tmp_fpath = str_format("%s.tmp", m_path.c_str());
int r = fffile_rename(m_path.c_str(), tmp_fpath.c_str());
if (r != 0) {
return str_format("Failed to rename file: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
std::optional<std::string> err;
fffd tmp_fd = fffile_open(m_path.c_str(), FFO_CREATE | FFO_APPEND | FFO_RDWR);
if (tmp_fd == FF_BADFD) {
err = str_format("Failed to open temp file: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
goto exit;
}
r = fffile_seek(m_fd, m_read_offset, SEEK_SET);
if (r < 0) {
err = str_format("Failed to set file offset: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
goto exit;
}
err = transfer_file2file(tmp_fd, m_fd, fsize - m_read_offset);
if (err.has_value()) {
err = str_format("Failed to transfer file content: %s", err->c_str());
goto exit;
}
std::swap(m_fd, tmp_fd);
m_read_offset = 0;
exit:
fffile_close(tmp_fd);
if (err.has_value()) {
fffile_rename(tmp_fpath.c_str(), m_path.c_str());
}
fffile_rm(tmp_fpath.c_str());
return err;
}
std::optional<std::string> MemfileBuffer::write_in_file(U8View data) {
if (data.empty()) {
return std::nullopt;
}
int64_t fsize = 0;
if (m_fd == FF_BADFD) {
m_fd = fffile_open(m_path.c_str(), FFO_CREATE | FFO_APPEND | FFO_RDWR);
if (m_fd == FF_BADFD) {
return str_format("Failed to open file: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
} else {
fsize = fffile_size(m_fd);
if (fsize < 0) {
return str_format("Failed to get file size: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
}
if (need_to_strip_file(fsize, data.size())) {
if (std::optional<std::string> err = strip_file(); err.has_value()) {
return str_format("Failed to strip file: %s", err->c_str());
}
}
size_t to_write = 0;
fsize = fffile_size(m_fd);
if (fsize < 0) {
return str_format("Failed to get file size: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
if ((size_t) fsize >= m_max_file_size) {
return "File reached its capacity";
}
to_write = std::min(data.size(), (size_t) (m_max_file_size - fsize));
data = {data.data(), to_write};
if (!data.empty() && 0 >= fffile_write(m_fd, data.data(), data.size())) {
return str_format("Failed to write data: %s (%d)", fferr_strp(fferr_last()), (int) fferr_last());
}
return std::nullopt;
}
} // namespace ag
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <FFOS/file.h>
#include "vpn/internal/data_buffer.h"
namespace ag {
class MemfileBuffer : public DataBuffer {
public:
/**
* @param path file path
* @param mem_threshold memory buffer size (exceeding this limit causes storing incoming data in file)
* @param max_file_size maximum file size (exceeding this limit causes data truncation)
*/
MemfileBuffer(std::string path, size_t mem_threshold, size_t max_file_size = SIZE_MAX);
~MemfileBuffer() override;
MemfileBuffer(const MemfileBuffer &) = delete;
MemfileBuffer &operator=(const MemfileBuffer &) = delete;
MemfileBuffer(MemfileBuffer &&) noexcept = delete;
MemfileBuffer &operator=(MemfileBuffer &&) noexcept = delete;
private:
std::unique_ptr<DataBuffer> m_mem_buffer; // memory buffer
size_t m_threshold = 0; // memory buffer theshold
size_t m_max_file_size = SIZE_MAX; // maximum file size
fffd m_fd = FF_BADFD; // file descriptor
size_t m_read_offset = 0; // file read offset
std::string m_path; // full file path
std::optional<std::string> init() override;
[[nodiscard]] size_t size() const override;
std::optional<std::string> push(U8View data) override;
std::optional<std::string> push(std::vector<uint8_t> data) override;
BufferPeekResult peek() override;
void drain(size_t length) override;
/** Fill free space in memory buffer with file content */
std::optional<std::string> transfer_file2mem();
/** Fill free space in memory buffer with given data */
std::vector<uint8_t> transfer_mem2mem(std::vector<uint8_t> data);
U8View transfer_mem2mem(U8View data);
/** Get free space size in memory buffer */
[[nodiscard]] size_t get_free_mem_space() const;
/** Check if read part needs to be stripped from file */
[[nodiscard]] bool need_to_strip_file(size_t fsize, size_t data_size) const;
/** Strip read part from file */
std::optional<std::string> strip_file();
/** Write data in file */
std::optional<std::string> write_in_file(U8View data);
};
} // namespace ag
+59
View File
@@ -0,0 +1,59 @@
#include "memory_buffer.h"
#include <algorithm>
#include <cassert>
namespace ag {
MemoryBuffer::MemoryBuffer() = default;
MemoryBuffer::~MemoryBuffer() = default;
std::optional<std::string> MemoryBuffer::init() {
return std::nullopt;
}
size_t MemoryBuffer::size() const {
return m_total_size;
}
std::optional<std::string> MemoryBuffer::push(U8View data) {
return push(std::vector<uint8_t>(data.data(), data.data() + data.length()));
}
std::optional<std::string> MemoryBuffer::push(std::vector<uint8_t> data) {
if (!data.empty()) {
m_total_size += data.size();
m_chunks.emplace(std::move(data));
}
return std::nullopt;
}
BufferPeekResult MemoryBuffer::peek() {
U8View chunk;
if (!m_chunks.empty()) {
const std::vector<uint8_t> &front = m_chunks.front();
chunk = {front.data(), front.size()};
}
return {std::nullopt, chunk};
}
void MemoryBuffer::drain(size_t length) {
assert(length <= m_total_size);
m_total_size -= length;
while (length > 0) {
assert(!m_chunks.empty());
std::vector<uint8_t> &front = m_chunks.front();
size_t to_remove = std::min(front.size(), length);
front.erase(front.begin(), front.begin() + to_remove);
length -= to_remove;
if (front.empty()) {
m_chunks.pop();
}
}
}
} // namespace ag
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <queue>
#include <vector>
#include "vpn/internal/data_buffer.h"
namespace ag {
class MemoryBuffer : public DataBuffer {
public:
MemoryBuffer();
~MemoryBuffer() override;
private:
size_t m_total_size = 0;
std::queue<std::vector<uint8_t>> m_chunks;
std::optional<std::string> init() override;
[[nodiscard]] size_t size() const override;
std::optional<std::string> push(U8View data) override;
std::optional<std::string> push(std::vector<uint8_t> data) override;
BufferPeekResult peek() override;
void drain(size_t length) override;
};
} // namespace ag
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <cassert>
#include "vpn/internal/server_upstream.h"
namespace ag {
/**
* Extends `ServerUpstream` to make it usable by `upstream_multiplexer_t`
*/
class MultiplexableUpstream : public ServerUpstream {
public:
MultiplexableUpstream(
const VpnUpstreamProtocolConfig &protocol_config, int id, VpnClient *vpn, SeverHandler handler)
: ServerUpstream(id, protocol_config)
, m_id(id) {
if (!ServerUpstream::init(vpn, handler)) {
assert(0);
}
}
~MultiplexableUpstream() override = default;
/**
* Get total number of current connections
*/
[[nodiscard]] virtual size_t connections_num() const = 0;
/**
* Create connection to peer. Result will be raised asynchronously with
* `SERVER_EVENT_CONNECTION_OPENED` in case of success, or with `SERVER_EVENT_ERROR` in case of
* error.
* @param id the connection identifier
* @param addr source and destination address pair
* @param proto connection protocol
* @param app_name name of the application that initiated this connection (optional)
* @return true in case of success, false otherwise
*/
[[nodiscard]] virtual bool open_connection(
uint64_t id, const TunnelAddressPair *addr, int proto, std::string_view app_name) = 0;
/**
* Get upstream identifier
*/
[[nodiscard]] int get_id() const {
return m_id;
}
protected:
int m_id = 0;
private:
void deinit() final {
}
uint64_t open_connection(const TunnelAddressPair *, int, std::string_view) final {
return NON_ID;
}
};
} // namespace ag
+253
View File
@@ -0,0 +1,253 @@
#include "single_upstream_connector.h"
#include <atomic>
#include <magic_enum.hpp>
#include <optional>
#include "common/logger.h"
#include "vpn/fsm.h"
#include "vpn/utils.h"
#define log_connector(con_, lvl_, fmt_, ...) lvl_##log((con_)->log, "[{}] " fmt_, (con_)->id, ##__VA_ARGS__)
namespace ag {
static std::atomic<int> g_next_connector_id = 0;
enum State : uint32_t {
S_DISCONNECTED,
S_CONNECTING,
S_DISCONNECTING,
};
enum Event {
E_RUN_CONNECT,
E_DISCONNECT,
E_DEFERRED_DISCONNECT,
E_SESSION_OPENED,
E_SESSION_CLOSED,
E_SESSION_ERROR,
E_HEALTH_CHECK_READY,
};
static constexpr auto STATE_NAMES = make_enum_names_array<State>();
static constexpr auto EVENT_NAMES = make_enum_names_array<Event>();
struct SingleUpstreamConnector::Impl {
Fsm fsm;
std::unique_ptr<ServerUpstream> upstream;
SingleUpstreamConnector &parent;
std::optional<VpnError> pending_error;
ag::AutoTaskId deferred_task;
int id = g_next_connector_id.fetch_add(1, std::memory_order_relaxed);
ag::Logger log{"SUCONNECTOR"};
static FsmParameters make_fsm_params(SingleUpstreamConnector::Impl *self) {
return {S_DISCONNECTED, FsmTransitionTable{std::begin(TRANSITION_TABLE), std::end(TRANSITION_TABLE)}, self,
"SUCONNECTOR", STATE_NAMES.data(), EVENT_NAMES.data()};
}
Impl(SingleUpstreamConnector &parent, std::unique_ptr<ServerUpstream> upstream)
: fsm(make_fsm_params(this))
, upstream(std::move(upstream))
, parent(parent) {
}
~Impl() = default;
static void upstream_handler(void *arg, ServerEvent what, void *data) {
auto *self = (Impl *) arg;
switch (what) {
case SERVER_EVENT_SESSION_OPENED: {
log_connector(self, dbg, "Session is opened successfully");
self->fsm.perform_transition(E_SESSION_OPENED, nullptr);
break;
}
case SERVER_EVENT_SESSION_CLOSED: {
log_connector(self, dbg, "Server session is closed");
self->fsm.perform_transition(E_SESSION_CLOSED, nullptr);
break;
}
case SERVER_EVENT_HEALTH_CHECK_RESULT: {
const auto *error = (VpnError *) data;
if (error == nullptr || error->code == VPN_EC_NOERROR) {
log_connector(self, dbg, "Health check succeeded");
} else {
log_connector(self, dbg, "Health check error: {} ({})", error->text, error->code);
}
self->fsm.perform_transition(E_HEALTH_CHECK_READY, data);
break;
}
case SERVER_EVENT_ERROR: {
const auto *event = (ServerError *) data;
assert(event->id == NON_ID);
log_connector(self, dbg, "Server session terminated with error: {} ({})",
safe_to_string_view(event->error.text), event->error.code);
self->fsm.perform_transition(E_SESSION_ERROR, (void *) &event->error);
break;
}
case SERVER_EVENT_CONNECTION_OPENED:
case SERVER_EVENT_CONNECTION_CLOSED:
case SERVER_EVENT_READ:
case SERVER_EVENT_DATA_SENT:
case SERVER_EVENT_GET_AVAILABLE_TO_SEND:
case SERVER_EVENT_ECHO_REPLY:
assert(0);
break;
}
}
static bool is_successful(const void *arg, void *data) {
const auto *self = (Impl *) arg;
const auto *error = (VpnError *) data;
return (error == nullptr || error->code == VPN_EC_NOERROR)
&& (!self->pending_error.has_value() || self->pending_error->code == VPN_EC_NOERROR);
}
static void do_connect(void *arg, void *data) {
auto *self = (Impl *) arg;
log_connector(self, trace, "...");
uint32_t timeout_ms = (data == nullptr) ? 0 : *(uint32_t *) data;
self->upstream->handler = {upstream_handler, self};
if (!self->upstream->open_session(timeout_ms)) {
self->pending_error = {VPN_EC_ERROR, "Failed to open session with endpoint"};
}
log_connector(self, trace, "Done");
}
static void do_health_check(void *arg, void *) {
auto *self = (Impl *) arg;
log_connector(self, trace, "...");
self->deferred_task = ag::submit(self->parent.PARAMETERS.ev_loop,
{self, [](void *arg, TaskId) {
auto *self = (Impl *) arg;
self->deferred_task.release();
if (VpnError e = self->upstream->do_health_check(); e.code != VPN_EC_NOERROR) {
self->fsm.perform_transition(E_HEALTH_CHECK_READY, &e);
}
}});
log_connector(self, trace, "Done");
}
static void do_disconnect(void *arg, void *data) {
auto *self = (Impl *) arg;
log_connector(self, trace, "...");
if (const auto *error = (VpnError *) data;
!self->pending_error.has_value() && error != nullptr && error->code != VPN_EC_NOERROR) {
self->pending_error = *error;
}
self->upstream->close_session();
self->upstream->deinit();
self->deferred_task.reset();
log_connector(self, trace, "Done");
}
static void submit_disconnect(void *arg, void *data) {
auto *self = (Impl *) arg;
log_connector(self, trace, "...");
const auto *error = (VpnError *) data;
if (!self->pending_error.has_value() && error != nullptr && error->code != VPN_EC_NOERROR) {
self->pending_error = *error;
}
self->deferred_task = ag::submit(self->parent.PARAMETERS.ev_loop, {self, [](void *arg, TaskId) {
auto *self = (Impl *) arg;
self->deferred_task.release();
self->fsm.perform_transition(
E_DEFERRED_DISCONNECT, nullptr);
}});
log_connector(self, trace, "Done");
}
static void raise_connected(void *arg, void *) {
auto *self = (Impl *) arg;
self->upstream->handler = self->parent.PARAMETERS.upstream_handler;
EndpointConnectorHandler connector_handler = self->parent.PARAMETERS.connector_handler;
connector_handler.func(connector_handler.arg, std::move(self->upstream));
}
static void raise_disconnected(void *arg, void *data) {
auto *self = (Impl *) arg;
VpnError e = {};
if (self->pending_error.has_value()) {
e = std::exchange(self->pending_error, std::nullopt).value();
} else if (data != nullptr) {
e = *(VpnError *) data;
}
EndpointConnectorHandler connector_handler = self->parent.PARAMETERS.connector_handler;
connector_handler.func(connector_handler.arg, e);
}
// clang-format off
static constexpr FsmTransitionEntry TRANSITION_TABLE[] = {
{S_DISCONNECTED, E_RUN_CONNECT, Fsm::ANYWAY, do_connect, S_CONNECTING, Fsm::DO_NOTHING},
{S_DISCONNECTED, E_SESSION_CLOSED, Fsm::ANYWAY, Fsm::DO_NOTHING, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{S_DISCONNECTED, E_DISCONNECT, Fsm::ANYWAY, Fsm::DO_NOTHING, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{S_CONNECTING, E_SESSION_OPENED, Fsm::ANYWAY, do_health_check, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{S_CONNECTING, E_HEALTH_CHECK_READY, is_successful, Fsm::DO_NOTHING, S_DISCONNECTED, raise_connected},
{S_CONNECTING, E_HEALTH_CHECK_READY, Fsm::OTHERWISE, submit_disconnect, S_DISCONNECTING, Fsm::DO_NOTHING},
{S_CONNECTING, E_SESSION_CLOSED, Fsm::ANYWAY, Fsm::DO_NOTHING, S_DISCONNECTED, raise_disconnected},
{S_CONNECTING, E_SESSION_ERROR, Fsm::ANYWAY, submit_disconnect, S_DISCONNECTING, Fsm::DO_NOTHING},
{S_DISCONNECTING, E_SESSION_CLOSED, Fsm::ANYWAY, Fsm::DO_NOTHING, S_DISCONNECTED, raise_disconnected},
{S_DISCONNECTING, E_DEFERRED_DISCONNECT, Fsm::ANYWAY, do_disconnect, S_DISCONNECTED, raise_disconnected},
{Fsm::ANY_SOURCE_STATE, E_DISCONNECT, Fsm::ANYWAY, do_disconnect, S_DISCONNECTED, Fsm::DO_NOTHING},
};
// clang-format on
};
void SingleUpstreamConnector::delete_impl(Impl *obj) {
delete obj;
}
SingleUpstreamConnector::SingleUpstreamConnector(
const EndpointConnectorParameters &parameters, std::unique_ptr<ServerUpstream> upstream)
: EndpointConnector(parameters)
, m_impl(new Impl(*this, std::move(upstream))) {
}
VpnError SingleUpstreamConnector::connect(uint32_t timeout_ms) {
log_connector(m_impl, trace, "...");
if (State s = (State) m_impl->fsm.get_state(); s != S_DISCONNECTED) {
log_connector(m_impl, dbg, "Invalid state: {}", magic_enum::enum_name(s));
return {VPN_EC_ERROR, "Invalid state"};
}
if (!m_impl->upstream->init(this->PARAMETERS.vpn_client, {&Impl::upstream_handler, m_impl.get()})) {
return {VPN_EC_ERROR, "Failed to initialize upstream"};
}
m_impl->fsm.perform_transition(E_RUN_CONNECT, &timeout_ms);
if (m_impl->pending_error.has_value()) {
VpnError e = std::exchange(m_impl->pending_error, std::nullopt).value();
this->disconnect();
log_connector(m_impl, dbg, "Failed: {} ({})", e.text, e.code);
return e;
}
log_connector(m_impl, trace, "Done");
return {};
}
void SingleUpstreamConnector::disconnect() {
log_connector(m_impl, dbg, "...");
m_impl->fsm.perform_transition(E_DISCONNECT, nullptr);
log_connector(m_impl, dbg, "Done");
}
} // namespace ag
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "vpn/internal/endpoint_connector.h"
namespace ag {
class SingleUpstreamConnector : public EndpointConnector {
public:
SingleUpstreamConnector(const EndpointConnectorParameters &parameters, std::unique_ptr<ServerUpstream> upstream);
~SingleUpstreamConnector() override = default;
SingleUpstreamConnector(const SingleUpstreamConnector &) = delete;
SingleUpstreamConnector &operator=(const SingleUpstreamConnector &) = delete;
SingleUpstreamConnector(SingleUpstreamConnector &&) = delete;
SingleUpstreamConnector &operator=(SingleUpstreamConnector &&) = delete;
private:
struct Impl;
friend struct Impl;
static void delete_impl(Impl *);
DeclPtr<Impl, &delete_impl> m_impl;
VpnError connect(uint32_t timeout_ms) override;
void disconnect() override;
};
} // namespace ag
+215
View File
@@ -0,0 +1,215 @@
#include "socks_listener.h"
#include <cassert>
#include "vpn/internal/vpn_client.h"
#include "vpn/utils.h"
namespace ag {
static constexpr Socks5ConnectResult TUNNEL_TO_SOCKS_CONNECT_RESULT[] = {
[CCR_PASS] = S5LCR_SUCCESS,
[CCR_DROP] = S5LCR_TIMEOUT,
[CCR_REJECT] = S5LCR_REJECT,
[CCR_UNREACH] = S5LCR_UNREACHABLE,
};
static TunnelAddress socks_to_client_address(const Socks5ConnectionAddress *addr) {
switch (addr->type) {
case S5CAT_SOCKADDR:
return addr->ip;
case S5CAT_DOMAIN_NAME:
return NamePort{addr->domain.name, addr->domain.port};
}
return {};
}
static ClientListener::InitResult convert_socks5_listener_start_result(Socks5ListenerStartResult r) {
switch (r) {
case SOCKS5L_START_SUCCESS:
return ClientListener::InitResult::SUCCESS;
case SOCKS5L_START_ADDR_IN_USE:
return ClientListener::InitResult::ADDR_IN_USE;
case SOCKS5L_START_FAILURE:
return ClientListener::InitResult::FAILURE;
}
}
void SocksListener::socks_handler(void *arg, Socks5ListenerEvent what, void *data) {
auto *client = (SocksListener *) arg;
switch (what) {
case SOCKS5L_EVENT_GENERATE_CONN_ID: {
size_t id = client->vpn->listener_conn_id_generator.get();
memcpy(data, &id, sizeof(id));
break;
}
case SOCKS5L_EVENT_CONNECT_REQUEST: {
const Socks5ConnectRequestEvent *socks_event = (Socks5ConnectRequestEvent *) data;
TunnelAddress dst = socks_to_client_address(socks_event->dst);
ClientConnectRequest event = {socks_event->id, socks_event->proto, socks_event->src, &dst};
event.app_name = socks_event->app_name;
client->handler.func(client->handler.arg, CLIENT_EVENT_CONNECT_REQUEST, &event);
break;
}
case SOCKS5L_EVENT_CONNECTION_ACCEPTED: {
client->handler.func(client->handler.arg, CLIENT_EVENT_CONNECTION_ACCEPTED, data);
break;
}
case SOCKS5L_EVENT_READ: {
auto *socks_event = (Socks5ReadEvent *) data;
ClientRead event = {socks_event->id, socks_event->data, socks_event->length, 0};
client->handler.func(client->handler.arg, CLIENT_EVENT_READ, &event);
socks_event->result = event.result;
break;
}
case SOCKS5L_EVENT_DATA_SENT: {
const Socks5DataSentEvent *socks_event = (Socks5DataSentEvent *) data;
ClientDataSentEvent event = {socks_event->id, socks_event->length};
client->handler.func(client->handler.arg, CLIENT_EVENT_DATA_SENT, &event);
break;
}
case SOCKS5L_EVENT_CONNECTION_CLOSED: {
auto *socks_event = (Socks5ConnectionClosedEvent *) data;
client->handler.func(client->handler.arg, CLIENT_EVENT_CONNECTION_CLOSED, &socks_event->id);
break;
}
case SOCKS5L_EVENT_PROTECT_SOCKET: {
vpn_client::Handler *vpn_handler = &client->vpn->parameters.handler;
vpn_handler->func(vpn_handler->arg, vpn_client::EVENT_PROTECT_SOCKET, data);
break;
}
}
}
static VpnSocksListenerConfig clone_config(const VpnSocksListenerConfig *config) {
return VpnSocksListenerConfig{
.listen_address = config->listen_address,
.username = safe_strdup(config->username),
.password = safe_strdup(config->password),
};
}
static void destroy_cloned_config(VpnSocksListenerConfig *config) {
free((void *) config->username);
free((void *) config->password);
*config = {};
}
SocksListener::SocksListener(const VpnSocksListenerConfig *config)
: m_config(clone_config(config)) {
}
SocksListener::~SocksListener() {
destroy_cloned_config(&m_config);
}
const sockaddr_storage &SocksListener::get_listen_address() const {
return *socks5_listener_listen_address(m_socks5_listener);
}
ClientListener::InitResult SocksListener::init(VpnClient *vpn, ClientHandler handler) {
if (auto result = this->ClientListener::init(vpn, handler); result != InitResult::SUCCESS) {
return result;
}
Socks5ListenerConfig socks5_config = {
.ev_loop = vpn->parameters.ev_loop,
.listen_address = m_config.listen_address,
.timeout_ms = vpn->listener_config.timeout_ms,
.socket_manager = vpn->parameters.network_manager->socket,
.read_threshold = 0,
.username = safe_to_string_view(m_config.username),
.password = safe_to_string_view(m_config.password),
};
if (this->vpn->tmp_files_base_path.has_value()) {
socks5_config.read_threshold = this->vpn->conn_memory_buffer_threshold;
}
Socks5ListenerHnadler event_handler = {socks_handler, this};
m_socks5_listener = socks5_listener_create(&socks5_config, &event_handler);
if (m_socks5_listener == nullptr) {
errlog(m_log, "Failed to create SOCKS listener");
this->deinit();
return InitResult::FAILURE;
}
if (Socks5ListenerStartResult result = socks5_listener_start(m_socks5_listener); result != SOCKS5L_START_SUCCESS) {
errlog(m_log, "Failed to start SOCKS listener");
this->deinit();
return convert_socks5_listener_start_result(result);
}
vpn->socks_listener_address = *socks5_listener_listen_address(m_socks5_listener);
return InitResult::SUCCESS;
}
void SocksListener::deinit() {
if (m_socks5_listener != nullptr) {
socks5_listener_stop(m_socks5_listener);
socks5_listener_destroy(load_and_null(m_socks5_listener));
}
m_deferred_tasks.clear();
}
void SocksListener::complete_connect_request(uint64_t id, ClientConnectResult result) {
socks5_listener_complete_connect_request(m_socks5_listener, id, TUNNEL_TO_SOCKS_CONNECT_RESULT[result]);
}
void SocksListener::close_connection(uint64_t id, bool graceful, bool async) {
if (!async) {
socks5_listener_close_connection(m_socks5_listener, id, graceful);
} else {
struct CloseCtx {
SocksListener *listener;
uint64_t id;
bool graceful;
};
m_deferred_tasks.emplace(ag::submit(vpn->parameters.ev_loop,
{
new CloseCtx{this, id, graceful},
[](void *arg, TaskId task_id) {
auto *ctx = (CloseCtx *) arg;
ctx->listener->m_deferred_tasks.erase(ag::make_auto_id(task_id));
socks5_listener_close_connection(ctx->listener->m_socks5_listener, ctx->id, ctx->graceful);
},
[](void *arg) {
delete (CloseCtx *) arg;
},
}));
}
}
ssize_t SocksListener::send(uint64_t id, const uint8_t *data, size_t length) {
int r = socks5_listener_send_data(m_socks5_listener, id, data, length);
if (r == 0) {
r = (int) length;
}
return r;
}
void SocksListener::consume(uint64_t id, size_t n) {
// do nothing
}
TcpFlowCtrlInfo SocksListener::flow_control_info(uint64_t id) {
return socks5_listener_flow_ctrl_info(m_socks5_listener, id);
}
void SocksListener::turn_read(uint64_t id, bool on) {
socks5_listener_turn_read(m_socks5_listener, id, on);
}
} // namespace ag
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <set>
#include "common/logger.h"
#include "net/socks5_listener.h"
#include "vpn/internal/client_listener.h"
namespace ag {
class SocksListener : public ClientListener {
public:
explicit SocksListener(const VpnSocksListenerConfig *config);
~SocksListener() override;
SocksListener(const SocksListener &) = delete;
SocksListener &operator=(const SocksListener &) = delete;
SocksListener(SocksListener &&) noexcept = delete;
SocksListener &operator=(SocksListener &&) noexcept = delete;
/**
* Get the address the listener is listening on
*/
[[nodiscard]] const sockaddr_storage &get_listen_address() const;
private:
Socks5Listener *m_socks5_listener = nullptr;
std::set<ag::AutoTaskId> m_deferred_tasks;
VpnSocksListenerConfig m_config = {};
ag::Logger m_log{"SOCKS_LISTENER"};
InitResult init(VpnClient *vpn, ClientHandler handler) override;
void deinit() override;
void complete_connect_request(uint64_t id, ClientConnectResult result) override;
void close_connection(uint64_t id, bool graceful, bool async) override;
ssize_t send(uint64_t id, const uint8_t *data, size_t length) override;
void consume(uint64_t id, size_t n) override;
TcpFlowCtrlInfo flow_control_info(uint64_t id) override;
void turn_read(uint64_t id, bool on) override;
static void socks_handler(void *arg, Socks5ListenerEvent what, void *data);
};
} // namespace ag
+365
View File
@@ -0,0 +1,365 @@
#include "tun_device_listener.h"
#include <algorithm>
#include <cassert>
#include <numeric>
#define log_conn(lstnr_, id_, lvl_, fmt_, ...) \
lvl_##log((lstnr_)->m_log, "[L:{}] " fmt_, (uint64_t) (id_), ##__VA_ARGS__)
namespace ag {
enum ConnectionFlags {
CF_READ_ENABLED = 1 << 0, // `CLIENT_EVENT_READ` can be raised
CF_CLOSING = 1 << 1, // a connection was closed on server-side
};
struct CompleteCtx {
TunListener *listener;
uint64_t id;
};
struct CloseAsyncCtx {
TunListener *listener;
uint64_t id;
bool graceful;
};
static constexpr TcpipAction CONNECT_RESULT_TO_TCPIP_ACTION[] = {
[CCR_PASS] = TCPIP_ACT_BYPASS,
[CCR_DROP] = TCPIP_ACT_DROP,
[CCR_REJECT] = TCPIP_ACT_REJECT,
[CCR_UNREACH] = TCPIP_ACT_REJECT_UNREACHABLE,
};
static VpnTunListenerConfig clone_config(const VpnTunListenerConfig *config) {
return VpnTunListenerConfig{
.fd = config->fd,
.mtu_size = config->mtu_size,
.pcap_filename = safe_strdup(config->pcap_filename),
};
}
static void destroy_cloned_config(VpnTunListenerConfig *config) {
free((void *) config->pcap_filename);
*config = {};
}
TunListener::TunListener(const VpnTunListenerConfig *config)
: m_config{clone_config(config)} {
if (m_config.mtu_size == 0) {
m_config.mtu_size = DEFAULT_MTU_SIZE;
}
}
TunListener::~TunListener() {
destroy_cloned_config(&m_config);
}
ClientListener::InitResult TunListener::init(VpnClient *vpn, ClientHandler handler) {
if (auto result = this->ClientListener::init(vpn, handler); result != InitResult::SUCCESS) {
return result;
}
TcpipParameters tcpip_params = {
.tun_fd = m_config.fd,
.event_loop = this->vpn->parameters.ev_loop,
.mtu_size = m_config.mtu_size,
.pcap_filename = m_config.pcap_filename,
.handler = {tcpip_handler, this},
};
m_tcpip = tcpip_open(&tcpip_params);
if (m_tcpip == nullptr) {
errlog(m_log, "Failed to initialize TCP/IP stack");
deinit();
return InitResult::FAILURE;
}
return InitResult::SUCCESS;
}
void TunListener::deinit() {
tcpip_close(m_tcpip);
m_tcpip = nullptr;
}
int TunListener::read_out_pending_data(uint64_t id, Connection *conn) const {
std::queue<std::vector<uint8_t>> &pending = conn->unread_data;
while ((conn->flags & CF_READ_ENABLED) && !pending.empty()) {
std::vector<uint8_t> &chunk = pending.front();
ClientRead event = {id, chunk.data(), chunk.size(), 0};
this->handler.func(this->handler.arg, CLIENT_EVENT_READ, &event);
if (conn->proto == IPPROTO_UDP) {
// consume sent or drop unsent/failed UDP packet
chunk.clear();
} else if (event.result >= 0) {
chunk.erase(chunk.begin(), chunk.begin() + event.result);
} else {
return event.result;
}
if (chunk.empty()) {
pending.pop();
}
}
return 0;
}
void TunListener::tcpip_handler(void *arg, TcpipEvent what, void *data) {
auto *listener = (TunListener *) arg;
switch (what) {
case TCPIP_EVENT_GENERATE_CONN_ID: {
size_t id = listener->vpn->listener_conn_id_generator.get();
memcpy(data, &id, sizeof(id));
break;
}
case TCPIP_EVENT_CONNECT_REQUEST: {
auto *tcp_event = (TcpipConnectRequestEvent *) data;
auto [i, ok] = listener->m_connections.emplace(std::make_pair(tcp_event->id, Connection{}));
assert(ok);
i->second.proto = tcp_event->proto;
TunnelAddress dst(*(sockaddr_storage *) tcp_event->dst);
ClientConnectRequest event = {tcp_event->id, tcp_event->proto, tcp_event->src, &dst};
listener->handler.func(listener->handler.arg, CLIENT_EVENT_CONNECT_REQUEST, &event);
break;
}
case TCPIP_EVENT_CONNECTION_ACCEPTED: {
listener->handler.func(listener->handler.arg, CLIENT_EVENT_CONNECTION_ACCEPTED, data);
break;
}
case TCPIP_EVENT_READ: {
auto *tcp_event = (TcpipReadEvent *) data;
auto i = listener->m_connections.find(tcp_event->id);
if (i == listener->m_connections.end()) {
tcp_event->result = -1;
break;
}
Connection *conn = &i->second;
tcp_event->result = listener->read_out_pending_data(tcp_event->id, conn);
if (tcp_event->result < 0) {
break;
}
std::queue<std::vector<uint8_t>> &pending = conn->unread_data;
size_t iov_idx = 0;
evbuffer_iovec iov[tcp_event->iovlen];
memcpy(iov, tcp_event->iov, sizeof(*tcp_event->iov) * tcp_event->iovlen);
if ((conn->flags & CF_READ_ENABLED) && pending.empty()) {
ClientRead event = {tcp_event->id, nullptr, 0, 0};
for (iov_idx = 0; iov_idx < tcp_event->iovlen; ++iov_idx) {
evbuffer_iovec *v = &iov[iov_idx];
do {
event.data = (uint8_t *) v->iov_base;
event.length = v->iov_len;
listener->handler.func(listener->handler.arg, CLIENT_EVENT_READ, &event);
if (event.result >= 0) {
tcp_event->result += event.result;
v->iov_base = (uint8_t *) v->iov_base + event.result;
v->iov_len -= event.result;
if (!(conn->flags & CF_READ_ENABLED)) {
goto loop_exit;
}
} else {
tcp_event->result = event.result;
goto loop_exit;
}
} while (v->iov_len > 0);
}
}
loop_exit:
if (tcp_event->result >= 0 && iov_idx < tcp_event->iovlen) {
// not completely sent
std::for_each(&iov[iov_idx], iov + tcp_event->iovlen - iov_idx, [&pending](const evbuffer_iovec &vec) {
pending.emplace((uint8_t *) vec.iov_base, (uint8_t *) vec.iov_base + vec.iov_len);
});
}
break;
}
case TCPIP_EVENT_DATA_SENT: {
auto *tcp_event = (TcpipDataSentEvent *) data;
auto i = listener->m_connections.find(tcp_event->id);
if (i == listener->m_connections.end()) {
break;
}
Connection *conn = &i->second;
conn->sent_since_last_event += tcp_event->length;
conn->scheduled_to_send -= tcp_event->length;
ClientDataSentEvent event = {tcp_event->id, conn->sent_since_last_event};
conn->sent_since_last_event = 0;
listener->handler.func(listener->handler.arg, CLIENT_EVENT_DATA_SENT, &event);
if ((conn->flags & CF_CLOSING) && conn->scheduled_to_send == 0) {
conn->close_task_id = ag::submit(listener->vpn->parameters.ev_loop,
{new CloseAsyncCtx{listener, tcp_event->id, true},
[](void *arg, TaskId) {
auto *ctx = (CloseAsyncCtx *) arg;
tcpip_close_connection(ctx->listener->m_tcpip, ctx->id, ctx->graceful);
},
[](void *arg) {
delete (CloseAsyncCtx *) arg;
}});
}
break;
}
case TCPIP_EVENT_CONNECTION_CLOSED: {
uint64_t id = *(uint64_t *) data;
listener->handler.func(listener->handler.arg, CLIENT_EVENT_CONNECTION_CLOSED, &id);
if (auto i = listener->m_connections.find(id); i != listener->m_connections.end()) {
const Connection *conn = &i->second;
log_conn(listener, id, dbg, "Remaining unsent={} unread={}", conn->scheduled_to_send,
conn->unread_data.size());
listener->m_connections.erase(i);
dbglog(listener->m_log, "Remaining connections: {}", listener->m_connections.size());
}
break;
}
case TCPIP_EVENT_STAT_NOTIFY: {
// do nothing
break;
}
case TCPIP_EVENT_TUN_OUTPUT: {
listener->handler.func(listener->handler.arg, CLIENT_EVENT_OUTPUT, data);
break;
}
case TCPIP_EVENT_ICMP_ECHO:
listener->handler.func(listener->handler.arg, CLIENT_EVENT_ICMP_ECHO_REQUEST, data);
break;
}
}
void TunListener::complete_connect_request(uint64_t id, ClientConnectResult result) {
tcpip_complete_connect_request(m_tcpip, id, CONNECT_RESULT_TO_TCPIP_ACTION[result]);
}
void TunListener::complete_read(void *arg, TaskId) {
auto *ctx = (CompleteCtx *) arg;
TunListener *listener = ctx->listener;
auto i = listener->m_connections.find(ctx->id);
if (i != listener->m_connections.end()) {
Connection *conn = &i->second;
conn->complete_read_task_id.release();
if (0 != listener->read_out_pending_data(ctx->id, conn)) {
listener->close_connection(ctx->id, false, false);
}
}
}
void TunListener::close_connection(uint64_t id, bool graceful, bool async) {
auto i = m_connections.find(id);
if (i == m_connections.end()) {
return;
}
Connection *conn = &i->second;
if (graceful && conn->scheduled_to_send > 0) {
// defer closing until all the pending data is sent to client
conn->flags |= CF_CLOSING;
} else {
if (conn->scheduled_to_send > 0 || !conn->unread_data.empty()) {
log_conn(this, id, dbg, "Remaining unsent={} unread={}", conn->scheduled_to_send, conn->unread_data.size());
}
if (!async) {
tcpip_close_connection(m_tcpip, id, graceful);
} else {
conn->close_task_id = ag::submit(this->vpn->parameters.ev_loop,
{new CloseAsyncCtx{this, id, true},
[](void *arg, TaskId) {
auto *ctx = (CloseAsyncCtx *) arg;
tcpip_close_connection(ctx->listener->m_tcpip, ctx->id, ctx->graceful);
},
[](void *arg) {
delete (CloseAsyncCtx *) arg;
}});
}
}
}
ssize_t TunListener::send(uint64_t id, const uint8_t *data, size_t length) {
auto i = m_connections.find(id);
if (i == m_connections.end()) {
return -1;
}
Connection *conn = &i->second;
if (conn->flags & CF_CLOSING) {
return 0;
}
int r = tcpip_send_to_client(m_tcpip, id, data, length);
if (r >= 0) {
conn->scheduled_to_send += r;
}
return r;
}
void TunListener::consume(uint64_t id, size_t n) {
if (n > 0) {
log_conn(this, id, trace, "{}", n);
}
tcpip_sent_to_remote(m_tcpip, id, n);
}
TcpFlowCtrlInfo TunListener::flow_control_info(uint64_t id) {
return tcpip_flow_ctrl_info(m_tcpip, id);
}
void TunListener::turn_read(uint64_t id, bool on) {
auto i = m_connections.find(id);
if (i == m_connections.end()) {
return;
}
Connection *conn = &i->second;
if (!!(conn->flags & CF_READ_ENABLED) == on) {
// nothing to do
return;
}
log_conn(this, id, trace, "{}", on ? "on" : "off");
if (on) {
conn->flags |= CF_READ_ENABLED;
} else {
conn->flags &= ~CF_READ_ENABLED;
}
if (on && !conn->complete_read_task_id.has_value() && !conn->unread_data.empty()) {
// we have some unread data on the connection - complete it
conn->complete_read_task_id =
ag::submit(this->vpn->parameters.ev_loop, {new CompleteCtx{this, id}, complete_read, [](void *arg) {
delete (CompleteCtx *) arg;
}});
}
}
int TunListener::process_client_packets(VpnPackets packets) {
tcpip_tun_input(m_tcpip, packets.data, packets.size);
return 0;
}
void TunListener::process_icmp_reply(const IcmpEchoReply &reply) {
tcpip_process_icmp_echo_reply(m_tcpip, &reply);
}
} // namespace ag
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <queue>
#include <unordered_map>
#include <vector>
#include "tcpip/tcpip.h"
#include "vpn/internal/client_listener.h"
#include "vpn/internal/vpn_client.h"
#include "vpn/vpn.h"
namespace ag {
class TunListener : public ClientListener {
public:
explicit TunListener(const VpnTunListenerConfig *config);
~TunListener() override;
TunListener(const TunListener &) = delete;
TunListener &operator=(const TunListener &) = delete;
TunListener(TunListener &&) noexcept = delete;
TunListener &operator=(TunListener &&) noexcept = delete;
private:
struct Connection {
size_t sent_since_last_event = 0; // sent bytes since last `CLIENT_EVENT_DATA_SENT`
// number of bytes passed in send, but still not confirmed by sent event
// signed because for UDP it's reported immeditely, before the result of send is accumulated
ssize_t scheduled_to_send = 0;
uint32_t flags = 0;
int proto = 0; // connection protocol (TCP/UDP)
// @todo: consider using `DataBuffer`, but it needs some modifications to guarantee
// that peeked chunks would be the same as pushed ones
// buffer for data raised with `TCPIP_EVENT_READ`, but wasn't actaully sent to server
std::queue<std::vector<uint8_t>> unread_data;
ag::AutoTaskId complete_read_task_id;
ag::AutoTaskId close_task_id;
};
TcpipCtx *m_tcpip = nullptr;
std::unordered_map<uint64_t, Connection> m_connections;
ag::Logger m_log{"TUN_LISTENER"};
VpnTunListenerConfig m_config;
InitResult init(VpnClient *vpn, ClientHandler handler) override;
void deinit() override;
void complete_connect_request(uint64_t id, ClientConnectResult result) override;
void close_connection(uint64_t id, bool graceful, bool async) override;
ssize_t send(uint64_t id, const uint8_t *data, size_t length) override;
void consume(uint64_t id, size_t n) override;
TcpFlowCtrlInfo flow_control_info(uint64_t id) override;
void turn_read(uint64_t id, bool on) override;
int process_client_packets(VpnPackets packets) override;
void process_icmp_reply(const IcmpEchoReply &reply) override;
static void tcpip_handler(void *arg, TcpipEvent id, void *data);
static void complete_read(void *arg, TaskId task_id);
int read_out_pending_data(uint64_t id, Connection *conn) const;
};
} // namespace ag
+1344
View File
File diff suppressed because it is too large Load Diff
+514
View File
@@ -0,0 +1,514 @@
#include "upstream_multiplexer.h"
#include <algorithm>
#include <cassert>
#include <numeric>
#include <magic_enum.hpp>
#include "common/net_utils.h"
#include "vpn/internal/vpn_client.h"
#define log_mux(mux_, lvl_, fmt_, ...) lvl_##log((mux_)->m_log, "[{}] " fmt_, (mux_)->id, ##__VA_ARGS__)
#define log_conn(mux_, cid_, lvl_, fmt_, ...) \
lvl_##log((mux_)->m_log, "[{}] [R:{}] " fmt_, (mux_)->id, (uint64_t) (cid_), ##__VA_ARGS__)
namespace ag {
struct UpstreamCtx {
UpstreamMultiplexer *mux;
int id;
};
enum UpstreamState {
US_OPENING_SESSION,
US_SESSION_OPENED,
};
struct UpstreamInfo {
UpstreamInfo(const UpstreamMultiplexer::MakeUpstream &make_upstream,
const VpnUpstreamProtocolConfig &protocol_config, int id, VpnClient *vpn,
decltype(SeverHandler::func) handler, std::unique_ptr<UpstreamCtx> ctx)
: upstream(make_upstream(protocol_config, id, vpn, {handler, ctx.get()}))
, ctx(std::move(ctx)) {
}
UpstreamState state = US_OPENING_SESSION;
std::unique_ptr<MultiplexableUpstream> upstream;
std::unique_ptr<UpstreamCtx> ctx;
ag::AutoTaskId deferred_task_id;
};
UpstreamMultiplexer::UpstreamMultiplexer(
int id, const VpnUpstreamProtocolConfig &protocol_config, size_t upstreams_num, MakeUpstream make_upstream)
: ServerUpstream(id, protocol_config)
, m_max_upstreams_num((upstreams_num == 0) ? DEFAULT_UPSTREAMS_NUM : upstreams_num)
, m_make_upstream(std::move(make_upstream)) {
m_upstreams_pool.reserve(m_max_upstreams_num);
}
UpstreamMultiplexer::~UpstreamMultiplexer() = default;
bool UpstreamMultiplexer::init(VpnClient *vpn, SeverHandler handler) {
if (!this->ServerUpstream::init(vpn, handler)) {
log_mux(this, err, "Failed to initialize base upstream");
deinit();
return false;
}
return true;
}
void UpstreamMultiplexer::deinit() {
}
bool UpstreamMultiplexer::open_session(uint32_t timeout_ms) {
assert(m_upstreams_pool.empty());
int upstream_id = select_upstream_for_connection();
if (!open_new_upstream(upstream_id, timeout_ms)) {
log_mux(this, err, "Failed to open session");
return false;
}
return true;
}
void UpstreamMultiplexer::close_session() {
for (auto &[_, info] : m_upstreams_pool) {
info->upstream->close_session();
}
m_upstreams_pool.clear();
}
uint64_t UpstreamMultiplexer::open_connection(const TunnelAddressPair *addr, int proto, std::string_view app_name) {
int upstream_id = select_upstream_for_connection();
uint64_t conn_id = this->vpn->upstream_conn_id_generator.get();
auto i = m_upstreams_pool.find(upstream_id);
if (i != m_upstreams_pool.end()) {
log_conn(this, conn_id, trace, "Using open upstream (id={})", upstream_id);
if (!open_connection(upstream_id, conn_id, addr, proto, app_name)) {
conn_id = NON_ID;
}
} else if (open_new_upstream(upstream_id, 0)) {
log_conn(this, conn_id, dbg, "Opening new upstream (id={})", upstream_id);
m_pending_connections.emplace(conn_id, PendingConnection{{upstream_id}, *addr, proto, std::string(app_name)});
} else if (std::optional<int> reserve_id = select_existing_upstream(upstream_id, true); reserve_id.has_value()) {
upstream_id = reserve_id.value();
log_conn(this, conn_id, dbg, "Failed to create new upstream, using existing one (id={})", upstream_id);
if (!open_connection(upstream_id, conn_id, addr, proto, app_name)) {
log_conn(this, conn_id, dbg, "Failed to fall back on existing upstream");
conn_id = NON_ID;
}
} else {
log_conn(this, conn_id, dbg, "Failed to create a new upstream, no upstreams available", upstream_id);
conn_id = NON_ID;
}
return conn_id;
}
void UpstreamMultiplexer::close_connection(uint64_t id, bool graceful, bool async) {
MultiplexableUpstream *upstream = get_upstream_by_conn(id);
if (upstream != nullptr) {
upstream->close_connection(id, graceful, async);
} else {
log_conn(this, id, dbg, "Connection was not found");
}
}
ssize_t UpstreamMultiplexer::send(uint64_t id, const uint8_t *data, size_t length) {
ssize_t result = -1;
MultiplexableUpstream *upstream = get_upstream_by_conn(id);
if (upstream != nullptr) {
result = upstream->send(id, data, length);
} else {
log_conn(this, id, dbg, "Connection was not found");
}
return result;
}
void UpstreamMultiplexer::consume(uint64_t id, size_t length) {
MultiplexableUpstream *upstream = get_upstream_by_conn(id);
if (upstream != nullptr) {
upstream->consume(id, length);
} else {
log_conn(this, id, dbg, "Connection was not found");
}
}
size_t UpstreamMultiplexer::available_to_send(uint64_t id) {
ssize_t result = 0;
MultiplexableUpstream *upstream = get_upstream_by_conn(id);
if (upstream != nullptr) {
result = upstream->available_to_send(id);
} else {
log_conn(this, id, dbg, "Connection was not found");
}
return result;
}
void UpstreamMultiplexer::update_flow_control(uint64_t id, TcpFlowCtrlInfo info) {
MultiplexableUpstream *upstream = get_upstream_by_conn(id);
if (upstream != nullptr) {
upstream->update_flow_control(id, info);
} else {
log_conn(this, id, dbg, "Connection was not found");
}
}
VpnError UpstreamMultiplexer::do_health_check() {
if (m_health_check_upstream_id.has_value()) {
log_mux(this, dbg, "Another health check is already in progress, ignoring this one");
return {};
}
std::optional<int> upstream_id;
UpstreamInfo *info = nullptr;
for (auto &[id, i] : m_upstreams_pool) {
if (i->state == US_SESSION_OPENED) {
upstream_id = id;
info = i.get();
break;
}
}
if (info == nullptr) {
return {VPN_EC_ERROR, "There are no open sessions"};
}
VpnError error = info->upstream->do_health_check();
if (error.code == VPN_EC_NOERROR) {
m_health_check_upstream_id = upstream_id;
}
return error;
}
VpnConnectionStats UpstreamMultiplexer::get_connection_stats() const {
static constexpr auto PICK_WORST_RTT = [](uint32_t lh, uint32_t rh) -> uint32_t {
return std::max(lh, rh);
};
static constexpr auto PICK_WORST_LOSS_RATIO = [](double lh, double rh) -> double {
return std::max(lh, rh);
};
VpnConnectionStats stats = {};
for (const auto &[_, i] : m_upstreams_pool) {
if (i->state == US_SESSION_OPENED) {
VpnConnectionStats i_stats = i->upstream->get_connection_stats();
stats = {
PICK_WORST_RTT(stats.rtt_us, i_stats.rtt_us),
PICK_WORST_LOSS_RATIO(stats.packet_loss_ratio, i_stats.packet_loss_ratio),
};
}
}
return stats;
}
void UpstreamMultiplexer::on_icmp_request(IcmpEchoRequestEvent &event) {
auto it = std::find_if(m_upstreams_pool.begin(), m_upstreams_pool.end(), [](const auto &i) {
return i.second->state == US_SESSION_OPENED;
});
if (it == m_upstreams_pool.end()) {
log_mux(this, dbg, "Failed to find a connected upstream");
assert(0);
event.result = -1;
return;
}
it->second->upstream->on_icmp_request(event);
}
void UpstreamMultiplexer::mark_closed_upstream(int upstream_id, ag::AutoTaskId task_id) {
auto it = m_upstreams_pool.find(upstream_id);
if (it == m_upstreams_pool.end()) {
log_mux(this, err, "Inexistent upstream: id={}", upstream_id);
assert(0);
return;
}
it->second->deferred_task_id = std::move(task_id);
auto node = m_upstreams_pool.extract(it);
m_closed_upstreams.emplace(node.key(), std::move(node.mapped()));
}
void UpstreamMultiplexer::finalize_closed_upstream(int upstream_id, bool async) {
static constexpr auto ACTION = [](UpstreamMultiplexer *mux, int id) {
if (auto i = mux->m_closed_upstreams.find(id); i != mux->m_closed_upstreams.end()) {
i->second->deferred_task_id.release();
mux->m_closed_upstreams.erase(i);
}
log_mux(mux, dbg, "Remaining upstreams={} connections={} pending connections={}", mux->m_upstreams_pool.size(),
mux->m_connections.size(), mux->m_pending_connections.size());
if (mux->m_upstreams_pool.empty() && mux->m_closed_upstreams.empty()) {
mux->handler.func(mux->handler.arg, SERVER_EVENT_SESSION_CLOSED, nullptr);
}
};
if (!async) {
ACTION(this, upstream_id);
} else {
auto it = m_closed_upstreams.find(upstream_id);
if (it == m_closed_upstreams.end()) {
log_mux(this, err, "Inexistent upstream: id={}", upstream_id);
assert(0);
return;
}
UpstreamCtx *ctx = it->second->ctx.get();
it->second->deferred_task_id = ag::submit(vpn->parameters.ev_loop,
{
ctx,
[](void *arg, TaskId) {
auto *ctx = (UpstreamCtx *) arg;
ACTION(ctx->mux, ctx->id);
},
});
}
}
static bool is_fatal_error(const VpnError &error) {
return error.code == VPN_EC_AUTH_REQUIRED;
}
void UpstreamMultiplexer::child_upstream_handler(void *arg, ServerEvent what, void *data) {
auto *ctx = (UpstreamCtx *) arg;
UpstreamMultiplexer *mux = ctx->mux;
auto pool_it = mux->m_upstreams_pool.find(ctx->id);
if (pool_it == mux->m_upstreams_pool.end()) {
pool_it = mux->m_closed_upstreams.find(ctx->id);
if (pool_it == mux->m_closed_upstreams.end()) {
log_mux(mux, err, "Got event on closed upstream: id={} event={}", ctx->id, magic_enum::enum_name(what));
assert(0);
return;
}
}
switch (what) {
case SERVER_EVENT_SESSION_OPENED:
if (mux->m_upstreams_pool.size() == 1) {
mux->handler.func(mux->handler.arg, SERVER_EVENT_SESSION_OPENED, data);
}
pool_it->second->state = US_SESSION_OPENED;
for (auto i = mux->m_pending_connections.begin(); i != mux->m_pending_connections.end();) {
const PendingConnection *conn = &i->second;
if (conn->upstream_id == ctx->id) {
mux->proceed_pending_connection(conn->upstream_id, i->first, conn);
i = mux->m_pending_connections.erase(i);
} else {
++i;
}
}
break;
case SERVER_EVENT_SESSION_CLOSED: {
for (auto i = mux->m_pending_connections.begin(); i != mux->m_pending_connections.end();) {
const PendingConnection *conn = &i->second;
if (conn->upstream_id == ctx->id) {
ServerError err_event = {i->first, {ag::utils::AG_ECONNREFUSED, "Session closed"}};
mux->handler.func(mux->handler.arg, SERVER_EVENT_ERROR, &err_event);
i = mux->m_pending_connections.erase(i);
} else {
++i;
}
}
mux->mark_closed_upstream(ctx->id, {});
mux->finalize_closed_upstream(ctx->id, true);
break;
}
case SERVER_EVENT_CONNECTION_CLOSED: {
uint64_t id = *(uint64_t *) data;
assert(mux->m_connections.count(id) != 0);
mux->m_connections.erase(id);
log_mux(mux, dbg, "Remaining upstreams={} connections={} pending connections={}", mux->m_upstreams_pool.size(),
mux->m_connections.size(), mux->m_pending_connections.size());
[[fallthrough]];
}
case SERVER_EVENT_CONNECTION_OPENED:
case SERVER_EVENT_READ:
case SERVER_EVENT_DATA_SENT:
case SERVER_EVENT_GET_AVAILABLE_TO_SEND:
case SERVER_EVENT_ECHO_REPLY:
mux->handler.func(mux->handler.arg, what, data);
break;
case SERVER_EVENT_HEALTH_CHECK_RESULT:
mux->handler.func(mux->handler.arg, what, data);
mux->m_health_check_upstream_id.reset();
break;
case SERVER_EVENT_ERROR: {
const ServerError *event = (ServerError *) data;
if (event->id != NON_ID) {
mux->m_connections.erase(event->id);
log_mux(mux, dbg, "Remaining upstreams={} connections={} pending connections={}",
mux->m_upstreams_pool.size(), mux->m_connections.size(), mux->m_pending_connections.size());
mux->handler.func(mux->handler.arg, SERVER_EVENT_ERROR, data);
} else if (is_fatal_error(event->error)
// do not ignore errors on a health checking upstream
|| mux->m_health_check_upstream_id == ctx->id) {
mux->handler.func(mux->handler.arg, SERVER_EVENT_ERROR, data);
} else {
mux->mark_closed_upstream(ctx->id,
ag::submit(mux->vpn->parameters.ev_loop,
{
ctx,
[](void *arg, TaskId) {
auto *ctx = (UpstreamCtx *) arg;
UpstreamMultiplexer *mux = ctx->mux;
auto it = mux->m_closed_upstreams.find(ctx->id);
if (it != mux->m_closed_upstreams.end()) {
it->second->upstream->close_session();
} else {
log_mux(mux, err, "Inexistent upstream: id={}", ctx->id);
assert(0);
}
mux->finalize_closed_upstream(ctx->id, false);
},
}));
}
break;
}
}
}
MultiplexableUpstream *UpstreamMultiplexer::get_upstream_by_conn(uint64_t id) const {
auto it_id = m_connections.find(id);
if (it_id == m_connections.end()) {
log_conn(this, id, dbg, "Connection not found");
return nullptr;
}
auto pool_it = m_upstreams_pool.find(it_id->second.upstream_id);
if (pool_it == m_upstreams_pool.end()) {
log_conn(this, id, dbg, "Upstream for connection not found: {}", it_id->second.upstream_id);
return nullptr;
}
return pool_it->second->upstream.get();
}
std::optional<int> UpstreamMultiplexer::select_existing_upstream(
std::optional<int> ignored_upstream, bool allow_underflow) const {
// for the first try to pick underloaded upstream
for (const auto &[id, _] : m_upstreams_pool) {
if (ignored_upstream != id && connections_num_by_upstream(id) < NEW_UPSTREAM_CONNECTIONS_NUM_THRESHOLD) {
return id;
}
}
// if a caller wants an existing upstream or the number of open upstreams reached the cap,
// choose the least loaded
if (allow_underflow || m_upstreams_pool.size() == DEFAULT_UPSTREAMS_NUM) {
std::optional<decltype(m_upstreams_pool.begin())> least_loaded;
for (auto i = m_upstreams_pool.begin(); i != m_upstreams_pool.end(); ++i) {
if (i->first == ignored_upstream) {
continue;
}
if (!least_loaded.has_value()
|| (*least_loaded)->second->upstream->connections_num() > i->second->upstream->connections_num()) {
least_loaded = i;
}
}
if (least_loaded.has_value()) {
return (*least_loaded)->first;
}
}
return std::nullopt;
}
int UpstreamMultiplexer::select_upstream_for_connection() {
std::optional<int> id = select_existing_upstream(std::nullopt, false);
if (id.has_value()) {
return id.value();
}
// otherwise, create a new one
return m_next_upstream_id++;
}
bool UpstreamMultiplexer::open_new_upstream(int id, uint32_t timeout_ms) {
assert(m_upstreams_pool.count(id) == 0);
std::unique_ptr<UpstreamCtx> ctx = std::make_unique<UpstreamCtx>(UpstreamCtx{this, id});
std::unique_ptr<UpstreamInfo> info = std::make_unique<UpstreamInfo>(
m_make_upstream, this->PROTOCOL_CONFIG.value(), id, this->vpn, &child_upstream_handler, std::move(ctx));
if (!info->upstream->open_session(timeout_ms)) {
log_mux(this, err, "Failed to open session");
return false;
}
m_upstreams_pool[id] = std::move(info);
return true;
}
bool UpstreamMultiplexer::open_connection(
int upstream_id, uint64_t conn_id, const TunnelAddressPair *addr, int proto, std::string_view app_name) {
auto i = m_upstreams_pool.find(upstream_id);
if (i == m_upstreams_pool.end()) {
log_mux(this, err, "Failed to find selected upstream for connection in the list: {}", upstream_id);
assert(0);
return false;
}
bool successful = true;
UpstreamInfo *info = i->second.get();
switch (info->state) {
case US_OPENING_SESSION:
log_conn(this, conn_id, trace, "Postpone connection until session is established");
m_pending_connections.emplace(conn_id, PendingConnection{{upstream_id}, *addr, proto, std::string(app_name)});
break;
case US_SESSION_OPENED:
successful = info->upstream->open_connection(conn_id, addr, proto, app_name);
if (successful) {
m_connections.emplace(conn_id, Connection{upstream_id});
}
break;
}
return successful;
}
void UpstreamMultiplexer::proceed_pending_connection(int upstream_id, uint64_t conn_id, const PendingConnection *conn) {
if (open_connection(upstream_id, conn_id, &conn->addr, conn->proto, conn->app_name)) {
return;
}
assert(!m_upstreams_pool.empty());
int fallback_upstream_id = m_upstreams_pool.begin()->first;
log_conn(this, conn_id, dbg,
"Failed to open connection on new upstream (id={}), falling back on existing one (id={})", upstream_id,
fallback_upstream_id);
if (!open_connection(fallback_upstream_id, conn_id, &conn->addr, conn->proto, conn->app_name)) {
log_conn(this, conn_id, dbg, "Failed to fall back on existing upstream");
ServerError err_event = {conn_id, {ag::utils::AG_ECONNREFUSED, "Failed to connect"}};
this->handler.func(this->handler.arg, SERVER_EVENT_ERROR, &err_event);
}
}
size_t UpstreamMultiplexer::connections_num_by_upstream(int id) const {
assert(m_upstreams_pool.count(id) != 0);
return m_upstreams_pool.find(id)->second->upstream->connections_num()
+ std::accumulate(m_pending_connections.begin(), m_pending_connections.end(), 0,
[id](size_t acc, const auto &i) -> size_t {
return acc + ((id == i.second.upstream_id) ? 1 : 0);
});
}
} // namespace ag
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include "common/logger.h"
#include "multiplexable_upstream.h"
namespace ag {
struct UpstreamInfo;
class UpstreamMultiplexer : public ServerUpstream {
public:
// Maximum number of upstreams
static constexpr size_t DEFAULT_UPSTREAMS_NUM = 8;
// Number of connection exceeding which a new upstream will be opened
static constexpr size_t NEW_UPSTREAM_CONNECTIONS_NUM_THRESHOLD = 5;
using MakeUpstream = std::unique_ptr<MultiplexableUpstream> (*)(
const VpnUpstreamProtocolConfig &protocol_config, int id, VpnClient *vpn, SeverHandler handler);
UpstreamMultiplexer(
int id, const VpnUpstreamProtocolConfig &protocol_config, size_t upstreams_num, MakeUpstream make_upstream);
~UpstreamMultiplexer() override;
UpstreamMultiplexer() = delete;
UpstreamMultiplexer(const UpstreamMultiplexer &) = delete;
UpstreamMultiplexer &operator=(const UpstreamMultiplexer &) = delete;
UpstreamMultiplexer(UpstreamMultiplexer &&) = delete;
UpstreamMultiplexer &operator=(UpstreamMultiplexer &&) = delete;
private:
struct Connection {
int upstream_id;
};
struct PendingConnection : public Connection {
TunnelAddressPair addr;
int proto;
std::string app_name;
};
std::unordered_map<uint64_t, Connection> m_connections;
std::unordered_map<int, std::unique_ptr<UpstreamInfo>> m_upstreams_pool;
size_t m_max_upstreams_num = DEFAULT_UPSTREAMS_NUM;
std::unordered_map<uint64_t, PendingConnection> m_pending_connections;
int m_next_upstream_id = 0;
std::unordered_map<int, std::unique_ptr<UpstreamInfo>> m_closed_upstreams; // Upstreams waiting to be deleted
std::optional<int> m_health_check_upstream_id; // id of an upstream which performs health check
MakeUpstream m_make_upstream;
std::vector<ag::AutoTaskId> m_failed_icmp_pings;
ag::Logger m_log{"UPSTREAM_MUX"};
bool init(VpnClient *vpn, SeverHandler handler) override;
void deinit() override;
bool open_session(uint32_t timeout_ms) override;
void close_session() override;
uint64_t open_connection(const TunnelAddressPair *addr, int proto, std::string_view app_name) override;
void close_connection(uint64_t id, bool graceful, bool async) override;
ssize_t send(uint64_t id, const uint8_t *data, size_t length) override;
void consume(uint64_t id, size_t length) override;
size_t available_to_send(uint64_t id) override;
void update_flow_control(uint64_t id, TcpFlowCtrlInfo info) override;
VpnError do_health_check() override;
[[nodiscard]] VpnConnectionStats get_connection_stats() const override;
void on_icmp_request(IcmpEchoRequestEvent &event) override;
static void child_upstream_handler(void *arg, ServerEvent what, void *data);
[[nodiscard]] MultiplexableUpstream *get_upstream_by_conn(uint64_t id) const;
[[nodiscard]] std::optional<int> select_existing_upstream(
std::optional<int> ignored_upstream, bool allow_underflow) const;
int select_upstream_for_connection();
bool open_new_upstream(int id, uint32_t timeout_ms);
bool open_connection(
int upstream_id, uint64_t conn_id, const TunnelAddressPair *addr, int proto, std::string_view app_name);
void proceed_pending_connection(int upstream_id, uint64_t conn_id, const PendingConnection *conn);
[[nodiscard]] size_t connections_num_by_upstream(int upstream_id) const;
void mark_closed_upstream(int upstream_id, ag::AutoTaskId task_id);
void finalize_closed_upstream(int upstream_id, bool async);
};
} // namespace ag
+209
View File
@@ -0,0 +1,209 @@
#include <algorithm>
#include <numeric>
#include <FFOS/dir.h>
#include <FFOS/file.h>
#include "common/base64.h"
#include "common/net_utils.h"
#include "common/utils.h"
#include "http_udp_multiplexer.h"
#include "vpn/internal/utils.h"
#include "vpn/platform.h"
#include "vpn/utils.h"
namespace ag {
std::string tunnel_addr_to_str(const TunnelAddress *tun_addr) {
std::string out = {};
if (const sockaddr_storage *addr = std::get_if<sockaddr_storage>(tun_addr)) {
out = sockaddr_to_str((sockaddr *) addr);
} else if (const NamePort *domain = std::get_if<NamePort>(tun_addr)) {
out.reserve(domain->name.length() + 6);
out += domain->name;
if (domain->port > 0) {
out += ":";
out += std::to_string(domain->port);
}
}
return out;
}
std::string make_buffer_file_path(const char *base_path, uint64_t id) {
return str_format("%s/" CONN_BUFFER_FILE_NAME_FMT, base_path, (uint64_t) time(nullptr), id);
}
static bool is_conn_buffer_file(const char *fname) {
uint64_t tmp;
return 2 == std::sscanf(fname, CONN_BUFFER_FILE_NAME_FMT, &tmp, &tmp);
}
void clean_up_buffer_files(const char *dir) {
char path[strlen(dir) + 256];
strcpy(path, dir);
ffdirentry dent = {};
ffdir d = ffdir_open(path, sizeof(path), &dent);
if (!d) {
return;
}
while (0 == ffdir_read(d, &dent)) {
std::string fn = safe_path_name(ffdir_entryname(&dent));
if (fn == "." || fn == "..") {
continue;
}
ffdir_einfo *entry_info = ffdir_entryinfo(&dent);
if (entry_info != nullptr && !fffile_isdir(fffile_infoattr(entry_info)) && is_conn_buffer_file(fn.c_str())) {
fffile_rm(str_format("%s/%s", dir, fn.c_str()).c_str());
}
}
ffdir_close(d);
}
VpnUpstreamConfig vpn_upstream_config_clone(const VpnUpstreamConfig *src) {
VpnUpstreamConfig dst = *src;
vpn_location_clone(&dst.location, &src->location);
dst.username = safe_strdup(src->username);
dst.password = safe_strdup(src->password);
return dst;
}
void vpn_upstream_config_destroy(VpnUpstreamConfig *config) {
vpn_location_destroy(&config->location);
free((char *) config->username);
free((char *) config->password);
*config = {};
}
static void set_auth_info(HttpHeaders *headers, std::string_view creds);
void log_headers(const ag::Logger &log, uint64_t stream_id, const HttpHeaders *headers, const char *msg) {
if (log.is_enabled(ag::LOG_LEVEL_DEBUG)) {
std::string hdr_str;
if (headers->status_code == 0) {
HttpHeaders sheaders = *headers;
sheaders.remove_field("proxy-authorization");
set_auth_info(&sheaders, "__stripped__");
hdr_str = http_headers_to_http1_message(&sheaders, false);
} else {
hdr_str = http_headers_to_http1_message(headers, false);
}
hdr_str = http_headers_to_http1_message(headers, false);
dbglog(log, "[SID:{}] {}:\n{}", stream_id, msg, hdr_str);
}
}
// Connection errors
// DNS resolution failed (reasons see below)
// HTTP/1.1 502 Bad Gateway
// X-Adguard-Vpn-Error: <hostname>
// X-Warning: <warn-code> - <warn-text>
// For other reasons:
// HTTP/1.1 502 Bad Gateway
// X-Warning: <warn-code> - <warn-text>
// Warn codes:
enum VpnWarnCode {
CONNECTION_FAILED = 300, // Connection failed for some reasons
HOST_UNREACHABLE = 301, // Remote host is unreachable
CONNECTION_TIMEDOUT = 302, // Connection timed out
DNS_NONROUTABLE = 310, // DNS: resolved address in non-routable network
DNS_LOOPBACK = 311, // DNS: resolved address in loopback
DNS_BLOCKED = 312, // DNS: blocked by Adguard DNS
};
VpnError bad_http_response_to_connect_error(const HttpHeaders *response) {
VpnError err = {ag::utils::AG_ECONNREFUSED, "Bad response status"};
if (response->status_code != 502) {
return err;
}
if (auto vpn_error = response->get_field("X-Adguard-Vpn-Error")) {
// do nothing - DNS resolution error, just return refused
} else if (auto warning = response->get_field("X-Warning")) {
if (auto code = ag::utils::to_integer<int>(*warning)) {
switch (*code) {
case HOST_UNREACHABLE:
err.code = AG_ENETUNREACH;
break;
case CONNECTION_TIMEDOUT:
err.code = ag::utils::AG_ETIMEDOUT;
break;
default:
break;
}
}
}
return err;
}
// Put `user-agent: <platform> <app_name>`, e.g. `user-agent: Windows chrome.exe`
static void put_user_agent(HttpHeaders *headers, std::string_view app_name) {
char buf[2 * UDPPKT_APP_MAXSIZE];
int r = snprintf(buf, sizeof(buf), "%s %.*s", AG_PLATFORM, (int) app_name.size(), app_name.data());
headers->put_field("user-agent", std::string{buf, size_t(r)});
}
static void set_auth_info(HttpHeaders *headers, std::string_view creds) {
size_t buf_size = creds.length() + 10;
char buffer[buf_size];
int r = snprintf(buffer, sizeof(buffer), "Basic %.*s", (int) creds.length(), creds.data());
headers->put_field("proxy-authorization", std::string{buffer, size_t(r)});
}
HttpHeaders make_http_connect_request(
HttpVersion version, const TunnelAddress *dst_addr, std::string_view app_name, std::string_view creds) {
HttpHeaders headers{.version = version};
headers.method = HTTP_METHOD_CONNECT;
headers.authority = tunnel_addr_to_str(dst_addr);
put_user_agent(&headers, app_name.empty() ? "unknown" : app_name);
set_auth_info(&headers, creds);
return headers;
}
std::string make_credentials(std::string_view username, std::string_view password) {
std::string creds =
str_format("%.*s:%.*s", (int) username.length(), username.data(), (int) password.length(), password.data());
return ag::encode_to_base64({(uint8_t *) creds.data(), creds.size()}, false);
}
std::variant<SslPtr, std::string> make_ssl(
int (*verification_callback)(X509_STORE_CTX *, void *), void *arg, U8View alpn_protos, const char *sni) {
DeclPtr<SSL_CTX, SSL_CTX_free> ctx{SSL_CTX_new(TLS_client_method())};
SSL_CTX_set_verify(ctx.get(), SSL_VERIFY_PEER, nullptr);
SSL_CTX_set_cert_verify_callback(ctx.get(), verification_callback, arg);
if (0 != SSL_CTX_set_alpn_protos(ctx.get(), alpn_protos.data(), alpn_protos.size())) {
return "Failed to set ALPN protocols";
}
SslPtr ssl{SSL_new(ctx.get())};
if (0 == SSL_set_tlsext_host_name(ssl.get(), sni)) {
return "Failed to set SNI";
}
#if 0
if (char *ssl_keylog_file = getenv("SSLKEYLOGFILE"); ssl_keylog_file != nullptr) {
static DeclPtr<std::FILE, &std::fclose> handle{ std::fopen(ssl_keylog_file, "a") };
SSL_CTX_set_keylog_callback(ctx.get(),
[] (const SSL *, const char *line) {
fprintf(handle.get(), "%s\n", line);
fflush(handle.get());
});
}
#endif
return ssl;
}
} // namespace ag
+727
View File
@@ -0,0 +1,727 @@
#include <atomic>
#include <cassert>
#include <chrono>
#include <event2/util.h>
#include "direct_upstream.h"
#include "fallbackable_upstream_connector.h"
#include "http2_upstream.h"
#include "http3_upstream.h"
#include "memfile_buffer.h"
#include "memory_buffer.h"
#include "single_upstream_connector.h"
#include "socks_listener.h"
#include "upstream_multiplexer.h"
#include "vpn/internal/client_listener.h"
#include "vpn/internal/server_upstream.h"
#include "vpn/internal/tunnel.h"
#include "vpn/internal/vpn_client.h"
#include "vpn/platform.h"
#include "vpn/utils.h"
#include "vpn/vpn.h"
#define log_client(cli_, lvl_, fmt_, ...) lvl_##log((cli_)->log, "[{}] " fmt_, (cli_)->id, ##__VA_ARGS__)
namespace ag {
static std::atomic_int g_next_id = 0;
static constexpr std::string_view DNS_PROXY_CHECK_DOMAIN = "ipv4only.arpa";
using namespace std::chrono;
namespace vpn_client {
enum State {
S_DISCONNECTED,
S_CONNECTING,
S_CONNECTED,
S_DISCONNECTING,
};
enum SessionEvent {
E_RUN_CONNECT,
E_DISCONNECT,
E_DEFERRED_DISCONNECT,
E_RUN_PREPARATION_FAIL,
E_SESSION_OPENED,
E_SESSION_CLOSED,
E_SESSION_ERROR,
E_HEALTH_CHECK_READY,
};
static constexpr auto STATE_NAMES = make_enum_names_array<State>();
static constexpr auto EVENT_NAMES = make_enum_names_array<SessionEvent>();
static bool is_successful(const void *ctx, void *data);
static void run_connect(void *ctx, void *data);
static void schedule_health_check(void *ctx, void *data);
static void raise_connected(void *ctx, void *data);
static void raise_disconnected(void *ctx, void *data);
static void run_disconnect(void *ctx, void *data);
static void submit_disconnect(void *ctx, void *data);
// clang-format off
static constexpr FsmTransitionEntry TRANSITION_TABLE[] = {
{S_DISCONNECTED, E_RUN_CONNECT, Fsm::ANYWAY, run_connect, S_CONNECTING, Fsm::DO_NOTHING},
{S_DISCONNECTED, E_SESSION_CLOSED, Fsm::ANYWAY, Fsm::DO_NOTHING, S_DISCONNECTED, raise_disconnected},
{S_DISCONNECTED, E_DISCONNECT, Fsm::ANYWAY, Fsm::DO_NOTHING, S_DISCONNECTED, Fsm::DO_NOTHING},
{S_CONNECTING, E_SESSION_OPENED, Fsm::ANYWAY, schedule_health_check, S_CONNECTED, raise_connected},
{S_CONNECTING, E_SESSION_CLOSED, Fsm::ANYWAY, Fsm::DO_NOTHING, S_DISCONNECTED, raise_disconnected},
{S_CONNECTED, E_SESSION_CLOSED, Fsm::ANYWAY, Fsm::DO_NOTHING, S_DISCONNECTED, raise_disconnected},
{S_CONNECTED, E_SESSION_ERROR, Fsm::ANYWAY, submit_disconnect, S_DISCONNECTING, Fsm::DO_NOTHING},
{S_CONNECTED, E_RUN_PREPARATION_FAIL, Fsm::ANYWAY, submit_disconnect, S_DISCONNECTING, Fsm::DO_NOTHING},
{S_CONNECTED, E_HEALTH_CHECK_READY, is_successful, schedule_health_check, Fsm::SAME_TARGET_STATE,
Fsm::DO_NOTHING},
{S_CONNECTED, E_HEALTH_CHECK_READY, Fsm::OTHERWISE, submit_disconnect, S_DISCONNECTING, Fsm::DO_NOTHING},
{S_DISCONNECTING, E_SESSION_CLOSED, Fsm::ANYWAY, Fsm::DO_NOTHING, S_DISCONNECTED, raise_disconnected},
{S_DISCONNECTING, E_DISCONNECT, Fsm::ANYWAY, Fsm::DO_NOTHING, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{S_DISCONNECTING, E_DEFERRED_DISCONNECT, Fsm::ANYWAY, run_disconnect, S_DISCONNECTED, raise_disconnected},
{Fsm::ANY_SOURCE_STATE, E_DISCONNECT, Fsm::ANYWAY, run_disconnect, S_DISCONNECTED, Fsm::DO_NOTHING},
};
// clang-format on
} // namespace vpn_client
static void release_deferred_task(VpnClient *self, TaskId task) {
if (auto n = self->deferred_tasks.extract(ag::make_auto_id(task)); !n.empty()) {
n.value().release();
}
}
static void endpoint_connector_handler(void *arg, EndpointConnectorResult result) {
auto *self = (VpnClient *) arg;
self->pending_error.reset();
if (const auto *e = std::get_if<VpnError>(&result); e != nullptr) {
self->pending_error = *e;
} else {
self->endpoint_upstream = std::move(std::get<std::unique_ptr<ServerUpstream>>(result));
}
self->deferred_tasks.emplace(ag::submit(
self->parameters.ev_loop, {self, [](void *arg, TaskId task_id) {
auto *self = (VpnClient *) arg;
release_deferred_task(self, task_id);
self->endpoint_connector.reset();
if (!self->pending_error.has_value()) {
self->tunnel->upstream_handler(self->endpoint_upstream.get(),
SERVER_EVENT_SESSION_OPENED, nullptr);
self->fsm.perform_transition(vpn_client::E_SESSION_OPENED, nullptr);
// reconnect or `listen` was called before connection procedure
// completion
if (self->client_listener != nullptr) {
self->tunnel->on_exclusions_updated();
if (self->dns_proxy != nullptr) {
self->do_dns_upstream_health_check();
}
}
} else {
self->fsm.perform_transition(vpn_client::E_SESSION_CLOSED, nullptr);
}
}}));
}
static void vpn_upstream_handler(void *arg, ServerEvent what, void *data) {
auto *vpn = (VpnClient *) arg;
assert(vpn->endpoint_connector == nullptr);
bool is_disconnected =
what == SERVER_EVENT_SESSION_CLOSED || (what == SERVER_EVENT_ERROR && ((ServerError *) data)->id == NON_ID);
if (is_disconnected) {
vpn->tunnel->on_before_endpoint_disconnect(vpn->endpoint_upstream.get());
}
vpn->tunnel->upstream_handler(vpn->endpoint_upstream.get(), what, data);
switch (what) {
case SERVER_EVENT_SESSION_OPENED: {
assert(0);
break;
}
case SERVER_EVENT_SESSION_CLOSED: {
log_client(vpn, dbg, "Server session is closed");
vpn->fsm.perform_transition(vpn_client::E_SESSION_CLOSED, nullptr);
break;
}
case SERVER_EVENT_HEALTH_CHECK_RESULT: {
const VpnError *error = (VpnError *) data;
if (error == nullptr || error->code == VPN_EC_NOERROR) {
log_client(vpn, dbg, "Health check succeeded");
} else {
log_client(vpn, dbg, "Health check error: {} ({})", error->text, error->code);
}
vpn->fsm.perform_transition(vpn_client::E_HEALTH_CHECK_READY, data);
break;
}
case SERVER_EVENT_ERROR: {
const ServerError *event = (ServerError *) data;
if (event->id != NON_ID) {
break;
}
log_client(vpn, dbg, "Server session terminated with error: {} ({})", safe_to_string_view(event->error.text),
event->error.code);
vpn->fsm.perform_transition(vpn_client::E_SESSION_ERROR, (void *) &event->error);
break;
}
case SERVER_EVENT_CONNECTION_OPENED:
case SERVER_EVENT_CONNECTION_CLOSED:
case SERVER_EVENT_READ:
case SERVER_EVENT_DATA_SENT:
case SERVER_EVENT_GET_AVAILABLE_TO_SEND:
case SERVER_EVENT_ECHO_REPLY:
// do nothing
break;
}
if (is_disconnected) {
vpn->tunnel->on_after_endpoint_disconnect(vpn->endpoint_upstream.get());
}
}
static void direct_upstream_handler(void *arg, ServerEvent what, void *data) {
auto *vpn = (VpnClient *) arg;
vpn->tunnel->upstream_handler(vpn->bypass_upstream.get(), what, data);
}
static void listener_handler(void *arg, ClientEvent what, void *data) {
auto *vpn = (VpnClient *) arg;
vpn->tunnel->listener_handler(vpn->client_listener.get(), what, data);
}
static void dns_proxy_listener_handler(void *arg, ClientEvent what, void *data) {
auto *vpn = (VpnClient *) arg;
vpn->tunnel->listener_handler(vpn->dns_proxy_listener.get(), what, data);
}
static void dns_resolver_handler(void *arg, VpnDnsResolveId, VpnDnsResolverResult result) {
auto *self = (VpnClient *) arg;
if (std::holds_alternative<VpnDnsResolverSuccess>(result)) {
log_client(self, dbg, "DNS resolver health check succeeded");
} else {
log_client(self, dbg, "DNS resolver health check failed");
VpnDnsUpstreamUnavailableEvent event = {
.upstream = self->listener_config.dns_upstream,
};
self->parameters.handler.func(self->parameters.handler.arg, vpn_client::EVENT_DNS_UPSTREAM_UNAVAILABLE, &event);
}
}
static FsmParameters make_fsm_params(VpnClient *vpn) {
return {vpn_client::S_DISCONNECTED,
FsmTransitionTable{std::begin(vpn_client::TRANSITION_TABLE), std::end(vpn_client::TRANSITION_TABLE)}, vpn,
vpn_client::LOG_NAME, vpn_client::STATE_NAMES.data(), vpn_client::EVENT_NAMES.data()};
}
VpnClient::VpnClient(vpn_client::Parameters parameters)
: fsm(make_fsm_params(this))
, parameters(parameters)
, id(g_next_id++) {
}
VpnError VpnClient::init(const VpnSettings *settings) {
log_client(this, dbg, "...");
this->quic_enabled = settings->quic_enabled;
this->kill_switch_on = settings->killswitch_enabled;
update_exclusions(settings->mode, {settings->exclusions.data, settings->exclusions.size});
if (settings->tmp_files_base_path != nullptr) {
this->tmp_files_base_path = settings->tmp_files_base_path;
if (this->tmp_files_base_path->back() == '/') {
this->tmp_files_base_path->pop_back();
}
this->conn_memory_buffer_threshold = settings->conn_memory_buffer_threshold;
if (settings->conn_memory_buffer_threshold == 0) {
this->conn_memory_buffer_threshold = VPN_DEFAULT_CONN_MEMORY_BUFFER_THRESHOLD;
}
this->max_conn_buffer_file_size = settings->max_conn_buffer_file_size;
if (settings->max_conn_buffer_file_size == 0) {
this->max_conn_buffer_file_size = VPN_DEFAULT_MAX_CONN_BUFFER_FILE_SIZE;
}
}
VpnError error = {};
if (!this->tunnel->init(this)) {
error = {VPN_EC_INVALID_SETTINGS, "Failed to initialize connection tunnelling module"};
goto fail;
}
this->bypass_upstream = std::make_unique<DirectUpstream>(next_upstream_id());
if (!this->bypass_upstream->init(this, (SeverHandler){&direct_upstream_handler, this})) {
error = {VPN_EC_INVALID_SETTINGS, "Failed to initialize an upstream for bypassed connections"};
goto fail;
}
log_client(this, dbg, "Done");
goto exit;
fail:
log_client(this, err, "Failed: {} ({})", safe_to_string_view(error.text), error.code);
exit:
return error;
}
static VpnError client_connect(VpnClient *vpn, uint32_t timeout_ms) {
log_client(vpn, dbg, "...");
vpn->fsm.perform_transition(vpn_client::E_RUN_CONNECT, &timeout_ms);
if (!vpn->pending_error.has_value()) {
log_client(vpn, dbg, "Started");
} else {
log_client(vpn, dbg, "Failed: {} ({})", vpn->pending_error->text, vpn->pending_error->code);
}
return std::exchange(vpn->pending_error, std::nullopt).value_or(VpnError{});
}
static void submit_health_check(VpnClient *vpn, milliseconds postpone) {
vpn->deferred_tasks.emplace(ag::schedule(vpn->parameters.ev_loop,
{vpn,
[](void *arg, TaskId task_id) {
auto *vpn = (VpnClient *) arg;
release_deferred_task(vpn, task_id);
VpnError error = vpn->endpoint_upstream->do_health_check();
if (error.code != VPN_EC_NOERROR) {
vpn->fsm.perform_transition(vpn_client::E_HEALTH_CHECK_READY, &error);
}
}},
postpone.count()));
}
static std::unique_ptr<ServerUpstream> make_upstream(const VpnUpstreamProtocolConfig &protocol) {
std::unique_ptr<ServerUpstream> upstream;
switch (protocol.type) {
case VPN_UP_HTTP2:
upstream = std::make_unique<UpstreamMultiplexer>(VpnClient::next_upstream_id(), protocol,
protocol.http2.connections_num,
[](const VpnUpstreamProtocolConfig &protocol_config, int id, VpnClient *vpn,
SeverHandler handler) -> std::unique_ptr<MultiplexableUpstream> {
return std::make_unique<Http2Upstream>(protocol_config, id, vpn, handler);
});
break;
case VPN_UP_HTTP3:
upstream = std::make_unique<Http3Upstream>(VpnClient::next_upstream_id(), protocol);
break;
}
return upstream;
}
VpnError VpnClient::connect(vpn_client::EndpointConnectionConfig config, uint32_t timeout_ms) {
log_client(this, dbg, "...");
VpnError error = {};
if (this->fsm.get_state() != vpn_client::S_DISCONNECTED) {
error = {VPN_EC_ERROR, "Invalid state"};
log_client(this, err, "{}: {}", safe_to_string_view(error.text), this->fsm.get_state());
return error;
}
this->upstream_config = std::move(config);
EndpointConnectorParameters connector_parameters = {
this->parameters.ev_loop,
this,
{&vpn_upstream_handler, this},
{&endpoint_connector_handler, this},
};
std::unique_ptr<ServerUpstream> main_upstream = make_upstream(this->upstream_config.main_protocol);
if (this->upstream_config.fallback.enabled) {
std::unique_ptr<ServerUpstream> fallback_upstream = make_upstream(this->upstream_config.fallback.protocol);
this->endpoint_connector = std::make_unique<FallbackableUpstreamConnector>(connector_parameters,
std::move(main_upstream), std::move(fallback_upstream),
std::chrono::milliseconds(this->upstream_config.fallback.connect_delay_ms));
} else {
this->endpoint_connector =
std::make_unique<SingleUpstreamConnector>(connector_parameters, std::move(main_upstream));
}
error = client_connect(this, timeout_ms);
if (error.code != VPN_EC_NOERROR) {
goto fail;
}
log_client(this, dbg, "Done");
return error;
fail:
disconnect();
if (error.code == 0) {
error.code = VPN_EC_ERROR;
}
if (error.text == nullptr) {
error.text = "Internal error";
}
log_client(this, dbg, "Failed: {} ({})", safe_to_string_view(error.text), error.code);
return error;
}
VpnError VpnClient::listen(
std::unique_ptr<ClientListener> listener, const VpnListenerConfig *config, bool ipv6_available) {
log_client(this, dbg, "...");
this->client_listener = std::move(listener);
this->listener_config = vpn_listener_config_clone(config);
VpnError error = {.code = VPN_EC_ERROR};
this->ipv6_available = ipv6_available;
if (this->listener_config.timeout_ms == 0) {
this->listener_config.timeout_ms = VPN_DEFAULT_TCP_TIMEOUT_MS;
}
switch (this->client_listener->init(this, (ClientHandler){&listener_handler, this})) {
case ClientListener::InitResult::SUCCESS:
break;
case ClientListener::InitResult::ADDR_IN_USE:
error = {VPN_EC_ADDR_IN_USE, "Failed to initialize client listener: address in use"};
goto fail;
case ClientListener::InitResult::FAILURE:
error.text = "Failed to initialize client listener";
goto fail;
}
if (this->tmp_files_base_path.has_value()) {
clean_up_buffer_files(this->tmp_files_base_path->c_str());
}
if (this->listener_config.dns_upstream != nullptr) {
VpnSocksListenerConfig dns_listener_config{};
this->dns_proxy_listener = std::make_unique<SocksListener>(&dns_listener_config);
if (this->dns_proxy_listener->init(this, {&dns_proxy_listener_handler, this})
!= ClientListener::InitResult::SUCCESS) {
error = {VPN_EC_INVALID_SETTINGS, "Failed to initialize DNS proxy listener"};
goto fail;
}
this->dns_proxy = std::make_unique<DnsProxyAccessor>(DnsProxyAccessor::Parameters{
.resolver_address = this->listener_config.dns_upstream,
.socks_listener_address = ((SocksListener &) *this->dns_proxy_listener).get_listen_address(),
.cert_verify_handler = this->parameters.cert_verify_handler,
.ipv6_available = ipv6_available,
});
if (!this->dns_proxy->start(this->upstream_config.timeout)) {
error.text = "Failed to start DNS proxy";
goto fail;
}
}
// got here after connect procedure completion
if (this->fsm.get_state() == vpn_client::S_CONNECTED) {
this->tunnel->on_exclusions_updated();
if (this->dns_proxy != nullptr) {
this->do_dns_upstream_health_check();
}
}
log_client(this, dbg, "Done");
return {};
fail:
this->fsm.perform_transition(vpn_client::E_RUN_PREPARATION_FAIL, nullptr);
log_client(this, err, "Failed: {}", error.text);
return error;
}
void VpnClient::disconnect() {
log_client(this, dbg, "...");
this->fsm.perform_transition(vpn_client::E_DISCONNECT, nullptr);
log_client(this, dbg, "Done");
}
void VpnClient::finalize_disconnect() {
log_client(this, dbg, "...");
if (this->client_listener != nullptr) {
this->client_listener->deinit();
this->client_listener = nullptr;
}
if (this->dns_proxy != nullptr) {
this->dns_proxy->stop();
this->dns_proxy = nullptr;
}
if (this->dns_proxy_listener != nullptr) {
this->dns_proxy_listener->deinit();
this->dns_proxy_listener = nullptr;
}
if (this->endpoint_upstream != nullptr) {
this->endpoint_upstream->deinit();
this->endpoint_upstream = nullptr;
}
if (this->bypass_upstream != nullptr) {
this->bypass_upstream->close_session();
}
if (this->tunnel != nullptr) {
this->tunnel->deinit();
}
this->endpoint_connector.reset();
if (this->tmp_files_base_path.has_value()) {
clean_up_buffer_files(this->tmp_files_base_path->c_str());
}
this->deferred_tasks.clear();
this->fsm.reset();
log_client(this, dbg, "Done");
}
void VpnClient::deinit() {
log_client(this, dbg, "...");
if (this->bypass_upstream != nullptr) {
this->bypass_upstream->deinit();
this->bypass_upstream = nullptr;
}
log_client(this, dbg, "Done");
}
VpnClient::~VpnClient() {
log_client(this, dbg, "...");
vpn_listener_config_destroy(&this->listener_config);
log_client(this, dbg, "Done");
}
void VpnClient::process_client_packets(VpnPackets packets) {
if (!this->client_listener) {
log_client(this, warn, "Packet listener is not initialized, dropping client packet.");
return;
}
this->client_listener->process_client_packets(packets);
}
void VpnClient::update_exclusions(VpnMode mode, std::string_view exclusions) {
log_client(this, dbg, "Mode={}", magic_enum::enum_name(mode));
this->domain_filter.update_exclusions(mode, exclusions);
if (this->fsm.get_state() == vpn_client::S_CONNECTED) {
this->tunnel->on_exclusions_updated();
}
}
void VpnClient::reset_connections(int uid) {
if (this->fsm.get_state() == vpn_client::S_CONNECTED) {
this->tunnel->reset_connections(uid);
}
}
void VpnClient::update_parameters(vpn_client::Parameters parameters) {
this->parameters = parameters;
}
void VpnClient::do_health_check() {
submit_health_check(this, milliseconds(0));
}
void VpnClient::do_dns_upstream_health_check() {
this->tunnel->dns_resolver->resolve(
VDRQ_FOREGROUND, std::string(DNS_PROXY_CHECK_DOMAIN), 1 << dns_utils::RT_A, {dns_resolver_handler, this});
}
VpnConnectionStats VpnClient::get_connection_stats() const {
// should be ensured by `vpn_manager`
assert(this->fsm.get_state() == vpn_client::S_CONNECTED);
return (this->fsm.get_state() == vpn_client::S_CONNECTED) ? this->endpoint_upstream->get_connection_stats()
: VpnConnectionStats{};
}
std::unique_ptr<DataBuffer> VpnClient::make_buffer(uint64_t id) const {
if (this->tmp_files_base_path.has_value()) {
return std::make_unique<MemfileBuffer>(make_buffer_file_path(this->tmp_files_base_path->c_str(), id),
this->conn_memory_buffer_threshold, this->max_conn_buffer_file_size);
}
return std::make_unique<MemoryBuffer>();
}
int VpnClient::next_upstream_id() {
static std::atomic_int next_upstream_id = 0;
return next_upstream_id.fetch_add(1, std::memory_order_relaxed);
}
bool VpnClient::may_send_icmp_request() const {
return this->fsm.get_state() == vpn_client::S_CONNECTED;
}
void VpnClient::handle_sleep() {
log_client(this, dbg, "...");
switch (auto s = (vpn_client::State) this->fsm.get_state()) {
case vpn_client::S_CONNECTING:
case vpn_client::S_CONNECTED:
this->endpoint_upstream->handle_sleep();
break;
case vpn_client::S_DISCONNECTED:
case vpn_client::S_DISCONNECTING:
log_client(this, dbg, "Ignoring due to state: {}", magic_enum::enum_name(s));
break;
}
log_client(this, dbg, "Done");
}
void VpnClient::handle_wake() {
log_client(this, dbg, "...");
switch (auto s = (vpn_client::State) this->fsm.get_state()) {
case vpn_client::S_CONNECTING:
case vpn_client::S_CONNECTED:
this->endpoint_upstream->handle_wake();
break;
case vpn_client::S_DISCONNECTED:
case vpn_client::S_DISCONNECTING:
log_client(this, dbg, "Ignoring due to state: {}", magic_enum::enum_name(s));
break;
}
log_client(this, dbg, "Done");
}
std::optional<VpnConnectAction> VpnClient::finalize_connect_action(
ConnectRequestResult &request_result, bool only_app_initiated_dns) const {
return this->tunnel->finalize_connect_action(request_result, only_app_initiated_dns);
}
// NOLINT(readability-make-member-function-const)
void VpnClient::complete_connect_request(uint64_t id, std::optional<VpnConnectAction> action) {
return this->tunnel->complete_connect_request(id, action);
}
void VpnClient::reject_connect_request(uint64_t id) { // NOLINT(readability-make-member-function-const)
return this->client_listener->complete_connect_request(id, CCR_REJECT);
}
void VpnClient::reset_connection(uint64_t id) { // NOLINT(readability-make-member-function-const)
this->tunnel->reset_connection(id);
}
static bool vpn_client::is_successful(const void *ctx, void *data) {
const VpnError *error = (VpnError *) data;
const VpnClient *vpn = (VpnClient *) ctx;
return (error == nullptr || error->code == VPN_EC_NOERROR)
&& (!vpn->pending_error.has_value() || vpn->pending_error->code == VPN_EC_NOERROR);
}
static void vpn_client::run_connect(void *ctx, void *data) {
auto *vpn = (VpnClient *) ctx;
log_client(vpn, trace, "...");
uint32_t timeout_ms = (data == nullptr) ? 0 : *(uint32_t *) data;
if (VpnError e = vpn->endpoint_connector->connect(timeout_ms); e.code != VPN_EC_NOERROR) {
vpn->pending_error = e;
}
log_client(vpn, trace, "Done");
}
static void vpn_client::schedule_health_check(void *ctx, void *) {
auto *vpn = (VpnClient *) ctx;
log_client(vpn, trace, "...");
submit_health_check(vpn, vpn->upstream_config.endpoint_pinging_period);
log_client(vpn, trace, "Done");
}
static void vpn_client::raise_connected(void *ctx, void *) {
auto *vpn = (VpnClient *) ctx;
log_client(vpn, trace, "...");
vpn->parameters.handler.func(vpn->parameters.handler.arg, EVENT_CONNECTED, nullptr);
log_client(vpn, trace, "Done");
}
static void vpn_client::raise_disconnected(void *ctx, void *) {
auto *vpn = (VpnClient *) ctx;
log_client(vpn, trace, "...");
if (!vpn->pending_error.has_value()) {
vpn->parameters.handler.func(vpn->parameters.handler.arg, EVENT_DISCONNECTED, nullptr);
} else {
vpn->parameters.handler.func(vpn->parameters.handler.arg, EVENT_ERROR, &vpn->pending_error.value());
vpn->pending_error.reset();
}
log_client(vpn, trace, "Done");
}
static void vpn_client::run_disconnect(void *ctx, void *data) {
auto *vpn = (VpnClient *) ctx;
log_client(vpn, trace, "...");
if (vpn->dns_proxy_listener != nullptr) {
// Stop the listener here to complete all the pending DNS requests.
// But do not delete it here, because connections in the tunnel refer to it.
vpn->dns_proxy_listener->deinit();
}
vpn->tunnel->on_before_endpoint_disconnect(vpn->endpoint_upstream.get());
const VpnError *error = (VpnError *) data;
if (!vpn->pending_error.has_value() && error != nullptr && error->code != VPN_EC_NOERROR) {
vpn->pending_error = *error;
}
if (vpn->dns_proxy != nullptr) {
vpn->dns_proxy->stop();
vpn->dns_proxy = nullptr;
}
if (vpn->endpoint_connector != nullptr) {
vpn->endpoint_connector->disconnect();
} else {
vpn->endpoint_upstream->close_session();
}
// @note: this is kind of ad hoc solution just to be sure that tunnel will not try to close
// a server side connection through this upstream after the corresponding client side
// connection is closed
vpn->tunnel->on_after_endpoint_disconnect(vpn->endpoint_upstream.get());
log_client(vpn, trace, "Done");
}
static void vpn_client::submit_disconnect(void *ctx, void *data) {
auto *vpn = (VpnClient *) ctx;
log_client(vpn, trace, "...");
const VpnError *error = (VpnError *) data;
if (!vpn->pending_error.has_value() && error != nullptr && error->code != VPN_EC_NOERROR) {
vpn->pending_error = *error;
}
vpn->deferred_tasks.emplace(ag::submit(vpn->parameters.ev_loop, {vpn, [](void *arg, TaskId task_id) {
auto *vpn = (VpnClient *) arg;
release_deferred_task(vpn, task_id);
vpn->fsm.perform_transition(
E_DEFERRED_DISCONNECT, nullptr);
}}));
log_client(vpn, trace, "Done");
}
} // namespace ag
+52
View File
@@ -0,0 +1,52 @@
#include "vpn/internal/vpn_connection.h"
#include "net/dns_utils.h"
namespace ag {
VpnConnection *VpnConnection::make(uint64_t client_id, TunnelAddressPair addr, int proto) {
VpnConnection *self; // NOLINT(cppcoreguidelines-init-variables)
if (proto == IPPROTO_TCP) {
self = new TcpVpnConnection{};
} else {
assert(proto == IPPROTO_UDP);
self = new UdpVpnConnection{};
}
self->client_id = client_id;
self->addr = std::move(addr);
self->proto = proto;
const sockaddr *dst = (sockaddr *) std::get_if<sockaddr_storage>(&self->addr.dst);
self->flags.set(
CONNF_PLAIN_DNS_CONNECTION, dst != nullptr && dns_utils::PLAIN_DNS_PORT_NUMBER == sockaddr_get_port(dst));
return self;
}
SockAddrTag VpnConnection::make_tag() const {
const sockaddr_storage *dst = std::get_if<sockaddr_storage>(&this->addr.dst);
return {(dst != nullptr) ? *dst : (sockaddr_storage){}, this->app_name};
}
bool UdpVpnConnection::check_dns_queries_completed(PacketDirection dir) {
assert(this->flags.test(CONNF_PLAIN_DNS_CONNECTION));
this->count_dns_message(dir);
return this->are_dns_queries_completed();
}
void UdpVpnConnection::count_dns_message(PacketDirection type) {
switch (type) {
case PD_OUTGOING:
++m_dns_query_counter;
break;
case PD_INCOMING:
--m_dns_query_counter;
break;
}
}
bool UdpVpnConnection::are_dns_queries_completed() const {
return m_dns_query_counter == 0;
}
} // namespace ag
+597
View File
@@ -0,0 +1,597 @@
#include <algorithm>
#include <bitset>
#include <limits>
#include "net/dns_utils.h"
#include "vpn/internal/vpn_client.h"
#include "vpn/internal/vpn_dns_resolver.h"
#define log_resolver(r_, lvl_, fmt_, ...) lvl_##log((r_)->log, fmt_, ##__VA_ARGS__)
#define log_conn(r_, cid_, lvl_, fmt_, ...) lvl_##log((r_)->log, "[L:{}] " fmt_, (cid_), ##__VA_ARGS__)
using namespace std::chrono;
namespace ag {
static const sockaddr_storage BOOTSTRAP_ADDRESSES[] = {
sockaddr_from_str("94.140.14.140:53"),
sockaddr_from_str("94.140.14.141:53"),
sockaddr_from_str("1.1.1.1:53"),
sockaddr_from_str("8.8.8.8:53"),
};
static const sockaddr_storage CUSTOM_SRC_IP = sockaddr_from_str("127.0.0.11");
static constexpr std::string_view CUSTOM_APP_NAME = "__vpn_dns_resolver__";
static constexpr std::string_view RESOLVER_DOMAIN = "dns-unfiltered.adguard.com";
/// Manually resolved `RESOLVER_DOMAIN`
static const sockaddr_storage DEFAULT_RESOLVER_ADDRESS = sockaddr_from_str("94.140.14.140:53");
static constexpr size_t RESOLVE_CAPACITIES[magic_enum::enum_count<VpnDnsResolverQueue>()] = {
[VDRQ_BACKGROUND] = VpnDnsResolver::MAX_PARALLEL_BACKGROUND_RESOLVES,
[VDRQ_FOREGROUND] = std::numeric_limits<size_t>::max(),
};
void VpnDnsResolver::set_ipv6_availability(bool available) {
m_ipv6_available = available;
}
std::optional<VpnDnsResolveId> VpnDnsResolver::resolve(
VpnDnsResolverQueue queue, std::string name, RecordTypeSet record_types, ResultHandler result_handler) {
log_resolver(this, trace, "{}", name);
VpnDnsResolveId id = this->next_id++;
this->queues[queue].emplace(id, Resolve{std::move(name), record_types, result_handler});
if (std::holds_alternative<BootstrapState>(this->state)) {
return id;
}
if (std::holds_alternative<ResolveState>(this->state)) {
if (!this->deferred_resolve_task.has_value()) {
this->deferred_resolve_task =
ag::submit(this->vpn->parameters.ev_loop, {this, [](void *arg, TaskId) {
auto *self = (VpnDnsResolver *) arg;
self->deferred_resolve_task.release();
self->resolve_pending_domains();
}});
}
return id;
}
if (!std::holds_alternative<std::monostate>(this->state)) {
log_resolver(this, warn, "Invalid resolver state: {}", this->state.index());
assert(0);
return std::nullopt;
}
uint64_t connection_ids[std::size(BOOTSTRAP_ADDRESSES)];
std::generate(
std::begin(connection_ids), std::end(connection_ids), [gen = &this->vpn->listener_conn_id_generator]() {
return gen->get();
});
auto &bootstrap = this->state.emplace<BootstrapState>();
bootstrap.connections.reserve(std::size(BOOTSTRAP_ADDRESSES));
std::transform(std::begin(connection_ids), std::end(connection_ids),
std::inserter(bootstrap.connections, bootstrap.connections.begin()), [](uint64_t id) {
return std::make_pair(id, BootstrapState::Connection{});
});
TunnelAddress dst;
sockaddr_storage src = this->make_source_address();
ClientConnectRequest event = {
0,
IPPROTO_UDP,
(sockaddr *) &src,
&dst,
CUSTOM_APP_NAME,
};
for (size_t i = 0; i < std::size(BOOTSTRAP_ADDRESSES); ++i) {
dst = BOOTSTRAP_ADDRESSES[i];
event.id = connection_ids[i];
this->handler.func(this->handler.arg, CLIENT_EVENT_CONNECT_REQUEST, &event);
}
if (std::holds_alternative<BootstrapState>(this->state)) {
bootstrap.timeout_task = ag::schedule(this->vpn->parameters.ev_loop, {this, on_bootstrap_timeout},
duration_cast<milliseconds>(this->vpn->upstream_config.timeout).count());
}
return id;
}
void VpnDnsResolver::cancel(VpnDnsResolveId id) {
for (Queue &q : this->queues) {
if (q.erase(id) != 0) {
return;
}
}
auto *resolve = std::get_if<ResolveState>(&this->state);
if (resolve == nullptr) {
return;
}
auto on_the_wire_it = std::find_if(resolve->queries.begin(), resolve->queries.end(), [id](const auto &i) {
return i.second.id == id;
});
if (on_the_wire_it != resolve->queries.end()) {
resolve->queries.erase(on_the_wire_it);
}
}
void VpnDnsResolver::stop_resolving() {
this->stopping = true;
std::vector<uint64_t> connections;
if (auto *bootstrap = std::get_if<BootstrapState>(&this->state); bootstrap != nullptr) {
connections.reserve(bootstrap->connections.size());
std::transform(bootstrap->connections.begin(), bootstrap->connections.end(), std::back_inserter(connections),
[](const auto &i) {
return i.first;
});
} else if (auto *resolve = std::get_if<ResolveState>(&this->state); resolve != nullptr) {
connections.push_back(resolve->connection_id);
}
for (uint64_t id : connections) {
this->close_connection(id, false, false);
}
this->deinit();
this->stopping = false;
}
void VpnDnsResolver::deinit() {
m_dns_resolver_address.reset();
this->queues = {};
this->state = std::monostate{};
this->accepting_connections.clear();
this->deferred_accept_task.reset();
this->closing_connections.clear();
this->deferred_close_task.reset();
this->deferred_resolve_task.reset();
}
void VpnDnsResolver::complete_connect_request(uint64_t id, ClientConnectResult result) {
if (std::holds_alternative<std::monostate>(this->state)
|| this->closing_connections.end()
!= std::find(this->closing_connections.begin(), this->closing_connections.end(), id)) {
return;
}
if (result != CCR_PASS) {
log_conn(this, id, dbg, "Failed to make connection: {}", magic_enum::enum_name(result));
this->close_connection(id, false, true);
return;
}
this->accepting_connections.push_back(id);
if (!this->deferred_accept_task.has_value()) {
this->deferred_accept_task =
ag::submit(this->vpn->parameters.ev_loop, {this, [](void *arg, TaskId) {
auto *self = (VpnDnsResolver *) arg;
self->deferred_accept_task.release();
std::vector<uint64_t> connections;
connections.swap(self->accepting_connections);
for (uint64_t id : connections) {
self->accept_pending_connection(id);
}
}});
}
}
void VpnDnsResolver::accept_pending_connection(uint64_t id) {
if (auto *bootstrap = std::get_if<BootstrapState>(&this->state); bootstrap != nullptr) {
auto it = bootstrap->connections.find(id);
if (it == bootstrap->connections.end()) {
log_conn(this, id, dbg, "Not found among bootstrap connections");
this->close_connection(id, false, false);
assert(0);
return;
}
this->handler.func(this->handler.arg, CLIENT_EVENT_CONNECTION_ACCEPTED, &id);
if (!std::holds_alternative<BootstrapState>(this->state)
|| bootstrap->connections.end() == (it = bootstrap->connections.find(id))) {
return;
}
BootstrapState::Connection &conn = it->second;
conn.queries = this->send_request(id, RESOLVER_DOMAIN, 1 << dns_utils::RT_A | 1 << dns_utils::RT_AAAA);
if (std::all_of(conn.queries.begin(), conn.queries.end(), [](const auto &i) {
return !i.has_value();
})) {
this->close_connection(id, true, false);
}
return;
}
this->handler.func(this->handler.arg, CLIENT_EVENT_CONNECTION_ACCEPTED, &id);
auto *resolve = std::get_if<ResolveState>(&this->state);
if (resolve == nullptr) {
return;
}
if (resolve->is_open) {
log_conn(this, resolve->connection_id, dbg, "Resolving connection is already open");
this->close_connection(id, false, false);
assert(0);
return;
}
if (resolve->connection_id != id) {
log_conn(this, resolve->connection_id, dbg, "Unexpected resolving connection ID: {}", resolve->connection_id,
id);
this->close_connection(id, false, false);
assert(0);
return;
}
resolve->is_open = true;
this->resolve_pending_domains();
}
void VpnDnsResolver::close_connection(uint64_t id, bool graceful, bool async) {
if (auto it = std::find(this->accepting_connections.begin(), this->accepting_connections.end(), id);
it != this->accepting_connections.end()) {
this->accepting_connections.erase(it);
}
if (async) {
this->closing_connections.push_back(id);
if (!this->deferred_close_task.has_value()) {
this->deferred_close_task =
ag::submit(this->vpn->parameters.ev_loop, {this, [](void *arg, TaskId) {
auto *self = (VpnDnsResolver *) arg;
self->deferred_close_task.release();
std::vector<uint64_t> connections;
connections.swap(self->closing_connections);
for (uint64_t id : connections) {
self->close_connection(id, true, false);
}
}});
}
return;
}
if (auto it = std::find(this->closing_connections.begin(), this->closing_connections.end(), id);
it != this->closing_connections.end()) {
this->closing_connections.erase(it);
}
if (auto *bootstrap = std::get_if<BootstrapState>(&this->state); bootstrap != nullptr) {
bootstrap->connections.erase(id);
if (!bootstrap->connections.empty()) {
goto raise_event;
}
if (!m_dns_resolver_address.has_value()) {
log_resolver(this, dbg, "Failed to bootstrap the resolver, falling back to the default address: {}",
sockaddr_to_str((sockaddr *) &DEFAULT_RESOLVER_ADDRESS));
m_dns_resolver_address = DEFAULT_RESOLVER_ADDRESS;
}
this->state.emplace<ResolveState>();
assert(!this->deferred_resolve_task.has_value());
this->deferred_resolve_task =
ag::submit(this->vpn->parameters.ev_loop, {this, [](void *arg, TaskId) {
auto *self = (VpnDnsResolver *) arg;
self->deferred_resolve_task.release();
self->resolve_pending_domains();
}});
} else if (auto *resolve = std::get_if<ResolveState>(&this->state);
resolve != nullptr && resolve->connection_id == id) {
log_resolver(this, dbg, "Resolve connection has been closed");
if (!this->stopping) {
while (!resolve->queries.empty()) {
ResolveState::Query q = resolve->queries.begin()->second;
resolve->queries.erase(resolve->queries.begin());
raise_result(q.result_handler, q.id, VpnDnsResolverFailure{q.record_type});
}
for (Queue &queue : this->queues) {
while (!queue.empty()) {
auto [entry_id, entry] = std::move(*queue.begin());
queue.erase(queue.begin());
for (size_t i = 0; i < entry.record_types.size(); ++i) {
if (entry.record_types.test(i)) {
raise_result(entry.handler, entry_id, VpnDnsResolverFailure{dns_utils::RecordType(i)});
}
}
}
}
}
this->state = std::monostate{};
}
raise_event:
this->handler.func(this->handler.arg, CLIENT_EVENT_CONNECTION_CLOSED, &id);
}
ssize_t VpnDnsResolver::send(uint64_t id, const uint8_t *data, size_t length) {
log_conn(this, id, dbg, "{}", length);
if (std::holds_alternative<std::monostate>(this->state)) {
log_conn(this, id, dbg, "Invalid state: idle");
return -1;
}
dns_utils::DecodeResult r = dns_utils::decode_packet({data, length});
if (const auto *e = std::get_if<dns_utils::Error>(&r); e != nullptr) {
log_conn(this, id, dbg, "Failed to parse reply: {}", e->description);
return -1;
}
if (auto *bootstrap = std::get_if<BootstrapState>(&this->state); bootstrap != nullptr) {
auto it = bootstrap->connections.find(id);
if (bootstrap->connections.end() == it) {
log_conn(this, id, dbg, "Not found");
return -1;
}
uint16_t reply_id; // NOLINT(cppcoreguidelines-init-variables)
BootstrapState::Connection &conn = it->second;
if (const auto *inapplicable_reply = std::get_if<dns_utils::InapplicablePacket>(&r);
inapplicable_reply != nullptr) {
log_conn(this, id, trace, "Packet holds inapplicable reply");
reply_id = inapplicable_reply->id;
} else {
const auto &reply = std::get<dns_utils::DecodedReply>(r);
reply_id = reply.id;
if (!reply.addresses.empty()) {
m_dns_resolver_address =
sockaddr_from_raw(reply.addresses[0].ip.data(), reply.addresses[0].ip.size(), htons(53));
log_conn(this, id, dbg, "Got resolver address: {}",
sockaddr_to_str((sockaddr *) &m_dns_resolver_address.value()));
} else {
log_conn(this, id, trace, "Dropping reply without any address");
}
}
if (reply_id == conn.queries[0]) {
conn.queries[0].reset();
} else {
assert(reply_id == conn.queries[1]);
conn.queries[1].reset();
}
if (m_dns_resolver_address.has_value()) {
uint64_t bootstrap_connections[bootstrap->connections.size()];
std::transform(bootstrap->connections.begin(), bootstrap->connections.end(), bootstrap_connections,
[](const auto &i) -> uint64_t {
return i.first;
});
for (uint64_t bc_id : bootstrap_connections) { // NOLINT(clang-analyzer-core.uninitialized.Assign)
this->close_connection(bc_id, true, false);
}
} else if (std::all_of(conn.queries.begin(), conn.queries.end(), [](const auto &i) {
return !i.has_value();
})) {
this->close_connection(id, true, false);
}
return (ssize_t) length;
}
auto &resolve = std::get<ResolveState>(this->state);
if (resolve.connection_id != id) {
log_conn(this, id, dbg, "Wrong connection ID");
assert(0);
return -1;
}
VpnDnsResolverResult result = VpnDnsResolverFailure{};
uint16_t reply_id; // NOLINT(cppcoreguidelines-init-variables)
if (const auto *inapplicable_packet = std::get_if<dns_utils::InapplicablePacket>(&r);
inapplicable_packet != nullptr) {
log_conn(this, id, trace, "Packet holds inapplicable packet");
reply_id = inapplicable_packet->id;
} else if (const auto *request = std::get_if<dns_utils::DecodedRequest>(&r); request != nullptr) {
log_conn(this, id, trace, "Packet holds DNS request");
reply_id = request->id;
} else {
const auto &reply = std::get<dns_utils::DecodedReply>(r);
reply_id = reply.id;
if (!reply.addresses.empty()) {
result = VpnDnsResolverSuccess{
.addr = sockaddr_from_raw(reply.addresses[0].ip.data(), reply.addresses[0].ip.size(), 0)};
} else {
log_conn(this, id, dbg, "Resolved address list is empty");
}
// @note: resolved addresses are passed to filter via the DNS sniffer in the tunnel
}
if (auto it = resolve.queries.find(reply_id); it != resolve.queries.end()) {
if (auto *failure = std::get_if<VpnDnsResolverFailure>(&result); failure != nullptr) {
failure->record_type = it->second.record_type;
}
ResolveState::Query q = it->second;
resolve.queries.erase(it);
raise_result(q.result_handler, q.id, result);
}
this->resolve_pending_domains();
return (ssize_t) length;
}
void VpnDnsResolver::consume(uint64_t, size_t) {
}
TcpFlowCtrlInfo VpnDnsResolver::flow_control_info(uint64_t id) {
if (std::holds_alternative<std::monostate>(this->state)) {
return {};
}
if (auto *bootstrap = std::get_if<BootstrapState>(&this->state); bootstrap != nullptr) {
return (bootstrap->connections.count(id) != 0) ? TcpFlowCtrlInfo{UDP_MAX_DATAGRAM_SIZE, UDP_MAX_DATAGRAM_SIZE}
: TcpFlowCtrlInfo{};
}
auto &resolve = std::get<ResolveState>(this->state);
return (resolve.connection_id == id) ? TcpFlowCtrlInfo{UDP_MAX_DATAGRAM_SIZE, UDP_MAX_DATAGRAM_SIZE}
: TcpFlowCtrlInfo{};
}
void VpnDnsResolver::turn_read(uint64_t, bool) {
}
int VpnDnsResolver::process_client_packets(VpnPackets) {
assert(0);
return -1;
}
std::optional<std::pair<uint16_t, std::vector<uint8_t>>> VpnDnsResolver::make_request(
bool is_aaaa, std::string_view name) const {
dns_utils::EncodeResult r = dns_utils::encode_request({is_aaaa ? dns_utils::RT_AAAA : dns_utils::RT_A, name});
if (const auto *e = std::get_if<dns_utils::Error>(&r); e != nullptr) {
log_resolver(this, dbg, "Failed to encode packet: {}", e->description);
return std::nullopt;
}
auto &req = std::get<dns_utils::EncodedRequest>(r);
return {{req.id, std::move(req.data)}};
}
std::optional<uint16_t> VpnDnsResolver::send_request(bool is_aaaa, uint64_t conn_id, std::string_view name) {
auto req = this->make_request(is_aaaa, name);
if (!req.has_value()) {
return std::nullopt;
}
auto &[query_id, data] = req.value();
ClientRead event = {conn_id, data.data(), data.size()};
this->handler.func(this->handler.arg, CLIENT_EVENT_READ, &event);
if (event.result != (int) data.size()) {
log_conn(this, conn_id, dbg, "Failed to send request: {}", event.result);
return std::nullopt;
}
return query_id;
}
std::array<std::optional<uint16_t>, 2> VpnDnsResolver::send_request(
uint64_t conn_id, std::string_view name, RecordTypeSet record_types) {
return {
record_types.test(dns_utils::RT_A) ? this->send_request(false, conn_id, name) : std::nullopt,
(m_ipv6_available && record_types.test(dns_utils::RT_AAAA)) ? this->send_request(true, conn_id, name)
: std::nullopt,
};
}
void VpnDnsResolver::resolve_pending_domains() {
if (!std::holds_alternative<ResolveState>(this->state)) {
log_resolver(this, dbg, "Invalid state: {}", this->state.index());
assert(0);
return;
}
if (std::all_of(this->queues.begin(), this->queues.end(), [](const Queue &q) {
return q.empty();
})) {
// nothing to do
return;
}
auto *resolve = std::get_if<ResolveState>(&this->state);
resolve->timeout_task = ag::schedule(this->vpn->parameters.ev_loop, {this, on_resolve_timeout},
duration_cast<milliseconds>(this->vpn->upstream_config.timeout).count());
if (!resolve->is_open) {
resolve->connection_id = this->vpn->listener_conn_id_generator.get();
sockaddr_storage src = this->make_source_address();
TunnelAddress dst = m_dns_resolver_address.value();
ClientConnectRequest event = {
resolve->connection_id,
IPPROTO_UDP,
(sockaddr *) &src,
&dst,
CUSTOM_APP_NAME,
};
this->handler.func(this->handler.arg, CLIENT_EVENT_CONNECT_REQUEST, &event);
return;
}
for (VpnDnsResolverQueue q : magic_enum::enum_values<VpnDnsResolverQueue>()) {
this->resolve_queue(q);
}
}
void VpnDnsResolver::resolve_queue(VpnDnsResolverQueue queue_type) {
auto &resolve_state = std::get<ResolveState>(this->state);
Queue &queue = this->queues[queue_type];
while (resolve_state.queries.size() < RESOLVE_CAPACITIES[queue_type] && !queue.empty()) {
auto [entry_id, entry] = std::move(*queue.begin());
queue.erase(queue.begin());
auto ids = this->send_request(resolve_state.connection_id, entry.name, entry.record_types);
static_assert(ids.size() == decltype(entry.record_types){}.size());
for (size_t i = 0; i < ids.size(); ++i) {
const auto &id = ids[i];
if (id.has_value()) {
resolve_state.queries.emplace(id.value(),
ResolveState::Query{
.id = entry_id,
.record_type = dns_utils::RecordType(i),
.result_handler = entry.handler,
});
} else if (entry.record_types.test(i)) {
raise_result(entry.handler, entry_id, VpnDnsResolverFailure{dns_utils::RecordType(i)});
}
}
}
}
sockaddr_storage VpnDnsResolver::make_source_address() {
sockaddr_storage addr = CUSTOM_SRC_IP;
sockaddr_set_port((sockaddr *) &addr, this->next_connection_port++);
return addr;
}
void VpnDnsResolver::raise_result(ResultHandler h, VpnDnsResolveId id, VpnDnsResolverResult result) {
if (h.func != nullptr) {
h.func(h.arg, id, result);
}
}
void VpnDnsResolver::on_bootstrap_timeout(void *arg, TaskId) {
auto *self = (VpnDnsResolver *) arg;
log_resolver(self, dbg, "...");
auto *bootstrap = std::get_if<BootstrapState>(&self->state);
if (bootstrap == nullptr) {
log_resolver(self, dbg, "Invalid state: {}", self->state.index());
assert(0);
return;
}
bootstrap->timeout_task.release();
uint64_t bootstrap_connections[bootstrap->connections.size()];
std::transform(bootstrap->connections.begin(), bootstrap->connections.end(), bootstrap_connections,
[](const auto &i) -> uint64_t {
return i.first;
});
for (uint64_t bc_id : bootstrap_connections) { // NOLINT(clang-analyzer-core.uninitialized.Assign)
self->close_connection(bc_id, true, false);
}
}
void VpnDnsResolver::on_resolve_timeout(void *arg, TaskId) {
auto *self = (VpnDnsResolver *) arg;
log_resolver(self, dbg, "...");
auto *resolve = std::get_if<ResolveState>(&self->state);
if (resolve == nullptr) {
log_resolver(self, dbg, "Invalid state: {}", self->state.index());
assert(0);
return;
}
resolve->timeout_task.release();
self->close_connection(resolve->connection_id, true, false);
}
} // namespace ag
+597
View File
@@ -0,0 +1,597 @@
#include <algorithm>
#include "vpn/event_loop.h"
#include "vpn/utils.h"
#include "vpn_fsm.h"
#include "vpn_manager.h"
using namespace std::chrono;
using namespace ag::vpn_fsm;
namespace ag {
static bool need_to_ping(const void *ctx, void *data);
static bool is_fatal_error(const void *ctx, void *data);
static bool need_to_ping_on_recovery(const void *ctx, void *data);
static bool fall_into_recovery(const void *ctx, void *data);
static bool no_connect_attempts(const void *ctx, void *data);
static bool last_active_endpoint(const void *ctx, void *data);
static bool network_loss_suspected(const void *ctx, void *data);
static void run_ping(void *ctx, void *data);
static void connect_client(void *ctx, void *data);
static void complete_connect(void *ctx, void *data);
static void retry_connect(void *ctx, void *data);
static void prepare_for_recovery(void *ctx, void *data);
static void reconnect_client(void *ctx, void *data);
static void finalize_recovery(void *ctx, void *data);
static void do_disconnect(void *ctx, void *data);
static void do_health_check(void *ctx, void *data);
static void start_listening(void *ctx, void *data);
static void on_wrong_connect_state(void *ctx, void *data);
static void on_wrong_listen_state(void *ctx, void *data);
static void on_network_loss(void *ctx, void *data);
static void abandon_endpoint(void *ctx, void *data);
static void raise_state(void *ctx, void *data);
static bool can_complete(const void *ctx, void *data);
static bool is_kill_switch_on(const void *ctx, void *data);
static bool should_postpone(const void *ctx, void *data);
static void complete_request(void *ctx, void *data);
static void postpone_request(void *ctx, void *data);
static void reject_request(void *ctx, void *data);
static void bypass_until_connected(void *ctx, void *data);
// clang-format off
static constexpr FsmTransitionEntry TRANSITION_TABLE[] = {
{VPN_SS_DISCONNECTED, CE_DO_CONNECT, need_to_ping, run_ping, VPN_SS_CONNECTING, raise_state},
{VPN_SS_DISCONNECTED, CE_DO_CONNECT, Fsm::OTHERWISE, connect_client, VPN_SS_CONNECTING, raise_state},
{VPN_SS_DISCONNECTED, CE_CLIENT_DISCONNECTED, Fsm::ANYWAY, Fsm::DO_NOTHING, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_DISCONNECTED, CE_SHUTDOWN, Fsm::ANYWAY, do_disconnect, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_DISCONNECTED, CE_START_LISTENING, Fsm::ANYWAY, on_wrong_listen_state, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_CONNECTING, CE_RETRY_CONNECT, need_to_ping, run_ping, VPN_SS_CONNECTING, Fsm::DO_NOTHING},
{VPN_SS_CONNECTING, CE_RETRY_CONNECT, Fsm::OTHERWISE, connect_client, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_CONNECTING, CE_PING_READY, Fsm::ANYWAY, connect_client, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_CONNECTING, CE_PING_FAIL, fall_into_recovery, prepare_for_recovery, VPN_SS_WAITING_RECOVERY, raise_state},
{VPN_SS_CONNECTING, CE_PING_FAIL, no_connect_attempts, complete_connect, VPN_SS_DISCONNECTED, raise_state},
{VPN_SS_CONNECTING, CE_PING_FAIL, last_active_endpoint, complete_connect, VPN_SS_DISCONNECTED, raise_state},
{VPN_SS_CONNECTING, CE_PING_FAIL, Fsm::OTHERWISE, retry_connect, VPN_SS_CONNECTING, Fsm::DO_NOTHING},
{VPN_SS_CONNECTING, CE_CLIENT_READY, Fsm::ANYWAY, complete_connect, VPN_SS_CONNECTED, raise_state},
{VPN_SS_CONNECTING, CE_CLIENT_DISCONNECTED, is_fatal_error, complete_connect, VPN_SS_DISCONNECTED, raise_state},
{VPN_SS_CONNECTING, CE_CLIENT_DISCONNECTED, fall_into_recovery, prepare_for_recovery, VPN_SS_WAITING_RECOVERY, raise_state},
{VPN_SS_CONNECTING, CE_CLIENT_DISCONNECTED, no_connect_attempts, complete_connect, VPN_SS_DISCONNECTED, raise_state},
{VPN_SS_CONNECTING, CE_CLIENT_DISCONNECTED, last_active_endpoint, complete_connect, VPN_SS_DISCONNECTED, raise_state},
{VPN_SS_CONNECTING, CE_CLIENT_DISCONNECTED, Fsm::OTHERWISE, retry_connect, VPN_SS_CONNECTING, Fsm::DO_NOTHING},
{VPN_SS_CONNECTED, CE_NETWORK_CHANGE, network_loss_suspected, on_network_loss, VPN_SS_RECOVERING, raise_state},
{VPN_SS_CONNECTED, CE_NETWORK_CHANGE, Fsm::OTHERWISE, do_health_check, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_CONNECTED, CE_ABANDON_ENDPOINT, Fsm::ANYWAY, abandon_endpoint, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_WAITING_RECOVERY, CE_NETWORK_CHANGE, network_loss_suspected, on_network_loss, VPN_SS_RECOVERING, raise_state},
{VPN_SS_WAITING_RECOVERY, CE_NETWORK_CHANGE, need_to_ping_on_recovery, run_ping, VPN_SS_RECOVERING, raise_state},
{VPN_SS_WAITING_RECOVERY, CE_NETWORK_CHANGE, Fsm::OTHERWISE, connect_client, VPN_SS_RECOVERING, raise_state},
{VPN_SS_WAITING_RECOVERY, CE_DO_RECOVERY, need_to_ping_on_recovery, run_ping, VPN_SS_RECOVERING, raise_state},
{VPN_SS_WAITING_RECOVERY, CE_DO_RECOVERY, Fsm::OTHERWISE, connect_client, VPN_SS_RECOVERING, raise_state},
{VPN_SS_WAITING_RECOVERY, CE_CLIENT_DISCONNECTED, is_fatal_error, do_disconnect, VPN_SS_DISCONNECTED, raise_state},
{VPN_SS_WAITING_RECOVERY, CE_CLIENT_DISCONNECTED, Fsm::OTHERWISE, do_disconnect, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_RECOVERING, CE_NETWORK_CHANGE, network_loss_suspected, on_network_loss, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_RECOVERING, CE_PING_READY, Fsm::ANYWAY, reconnect_client, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{VPN_SS_RECOVERING, CE_PING_FAIL, Fsm::ANYWAY, prepare_for_recovery, VPN_SS_WAITING_RECOVERY, raise_state},
{VPN_SS_RECOVERING, CE_CLIENT_READY, Fsm::ANYWAY, finalize_recovery, VPN_SS_CONNECTED, raise_state},
{Fsm::ANY_SOURCE_STATE, CE_CLIENT_DISCONNECTED, is_fatal_error, do_disconnect, VPN_SS_DISCONNECTED, raise_state},
{Fsm::ANY_SOURCE_STATE, CE_CLIENT_DISCONNECTED, Fsm::OTHERWISE, prepare_for_recovery, VPN_SS_WAITING_RECOVERY, raise_state},
{Fsm::ANY_SOURCE_STATE, CE_SHUTDOWN, Fsm::ANYWAY, do_disconnect, VPN_SS_DISCONNECTED, raise_state},
{Fsm::ANY_SOURCE_STATE, CE_DO_CONNECT, Fsm::ANYWAY, on_wrong_connect_state, VPN_SS_DISCONNECTED, raise_state},
{Fsm::ANY_SOURCE_STATE, CE_START_LISTENING, Fsm::ANYWAY, start_listening, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{Fsm::ANY_SOURCE_STATE, CE_COMPLETE_REQUEST, can_complete, complete_request, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{Fsm::ANY_SOURCE_STATE, CE_COMPLETE_REQUEST, should_postpone, postpone_request, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{Fsm::ANY_SOURCE_STATE, CE_COMPLETE_REQUEST, is_kill_switch_on, reject_request, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
{Fsm::ANY_SOURCE_STATE, CE_COMPLETE_REQUEST, Fsm::OTHERWISE, bypass_until_connected, Fsm::SAME_TARGET_STATE, Fsm::DO_NOTHING},
};
// clang-format on
FsmTransitionTable vpn_fsm::get_transition_table() {
return {std::begin(TRANSITION_TABLE), std::end(TRANSITION_TABLE)};
}
static void postponement_window_timer_cb(evutil_socket_t, short, void *arg);
static void initiate_recovery(Vpn *vpn) {
milliseconds now = duration_cast<milliseconds>(steady_clock::now().time_since_epoch());
uint32_t elapsed = 0;
if (vpn->recovery.start_ts != milliseconds(0)) {
elapsed = std::max((int) (now - vpn->recovery.attempt_start_ts).count(), 0);
} else {
vpn->recovery.start_ts = now;
vpn->postponement_window_timer.reset(
evtimer_new(vpn_event_loop_get_base(vpn->ev_loop.get()), postponement_window_timer_cb, vpn));
timeval tv = ms_to_timeval(VPN_DEFAULT_POSTPONEMENT_WINDOW_MS);
evtimer_add(vpn->postponement_window_timer.get(), &tv);
}
// try to recover immediately if a previous attempt has taken the whole period
uint32_t time_to_next = 0;
if (vpn->recovery.attempt_interval_ms >= elapsed) {
time_to_next = vpn->recovery.attempt_interval_ms - elapsed;
}
log_vpn(vpn, dbg, "Time to next recovery: {}ms", time_to_next);
vpn->submit(
[vpn]() {
log_vpn(vpn, dbg, "Recovering session...");
vpn->recovery.attempt_start_ts = duration_cast<milliseconds>(steady_clock::now().time_since_epoch());
vpn->fsm.perform_transition(vpn_fsm::CE_DO_RECOVERY, nullptr);
},
time_to_next);
vpn->recovery.attempt_interval_ms *= vpn->upstream_config.recovery.backoff_rate;
milliseconds next_attempt_ts = now + milliseconds(time_to_next);
if ((next_attempt_ts - vpn->recovery.start_ts).count() >= vpn->upstream_config.recovery.location_update_period_ms) {
log_vpn(vpn, dbg, "Resetting recovery state due to the recovery took too long");
vpn->recovery = {};
vpn->register_selected_endpoint_fail();
}
vpn->recovery.to_next_ms = time_to_next;
}
static void pinger_handler(void *arg, const LocationsPingerResult *result) {
if (result == nullptr) {
// ignore ping finished event
return;
}
Vpn *vpn = (Vpn *) arg;
const VpnEndpoint *endpoint = nullptr;
if (result->endpoint != nullptr) {
for (size_t i = 0; i < vpn->upstream_config.location.endpoints.size; ++i) {
endpoint = &vpn->upstream_config.location.endpoints.data[i];
if (vpn_endpoint_equals(result->endpoint, endpoint)) {
break;
}
}
}
vpn->selected_endpoint_info = {endpoint};
if (endpoint != nullptr) {
log_vpn(vpn, dbg, "Using endpoint '{}' {} (ping={}ms)", endpoint->name,
sockaddr_to_str((sockaddr *) &endpoint->address), result->ping_ms);
vpn->fsm.perform_transition(vpn_fsm::CE_PING_READY, nullptr);
} else {
VpnError error = {VPN_EC_LOCATION_UNAVAILABLE, "None of the endpoints were pinged successfully"};
log_vpn(vpn, warn, "{}", error.text);
vpn->fsm.perform_transition(vpn_fsm::CE_PING_FAIL, &error);
}
}
static bool are_there_active_endpoints(const Vpn *vpn) {
return vpn->inactive_endpoints.size() < vpn->upstream_config.location.endpoints.size;
}
template <typename It, typename Pred, typename Free>
static It remove_if(It begin, It end, Pred &&pred, Free &&free) {
begin = std::find_if(begin, end, std::forward<Pred>(pred));
if (begin != end) {
It it = begin;
while (++it != end) {
if (!std::forward<Pred>(pred)(*it)) {
std::forward<Free>(free)(*begin);
*begin = *it;
*it = {};
++begin;
}
}
}
return begin;
}
static VpnLocation filter_out_inactive_endpoints(const Vpn *vpn, const VpnLocation &src) {
VpnLocation dst;
vpn_location_clone(&dst, &src);
assert(src.endpoints.size >= vpn->inactive_endpoints.size());
size_t available_endpoints_num = src.endpoints.size - vpn->inactive_endpoints.size();
if (available_endpoints_num > 0) {
for (const VpnEndpoint *inactive_endpoint : vpn->inactive_endpoints) {
VpnEndpoint *end = dst.endpoints.data + dst.endpoints.size;
VpnEndpoint *begin = remove_if(
dst.endpoints.data, end,
[inactive_endpoint](const VpnEndpoint &endpoint) {
return vpn_endpoint_equals(&endpoint, inactive_endpoint);
},
[](VpnEndpoint &endpoint) {
vpn_endpoint_destroy(&endpoint);
});
dst.endpoints.size -= (end - begin);
for (; begin != end; ++begin) {
vpn_endpoint_destroy(begin);
}
}
} else {
log_vpn(vpn, dbg, "All endpoints are marked inactive, re-ping them all in case some were resurrected");
}
return dst;
}
static bool is_fatal_error_code(int code) {
return code == VPN_EC_AUTH_REQUIRED;
}
static void run_client_connect(Vpn *vpn, uint32_t timeout_ms = 0) {
VpnError error = vpn->client.connect(vpn->make_client_upstream_config(), timeout_ms);
if (error.code == VPN_EC_NOERROR) {
vpn->client_state = vpn_manager::CLIS_CONNECTING;
vpn->pending_error.reset();
} else {
log_vpn(vpn, dbg, "Failed to connect: {} ({})", safe_to_string_view(error.text), error.code);
vpn->pending_error = error;
vpn->submit([vpn] {
vpn->fsm.perform_transition(CE_CLIENT_DISCONNECTED, nullptr);
});
}
}
static bool need_to_ping(const void *ctx, void *) {
const Vpn *vpn = (Vpn *) ctx;
const auto *endpoints = &vpn->upstream_config.location.endpoints;
// special case: a single endpoint is specified without resolved address
return !(endpoints->size == 1 && endpoints->data[0].address.ss_family == AF_UNSPEC);
}
static bool need_to_ping_on_recovery(const void *ctx, void *data) {
if (!need_to_ping(ctx, data)) {
return false;
}
const Vpn *vpn = (Vpn *) ctx;
if (vpn->selected_endpoint_info.endpoint == nullptr) {
// we lost endpoint for some reason, need to refresh the location
return true;
}
milliseconds now = duration_cast<milliseconds>(steady_clock::now().time_since_epoch());
return (now - vpn->recovery.start_ts).count() >= vpn->upstream_config.recovery.location_update_period_ms;
}
static bool fall_into_recovery(const void *ctx, void *data) {
const auto *vpn = (Vpn *) ctx;
return std::holds_alternative<vpn_manager::ConnectFallIntoRecovery>(vpn->connect_retry_info);
}
static bool no_connect_attempts(const void *ctx, void *) {
const auto *vpn = (Vpn *) ctx;
const auto *several_attempts = std::get_if<vpn_manager::ConnectSeveralAttempts>(&vpn->connect_retry_info);
return several_attempts != nullptr && several_attempts->attempts_left == 0;
}
static bool last_active_endpoint(const void *ctx, void *) {
const Vpn *vpn = (Vpn *) ctx;
return vpn->upstream_config.location.endpoints.size <= vpn->inactive_endpoints.size() + 1;
}
static bool network_loss_suspected(const void *, void *data) {
bool network_loss_suspected = *(bool *) data;
return network_loss_suspected;
}
static bool is_fatal_error(const void *ctx, void *data) {
const VpnError *error = (VpnError *) data;
const Vpn *vpn = (Vpn *) ctx;
return (error != nullptr && is_fatal_error_code(error->code))
|| is_fatal_error_code(vpn->pending_error.value_or(VpnError{}).code);
}
static void run_ping(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
vpn->stop_pinging();
VpnLocation filtered_location = filter_out_inactive_endpoints(vpn, vpn->upstream_config.location);
LocationsPingerInfo pinger_info = {vpn->upstream_config.location_ping_timeout_ms, {&filtered_location, 1}, 1};
vpn->pinger.reset(locations_pinger_start(&pinger_info, {pinger_handler, vpn}, vpn->ev_loop.get()));
vpn_location_destroy(&filtered_location);
vpn->pending_error.reset();
log_vpn(vpn, trace, "Done");
}
static void connect_client(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
run_client_connect(vpn);
log_vpn(vpn, trace, "Done");
}
static void complete_connect(void *ctx, void *data) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
const VpnError *error = (VpnError *) data;
if (!vpn->pending_error.has_value() && error != nullptr && error->code != VPN_EC_NOERROR) {
vpn->disconnect();
vpn->pending_error = *error;
}
vpn->recovery = {};
vpn->inactive_endpoints.clear();
log_vpn(vpn, trace, "Done");
}
static void retry_connect(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
// mark current endpoint inactive without retries like in connected case
vpn->mark_selected_endpoint_inactive();
if (auto *several_attempts = std::get_if<vpn_manager::ConnectSeveralAttempts>(&vpn->connect_retry_info)) {
several_attempts->attempts_left -= 1;
} else {
assert(0);
}
vpn->disconnect();
vpn->submit([vpn] {
vpn->fsm.perform_transition(CE_RETRY_CONNECT, nullptr);
});
log_vpn(vpn, trace, "Done");
}
static void prepare_for_recovery(void *ctx, void *data) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
vpn->disconnect();
initiate_recovery(vpn);
const VpnError *error = (VpnError *) data;
if (!are_there_active_endpoints(vpn)) {
vpn->pending_error = {VPN_EC_LOCATION_UNAVAILABLE, "Got errors on each endpoint of location"};
log_vpn(vpn, dbg, "No active endpoints left");
} else if (!vpn->pending_error.has_value() && error != nullptr && error->code != VPN_EC_NOERROR) {
vpn->pending_error = *error;
}
log_vpn(vpn, trace, "Done");
}
static void reconnect_client(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
vpn->disconnect_client();
uint32_t timeout_ms = std::min(vpn->recovery.attempt_interval_ms, vpn->upstream_config.timeout_ms);
run_client_connect(vpn, timeout_ms);
log_vpn(vpn, trace, "Done");
}
static void finalize_recovery(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
vpn->recovery = {};
vpn->stop_pinging();
vpn->inactive_endpoints.clear();
vpn->selected_endpoint_info.recoveries_num = 0;
vpn->postponement_window_timer.reset();
vpn->complete_postponed_requests();
vpn->reset_bypassed_connections();
log_vpn(vpn, trace, "Done");
}
static void do_disconnect(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
vpn->disconnect();
log_vpn(vpn, trace, "Done");
}
static void do_health_check(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
switch (vpn->client_state) {
case vpn_manager::CLIS_DISCONNECTED:
case vpn_manager::CLIS_CONNECTING:
log_vpn(vpn, dbg, "Ignoring due to current client state: {}", magic_enum::enum_name(vpn->client_state));
break;
case vpn_manager::CLIS_CONNECTED:
vpn->client.do_health_check();
break;
}
log_vpn(vpn, trace, "Done");
}
static void start_listening(void *ctx, void *data) {
auto *vpn = (Vpn *) ctx;
auto *args = (StartListeningArgs *) data;
log_vpn(vpn, info, "...");
const VpnLocation &location = vpn->upstream_config.location;
bool ipv6_available = std::any_of(location.endpoints.data, location.endpoints.data + location.endpoints.size,
[](const VpnEndpoint &e) -> bool {
return e.address.ss_family == AF_INET6;
});
VpnError error = vpn->client.listen(std::move(args->listener), args->config, ipv6_available);
if (error.code != VPN_EC_NOERROR) {
log_vpn(vpn, err, "Client run failed: {} ({})", safe_to_string_view(error.text), error.code);
vpn->submit([vpn, error] {
vpn->pending_error = error;
vpn->fsm.perform_transition(CE_SHUTDOWN, nullptr);
});
} else {
log_vpn(vpn, info, "Client has been successfully prepared to run");
}
}
static void on_wrong_connect_state(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
vpn->disconnect();
vpn->pending_error = {VPN_EC_INVALID_STATE, "Invalid state for connecting"};
log_vpn(vpn, err, "{}: {}", vpn->pending_error->text,
magic_enum::enum_name((VpnSessionState) vpn->fsm.get_state()));
}
static void on_wrong_listen_state(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, err, "Invalid state for listenning: {}",
magic_enum::enum_name((VpnSessionState) vpn->fsm.get_state()));
}
static void on_network_loss(void *ctx, void *data) {
Vpn *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
vpn->disconnect_client();
bool network_loss_suspected = *(bool *) data;
if (network_loss_suspected) {
vpn->inactive_endpoints.clear();
}
run_ping(ctx, nullptr);
log_vpn(vpn, trace, "Done");
}
static void raise_state(void *ctx, void *) {
Vpn *vpn = (Vpn *) ctx;
auto state = (VpnSessionState) vpn->fsm.get_state();
VpnStateChangedEvent event = {vpn->upstream_config.location.id, state};
log_vpn(vpn, info, "{}", magic_enum::enum_name((VpnSessionState) vpn->fsm.get_state()));
switch (state) {
case VPN_SS_WAITING_RECOVERY:
event.waiting_recovery_info = {vpn->pending_error.value_or(VpnError{}), vpn->recovery.to_next_ms};
break;
case VPN_SS_CONNECTED:
assert(vpn->selected_endpoint_info.endpoint != nullptr);
event.connected_info = {vpn->selected_endpoint_info.endpoint, vpn->client.endpoint_upstream->get_protocol()};
break;
case VPN_SS_DISCONNECTED:
case VPN_SS_CONNECTING:
case VPN_SS_RECOVERING:
event.error = vpn->pending_error.value_or(VpnError{});
break;
}
vpn->handler.func(vpn->handler.arg, VPN_EVENT_STATE_CHANGED, (void *) &event);
}
void abandon_endpoint(void *ctx, void *) {
auto *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
vpn->mark_selected_endpoint_inactive();
vpn->disconnect_client();
log_vpn(vpn, trace, "Done");
}
static bool can_complete(const void *ctx, void *data) {
auto *result = (ConnectRequestResult *) data;
if (result->action == VPN_CA_FORCE_BYPASS) {
return true;
}
const auto *vpn = (Vpn *) ctx;
int state = vpn->fsm.get_state();
return state == VPN_SS_CONNECTED || state == VPN_SS_CONNECTING || state == VPN_SS_DISCONNECTED;
}
static bool is_kill_switch_on(const void *ctx, void *) {
const auto *vpn = (Vpn *) ctx;
return vpn->client.kill_switch_on;
}
static void complete_request(void *ctx, void *data) {
auto *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
auto *result = (ConnectRequestResult *) data;
vpn->client.complete_connect_request(result->id, result->action);
log_vpn(vpn, trace, "Done");
}
static void reject_request(void *ctx, void *data) {
auto *vpn = (Vpn *) ctx;
auto *result = (ConnectRequestResult *) data;
log_vpn(vpn, dbg, "Rejecting connection [L:{}]: not ready to route through endpoint", result->id);
vpn->client.reject_connect_request(result->id);
log_vpn(vpn, trace, "Done");
}
static void bypass_until_connected(void *ctx, void *data) {
auto *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
auto *result = (ConnectRequestResult *) data;
vpn->bypassed_connection_ids.emplace_back(result->id);
vpn->client.complete_connect_request(result->id, VPN_CA_FORCE_BYPASS);
log_vpn(vpn, trace, "Done");
}
static bool should_postpone(const void *ctx, void *) {
auto *vpn = (Vpn *) ctx;
return vpn->postponement_window_timer != nullptr;
}
static void postpone_request(void *ctx, void *data) {
auto *vpn = (Vpn *) ctx;
log_vpn(vpn, trace, "...");
auto *request = (ConnectRequestResult *) data;
vpn->postponed_requests.emplace_back(std::move(*request));
log_vpn(vpn, trace, "Done");
}
static void postponement_window_timer_cb(int, short, void *arg) {
auto *vpn = (Vpn *) arg;
log_vpn(vpn, trace, "...");
vpn->postponement_window_timer.reset();
for (auto &request : vpn->postponed_requests) {
if (vpn->client.kill_switch_on) {
vpn->client.reject_connect_request(request.id);
} else {
vpn->client.complete_connect_request(request.id, VPN_CA_FORCE_BYPASS);
vpn->bypassed_connection_ids.emplace_back(request.id);
}
}
vpn->postponed_requests.clear();
log_vpn(vpn, trace, "Done");
}
} // namespace ag
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <utility>
#include "vpn/fsm.h"
#include "vpn/vpn.h"
namespace ag {
namespace vpn_fsm {
enum ConnectEvent {
CE_DO_CONNECT, // start the connection procedure
CE_RETRY_CONNECT, // start the next attempt of the connection procedure
CE_PING_READY, // got locations pinger result
CE_PING_FAIL, // location pinging failed
CE_CLIENT_READY, // http client successfully connected
CE_CLIENT_DISCONNECTED, // http client disconnected for some reason
CE_DO_RECOVERY, // need to run recovery
CE_SHUTDOWN, // shutting down
CE_NETWORK_CHANGE, // network has been changed
CE_START_LISTENING, // start listenning for connections from client
CE_ABANDON_ENDPOINT, // mark current endpoint inactive and do recovery
CE_COMPLETE_REQUEST, // complete connection request
};
FsmTransitionTable get_transition_table();
} // namespace vpn_fsm
} // namespace ag
+792
View File
@@ -0,0 +1,792 @@
#include <atomic>
#include <condition_variable>
#include "socks_listener.h"
#include "tun_device_listener.h"
#include "vpn/internal/domain_filter.h"
#include "vpn/internal/utils.h"
#include "vpn/utils.h"
#include "vpn/vpn.h"
#include "vpn_fsm.h"
#include "vpn_manager.h"
#undef gettid
#include "upstream/upstream_utils.h"
/** This dummy variable is preventing the linker from excluding modules from dll */
int g_exp_init_adguard_vpncore [[maybe_unused]] = 0;
namespace ag {
using namespace std::chrono;
static std::atomic_int g_next_id = 0;
static VpnPackets vpn_packets_clone(VpnPackets packets);
static void vpn_packets_destroy(VpnPackets *packets);
static int ssl_verify_callback(const char *host_name, const sockaddr *host_ip, X509_STORE_CTX *ctx, void *arg);
static void client_handler(void *arg, vpn_client::Event what, void *data);
static void shutdown_cb(Vpn *vpn);
static const char *check_address(const sockaddr_storage *addr);
static constexpr auto STATE_NAMES = make_enum_names_array<VpnSessionState>();
static constexpr auto EVENT_NAMES = make_enum_names_array<vpn_fsm::ConnectEvent>();
static FsmParameters make_fsm_params(Vpn *vpn) {
return {VPN_SS_DISCONNECTED, vpn_fsm::get_transition_table(), vpn, vpn_manager::LOG_NAME, STATE_NAMES.data(),
EVENT_NAMES.data()};
}
Vpn::Vpn()
: fsm(make_fsm_params(this))
, client(make_client_parameters())
, id(g_next_id++) {
}
Vpn::~Vpn() {
vpn_upstream_config_destroy(&this->upstream_config);
}
void Vpn::update_upstream_config(const VpnUpstreamConfig *config) {
vpn_upstream_config_destroy(&this->upstream_config);
this->upstream_config = vpn_upstream_config_clone(config);
if (!this->upstream_config.fallback.enabled && this->upstream_config.protocol.type == VPN_UP_HTTP3) {
log_vpn(this, info, "Setting forcibly HTTP/2 as fallback protocol");
this->upstream_config.fallback.enabled = true;
this->upstream_config.fallback.protocol.type = VPN_UP_HTTP2;
}
if (this->upstream_config.location_ping_timeout_ms == 0) {
this->upstream_config.location_ping_timeout_ms = DEFAULT_PING_TIMEOUT_MS;
}
if (this->upstream_config.timeout_ms == 0) {
this->upstream_config.timeout_ms = VPN_DEFAULT_ENDPOINT_UPSTREAM_TIMEOUT_MS;
}
if (this->upstream_config.endpoint_pinging_period_ms == 0) {
this->upstream_config.endpoint_pinging_period_ms = VPN_DEFAULT_ENDPOINT_PINGING_PERIOD_MS;
}
if (this->upstream_config.recovery.backoff_rate < 1) {
this->upstream_config.recovery.backoff_rate = VPN_DEFAULT_RECOVERY_BACKOFF_RATE;
}
if (this->upstream_config.recovery.location_update_period_ms == 0) {
this->upstream_config.recovery.location_update_period_ms = VPN_DEFAULT_RECOVERY_LOCATION_UPDATE_PERIOD_MS;
}
if (VpnUpstreamFallbackConfig &fallback = this->upstream_config.fallback; fallback.enabled) {
if (fallback.connect_delay_ms == 0) {
fallback.connect_delay_ms = VPN_DEFAULT_FALLBACK_CONNECT_DELAY_MS;
}
}
}
vpn_client::Parameters Vpn::make_client_parameters() const {
return {
this->ev_loop.get(),
this->dns_base,
this->network_manager.get(),
{client_handler, (void *) this},
{ssl_verify_callback, (void *) this},
};
}
vpn_client::EndpointConnectionConfig Vpn::make_client_upstream_config() const {
return {
this->upstream_config.protocol,
this->upstream_config.fallback,
this->get_endpoint(),
milliseconds(this->upstream_config.timeout_ms),
this->upstream_config.username,
this->upstream_config.password,
milliseconds(this->upstream_config.endpoint_pinging_period_ms),
};
}
void Vpn::disconnect_client() {
switch (this->client_state) {
case vpn_manager::CLIS_DISCONNECTED:
// do nothing
break;
case vpn_manager::CLIS_CONNECTED:
case vpn_manager::CLIS_CONNECTING:
this->client.disconnect();
this->client_state = vpn_manager::CLIS_DISCONNECTED;
break;
}
}
void Vpn::stop_pinging() {
if (this->pinger != nullptr) {
locations_pinger_stop(this->pinger.get());
this->pinger.reset();
}
}
void Vpn::disconnect() {
this->stop_pinging();
this->disconnect_client();
}
bool Vpn::run_event_loop() {
log_vpn(this, info, "Starting event loop...");
if (this->ev_loop == nullptr) {
this->ev_loop.reset(vpn_event_loop_create());
if (this->ev_loop == nullptr) {
log_vpn(this, err, "Failed to create event loop");
return false;
}
this->dns_base =
dns_manager_create_base(this->network_manager->dns, vpn_event_loop_get_base(this->ev_loop.get()));
}
this->executor_thread = std::thread([this]() {
int ret = vpn_event_loop_run(this->ev_loop.get());
if (ret != 0) {
log_vpn(this, err, "Event loop run returned {}, shutting down", ret);
this->pending_error = {.code = VPN_EC_EVENT_LOOP_FAILURE, .text = "Event loop run error"};
shutdown_cb(this);
}
});
if (!vpn_event_loop_dispatch_sync(this->ev_loop.get(), nullptr, nullptr)) {
log_vpn(this, err, "Event loop did not start");
assert(0);
vpn_event_loop_stop(this->ev_loop.get());
if (this->executor_thread.joinable()) {
this->executor_thread.join();
}
return false;
}
log_vpn(this, info, "Event loop has been started");
return true;
}
void Vpn::submit(std::function<void()> &&func, uint32_t ms) {
VpnEventLoopTask task = {
new std::function(std::move(func)),
[](void *arg, TaskId task_id) {
std::function<void()> *func = (std::function<void()> *) arg;
(*func)();
},
[](void *arg) {
delete (std::function<void()> *) arg;
},
};
if (ms == 0) {
vpn_event_loop_submit(this->ev_loop.get(), task);
} else {
vpn_event_loop_schedule(this->ev_loop.get(), task, ms);
}
}
const VpnEndpoint *Vpn::get_endpoint() const {
if (this->selected_endpoint_info.endpoint != nullptr) {
return this->selected_endpoint_info.endpoint;
}
// find first active
for (size_t i = 0; i < this->upstream_config.location.endpoints.size; ++i) {
const VpnEndpoint *endpoint = &this->upstream_config.location.endpoints.data[i];
auto it = std::find(this->inactive_endpoints.begin(), this->inactive_endpoints.end(), endpoint);
if (it == this->inactive_endpoints.end()) {
return endpoint;
}
}
// if none, just return the first one
return &this->upstream_config.location.endpoints.data[0];
}
void Vpn::register_selected_endpoint_fail() {
if (this->selected_endpoint_info.endpoint == nullptr) {
return;
}
++this->selected_endpoint_info.recoveries_num;
if (this->selected_endpoint_info.recoveries_num >= vpn_manager::INACTIVE_ENDPOINT_RECOVERIES_NUM) {
this->mark_selected_endpoint_inactive();
}
}
void Vpn::mark_selected_endpoint_inactive() {
if (this->selected_endpoint_info.endpoint == nullptr) {
return;
}
const VpnEndpoint *endpoint = this->selected_endpoint_info.endpoint;
this->selected_endpoint_info = {};
auto it = std::find(this->inactive_endpoints.begin(), this->inactive_endpoints.end(), endpoint);
if (it != this->inactive_endpoints.end()) {
return;
}
this->inactive_endpoints.push_back(endpoint);
log_vpn(this, info, "Adding endpoint to the list of inactive: '{}' {} (list size={})", endpoint->name,
sockaddr_to_str((sockaddr *) &endpoint->address), this->inactive_endpoints.size());
}
void Vpn::complete_postponed_requests() {
log_vpn(this, trace, "...");
for (auto &request : this->postponed_requests) {
this->client.complete_connect_request(request.id, request.action);
}
this->postponed_requests.clear();
log_vpn(this, trace, "Done");
}
void Vpn::reset_bypassed_connections() {
log_vpn(this, trace, "...");
for (uint64_t id : this->bypassed_connection_ids) {
this->client.reset_connection(id);
}
this->bypassed_connection_ids.clear();
log_vpn(this, trace, "Done");
}
Vpn *vpn_open(const VpnSettings *settings) {
DeclPtr<Vpn, &vpn_close> vpn{new Vpn{}};
log_vpn(vpn, info, "...");
if (vpn->ev_loop == nullptr) {
log_vpn(vpn, err, "Failed to create event loop");
return nullptr;
}
vpn->handler = settings->handler;
VpnError error = vpn->client.init(settings);
if (error.code == VPN_EC_NOERROR) {
log_vpn(vpn, info, "Done");
} else {
log_vpn(vpn, err, "Failed: {} ({})", safe_to_string_view(error.text), error.code);
vpn.reset();
}
return vpn.release();
}
static VpnError validate_upstream_config(const Vpn *vpn, const VpnUpstreamConfig *config) {
if (config->location.endpoints.size == 0) {
return {VPN_EC_INVALID_SETTINGS, "At least one endpoint must be specified"};
}
if (config->username == nullptr || config->password == nullptr) {
return {VPN_EC_INVALID_SETTINGS, "Both username and password must be specified"};
}
for (size_t i = 0; i < config->location.endpoints.size; ++i) {
const VpnEndpoint *i_ep = &config->location.endpoints.data[i];
if (i_ep->name == nullptr) {
log_vpn(vpn, err, "Invalid endpoint's address='{}'", sockaddr_to_str((sockaddr *) &i_ep->address));
return {VPN_EC_INVALID_SETTINGS, "Names must be specified for each endpoint"};
}
if (config->location.endpoints.size > 1 && i_ep->address.ss_family == AF_UNSPEC) {
log_vpn(vpn, err, "Invalid endpoint's name='{}'", i_ep->name);
return {VPN_EC_INVALID_SETTINGS, "In case of multiple endpoints addresses must be specified for each one"};
}
if (const char *error_message = check_address(&i_ep->address); error_message != nullptr) {
log_vpn(vpn, err, "Invalid endpoint address {} ({}): {}", sockaddr_to_str((sockaddr *) &i_ep->address),
i_ep->name, error_message);
return {VPN_EC_INVALID_SETTINGS, "Invalid endpoint address"};
}
}
return {};
}
VpnError vpn_connect(Vpn *vpn, const VpnConnectParameters *parameters) {
log_vpn(vpn, info, "...");
std::unique_lock l(vpn->stop_guard);
VpnError error = validate_upstream_config(vpn, &parameters->upstream_config);
if (error.code != VPN_EC_NOERROR) {
log_vpn(vpn, err, "Upstream configuration validation failed: {}", error.text);
return error;
}
if (vpn->executor_thread.joinable()) {
error = {VPN_EC_ERROR, "VPN client worker is already running"};
log_vpn(vpn, err, "{}", error.text);
return error;
}
if (!vpn->run_event_loop()) {
error = {VPN_EC_EVENT_LOOP_FAILURE, "Failed to start event loop for operation"};
log_vpn(vpn, err, "{}", error.text);
return error;
}
vpn_manager::ConnectRetryInfo retry_info;
switch (parameters->retry_info.policy) {
case VPN_CRP_SEVERAL_ATTEMPTS:
retry_info = vpn_manager::ConnectSeveralAttempts{
.attempts_left = (parameters->retry_info.attempts_num <= 0)
? VPN_DEFAULT_CONNECT_ATTEMPTS_NUM
: size_t(parameters->retry_info.attempts_num),
};
break;
case VPN_CRP_FALL_INTO_RECOVERY:
retry_info = vpn_manager::ConnectFallIntoRecovery{};
break;
}
vpn->submit([vpn, cfg = vpn_upstream_config_clone(&parameters->upstream_config),
retry_info = std::move(retry_info)]() mutable {
vpn->client.update_parameters(vpn->make_client_parameters());
vpn->update_upstream_config(&cfg);
vpn->connect_retry_info = std::move(retry_info);
if (auto *several_attempts = std::get_if<vpn_manager::ConnectSeveralAttempts>(&vpn->connect_retry_info);
several_attempts != nullptr) {
several_attempts->attempts_left -= 1;
}
vpn->fsm.perform_transition(vpn_fsm::CE_DO_CONNECT, nullptr);
vpn_upstream_config_destroy(&cfg);
});
log_vpn(vpn, info, "Done");
return error;
}
void vpn_force_reconnect(Vpn *vpn) {
log_vpn(vpn, info, "...");
std::unique_lock l(vpn->stop_guard);
vpn->submit([vpn]() {
vpn->fsm.perform_transition(vpn_fsm::CE_DO_RECOVERY, nullptr);
});
log_vpn(vpn, info, "Done");
}
VpnError vpn_listen(Vpn *vpn, VpnListener *listener_, const VpnListenerConfig *config) {
log_vpn(vpn, info, "...");
if (!listener_) {
return VpnError{.code = VPN_EC_INVALID_SETTINGS, .text = "Listener is NULL"};
}
if (!config) {
return VpnError{.code = VPN_EC_INVALID_SETTINGS, .text = "Listener config is NULL"};
}
std::unique_ptr<ClientListener> listener((ClientListener *) listener_);
std::unique_lock l(vpn->stop_guard);
if (!vpn->executor_thread.joinable()) {
VpnError error = {VPN_EC_ERROR, "VPN client worker is not running"};
log_vpn(vpn, err, "{}", error.text);
return error;
}
ag::dispatch_sync(vpn->ev_loop.get(), [&]() mutable {
StartListeningArgs args{.listener = std::move(listener), .config = config};
vpn->fsm.perform_transition(vpn_fsm::CE_START_LISTENING, &args);
});
log_vpn(vpn, info, "Done");
return {};
}
void vpn_stop(Vpn *vpn) {
log_vpn(vpn, info, "...");
std::unique_lock l(vpn->stop_guard);
if (vpn->ev_loop != nullptr) {
vpn->submit([vpn]() {
shutdown_cb(vpn);
});
log_vpn(vpn, info, "Stopping event loop...");
vpn_event_loop_stop(vpn->ev_loop.get());
log_vpn(vpn, info, "Event loop has been stopped");
}
if (vpn->executor_thread.joinable()) {
vpn->executor_thread.join();
}
vpn->fsm.reset();
vpn->pending_error.reset();
vpn->client_state = vpn_manager::CLIS_DISCONNECTED;
vpn->client.finalize_disconnect();
dns_manager_delete_base(vpn->network_manager->dns, load_and_null(vpn->dns_base));
socket_manager_complete_all(vpn->network_manager->socket);
vpn->stop_pinging();
vpn->postponement_window_timer.reset();
vpn->ev_loop.reset();
vpn->recovery = {};
vpn->selected_endpoint_info = {};
vpn->inactive_endpoints.clear();
vpn->update_exclusions_task.release(); // The event loop is stopped, no need to reset()
vpn->postponed_requests.clear();
vpn->bypassed_connection_ids.clear();
log_vpn(vpn, info, "Done");
}
void vpn_close(Vpn *vpn) {
if (vpn == NULL) {
return;
}
log_vpn(vpn, info, "...");
vpn->client.deinit();
log_vpn(vpn, info, "Done");
delete vpn;
}
VpnListenerConfig vpn_get_listener_config(const Vpn *vpn) {
std::unique_lock l(vpn->stop_guard);
return vpn_listener_config_clone(&vpn->client.listener_config);
}
VpnListenerConfig vpn_listener_config_clone(const VpnListenerConfig *config) {
return VpnListenerConfig{
.timeout_ms = config->timeout_ms,
.dns_upstream = safe_strdup(config->dns_upstream),
};
}
void vpn_listener_config_destroy(VpnListenerConfig *config) {
free((char *) config->dns_upstream);
*config = {};
}
static void vpn_complete_connect_request_task(Vpn *vpn, ConnectRequestResult result) {
log_vpn(vpn, dbg, "{}", result.to_string());
result.action = vpn->client.finalize_connect_action(
result, vpn->client.kill_switch_on && vpn->fsm.get_state() != VPN_SS_CONNECTED);
vpn->fsm.perform_transition(vpn_fsm::CE_COMPLETE_REQUEST, &result);
}
void vpn_complete_connect_request(Vpn *vpn, const VpnConnectionInfo *info) {
std::unique_lock l(vpn->stop_guard);
ConnectRequestResult result = {
info->id,
info->action,
(info->appname != nullptr) ? info->appname : "",
info->uid,
};
vpn->submit([vpn, result = std::move(result)]() mutable {
vpn_complete_connect_request_task(vpn, std::move(result));
});
}
void vpn_update_exclusions(Vpn *vpn, VpnMode mode, VpnStr exclusions) {
log_vpn(vpn, info, "...");
std::unique_lock l(vpn->stop_guard);
struct Ctx {
Vpn *vpn;
VpnMode mode;
std::string exclusions;
};
auto *ctx = new Ctx{
.vpn = vpn,
.mode = mode,
.exclusions = {exclusions.data, exclusions.size},
};
vpn->update_exclusions_task = ag::submit(vpn->ev_loop.get(),
{
.arg = ctx,
.action =
[](void *arg, TaskId task_id) {
auto *ctx = (Ctx *) arg;
ctx->vpn->client.reset_connections(-1);
ctx->vpn->client.update_exclusions(ctx->mode, ctx->exclusions);
},
.finalize =
[](void *arg) {
delete (Ctx *) arg;
},
});
log_vpn(vpn, info, "Done");
}
void vpn_reset_connections(Vpn *vpn, int uid) {
log_vpn(vpn, info, "UID={}", uid);
std::unique_lock l(vpn->stop_guard);
vpn->submit([vpn, uid]() {
vpn->client.reset_connections(uid);
});
log_vpn(vpn, info, "Done");
}
void vpn_notify_network_change(Vpn *vpn, bool network_loss_suspected) {
log_vpn(vpn, info, "Loss suspected={}", network_loss_suspected);
std::unique_lock l(vpn->stop_guard);
vpn->submit([vpn, network_loss_suspected]() {
vpn->fsm.perform_transition(vpn_fsm::CE_NETWORK_CHANGE, (void *) &network_loss_suspected);
});
log_vpn(vpn, info, "Done");
}
void vpn_request_endpoint_connection_stats(Vpn *vpn) {
log_vpn(vpn, dbg, "...");
std::unique_lock l(vpn->stop_guard);
if (!vpn_event_loop_is_active(vpn->ev_loop.get())) {
VpnEndpointConnectionStatsEvent event = {{VPN_EC_ERROR, "Event loop is stopped"}};
vpn->handler.func(vpn->handler.arg, VPN_EVENT_ENDPOINT_CONNECTION_STATS, &event);
}
vpn->submit([vpn]() {
VpnEndpointConnectionStatsEvent event = {};
VpnSessionState state = (VpnSessionState) vpn->fsm.get_state();
if (state == VPN_SS_CONNECTED) {
event.protocol = vpn->client.endpoint_upstream->get_protocol();
event.stats = vpn->client.get_connection_stats();
} else {
event.error = {VPN_EC_INVALID_STATE, "Invalid state"};
log_vpn(vpn, dbg, "Can't get endpoint connection statistics in unsuitable state: {}",
magic_enum::enum_name(state));
}
vpn->handler.func(vpn->handler.arg, VPN_EVENT_ENDPOINT_CONNECTION_STATS, &event);
});
log_vpn(vpn, dbg, "Done");
}
void vpn_notify_sleep(Vpn *vpn, void (*completion_handler)(void *), void *arg) {
log_vpn(vpn, dbg, "...");
std::unique_lock l(vpn->stop_guard);
struct vpn_notify_sleep_ctx_t {
void (*handler)(void *);
void *handler_arg;
Vpn *vpn;
};
// Completion handler MUST be called even if the task doesn't run
vpn_event_loop_submit(vpn->ev_loop.get(),
{
.arg = new vpn_notify_sleep_ctx_t{.handler = completion_handler, .handler_arg = arg, .vpn = vpn},
.action =
[](void *arg, TaskId id) {
auto *ctx = (vpn_notify_sleep_ctx_t *) arg;
ctx->vpn->client.handle_sleep();
},
.finalize =
[](void *arg) {
auto *ctx = (vpn_notify_sleep_ctx_t *) arg;
assert(ctx->handler);
ctx->handler(ctx->handler_arg);
delete ctx;
},
});
log_vpn(vpn, dbg, "Done");
}
void vpn_notify_wake(Vpn *vpn) {
log_vpn(vpn, dbg, "...");
std::unique_lock l(vpn->stop_guard);
vpn->submit([vpn] {
vpn->client.handle_wake();
});
log_vpn(vpn, dbg, "Done");
}
VpnExclusionValidationStatus vpn_validate_exclusion(const char *text) {
switch (DomainFilter::validate_entry(text)) {
case DFVS_OK:
return VPN_EVS_OK;
case DFVS_MALFORMED:
return VPN_EVS_MALFORMED;
}
return VPN_EVS_OK;
}
VpnDnsUpstreamValidationStatus vpn_validate_dns_upstream(const char *address) {
ag::UpstreamOptions opts = {
.address = address,
.bootstrap = {"1.1.1.1"},
};
std::optional err = ag::test_upstream(opts, true, nullptr, true);
if (err.has_value()) {
ag::Logger log{__func__};
dbglog(log, "{}", *err);
return VPN_DUVS_MALFORMED;
}
return VPN_DUVS_OK;
}
void vpn_process_client_packets(Vpn *vpn, VpnPackets packets) {
std::unique_lock l(vpn->stop_guard);
vpn->submit([vpn, packets = vpn_packets_clone(packets)]() mutable {
vpn->client.process_client_packets(packets);
vpn_packets_destroy(&packets);
});
}
static VpnPackets vpn_packets_clone(VpnPackets packets) {
VpnPackets copy = {};
copy.data = new evbuffer_iovec[packets.size];
for (size_t i = 0; i < packets.size; ++i) {
evbuffer_iovec packet = {malloc(packets.data[i].iov_len)};
if (packet.iov_base != nullptr) {
memcpy(packet.iov_base, packets.data[i].iov_base, packets.data[i].iov_len);
packet.iov_len = packets.data[i].iov_len;
copy.data[copy.size++] = packet;
}
}
return copy;
}
static void vpn_packets_destroy(VpnPackets *packets) {
if (packets) {
for (size_t i = 0; i < packets->size; ++i) {
free(packets->data[i].iov_base);
}
delete[] packets->data;
}
}
static int ssl_verify_callback(const char *host_name, const sockaddr *host_ip, X509_STORE_CTX *ctx, void *arg) {
const Vpn *vpn = (Vpn *) arg;
X509 *cert = X509_STORE_CTX_get0_cert(ctx);
if ((host_name != nullptr || (host_ip != nullptr && host_ip->sa_family != AF_UNSPEC))
&& (host_name == nullptr || !tls_verify_cert_host_name(cert, host_name))
&& (host_ip == nullptr || host_ip->sa_family == AF_UNSPEC
|| !tls_verify_cert_ip(cert, sockaddr_to_str(host_ip).c_str()))) {
log_vpn(vpn, err, "Server host name or IP doesn't match certificate");
return 0;
}
int result = 0;
VpnVerifyCertificateEvent event = {ctx, 0};
vpn->handler.func(vpn->handler.arg, VPN_EVENT_VERIFY_CERTIFICATE, &event);
if (event.result == 0) {
result = 1;
} else {
log_vpn(vpn, err, "Failed to verify certificate");
}
return result;
}
static void client_handler(void *arg, vpn_client::Event what, void *data) {
Vpn *vpn = (Vpn *) arg;
switch (what) {
case vpn_client::EVENT_PROTECT_SOCKET:
vpn->handler.func(vpn->handler.arg, VPN_EVENT_PROTECT_SOCKET, data);
break;
case vpn_client::EVENT_VERIFY_CERTIFICATE:
vpn->handler.func(vpn->handler.arg, VPN_EVENT_VERIFY_CERTIFICATE, data);
break;
case vpn_client::EVENT_CONNECTED:
vpn->client_state = vpn_manager::CLIS_CONNECTED;
vpn->fsm.perform_transition(vpn_fsm::CE_CLIENT_READY, nullptr);
break;
case vpn_client::EVENT_OUTPUT:
vpn->handler.func(vpn->handler.arg, VPN_EVENT_CLIENT_OUTPUT, data);
break;
case vpn_client::EVENT_CONNECT_REQUEST:
vpn->handler.func(vpn->handler.arg, VPN_EVENT_CONNECT_REQUEST, data);
break;
case vpn_client::EVENT_ERROR:
case vpn_client::EVENT_DISCONNECTED:
vpn->client_state = vpn_manager::CLIS_DISCONNECTED;
vpn->fsm.perform_transition(vpn_fsm::CE_CLIENT_DISCONNECTED, data);
break;
case vpn_client::EVENT_DNS_UPSTREAM_UNAVAILABLE:
vpn->handler.func(vpn->handler.arg, VPN_EVENT_DNS_UPSTREAM_UNAVAILABLE, data);
break;
}
}
static void shutdown_cb(Vpn *vpn) {
vpn->fsm.perform_transition(vpn_fsm::CE_SHUTDOWN, nullptr);
}
static const char *check_address(const sockaddr_storage *addr) {
switch (addr->ss_family) {
case AF_INET: {
const sockaddr_in *v4 = (sockaddr_in *) addr;
return (v4->sin_port > 0) ? nullptr : "Port must be specified";
}
case AF_INET6: {
const sockaddr_in6 *v6 = (sockaddr_in6 *) addr;
return (v6->sin6_port > 0) ? nullptr : "Port must be specified";
}
case AF_UNSPEC:
return nullptr;
default:
return "Unknown family";
}
}
VpnEventLoop *vpn_get_event_loop(Vpn *vpn) {
std::unique_lock l(vpn->stop_guard);
return vpn->ev_loop.get();
}
void vpn_abandon_current_endpoint(Vpn *vpn) {
log_vpn(vpn, info, "...");
std::unique_lock l(vpn->stop_guard);
vpn->submit([vpn]() {
vpn->fsm.perform_transition(vpn_fsm::CE_ABANDON_ENDPOINT, nullptr);
});
log_vpn(vpn, info, "Done");
}
VpnListener *vpn_create_tun_listener(Vpn *, const VpnTunListenerConfig *config) {
return std::make_unique<TunListener>(config).release();
}
VpnListener *vpn_create_socks_listener(Vpn *, const VpnSocksListenerConfig *config) {
return std::make_unique<SocksListener>(config).release();
}
sockaddr_storage vpn_get_socks_listener_address(Vpn *vpn) {
sockaddr_storage ret{};
std::scoped_lock l(vpn->stop_guard);
ag::dispatch_sync(vpn->ev_loop.get(), [&]() mutable {
ret = vpn->client.socks_listener_address;
});
return ret;
}
} // namespace ag
+150
View File
@@ -0,0 +1,150 @@
#pragma once
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>
#include <variant>
#include <vector>
#include <event2/dns.h>
#include "common/logger.h"
#include "net/locations_pinger.h"
#include "net/network_manager.h"
#include "net/tls.h"
#include "net/utils.h"
#include "vpn/event_loop.h"
#include "vpn/fsm.h"
#include "vpn/internal/utils.h"
#include "vpn/internal/vpn_client.h"
#include "vpn/platform.h"
#include "vpn/utils.h"
#include "vpn/vpn.h"
namespace ag {
namespace vpn_manager {
enum ClientConnectionState {
CLIS_DISCONNECTED,
CLIS_CONNECTING,
CLIS_CONNECTED,
};
struct RecoveryInfo {
std::chrono::milliseconds start_ts{0}; // session recovery start timestamp
std::chrono::milliseconds attempt_start_ts{0}; // last recovery attempt start timestamp
uint32_t attempt_interval_ms =
ag::VPN_DEFAULT_INITIAL_RECOVERY_INTERVAL_MS; // last interval between recovery attempts
uint32_t to_next_ms = 0; // left to next attempt
};
struct SelectedEndpointInfo {
const VpnEndpoint *endpoint = nullptr; // pointer to endpoint in `upstream_config.location`
uint32_t recoveries_num = 0; // the number of recovery attempts to the endpoint
};
static constexpr const char *LOG_NAME = "VPNCORE";
// the number of recovery attempts before marking an endpoint inactive
static constexpr size_t INACTIVE_ENDPOINT_RECOVERIES_NUM = 1;
struct ConnectSeveralAttempts {
size_t attempts_left = ag::VPN_DEFAULT_CONNECT_ATTEMPTS_NUM;
};
struct ConnectFallIntoRecovery {};
using ConnectRetryInfo = std::variant<
// VPN_CRP_SEVERAL_ATTEMPTS
ConnectSeveralAttempts,
// VPN_CRP_FALL_INTO_RECOVERY
ConnectFallIntoRecovery>;
} // namespace vpn_manager
struct Vpn {
Vpn(const Vpn &) = delete;
Vpn(Vpn &&) = delete;
Vpn &operator=(const Vpn &) = delete;
Vpn &operator=(Vpn &&) = delete;
Vpn();
~Vpn();
void update_upstream_config(const VpnUpstreamConfig *config);
vpn_client::Parameters make_client_parameters() const;
vpn_client::EndpointConnectionConfig make_client_upstream_config() const;
void disconnect_client();
void stop_pinging();
void disconnect();
bool run_event_loop();
void submit(std::function<void()> &&func, uint32_t ms = 0);
/**
* Get endpoint to connect to
* @return the selected one if some, the first active from the location list otherwise
*/
const VpnEndpoint *get_endpoint() const;
/**
* Increment failures counter and mark the selected endpoint inactive
* if it's reached the threshold
*/
void register_selected_endpoint_fail();
/**
* Mark the selected endpoint inactive unconditionally
*/
void mark_selected_endpoint_inactive();
void complete_postponed_requests();
void reset_bypassed_connections();
Fsm fsm;
std::optional<VpnError> pending_error;
std::thread executor_thread;
DeclPtr<VpnEventLoop, &vpn_event_loop_destroy> ev_loop{vpn_event_loop_create()};
vpn_manager::RecoveryInfo recovery = {};
VpnHandler handler = {};
DeclPtr<VpnNetworkManager, &vpn_network_manager_destroy> network_manager{vpn_network_manager_get()};
evdns_base *dns_base = dns_manager_create_base(this->network_manager->dns,
vpn_event_loop_get_base(this->ev_loop.get())); // DNS base used for resolving hosts in bypassing upstream
VpnUpstreamConfig upstream_config = {};
vpn_manager::SelectedEndpointInfo selected_endpoint_info = {}; // the most suitable endpoint
DeclPtr<LocationsPinger, &locations_pinger_destroy> pinger;
vpn_manager::ClientConnectionState client_state = vpn_manager::CLIS_DISCONNECTED;
VpnClient client;
// An endpoint becomes inactive in case it was disconnected for any reason.
// If all endpoints were marked inactive, the library re-pings them all in case some of them were resurrected.
// This list is reset on successful recovery and on `vpn_stop` call.
std::vector<const VpnEndpoint *> inactive_endpoints; // pointers to endpoints in `upstream_config.location`
vpn_manager::ConnectRetryInfo connect_retry_info;
// Ids of connections bypassed during recovery
std::vector<uint64_t> bypassed_connection_ids;
// Completed connect requests whose processing is postponed until VPN is connected
std::vector<ConnectRequestResult> postponed_requests;
// This timer counts down the time during which connect requests can be postponed.
// It is started when recovery starts and reset when recovery is done.
// If it expires before recovery is done, all postponed connect requests are bypassed.
DeclPtr<event, &event_free> postponement_window_timer;
mutable std::mutex stop_guard;
ag::AutoTaskId update_exclusions_task; // Guarded by stop_guard
ag::Logger log{vpn_manager::LOG_NAME};
int id;
};
struct StartListeningArgs {
std::unique_ptr<ClientListener> listener;
const VpnListenerConfig *config;
};
#define log_vpn(vpn_, lvl_, fmt_, ...) lvl_##log((vpn_)->log, "[{}] " fmt_, (vpn_)->id, ##__VA_ARGS__)
} // namespace ag

Some files were not shown because too many files have changed in this diff Show More