diff --git a/.clang-tidy b/.clang-tidy index 08f92a894..6c2d98e86 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ # clang-tidy configuration for shadowsocks-libev project sources (src/ only). -# Bundled submodules (libcork/, libipset/, libbloom/) are third-party code and +# Bundled sources under third_party/ and src/blake3/ are third-party code and # are excluded by the file regex used in CI: # run-clang-tidy -p build '/src/[^/]+\.c$' # diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24a18db3e..340561992 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,8 +15,6 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Install dependencies (Linux) if: runner.os == 'Linux' @@ -31,7 +29,7 @@ jobs: - name: Build run: | mkdir -p build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Release + cmake .. -DCMAKE_BUILD_TYPE=Release -DSS_DEPENDENCY_MODE=system -DWITH_STATIC=OFF jobs="$(nproc 2>/dev/null || sysctl -n hw.ncpu)" make -j"$jobs" @@ -39,15 +37,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y libpcre2-dev libmbedtls-dev libsodium-dev libev-dev libc-ares-dev sudo apt-get install -y --no-install-recommends \ - build-essential debhelper dpkg-dev fakeroot asciidoc-base xmlto pkg-config + build-essential cmake debhelper dpkg-dev fakeroot asciidoc-base xmlto pkg-config - name: Debian package build test run: bash tests/test_deb_build.sh diff --git a/.github/workflows/portability.yml b/.github/workflows/portability.yml new file mode 100644 index 000000000..04c150706 --- /dev/null +++ b/.github/workflows/portability.yml @@ -0,0 +1,199 @@ +name: portability + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + bundled: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-latest] + minimal: ['OFF', 'ON'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + - name: Build without installed third-party development packages + run: | + cmake -S . -B build -DSS_MINIMAL=${{ matrix.minimal }} + cmake --build build --parallel 4 + ctest --test-dir build -L 'unit|vendor' --output-on-failure --no-tests=error + - name: Real TCP and UDP relay + run: python3 tests/interop.py --self --bin build/bin + - name: SIP003 transport and child cleanup + if: matrix.minimal == 'OFF' + run: python3 tests/interop.py --self --bin build/bin --plugin tests/sip003_fixture.py + - name: Relocated installation and public API consumers + run: | + cmake --install build --prefix "$PWD/install" + mv install relocated + cmake -S tests/consumer -B consumer -DCMAKE_PREFIX_PATH="$PWD/relocated" + cmake --build consumer + ./consumer/consumer_static + ./consumer/consumer_shared + + windows: + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v6 + - uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-ninja + mingw-w64-ucrt-x86_64-python + - name: Build and run native Windows binaries + run: | + cmake -S . -B build -G Ninja + cmake --build build --parallel 4 + ctest --test-dir build -L 'unit|vendor' --output-on-failure --no-tests=error + objdump -p build/bin/ss-server.exe | sed -n '/DLL Name/p' + python tests/interop.py --self --bin build/bin --isolate-windows-runtime + - name: Installed consumers + run: | + cmake --install build --prefix "$PWD/install" + cmake -S tests/consumer -B consumer -G Ninja -DCMAKE_PREFIX_PATH="$PWD/install" + cmake --build consumer + - name: Run installed consumers without toolchain DLLs + shell: pwsh + run: | + $env:PATH = "$env:GITHUB_WORKSPACE\install\bin;$env:SystemRoot\System32;$env:SystemRoot" + & ./consumer/consumer_static.exe + if ($LASTEXITCODE -ne 0) { throw "Static consumer failed: $LASTEXITCODE" } + & ./consumer/consumer_shared.exe + if ($LASTEXITCODE -ne 0) { throw "Shared consumer failed: $LASTEXITCODE" } + + static-macos: + runs-on: macos-latest + env: + CC: clang + steps: + - uses: actions/checkout@v6 + - name: Build Clang static-only installation + run: | + cmake -S . -B build-static -DCMAKE_C_COMPILER=clang -DSS_DEPENDENCY_MODE=bundled -DWITH_STATIC=ON -DSS_BUILD_STATIC_LIBRARY=ON -DSS_BUILD_SHARED_LIBRARY=OFF + cmake --build build-static --parallel 4 + ctest --test-dir build-static -L 'unit|vendor' --output-on-failure --no-tests=error + cmake --install build-static --prefix "$PWD/install-static" + mv install-static relocated-static + cmake -S tests/consumer -B consumer-static -DCMAKE_C_COMPILER=clang -DCMAKE_PREFIX_PATH="$PWD/relocated-static" + cmake --build consumer-static + test ! -e consumer-static/consumer_shared + ./consumer-static/consumer_static + - name: Reject third-party dynamic libraries + run: python3 tests/check_static_runtime.py relocated-static/bin/ss-* consumer-static/consumer_static + - name: Exercise installed static programs + run: | + python3 tests/interop.py --self --bin relocated-static/bin + python3 tests/interop.py --self --bin relocated-static/bin --plugin tests/sip003_fixture.py + - uses: actions/upload-artifact@v4 + with: + name: static-macos-${{ runner.arch }} + path: relocated-static/ + if-no-files-found: error + + static-windows: + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v6 + - uses: msys2/setup-msys2@v2 + with: + msystem: CLANG64 + update: true + install: >- + mingw-w64-clang-x86_64-clang + mingw-w64-clang-x86_64-llvm + mingw-w64-clang-x86_64-cmake + mingw-w64-clang-x86_64-ninja + mingw-w64-clang-x86_64-python + - name: Build Clang static-only installation + run: | + cmake -S . -B build-static -G Ninja -DCMAKE_C_COMPILER=clang -DSS_DEPENDENCY_MODE=bundled -DWITH_STATIC=ON -DSS_BUILD_STATIC_LIBRARY=ON -DSS_BUILD_SHARED_LIBRARY=OFF + cmake --build build-static --parallel 4 + ctest --test-dir build-static -L 'unit|vendor' --output-on-failure --no-tests=error + cmake --install build-static --prefix "$PWD/install-static" + mv install-static relocated-static + cmake -S tests/consumer -B consumer-static -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_PREFIX_PATH="$PWD/relocated-static" + cmake --build consumer-static + test ! -e consumer-static/consumer_shared.exe + - name: Reject third-party DLL imports, including delay imports + run: python tests/check_static_runtime.py relocated-static/bin/ss-*.exe consumer-static/consumer_static.exe + - name: Exercise installed programs without toolchain DLLs + run: python tests/interop.py --self --bin relocated-static/bin --isolate-windows-runtime + - name: Run static consumer with only Windows system directories on PATH + shell: pwsh + run: | + $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot" + & ./consumer-static/consumer_static.exe + if ($LASTEXITCODE -ne 0) { throw "Static consumer failed: $LASTEXITCODE" } + - uses: actions/upload-artifact@v4 + with: + name: static-windows-${{ runner.arch }} + path: relocated-static/ + if-no-files-found: error + + static-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Build and test fully static Clang/musl executables + run: docker build -f docker/static/Dockerfile --target artifacts --output type=local,dest=static-linux . + - uses: actions/upload-artifact@v4 + with: + name: static-linux-${{ runner.arch }} + path: static-linux/ + if-no-files-found: error + + freebsd: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: vmactions/freebsd-vm@v1 + with: + release: '14.3' + usesh: true + prepare: pkg install -y cmake ninja python3 + run: | + cmake -S . -B build -G Ninja + cmake --build build --parallel 4 + ctest --test-dir build -L 'unit|vendor' --output-on-failure --no-tests=error + python3 tests/interop.py --self --bin build/bin + cmake --install build --prefix "$PWD/install" + cmake -S tests/consumer -B consumer -G Ninja -DCMAKE_PREFIX_PATH="$PWD/install" + cmake --build consumer + ./consumer/consumer_static + ./consumer/consumer_shared + + offline-musl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Source archive + run: python3 scripts/source_archive.py release/shadowsocks-libev.tar.gz + - name: Prepare compiler image + run: | + printf 'FROM alpine:3.24\nRUN apk add --no-cache build-base cmake python3 linux-headers\n' > /tmp/ss-toolchain.Dockerfile + docker build -t ss-toolchain -f /tmp/ss-toolchain.Dockerfile /tmp + - name: Build release archive with network disabled + run: | + docker run --rm --network none -v "$PWD/release:/release:ro" ss-toolchain sh -ec ' + mkdir /src + tar xzf /release/shadowsocks-libev.tar.gz -C /src --strip-components=1 + cmake -S /src -B /build -DCMAKE_DISABLE_FIND_PACKAGE_Python3=TRUE + cmake --build /build --parallel 4 + ctest --test-dir /build -L "unit|vendor" --output-on-failure --no-tests=error + python3 /src/tests/interop.py --self --bin /build/bin + ' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 69bbea416..9af7418d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,8 +15,6 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Install dependencies (Linux) if: runner.os == 'Linux' @@ -47,21 +45,19 @@ jobs: - name: Stress test run: | - python3 tests/stress_test.py --bin build/shared/bin/ --size 10 + python3 tests/stress_test.py --bin build/bin/ --size 10 - name: ss-redir transparent proxy test (QEMU) if: runner.os == 'Linux' run: | sudo apt-get install -y qemu-system-x86 - bash tests/test_redir_qemu.sh build/shared/bin/ + bash tests/test_redir_qemu.sh build/bin/ timeout-minutes: 8 asan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Install dependencies run: | @@ -78,7 +74,7 @@ jobs: run: ctest --test-dir build -LE memcheck --output-on-failure --no-tests=error - name: Stress test (ASan + UBSan) - run: python3 tests/stress_test.py --bin build/shared/bin/ --size 10 + run: python3 tests/stress_test.py --bin build/bin/ --size 10 clang-tidy: runs-on: ubuntu-latest @@ -91,8 +87,6 @@ jobs: MAX_WARNINGS: 0 steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Install dependencies run: | @@ -104,7 +98,7 @@ jobs: - name: Run clang-tidy on project sources run: | - run-clang-tidy-18 -quiet -p build '/src/[^/]+\.c$' 2>/dev/null | tee tidy.log || true + run-clang-tidy-18 -quiet -p build "$PWD/src/[^/]+\.c$" 2>/dev/null | tee tidy.log || true grep 'warning:' tidy.log | sort -u > warnings.txt || true count="$(wc -l < warnings.txt)" echo "clang-tidy warnings: $count (max allowed: $MAX_WARNINGS)" @@ -128,8 +122,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Install dependencies run: | @@ -145,7 +137,7 @@ jobs: - name: Run tests run: | ctest --test-dir build -LE memcheck --output-on-failure --no-tests=error - python3 tests/stress_test.py --bin build/shared/bin/ --size 10 + python3 tests/stress_test.py --bin build/bin/ --size 10 - name: Generate coverage report run: | @@ -162,3 +154,19 @@ jobs: with: name: coverage-html path: build/coverage-html/ + + interop: + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + - name: Install dependencies and independent peer + run: brew install mbedtls@3 libsodium libev c-ares pcre2 shadowsocks-rust + - name: Build + run: | + cmake -S . -B build -DSS_DEPENDENCY_MODE=system -DWITH_STATIC=OFF + cmake --build build --parallel + - name: Required interoperability + env: + SS_REQUIRE_INTEROP: '1' + SS_BIN_DIR: build/bin + run: bash tests/test_interop_rust.sh diff --git a/.gitignore b/.gitignore index 385ad90f7..228da5b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,15 @@ doc/*.html # CMake objection obj-* + +# Pinned source archives are part of offline release builds. +!third_party/dist/*.tar.gz +!third_party/dist/*.tar.bz2 + +# Former dependency checkouts retained locally during migration. +/libipset/ +/libbloom/ +/libcork/ + +# Python integration-test bytecode. +__pycache__/ diff --git a/.gitmodules b/.gitmodules index bf4d6cac0..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +0,0 @@ -[submodule "libcork"] - path = libcork - url = https://github.com/shadowsocks/libcork.git - ignore = dirty -[submodule "libipset"] - path = libipset - url = https://github.com/shadowsocks/ipset.git - ignore = dirty -[submodule "libbloom"] - path = libbloom - url = https://github.com/shadowsocks/libbloom.git - ignore = dirty diff --git a/CMakeLists.txt b/CMakeLists.txt index b1217dd59..76188be45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) set(PROJECT_NAME shadowsocks-libev) set(RELEASE_DATE 2026-02-09) @@ -6,9 +6,13 @@ set(PROJECT_VERSION "3.3.6") set(PROJECT_DESC "a lightweight secured socks5 proxy") set(PROJECT_URL "https://shadowsocks.org") set(PROJECT_ISSUES_URL "https://github.com/shadowsocks/shadowsocks-libev") -project(${PROJECT_NAME} VERSION ${PROJECT_VERSION}) +project(${PROJECT_NAME} VERSION ${PROJECT_VERSION} LANGUAGES C) +if(MSVC) + message(FATAL_ERROR "MSVC is not supported yet. Use MinGW-w64 (UCRT64) or the Zig Windows toolchain; see docs/modernization.md.") +endif() include(GNUInstallDirs) +include(CTest) # Compilation database for clang-tidy and other tooling set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -19,63 +23,48 @@ set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS ON) -# Note: -Werror is applied per-target in src/CMakeLists.txt to avoid -# breaking bundled submodules (libcork, libipset, libbloom) -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_GNU_SOURCE") -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -O2 -Wall -Wno-deprecated-declarations -fno-strict-aliasing") - option(ENABLE_SANITIZERS "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF) -if (ENABLE_SANITIZERS) - set(SANITIZER_FLAGS "-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SANITIZER_FLAGS}") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${SANITIZER_FLAGS}") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${SANITIZER_FLAGS}") - message(STATUS "Sanitizers enabled (ASan + UBSan)") -endif () - -option(ENABLE_COVERAGE "Build with coverage instrumentation (gcov)" OFF) -if (ENABLE_COVERAGE) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 --coverage") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --coverage") - message(STATUS "Coverage instrumentation enabled") -endif () +option(ENABLE_COVERAGE "Build with coverage instrumentation" OFF) +option(SS_WARNINGS_AS_ERRORS "Treat project warnings as errors" ON) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake) -set(RUNTIME_SHARED_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/shared/bin) set(CMAKE_MACOSX_RPATH TRUE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -if (NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Debug) +if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build configuration" FORCE) endif () # Detect linux -if (UNIX AND NOT APPLE) +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") set(LINUX TRUE) endif () message(STATUS "Running cmake version ${CMAKE_VERSION}") -option(WITH_STATIC "build with static libraries." ON) -option(WITH_EMBEDDED_SRC "build with embedded libcork, libipset, and libbloom source." ON) +option(WITH_STATIC "Link executable dependencies statically" ON) +option(SS_BUILD_EXECUTABLES "Build command-line programs" ON) +option(SS_BUILD_STATIC_LIBRARY "Build the static embedding library" ON) +option(SS_BUILD_SHARED_LIBRARY "Build the shared embedding library" ON) +option(SS_INSTALL_TOOLS "Install platform shell tools" OFF) option(ENABLE_CONNMARKTOS "Enable saved connmark to IP TOS QoS feature" OFF) option(ENABLE_NFTABLES "Report malicious IP to nftables" OFF) -# When choose to not use embedded libcork, libipset and libbloom, use libs shipped by system -if (NOT WITH_EMBEDDED_SRC) - set(USE_SYSTEM_SHARED_LIB TRUE) -endif () - -# Find dependencies via Find modules -find_package(PCRE2 REQUIRED) -find_package(MbedTLS REQUIRED) -find_package(Sodium REQUIRED) -find_package(Cares REQUIRED) +option(SS_MINIMAL "Disable regex, plugins, manager and legacy stream ciphers" OFF) +option(SS_ENABLE_REGEX "Enable PCRE2 ACL patterns" ON) +option(SS_ENABLE_PLUGINS "Enable SIP003 and obfsproxy subprocesses" ON) +option(SS_ENABLE_LEGACY "Enable legacy stream ciphers" ON) +if(SS_MINIMAL) + set(SS_ENABLE_REGEX OFF) + set(SS_ENABLE_PLUGINS OFF) + set(SS_ENABLE_LEGACY OFF) + set(WITH_SS_MANAGER OFF) +endif() +include(cmake/Dependencies.cmake) # Connmarktos support if(ENABLE_CONNMARKTOS) @@ -108,100 +97,20 @@ endif() # Run platform tests include(${PROJECT_SOURCE_DIR}/cmake/configure.cmake) configure_file(${PROJECT_SOURCE_DIR}/cmake/config.h.cmake ${PROJECT_BINARY_DIR}/src/config.h) -add_definitions(-I${PROJECT_BINARY_DIR}/src) -add_definitions(-DHAVE_CONFIG_H) - -# pkg-config -configure_file( - ${PROJECT_SOURCE_DIR}/cmake/shadowsocks-libev.pc.cmake - ${PROJECT_BINARY_DIR}/pkgconfig/shadowsocks-libev.pc - @ONLY -) -install(FILES - ${PROJECT_BINARY_DIR}/pkgconfig/shadowsocks-libev.pc - DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig - ) - -if (WITH_EMBEDDED_SRC) -# We need libcork,libipset headers -include_directories(libcork/include) -include_directories(libipset/include) -include_directories(libbloom/murmur2) -include_directories(libbloom) - -set(LIBCORK_SOURCE - libcork/src/libcork/cli/commands.c - libcork/src/libcork/core/allocator.c - libcork/src/libcork/core/error.c - libcork/src/libcork/core/gc.c - libcork/src/libcork/core/hash.c - libcork/src/libcork/core/ip-address.c - libcork/src/libcork/core/mempool.c - libcork/src/libcork/core/timestamp.c - libcork/src/libcork/core/u128.c - libcork/src/libcork/core/version.c - libcork/src/libcork/ds/array.c - libcork/src/libcork/ds/bitset.c - libcork/src/libcork/ds/buffer.c - libcork/src/libcork/ds/dllist.c - libcork/src/libcork/ds/file-stream.c - libcork/src/libcork/ds/hash-table.c - libcork/src/libcork/ds/managed-buffer.c - libcork/src/libcork/ds/ring-buffer.c - libcork/src/libcork/ds/slice.c - libcork/src/libcork/posix/directory-walker.c - libcork/src/libcork/posix/env.c - libcork/src/libcork/posix/exec.c - libcork/src/libcork/posix/files.c - libcork/src/libcork/posix/process.c - libcork/src/libcork/pthreads/thread.c - ) -if (NOT MINGW) -set(LIBCORK_SOURCE ${LIBCORK_SOURCE} libcork/src/libcork/posix/subprocess.c) -else () -set(LIBCORK_SOURCE ${LIBCORK_SOURCE} libcork/src/libcork/posix/mingw.c) -endif () - -add_library(cork STATIC ${LIBCORK_SOURCE}) -target_compile_definitions(cork PUBLIC -DCORK_API=CORK_LOCAL) -if (MINGW) -target_link_libraries(cork ws2_32) -endif () - -set(LIBIPSET_SOURCE - libipset/src/libipset/general.c - libipset/src/libipset/bdd/assignments.c - libipset/src/libipset/bdd/basics.c - libipset/src/libipset/bdd/bdd-iterator.c - libipset/src/libipset/bdd/expanded.c - libipset/src/libipset/bdd/reachable.c - libipset/src/libipset/bdd/read.c - libipset/src/libipset/bdd/write.c - libipset/src/libipset/map/allocation.c - libipset/src/libipset/map/inspection.c - libipset/src/libipset/map/ipv4_map.c - libipset/src/libipset/map/ipv6_map.c - libipset/src/libipset/map/storage.c - libipset/src/libipset/set/allocation.c - libipset/src/libipset/set/inspection.c - libipset/src/libipset/set/ipv4_set.c - libipset/src/libipset/set/ipv6_set.c - libipset/src/libipset/set/iterator.c - libipset/src/libipset/set/storage.c - ) - -add_library(ipset STATIC ${LIBIPSET_SOURCE}) +include(cmake/ProjectOptions.cmake) +include_directories(${PROJECT_BINARY_DIR}/src) +add_compile_definitions(HAVE_CONFIG_H) set(LIBBLOOM_SOURCE - libbloom/bloom.c - libbloom/murmur2/MurmurHash2.c + third_party/bloom/bloom.c + third_party/bloom/murmur2/MurmurHash2.c ) add_library(bloom STATIC ${LIBBLOOM_SOURCE}) -target_link_libraries(ipset cork bloom) -endif () +target_include_directories(bloom PUBLIC third_party/bloom third_party/bloom/murmur2) add_subdirectory(src) +include(cmake/InstallPackage.cmake) add_subdirectory(doc) # Testing @@ -234,9 +143,11 @@ if (ENABLE_COVERAGE) endif () # Install ss-nat on Linux -if(LINUX) +if(LINUX AND SS_INSTALL_TOOLS) install(PROGRAMS src/ss-nat DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() # Install ss-setup TUI tool -install(PROGRAMS scripts/ss-setup.sh DESTINATION ${CMAKE_INSTALL_BINDIR} RENAME ss-setup) +if(UNIX AND SS_INSTALL_TOOLS) + install(PROGRAMS scripts/ss-setup.sh DESTINATION ${CMAKE_INSTALL_BINDIR} RENAME ss-setup) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..7990a8a5f --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,143 @@ +{ + "version": 2, + "cmakeMinimumRequired": { + "major": 3, + "minor": 20, + "patch": 0 + }, + "configurePresets": [ + { + "name": "system", + "displayName": "System dependencies", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-system", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "WITH_STATIC": "OFF", + "WITH_DOC_MAN": "OFF", + "WITH_DOC_HTML": "OFF", + "SS_DEPENDENCY_MODE": "system" + } + }, + { + "name": "system-static", + "inherits": "system", + "binaryDir": "${sourceDir}/build-system-static", + "cacheVariables": { + "WITH_STATIC": "ON", + "SS_BUILD_SHARED_LIBRARY": "OFF" + } + }, + { + "name": "asan", + "inherits": "system", + "binaryDir": "${sourceDir}/build-asan", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "ENABLE_SANITIZERS": "ON" + } + }, + { + "name": "bundled", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-bundled", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "SS_DEPENDENCY_MODE": "bundled", + "WITH_STATIC": "ON" + } + }, + { + "name": "minimal", + "inherits": "bundled", + "binaryDir": "${sourceDir}/build-minimal", + "cacheVariables": { + "SS_MINIMAL": "ON" + } + } + ], + "buildPresets": [ + { + "name": "system", + "configurePreset": "system" + }, + { + "name": "system-static", + "configurePreset": "system-static" + }, + { + "name": "asan", + "configurePreset": "asan" + }, + { + "name": "bundled", + "configurePreset": "bundled" + }, + { + "name": "minimal", + "configurePreset": "minimal" + } + ], + "testPresets": [ + { + "name": "system", + "configurePreset": "system", + "output": { + "outputOnFailure": true + }, + "filter": { + "include": { + "label": "unit" + } + } + }, + { + "name": "system-static", + "configurePreset": "system-static", + "output": { + "outputOnFailure": true + }, + "filter": { + "include": { + "label": "unit" + } + } + }, + { + "name": "asan", + "configurePreset": "asan", + "output": { + "outputOnFailure": true + }, + "filter": { + "include": { + "label": "unit" + } + } + }, + { + "name": "bundled", + "configurePreset": "bundled", + "output": { + "outputOnFailure": true + }, + "filter": { + "include": { + "label": "unit|vendor" + } + } + }, + { + "name": "minimal", + "configurePreset": "minimal", + "output": { + "outputOnFailure": true + }, + "filter": { + "include": { + "label": "unit|vendor" + } + } + } + ] +} diff --git a/README.md b/README.md index 32b69a12b..234467d90 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ sudo snap install shadowsocks-libev --edge + [Run](#run) + [Run as client](#run-as-client) - [OpenWRT](#openwrt) -- [OS X](#os-x) +- [macOS](#macos) - [Windows (MinGW)](#windows-mingw) - [Docker](#docker) @@ -71,55 +71,58 @@ sudo snap install shadowsocks-libev --edge ### Build from source (CMake) -shadowsocks-libev uses CMake as its sole build system. Start by pulling submodules: +The default build uses pinned sources included in this repository. It needs a +C11 compiler, CMake 3.20+, and Make or Ninja. No Git submodules, dependency +package installations, or network access are needed for configuration/build. +Python is used only by integration tests; documentation generation is optional. -```bash -git submodule update --init --recursive -``` - -Then build: - -```bash -mkdir -p build && cd build -cmake .. -make -sudo make install +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel +ctest --test-dir build -L 'unit|vendor' --output-on-failure +cmake --install build --prefix /your/install/prefix ``` -To run unit tests: +Programs are in `build/bin/`. Bundled binaries link to platform runtime +libraries; they do not require separately installed third-party libraries. -```bash -cd build -ctest --output-on-failure -``` - -#### CMake options +For a smaller build, use `-DSS_MINIMAL=ON`. It excludes PCRE2 regex, plugin +subprocesses, the manager, and legacy stream ciphers. Minimal ACLs support +IPv4/IPv6 CIDRs, `full:example.com` for exact domains, and +`suffix:example.com` for the apex and subdomains. Literal matches ignore ASCII +case and respect label boundaries. Unsupported regex rules are rejected. -For a complete list of available options, run `cmake -LH` from a build directory. -Commonly used options: +Distribution packages can use `-DSS_DEPENDENCY_MODE=system -DWITH_STATIC=OFF` +with libev, c-ares, libsodium, Mbed TLS 3.x, and PCRE2 development packages. +Use `-DCMAKE_PREFIX_PATH=/opt/homebrew/opt/mbedtls@3` when needed on macOS. -| Option | Default | Description | +| Option | Default | Purpose | |---|---|---| -| `-DWITH_EMBEDDED_SRC=OFF` | `ON` | Use system libcork/libipset/libbloom instead of bundled submodules | -| `-DWITH_DOC_MAN=OFF` | `ON` | Skip man page generation (removes asciidoc/xmlto dependency) | -| `-DBUILD_TESTING=OFF` | `ON` | Disable unit tests | -| `-DENABLE_CONNMARKTOS=ON` | `OFF` | Linux netfilter conntrack QoS support | -| `-DENABLE_NFTABLES=ON` | `OFF` | nftables firewall integration | - -On macOS, if libraries are installed via Homebrew, specify paths: - -```bash -cmake .. -DCMAKE_PREFIX_PATH="/usr/local/opt/mbedtls;/usr/local/opt/libsodium" -``` +| `SS_DEPENDENCY_MODE` | `bundled` | `bundled` sources or `system` libraries | +| `WITH_STATIC` | `ON` | Link dependency archives; system mode also supports shared dependencies | +| `SS_BUILD_EXECUTABLES` | `ON` | Command-line tools | +| `SS_BUILD_STATIC_LIBRARY` | `ON` | Static embedding library with installed dependency archives | +| `SS_BUILD_SHARED_LIBRARY` | `ON` | Shared embedding library | +| `SS_MINIMAL` | `OFF` | Disable regex, plugins, manager, and legacy stream ciphers | +| `SS_ENABLE_REGEX` / `SS_ENABLE_PLUGINS` / `SS_ENABLE_LEGACY` | `ON` | Individual compatibility features | +| `WITH_DOC_MAN` / `WITH_DOC_HTML` | `OFF` | Generate documentation (requires asciidoc; man pages also need xmlto) | +| `SS_INSTALL_TOOLS` | `OFF` | Install platform shell helpers | +| `ENABLE_SANITIZERS` | `OFF` | AddressSanitizer and UndefinedBehaviorSanitizer | +| `ENABLE_CONNMARKTOS` / `ENABLE_NFTABLES` | `OFF` | Optional Linux firewall integrations | + +CMake consumers can use `find_package(shadowsocks-libev CONFIG REQUIRED)` and +link `shadowsocks::static`, `shadowsocks::shared`, or `shadowsocks::shadowsocks` +(which prefers the shared library when installed). A pkg-config file is also +installed. Dependency provenance and update instructions are in +[third_party/README.md](third_party/README.md); modernization progress and +validation limits are tracked in [docs/modernization.md](docs/modernization.md). ### Debian & Ubuntu -#### Install from repository (not recommended) - -Shadowsocks-libev is available in the official repository for following distributions: +#### Distribution packages -* Debian 8 or higher, including oldoldstable (jessie), old stable (stretch), stable (buster), testing (bullseye) and unstable (sid) -* Ubuntu 16.10 or higher +Package availability and versions depend on the distribution release; packaged +versions can differ from this source branch. ```bash sudo apt update @@ -128,17 +131,15 @@ sudo apt install shadowsocks-libev #### Build deb package from source -You can build shadowsocks-libev and all its dependencies by script: +Install the build dependencies listed in `debian/control`, then build the +packages from this checkout: ```bash -mkdir -p ~/build-area/ -cp ./scripts/build_deb.sh ~/build-area/ -cd ~/build-area -./build_deb.sh +dpkg-buildpackage -b -us -uc ``` -For older systems, building `.deb` packages is not supported. -Please try to build and install directly from source. See the [Linux](#linux) section below. +Debian packaging explicitly uses system libraries. For a bundled build without +library development packages, use the [CMake instructions](#build-from-source-cmake). #### Configure and start the service @@ -156,19 +157,10 @@ sudo systemctl start shadowsocks-libev # for systemd ### Fedora & RHEL -Supported distributions: - -* Recent Fedora versions (until EOL) -* RHEL 6, 7 and derivatives (including CentOS, Scientific Linux) - -#### Build from source with centos - -If you are using CentOS 7, you need to install these prerequirements to build from source code: - -```bash -yum install epel-release -y -yum install gcc gettext autoconf libtool automake make pcre-devel asciidoc xmlto c-ares-devel libev-devel libsodium-devel mbedtls-devel -y -``` +Use the bundled CMake build above with a C11 compiler, CMake 3.20+ and Make +or Ninja. Older distribution toolchains may need upgrading. Autotools, gettext +and separately installed crypto/event/DNS development libraries are not required +for bundled mode. ### Archlinux & Manjaro @@ -193,47 +185,20 @@ nix-env -iA nixpkgs.shadowsocks-libev ### Linux -In general, you need the following build dependencies: - -* cmake (>= 3.2) -* a C compiler (gcc or clang) -* pkg-config -* libmbedtls -* libsodium (>= 1.0.4) -* libpcre2 -* libev -* libc-ares -* asciidoc (for documentation only) -* xmlto (for documentation only) - -If your system is too old to provide libmbedtls and libsodium (>= 1.0.4), you will need to either install those libraries manually or upgrade your system. - -Install build dependencies for your distribution: +The default bundled build needs only a C11 compiler, CMake 3.20+ and Make +or Ninja. For example, on Debian/Ubuntu: ```bash -# Debian / Ubuntu -sudo apt-get install --no-install-recommends build-essential cmake pkg-config \ - libpcre2-dev libev-dev libc-ares-dev libmbedtls-dev libsodium-dev \ - asciidoc xmlto - -# CentOS / Fedora / RHEL -sudo yum install gcc cmake make pkg-config pcre2-devel c-ares-devel \ - libev-devel libsodium-devel mbedtls-devel asciidoc xmlto - -# Arch -sudo pacman -S gcc cmake make pkg-config pcre2 c-ares libev libsodium mbedtls \ - asciidoc xmlto +sudo apt-get install --no-install-recommends build-essential cmake +cmake -S . -B build +cmake --build build --parallel +ctest --test-dir build -L 'unit|vendor' --output-on-failure +sudo cmake --install build ``` -Then build and install: - -```bash -git submodule update --init --recursive -mkdir -p build && cd build -cmake .. -make -sudo make install -``` +Distribution packagers can install `libpcre2-dev libev-dev libc-ares-dev +libmbedtls-dev libsodium-dev` and select `-DSS_DEPENDENCY_MODE=system +-DWITH_STATIC=OFF`. Documentation additionally needs asciidoc and xmlto. ### FreeBSD #### Install @@ -284,40 +249,35 @@ Note that is simply a workaround, each time you upgrade the port your changes wi The OpenWRT project is maintained here: [openwrt-shadowsocks](https://github.com/shadowsocks/openwrt-shadowsocks). -### OS X -For OS X, use [Homebrew](http://brew.sh) to install or build. - -Install Homebrew: - -```bash -ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" -``` -Install shadowsocks-libev: +### macOS -```bash -brew install shadowsocks-libev -``` +Use the bundled CMake instructions above with Xcode Command Line Tools and +CMake. The bundled executables require no Homebrew runtime libraries. +For system mode, use Mbed TLS 3 and point `CMAKE_PREFIX_PATH` at its prefix. ### Windows (MinGW) -To build Windows native binaries, the recommended method is to use Docker: - -* On Windows: double-click `make.bat` in `docker\mingw` -* On Unix-like system: - - cd shadowsocks-libev/docker/mingw - make -A tarball with 32-bit and 64-bit binaries will be generated in the same directory. +In an MSYS2 UCRT64 shell, install the toolchain and build native Windows programs: -You could also manually use MinGW-w64 compilers to build in Unix-like shell (MSYS2/Cygwin), or cross-compile on Unix-like systems (Linux/MacOS). Please refer to build scripts in `docker/mingw`. - -Currently you need to use a patched libev library for MinGW: +```bash +pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja +cmake -S . -B build -G Ninja +cmake --build build --parallel +ctest --test-dir build -L 'unit|vendor' --output-on-failure +``` -* https://github.com/shadowsocks/libev/archive/mingw.zip +Unix hosts with Zig installed can cross-compile without a separate MinGW SDK: -Notice that TCP Fast Open (TFO) is only available on **Windows 10**, **1607** or later version (precisely, build >= 14393). If you are using **1709** (build 16299) or later version, you also need to run the following command in PowerShell/Command Prompt **as Administrator** and **reboot** to use TFO properly: +```bash +cmake -S . -B build-windows -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/zig-windows.cmake +cmake --build build-windows --parallel +``` - netsh int tcp set global fastopenfallback=disabled +Cross-compilation does not run Windows tests. The portability workflow runs +native UCRT64 tests and TCP/UDP relay checks on Windows. Bundled mode is required +because libev must use the project's Winsock handle adapter. MSVC remains a +separate, unsupported milestone; configuration reports this explicitly. +The historical Autotools scripts in `docker/mingw` are superseded by this build. ### Docker @@ -521,7 +481,7 @@ Select "Install a SIP003 plugin" from the main menu (requires root). Supports au ### ss-nat -`ss-nat` is a helper script that sets up iptables NAT rules for `ss-redir` to provide transparent TCP/UDP redirection. It is installed on Linux systems by `make install`. +`ss-nat` is a helper script that sets up iptables NAT rules for `ss-redir` to provide transparent TCP/UDP redirection. Enable `-DSS_INSTALL_TOOLS=ON` to install it on Linux. **Prerequisites:** Linux with `iptables`, `ipset`, and optionally TPROXY kernel module for UDP diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake new file mode 100644 index 000000000..3dd28b451 --- /dev/null +++ b/cmake/Dependencies.cmake @@ -0,0 +1,69 @@ +set(SS_DEPENDENCY_MODE "bundled" CACHE STRING "Dependency source: bundled or system") +set_property(CACHE SS_DEPENDENCY_MODE PROPERTY STRINGS bundled system) +set(SS_MATH_LIBRARY "") +if(NOT WIN32) + set(SS_MATH_LIBRARY m) +endif() +find_package(Threads REQUIRED) +if(SS_DEPENDENCY_MODE STREQUAL "bundled") + if(NOT WITH_STATIC) + message(FATAL_ERROR "Bundled mode requires WITH_STATIC=ON; use system mode for shared dependencies") + endif() + add_subdirectory(third_party) + return() +elseif(NOT SS_DEPENDENCY_MODE STREQUAL "system") + message(FATAL_ERROR "SS_DEPENDENCY_MODE must be bundled or system") +endif() +if(WIN32) + message(FATAL_ERROR "Windows requires bundled dependencies: libev must use this project's Winsock handle adapter") +endif() + +# Select one linkage mode consistently for programs, libraries, and tests. +# Never require shared copies in a static dependency build. +if(WITH_STATIC) + set(_ss_saved_suffixes ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a) +endif() +if(SS_ENABLE_REGEX) + find_package(PCRE2 REQUIRED) +endif() +find_package(MbedTLS REQUIRED) +find_package(Sodium REQUIRED) +find_package(Cares REQUIRED) +find_path(LIBEV_INCLUDE_DIR ev.h) +find_library(LIBEV_LIBRARY NAMES ev) +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libev REQUIRED_VARS LIBEV_LIBRARY LIBEV_INCLUDE_DIR) +find_package(Threads REQUIRED) +if(WITH_STATIC) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${_ss_saved_suffixes}) +endif() + +function(ss_import_dependency target location includes) + add_library(${target} UNKNOWN IMPORTED GLOBAL) + set_target_properties(${target} PROPERTIES + IMPORTED_LOCATION "${location}" + INTERFACE_INCLUDE_DIRECTORIES "${includes}") +endfunction() +ss_import_dependency(ss_sodium "${SODIUM_LIBRARY}" "${SODIUM_INCLUDE_DIRS}") +ss_import_dependency(ss_mbedcrypto "${MBEDTLS_CRYPTO_LIBRARY}" "${MBEDTLS_INCLUDE_DIRS}") +ss_import_dependency(ss_cares "${CARES_LIBRARY}" "${CARES_INCLUDE_DIRS}") +if(SS_ENABLE_REGEX) + ss_import_dependency(ss_pcre2 "${PCRE2_LIBRARY}" "${PCRE2_INCLUDE_DIRS}") +endif() +ss_import_dependency(ss_ev "${LIBEV_LIBRARY}" "${LIBEV_INCLUDE_DIR}") +if(WIN32) + set_property(TARGET ss_cares APPEND PROPERTY INTERFACE_LINK_LIBRARIES ws2_32 iphlpapi) + set_property(TARGET ss_ev APPEND PROPERTY INTERFACE_LINK_LIBRARIES ws2_32) + if(WITH_STATIC) + set_property(TARGET ss_cares APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS CARES_STATICLIB) + if(SS_ENABLE_REGEX) + set_property(TARGET ss_pcre2 APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS PCRE2_STATIC) + endif() + set_property(TARGET ss_sodium APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS SODIUM_STATIC) + endif() +endif() +set(SS_MATH_LIBRARY "") +if(NOT WIN32) + set(SS_MATH_LIBRARY m) +endif() diff --git a/cmake/FindCares.cmake b/cmake/FindCares.cmake index 07c248610..825fd4e39 100644 --- a/cmake/FindCares.cmake +++ b/cmake/FindCares.cmake @@ -42,3 +42,6 @@ else() set(CARES_FOUND FALSE) message(FATAL_ERROR "Could not find c-ares library. Install libc-ares-dev or equivalent.") endif() + +# CMake find_dependency uses the exact package-name spelling. +set(Cares_FOUND ${CARES_FOUND}) diff --git a/cmake/FindMbedTLS.cmake b/cmake/FindMbedTLS.cmake index fca87e149..fc9a6294b 100644 --- a/cmake/FindMbedTLS.cmake +++ b/cmake/FindMbedTLS.cmake @@ -18,6 +18,8 @@ include(CheckCSourceCompiles) # This codebase targets the mbedTLS 3.x API, so prefer a 3.x installation. set(_MBEDTLS_PREFIX_HINTS + ${MbedTLS_ROOT} + ${CMAKE_PREFIX_PATH} /opt/homebrew/opt/mbedtls@3 /usr/local/opt/mbedtls@3 /opt/homebrew/opt/mbedtls @@ -29,11 +31,12 @@ set(_MBEDTLS_PREFIX_HINTS # Locate a prefix that provides both the headers and libmbedcrypto, so the # two can never be drawn from different installations. +if(NOT CMAKE_CROSSCOMPILING) foreach(_prefix IN LISTS _MBEDTLS_PREFIX_HINTS) if(EXISTS "${_prefix}/include/mbedtls/cipher.h") find_library(_MBEDTLS_CRYPTO_IN_PREFIX NAMES mbedcrypto - PATHS "${_prefix}/lib" + PATHS "${_prefix}/lib" "${_prefix}/lib/${CMAKE_LIBRARY_ARCHITECTURE}" NO_DEFAULT_PATH ) if(_MBEDTLS_CRYPTO_IN_PREFIX) @@ -43,6 +46,7 @@ foreach(_prefix IN LISTS _MBEDTLS_PREFIX_HINTS) unset(_MBEDTLS_CRYPTO_IN_PREFIX CACHE) endif() endforeach() +endif() if(_MBEDTLS_ROOT) # Pin every component to the prefix chosen above. @@ -53,12 +57,12 @@ if(_MBEDTLS_ROOT) ) find_library(MBEDTLS_CRYPTO_LIBRARY NAMES mbedcrypto - PATHS "${_MBEDTLS_ROOT}/lib" + PATHS "${_MBEDTLS_ROOT}/lib" "${_MBEDTLS_ROOT}/lib/${CMAKE_LIBRARY_ARCHITECTURE}" NO_DEFAULT_PATH ) find_library(MBEDTLS_TLS_LIBRARY NAMES mbedtls - PATHS "${_MBEDTLS_ROOT}/lib" + PATHS "${_MBEDTLS_ROOT}/lib" "${_MBEDTLS_ROOT}/lib/${CMAKE_LIBRARY_ARCHITECTURE}" NO_DEFAULT_PATH ) else() @@ -74,6 +78,17 @@ if(MBEDTLS_INCLUDE_DIR AND MBEDTLS_CRYPTO_LIBRARY) set(MBEDTLS_FOUND TRUE) set(MBEDTLS_INCLUDE_DIRS ${MBEDTLS_INCLUDE_DIR}) + set(_mbedtls_version_header "${MBEDTLS_INCLUDE_DIR}/mbedtls/version.h") + if(EXISTS "${MBEDTLS_INCLUDE_DIR}/mbedtls/build_info.h") + set(_mbedtls_version_header "${MBEDTLS_INCLUDE_DIR}/mbedtls/build_info.h") + endif() + file(STRINGS "${_mbedtls_version_header}" _mbedtls_version_line + REGEX "^#define MBEDTLS_VERSION_STRING +\"[0-9.]+\"") + string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" MBEDTLS_VERSION "${_mbedtls_version_line}") + if(NOT MBEDTLS_VERSION OR MBEDTLS_VERSION VERSION_LESS 2 OR NOT MBEDTLS_VERSION VERSION_LESS 4) + message(FATAL_ERROR "Supported system Mbed TLS versions are 2.x and 3.x; found ${MBEDTLS_VERSION}") + endif() + set(CMAKE_REQUIRED_INCLUDES ${MBEDTLS_INCLUDE_DIR}) set(CMAKE_REQUIRED_LIBRARIES ${MBEDTLS_CRYPTO_LIBRARY}) @@ -136,3 +151,11 @@ else() set(MBEDTLS_FOUND FALSE) message(FATAL_ERROR "Could not find mbedTLS library. Install libmbedtls-dev or equivalent.") endif() + +# CMake find_dependency uses the exact package-name spelling. +set(MbedTLS_FOUND ${MBEDTLS_FOUND}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(MbedTLS + REQUIRED_VARS MBEDTLS_INCLUDE_DIR MBEDTLS_CRYPTO_LIBRARY + VERSION_VAR MBEDTLS_VERSION) diff --git a/cmake/FindPCRE2.cmake b/cmake/FindPCRE2.cmake index 848ca8e74..b610fca9e 100644 --- a/cmake/FindPCRE2.cmake +++ b/cmake/FindPCRE2.cmake @@ -1,63 +1,6 @@ -# FindPCRE2.cmake - Find PCRE2 library (8-bit) -# -# Sets: -# PCRE2_FOUND -# PCRE2_INCLUDE_DIRS -# PCRE2_LIBRARIES - -find_package(PkgConfig QUIET) - -if(PkgConfig_FOUND) - pkg_check_modules(_PCRE2 QUIET libpcre2-8) -endif() - -if(_PCRE2_FOUND) - set(PCRE2_INCLUDE_DIRS ${_PCRE2_INCLUDE_DIRS}) - set(PCRE2_LIBRARIES ${_PCRE2_LIBRARIES}) - set(PCRE2_FOUND TRUE) -else() - # Try pcre2-config - find_program(PCRE2_CONFIG pcre2-config) - if(PCRE2_CONFIG) - execute_process(COMMAND ${PCRE2_CONFIG} --cflags - OUTPUT_VARIABLE PCRE2_CFLAGS - OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(COMMAND ${PCRE2_CONFIG} --libs8 - OUTPUT_VARIABLE PCRE2_LDFLAGS - OUTPUT_STRIP_TRAILING_WHITESPACE) - string(REGEX REPLACE "-I" "" PCRE2_INCLUDE_DIRS "${PCRE2_CFLAGS}") - set(PCRE2_LIBRARIES ${PCRE2_LDFLAGS}) - set(PCRE2_FOUND TRUE) - else() - # Manual search - find_path(PCRE2_INCLUDE_DIR - NAMES pcre2.h - HINTS - /opt/homebrew/include - /usr/local/include - /usr/include - ) - - find_library(PCRE2_LIBRARY - NAMES pcre2-8 - HINTS - /opt/homebrew/lib - /usr/local/lib - /usr/lib - ) - - if(PCRE2_INCLUDE_DIR AND PCRE2_LIBRARY) - set(PCRE2_INCLUDE_DIRS ${PCRE2_INCLUDE_DIR}) - set(PCRE2_LIBRARIES ${PCRE2_LIBRARY}) - set(PCRE2_FOUND TRUE) - else() - set(PCRE2_FOUND FALSE) - endif() - endif() -endif() - -if(PCRE2_FOUND) - message(STATUS "Found PCRE2: ${PCRE2_LIBRARIES}") -else() - message(FATAL_ERROR "Could not find PCRE2 library. Install libpcre2-dev or equivalent.") -endif() +find_path(PCRE2_INCLUDE_DIR NAMES pcre2.h) +find_library(PCRE2_LIBRARY NAMES pcre2-8) +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PCRE2 REQUIRED_VARS PCRE2_INCLUDE_DIR PCRE2_LIBRARY) +set(PCRE2_INCLUDE_DIRS ${PCRE2_INCLUDE_DIR}) +set(PCRE2_LIBRARIES ${PCRE2_LIBRARY}) diff --git a/cmake/FindSodium.cmake b/cmake/FindSodium.cmake index 19013bc5c..9fe5c36ab 100644 --- a/cmake/FindSodium.cmake +++ b/cmake/FindSodium.cmake @@ -55,3 +55,6 @@ else() set(SODIUM_FOUND FALSE) message(FATAL_ERROR "Could not find libsodium. Install libsodium-dev or equivalent.") endif() + +# CMake find_dependency uses the exact package-name spelling. +set(Sodium_FOUND ${SODIUM_FOUND}) diff --git a/cmake/InstallPackage.cmake b/cmake/InstallPackage.cmake new file mode 100644 index 000000000..4db28a13f --- /dev/null +++ b/cmake/InstallPackage.cmake @@ -0,0 +1,78 @@ +include(CMakePackageConfigHelpers) +if(SS_BUILD_STATIC_LIBRARY OR SS_BUILD_SHARED_LIBRARY) +set(SS_SYSTEM_DEPENDENCIES "") +set(SS_PC_PRIVATE "") +set(private_links) +set(private_content "# Generated private linkage for the installed static library.\n") +if(SS_BUILD_STATIC_LIBRARY) + set(private_targets bloom blake3) + if(SS_DEPENDENCY_MODE STREQUAL "bundled") + list(APPEND private_targets ss_sodium ss_mbedcrypto ss_ev ss_cares) + if(SS_ENABLE_REGEX) + list(APPEND private_targets ss_pcre2) + endif() + else() + set(SS_SYSTEM_DEPENDENCIES + "list(APPEND CMAKE_MODULE_PATH \"\${CMAKE_CURRENT_LIST_DIR}/modules\")\n find_dependency(MbedTLS ${MBEDTLS_VERSION} EXACT)\n find_dependency(Sodium)\n find_dependency(Cares)\n find_library(SS_CONSUMER_EV NAMES ev REQUIRED)") + if(SS_ENABLE_REGEX) + string(APPEND SS_SYSTEM_DEPENDENCIES "\n find_dependency(PCRE2)") + list(APPEND private_links "\${PCRE2_LIBRARY}") + string(APPEND SS_PC_PRIVATE " -lpcre2-8") + endif() + list(APPEND private_links "\${SODIUM_LIBRARY}" "\${MBEDTLS_CRYPTO_LIBRARY}" + "\${CARES_LIBRARY}" "\${SS_CONSUMER_EV}") + string(APPEND SS_PC_PRIVATE " -lsodium -lmbedcrypto -lcares -lev") + install(FILES cmake/FindMbedTLS.cmake cmake/FindSodium.cmake + cmake/FindCares.cmake cmake/FindPCRE2.cmake cmake/mbedtls_version_check.c + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/shadowsocks-libev/modules) + endif() + foreach(target IN LISTS private_targets) + install(FILES $ + DESTINATION ${CMAKE_INSTALL_LIBDIR}/shadowsocks-libev) + list(APPEND private_links "\${_ss_libdir}/shadowsocks-libev/$") + string(APPEND SS_PC_PRIVATE " \${libdir}/shadowsocks-libev/$") + endforeach() + list(APPEND private_links Threads::Threads ${SS_MATH_LIBRARY} ${CMAKE_DL_LIBS}) + if(WIN32) + list(APPEND private_links ws2_32 iphlpapi bcrypt advapi32) + string(APPEND SS_PC_PRIVATE " -lws2_32 -liphlpapi -lbcrypt -ladvapi32") + else() + string(APPEND SS_PC_PRIVATE " -lm -pthread") + if(APPLE) + list(APPEND private_links resolv) + string(APPEND SS_PC_PRIVATE " -lresolv") + endif() + endif() + # Resolve libdir relative to this installed config, supporting --prefix relocation. + file(RELATIVE_PATH libdir_from_config "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/shadowsocks-libev" "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") + string(APPEND private_content "get_filename_component(_ss_libdir \"\${CMAKE_CURRENT_LIST_DIR}/${libdir_from_config}\" ABSOLUTE)\n") + string(APPEND private_content "set_property(TARGET shadowsocks::static PROPERTY INTERFACE_LINK_LIBRARIES \"${private_links}\")\n") +endif() +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/package/shadowsocks-private.cmake" CONTENT "${private_content}") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/package/shadowsocks-private.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/shadowsocks-libev) + +set(SS_STATIC_FILENAME "${CMAKE_STATIC_LIBRARY_PREFIX}shadowsocks-libev${CMAKE_STATIC_LIBRARY_SUFFIX}") +set(SS_SHARED_FILENAME "${CMAKE_SHARED_LIBRARY_PREFIX}shadowsocks-libev${CMAKE_SHARED_LIBRARY_SUFFIX}") +set(SS_SHARED_IMPLIB "${CMAKE_IMPORT_LIBRARY_PREFIX}shadowsocks-libev${CMAKE_IMPORT_LIBRARY_SUFFIX}") +configure_package_config_file(cmake/shadowsocks-libev-config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/package/shadowsocks-libev-config.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/shadowsocks-libev + PATH_VARS CMAKE_INSTALL_LIBDIR CMAKE_INSTALL_BINDIR CMAKE_INSTALL_INCLUDEDIR) +write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/package/shadowsocks-libev-config-version.cmake" + VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/package/shadowsocks-libev-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/package/shadowsocks-libev-config-version.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/shadowsocks-libev) +file(RELATIVE_PATH SS_PC_PREFIX_RELATIVE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/pkgconfig" "${CMAKE_CURRENT_BINARY_DIR}") +configure_file(cmake/shadowsocks-libev.pc.cmake "${CMAKE_CURRENT_BINARY_DIR}/package/shadowsocks-libev.pc.in" @ONLY) +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/pkgconfig/shadowsocks-libev.pc" + INPUT "${CMAKE_CURRENT_BINARY_DIR}/package/shadowsocks-libev.pc.in") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pkgconfig/shadowsocks-libev.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +endif() + +install(FILES COPYING LICENSE third_party/README.md third_party/manifest.json + DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/shadowsocks-libev) +install(FILES third_party/bloom/LICENSE DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/shadowsocks-libev/bloom) +install(DIRECTORY src/blake3/ DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/shadowsocks-libev/blake3 + FILES_MATCHING PATTERN "LICENSE*") diff --git a/cmake/ProjectOptions.cmake b/cmake/ProjectOptions.cmake new file mode 100644 index 000000000..cb160b7e1 --- /dev/null +++ b/cmake/ProjectOptions.cmake @@ -0,0 +1,52 @@ +include(CheckCCompilerFlag) +option(DISABLE_SSP "Disable stack protector" OFF) +if(NOT DISABLE_SSP AND NOT MSVC) + check_c_compiler_flag(-fstack-protector-strong HAS_STACK_PROTECTOR) +endif() + +function(ss_project_options target) + set_property(TARGET ${target} PROPERTY NO_SYSTEM_FROM_IMPORTED TRUE) + target_include_directories(${target} BEFORE PRIVATE ${MBEDTLS_INCLUDE_DIRS}) + if(WIN32) + target_compile_definitions(${target} PRIVATE WIN32_LEAN_AND_MEAN __USE_MINGW_ANSI_STDIO=1) + # MinGW can otherwise pull in libwinpthread/libgcc from the toolchain, + # even when all explicitly selected dependencies are static archives. + get_target_property(ss_target_type ${target} TYPE) + # Zig already supplies its own runtime for DLLs; its -static switch + # changes the output kind instead of just selecting static libraries. + if(NOT ss_target_type STREQUAL "SHARED_LIBRARY" OR CMAKE_C_COMPILER_ID STREQUAL "GNU") + target_link_options(${target} PRIVATE -static) + endif() + endif() + if(MSVC) + target_compile_options(${target} PRIVATE /W4) + if(SS_WARNINGS_AS_ERRORS) + target_compile_options(${target} PRIVATE /WX) + endif() + else() + target_compile_options(${target} PRIVATE -Wall -Wno-deprecated-declarations -fno-strict-aliasing) + if(SS_WARNINGS_AS_ERRORS) + target_compile_options(${target} PRIVATE -Werror) + endif() + if(HAS_STACK_PROTECTOR) + target_compile_options(${target} PRIVATE -fstack-protector-strong) + endif() + if(MINGW OR CYGWIN) + target_compile_options(${target} PRIVATE -mno-ms-bitfields) + endif() + endif() + if(LINUX) + target_compile_definitions(${target} PRIVATE _GNU_SOURCE) + endif() + if(ENABLE_SANITIZERS) + if(MSVC) + message(FATAL_ERROR "ASan + UBSan requires GCC or Clang") + endif() + target_compile_options(${target} PRIVATE -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer) + target_link_options(${target} PRIVATE -fsanitize=address,undefined) + endif() + if(ENABLE_COVERAGE) + target_compile_options(${target} PRIVATE -O0 --coverage) + target_link_options(${target} PRIVATE --coverage) + endif() +endfunction() diff --git a/cmake/config.h.cmake b/cmake/config.h.cmake index 55984be4f..de889cc81 100644 --- a/cmake/config.h.cmake +++ b/cmake/config.h.cmake @@ -307,4 +307,8 @@ /* Define as `fork' if `vfork' does not work. */ #cmakedefine vfork +#cmakedefine01 SS_ENABLE_REGEX +#cmakedefine01 SS_ENABLE_PLUGINS +#cmakedefine01 SS_ENABLE_LEGACY + #endif diff --git a/cmake/configure.cmake b/cmake/configure.cmake index 165749752..2c6d2496d 100644 --- a/cmake/configure.cmake +++ b/cmake/configure.cmake @@ -16,7 +16,7 @@ include(CheckCCompilerFlag) # Set CONNECT_IN_PROGRESS based on platform if(MINGW) - set(CONNECT_IN_PROGRESS "WSAEWOULDBLOCK") + set(CONNECT_IN_PROGRESS "EWOULDBLOCK") else() set(CONNECT_IN_PROGRESS "EINPROGRESS") endif() @@ -180,18 +180,3 @@ endif () if (NOT HAVE_WORKING_VFORK) set(vfork fork) endif () - -# Stack protector detection -option(DISABLE_SSP "Disable -fstack-protector" OFF) -if(NOT DISABLE_SSP) - check_c_compiler_flag(-fstack-protector HAS_STACK_PROTECTOR) - if(HAS_STACK_PROTECTOR) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector") - message(STATUS "Stack protector enabled") - endif() -endif() - -# MinGW/Cygwin compiler flags -if(MINGW OR CYGWIN) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mno-ms-bitfields") -endif() diff --git a/cmake/shadowsocks-libev-config.cmake.in b/cmake/shadowsocks-libev-config.cmake.in new file mode 100644 index 000000000..6708b8fef --- /dev/null +++ b/cmake/shadowsocks-libev-config.cmake.in @@ -0,0 +1,34 @@ +@PACKAGE_INIT@ + +if(@SS_BUILD_SHARED_LIBRARY@ AND NOT TARGET shadowsocks::shared) + add_library(shadowsocks::shared SHARED IMPORTED) + set_target_properties(shadowsocks::shared PROPERTIES + IMPORTED_LOCATION "@PACKAGE_CMAKE_INSTALL_LIBDIR@/@SS_SHARED_FILENAME@" + INTERFACE_INCLUDE_DIRECTORIES "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") + if(WIN32) + set_target_properties(shadowsocks::shared PROPERTIES + IMPORTED_LOCATION "@PACKAGE_CMAKE_INSTALL_BINDIR@/@SS_SHARED_FILENAME@" + IMPORTED_IMPLIB "@PACKAGE_CMAKE_INSTALL_LIBDIR@/@SS_SHARED_IMPLIB@") + endif() +endif() +if(@SS_BUILD_STATIC_LIBRARY@ AND NOT TARGET shadowsocks::static) + include(CMakeFindDependencyMacro) + find_dependency(Threads) + @SS_SYSTEM_DEPENDENCIES@ + add_library(shadowsocks::static STATIC IMPORTED) + set_target_properties(shadowsocks::static PROPERTIES + IMPORTED_LOCATION "@PACKAGE_CMAKE_INSTALL_LIBDIR@/@SS_STATIC_FILENAME@" + INTERFACE_INCLUDE_DIRECTORIES "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") + include("${CMAKE_CURRENT_LIST_DIR}/shadowsocks-private.cmake") + if(WIN32) + set_property(TARGET shadowsocks::static PROPERTY INTERFACE_LINK_OPTIONS -static) + endif() +endif() +if(NOT TARGET shadowsocks::shadowsocks) + if(TARGET shadowsocks::shared) + add_library(shadowsocks::shadowsocks ALIAS shadowsocks::shared) + elseif(TARGET shadowsocks::static) + add_library(shadowsocks::shadowsocks ALIAS shadowsocks::static) + endif() +endif() +check_required_components(shadowsocks-libev) diff --git a/cmake/shadowsocks-libev.pc.cmake b/cmake/shadowsocks-libev.pc.cmake index 2ddd75ef4..eb1dfa339 100644 --- a/cmake/shadowsocks-libev.pc.cmake +++ b/cmake/shadowsocks-libev.pc.cmake @@ -1,4 +1,4 @@ -prefix=@CMAKE_INSTALL_PREFIX@ +prefix=${pcfiledir}/@SS_PC_PREFIX_RELATIVE@ exec_prefix=${prefix} libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ @@ -10,3 +10,4 @@ Version: @PROJECT_VERSION@ Requires: Cflags: -I${includedir} Libs: -L${libdir} -lshadowsocks-libev +Libs.private: @SS_PC_PRIVATE@ diff --git a/cmake/toolchains/zig-windows.cmake b/cmake/toolchains/zig-windows.cmake new file mode 100644 index 000000000..31bad818a --- /dev/null +++ b/cmake/toolchains/zig-windows.cmake @@ -0,0 +1,20 @@ +# Cross-compile the C project from a Unix host without Docker or a MinGW SDK. +# cmake -S . -B build-windows -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/zig-windows.cmake +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR AMD64) +find_program(SS_ZIG_EXECUTABLE NAMES zig REQUIRED) +set(CMAKE_C_COMPILER "${SS_ZIG_EXECUTABLE}" cc -target x86_64-windows-gnu) +# CMake's archive tools accept an executable path, not an argv list. +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/zig-tools") +foreach(tool ar ranlib) + set(wrapper "${CMAKE_BINARY_DIR}/zig-tools/${tool}") + file(WRITE "${wrapper}" "#!/bin/sh\nexec \"${SS_ZIG_EXECUTABLE}\" ${tool} \"$@\"\n") + file(CHMOD "${wrapper}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) + set(CMAKE_${tool} "${wrapper}") +endforeach() +set(CMAKE_AR "${CMAKE_BINARY_DIR}/zig-tools/ar") +set(CMAKE_RANLIB "${CMAKE_BINARY_DIR}/zig-tools/ranlib") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/debian/control b/debian/control index 132443f6f..30317194f 100644 --- a/debian/control +++ b/debian/control @@ -7,6 +7,7 @@ Uploaders: Boyuan Yang , Roger Shimizu Build-Depends: + cmake (>= 3.20), asciidoc-base | asciidoc, debhelper (>= 10), libc-ares-dev, @@ -51,6 +52,11 @@ Section: libdevel Breaks: shadowsocks-libev (<< 2.4.0) Depends: libshadowsocks-libev2 (= ${binary:Version}), + libc-ares-dev, + libev-dev, + libmbedtls-dev, + libpcre2-dev, + libsodium-dev, ${misc:Depends} Description: lightweight and secure socks5 proxy (development files) Shadowsocks-libev is a lightweight and secure socks5 proxy for diff --git a/debian/libshadowsocks-libev-dev.install b/debian/libshadowsocks-libev-dev.install index 304e0ed96..60b6f6461 100644 --- a/debian/libshadowsocks-libev-dev.install +++ b/debian/libshadowsocks-libev-dev.install @@ -1,3 +1,6 @@ usr/include/ usr/lib/*/libshadowsocks-libev.so +usr/lib/*/libshadowsocks-libev.a +usr/lib/*/shadowsocks-libev/ +usr/lib/*/cmake/shadowsocks-libev/ usr/lib/*/pkgconfig/ diff --git a/debian/rules b/debian/rules index 2121df06a..75e9b52f6 100755 --- a/debian/rules +++ b/debian/rules @@ -11,7 +11,7 @@ override_dh_auto_install: dh_auto_install override_dh_auto_configure: - dh_auto_configure -- -DWITH_STATIC=OFF + dh_auto_configure -- -DSS_DEPENDENCY_MODE=system -DWITH_STATIC=OFF -DWITH_DOC_MAN=ON -DWITH_DOC_HTML=ON -DSS_INSTALL_TOOLS=ON override_dh_installchangelogs: dh_installchangelogs -XChanges diff --git a/debian/shadowsocks-libev.install b/debian/shadowsocks-libev.install index 24fa1f6d0..cad7d5a6b 100644 --- a/debian/shadowsocks-libev.install +++ b/debian/shadowsocks-libev.install @@ -5,3 +5,4 @@ debian/shadowsocks-libev-*.service lib/systemd/system usr/bin/ usr/share/man/ usr/share/doc/shadowsocks-libev/ +usr/share/licenses/shadowsocks-libev/ diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 9abd45520..8c47c85ff 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -1,18 +1,14 @@ find_program(XMLTO_EXECUTABLE NAMES xmlto) find_program(ASCIIDOC_EXECUTABLE NAMES asciidoc asciidoc.py) -# Opt-in doc build option -if (NOT XMLTO_EXECUTABLE OR NOT ASCIIDOC_EXECUTABLE) - option(WITH_DOC_MAN "Build manpage doc" OFF) -else () - option(WITH_DOC_MAN "Build manpage doc" ON) -endif () - -if (NOT ASCIIDOC_EXECUTABLE) - option(WITH_DOC_HTML "Build html doc" OFF) -else () - option(WITH_DOC_HTML "Build html doc" ON) -endif () +option(WITH_DOC_MAN "Build manpage documentation" OFF) +option(WITH_DOC_HTML "Build HTML documentation" OFF) +if((WITH_DOC_MAN OR WITH_DOC_HTML) AND NOT ASCIIDOC_EXECUTABLE) + message(FATAL_ERROR "Documentation generation requires asciidoc") +endif() +if(WITH_DOC_MAN AND NOT XMLTO_EXECUTABLE) + message(FATAL_ERROR "Manpage generation requires xmlto") +endif() # NOTE For brew user, we have to setup this env var. see `brew info asciidoc' set(XMLTO_ENV) diff --git a/docker/alpine/Dockerfile b/docker/alpine/Dockerfile index 799497463..e5bc27cb6 100644 --- a/docker/alpine/Dockerfile +++ b/docker/alpine/Dockerfile @@ -16,17 +16,12 @@ RUN set -x \ && apk add --no-cache --virtual .build-deps \ build-base \ cmake \ - c-ares-dev \ libcap \ - libev-dev \ - libsodium-dev \ linux-headers \ - mbedtls-dev \ - pcre2-dev \ # Build & install && cd /tmp/repo \ && mkdir -p build && cd build \ - && cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local -DBUILD_TESTING=OFF -DWITH_STATIC=OFF -DCMAKE_BUILD_TYPE=Release \ + && cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local -DBUILD_TESTING=OFF -DSS_BUILD_STATIC_LIBRARY=OFF -DSS_BUILD_SHARED_LIBRARY=OFF -DCMAKE_BUILD_TYPE=Release \ && make -j$(getconf _NPROCESSORS_ONLN) \ && make install \ && cd /usr/local/bin \ diff --git a/docker/static/Dockerfile b/docker/static/Dockerfile new file mode 100644 index 000000000..76fdaf454 --- /dev/null +++ b/docker/static/Dockerfile @@ -0,0 +1,33 @@ +ARG ALPINE_IMAGE=alpine:3.24 +FROM ${ALPINE_IMAGE} AS build +ARG ALPINE_MIRROR=https://dl-cdn.alpinelinux.org/alpine +RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk/repositories \ + && apk add --no-cache clang lld make musl-dev compiler-rt cmake linux-headers python3 binutils file +ENV CC=clang +WORKDIR /src +COPY . . +ARG SS_MINIMAL=OFF +RUN --network=none cmake -S . -B /build \ + -DCMAKE_BUILD_TYPE=Release \ + -DSS_DEPENDENCY_MODE=bundled -DWITH_STATIC=ON \ + -DSS_BUILD_STATIC_LIBRARY=OFF -DSS_BUILD_SHARED_LIBRARY=OFF \ + -DSS_MINIMAL=${SS_MINIMAL} -DCMAKE_EXE_LINKER_FLAGS="-static -fuse-ld=lld --rtlib=compiler-rt" \ + -DCMAKE_INSTALL_PREFIX=/out \ + && cmake --build /build --parallel 4 \ + && ctest --test-dir /build -L 'unit|vendor' --output-on-failure --no-tests=error \ + && python3 tests/interop.py --self --bin /build/bin \ + && cmake --install /build --strip \ + && for binary in /out/bin/ss-*; do \ + file "$binary"; \ + ! readelf -l "$binary" | grep -q INTERP || exit 1; \ + ! readelf -d "$binary" | grep -q NEEDED || exit 1; \ + done + +# Export binaries and their license files with --target artifacts --output. +FROM scratch AS artifacts +COPY --from=build /out/ / + +# No libc, dynamic loader, package manager or shell in the runtime image. +FROM scratch AS runtime +COPY --from=build /out/ /usr/local/ +ENTRYPOINT ["/usr/local/bin/ss-server"] diff --git a/docker/static/Dockerfile.dockerignore b/docker/static/Dockerfile.dockerignore new file mode 100644 index 000000000..d81d48472 --- /dev/null +++ b/docker/static/Dockerfile.dockerignore @@ -0,0 +1,8 @@ +.git +build* +release +libcork +libipset +libbloom +**/__pycache__ +**/.DS_Store diff --git a/docker/static/README.md b/docker/static/README.md new file mode 100644 index 000000000..a946206e7 --- /dev/null +++ b/docker/static/README.md @@ -0,0 +1,41 @@ +# Fully static Linux builds + +The Alpine/musl builder uses Clang with LLD by default and links both third-party dependencies and libc statically. +`WITH_STATIC=ON` alone only selects static third-party libraries; this image also +passes `-DCMAKE_EXE_LINKER_FLAGS=-static` and omits shared-library outputs. +The compile, unit/vendor tests and six-method TCP/UDP relay tests run with Docker +network access disabled. The final ELF check rejects interpreters and dynamic +library dependencies. Initial image/toolchain installation requires networking. + +From the repository root, export binaries and licenses (ARM64 example): + +```sh +docker build --platform linux/arm64 -f docker/static/Dockerfile \ + --target artifacts --output type=local,dest=build-artifacts/static-linux-arm64 . +``` + +Requires Docker Buildx/BuildKit. If Docker Hub is unavailable, add +`--build-arg ALPINE_IMAGE=public.ecr.aws/docker/library/alpine:3.24`. +An alternate package mirror can be selected with `--build-arg ALPINE_MIRROR=https://mirrors.aliyun.com/alpine`; APK signature checks remain enabled. +Docker build proxy settings apply to package installation; a broken configured +proxy can be bypassed for this command with empty `HTTP_PROXY`, `HTTPS_PROXY`, +`ALL_PROXY` build arguments and their lowercase equivalents. + +Use `--platform linux/amd64` and a different destination for x86-64. Docker needs +a matching native builder or CPU emulation to compile and run the tests for a +foreign architecture. Add `--build-arg SS_MINIMAL=ON` for the minimal profile. + +Build the scratch runtime image using the same cached build stage: + +```sh +docker build --platform linux/arm64 -f docker/static/Dockerfile \ + --target runtime -t shadowsocks-libev:static-arm64 . +docker run --rm -p 8388:8388/tcp -p 8388:8388/udp \ + shadowsocks-libev:static-arm64 \ + -s 0.0.0.0 -p 8388 -k example-password -m aes-256-gcm -u +``` + +The runtime image has no shell or external plugin executables. Supply any SIP003 +plugin and its own runtime requirements separately. Configuration files can be +bind-mounted and passed with `-c`. Exported binaries run on Linux with the same +CPU architecture; they do not run directly on macOS or Windows. diff --git a/docs/measurements/baseline-binaries-macos-arm64.json b/docs/measurements/baseline-binaries-macos-arm64.json new file mode 100644 index 000000000..f36664aea --- /dev/null +++ b/docs/measurements/baseline-binaries-macos-arm64.json @@ -0,0 +1,42 @@ +{ + "commit": "8fe386b5c270fc34d2bf53ef5ffb93d93242fac2", + "platform": "Darwin arm64", + "binaries": { + "ss-local": { + "bytes": 295912, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/opt/homebrew/opt/libev/lib/libev.4.dylib (compatibility version 5.0.0, current version 5.0.0)", + "\t/opt/homebrew/opt/c-ares/lib/libcares.2.dylib (compatibility version 2.0.0, current version 2.19.5)", + "\t/usr/lib/libpcre2-8.0.dylib (compatibility version 12.0.0, current version 12.2.0)", + "\t/opt/homebrew/opt/libsodium/lib/libsodium.26.dylib (compatibility version 31.0.0, current version 31.0.0)", + "\t/opt/homebrew/opt/mbedtls@3/lib/libmbedtls.21.dylib (compatibility version 21.0.0, current version 3.6.6)", + "\t/opt/homebrew/opt/mbedtls@3/lib/libmbedcrypto.16.dylib (compatibility version 16.0.0, current version 3.6.6)" + ] + }, + "ss-server": { + "bytes": 314360, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/opt/homebrew/opt/libev/lib/libev.4.dylib (compatibility version 5.0.0, current version 5.0.0)", + "\t/opt/homebrew/opt/c-ares/lib/libcares.2.dylib (compatibility version 2.0.0, current version 2.19.5)", + "\t/usr/lib/libpcre2-8.0.dylib (compatibility version 12.0.0, current version 12.2.0)", + "\t/opt/homebrew/opt/libsodium/lib/libsodium.26.dylib (compatibility version 31.0.0, current version 31.0.0)", + "\t/opt/homebrew/opt/mbedtls@3/lib/libmbedtls.21.dylib (compatibility version 21.0.0, current version 3.6.6)", + "\t/opt/homebrew/opt/mbedtls@3/lib/libmbedcrypto.16.dylib (compatibility version 16.0.0, current version 3.6.6)" + ] + }, + "ss-tunnel": { + "bytes": 268680, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/opt/homebrew/opt/libev/lib/libev.4.dylib (compatibility version 5.0.0, current version 5.0.0)", + "\t/opt/homebrew/opt/c-ares/lib/libcares.2.dylib (compatibility version 2.0.0, current version 2.19.5)", + "\t/usr/lib/libpcre2-8.0.dylib (compatibility version 12.0.0, current version 12.2.0)", + "\t/opt/homebrew/opt/libsodium/lib/libsodium.26.dylib (compatibility version 31.0.0, current version 31.0.0)", + "\t/opt/homebrew/opt/mbedtls@3/lib/libmbedtls.21.dylib (compatibility version 21.0.0, current version 3.6.6)", + "\t/opt/homebrew/opt/mbedtls@3/lib/libmbedcrypto.16.dylib (compatibility version 16.0.0, current version 3.6.6)" + ] + } + } +} diff --git a/docs/measurements/baseline-static-binaries-macos-arm64.json b/docs/measurements/baseline-static-binaries-macos-arm64.json new file mode 100644 index 000000000..868cb000f --- /dev/null +++ b/docs/measurements/baseline-static-binaries-macos-arm64.json @@ -0,0 +1,24 @@ +{ + "commit": "8fe386b5c270fc34d2bf53ef5ffb93d93242fac2", + "platform": "Darwin arm64", + "binaries": { + "ss-local": { + "bytes": 1326328, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)" + ] + }, + "ss-server": { + "bytes": 1500968, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)" + ] + }, + "ss-tunnel": { + "bytes": 828712, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)" + ] + } + } +} diff --git a/docs/measurements/baseline-stress-macos-arm64.json b/docs/measurements/baseline-stress-macos-arm64.json new file mode 100644 index 000000000..5d3c93ec6 --- /dev/null +++ b/docs/measurements/baseline-stress-macos-arm64.json @@ -0,0 +1,101 @@ +[ + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 7063.43292556653, + "duration_sec": 0.11876106262207031, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2656, + "server_rss_after_kb": 3472, + "tunnel_rss_before_kb": 2400, + "tunnel_rss_after_kb": 2576, + "error": null + }, + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 7001.809356509003, + "duration_sec": 0.11980628967285156, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2704, + "server_rss_after_kb": 3504, + "tunnel_rss_before_kb": 2400, + "tunnel_rss_after_kb": 2576, + "error": null + }, + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 6698.608105647417, + "duration_sec": 0.1252291202545166, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2688, + "server_rss_after_kb": 3488, + "tunnel_rss_before_kb": 2384, + "tunnel_rss_after_kb": 2560, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 7690.119291065227, + "duration_sec": 0.10908293724060059, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2704, + "server_rss_after_kb": 3472, + "tunnel_rss_before_kb": 2400, + "tunnel_rss_after_kb": 2576, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 7890.252575300613, + "duration_sec": 0.10631608963012695, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2672, + "server_rss_after_kb": 3488, + "tunnel_rss_before_kb": 2400, + "tunnel_rss_after_kb": 2576, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 7775.56902641376, + "duration_sec": 0.10788416862487793, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2704, + "server_rss_after_kb": 3520, + "tunnel_rss_before_kb": 2400, + "tunnel_rss_after_kb": 2576, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 3438.7140620682108, + "duration_sec": 0.24394607543945312, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2656, + "server_rss_after_kb": 3552, + "tunnel_rss_before_kb": 2384, + "tunnel_rss_after_kb": 2592, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 3471.04357984401, + "duration_sec": 0.24167394638061523, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2656, + "server_rss_after_kb": 3408, + "tunnel_rss_before_kb": 2368, + "tunnel_rss_after_kb": 2576, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 3387.1740528144805, + "duration_sec": 0.24765801429748535, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2656, + "server_rss_after_kb": 3472, + "tunnel_rss_before_kb": 2384, + "tunnel_rss_after_kb": 2592, + "error": null + } +] \ No newline at end of file diff --git a/docs/measurements/baseline-udp-macos-arm64.json b/docs/measurements/baseline-udp-macos-arm64.json new file mode 100644 index 000000000..d4a61d10c --- /dev/null +++ b/docs/measurements/baseline-udp-macos-arm64.json @@ -0,0 +1,74 @@ +[ + { + "cipher": "aes-128-gcm", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.25436816699948395, + "payload_mbps": 75.4811430474276 + }, + { + "cipher": "aes-128-gcm", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2318156250003085, + "payload_mbps": 82.8244429165396 + }, + { + "cipher": "aes-128-gcm", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.24796225000136474, + "payload_mbps": 77.43114123175735 + }, + { + "cipher": "aes-256-gcm", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2386028750006517, + "payload_mbps": 80.46843526067134 + }, + { + "cipher": "aes-256-gcm", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2360522500002844, + "payload_mbps": 81.33792412475148 + }, + { + "cipher": "aes-256-gcm", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.23466012499920907, + "payload_mbps": 81.82046268007704 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.24850704099844734, + "payload_mbps": 77.26139236481416 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.24412816600124643, + "payload_mbps": 78.64721352923273 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.24165658300080395, + "payload_mbps": 79.45159102053563 + } +] diff --git a/docs/measurements/bundled-binaries-macos-arm64.json b/docs/measurements/bundled-binaries-macos-arm64.json new file mode 100644 index 000000000..d7e2a17b3 --- /dev/null +++ b/docs/measurements/bundled-binaries-macos-arm64.json @@ -0,0 +1,27 @@ +{ + "commit": "46d3acd9d6b72e088b688c2f84aae5c0466885d8", + "platform": "Darwin arm64", + "binaries": { + "ss-local": { + "bytes": 756920, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)" + ] + }, + "ss-server": { + "bytes": 962632, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)" + ] + }, + "ss-tunnel": { + "bytes": 416440, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)" + ] + } + } +} diff --git a/docs/measurements/bundled-stress-macos-arm64.json b/docs/measurements/bundled-stress-macos-arm64.json new file mode 100644 index 000000000..b8d0c92a5 --- /dev/null +++ b/docs/measurements/bundled-stress-macos-arm64.json @@ -0,0 +1,101 @@ +[ + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 7860.532450008042, + "duration_sec": 0.10671806335449219, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2288, + "server_rss_after_kb": 3072, + "tunnel_rss_before_kb": 1872, + "tunnel_rss_after_kb": 2016, + "error": null + }, + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 6582.251473027347, + "duration_sec": 0.1274428367614746, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2256, + "server_rss_after_kb": 3040, + "tunnel_rss_before_kb": 1872, + "tunnel_rss_after_kb": 2032, + "error": null + }, + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 7002.575801490699, + "duration_sec": 0.11979317665100098, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2256, + "server_rss_after_kb": 3008, + "tunnel_rss_before_kb": 1872, + "tunnel_rss_after_kb": 2032, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 8336.501684349796, + "duration_sec": 0.10062503814697266, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2256, + "server_rss_after_kb": 2768, + "tunnel_rss_before_kb": 1872, + "tunnel_rss_after_kb": 2016, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 8556.573148352736, + "duration_sec": 0.0980370044708252, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2256, + "server_rss_after_kb": 2992, + "tunnel_rss_before_kb": 1872, + "tunnel_rss_after_kb": 2032, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 8498.291637500875, + "duration_sec": 0.0987093448638916, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2288, + "server_rss_after_kb": 3072, + "tunnel_rss_before_kb": 1872, + "tunnel_rss_after_kb": 2016, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 3276.7750490181143, + "duration_sec": 0.25600194931030273, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2272, + "server_rss_after_kb": 3072, + "tunnel_rss_before_kb": 1856, + "tunnel_rss_after_kb": 2000, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 3267.788061223197, + "duration_sec": 0.25670599937438965, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2272, + "server_rss_after_kb": 3072, + "tunnel_rss_before_kb": 1840, + "tunnel_rss_after_kb": 1984, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 2988.673848516679, + "duration_sec": 0.28067994117736816, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2272, + "server_rss_after_kb": 3040, + "tunnel_rss_before_kb": 1856, + "tunnel_rss_after_kb": 2032, + "error": null + } +] \ No newline at end of file diff --git a/docs/measurements/bundled-udp-macos-arm64.json b/docs/measurements/bundled-udp-macos-arm64.json new file mode 100644 index 000000000..495a39776 --- /dev/null +++ b/docs/measurements/bundled-udp-macos-arm64.json @@ -0,0 +1,74 @@ +[ + { + "cipher": "aes-128-gcm", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.25596854199829977, + "payload_mbps": 75.00921734408884 + }, + { + "cipher": "aes-128-gcm", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.25268954199964355, + "payload_mbps": 75.98256678160065 + }, + { + "cipher": "aes-128-gcm", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2656113329994696, + "payload_mbps": 72.28607222131723 + }, + { + "cipher": "aes-256-gcm", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.31145487499998126, + "payload_mbps": 61.6461694491093 + }, + { + "cipher": "aes-256-gcm", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.30316929199943843, + "payload_mbps": 63.3309523974993 + }, + { + "cipher": "aes-256-gcm", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.281550084000628, + "payload_mbps": 68.1939061327262 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.29268420900007186, + "payload_mbps": 65.59971262404282 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.282807709001645, + "payload_mbps": 67.89065286720428 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.25814837500001886, + "payload_mbps": 74.37583134117578 + } +] diff --git a/docs/measurements/minimal-binaries-macos-arm64.json b/docs/measurements/minimal-binaries-macos-arm64.json new file mode 100644 index 000000000..1a57434c6 --- /dev/null +++ b/docs/measurements/minimal-binaries-macos-arm64.json @@ -0,0 +1,27 @@ +{ + "commit": "46d3acd9d6b72e088b688c2f84aae5c0466885d8", + "platform": "Darwin arm64", + "binaries": { + "ss-local": { + "bytes": 416424, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)" + ] + }, + "ss-server": { + "bytes": 622024, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)" + ] + }, + "ss-tunnel": { + "bytes": 397704, + "dependencies": [ + "\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)", + "\t/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)" + ] + } + } +} diff --git a/docs/modernization.md b/docs/modernization.md new file mode 100644 index 000000000..4a9aaad79 --- /dev/null +++ b/docs/modernization.md @@ -0,0 +1,165 @@ +# Self-contained C modernization + +Status: complete for the agreed modernization scope. Native MSVC remains an +explicitly deferred milestone. Implementation branch: feature/self-contained-portable-c; +review: https://github.com/shadowsocks/shadowsocks-libev/pull/3050 (draft). + +## Compatibility contract + +Keep C11 and the existing CLI, JSON configuration, ss:// parsing, SIP003 plugins, +ACL blacklist/whitelist/outbound precedence, public shadowsocks.h API, legacy +stream ciphers, AEAD and SIP022 TCP/UDP behavior in the compatibility profile. +Removing regex or legacy ciphers is explicit opt-in, with configuration errors +for unavailable functionality. Linux redirection/netfilter remains Linux-only. + +## Work and evidence + +- [x] Baseline: fresh rebuild, unit/integration results, binary/dependency sizes, + TCP and UDP throughput, and memory measurements. +- [x] Required independent interoperability including TCP and UDP; missing peers + fail the required CI job and are reported as skips in optional local runs. +- [x] Target-scoped CMake, separate programs/static/shared library options, + reliable dependency discovery, installation/export metadata and presets. +- [x] Offline bundled dependency mode and source release archive; explicit system + mode for distro packages; checksums, licenses, versions, update procedure. +- [x] Remove libcork: address parsing, lists, hash maps, subprocess lifecycle. +- [x] Remove libipset: bounded IPv4/IPv6 prefix storage with insertion/deletion; + differential tests including holes inside larger networks. +- [x] Vendor the small bloom filter and remove all three submodule requirements. +- [x] Portability layer for sockets/errors, I/O, clocks, randomness and processes. +- [x] Runtime CI: Linux glibc/musl, macOS, FreeBSD, Windows MinGW. +- [x] Document MSVC/event-loop compatibility assessment as a later milestone. +- [x] Minimal profile: no regex/plugins/manager/legacy stream ciphers. +- [x] Crypto-only Mbed TLS configuration and evidence-backed provider assessment. +- [x] Complete sanitizers/static analysis, compatibility and stress validation, + release archive offline build, installed consumer, before/after report. + +## Baseline provenance + +Base commit: 8fe386b. Its libcork checkout is 074e074b, different from the +superproject gitlink; this was pre-existing local state. That checkout, libipset +and libbloom are preserved and ignored locally. Fresh baseline builds use those +same checkouts, with no modernization changes applied to baseline source. +The fresh baseline passes all 13 original unit tests. + +## Build interface + +Requires CMake 3.20+, a C11 compiler, and a build executor. Presets use Ninja; +manual configuration may use another generator. + +``` +cmake --preset system +cmake --build --preset system +ctest --preset system +``` + +Programs are in build-system/bin for either linkage mode. WITH_STATIC selects +static third-party dependencies; it does not promise a static libc or OS runtime. +SS_BUILD_EXECUTABLES, SS_BUILD_STATIC_LIBRARY and SS_BUILD_SHARED_LIBRARY control +outputs independently. Docs and platform shell tools are explicit opt-ins. +Bundled mode is the default and the `bundled` / `minimal` presets exercise it. +Maintainers stage the intended source files, then run +`scripts/source_archive.py build-artifacts/shadowsocks-libev.tar.gz`. +The archive includes tracked working-tree contents (so uncommitted changes are +included); record the final commit with a published release and use a clean +checkout for publishing. No submodules are accepted in the archive. + +## Platform validation + +Implementation validation at commit cdb192a passes the full platform matrix: +Linux glibc x86-64/ARM64 and macOS full/minimal builds, real TCP/UDP relay, SIP003 +cleanup (full profile), and relocated installed consumers. FreeBSD 14.3 passes +compilation, unit/vendor tests, real TCP/UDP relay and installed consumers. +Native Windows UCRT64 passes all 31 unit/vendor tests, six-method TCP/UDP relay, +and installed static/shared consumers. Relay and consumer execution excludes +MSYS2/toolchain DLL directories from PATH; binaries require only Windows system +DLLs. Zig cross-builds also include both libraries and installed consumers. + +Alpine/musl builds the release archive with networking and Python discovery +disabled and passes all unit/vendor and TCP/UDP tests. Linux Valgrind, ASan/UBSan, +coverage, required shadowsocks-rust interoperability, clang-tidy-18, and Debian +package construction pass. Evidence: [platform matrix](https://github.com/shadowsocks/shadowsocks-libev/actions/runs/34505389620), +[tests and analysis](https://github.com/shadowsocks/shadowsocks-libev/actions/runs/34505389548), +[system builds and packaging](https://github.com/shadowsocks/shadowsocks-libev/actions/runs/34505389561). + +The same committed source archive also builds locally on Linux arm64 with +networking and Python discovery disabled, passing all 31 unit/vendor tests and +six-method TCP/UDP plus SIP003 tests. The source release itself needs no Git or +Python to compile. Executable-only installation omits library metadata, and a +minimal static-library-only build succeeds with the PCRE2 archive removed. +The final documentation commit is rechecked by the same CI matrix before delivery. + +## Compatibility details + +Address classification now uses inet_pton consistently with socket conversion; +malformed dotted addresses previously accepted by libcork but rejected by +inet_pton are no longer classified as IP literals. Full and suffix ACL rules +are explicit additions; normal regex remains available by default. +Legacy methods unavailable in Mbed TLS 3 (such as RC4) are not resurrected. +Crypto providers remain libsodium plus the reduced Mbed TLS primitive set: +AES-128/192-GCM, software AES fallback, MD5/SHA1 compatibility derivation, and +legacy AES/Camellia stream modes prevent a transparent libsodium-only switch. +See https://doc.libsodium.org/secret-key_cryptography/aead/aes-256-gcm and +https://shadowsocks.org/doc/sip022.html for the provider and protocol contracts. + +## Local validation + +- Fresh base-commit build (8fe386b) passes all 13 original unit tests, using the + preserved dependency checkouts described above. Both its system-shared and + static-dependency programs were rebuilt for size comparisons. +- macOS full bundled: 31 tests; minimal: 29 tests. System dependencies: 17 tests. +- ASan + UBSan: all 17 project tests and six-method TCP/UDP SIP003 tests pass. +- Clang-tidy: zero warnings/errors with Homebrew LLVM; project compiler warnings + remain errors. CI additionally uses its existing pinned clang-tidy-18 check. +- IP-set differential test: 1,028,000 comparisons against the original libipset, + including randomized full-width IPv4/IPv6 insertion/deletion and membership. +- Linux x86-64 musl: 31 unit/vendor tests and all six TCP/UDP relay cases pass + in containers with networking disabled. The preinstalled cross compiler + intermittently crashed under CPU emulation; incremental retries completed. +- Installed bundled static/shared consumers run after relocation. Installed + system-mode static/shared consumers also link and run. System static exports + require the exact Mbed TLS version used at build time to avoid an ABI mismatch. +- Windows x86-64: programs, both libraries, all tests and installed consumers + cross-compile with Zig. Native UCRT64 execution and installed consumers pass + with runtime DLL lookup restricted to the installation and Windows system paths. +- SIP003 fixture: real TCP forwarding, UDP bypass, and child cleanup pass through + the actual programs. DNS cancellation covers outstanding A/AAAA requests, + exactly-once callbacks/free callbacks and reinitialization after shutdown. + +### MSVC assessment + +MSVC is an explicitly deferred milestone, not a supported build today. MinGW-w64 +supplies the POSIX compatibility headers/functions (`unistd.h`, `getopt`, +`ssize_t`, string helpers) used throughout the existing CLI. Native MSVC needs +those interfaces isolated, compiler-specific flags in the libsodium adapter, +libev/Win32 build verification and export/import validation. CMake now rejects +MSVC early with the supported UCRT64/Zig alternatives. Removing the event loop +or inventing replacement crypto implementations is outside this modernization. + +### Crypto provider decision + +Retain libsodium plus Mbed TLS crypto primitives. The bundled Mbed TLS config +omits TLS, X.509, certificate parsing and handshake machinery; it retains only +cipher/hash primitives needed by protocol compatibility. Libsodium alone cannot +replace AES-128-GCM, software AES fallback, MD5/SHA1 compatibility derivation and +legacy stream modes. PCRE2 is absent in the minimal profile. This removes three +submodule dependencies and reduces what is built without replacing audited +cryptographic algorithms with project-specific implementations. + +See [measurements and tradeoffs](performance.md) for reproducible local results. + +## Static build CI + +The `static-macos` and `static-windows` jobs use Clang, bundled dependencies and +`SS_BUILD_SHARED_LIBRARY=OFF`. They run unit/vendor tests, relocate the installed +static library, build its consumer, and exercise the installed TCP/UDP programs. +The macOS job also exercises SIP003. `tests/check_static_runtime.py` rejects +non-system dylibs and DLLs (including Windows delay imports) in every installed +program and the static consumer. Windows runtime tests exclude toolchain DLL +paths. Windows uses the native [MSYS2 CLANG64 environment](https://www.msys2.org/docs/environments/). + +macOS and Windows retain their OS libraries; static builds on these platforms +mean no separately distributed project, crypto, event-loop or toolchain shared +libraries. The `static-linux` job builds the Clang/musl Dockerfile, which also +links libc statically and rejects ELF interpreters and `NEEDED` entries. All +three jobs publish the tested installations as workflow artifacts. diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 000000000..b2f3d1ba5 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,76 @@ +# Modernization measurements + +Local macOS arm64 results, 2026-09-10. Base commit: `8fe386b`; comparison: +`feature/self-contained-portable-c`. Raw JSON lives in [measurements](measurements/). +These are short loopback measurements on one machine, not capacity guarantees. + +## Size and runtime dependencies + +Unstripped Release executable sizes in bytes: + +| Program | Old system-shared | Old static dependencies | Bundled | Minimal | +|---|---:|---:|---:|---:| +| ss-local | 295,912 | 1,326,328 | 756,920 | 416,424 | +| ss-server | 314,360 | 1,500,968 | 962,632 | 622,024 | +| ss-tunnel | 268,680 | 828,712 | 416,440 | 397,704 | + +Bundled programs link only macOS OS libraries (`libSystem`, and where needed +`libresolv`). They need no separately installed libev, c-ares, PCRE2, libsodium +or Mbed TLS. Compared with the old static-dependency build, ss-server shrinks +about 36%; minimal shrinks about 59%. Compared with a shared-dependency binary +alone, bundling makes the executable larger; that comparison excludes the old +libraries' disk footprint. Bundled source archives add roughly 12 MB to checkout +and release size. There is no configure-time network access or submodule fetch. + +## TCP and resident memory + +Median of three 100 MiB transfers using `tests/stress_test.py`, old shared +dependencies versus bundled. Both sets pass all nine transfers. + +| Cipher | Before Mbps | Bundled Mbps | Before server RSS after, KiB | Bundled server RSS after, KiB | +|---|---:|---:|---:|---:| +| AES-128-GCM | 7,002 | 7,003 | 3,488 | 3,040 | +| AES-256-GCM | 7,776 | 8,498 | 3,488 | 2,992 | +| ChaCha20-IETF-Poly1305 | 3,439 | 3,268 | 3,472 | 3,072 | + +Bundled tunnel RSS after transfer was about 2,000–2,032 KiB versus +2,576–2,592 KiB before. RSS includes OS accounting and shared-page effects; +this is not a proof of bounded memory under arbitrary workloads. Transfer times +are short (roughly 0.1–0.3 s), so the apparent AES-256 improvement and ChaCha +regression need longer controlled measurements before drawing performance claims. + +## UDP + +Median of three runs, 2,000 sequential 1,200-byte echo packets per run through +SOCKS5 UDP ASSOCIATE. Every packet's payload and relay source port are checked. + +| Cipher | Before payload Mbps | Bundled payload Mbps | +|---|---:|---:| +| AES-128-GCM | 77.43 | 75.01 | +| AES-256-GCM | 81.34 | 63.33 | +| ChaCha20-IETF-Poly1305 | 78.65 | 67.89 | + +This round-trip benchmark includes Python thread creation, scheduling and SOCKS +overhead. It shows lower observed UDP rates in this run, up to 22%; modernization +is not claimed to improve UDP throughput. The portable libsodium backend is a +maintenance/performance tradeoff. Optimized backends require separate compiler +probes and cross-platform known-answer tests before adoption. + +Independently of these timings, the new interoperability test exposed the old +MTU-sized receive buffer truncating 4 KiB UDP payloads. Full datagram reception +now passes 1, 128, 1,200 and 4,096-byte interoperability cases against +shadowsocks-rust in both directions. + +## Reproduce + +```sh +python3 tests/stress_test.py --bin build-bundled/bin --size 100 --repeat 3 --json tcp.json +python3 tests/measure_udp.py --bin build-bundled/bin --output udp.json +python3 tests/interop.py --bin build-bundled/bin +python3 tests/interop.py --self --bin build-bundled/bin --plugin tests/sip003_fixture.py +``` + +Run comparisons serially. The baseline was rebuilt from its own worktree using +the preserved original dependency checkouts; no modernization fixes were applied +to that source. The historical curl interop harness obeyed `no_proxy` and could +bypass SOCKS, so its old results are excluded from compatibility evidence. diff --git a/libbloom b/libbloom deleted file mode 160000 index 437e1add5..000000000 --- a/libbloom +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 437e1add5a2b9a87797d8c648df7cf5f3ee155a8 diff --git a/libcork b/libcork deleted file mode 160000 index 5ed3f8371..000000000 --- a/libcork +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5ed3f837171fc50614f579c0571365914e6c3ab9 diff --git a/libipset b/libipset deleted file mode 160000 index 3ea7fe30a..000000000 --- a/libipset +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3ea7fe30adf4b39b27d932e5a70a2ddce4adb508 diff --git a/scripts/git_archive.sh b/scripts/git_archive.sh index aa932df06..5b365b99f 100755 --- a/scripts/git_archive.sh +++ b/scripts/git_archive.sh @@ -1,44 +1,14 @@ #!/usr/bin/env bash -set -e - -archive() { - export TARBALL_NAME=$1 - export TARBALL_OUTDIR=$2 - - # archive this repo - cd "$(git rev-parse --show-toplevel)" - git archive HEAD --format=tar --prefix="${TARBALL_NAME}/" \ - -o "${TARBALL_OUTDIR}/${TARBALL_NAME}.tar" - # archive submodules - git submodule update --init - git submodule foreach --quiet 'git archive HEAD --format=tar \ - --prefix="${TARBALL_NAME}/${path}/" \ - -o "${TARBALL_OUTDIR}/${TARBALL_NAME}-submodule-${path}-${sha1}.tar" - tar -n --concatenate --file="${TARBALL_OUTDIR}/${TARBALL_NAME}.tar" \ - "${TARBALL_OUTDIR}/${TARBALL_NAME}-submodule-${path}-${sha1}.tar"' - gzip -c "${TARBALL_OUTDIR}/${TARBALL_NAME}.tar" > "${TARBALL_OUTDIR}/${TARBALL_NAME}.tar.gz" - - # clean-up - git submodule foreach --quiet 'rm ${TARBALL_OUTDIR}/${TARBALL_NAME}-submodule-${path}-${sha1}.tar' - rm "${TARBALL_OUTDIR}/${TARBALL_NAME}.tar" -} - -TARGET_TARBALL_NAME=shadowsocks-libev -TARGET_TARBALL_DIR=$(git rev-parse --show-toplevel) - -while getopts "n:o:" opt -do - case ${opt} in - o) - TARGET_TARBALL_DIR=$(readlink -f -- $OPTARG) - ;; - n) - TARGET_TARBALL_NAME=$OPTARG - ;; - \?) - exit 1 - ;; +# Backward-compatible maintainer entrypoint for the offline release archive. +set -euo pipefail +root=$(git rev-parse --show-toplevel) +name=shadowsocks-libev +directory=$root +while getopts "n:o:" opt; do + case "$opt" in + n) name=$OPTARG ;; + o) directory=$OPTARG ;; + *) exit 1 ;; esac done - -archive "${TARGET_TARBALL_NAME}" "${TARGET_TARBALL_DIR}" +exec python3 "$root/scripts/source_archive.py" "$directory/$name.tar.gz" --prefix "$name" diff --git a/scripts/source_archive.py b/scripts/source_archive.py new file mode 100755 index 000000000..dd2e2fb03 --- /dev/null +++ b/scripts/source_archive.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Create an offline source archive from Git's tracked working-tree files. + +Maintainer-only tool; building the resulting archive needs neither Git nor +Python (unless integration tests or generated documentation are requested). +""" +import argparse +import gzip +import io +import os +from pathlib import Path +import subprocess +import tarfile + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("output", type=Path) + parser.add_argument("--prefix", default="shadowsocks-libev-3.3.6") + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + if "/" in args.prefix or args.prefix in ("", ".", ".."): + parser.error("prefix must be a single directory name") + epoch = int(os.environ.get("SOURCE_DATE_EPOCH") or subprocess.check_output( + ["git", "show", "-s", "--format=%ct", "HEAD"], cwd=root, text=True).strip()) + entries = subprocess.check_output(["git", "ls-files", "--stage", "-z"], cwd=root).split(b"\0") + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open("wb") as raw, gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=epoch) as compressed: + with tarfile.open(fileobj=compressed, mode="w|") as archive: + for entry in entries: + if not entry: + continue + metadata, encoded = entry.split(b"\t", 1) + mode, _, stage = metadata.split() + if stage != b"0" or mode == b"160000": + raise RuntimeError("Unmerged entry or submodule in source archive") + relative = os.fsdecode(encoded) + path = root / relative + info = tarfile.TarInfo(args.prefix + "/" + relative) + info.mtime = epoch + info.mode = int(mode, 8) & 0o777 + if path.is_symlink(): + info.type = tarfile.SYMTYPE + info.linkname = os.readlink(path) + archive.addfile(info) + else: + content = path.read_bytes() + info.size = len(content) + archive.addfile(info, io.BytesIO(content)) + print(args.output) + + +if __name__ == "__main__": + main() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4aab087ce..32685e04f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,7 @@ endif () set(SS_ACL_SOURCE acl.c + ipset.c rule.c ) @@ -18,7 +19,6 @@ set(SS_CRYPTO_SOURCE aead.c aead2022.c aead2022_udp.c - stream.c base64.c ) @@ -39,9 +39,14 @@ target_include_directories(blake3 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_options(blake3 PRIVATE -Wno-error -Wno-unused-function) set_target_properties(blake3 PROPERTIES POSITION_INDEPENDENT_CODE ON) -set(SS_PLUGIN_SOURCE - plugin.c - ) +if(SS_ENABLE_LEGACY) + list(APPEND SS_CRYPTO_SOURCE stream.c) +endif() +if(SS_ENABLE_PLUGINS) + set(SS_PLUGIN_SOURCE plugin.c process.c) +else() + set(SS_PLUGIN_SOURCE plugin_disabled.c) +endif() set(SS_SHARED_SOURCES ppbloom.c @@ -104,243 +109,63 @@ set(SS_REDIR_SOURCE ${SS_PLUGIN_SOURCE} ) -# Apply -Werror only to project sources (not bundled submodules) -set(SS_WERROR_FLAGS -Werror) - -# Include directories from Find modules -# MbedTLS must come first to avoid conflicts with other mbedtls versions -# that may be in /opt/homebrew/include (e.g. mbedtls 4.x vs 3.x) -include_directories(BEFORE ${MBEDTLS_INCLUDE_DIRS}) -include_directories(${SODIUM_INCLUDE_DIRS}) -include_directories(${PCRE2_INCLUDE_DIRS}) -include_directories(${CARES_INCLUDE_DIRS}) - -# Extract mbedTLS lib dir to ensure static/shared libs come from same version -get_filename_component(_MBEDTLS_LIB_DIR "${MBEDTLS_CRYPTO_LIBRARY}" DIRECTORY) - -if (WITH_STATIC) -find_library(LIBSODIUM libsodium.a) -find_library(LIBMBEDTLS libmbedtls.a HINTS ${_MBEDTLS_LIB_DIR} NO_DEFAULT_PATH) -find_library(LIBMBEDTLS libmbedtls.a) -find_library(LIBMBEDCRYPTO libmbedcrypto.a HINTS ${_MBEDTLS_LIB_DIR} NO_DEFAULT_PATH) -find_library(LIBMBEDCRYPTO libmbedcrypto.a) -find_library(LIBEV libev.a) -find_library(LIBUDNS NAMES libcares.a libcares_static.a) -find_library(LIBPCRE2 libpcre2-8.a) - -# Dependencies we need for static and shared -list(APPEND DEPS - m - blake3 - bloom - ${LIBEV} - ${LIBUDNS} - ${LIBPCRE2} - ${LIBSODIUM} - ${LIBMBEDTLS} - ${LIBMBEDCRYPTO} - ) - -if (MINGW) -list(APPEND DEPS ws2_32 iphlpapi) -add_compile_definitions(CARES_STATICLIB PCRE2_STATIC) -endif () -endif () - -find_library(LIBSODIUM_SHARED sodium) -find_library(LIBMBEDTLS_SHARED mbedtls HINTS ${_MBEDTLS_LIB_DIR} NO_DEFAULT_PATH) -find_library(LIBMBEDTLS_SHARED mbedtls) -find_library(LIBMBEDCRYPTO_SHARED mbedcrypto HINTS ${_MBEDTLS_LIB_DIR} NO_DEFAULT_PATH) -find_library(LIBMBEDCRYPTO_SHARED mbedcrypto) -find_library(LIBEV_SHARED ev) -find_library(LIBUDNS_SHARED cares) -find_library(LIBPCRE2_SHARED pcre2-8) -if (WITH_EMBEDDED_SRC) -list(APPEND DEPS_SHARED - m - blake3 - bloom - cork - ipset - ${LIBEV_SHARED} - ${LIBUDNS_SHARED} - ${LIBPCRE2_SHARED} - ${LIBSODIUM_SHARED} - ${LIBMBEDTLS_SHARED} - ${LIBMBEDCRYPTO_SHARED} - ) -else () -find_library(LIBBLOOM_SHARED bloom) -find_library(LIBCORK_SHARED cork) -find_library(LIBCORKIPSET_SHARED corkipset) -list(APPEND DEPS_SHARED - m - blake3 - ${LIBBLOOM_SHARED} - ${LIBCORK_SHARED} - ${LIBCORKIPSET_SHARED} - ${LIBEV_SHARED} - ${LIBUDNS_SHARED} - ${LIBPCRE2_SHARED} - ${LIBSODIUM_SHARED} - ${LIBMBEDTLS_SHARED} - ${LIBMBEDCRYPTO_SHARED} - ) -endif () - -# Connmarktos/nftables libraries for ss-server -if(ENABLE_CONNMARKTOS) - list(APPEND DEPS_CONNMARKTOS ${NETFILTER_CONNTRACK_LIB}) - if(NFNETLINK_LIB) - list(APPEND DEPS_CONNMARKTOS ${NFNETLINK_LIB}) - endif() +set(SS_CORE_DEPS ss_ev ss_cares ss_sodium ss_mbedcrypto blake3 bloom Threads::Threads ${SS_MATH_LIBRARY}) +set(SS_ACL_DEPS) +if(SS_ENABLE_REGEX) + list(APPEND SS_ACL_DEPS ss_pcre2) endif() -if(ENABLE_NFTABLES) - list(APPEND DEPS_NFTABLES ${MNL_LIB} ${NFTNL_LIB}) -endif() - -find_package (Threads) - -if (WITH_STATIC) -# ------------------------------------------------------------------ -# Static -# By default we use normal name for static, all shared targets will add a `-shared' suffix -add_executable(ss-server ${SS_SERVER_SOURCE}) -add_executable(ss-tunnel ${SS_TUNNEL_SOURCE}) -if (WITH_SS_MANAGER) - add_executable(ss-manager ${SS_MANAGER_SOURCE}) -else () - add_executable(ss-manager EXCLUDE_FROM_ALL ${SS_MANAGER_SOURCE}) -endif () -add_executable(ss-local ${SS_LOCAL_SOURCE}) -if (WITH_SS_REDIR) - add_executable(ss-redir ${SS_REDIR_SOURCE}) -else () - add_executable(ss-redir EXCLUDE_FROM_ALL ${SS_REDIR_SOURCE}) -endif () -add_library(shadowsocks-libev STATIC ${LIBSHADOWSOCKS_LIBEV_SOURCE}) - - -target_compile_definitions(ss-server PUBLIC -DMODULE_REMOTE) -target_compile_definitions(ss-tunnel PUBLIC -DMODULE_TUNNEL) -target_compile_definitions(ss-manager PUBLIC -DMODULE_MANAGER) -target_compile_definitions(ss-local PUBLIC -DMODULE_LOCAL) -target_compile_definitions(ss-redir PUBLIC -DMODULE_REDIR) -target_compile_definitions(shadowsocks-libev PUBLIC -DMODULE_LOCAL -DLIB_ONLY) - -target_compile_options(ss-server PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(ss-tunnel PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(ss-manager PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(ss-local PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(ss-redir PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(shadowsocks-libev PRIVATE ${SS_WERROR_FLAGS}) - -# Connmarktos/nftables compile definitions if(ENABLE_CONNMARKTOS) - target_compile_definitions(ss-server PUBLIC -DUSE_NFCONNTRACK_TOS) + list(APPEND SS_SERVER_DEPS ${NETFILTER_CONNTRACK_LIB} ${NFNETLINK_LIB}) endif() if(ENABLE_NFTABLES) - target_compile_definitions(ss-server PUBLIC -DUSE_NFTABLES) + list(APPEND SS_SERVER_DEPS ${MNL_LIB} ${NFTNL_LIB}) endif() -target_include_directories(shadowsocks-libev PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -target_link_libraries(ss-server cork ipset ${DEPS} ${DEPS_CONNMARKTOS} ${DEPS_NFTABLES}) -target_link_libraries(ss-tunnel cork ${DEPS}) -target_link_libraries(ss-manager m bloom cork ${LIBEV} ${LIBUDNS}) -target_link_libraries(ss-local cork ipset ${DEPS}) -target_link_libraries(ss-redir cork ipset ${DEPS}) -target_link_libraries(shadowsocks-libev cork ipset ${DEPS}) -endif () - -# ------------------------------------------------------------------ -# Shared -# Shared target will have a `-shared' suffix -# All shared dependency also have `-shared' suffix -# For shared binary, we still use the same name as static, without `-shared', but will output to shared directory -add_executable(ss-server-shared ${SS_SERVER_SOURCE}) -add_executable(ss-tunnel-shared ${SS_TUNNEL_SOURCE}) -if (WITH_SS_MANAGER) - add_executable(ss-manager-shared ${SS_MANAGER_SOURCE}) -else () - add_executable(ss-manager-shared EXCLUDE_FROM_ALL ${SS_MANAGER_SOURCE}) -endif () -add_executable(ss-local-shared ${SS_LOCAL_SOURCE}) -if (WITH_SS_REDIR) - add_executable(ss-redir-shared ${SS_REDIR_SOURCE}) -else () - add_executable(ss-redir-shared EXCLUDE_FROM_ALL ${SS_REDIR_SOURCE}) -endif () -add_library(shadowsocks-libev-shared SHARED ${LIBSHADOWSOCKS_LIBEV_SOURCE}) - -target_compile_definitions(ss-server-shared PUBLIC -DMODULE_REMOTE) -target_compile_definitions(ss-tunnel-shared PUBLIC -DMODULE_TUNNEL) -target_compile_definitions(ss-manager-shared PUBLIC -DMODULE_MANAGER) -target_compile_definitions(ss-local-shared PUBLIC -DMODULE_LOCAL) -target_compile_definitions(ss-redir-shared PUBLIC -DMODULE_REDIR) -target_compile_definitions(shadowsocks-libev-shared PUBLIC -DMODULE_LOCAL -DLIB_ONLY) - -target_compile_options(ss-server-shared PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(ss-tunnel-shared PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(ss-manager-shared PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(ss-local-shared PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(ss-redir-shared PRIVATE ${SS_WERROR_FLAGS}) -target_compile_options(shadowsocks-libev-shared PRIVATE ${SS_WERROR_FLAGS}) - -# Connmarktos/nftables compile definitions for shared -if(ENABLE_CONNMARKTOS) - target_compile_definitions(ss-server-shared PUBLIC -DUSE_NFCONNTRACK_TOS) -endif() -if(ENABLE_NFTABLES) - target_compile_definitions(ss-server-shared PUBLIC -DUSE_NFTABLES) +function(ss_program name module sources) + add_executable(${name} ${sources}) + target_compile_definitions(${name} PRIVATE ${module}) + target_link_libraries(${name} PRIVATE ${SS_CORE_DEPS} ${ARGN}) + ss_project_options(${name}) + install(TARGETS ${name} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endfunction() + +if(SS_BUILD_EXECUTABLES) + ss_program(ss-local MODULE_LOCAL "${SS_LOCAL_SOURCE}" ${SS_ACL_DEPS}) + ss_program(ss-server MODULE_REMOTE "${SS_SERVER_SOURCE}" ${SS_ACL_DEPS} ${SS_SERVER_DEPS}) + ss_program(ss-tunnel MODULE_TUNNEL "${SS_TUNNEL_SOURCE}") + if(WITH_SS_MANAGER) + ss_program(ss-manager MODULE_MANAGER "${SS_MANAGER_SOURCE}") + endif() + if(WITH_SS_REDIR) + if(NOT LINUX) + message(FATAL_ERROR "ss-redir requires Linux") + endif() + ss_program(ss-redir MODULE_REDIR "${SS_REDIR_SOURCE}") + endif() endif() -target_include_directories(shadowsocks-libev-shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -target_link_libraries(ss-server-shared ${DEPS_SHARED} ${DEPS_CONNMARKTOS} ${DEPS_NFTABLES}) -target_link_libraries(ss-tunnel-shared ${DEPS_SHARED}) -target_link_libraries(ss-manager-shared ${CMAKE_THREAD_LIBS_INIT} ${LIBEV_SHARED} ${LIBUDNS_SHARED} ${DEPS_SHARED}) -target_link_libraries(ss-local-shared ${DEPS_SHARED}) -target_link_libraries(ss-redir-shared ${DEPS_SHARED}) -target_link_libraries(shadowsocks-libev-shared ${DEPS_SHARED}) - -set_target_properties(ss-server-shared PROPERTIES OUTPUT_NAME ss-server) -set_target_properties(ss-tunnel-shared PROPERTIES OUTPUT_NAME ss-tunnel) -set_target_properties(ss-manager-shared PROPERTIES OUTPUT_NAME ss-manager) -set_target_properties(ss-local-shared PROPERTIES OUTPUT_NAME ss-local) -set_target_properties(ss-redir-shared PROPERTIES OUTPUT_NAME ss-redir) - -set_target_properties(ss-server-shared ss-tunnel-shared ss-manager-shared ss-local-shared ss-redir-shared - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${RUNTIME_SHARED_OUTPUT_DIRECTORY} - ) - -set_target_properties(shadowsocks-libev-shared PROPERTIES OUTPUT_NAME shadowsocks-libev) -set_target_properties(shadowsocks-libev-shared PROPERTIES VERSION 2.0.0 SOVERSION 2) -target_compile_definitions(shadowsocks-libev-shared PUBLIC -DMODULE_LOCAL) -target_link_libraries(shadowsocks-libev-shared ${DEPS_SHARED}) - -# ------------------------------------------------------------------ -# Misc -# Recommend to install shared by default -install(DIRECTORY ${RUNTIME_SHARED_OUTPUT_DIRECTORY}/ - USE_SOURCE_PERMISSIONS - DESTINATION ${CMAKE_INSTALL_BINDIR}) - -if (WITH_STATIC) - install(TARGETS shadowsocks-libev - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) -endif () - -install(TARGETS shadowsocks-libev-shared - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) - -install(FILES shadowsocks.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) - - -add_custom_target(distclean - COMMAND ${CMAKE_COMMAND} -E echo WARNING: distclean target is not functional - COMMAND ${CMAKE_COMMAND} -E echo Use 'git clean -fdx' instead - VERBATIM - ) +foreach(kind STATIC SHARED) + if(SS_BUILD_${kind}_LIBRARY) + string(TOLOWER "${kind}" suffix) + set(target shadowsocks-libev-${suffix}) + add_library(${target} ${kind} ${LIBSHADOWSOCKS_LIBEV_SOURCE}) + target_compile_definitions(${target} PRIVATE MODULE_LOCAL LIB_ONLY) + target_include_directories(${target} PUBLIC + $ + $) + target_link_libraries(${target} PRIVATE ${SS_CORE_DEPS} ${SS_ACL_DEPS}) + set_target_properties(${target} PROPERTIES OUTPUT_NAME shadowsocks-libev) + if(kind STREQUAL "SHARED") + set_target_properties(${target} PROPERTIES VERSION 2.0.0 SOVERSION 2) + endif() + ss_project_options(${target}) + install(TARGETS ${target} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + endif() +endforeach() +if(SS_BUILD_STATIC_LIBRARY OR SS_BUILD_SHARED_LIBRARY) + install(FILES shadowsocks.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +endif() diff --git a/src/acl.c b/src/acl.c index 39886c99d..13e9b7e1b 100644 --- a/src/acl.c +++ b/src/acl.c @@ -25,33 +25,30 @@ #endif #include +#include -#ifdef USE_SYSTEM_SHARED_LIB -#include -#else -#include -#endif +#include "ipset.h" +#include "core.h" #include "rule.h" #include "netutils.h" #include "utils.h" -#include "cache.h" #include "acl.h" -static struct ip_set white_list_ipv4; -static struct ip_set white_list_ipv6; +static struct ss_ipset white_list_ipv4; +static struct ss_ipset white_list_ipv6; -static struct ip_set black_list_ipv4; -static struct ip_set black_list_ipv6; +static struct ss_ipset black_list_ipv4; +static struct ss_ipset black_list_ipv6; -static struct cork_dllist black_list_rules; -static struct cork_dllist white_list_rules; +static struct ss_list black_list_rules; +static struct ss_list white_list_rules; static int acl_mode = BLACK_LIST; -static struct ip_set outbound_block_list_ipv4; -static struct ip_set outbound_block_list_ipv6; -static struct cork_dllist outbound_block_list_rules; +static struct ss_ipset outbound_block_list_ipv4; +static struct ss_ipset outbound_block_list_ipv6; +static struct ss_list outbound_block_list_rules; static int parse_addr_cidr(const char *str, char *host, size_t host_len, int *cidr) @@ -120,22 +117,21 @@ init_acl(const char *path) } // initialize ipset - ipset_init_library(); - ipset_init(&white_list_ipv4); - ipset_init(&white_list_ipv6); - ipset_init(&black_list_ipv4); - ipset_init(&black_list_ipv6); - ipset_init(&outbound_block_list_ipv4); - ipset_init(&outbound_block_list_ipv6); + ss_ipset_clear(&white_list_ipv4); + ss_ipset_clear(&white_list_ipv6); + ss_ipset_clear(&black_list_ipv4); + ss_ipset_clear(&black_list_ipv6); + ss_ipset_clear(&outbound_block_list_ipv4); + ss_ipset_clear(&outbound_block_list_ipv6); - cork_dllist_init(&black_list_rules); - cork_dllist_init(&white_list_rules); - cork_dllist_init(&outbound_block_list_rules); + ss_list_init(&black_list_rules); + ss_list_init(&white_list_rules); + ss_list_init(&outbound_block_list_rules); - struct ip_set *list_ipv4 = &black_list_ipv4; - struct ip_set *list_ipv6 = &black_list_ipv6; - struct cork_dllist *rules = &black_list_rules; + struct ss_ipset *list_ipv4 = &black_list_ipv4; + struct ss_ipset *list_ipv6 = &black_list_ipv6; + struct ss_list *rules = &black_list_rules; FILE *f = fopen(path, "r"); if (f == NULL) { @@ -145,8 +141,7 @@ init_acl(const char *path) char buf[MAX_HOSTNAME_LEN]; - while (!feof(f)) - if (fgets(buf, 256, f)) { + while (!ferror(f) && !feof(f) && fgets(buf, sizeof(buf), f) != NULL) { // Discards the whole line if longer than 255 characters int long_line = 0; // 1: Long 2: Error while ((strlen(buf) == 255) && (buf[254] != '\n')) { @@ -214,8 +209,8 @@ init_acl(const char *path) continue; } - struct cork_ip addr; - int err = cork_ip_init(&addr, host); + struct ss_ip addr; + int err = ss_ip_init(&addr, host); if (!err) { if (addr.version == 4) { if (cidr > 32 || cidr == -2) { @@ -223,9 +218,9 @@ init_acl(const char *path) continue; } if (cidr >= 0) { - ipset_ipv4_add_network(list_ipv4, &(addr.ip.v4), cidr); + ss_ipset_assign(list_ipv4, addr.bytes, 32, (unsigned)cidr, true); } else { - ipset_ipv4_add(list_ipv4, &(addr.ip.v4)); + ss_ipset_assign(list_ipv4, addr.bytes, 32, 32, true); } } else if (addr.version == 6) { if (cidr > 128 || cidr == -2) { @@ -233,9 +228,9 @@ init_acl(const char *path) continue; } if (cidr >= 0) { - ipset_ipv6_add_network(list_ipv6, &(addr.ip.v6), cidr); + ss_ipset_assign(list_ipv6, addr.bytes, 128, (unsigned)cidr, true); } else { - ipset_ipv6_add(list_ipv6, &(addr.ip.v6)); + ss_ipset_assign(list_ipv6, addr.bytes, 128, 128, true); } } } else { @@ -243,7 +238,15 @@ init_acl(const char *path) if (rule == NULL) { continue; } - if (accept_rule_arg(rule, line) != 1 || init_rule(rule) != 1) { + int status = accept_rule_arg(rule, line); + if (status == 1) status = init_rule(rule); + if (status < 0) { + free_rule(rule); + fclose(f); + free_acl(); + return -1; + } + if (status != 1) { free_rule(rule); continue; } @@ -251,17 +254,21 @@ init_acl(const char *path) } } + int failed = ferror(f); fclose(f); - + if (failed) { + free_acl(); + return -1; + } return 0; } void -free_rules(struct cork_dllist *rules) +free_rules(struct ss_list *rules) { - struct cork_dllist_item *iter; - while ((iter = cork_dllist_head(rules)) != NULL) { - rule_t *rule = cork_container_of(iter, rule_t, entries); + struct ss_list_item *iter; + while ((iter = ss_list_head(rules)) != NULL) { + rule_t *rule = ss_container_of(iter, rule_t, entries); remove_rule(rule); } } @@ -269,12 +276,12 @@ free_rules(struct cork_dllist *rules) void free_acl(void) { - ipset_done(&black_list_ipv4); - ipset_done(&black_list_ipv6); - ipset_done(&white_list_ipv4); - ipset_done(&white_list_ipv6); - ipset_done(&outbound_block_list_ipv4); - ipset_done(&outbound_block_list_ipv6); + ss_ipset_clear(&black_list_ipv4); + ss_ipset_clear(&black_list_ipv6); + ss_ipset_clear(&white_list_ipv4); + ss_ipset_clear(&white_list_ipv6); + ss_ipset_clear(&outbound_block_list_ipv4); + ss_ipset_clear(&outbound_block_list_ipv6); free_rules(&black_list_rules); free_rules(&white_list_rules); @@ -295,9 +302,9 @@ get_acl_mode(void) int acl_match_host(const char *host) { - struct cork_ip addr; + struct ss_ip addr; int ret = 0; - int err = cork_ip_init(&addr, host); + int err = ss_ip_init(&addr, host); if (err) { int host_len = strlen(host); @@ -309,14 +316,14 @@ acl_match_host(const char *host) } if (addr.version == 4) { - if (ipset_contains_ipv4(&black_list_ipv4, &(addr.ip.v4))) + if (ss_ipset_contains(&black_list_ipv4, addr.bytes, 32)) ret = 1; - else if (ipset_contains_ipv4(&white_list_ipv4, &(addr.ip.v4))) + else if (ss_ipset_contains(&white_list_ipv4, addr.bytes, 32)) ret = -1; } else if (addr.version == 6) { - if (ipset_contains_ipv6(&black_list_ipv6, &(addr.ip.v6))) + if (ss_ipset_contains(&black_list_ipv6, addr.bytes, 128)) ret = 1; - else if (ipset_contains_ipv6(&white_list_ipv6, &(addr.ip.v6))) + else if (ss_ipset_contains(&white_list_ipv6, addr.bytes, 128)) ret = -1; } @@ -326,16 +333,16 @@ acl_match_host(const char *host) int acl_add_ip(const char *ip) { - struct cork_ip addr; - int err = cork_ip_init(&addr, ip); + struct ss_ip addr; + int err = ss_ip_init(&addr, ip); if (err) { return -1; } if (addr.version == 4) { - ipset_ipv4_add(&black_list_ipv4, &(addr.ip.v4)); + ss_ipset_assign(&black_list_ipv4, addr.bytes, 32, 32, true); } else if (addr.version == 6) { - ipset_ipv6_add(&black_list_ipv6, &(addr.ip.v6)); + ss_ipset_assign(&black_list_ipv6, addr.bytes, 128, 128, true); } return 0; @@ -344,16 +351,16 @@ acl_add_ip(const char *ip) int acl_remove_ip(const char *ip) { - struct cork_ip addr; - int err = cork_ip_init(&addr, ip); + struct ss_ip addr; + int err = ss_ip_init(&addr, ip); if (err) { return -1; } if (addr.version == 4) { - ipset_ipv4_remove(&black_list_ipv4, &(addr.ip.v4)); + ss_ipset_assign(&black_list_ipv4, addr.bytes, 32, 32, false); } else if (addr.version == 6) { - ipset_ipv6_remove(&black_list_ipv6, &(addr.ip.v6)); + ss_ipset_assign(&black_list_ipv6, addr.bytes, 128, 128, false); } return 0; @@ -366,9 +373,9 @@ acl_remove_ip(const char *ip) int outbound_block_match_host(const char *host) { - struct cork_ip addr; + struct ss_ip addr; int ret = 0; - int err = cork_ip_init(&addr, host); + int err = ss_ip_init(&addr, host); if (err) { int host_len = strlen(host); @@ -378,10 +385,10 @@ outbound_block_match_host(const char *host) } if (addr.version == 4) { - if (ipset_contains_ipv4(&outbound_block_list_ipv4, &(addr.ip.v4))) + if (ss_ipset_contains(&outbound_block_list_ipv4, addr.bytes, 32)) ret = 1; } else if (addr.version == 6) { - if (ipset_contains_ipv6(&outbound_block_list_ipv6, &(addr.ip.v6))) + if (ss_ipset_contains(&outbound_block_list_ipv6, addr.bytes, 128)) ret = 1; } diff --git a/src/aead.c b/src/aead.c index 8b4e104ed..6b1fa22a2 100644 --- a/src/aead.c +++ b/src/aead.c @@ -39,7 +39,7 @@ #include "aead.h" #include "aead_internal.h" #include "utils.h" -#include "winsock.h" +#include "ss_windows.h" #define NONE (-1) diff --git a/src/cache.c b/src/cache.c index ed251ed77..6d4fc6886 100644 --- a/src/cache.c +++ b/src/cache.c @@ -33,6 +33,7 @@ #include #include "cache.h" +#include "platform.h" #include "utils.h" /** Creates a new cache object @@ -118,7 +119,7 @@ cache_delete(struct cache *cache, int keep_data) * @return EINVAL if cache is NULL, 0 otherwise */ int -cache_clear(struct cache *cache, ev_tstamp age) +cache_clear(struct cache *cache, double age) { struct cache_entry *entry, *tmp; @@ -126,7 +127,7 @@ cache_clear(struct cache *cache, ev_tstamp age) return EINVAL; } - ev_tstamp now = ev_time(); + double now = ss_monotonic_time(); HASH_ITER(hh, cache->entries, entry, tmp){ if (now - entry->ts > age) { @@ -220,7 +221,7 @@ cache_lookup(struct cache *cache, char *key, size_t key_len, void *result) HASH_FIND(hh, cache->entries, key, key_len, tmp); if (tmp) { HASH_DELETE(hh, cache->entries, tmp); - tmp->ts = ev_time(); + tmp->ts = ss_monotonic_time(); // NOLINTNEXTLINE(clang-analyzer-core.DivideZero): uthash bucket count is never zero HASH_ADD_KEYPTR(hh, cache->entries, tmp->key, key_len, tmp); *dirty_hack = tmp->data; @@ -243,7 +244,7 @@ cache_key_exist(struct cache *cache, char *key, size_t key_len) HASH_FIND(hh, cache->entries, key, key_len, tmp); if (tmp) { HASH_DELETE(hh, cache->entries, tmp); - tmp->ts = ev_time(); + tmp->ts = ss_monotonic_time(); // NOLINTNEXTLINE(clang-analyzer-core.DivideZero): uthash bucket count is never zero HASH_ADD_KEYPTR(hh, cache->entries, tmp->key, key_len, tmp); return 1; @@ -287,7 +288,7 @@ cache_insert(struct cache *cache, char *key, size_t key_len, void *data) entry->key[key_len] = 0; entry->data = data; - entry->ts = ev_time(); + entry->ts = ss_monotonic_time(); // NOLINTNEXTLINE(clang-analyzer-core.DivideZero): uthash bucket count is never zero HASH_ADD_KEYPTR(hh, cache->entries, entry->key, key_len, entry); diff --git a/src/cache.h b/src/cache.h index 7ca8b3be8..1977c29f6 100644 --- a/src/cache.h +++ b/src/cache.h @@ -30,19 +30,13 @@ #include "uthash.h" -#ifdef HAVE_LIBEV_EV_H -#include -#else -#include -#endif - /** * A cache entry */ struct cache_entry { char *key; /** +#include +#include +#include "platform.h" + +#define ss_container_of(ptr, type, member) ((type *)((char *)(ptr) - offsetof(type, member))) + +struct ss_list_item { + struct ss_list_item *next, *prev; +}; +struct ss_list { + struct ss_list_item head; +}; +static inline void +ss_list_init(struct ss_list *list) +{ + list->head.next = list->head.prev = &list->head; +} +static inline void +ss_list_add(struct ss_list *list, struct ss_list_item *item) +{ + item->prev = list->head.prev; + item->next = &list->head; + item->prev->next = item; + list->head.prev = item; +} +static inline void +ss_list_remove(struct ss_list_item *item) +{ + item->prev->next = item->next; + item->next->prev = item->prev; +} +static inline struct ss_list_item * +ss_list_start(const struct ss_list *list) +{ + return list->head.next; +} +static inline bool +ss_list_is_end(const struct ss_list *list, const struct ss_list_item *item) +{ + return item == &list->head; +} +static inline struct ss_list_item * +ss_list_head(const struct ss_list *list) +{ + return ss_list_is_end(list, list->head.next) ? NULL : list->head.next; +} +/* Cache the next link before the body so the current item may be freed. */ +#define ss_list_foreach_void(list, item, after) \ + for ((item) = ss_list_start(list), (after) = (item)->next; \ + !ss_list_is_end((list), (item)); (item) = (after), (after) = (item)->next) + +struct ss_ip { + unsigned version; + uint8_t bytes[16]; +}; +static inline int +ss_ip_init(struct ss_ip *ip, const char *text) +{ + if (inet_pton(AF_INET, text, ip->bytes) == 1) { + ip->version = 4; + return 0; + } + if (inet_pton(AF_INET6, text, ip->bytes) == 1) { + ip->version = 6; + return 0; + } + ip->version = 0; + return -1; +} +#endif diff --git a/src/crypto.c b/src/crypto.c index 2ff07f89c..523faca91 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -150,6 +150,7 @@ crypto_init(const char *password, const char *key, const char *method) #endif if (method != NULL) { +#if SS_ENABLE_LEGACY for (i = 0; i < STREAM_CIPHER_NUM; i++) if (strcmp(method, supported_stream_ciphers[i]) == 0) { m = i; @@ -174,6 +175,7 @@ crypto_init(const char *password, const char *key, const char *method) return crypto; } +#endif for (i = 0; i < AEAD_CIPHER_NUM; i++) if (strcmp(method, supported_aead_ciphers[i]) == 0) { m = i; @@ -203,7 +205,7 @@ crypto_init(const char *password, const char *key, const char *method) } } - LOGE("invalid cipher name: %s", method); + LOGE("unsupported or disabled cipher: %s", method); return NULL; } diff --git a/src/crypto.h b/src/crypto.h index d0eaf9444..ef9a06a7a 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -49,7 +49,7 @@ typedef mbedtls_md_info_t digest_type_t; #define MAX_NONCE_LENGTH 32 #define MAX_MD_SIZE MBEDTLS_MD_MAX_SIZE /* we must have MBEDTLS_CIPHER_MODE_CFB defined */ -#if !defined(MBEDTLS_CIPHER_MODE_CFB) +#if SS_ENABLE_LEGACY && !defined(MBEDTLS_CIPHER_MODE_CFB) #error Cipher Feedback mode a.k.a CFB not supported by your mbed TLS. #endif #ifndef MBEDTLS_GCM_C diff --git a/src/ipset.c b/src/ipset.c new file mode 100644 index 000000000..51723ba29 --- /dev/null +++ b/src/ipset.c @@ -0,0 +1,99 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#include "ipset.h" +#include +#include +#include "utils.h" + +/* NULL is an empty subtree. A full node represents every address below it. + * Recursion is bounded by the address width, never by the number of rules. */ +struct ss_ipset_node { + struct ss_ipset_node *child[2]; + bool full; +}; + +static struct ss_ipset_node * +node_new(bool full) +{ + struct ss_ipset_node *node = ss_malloc(sizeof(*node)); + node->child[0] = node->child[1] = NULL; + node->full = full; + return node; +} + +static void +node_free(struct ss_ipset_node *node) +{ + if (node != NULL) { + node_free(node->child[0]); + node_free(node->child[1]); + free(node); + } +} + +void +ss_ipset_clear(struct ss_ipset *set) +{ + node_free(set->root); + set->root = NULL; +} + +static struct ss_ipset_node * +node_assign(struct ss_ipset_node *node, const uint8_t *address, + unsigned bit, unsigned prefix, bool present) +{ + if ((node == NULL && !present) || (node != NULL && node->full && present)) { + return node; + } + if (bit == prefix) { + node_free(node); + return present ? node_new(true) : NULL; + } + if (node == NULL) { + node = node_new(false); + } else if (node->full) { + node->child[0] = node_new(true); + node->child[1] = node_new(true); + node->full = false; + } + unsigned side = (address[bit / 8] >> (7 - bit % 8)) & 1; + node->child[side] = node_assign(node->child[side], address, bit + 1, prefix, present); + if (node->child[0] == NULL && node->child[1] == NULL) { + free(node); + return NULL; + } + if (node->child[0] != NULL && node->child[0]->full && + node->child[1] != NULL && node->child[1]->full) { + node_free(node->child[0]); + node_free(node->child[1]); + node->child[0] = node->child[1] = NULL; + node->full = true; + } + return node; +} + +void +ss_ipset_assign(struct ss_ipset *set, const uint8_t *address, + unsigned width, unsigned prefix, bool present) +{ + assert(width == 32 || width == 128); + if (prefix <= width) { + set->root = node_assign(set->root, address, 0, prefix, present); + } +} + +bool +ss_ipset_contains(const struct ss_ipset *set, const uint8_t *address, unsigned width) +{ + const struct ss_ipset_node *node = set->root; + for (unsigned bit = 0; node != NULL; bit++) { + if (node->full) { + return true; + } + if (bit == width) { + break; + } + unsigned side = (address[bit / 8] >> (7 - bit % 8)) & 1; + node = node->child[side]; + } + return false; +} diff --git a/src/ipset.h b/src/ipset.h new file mode 100644 index 000000000..47801377a --- /dev/null +++ b/src/ipset.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#ifndef SS_IPSET_H +#define SS_IPSET_H +#include +#include + +struct ss_ipset_node; +struct ss_ipset { + struct ss_ipset_node *root; +}; + +/* Addresses are network-order bytes; width is 32 or 128. A set stores one + * address family. Removing a host from a network creates a hole in that net. */ +void ss_ipset_clear(struct ss_ipset *set); +void ss_ipset_assign(struct ss_ipset *set, const uint8_t *address, + unsigned width, unsigned prefix, bool present); +bool ss_ipset_contains(const struct ss_ipset *set, const uint8_t *address, unsigned width); +#endif diff --git a/src/jconf.c b/src/jconf.c index 613c1ac8a..410e69cea 100644 --- a/src/jconf.c +++ b/src/jconf.c @@ -33,7 +33,7 @@ #include "json.h" #include "string.h" -#include +#include "core.h" #define check_json_value_type(value, expected_type, message) \ do { \ @@ -88,8 +88,8 @@ parse_addr(const char *str_in, ss_addr_t *addr) char *str = strdup(str_in); len = strlen(str_in); - struct cork_ip ip; - if (cork_ip_init(&ip, str) != -1) { + struct ss_ip ip; + if (ss_ip_init(&ip, str) != -1) { addr->host = str; addr->port = NULL; return; diff --git a/src/json.c b/src/json.c index 05a38b760..dd4e9d0bd 100644 --- a/src/json.c +++ b/src/json.c @@ -778,9 +778,10 @@ json_value * json_parse_ex (json_settings * settings, } /* Overflow check for integer */ - long prev = top->u.integer; - if ((prev > 0 && (prev > (LONG_MAX / 10) || (prev == (LONG_MAX / 10) && (b - '0') > (LONG_MAX % 10)))) || - (prev < 0 && (prev < (LONG_MIN / 10) || (prev == (LONG_MIN / 10) && -(b - '0') < (LONG_MIN % 10))))) { + json_int_t prev = top->u.integer; + /* Digits accumulate as a positive int64_t magnitude; + * long is only 32 bits on LLP64 platforms such as Windows. */ + if (prev > (INT64_MAX / 10) || (prev == (INT64_MAX / 10) && (b - '0') > (INT64_MAX % 10))) { sprintf(error, "%d:%d: Integer too large (overflow)", line_and_col); goto e_failed; } @@ -789,7 +790,7 @@ json_value * json_parse_ex (json_settings * settings, } /* Overflow check for fraction */ - if (num_fraction > (LONG_MAX / 10) || (num_fraction == (LONG_MAX / 10) && (b - '0') > (LONG_MAX % 10))) { + if (num_fraction > (INT64_MAX / 10) || (num_fraction == (INT64_MAX / 10) && (b - '0') > (INT64_MAX % 10))) { sprintf(error, "%d:%d: Fraction too large (overflow)", line_and_col); goto e_failed; } diff --git a/src/local.c b/src/local.c index 2a0ea2125..94f0402f8 100644 --- a/src/local.c +++ b/src/local.c @@ -49,7 +49,7 @@ #define SET_INTERFACE #endif -#include +#include "core.h" #include "netutils.h" #include "ssurl.h" @@ -58,7 +58,7 @@ #include "acl.h" #include "plugin.h" #include "local.h" -#include "winsock.h" +#include "ss_windows.h" #ifndef LIB_ONLY #ifdef __APPLE__ @@ -149,7 +149,7 @@ static void close_and_free_server(EV_P_ server_t *server); static remote_t *new_remote(int fd, int timeout); static server_t *new_server(int fd); -static struct cork_dllist connections; +static struct ss_list connections; #ifndef __MINGW32__ int @@ -189,15 +189,15 @@ create_and_bind(const char *addr, const char *port) } for (rp = result; rp != NULL; rp = rp->ai_next) { - listen_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + listen_sock = ss_socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (listen_sock == -1) { continue; } int opt = 1; - setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif if (reuse_port) { int err = set_reuseport(listen_sock); @@ -207,11 +207,11 @@ create_and_bind(const char *addr, const char *port) } if (tcp_incoming_sndbuf > 0) { - setsockopt(listen_sock, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int)); } if (tcp_incoming_rcvbuf > 0) { - setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int)); } s = bind(listen_sock, rp->ai_addr, rp->ai_addrlen); @@ -222,7 +222,7 @@ create_and_bind(const char *addr, const char *port) ERROR("bind"); } - close(listen_sock); + ss_socket_close(listen_sock); listen_sock = -1; } @@ -265,9 +265,9 @@ launch_or_create(const char *addr, const char *port) static void free_connections(struct ev_loop *loop) { - struct cork_dllist_item *curr, *next; - cork_dllist_foreach_void(&connections, curr, next) { - server_t *server = cork_container_of(curr, server_t, entries); + struct ss_list_item *curr, *next; + ss_list_foreach_void(&connections, curr, next) { + server_t *server = ss_container_of(curr, server_t, entries); remote_t *remote = server->remote; close_and_free_server(loop, server); close_and_free_remote(loop, remote); @@ -277,7 +277,7 @@ free_connections(struct ev_loop *loop) static void delayed_connect_cb(EV_P_ ev_timer *watcher, int revents) { - server_t *server = cork_container_of(watcher, server_t, + server_t *server = ss_container_of(watcher, server_t, delayed_connect_watcher); server_recv_cb(EV_A_ & server->recv_ctx->io, revents); @@ -296,7 +296,7 @@ server_handshake_reply(EV_P_ ev_io *w, int udp_assc, struct socks5_response *res if (udp_assc) { socklen_t addr_len = sizeof(sock_addr); if (getsockname(server->fd, (struct sockaddr *)&sock_addr, &addr_len) < 0) { - LOGE("getsockname: %s", strerror(errno)); + LOGE("getsockname: %s", strerror(ss_socket_error())); response->rep = SOCKS5_REP_CONN_REFUSED; send(server->fd, (char *)response, sizeof(struct socks5_response), 0); close_and_free_remote(EV_A_ remote); @@ -392,9 +392,9 @@ server_handshake(EV_P_ ev_io *w, buffer_t *buf) if (acl || verbose) { uint16_t p = load16_be(buf->data + request_len + in_addr_len); - if (!inet_ntop(AF_INET, (const void *)(buf->data + request_len), + if (!ss_inet_ntop(AF_INET, (const void *)(buf->data + request_len), ip, INET_ADDRSTRLEN)) { - LOGI("inet_ntop(AF_INET): %s", strerror(errno)); + LOGI("ss_inet_ntop(AF_INET): %s", strerror(ss_socket_error())); ip[0] = '\0'; } snprintf(port, sizeof(port), "%d", p); @@ -427,9 +427,9 @@ server_handshake(EV_P_ ev_io *w, buffer_t *buf) if (acl || verbose) { uint16_t p = load16_be(buf->data + request_len + in6_addr_len); - if (!inet_ntop(AF_INET6, (const void *)(buf->data + request_len), + if (!ss_inet_ntop(AF_INET6, (const void *)(buf->data + request_len), ip, INET6_ADDRSTRLEN)) { - LOGI("inet_ntop(AF_INET6): %s", strerror(errno)); + LOGI("ss_inet_ntop(AF_INET6): %s", strerror(ss_socket_error())); ip[0] = '\0'; } snprintf(port, sizeof(port), "%d", p); @@ -494,14 +494,14 @@ server_handshake(EV_P_ ev_io *w, buffer_t *buf) case AF_INET: { struct sockaddr_in *addr_in = (struct sockaddr_in *)&storage; - if (!inet_ntop(AF_INET, &(addr_in->sin_addr), ip, INET_ADDRSTRLEN)) + if (!ss_inet_ntop(AF_INET, &(addr_in->sin_addr), ip, INET_ADDRSTRLEN)) goto not_bypass; break; } case AF_INET6: { struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)&storage; - if (!inet_ntop(AF_INET6, &(addr_in6->sin6_addr), ip, INET6_ADDRSTRLEN)) + if (!ss_inet_ntop(AF_INET6, &(addr_in6->sin6_addr), ip, INET6_ADDRSTRLEN)) goto not_bypass; break; } @@ -650,7 +650,7 @@ server_stream(EV_P_ ev_io *w, buffer_t *buf) // connecting, wait until connected int r = connect(remote->fd, (struct sockaddr *)&(remote->addr), remote->addr_len); - if (r == -1 && errno != CONNECT_IN_PROGRESS) { + if (r == -1 && ss_socket_error() != CONNECT_IN_PROGRESS) { ERROR("connect"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); @@ -672,7 +672,7 @@ server_stream(EV_P_ ev_io *w, buffer_t *buf) do { int optval = 1; // Set fast open option - if (setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN, + if (ss_setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN, &optval, sizeof(optval)) != 0) { ERROR("setsockopt"); break; @@ -724,7 +724,7 @@ server_stream(EV_P_ ev_io *w, buffer_t *buf) NULL, 0, NULL, NULL); #elif defined(TCP_FASTOPEN_CONNECT) int optval = 1; - if (setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, + if (ss_setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, (void *)&optval, sizeof(optval)) < 0) FATAL("failed to set TCP_FASTOPEN_CONNECT"); s = connect(remote->fd, (struct sockaddr *)&(remote->addr), remote->addr_len); @@ -735,15 +735,15 @@ server_stream(EV_P_ ev_io *w, buffer_t *buf) s = send(remote->fd, remote->buf->data, remote->buf->len, 0); #endif if (s == -1) { - if (errno == CONNECT_IN_PROGRESS) { + if (ss_socket_error() == CONNECT_IN_PROGRESS) { // in progress, wait until connected remote->buf->idx = 0; ev_io_stop(EV_A_ & server_recv_ctx->io); ev_io_start(EV_A_ & remote->send_ctx->io); return; } else { - if (errno == EOPNOTSUPP || errno == EPROTONOSUPPORT || - errno == ENOPROTOOPT) { + if (ss_socket_error() == EOPNOTSUPP || ss_socket_error() == EPROTONOSUPPORT || + ss_socket_error() == ENOPROTOOPT) { LOGE("fast open is not supported on this platform"); // just turn it off fast_open = 0; @@ -767,7 +767,7 @@ server_stream(EV_P_ ev_io *w, buffer_t *buf) } else { int s = send(remote->fd, remote->buf->data, remote->buf->len, 0); if (s == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data, wait for send remote->buf->idx = 0; ev_io_stop(EV_A_ & server_recv_ctx->io); @@ -818,7 +818,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } else if (r == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data // continue to wait for recv return; @@ -915,7 +915,7 @@ server_send_cb(EV_P_ ev_io *w, int revents) ssize_t s = send(server->fd, server->buf->data + server->buf->idx, server->buf->len, 0); if (s == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (ss_socket_error() != EAGAIN && ss_socket_error() != EWOULDBLOCK) { ERROR("server_send_cb_send"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); @@ -954,7 +954,7 @@ static void remote_timeout_cb(EV_P_ ev_timer *watcher, int revents) { remote_ctx_t *remote_ctx - = cork_container_of(watcher, remote_ctx_t, watcher); + = ss_container_of(watcher, remote_ctx_t, watcher); remote_t *remote = remote_ctx->remote; server_t *server = remote->server; @@ -982,7 +982,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } else if (r == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data // continue to wait for recv return; @@ -1015,7 +1015,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) int s = send(server->fd, server->buf->data, server->buf->len, 0); if (s == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data, wait for send server->buf->idx = 0; ev_io_stop(EV_A_ & remote_recv_ctx->io); @@ -1036,8 +1036,8 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) // Disable TCP_NODELAY after the first response are sent if (!remote->recv_ctx->connected && !no_delay) { int opt = 0; - setsockopt(server->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); - setsockopt(remote->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(server->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(remote->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); } remote->recv_ctx->connected = 1; } @@ -1075,7 +1075,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) } // Make getpeername work - if (setsockopt(remote->fd, SOL_SOCKET, + if (ss_setsockopt(remote->fd, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, NULL, 0) != 0) { ERROR("setsockopt"); } @@ -1114,7 +1114,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) ssize_t s = send(remote->fd, remote->buf->data + remote->buf->idx, remote->buf->len, 0); if (s == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (ss_socket_error() != EAGAIN && ss_socket_error() != EWOULDBLOCK) { ERROR("remote_send_cb_send"); // close and free close_and_free_remote(EV_A_ remote); @@ -1186,7 +1186,7 @@ close_and_free_remote(EV_P_ remote_t *remote) ev_timer_stop(EV_A_ & remote->send_ctx->watcher); ev_io_stop(EV_A_ & remote->send_ctx->io); ev_io_stop(EV_A_ & remote->recv_ctx->io); - close(remote->fd); + ss_socket_close(remote->fd); free_remote(remote); } } @@ -1226,7 +1226,7 @@ new_server(int fd) ev_timer_init(&server->delayed_connect_watcher, delayed_connect_cb, 0.05, 0); - cork_dllist_add(&connections, &server->entries); + ss_list_add(&connections, &server->entries); return server; } @@ -1234,7 +1234,7 @@ new_server(int fd) static void free_server(server_t *server) { - cork_dllist_remove(&server->entries); + ss_list_remove(&server->entries); if (server->remote != NULL) { server->remote->server = NULL; @@ -1267,7 +1267,7 @@ close_and_free_server(EV_P_ server_t *server) ev_io_stop(EV_A_ & server->send_ctx->io); ev_io_stop(EV_A_ & server->recv_ctx->io); ev_timer_stop(EV_A_ & server->delayed_connect_watcher); - close(server->fd); + ss_socket_close(server->fd); free_server(server); } } @@ -1290,28 +1290,28 @@ create_remote(listen_ctx_t *listener, if (listener->mptcp < 0) { protocol = IPPROTO_MPTCP; // Enable upstream MPTCP } - int remotefd = socket(remote_addr->sa_family, SOCK_STREAM, protocol); + int remotefd = ss_socket(remote_addr->sa_family, SOCK_STREAM, protocol); if (remotefd == -1) { ERROR("socket"); return NULL; } int opt = 1; - setsockopt(remotefd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(remotefd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(remotefd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif // Enable out-of-tree MPTCP if (listener->mptcp > 1) { - int err = setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); + int err = ss_setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); if (err == -1) { ERROR("failed to enable out-of-tree multipath TCP"); } } else if (listener->mptcp == 1) { int i = 0; while ((listener->mptcp = mptcp_enabled_values[i]) > 0) { - int err = setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); + int err = ss_setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); if (err != -1) { break; } @@ -1323,11 +1323,11 @@ create_remote(listen_ctx_t *listener, } if (tcp_outgoing_sndbuf > 0) { - setsockopt(remotefd, SOL_SOCKET, SO_SNDBUF, &tcp_outgoing_sndbuf, sizeof(int)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_SNDBUF, &tcp_outgoing_sndbuf, sizeof(int)); } if (tcp_outgoing_rcvbuf > 0) { - setsockopt(remotefd, SOL_SOCKET, SO_RCVBUF, &tcp_outgoing_rcvbuf, sizeof(int)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_RCVBUF, &tcp_outgoing_rcvbuf, sizeof(int)); } // Setup @@ -1388,7 +1388,7 @@ static void plugin_watcher_cb(EV_P_ ev_io *w, int revents) { char buf[1]; - SOCKET fd = accept(plugin_watcher.fd, NULL, NULL); + SOCKET fd = ss_accept(plugin_watcher.fd, NULL, NULL); if (fd == INVALID_SOCKET) { return; } @@ -1408,16 +1408,16 @@ void accept_cb(EV_P_ ev_io *w, int revents) { listen_ctx_t *listener = (listen_ctx_t *)w; - int serverfd = accept(listener->fd, NULL, NULL); + int serverfd = ss_accept(listener->fd, NULL, NULL); if (serverfd == -1) { ERROR("accept"); return; } setnonblocking(serverfd); int opt = 1; - setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif server_t *server = new_server(serverfd); @@ -1878,7 +1878,7 @@ main(int argc, char **argv) // Listen on plugin control port if (plugin != NULL && plugin_watcher.port != 0) { SOCKET fd; - fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + fd = ss_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fd != INVALID_SOCKET) { plugin_watcher.valid = 0; do { @@ -2046,7 +2046,7 @@ main(int argc, char **argv) #endif // Init connections - cork_dllist_init(&connections); + ss_list_init(&connections); // Enter the loop ev_run(loop, 0); @@ -2204,7 +2204,7 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat } // Init connections - cork_dllist_init(&connections); + ss_list_init(&connections); if (callback) { callback(listen_ctx.fd, udp_fd, udata); @@ -2221,7 +2221,7 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat if (mode != UDP_ONLY) { ev_io_stop(loop, &listen_ctx.io); free_connections(loop); - close(listen_ctx.fd); + ss_socket_close(listen_ctx.fd); } if (mode != TCP_ONLY) { diff --git a/src/local.h b/src/local.h index 96ee92a5a..1cc457213 100644 --- a/src/local.h +++ b/src/local.h @@ -23,7 +23,7 @@ #ifndef _LOCAL_H #define _LOCAL_H -#include +#include "core.h" #ifdef HAVE_LIBEV_EV_H #include @@ -32,7 +32,7 @@ #endif #ifdef __MINGW32__ -#include "winsock.h" +#include "ss_windows.h" #endif #include "crypto.h" @@ -72,7 +72,7 @@ typedef struct server { ev_timer delayed_connect_watcher; - struct cork_dllist_item entries; + struct ss_list_item entries; } server_t; typedef struct remote_ctx { diff --git a/src/manager.c b/src/manager.c index 140af6ccf..bb2c2f681 100644 --- a/src/manager.c +++ b/src/manager.c @@ -51,7 +51,7 @@ #include #include #include -#include +#include "core.h" #if defined(HAVE_SYS_IOCTL_H) && defined(HAVE_NET_IF_H) && defined(__linux__) #include @@ -73,9 +73,9 @@ int verbose = 0; char *executable = "ss-server"; char *working_dir = NULL; -int working_dir_size = 0; +size_t working_dir_size = 0; -static struct cork_hash_table *server_table; +static struct server *server_table; static int copy_port(char *dst, size_t dst_len, const char *src, size_t src_len) @@ -760,30 +760,30 @@ create_and_bind(const char *host, const char *port, int protocol) } for (/*rp = result*/; rp != NULL; rp = rp->ai_next) { - listen_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + listen_sock = ss_socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (listen_sock == -1) { continue; } if (rp->ai_family == AF_INET6) { int ipv6only = host ? 1 : 0; - setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, sizeof(ipv6only)); + ss_setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, sizeof(ipv6only)); } int opt = 1; - setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif s = bind(listen_sock, rp->ai_addr, rp->ai_addrlen); if (s == 0) { /* We managed to bind successfully! */ - close(listen_sock); + ss_socket_close(listen_sock); break; } else { ERROR("bind"); - close(listen_sock); + ss_socket_close(listen_sock); } } @@ -845,8 +845,13 @@ add_server(struct manager_ctx *manager, struct server *server) return -1; } - bool new = false; - cork_hash_table_put(server_table, (void *)server->port, (void *)server, &new, NULL, NULL); + struct server *replaced; + // NOLINTNEXTLINE(clang-analyzer-core.DivideZero,clang-analyzer-security.ArrayBound): uthash initializes nonzero buckets and hashes the NUL-terminated port within its key length + HASH_REPLACE_STR(server_table, port, server, replaced); + if (replaced != NULL) { + destroy_server(replaced); + ss_free(replaced); + } return 0; } @@ -854,13 +859,19 @@ add_server(struct manager_ctx *manager, struct server *server) static void kill_pid_from_file(FILE *f) { - char buf[16]; + char buf[16] = {0}; int pid; if (fgets(buf, sizeof(buf), f) == NULL) { return; } - buf[strcspn(buf, "\r\n")] = '\0'; + for (size_t i = 0; i < sizeof(buf); i++) { + if (buf[i] == '\0') break; + if (buf[i] == '\r' || buf[i] == '\n') { + buf[i] = '\0'; + break; + } + } // Reject malformed pid file content instead of signaling a garbage pid if (ss_parse_int(buf, 1, INT_MAX, &pid) == 0) { kill(pid, SIGTERM); @@ -911,12 +922,13 @@ stop_server(char *prefix, char *port) static void remove_server(char *prefix, char *port) { - char *old_port = NULL; struct server *old_server = NULL; - cork_hash_table_delete(server_table, (void *)port, (void **)&old_port, (void **)&old_server); + // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound): uthash hashes only strlen(port) bytes; manager ports are validated and NUL-terminated + HASH_FIND_STR(server_table, port, old_server); if (old_server != NULL) { + HASH_DEL(server_table, old_server); destroy_server(old_server); ss_free(old_server); } @@ -930,7 +942,9 @@ update_stat(char *port, uint64_t traffic) if (verbose) { LOGI("update traffic %" PRIu64 " for port %s", traffic, port); } - void *ret = cork_hash_table_get(server_table, (void *)port); + struct server *ret; + // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound): uthash hashes only strlen(port) bytes; manager ports are validated and NUL-terminated + HASH_FIND_STR(server_table, port, ret); if (ret != NULL) { struct server *server = (struct server *)ret; server->traffic = traffic; @@ -1000,15 +1014,12 @@ manager_recv_cb(EV_P_ ev_io *w, int revents) ERROR("add_sendto"); } } else if (strcmp(action, "list") == 0) { - struct cork_hash_table_iterator iter; - struct cork_hash_table_entry *entry; + struct server *server, *next; char buf[BUF_SIZE]; memset(buf, 0, BUF_SIZE); strcpy(buf, "["); - cork_hash_table_iterator_init(server_table, &iter); - while ((entry = cork_hash_table_iterator_next(&iter)) != NULL) { - struct server *server = (struct server *)entry->value; + HASH_ITER(hh, server_table, server, next) { char *method = server->method ? server->method : manager->method; char entry_buf[BUF_SIZE]; size_t entry_pos = 0; @@ -1068,8 +1079,7 @@ manager_recv_cb(EV_P_ ev_io *w, int revents) update_stat(port, traffic); } else if (strcmp(action, "ping") == 0) { - struct cork_hash_table_entry *entry; - struct cork_hash_table_iterator server_iter; + struct server *server, *next; char buf[BUF_SIZE]; size_t pos = 0; @@ -1079,10 +1089,7 @@ manager_recv_cb(EV_P_ ev_io *w, int revents) goto ERROR_MSG; } - cork_hash_table_iterator_init(server_table, &server_iter); - - while ((entry = cork_hash_table_iterator_next(&server_iter)) != NULL) { - struct server *server = (struct server *)entry->value; + HASH_ITER(hh, server_table, server, next) { char entry_buf[64]; int entry_len = snprintf(entry_buf, sizeof(entry_buf), "\"%s\":%" PRIu64 ",", @@ -1191,18 +1198,18 @@ create_server_socket(const char *host, const char *port) } for (/*rp = result*/; rp != NULL; rp = rp->ai_next) { - server_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + server_sock = ss_socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (server_sock == -1) { continue; } if (rp->ai_family == AF_INET6) { int ipv6only = host ? 1 : 0; - setsockopt(server_sock, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, sizeof(ipv6only)); + ss_setsockopt(server_sock, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, sizeof(ipv6only)); } int opt = 1; - setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ss_setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); s = bind(server_sock, rp->ai_addr, rp->ai_addrlen); if (s == 0) { @@ -1212,7 +1219,7 @@ create_server_socket(const char *host, const char *port) ERROR("bind"); } - close(server_sock); + ss_socket_close(server_sock); } if (result != NULL) { @@ -1505,7 +1512,7 @@ main(int argc, char **argv) struct passwd *pw = getpwuid(getuid()); if (workdir == NULL || strlen(workdir) == 0) { - workdir = pw->pw_dir; + workdir = pw != NULL ? pw->pw_dir : NULL; // If home dir is still not defined or set to nologin/nonexistent, fall back to /tmp if (workdir == NULL || strlen(workdir) == 0 || strstr(workdir, "nologin") || strstr(workdir, "nonexistent") || strcmp(workdir, "/") == 0) { @@ -1520,6 +1527,9 @@ main(int argc, char **argv) working_dir = ss_malloc(working_dir_size); snprintf(working_dir, working_dir_size, "%s", workdir); } + if (working_dir == NULL) { + FATAL("unable to allocate working directory"); + } LOGI("working directory points to %s", working_dir); int err = mkdir(working_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); @@ -1597,7 +1607,7 @@ main(int argc, char **argv) FATAL("Couldn't open the directory"); } - server_table = cork_string_hash_table_new(MAX_PORT_NUM, 0); + server_table = NULL; if (conf != NULL) { for (i = 0; i < conf->port_password_num; i++) { @@ -1631,7 +1641,7 @@ main(int argc, char **argv) FATAL("manager unix socket path is too long"); } - sfd = socket(AF_UNIX, SOCK_DGRAM, 0); /* Create server socket */ + sfd = ss_socket(AF_UNIX, SOCK_DGRAM, 0); /* Create server socket */ if (sfd == -1) { ss_free(working_dir); FATAL("socket"); @@ -1674,14 +1684,12 @@ main(int argc, char **argv) } // Clean up - struct cork_hash_table_entry *entry; - struct cork_hash_table_iterator server_iter; - - cork_hash_table_iterator_init(server_table, &server_iter); - - while ((entry = cork_hash_table_iterator_next(&server_iter)) != NULL) { - struct server *server = (struct server *)entry->value; + struct server *server, *next; + HASH_ITER(hh, server_table, server, next) { stop_server(working_dir, server->port); + HASH_DEL(server_table, server); + destroy_server(server); + ss_free(server); } ev_signal_stop(EV_DEFAULT, &sigint_watcher); diff --git a/src/manager.h b/src/manager.h index d5aa83276..11e6c728c 100644 --- a/src/manager.h +++ b/src/manager.h @@ -24,7 +24,8 @@ #define _MANAGER_H #include -#include +#include "core.h" +#include "uthash.h" #ifdef HAVE_LIBEV_EV_H #include @@ -66,6 +67,7 @@ struct manager_ctx { }; struct server { + UT_hash_handle hh; char port[8]; char password[128]; char fast_open[8]; diff --git a/src/netutils.c b/src/netutils.c index 1c9259263..44f0cb410 100644 --- a/src/netutils.c +++ b/src/netutils.c @@ -25,7 +25,8 @@ #include #include -#include +#include "core.h" +#include #ifdef HAVE_CONFIG_H #include "config.h" @@ -57,7 +58,7 @@ static const char valid_label_bytes[] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; static int -parse_numeric_port(const char *port, in_port_t *port_out) +parse_numeric_port(const char *port, uint16_t *port_out) { char *endptr; unsigned long value; @@ -72,7 +73,7 @@ parse_numeric_port(const char *port, in_port_t *port_out) return -1; } - *port_out = (in_port_t)value; + *port_out = (uint16_t)value; return 0; } @@ -80,7 +81,7 @@ int set_reuseport(int socket) { int opt = 1; - return setsockopt(socket, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); + return ss_setsockopt(socket, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); } size_t @@ -101,7 +102,7 @@ setinterface(int socket_fd, const char *interface_name) struct ifreq interface; memset(&interface, 0, sizeof(struct ifreq)); strncpy(interface.ifr_name, interface_name, IFNAMSIZ - 1); - int res = setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, &interface, + int res = ss_setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, &interface, sizeof(struct ifreq)); return res; } @@ -114,8 +115,8 @@ parse_local_addr(struct sockaddr_storage *storage_v4, const char *host) { if (host != NULL) { - struct cork_ip ip; - if (cork_ip_init(&ip, host) != -1) { + struct ss_ip ip; + if (ss_ip_init(&ip, host) != -1) { if (ip.version == 4) { memset(storage_v4, 0, sizeof(struct sockaddr_storage)); struct sockaddr_in *addr = (struct sockaddr_in *)storage_v4; @@ -153,9 +154,9 @@ get_sockaddr(char *host, char *port, struct sockaddr_storage *storage, int block, int ipv6first) { - struct cork_ip ip; - if (cork_ip_init(&ip, host) != -1) { - in_port_t numeric_port = 0; + struct ss_ip ip; + if (ss_ip_init(&ip, host) != -1) { + uint16_t numeric_port = 0; if (port != NULL && parse_numeric_port(port, &numeric_port) == -1) { LOGE("invalid port: %s", port); return -1; diff --git a/src/netutils.h b/src/netutils.h index 47320a047..445d4018d 100644 --- a/src/netutils.h +++ b/src/netutils.h @@ -22,11 +22,13 @@ #ifndef _NETUTILS_H #define _NETUTILS_H +#include "platform.h" #ifdef __MINGW32__ -#include "winsock.h" +#include "ss_windows.h" #else #include +#include #endif #if defined(HAVE_LINUX_TCP_H) diff --git a/src/platform.h b/src/platform.h new file mode 100644 index 000000000..c6ec1eff2 --- /dev/null +++ b/src/platform.h @@ -0,0 +1,120 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#ifndef SS_PLATFORM_H +#define SS_PLATFORM_H +#include +#include +#include +#include +#ifdef _WIN32 +#include "ss_windows.h" +typedef SOCKET ss_socket_t; +#else +#include +#include +#include +#include +#include +typedef int ss_socket_t; +#endif + +/* Socket errors are separate from CRT errno on Windows. Return normalized + * errno constants for portable branching without redefining libc names. */ +static inline int +ss_socket_error(void) +{ +#ifdef _WIN32 + int error = WSAGetLastError(); + switch (error) { + case WSAEWOULDBLOCK: return EWOULDBLOCK; + case WSAEINPROGRESS: return EINPROGRESS; + case WSAEINTR: return EINTR; + case WSAEINVAL: return EINVAL; + case WSAEOPNOTSUPP: return EOPNOTSUPP; + case WSAEPROTONOSUPPORT: return EPROTONOSUPPORT; + case WSAENOPROTOOPT: return ENOPROTOOPT; + case WSAECONNRESET: return ECONNRESET; + case WSAENOTCONN: return ENOTCONN; + case WSAETIMEDOUT: return ETIMEDOUT; + default: return error; + } +#else + return errno; +#endif +} + +static inline int +ss_socket_close(ss_socket_t fd) +{ +#ifdef _WIN32 + return closesocket(fd); +#else + return close(fd); +#endif +} + +static inline ss_socket_t +ss_socket_noinherit(ss_socket_t fd) +{ +#ifdef _WIN32 + if (fd != INVALID_SOCKET && !SetHandleInformation((HANDLE)fd, HANDLE_FLAG_INHERIT, 0)) { + closesocket(fd); + WSASetLastError(WSAEINVAL); + return INVALID_SOCKET; + } +#else + if (fd >= 0 && fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { + int error = errno; + close(fd); + errno = error; + return -1; + } +#endif + return fd; +} + +static inline ss_socket_t +ss_socket(int family, int type, int protocol) +{ + return ss_socket_noinherit(socket(family, type, protocol)); +} + +static inline ss_socket_t +ss_accept(ss_socket_t fd, struct sockaddr *address, socklen_t *length) +{ + return ss_socket_noinherit(accept(fd, address, length)); +} + +static inline int +ss_setsockopt(ss_socket_t fd, int level, int option, const void *value, socklen_t length) +{ +#ifdef _WIN32 + return setsockopt(fd, level, option, (const char *)value, length); +#else + return setsockopt(fd, level, option, value, length); +#endif +} + +static inline const char * +ss_inet_ntop(int family, const void *address, char *text, socklen_t length) +{ +#ifdef _WIN32 + return inet_ntop(family, (void *)address, text, length); +#else + return inet_ntop(family, address, text, length); +#endif +} + +static inline double +ss_monotonic_time(void) +{ +#ifdef _WIN32 + LARGE_INTEGER counter, frequency; + if (!QueryPerformanceCounter(&counter) || !QueryPerformanceFrequency(&frequency)) abort(); + return (double)counter.QuadPart / (double)frequency.QuadPart; +#else + struct timespec value; + if (clock_gettime(CLOCK_MONOTONIC, &value) != 0) abort(); + return (double)value.tv_sec + (double)value.tv_nsec / 1000000000.0; +#endif +} +#endif diff --git a/src/plugin.c b/src/plugin.c index fe244af6f..d1aaacb7e 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -25,6 +25,7 @@ #endif #include +#include #ifndef __MINGW32__ #include #include @@ -32,12 +33,12 @@ #include #endif -#include -#include +#include "ss_process.h" +#include "platform.h" #include "utils.h" #include "plugin.h" -#include "winsock.h" +#include "ss_windows.h" #define CMD_RESRV_LEN 128 @@ -47,49 +48,8 @@ #define TEMPDIR #endif -static int exit_code; -static struct cork_env *env = NULL; -static struct cork_exec *exec = NULL; -static struct cork_subprocess *sub = NULL; -#ifdef __MINGW32__ -static uint16_t sub_control_port = 0; -void cork_subprocess_set_control(struct cork_subprocess *self, uint16_t port); -#endif - -static int -plugin_log__data(struct cork_stream_consumer *vself, - const void *buf, size_t size, bool is_first) -{ - size_t bytes_written = fwrite(buf, 1, size, stderr); - /* If there was an error writing to the file, then signal this - * to the producer */ - if (bytes_written == size) { - return 0; - } else { - cork_system_error_set(); - return -1; - } -} - -static int -plugin_log__eof(struct cork_stream_consumer *vself) -{ - /* We don't close the file, so there's nothing special to do at - * end-of-stream. */ - return 0; -} - -static void -plugin_log__free(struct cork_stream_consumer *vself) -{ - return; -} - -struct cork_stream_consumer plugin_log = { - .data = plugin_log__data, - .eof = plugin_log__eof, - .free = plugin_log__free, -}; +static struct ss_process *sub; +static uint16_t sub_control_port; static int start_ss_plugin(const char *plugin, @@ -100,32 +60,26 @@ start_ss_plugin(const char *plugin, const char *local_port, enum plugin_mode mode) { - cork_env_add(env, "SS_REMOTE_HOST", remote_host); - cork_env_add(env, "SS_REMOTE_PORT", remote_port); + ss_process_env(sub, "SS_REMOTE_HOST", remote_host); + ss_process_env(sub, "SS_REMOTE_PORT", remote_port); - cork_env_add(env, "SS_LOCAL_HOST", local_host); - cork_env_add(env, "SS_LOCAL_PORT", local_port); + ss_process_env(sub, "SS_LOCAL_HOST", local_host); + ss_process_env(sub, "SS_LOCAL_PORT", local_port); if (plugin_opts != NULL) - cork_env_add(env, "SS_PLUGIN_OPTIONS", plugin_opts); + ss_process_env(sub, "SS_PLUGIN_OPTIONS", plugin_opts); - exec = cork_exec_new(plugin); - cork_exec_add_param(exec, plugin); // argv[0] + ss_process_arg(sub, plugin); // argv[0] #ifdef __ANDROID__ extern int vpn; if (vpn) - cork_exec_add_param(exec, "-V"); + ss_process_arg(sub, "-V"); #endif - cork_exec_set_env(exec, env); - sub = cork_subprocess_new_exec(exec, NULL, NULL, &exit_code); -#ifdef __MINGW32__ - cork_subprocess_set_control(sub, sub_control_port); -#endif - return cork_subprocess_start(sub); + return ss_process_start(sub, sub_control_port); } #define OBFSPROXY_OPTS_MAX 4096 @@ -173,24 +127,20 @@ start_obfsproxy(const char *plugin, opts_dump = strndup(plugin_opts, OBFSPROXY_OPTS_MAX); if (!opts_dump) { ERROR("start_obfsproxy strndup failed"); - if (env != NULL) { - cork_env_free(env); - } return -ENOMEM; } } - exec = cork_exec_new(plugin); /* The first parameter will be skipped, so pass @plugin again */ - cork_exec_add_param(exec, plugin); + ss_process_arg(sub, plugin); - cork_exec_add_param(exec, "--data-dir"); + ss_process_arg(sub, "--data-dir"); buf_size = 20 + strlen(plugin) + strlen(remote_host) + strlen(remote_port) + strlen(local_host) + strlen(local_port); buf = ss_malloc(buf_size); snprintf(buf, buf_size, TEMPDIR "%s_%s:%s_%s:%s", plugin, remote_host, remote_port, local_host, local_port); - cork_exec_add_param(exec, buf); + ss_process_arg(sub, buf); /* * Iterate @plugin_opts by space @@ -198,7 +148,7 @@ start_obfsproxy(const char *plugin, if (opts_dump != NULL) { pch = strtok(opts_dump, " "); while (pch) { - cork_exec_add_param(exec, pch); + ss_process_arg(sub, pch); pch = strtok(NULL, " "); } } @@ -206,28 +156,23 @@ start_obfsproxy(const char *plugin, /* The rest options */ if (mode == MODE_CLIENT) { /* Client mode */ - cork_exec_add_param(exec, "--dest"); + ss_process_arg(sub, "--dest"); snprintf(buf, buf_size, "%s:%s", remote_host, remote_port); - cork_exec_add_param(exec, buf); - cork_exec_add_param(exec, "client"); + ss_process_arg(sub, buf); + ss_process_arg(sub, "client"); snprintf(buf, buf_size, "%s:%s", local_host, local_port); - cork_exec_add_param(exec, buf); + ss_process_arg(sub, buf); } else { /* Server mode */ - cork_exec_add_param(exec, "--dest"); + ss_process_arg(sub, "--dest"); snprintf(buf, buf_size, "%s:%s", local_host, local_port); - cork_exec_add_param(exec, buf); - cork_exec_add_param(exec, "server"); + ss_process_arg(sub, buf); + ss_process_arg(sub, "server"); snprintf(buf, buf_size, "%s:%s", remote_host, remote_port); - cork_exec_add_param(exec, buf); + ss_process_arg(sub, buf); } - cork_exec_set_env(exec, env); - sub = cork_subprocess_new_exec(exec, NULL, NULL, &exit_code); -#ifdef __MINGW32__ - cork_subprocess_set_control(sub, sub_control_port); -#endif - ret = cork_subprocess_start(sub); + ret = ss_process_start(sub, sub_control_port); ss_free(opts_dump); free(buf); return ret; @@ -253,9 +198,9 @@ start_plugin(const char *plugin, if (strlen(plugin) == 0) return 0; -#ifndef __MINGW32__ - env = cork_env_clone_current(); -#else + stop_plugin(); + sub = ss_process_new(plugin); +#ifdef __MINGW32__ sub_control_port = control_port; #endif @@ -265,14 +210,14 @@ start_plugin(const char *plugin, else ret = start_ss_plugin(plugin, plugin_opts, remote_host, remote_port, local_host, local_port, mode); - env = NULL; + if (ret != 0) stop_plugin(); return ret; } uint16_t get_local_port() { - int sock = socket(AF_INET, SOCK_STREAM, 0); + int sock = ss_socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { return 0; } @@ -283,16 +228,16 @@ get_local_port() serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = 0; if (bind(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { - close(sock); + ss_socket_close(sock); return 0; } socklen_t len = sizeof(serv_addr); if (getsockname(sock, (struct sockaddr *)&serv_addr, &len) == -1) { - close(sock); + ss_socket_close(sock); return 0; } - if (close(sock) < 0) { + if (ss_socket_close(sock) < 0) { return 0; } @@ -300,24 +245,14 @@ get_local_port() } void -stop_plugin() +stop_plugin(void) { - if (sub != NULL) { - cork_subprocess_abort(sub); -#ifndef __MINGW32__ - if (cork_subprocess_wait(sub) == -1) { - LOGI("error on terminating the plugin."); - } -#endif - cork_subprocess_free(sub); - } + ss_process_free(sub); + sub = NULL; } int -is_plugin_running() +is_plugin_running(void) { - if (sub != NULL) { - return cork_subprocess_is_finished(sub); - } - return 0; + return ss_process_running(sub); } diff --git a/src/plugin_disabled.c b/src/plugin_disabled.c new file mode 100644 index 000000000..f117ef0f5 --- /dev/null +++ b/src/plugin_disabled.c @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#include +#include "plugin.h" +#include "utils.h" +int +start_plugin(const char *plugin, const char *options, const char *remote_host, + const char *remote_port, const char *local_host, const char *local_port, +#ifdef __MINGW32__ + uint16_t control_port, +#endif + enum plugin_mode mode) +{ + (void)plugin; (void)options; (void)remote_host; (void)remote_port; + (void)local_host; (void)local_port; (void)mode; +#ifdef __MINGW32__ + (void)control_port; +#endif + LOGE("Plugin support is disabled in this build"); + return -1; +} +uint16_t get_local_port(void) { FATAL("Plugin support is disabled in this build"); return 0; } +void stop_plugin(void) {} +int is_plugin_running(void) { return 0; } diff --git a/src/process.c b/src/process.c new file mode 100644 index 000000000..d6c6a041b --- /dev/null +++ b/src/process.c @@ -0,0 +1,344 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#include "ss_process.h" +#include +#include +#include +#include "utils.h" +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +extern char **environ; +#endif + +struct ss_process { + char *program; + char **argv, **env; + size_t argc, envc; +#ifdef _WIN32 + HANDLE child, job, monitor; + uint16_t control_port; +#else + pid_t pid; +#endif +}; + +static char * +copy_string(const char *text) +{ + size_t size = strlen(text) + 1; + char *copy = ss_malloc(size); + memcpy(copy, text, size); + return copy; +} + +struct ss_process * +ss_process_new(const char *program) +{ + struct ss_process *process = ss_malloc(sizeof(*process)); + memset(process, 0, sizeof(*process)); + process->program = copy_string(program); + return process; +} + +void +ss_process_arg(struct ss_process *process, const char *argument) +{ + process->argv = ss_realloc(process->argv, (process->argc + 2) * sizeof(char *)); + process->argv[process->argc++] = copy_string(argument); + process->argv[process->argc] = NULL; +} + +void +ss_process_env(struct ss_process *process, const char *name, const char *value) +{ + size_t size = strlen(name) + strlen(value) + 2; + char *entry = ss_malloc(size); + snprintf(entry, size, "%s=%s", name, value); + process->env = ss_realloc(process->env, (process->envc + 2) * sizeof(char *)); + process->env[process->envc++] = entry; + process->env[process->envc] = NULL; +} + +#ifndef _WIN32 +static bool +overridden(const struct ss_process *process, const char *entry) +{ + const char *equals = strchr(entry, '='); + if (equals == NULL) { + return false; + } + size_t length = (size_t)(equals - entry) + 1; + for (size_t i = 0; i < process->envc; i++) { + if (strncmp(entry, process->env[i], length) == 0) { + return true; + } + } + return false; +} + +int +ss_process_start(struct ss_process *process, uint16_t control_port) +{ + (void)control_port; + size_t count = 0, used = 0; + while (environ[count] != NULL) { + count++; + } + char **env = ss_malloc((count + process->envc + 1) * sizeof(char *)); + for (size_t i = 0; i < count; i++) { + if (!overridden(process, environ[i])) { + env[used++] = environ[i]; + } + } + for (size_t i = 0; i < process->envc; i++) { + env[used++] = process->env[i]; + } + env[used] = NULL; + posix_spawnattr_t attr; + int error = posix_spawnattr_init(&attr); + if (error == 0) { + sigset_t empty, defaults; + sigemptyset(&empty); + sigemptyset(&defaults); + sigaddset(&defaults, SIGPIPE); + sigaddset(&defaults, SIGTERM); + sigaddset(&defaults, SIGINT); + error = posix_spawnattr_setpgroup(&attr, 0); + if (error == 0) error = posix_spawnattr_setsigmask(&attr, &empty); + if (error == 0) error = posix_spawnattr_setsigdefault(&attr, &defaults); + if (error == 0) error = posix_spawnattr_setflags(&attr, + POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF); + if (error == 0) error = posix_spawnp(&process->pid, process->program, NULL, + &attr, process->argv, env); + posix_spawnattr_destroy(&attr); + } + free(env); + if (error != 0) { + process->pid = 0; + errno = error; + return -1; + } + return 0; +} + +bool +ss_process_running(struct ss_process *process) +{ + if (process == NULL || process->pid == 0) return false; + siginfo_t info; + memset(&info, 0, sizeof(info)); + int result; + do { + result = waitid(P_PID, (id_t)process->pid, &info, WEXITED | WNOHANG | WNOWAIT); + } while (result < 0 && errno == EINTR); + /* Preserve the zombie until cleanup so its process-group ID cannot be + * reused before all descendants have been signaled. */ + if (result < 0 && errno == ECHILD) process->pid = 0; + return result == 0 && info.si_pid == 0; +} + +static void +stop_process(struct ss_process *process) +{ + if (process->pid == 0) return; + kill(-process->pid, SIGTERM); + for (unsigned i = 0; i < 50 && ss_process_running(process); i++) { + struct timespec delay = {0, 10000000}; + nanosleep(&delay, NULL); + } + if (process->pid != 0) { + kill(-process->pid, SIGKILL); + while (waitpid(process->pid, NULL, 0) < 0 && errno == EINTR) {} + process->pid = 0; + } +} +#else +static wchar_t * +wide_string(const char *text) +{ + int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text, -1, NULL, 0); + if (count == 0) return NULL; + wchar_t *wide = ss_malloc((size_t)count * sizeof(wchar_t)); + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text, -1, wide, count)) { + free(wide); + return NULL; + } + return wide; +} + +static DWORD WINAPI +monitor_process(void *argument) +{ + struct ss_process *process = argument; + WaitForSingleObject(process->child, INFINITE); + if (process->control_port != 0) { + SOCKET fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (fd != INVALID_SOCKET) { + struct sockaddr_in address = {0}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = htons(process->control_port); + if (connect(fd, (struct sockaddr *)&address, sizeof(address)) == 0) { + send(fd, "\0", 1, 0); + } + closesocket(fd); + } + } + return 0; +} + +static int +compare_environment(const void *left, const void *right) +{ + return _wcsicmp(*(const wchar_t *const *)left, *(const wchar_t *const *)right); +} + +static wchar_t * +sort_environment(wchar_t *env) +{ + size_t count = 0, length = 1; + for (wchar_t *entry = env; *entry; entry += wcslen(entry) + 1) { + count++; + length += wcslen(entry) + 1; + } + wchar_t **entries = ss_malloc((count + 1) * sizeof(*entries)); + size_t index = 0; + for (wchar_t *entry = env; *entry; entry += wcslen(entry) + 1) entries[index++] = entry; + qsort(entries, count, sizeof(*entries), compare_environment); + wchar_t *sorted = ss_malloc((length + 1) * sizeof(*sorted)), *out = sorted; + for (size_t i = 0; i < count; i++) { + size_t size = wcslen(entries[i]) + 1; + memcpy(out, entries[i], size * sizeof(*out)); + out += size; + } + *out++ = L'\0'; + *out = L'\0'; + free(entries); + free(env); + return sorted; +} + +int +ss_process_start(struct ss_process *process, uint16_t control_port) +{ + /* Quote each argv element according to the Windows C runtime rules. + * Double backslashes before a quote or the closing quote. */ + size_t capacity = 1; + for (size_t i = 0; i < process->argc; i++) capacity += 2 * strlen(process->argv[i]) + 4; + char *command = ss_malloc(capacity), *out = command; + for (size_t i = 0; i < process->argc; i++) { + if (i != 0) *out++ = ' '; + *out++ = '"'; + const char *arg = process->argv[i]; + while (*arg != '\0') { + size_t slashes = 0; + while (*arg == '\\') { slashes++; arg++; } + size_t copies = (*arg == '"' || *arg == '\0') ? slashes * 2 : slashes; + while (copies-- != 0) *out++ = '\\'; + if (*arg == '\0') break; + if (*arg == '"') *out++ = '\\'; + *out++ = *arg++; + } + *out++ = '"'; + } + *out = '\0'; + wchar_t *wide_command = wide_string(command); + free(command); + if (wide_command == NULL) return -1; + + wchar_t *inherited = GetEnvironmentStringsW(); + if (inherited == NULL) { free(wide_command); return -1; } + wchar_t **overrides = ss_malloc((process->envc + 1) * sizeof(wchar_t *)); + size_t total = 2; + for (const wchar_t *entry = inherited; *entry; entry += wcslen(entry) + 1) total += wcslen(entry) + 1; + for (size_t i = 0; i < process->envc; i++) { + overrides[i] = wide_string(process->env[i]); + if (overrides[i] == NULL) { + for (size_t j = 0; j < i; j++) free(overrides[j]); + free(overrides); FreeEnvironmentStringsW(inherited); free(wide_command); + return -1; + } + total += wcslen(overrides[i]) + 1; + } + wchar_t *env = ss_malloc(total * sizeof(wchar_t)), *dest = env; + for (const wchar_t *entry = inherited; *entry; entry += wcslen(entry) + 1) { + const wchar_t *equals = wcschr(entry, L'='); + bool replace = false; + if (equals != NULL && equals != entry) { + for (size_t i = 0; i < process->envc; i++) { + if (_wcsnicmp(entry, overrides[i], (size_t)(equals - entry) + 1) == 0) replace = true; + } + } + if (!replace) { + size_t length = wcslen(entry) + 1; + memcpy(dest, entry, length * sizeof(wchar_t)); dest += length; + } + } + for (size_t i = 0; i < process->envc; i++) { + size_t length = wcslen(overrides[i]) + 1; + memcpy(dest, overrides[i], length * sizeof(wchar_t)); dest += length; + free(overrides[i]); + } + *dest++ = L'\0'; *dest = L'\0'; + free(overrides); FreeEnvironmentStringsW(inherited); + env = sort_environment(env); + STARTUPINFOW startup = {0}; + PROCESS_INFORMATION child = {0}; + startup.cb = sizeof(startup); + process->job = CreateJobObjectW(NULL, NULL); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {0}; + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + BOOL ok = process->job != NULL && SetInformationJobObject(process->job, + JobObjectExtendedLimitInformation, &limits, sizeof(limits)); + if (ok) ok = CreateProcessW(NULL, wide_command, NULL, NULL, FALSE, + CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED, env, NULL, &startup, &child); + free(env); free(wide_command); + if (ok) { + process->child = child.hProcess; + ok = AssignProcessToJobObject(process->job, child.hProcess); + if (ok) ok = ResumeThread(child.hThread) != (DWORD)-1; + CloseHandle(child.hThread); + } + if (!ok) { + if (process->child) TerminateProcess(process->child, 1); + return -1; + } + process->control_port = control_port; + process->monitor = CreateThread(NULL, 0, monitor_process, process, 0, NULL); + return process->monitor == NULL ? -1 : 0; +} + +bool +ss_process_running(struct ss_process *process) +{ + return process != NULL && process->child != NULL && + WaitForSingleObject(process->child, 0) == WAIT_TIMEOUT; +} + +static void +stop_process(struct ss_process *process) +{ + if (process->job) { CloseHandle(process->job); process->job = NULL; } + if (process->monitor) { + WaitForSingleObject(process->monitor, INFINITE); + CloseHandle(process->monitor); + } + if (process->child) { WaitForSingleObject(process->child, INFINITE); CloseHandle(process->child); } +} +#endif + +void +ss_process_free(struct ss_process *process) +{ + if (process == NULL) return; + stop_process(process); + for (size_t i = 0; i < process->argc; i++) free(process->argv[i]); + for (size_t i = 0; i < process->envc; i++) free(process->env[i]); + free(process->argv); free(process->env); free(process->program); free(process); +} diff --git a/src/redir.c b/src/redir.c index 800a55c1b..6f1b223c8 100644 --- a/src/redir.c +++ b/src/redir.c @@ -41,7 +41,7 @@ #include #include -#include +#include "core.h" #ifdef HAVE_CONFIG_H #include "config.h" @@ -167,15 +167,15 @@ create_and_bind(const char *addr, const char *port) } for (rp = result; rp != NULL; rp = rp->ai_next) { - listen_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + listen_sock = ss_socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (listen_sock == -1) { continue; } int opt = 1; - setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif if (reuse_port) { int err = set_reuseport(listen_sock); @@ -194,7 +194,7 @@ create_and_bind(const char *addr, const char *port) optname = IPV6_TRANSPARENT; } - if (setsockopt(listen_sock, level, optname, &opt, sizeof(opt)) != 0) { + if (ss_setsockopt(listen_sock, level, optname, &opt, sizeof(opt)) != 0) { ERROR("setsockopt IP_TRANSPARENT"); exit(EXIT_FAILURE); } @@ -202,11 +202,11 @@ create_and_bind(const char *addr, const char *port) } if (tcp_incoming_sndbuf > 0) { - setsockopt(listen_sock, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int)); } if (tcp_incoming_rcvbuf > 0) { - setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int)); } s = bind(listen_sock, rp->ai_addr, rp->ai_addrlen); @@ -217,7 +217,7 @@ create_and_bind(const char *addr, const char *port) ERROR("bind"); } - close(listen_sock); + ss_socket_close(listen_sock); listen_sock = -1; } @@ -244,7 +244,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } else if (r == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data // continue to wait for recv return; @@ -265,11 +265,11 @@ server_recv_cb(EV_P_ ev_io *w, int revents) if (AF_INET == server->destaddr.ss_family) { struct sockaddr_in *sa = (struct sockaddr_in *)&(server->destaddr); - inet_ntop(AF_INET, &(sa->sin_addr), ipstr, INET_ADDRSTRLEN); + ss_inet_ntop(AF_INET, &(sa->sin_addr), ipstr, INET_ADDRSTRLEN); port = ntohs(sa->sin_port); } else { struct sockaddr_in6 *sa = (struct sockaddr_in6 *)&(server->destaddr); - inet_ntop(AF_INET6, &(sa->sin6_addr), ipstr, INET6_ADDRSTRLEN); + ss_inet_ntop(AF_INET6, &(sa->sin6_addr), ipstr, INET6_ADDRSTRLEN); port = ntohs(sa->sin6_port); } @@ -294,7 +294,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) int s = send(remote->fd, remote->buf->data, remote->buf->len, 0); if (s == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data, wait for send remote->buf->idx = 0; ev_io_stop(EV_A_ & server_recv_ctx->io); @@ -334,7 +334,7 @@ server_send_cb(EV_P_ ev_io *w, int revents) ssize_t s = send(server->fd, server->buf->data + server->buf->idx, server->buf->len, 0); if (s == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (ss_socket_error() != EAGAIN && ss_socket_error() != EWOULDBLOCK) { ERROR("send"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); @@ -358,7 +358,7 @@ server_send_cb(EV_P_ ev_io *w, int revents) static void delayed_connect_cb(EV_P_ ev_timer *watcher, int revents) { - server_t *server = cork_container_of(watcher, server_t, + server_t *server = ss_container_of(watcher, server_t, delayed_connect_watcher); remote_t *remote = server->remote; @@ -367,7 +367,7 @@ delayed_connect_cb(EV_P_ ev_timer *watcher, int revents) remote->addr = NULL; - if (r == -1 && errno != CONNECT_IN_PROGRESS) { + if (r == -1 && ss_socket_error() != CONNECT_IN_PROGRESS) { ERROR("connect"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); @@ -383,7 +383,7 @@ static void remote_timeout_cb(EV_P_ ev_timer *watcher, int revents) { remote_ctx_t *remote_ctx - = cork_container_of(watcher, remote_ctx_t, watcher); + = ss_container_of(watcher, remote_ctx_t, watcher); remote_t *remote = remote_ctx->remote; server_t *server = remote->server; @@ -409,7 +409,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } else if (r == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data // continue to wait for recv return; @@ -436,7 +436,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) int s = send(server->fd, server->buf->data, server->buf->len, 0); if (s == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data, wait for send server->buf->idx = 0; ev_io_stop(EV_A_ & remote_recv_ctx->io); @@ -457,8 +457,8 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) // Disable TCP_NODELAY after the first response are sent if (!remote->recv_ctx->connected && !no_delay) { int opt = 0; - setsockopt(server->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); - setsockopt(remote->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(server->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(remote->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); } remote->recv_ctx->connected = 1; } @@ -557,7 +557,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) if (remote->addr != NULL) { #if defined(TCP_FASTOPEN_CONNECT) int optval = 1; - if (setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, + if (ss_setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, (void *)&optval, sizeof(optval)) < 0) FATAL("failed to set TCP_FASTOPEN_CONNECT"); s = connect(remote->fd, remote->addr, get_sockaddr_len(remote->addr)); @@ -574,12 +574,12 @@ remote_send_cb(EV_P_ ev_io *w, int revents) remote->addr = NULL; if (s == -1) { - if (errno == CONNECT_IN_PROGRESS) { + if (ss_socket_error() == CONNECT_IN_PROGRESS) { ev_io_start(EV_A_ & remote_send_ctx->io); ev_timer_start(EV_A_ & remote_send_ctx->watcher); } else { - if (errno == EOPNOTSUPP || errno == EPROTONOSUPPORT || - errno == ENOPROTOOPT) { + if (ss_socket_error() == EOPNOTSUPP || ss_socket_error() == EPROTONOSUPPORT || + ss_socket_error() == ENOPROTOOPT) { fast_open = 0; LOGE("fast open is not supported on this platform"); } else { @@ -596,7 +596,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) } if (s == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (ss_socket_error() != EAGAIN && ss_socket_error() != EWOULDBLOCK) { ERROR("send"); // close and free close_and_free_remote(EV_A_ remote); @@ -667,7 +667,7 @@ close_and_free_remote(EV_P_ remote_t *remote) ev_timer_stop(EV_A_ & remote->send_ctx->watcher); ev_io_stop(EV_A_ & remote->send_ctx->io); ev_io_stop(EV_A_ & remote->recv_ctx->io); - close(remote->fd); + ss_socket_close(remote->fd); free_remote(remote); } } @@ -735,7 +735,7 @@ close_and_free_server(EV_P_ server_t *server) ev_io_stop(EV_A_ & server->send_ctx->io); ev_io_stop(EV_A_ & server->recv_ctx->io); ev_timer_stop(EV_A_ & server->delayed_connect_watcher); - close(server->fd); + ss_socket_close(server->fd); free_server(server); } } @@ -749,7 +749,7 @@ accept_cb(EV_P_ ev_io *w, int revents) int err; - int serverfd = accept(listener->fd, NULL, NULL); + int serverfd = ss_accept(listener->fd, NULL, NULL); if (serverfd == -1) { ERROR("accept"); return; @@ -763,9 +763,9 @@ accept_cb(EV_P_ ev_io *w, int revents) setnonblocking(serverfd); int opt = 1; - setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif int index = (int)randombytes_uniform((uint32_t)listener->remote_num); @@ -775,16 +775,16 @@ accept_cb(EV_P_ ev_io *w, int revents) if (listener->mptcp < 0) { protocol = IPPROTO_MPTCP; // Enable upstream MPTCP } - int remotefd = socket(remote_addr->sa_family, SOCK_STREAM, protocol); + int remotefd = ss_socket(remote_addr->sa_family, SOCK_STREAM, protocol); if (remotefd == -1) { ERROR("socket"); return; } // Set flags - setsockopt(remotefd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(remotefd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(remotefd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif // Enable TCP keepalive feature @@ -792,37 +792,37 @@ accept_cb(EV_P_ ev_io *w, int revents) int keepIdle = 40; int keepInterval = 20; int keepCount = 5; - setsockopt(remotefd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive)); - setsockopt(remotefd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepIdle, sizeof(keepIdle)); - setsockopt(remotefd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval)); - setsockopt(remotefd, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive)); + ss_setsockopt(remotefd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepIdle, sizeof(keepIdle)); + ss_setsockopt(remotefd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval)); + ss_setsockopt(remotefd, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount)); // Set non blocking setnonblocking(remotefd); if (listener->tos >= 0) { - int rc = setsockopt(remotefd, IPPROTO_IP, IP_TOS, &listener->tos, sizeof(listener->tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv4 dscp failed: %d", errno); + int rc = ss_setsockopt(remotefd, IPPROTO_IP, IP_TOS, &listener->tos, sizeof(listener->tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv4 dscp failed: %d", ss_socket_error()); } #ifdef IPV6_TCLASS - rc = setsockopt(remotefd, IPPROTO_IPV6, IPV6_TCLASS, &listener->tos, sizeof(listener->tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv6 dscp failed: %d", errno); + rc = ss_setsockopt(remotefd, IPPROTO_IPV6, IPV6_TCLASS, &listener->tos, sizeof(listener->tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv6 dscp failed: %d", ss_socket_error()); } #endif } // Enable out-of-tree MPTCP if (listener->mptcp > 1) { - int err = setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); + int err = ss_setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); if (err == -1) { ERROR("failed to enable out-of-tree multipath TCP"); } } else if (listener->mptcp == 1) { int i = 0; while ((listener->mptcp = mptcp_enabled_values[i]) > 0) { - int err = setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); + int err = ss_setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); if (err != -1) { break; } @@ -834,11 +834,11 @@ accept_cb(EV_P_ ev_io *w, int revents) } if (tcp_outgoing_sndbuf > 0) { - setsockopt(remotefd, SOL_SOCKET, SO_SNDBUF, &tcp_outgoing_sndbuf, sizeof(int)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_SNDBUF, &tcp_outgoing_sndbuf, sizeof(int)); } if (tcp_outgoing_rcvbuf > 0) { - setsockopt(remotefd, SOL_SOCKET, SO_RCVBUF, &tcp_outgoing_rcvbuf, sizeof(int)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_RCVBUF, &tcp_outgoing_rcvbuf, sizeof(int)); } server_t *server = new_server(serverfd); @@ -854,7 +854,7 @@ accept_cb(EV_P_ ev_io *w, int revents) } else { int r = connect(remotefd, remote_addr, get_sockaddr_len(remote_addr)); - if (r == -1 && errno != CONNECT_IN_PROGRESS) { + if (r == -1 && ss_socket_error() != CONNECT_IN_PROGRESS) { ERROR("connect"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); diff --git a/src/resolv.c b/src/resolv.c index 8d203fd9f..9c42d2f75 100644 --- a/src/resolv.c +++ b/src/resolv.c @@ -39,7 +39,7 @@ #include #include #else -#include "winsock.h" // Should be before +#include "ss_windows.h" // Should be before #endif #include @@ -49,18 +49,12 @@ #include #endif -#include +#include "core.h" #include "resolv.h" #include "utils.h" #include "netutils.h" -#ifdef __MINGW32__ -#define CONV_STATE_CB (ares_sock_state_cb) -#else -#define CONV_STATE_CB -#endif - /* * Implement DNS resolution interface using libc-ares */ @@ -107,7 +101,7 @@ static int resolv_mode = MODE_IPV4_FIRST; static void resolv_sock_cb(struct ev_loop *, struct ev_io *, int); static void resolv_timer_cb(struct ev_loop *, struct ev_timer *, int); -static void resolv_sock_state_cb(void *, int, int, int); +static void resolv_sock_state_cb(void *, ares_socket_t, int, int); static void dns_query_v4_cb(void *, int, int, struct hostent *); static void dns_query_v6_cb(void *, int, int, struct hostent *); @@ -156,7 +150,7 @@ resolv_init(struct ev_loop *loop, char *nameservers, int ipv6first) memset(&default_ctx, 0, sizeof(struct resolv_ctx)); default_ctx.options.sock_state_cb_data = &default_ctx; - default_ctx.options.sock_state_cb = CONV_STATE_CB resolv_sock_state_cb; + default_ctx.options.sock_state_cb = resolv_sock_state_cb; default_ctx.options.timeout = 3000; default_ctx.options.tries = 2; @@ -439,7 +433,7 @@ all_requests_are_null(struct resolv_query *query) static void resolv_timer_cb(struct ev_loop *loop, struct ev_timer *w, int revents) { - struct resolv_ctx *ctx = cork_container_of(w, struct resolv_ctx, timer); + struct resolv_ctx *ctx = ss_container_of(w, struct resolv_ctx, timer); ev_tstamp now = ev_now(default_loop); ev_tstamp after = ctx->last_tick - now + SS_TIMER_AFTER; @@ -460,7 +454,7 @@ resolv_timer_cb(struct ev_loop *loop, struct ev_timer *w, int revents) * Handle c-ares events */ static void -resolv_sock_state_cb(void *data, int s, int read, int write) +resolv_sock_state_cb(void *data, ares_socket_t s, int read, int write) { struct resolv_ctx *ctx = (struct resolv_ctx *)data; int events = (read ? EV_READ : 0) | (write ? EV_WRITE : 0); diff --git a/src/rule.c b/src/rule.c index 92b6d8a42..3821829a2 100644 --- a/src/rule.c +++ b/src/rule.c @@ -67,21 +67,30 @@ accept_rule_arg(rule_t *rule, const char *arg) } void -add_rule(struct cork_dllist *rules, rule_t *rule) +add_rule(struct ss_list *rules, rule_t *rule) { - cork_dllist_add(rules, &rule->entries); + ss_list_add(rules, &rule->entries); } int init_rule(rule_t *rule) { + if (strncmp(rule->pattern, "full:", 5) == 0) { + rule->kind = 1; + rule->literal = rule->pattern + 5; + } else if (strncmp(rule->pattern, "suffix:", 7) == 0) { + rule->kind = 2; + rule->literal = rule->pattern + 7; + } + if (rule->kind != 0) { + return *rule->literal != '\0' ? 1 : 0; + } +#if SS_ENABLE_REGEX if (rule->pattern_re == NULL) { int errcode; PCRE2_SIZE erroffset; - - rule->pattern_re = - pcre2_compile((PCRE2_SPTR)rule->pattern, PCRE2_ZERO_TERMINATED, - 0, &errcode, &erroffset, NULL); + rule->pattern_re = pcre2_compile((PCRE2_SPTR)rule->pattern, + PCRE2_ZERO_TERMINATED, 0, &errcode, &erroffset, NULL); if (rule->pattern_re == NULL) { PCRE2_UCHAR errbuf[256]; pcre2_get_error_message(errcode, errbuf, sizeof(errbuf)); @@ -89,30 +98,58 @@ init_rule(rule_t *rule) rule->pattern, errbuf, (int)erroffset); return 0; } - rule->match_data = pcre2_match_data_create_from_pattern( - rule->pattern_re, NULL); + rule->match_data = pcre2_match_data_create_from_pattern(rule->pattern_re, NULL); + if (rule->match_data == NULL) { + pcre2_code_free(rule->pattern_re); + rule->pattern_re = NULL; + return 0; + } } - return 1; +#else + LOGE("Regex ACL support is disabled; use full:domain or suffix:domain: %s", rule->pattern); + return -1; +#endif +} + +static bool +literal_equal(const char *a, const char *b, size_t length) +{ + for (size_t i = 0; i < length; i++) { + unsigned char x = (unsigned char)a[i], y = (unsigned char)b[i]; + if (x >= 'A' && x <= 'Z') x += 'a' - 'A'; + if (y >= 'A' && y <= 'Z') y += 'a' - 'A'; + if (x != y) return false; + } + return true; } rule_t * -lookup_rule(const struct cork_dllist *rules, const char *name, size_t name_len) +lookup_rule(const struct ss_list *rules, const char *name, size_t name_len) { - struct cork_dllist_item *curr, *next; + struct ss_list_item *curr, *next; if (name == NULL) { name = ""; name_len = 0; } - for (curr = cork_dllist_start(rules); !cork_dllist_is_end(rules, curr); + for (curr = ss_list_start(rules); !ss_list_is_end(rules, curr); curr = next) { next = curr->next; - rule_t *rule = cork_container_of(curr, rule_t, entries); + rule_t *rule = ss_container_of(curr, rule_t, entries); + if (rule->kind != 0) { + size_t length = strlen(rule->literal); + if (name_len >= length && + (name_len == length || (rule->kind == 2 && name[name_len - length - 1] == '.')) && + literal_equal(name + name_len - length, rule->literal, length)) return rule; + continue; + } +#if SS_ENABLE_REGEX if (pcre2_match(rule->pattern_re, (PCRE2_SPTR)name, name_len, 0, 0, rule->match_data, NULL) >= 0) return rule; +#endif } return NULL; @@ -121,7 +158,7 @@ lookup_rule(const struct cork_dllist *rules, const char *name, size_t name_len) void remove_rule(rule_t *rule) { - cork_dllist_remove(&rule->entries); + ss_list_remove(&rule->entries); free_rule(rule); } @@ -132,9 +169,11 @@ free_rule(rule_t *rule) return; ss_free(rule->pattern); +#if SS_ENABLE_REGEX if (rule->match_data != NULL) pcre2_match_data_free(rule->match_data); if (rule->pattern_re != NULL) pcre2_code_free(rule->pattern_re); +#endif ss_free(rule); } diff --git a/src/rule.h b/src/rule.h index e5703f5c8..b98aeed50 100644 --- a/src/rule.h +++ b/src/rule.h @@ -31,24 +31,31 @@ #include "config.h" #endif -#include +#include "core.h" +#if SS_ENABLE_REGEX #define PCRE2_CODE_UNIT_WIDTH 8 #include +#endif typedef struct rule { - struct cork_dllist_item entries; + struct ss_list_item entries; char *pattern; + /* 0: regex, 1: full domain, 2: domain suffix (including the apex). */ + unsigned kind; + const char *literal; +#if SS_ENABLE_REGEX /* Runtime fields */ pcre2_code *pattern_re; pcre2_match_data *match_data; +#endif } rule_t; -void add_rule(struct cork_dllist *, rule_t *); +void add_rule(struct ss_list *, rule_t *); int init_rule(rule_t *); -rule_t *lookup_rule(const struct cork_dllist *, const char *, size_t); +rule_t *lookup_rule(const struct ss_list *, const char *, size_t); void free_rule(rule_t *); void remove_rule(rule_t *); rule_t *new_rule(); diff --git a/src/server.c b/src/server.c index 433884756..fd7c05f90 100644 --- a/src/server.c +++ b/src/server.c @@ -43,7 +43,8 @@ #include #include #endif -#include +#include "core.h" +#include #if defined(HAVE_SYS_IOCTL_H) && defined(HAVE_NET_IF_H) && defined(__linux__) #include @@ -70,7 +71,7 @@ enum datatypes { #include "acl.h" #include "plugin.h" #include "server.h" -#include "winsock.h" +#include "ss_windows.h" #include "resolv.h" #ifndef EAGAIN @@ -166,7 +167,7 @@ static struct plugin_watcher_t { } plugin_watcher; #endif -static struct cork_dllist connections; +static struct ss_list connections; #ifndef __MINGW32__ static void @@ -188,7 +189,7 @@ stat_update_cb(EV_P_ ev_timer *watcher, int revents) parse_addr(manager_addr, &ip_addr); if (ip_addr.host == NULL || ip_addr.port == NULL) { - sfd = socket(AF_UNIX, SOCK_DGRAM, 0); + sfd = ss_socket(AF_UNIX, SOCK_DGRAM, 0); if (sfd == -1) { ERROR("stat_socket"); return; @@ -202,7 +203,7 @@ stat_update_cb(EV_P_ ev_timer *watcher, int revents) if (bind(sfd, (struct sockaddr *)&claddr, sizeof(struct sockaddr_un)) == -1) { ERROR("stat_bind"); - close(sfd); + ss_socket_close(sfd); return; } @@ -210,7 +211,7 @@ stat_update_cb(EV_P_ ev_timer *watcher, int revents) svaddr.sun_family = AF_UNIX; if (strlen(manager_addr) >= sizeof(svaddr.sun_path)) { LOGE("manager unix socket path is too long"); - close(sfd); + ss_socket_close(sfd); unlink(claddr.sun_path); return; } @@ -219,7 +220,7 @@ stat_update_cb(EV_P_ ev_timer *watcher, int revents) if (sendto(sfd, resp, strlen(resp) + 1, 0, (struct sockaddr *)&svaddr, sizeof(struct sockaddr_un)) != msgLen) { ERROR("stat_sendto"); - close(sfd); + ss_socket_close(sfd); return; } @@ -232,7 +233,7 @@ stat_update_cb(EV_P_ ev_timer *watcher, int revents) return; } - sfd = socket(storage.ss_family, SOCK_DGRAM, 0); + sfd = ss_socket(storage.ss_family, SOCK_DGRAM, 0); if (sfd == -1) { ERROR("stat_socket"); @@ -243,12 +244,12 @@ stat_update_cb(EV_P_ ev_timer *watcher, int revents) if (sendto(sfd, resp, strlen(resp) + 1, 0, (struct sockaddr *)&storage, addr_len) != msgLen) { ERROR("stat_sendto"); - close(sfd); + ss_socket_close(sfd); return; } } - close(sfd); + ss_socket_close(sfd); } #endif @@ -256,9 +257,9 @@ stat_update_cb(EV_P_ ev_timer *watcher, int revents) static void free_connections(struct ev_loop *loop) { - struct cork_dllist_item *curr, *next; - cork_dllist_foreach_void(&connections, curr, next) { - server_t *server = cork_container_of(curr, server_t, entries); + struct ss_list_item *curr, *next; + ss_list_foreach_void(&connections, curr, next) { + server_t *server = ss_container_of(curr, server_t, entries); remote_t *remote = server->remote; close_and_free_server(loop, server); close_and_free_remote(loop, remote); @@ -277,10 +278,10 @@ get_peer_name(int fd) if (err == 0) { if (addr.ss_family == AF_INET) { struct sockaddr_in *s = (struct sockaddr_in *)&addr; - inet_ntop(AF_INET, &s->sin_addr, peer_name, INET_ADDRSTRLEN); + ss_inet_ntop(AF_INET, &s->sin_addr, peer_name, INET_ADDRSTRLEN); } else if (addr.ss_family == AF_INET6) { struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr; - inet_ntop(AF_INET6, &s->sin6_addr, peer_name, INET6_ADDRSTRLEN); + ss_inet_ntop(AF_INET6, &s->sin6_addr, peer_name, INET6_ADDRSTRLEN); } } else { return NULL; @@ -523,10 +524,10 @@ setfastopen(int fd) #else int opt = 5; #endif - s = setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN, &opt, sizeof(opt)); + s = ss_setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN, &opt, sizeof(opt)); if (s == -1) { - if (errno == EPROTONOSUPPORT || errno == ENOPROTOOPT) { + if (ss_socket_error() == EPROTONOSUPPORT || ss_socket_error() == ENOPROTOOPT) { LOGE("fast open is not supported on this platform"); fast_open = 0; } else { @@ -604,20 +605,20 @@ create_and_bind(const char *host, const char *port, int mptcp) if (mptcp < 0) { protocol = IPPROTO_MPTCP; // Enable upstream MPTCP } - listen_sock = socket(rp->ai_family, rp->ai_socktype, protocol); + listen_sock = ss_socket(rp->ai_family, rp->ai_socktype, protocol); if (listen_sock == -1) { continue; } if (rp->ai_family == AF_INET6) { int opt = host ? 1 : 0; - setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); } int opt = 1; - setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif if (reuse_port) { int err = set_reuseport(listen_sock); @@ -627,18 +628,18 @@ create_and_bind(const char *host, const char *port, int mptcp) } if (tcp_incoming_sndbuf > 0) { - setsockopt(listen_sock, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int)); } if (tcp_incoming_rcvbuf > 0) { - setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int)); } // Enable out-of-tree mptcp if (mptcp == 1) { int i = 0; while ((mptcp = mptcp_enabled_values[i]) > 0) { - int err = setsockopt(listen_sock, IPPROTO_TCP, mptcp, &opt, sizeof(opt)); + int err = ss_setsockopt(listen_sock, IPPROTO_TCP, mptcp, &opt, sizeof(opt)); if (err != -1) { break; } @@ -658,7 +659,7 @@ create_and_bind(const char *host, const char *port, int mptcp) FATAL("failed to bind address"); } - close(listen_sock); + ss_socket_close(listen_sock); listen_sock = -1; } @@ -684,11 +685,11 @@ connect_to_remote(EV_P_ struct addrinfo *res, if (res->ai_addr->sa_family == AF_INET) { struct sockaddr_in s; memcpy(&s, res->ai_addr, sizeof(struct sockaddr_in)); - inet_ntop(AF_INET, &s.sin_addr, ipstr, INET_ADDRSTRLEN); + ss_inet_ntop(AF_INET, &s.sin_addr, ipstr, INET_ADDRSTRLEN); } else if (res->ai_addr->sa_family == AF_INET6) { struct sockaddr_in6 s; memcpy(&s, res->ai_addr, sizeof(struct sockaddr_in6)); - inet_ntop(AF_INET6, &s.sin6_addr, ipstr, INET6_ADDRSTRLEN); + ss_inet_ntop(AF_INET6, &s.sin6_addr, ipstr, INET6_ADDRSTRLEN); } if (outbound_block_match_host(ipstr) == 1) { @@ -699,26 +700,26 @@ connect_to_remote(EV_P_ struct addrinfo *res, } // initialize remote socks - sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + sockfd = ss_socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd == -1) { ERROR("socket"); - close(sockfd); + ss_socket_close(sockfd); return NULL; } int opt = 1; - setsockopt(sockfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(sockfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif - setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ss_setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); if (tcp_outgoing_sndbuf > 0) { - setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &tcp_outgoing_sndbuf, sizeof(int)); + ss_setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &tcp_outgoing_sndbuf, sizeof(int)); } if (tcp_outgoing_rcvbuf > 0) { - setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &tcp_outgoing_rcvbuf, sizeof(int)); + ss_setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &tcp_outgoing_rcvbuf, sizeof(int)); } // setup remote socks @@ -742,7 +743,7 @@ connect_to_remote(EV_P_ struct addrinfo *res, if (iface) { if (setinterface(sockfd, iface) == -1) { ERROR("setinterface"); - close(sockfd); + ss_socket_close(sockfd); return NULL; } } @@ -761,7 +762,7 @@ connect_to_remote(EV_P_ struct addrinfo *res, do { int optval = 1; // Set fast open option - if (setsockopt(sockfd, IPPROTO_TCP, TCP_FASTOPEN, + if (ss_setsockopt(sockfd, IPPROTO_TCP, TCP_FASTOPEN, &optval, sizeof(optval)) != 0) { ERROR("setsockopt"); break; @@ -802,7 +803,7 @@ connect_to_remote(EV_P_ struct addrinfo *res, int s = -1; #if defined(TCP_FASTOPEN_CONNECT) int optval = 1; - if (setsockopt(sockfd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, + if (ss_setsockopt(sockfd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, (void *)&optval, sizeof(optval)) < 0) FATAL("failed to set TCP_FASTOPEN_CONNECT"); s = connect(sockfd, res->ai_addr, res->ai_addrlen); @@ -824,11 +825,11 @@ connect_to_remote(EV_P_ struct addrinfo *res, s = send(sockfd, server->buf->data + server->buf->idx, server->buf->len, 0); #endif if (s == -1) { - if (errno == CONNECT_IN_PROGRESS) { + if (ss_socket_error() == CONNECT_IN_PROGRESS) { // The remote server doesn't support tfo or it's the first connection to the server. // It will automatically fall back to conventional TCP. - } else if (errno == EOPNOTSUPP || errno == EPROTONOSUPPORT || - errno == ENOPROTOOPT) { + } else if (ss_socket_error() == EOPNOTSUPP || ss_socket_error() == EPROTONOSUPPORT || + ss_socket_error() == ENOPROTOOPT) { // Disable fast open as it's not supported fast_open = 0; LOGE("fast open is not supported on this platform"); @@ -844,7 +845,7 @@ connect_to_remote(EV_P_ struct addrinfo *res, if (!fast_open) { int r = connect(sockfd, res->ai_addr, res->ai_addrlen); - if (r == -1 && errno != CONNECT_IN_PROGRESS) { + if (r == -1 && ss_socket_error() != CONNECT_IN_PROGRESS) { ERROR("connect"); close_and_free_remote(EV_A_ remote); return NULL; @@ -866,7 +867,7 @@ setMarkDscpCallback(enum nf_conntrack_msg_type type, struct nf_conntrack *ct, vo // Extract DSCP value from mark value tracker->dscp = tracker->mark & 0x00ff; int tos = (tracker->dscp) << 2; - if (setsockopt(server->fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)) != 0) { + if (ss_setsockopt(server->fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)) != 0) { ERROR("iptable setsockopt IP_TOS"); } } @@ -884,7 +885,7 @@ conntrackQuery(server_t *server) nfct_callback_register(h, NFCT_T_ALL, setMarkDscpCallback, (void *)server); int x = nfct_query(h, NFCT_Q_GET, tracker->ct); if (x == -1) { - LOGE("QOS: Failed to retrieve connection mark %s", strerror(errno)); + LOGE("QOS: Failed to retrieve connection mark %s", strerror(ss_socket_error())); } nfct_close(h); } else { @@ -970,7 +971,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } else if (r == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data // continue to wait for recv return; @@ -1007,7 +1008,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) if (server->stage == STAGE_STREAM) { int s = send(remote->fd, remote->buf->data, remote->buf->len, 0); if (s == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data, wait for send remote->buf->idx = 0; ev_io_stop(EV_A_ & server_recv_ctx->io); @@ -1059,7 +1060,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) addr->sin_family = AF_INET; if (server->buf->len >= in_addr_len + 3) { memcpy(&addr->sin_addr, server->buf->data + offset, in_addr_len); - inet_ntop(AF_INET, (const void *)(server->buf->data + offset), + ss_inet_ntop(AF_INET, (const void *)(server->buf->data + offset), host, INET_ADDRSTRLEN); offset += in_addr_len; } else { @@ -1096,8 +1097,8 @@ server_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } - struct cork_ip ip; - if (cork_ip_init(&ip, host) != -1) { + struct ss_ip ip; + if (ss_ip_init(&ip, host) != -1) { info.ai_socktype = SOCK_STREAM; info.ai_protocol = IPPROTO_TCP; if (ip.version == 4) { @@ -1132,7 +1133,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) addr->sin6_family = AF_INET6; if (server->buf->len >= in6_addr_len + 3) { memcpy(&addr->sin6_addr, server->buf->data + offset, in6_addr_len); - inet_ntop(AF_INET6, (const void *)(server->buf->data + offset), + ss_inet_ntop(AF_INET6, (const void *)(server->buf->data + offset), host, INET6_ADDRSTRLEN); offset += in6_addr_len; } else { @@ -1243,7 +1244,7 @@ server_send_cb(EV_P_ ev_io *w, int revents) ssize_t s = send(server->fd, server->buf->data + server->buf->idx, server->buf->len, 0); if (s == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (ss_socket_error() != EAGAIN && ss_socket_error() != EWOULDBLOCK) { ERROR("server_send_send"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); @@ -1276,7 +1277,7 @@ static void server_timeout_cb(EV_P_ ev_timer *watcher, int revents) { server_ctx_t *server_ctx - = cork_container_of(watcher, server_ctx_t, watcher); + = ss_container_of(watcher, server_ctx_t, watcher); server_t *server = server_ctx->server; remote_t *remote = server->remote; @@ -1381,7 +1382,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } else if (r == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data // continue to wait for recv return; @@ -1416,7 +1417,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) int s = send(server->fd, server->buf->data, server->buf->len, 0); if (s == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data, wait for send server->buf->idx = 0; ev_io_stop(EV_A_ & remote_recv_ctx->io); @@ -1437,8 +1438,8 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) // Disable TCP_NODELAY after the first response are sent if (!remote->recv_ctx->connected && !no_delay) { int opt = 0; - setsockopt(server->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); - setsockopt(remote->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(server->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(remote->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); } remote->recv_ctx->connected = 1; } @@ -1482,7 +1483,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) } // Make getpeername work - if (setsockopt(remote->fd, SOL_SOCKET, + if (ss_setsockopt(remote->fd, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, NULL, 0) != 0) { ERROR("setsockopt"); } @@ -1523,7 +1524,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) ssize_t s = send(remote->fd, remote->buf->data + remote->buf->idx, remote->buf->len, 0); if (s == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (ss_socket_error() != EAGAIN && ss_socket_error() != EWOULDBLOCK) { ERROR("remote_send_send"); // close and free close_and_free_remote(EV_A_ remote); @@ -1607,7 +1608,7 @@ close_and_free_remote(EV_P_ remote_t *remote) if (remote != NULL) { ev_io_stop(EV_A_ & remote->send_ctx->io); ev_io_stop(EV_A_ & remote->recv_ctx->io); - close(remote->fd); + ss_socket_close(remote->fd); free_remote(remote); if (verbose) { remote_conn--; @@ -1658,7 +1659,7 @@ new_server(int fd, listen_ctx_t *listener) ev_timer_init(&server->recv_ctx->watcher, server_timeout_cb, timeout, timeout); - cork_dllist_add(&connections, &server->entries); + ss_list_add(&connections, &server->entries); return server; } @@ -1677,7 +1678,7 @@ free_server(server_t *server) free(tracker); } #endif - cork_dllist_remove(&server->entries); + ss_list_remove(&server->entries); if (server->remote != NULL) { server->remote->server = NULL; @@ -1711,7 +1712,7 @@ close_and_free_server(EV_P_ server_t *server) ev_io_stop(EV_A_ & server->send_ctx->io); ev_io_stop(EV_A_ & server->recv_ctx->io); ev_timer_stop(EV_A_ & server->recv_ctx->watcher); - close(server->fd); + ss_socket_close(server->fd); free_server(server); if (verbose) { server_conn--; @@ -1752,7 +1753,7 @@ static void plugin_watcher_cb(EV_P_ ev_io *w, int revents) { char buf[1]; - SOCKET fd = accept(plugin_watcher.fd, NULL, NULL); + SOCKET fd = ss_accept(plugin_watcher.fd, NULL, NULL); if (fd == INVALID_SOCKET) { return; } @@ -1772,7 +1773,7 @@ static void accept_cb(EV_P_ ev_io *w, int revents) { listen_ctx_t *listener = (listen_ctx_t *)w; - int serverfd = accept(listener->fd, NULL, NULL); + int serverfd = ss_accept(listener->fd, NULL, NULL); if (serverfd == -1) { ERROR("accept"); return; @@ -1784,16 +1785,16 @@ accept_cb(EV_P_ ev_io *w, int revents) if ((get_acl_mode() == BLACK_LIST && acl_match_host(peer_name) == 1) || (get_acl_mode() == WHITE_LIST && acl_match_host(peer_name) >= 0)) { LOGE("Access denied from %s", peer_name); - close(serverfd); + ss_socket_close(serverfd); return; } } } int opt = 1; - setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif setnonblocking(serverfd); @@ -2259,7 +2260,7 @@ main(int argc, char **argv) // Listen on plugin control port if (plugin != NULL && plugin_watcher.port != 0) { SOCKET fd; - fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + fd = ss_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fd != INVALID_SOCKET) { plugin_watcher.valid = 0; do { @@ -2410,7 +2411,7 @@ main(int argc, char **argv) #endif // Init connections - cork_dllist_init(&connections); + ss_list_init(&connections); // start ev loop ev_run(loop, 0); @@ -2437,7 +2438,7 @@ main(int argc, char **argv) listen_ctx_t *listen_ctx = &listen_ctx_list[i]; if (mode != UDP_ONLY) { ev_io_stop(loop, &listen_ctx->io); - close(listen_ctx->fd); + ss_socket_close(listen_ctx->fd); } if (plugin != NULL) break; diff --git a/src/server.h b/src/server.h index cdff2c151..7cd5b4e67 100644 --- a/src/server.h +++ b/src/server.h @@ -24,7 +24,7 @@ #define _SERVER_H #include -#include +#include "core.h" #ifdef HAVE_LIBEV_EV_H #include @@ -33,7 +33,7 @@ #endif #ifdef __MINGW32__ -#include "winsock.h" +#include "ss_windows.h" #endif #include "crypto.h" @@ -89,7 +89,7 @@ typedef struct server { struct query *query; - struct cork_dllist_item entries; + struct ss_list_item entries; #ifdef USE_NFCONNTRACK_TOS struct dscptracker *tracker; #endif diff --git a/src/ss_process.h b/src/ss_process.h new file mode 100644 index 000000000..d0f97a5fd --- /dev/null +++ b/src/ss_process.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#ifndef SS_PROCESS_H +#define SS_PROCESS_H +#include +#include +struct ss_process; +struct ss_process *ss_process_new(const char *program); +void ss_process_arg(struct ss_process *process, const char *argument); +void ss_process_env(struct ss_process *process, const char *name, const char *value); +int ss_process_start(struct ss_process *process, uint16_t control_port); +bool ss_process_running(struct ss_process *process); +void ss_process_free(struct ss_process *process); +#endif diff --git a/src/winsock.h b/src/ss_windows.h similarity index 71% rename from src/winsock.h rename to src/ss_windows.h index 4e8acc7ed..e17892ea0 100644 --- a/src/winsock.h +++ b/src/ss_windows.h @@ -20,8 +20,8 @@ * . */ -#ifndef _WINSOCK_H -#define _WINSOCK_H +#ifndef SS_WINSOCK_COMPAT_H +#define SS_WINSOCK_COMPAT_H #ifdef __MINGW32__ @@ -39,42 +39,11 @@ #endif // Winsock headers -#include #include +#include #include #include -// Override POSIX error number -#ifdef errno -#undef errno -#endif -#define errno WSAGetLastError() - -#ifdef EWOULDBLOCK -#undef EWOULDBLOCK -#endif -#define EWOULDBLOCK WSAEWOULDBLOCK - -#ifdef CONNECT_IN_PROGRESS -#undef CONNECT_IN_PROGRESS -#endif -#define CONNECT_IN_PROGRESS WSAEWOULDBLOCK - -#ifdef EOPNOTSUPP -#undef EOPNOTSUPP -#endif -#define EOPNOTSUPP WSAEOPNOTSUPP - -#ifdef EPROTONOSUPPORT -#undef EPROTONOSUPPORT -#endif -#define EPROTONOSUPPORT WSAEPROTONOSUPPORT - -#ifdef ENOPROTOOPT -#undef ENOPROTOOPT -#endif -#define ENOPROTOOPT WSAENOPROTOOPT - // Check if ConnectEx supported in header #ifdef WSAID_CONNECTEX // Hardcode TCP fast open option @@ -85,13 +54,6 @@ #define TCP_FASTOPEN_WINSOCK 1 #endif -// Override close function -#define close(fd) closesocket(fd) - -// Override MinGW functions -#define setsockopt(a, b, c, d, e) setsockopt(a, b, c, (const char *)(d), e) -#define inet_ntop(a, b, c, d) inet_ntop(a, (void *)(b), c, d) - // Override Windows built-in functions #ifdef ERROR #undef ERROR @@ -120,4 +82,4 @@ int winsock_dummybind(SOCKET fd, struct sockaddr *sa); #endif // __MINGW32__ -#endif // _WINSOCK_H +#endif // SS_WINSOCK_COMPAT_H diff --git a/src/ssurl.c b/src/ssurl.c index 421c2079a..d319b71cc 100644 --- a/src/ssurl.c +++ b/src/ssurl.c @@ -228,10 +228,12 @@ ss_url_free(ss_url_t *url) int ss_url_parse(const char *uri, ss_url_t *url) { - if (uri == NULL || url == NULL) + if (url == NULL) return -1; memset(url, 0, sizeof(*url)); + if (uri == NULL) + return -1; if (strncasecmp(uri, SS_URL_SCHEME, strlen(SS_URL_SCHEME)) != 0) return -1; diff --git a/src/stream.c b/src/stream.c index f5e0a9563..7ecff67bd 100644 --- a/src/stream.c +++ b/src/stream.c @@ -391,7 +391,7 @@ stream_encrypt(buffer_t *plaintext, cipher_ctx_t *cipher_ctx, size_t capacity) } crypto_stream_xor_ic((uint8_t *)(ciphertext->data + nonce_len), (const uint8_t *)plaintext->data, - (uint64_t)(plaintext->len + padding), + (uint64_t)plaintext->len + padding, (const uint8_t *)cipher_ctx->nonce, cipher_ctx->counter / SODIUM_BLOCK_SIZE, cipher->key, cipher->method); @@ -557,7 +557,7 @@ stream_decrypt(buffer_t *ciphertext, cipher_ctx_t *cipher_ctx, size_t capacity) } crypto_stream_xor_ic((uint8_t *)plaintext->data, (const uint8_t *)(ciphertext->data), - (uint64_t)(ciphertext->len + padding), + (uint64_t)ciphertext->len + padding, (const uint8_t *)cipher_ctx->nonce, cipher_ctx->counter / SODIUM_BLOCK_SIZE, cipher->key, cipher->method); diff --git a/src/tunnel.c b/src/tunnel.c index 843222792..bd5470bc5 100644 --- a/src/tunnel.c +++ b/src/tunnel.c @@ -47,13 +47,13 @@ #define SET_INTERFACE #endif -#include +#include "core.h" #include "netutils.h" #include "utils.h" #include "plugin.h" #include "tunnel.h" -#include "winsock.h" +#include "ss_windows.h" #ifndef EAGAIN #define EAGAIN EWOULDBLOCK @@ -80,7 +80,7 @@ static void close_and_free_server(EV_P_ server_t *server); static int validate_tunnel_addr(const ss_addr_t *addr) { - struct cork_ip ip; + struct ss_ip ip; uint16_t port; if (addr->host == NULL || addr->port == NULL) { @@ -89,7 +89,7 @@ validate_tunnel_addr(const ss_addr_t *addr) if (ss_parse_uint16_port(addr->port, &port) == -1) { return -1; } - if (cork_ip_init(&ip, addr->host) == -1) { + if (ss_ip_init(&ip, addr->host) == -1) { size_t host_len = strlen(addr->host); if (!validate_hostname(addr->host, host_len)) { return -1; @@ -172,15 +172,15 @@ create_and_bind(const char *addr, const char *port) } for (rp = result; rp != NULL; rp = rp->ai_next) { - listen_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + listen_sock = ss_socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (listen_sock == -1) { continue; } int opt = 1; - setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif if (reuse_port) { int err = set_reuseport(listen_sock); @@ -190,11 +190,11 @@ create_and_bind(const char *addr, const char *port) } if (tcp_incoming_sndbuf > 0) { - setsockopt(listen_sock, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int)); } if (tcp_incoming_rcvbuf > 0) { - setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int)); + ss_setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int)); } s = bind(listen_sock, rp->ai_addr, rp->ai_addrlen); @@ -205,7 +205,7 @@ create_and_bind(const char *addr, const char *port) ERROR("bind"); } - close(listen_sock); + ss_socket_close(listen_sock); listen_sock = -1; } @@ -234,7 +234,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } else if (r == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data // continue to wait for recv return; @@ -260,7 +260,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) int s = send(remote->fd, remote->buf->data, remote->buf->len, 0); if (s == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data, wait for send remote->buf->idx = 0; ev_io_stop(EV_A_ & server_recv_ctx->io); @@ -297,7 +297,7 @@ server_send_cb(EV_P_ ev_io *w, int revents) ssize_t s = send(server->fd, server->buf->data + server->buf->idx, server->buf->len, 0); if (s == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (ss_socket_error() != EAGAIN && ss_socket_error() != EWOULDBLOCK) { ERROR("send"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); @@ -328,7 +328,7 @@ static void remote_timeout_cb(EV_P_ ev_timer *watcher, int revents) { remote_ctx_t *remote_ctx - = cork_container_of(watcher, remote_ctx_t, watcher); + = ss_container_of(watcher, remote_ctx_t, watcher); remote_t *remote = remote_ctx->remote; server_t *server = remote->server; @@ -358,7 +358,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) close_and_free_server(EV_A_ server); return; } else if (r == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data // continue to wait for recv return; @@ -385,7 +385,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) int s = send(server->fd, server->buf->data, server->buf->len, 0); if (s == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK) { // no data, wait for send server->buf->idx = 0; ev_io_stop(EV_A_ & remote_recv_ctx->io); @@ -406,8 +406,8 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) // Disable TCP_NODELAY after the first response are sent if (!remote->recv_ctx->connected && !no_delay) { int opt = 0; - setsockopt(server->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); - setsockopt(remote->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(server->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(remote->fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); } remote->recv_ctx->connected = 1; } @@ -442,8 +442,8 @@ remote_send_cb(EV_P_ ev_io *w, int revents) buffer_t *abuf = remote->buf; ss_addr_t *sa = &server->destaddr; - struct cork_ip ip; - if (cork_ip_init(&ip, sa->host) != -1) { + struct ss_ip ip; + if (ss_ip_init(&ip, sa->host) != -1) { if (ip.version == 4) { // send as IPv4 struct in_addr host; @@ -532,7 +532,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) do { int optval = 1; // Set fast open option - if (setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN, + if (ss_setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN, &optval, sizeof(optval)) != 0) { ERROR("setsockopt"); break; @@ -581,7 +581,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) NULL, 0, NULL, NULL); #elif defined(TCP_FASTOPEN_CONNECT) int optval = 1; - if (setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, + if (ss_setsockopt(remote->fd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, (void *)&optval, sizeof(optval)) < 0) FATAL("failed to set TCP_FASTOPEN_CONNECT"); s = connect(remote->fd, remote->addr, get_sockaddr_len(remote->addr)); @@ -598,13 +598,13 @@ remote_send_cb(EV_P_ ev_io *w, int revents) remote->addr = NULL; if (s == -1) { - if (errno == CONNECT_IN_PROGRESS) { + if (ss_socket_error() == CONNECT_IN_PROGRESS) { ev_io_start(EV_A_ & remote_send_ctx->io); ev_timer_start(EV_A_ & remote_send_ctx->watcher); } else { fast_open = 0; - if (errno == EOPNOTSUPP || errno == EPROTONOSUPPORT || - errno == ENOPROTOOPT) { + if (ss_socket_error() == EOPNOTSUPP || ss_socket_error() == EPROTONOSUPPORT || + ss_socket_error() == ENOPROTOOPT) { LOGE("fast open is not supported on this platform"); } else { ERROR("fast_open_connect"); @@ -620,7 +620,7 @@ remote_send_cb(EV_P_ ev_io *w, int revents) } if (s == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (ss_socket_error() != EAGAIN && ss_socket_error() != EWOULDBLOCK) { ERROR("send"); // close and free close_and_free_remote(EV_A_ remote); @@ -690,7 +690,7 @@ close_and_free_remote(EV_P_ remote_t *remote) ev_timer_stop(EV_A_ & remote->send_ctx->watcher); ev_io_stop(EV_A_ & remote->send_ctx->io); ev_io_stop(EV_A_ & remote->recv_ctx->io); - close(remote->fd); + ss_socket_close(remote->fd); free_remote(remote); } } @@ -755,7 +755,7 @@ close_and_free_server(EV_P_ server_t *server) if (server != NULL) { ev_io_stop(EV_A_ & server->send_ctx->io); ev_io_stop(EV_A_ & server->recv_ctx->io); - close(server->fd); + ss_socket_close(server->fd); free_server(server); } } @@ -764,16 +764,16 @@ static void accept_cb(EV_P_ ev_io *w, int revents) { struct listen_ctx *listener = (struct listen_ctx *)w; - int serverfd = accept(listener->fd, NULL, NULL); + int serverfd = ss_accept(listener->fd, NULL, NULL); if (serverfd == -1) { ERROR("accept"); return; } setnonblocking(serverfd); int opt = 1; - setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif int index = (int)randombytes_uniform((uint32_t)listener->remote_num); @@ -783,7 +783,7 @@ accept_cb(EV_P_ ev_io *w, int revents) if (listener->mptcp < 0) { protocol = IPPROTO_MPTCP; // Enable upstream MPTCP } - int remotefd = socket(remote_addr->sa_family, SOCK_STREAM, protocol); + int remotefd = ss_socket(remote_addr->sa_family, SOCK_STREAM, protocol); if (remotefd == -1) { ERROR("socket"); return; @@ -800,7 +800,7 @@ accept_cb(EV_P_ ev_io *w, int revents) if (!not_protect) { if (protect_socket(remotefd) == -1) { ERROR("protect_socket"); - close(remotefd); + ss_socket_close(remotefd); return; } } @@ -808,22 +808,22 @@ accept_cb(EV_P_ ev_io *w, int revents) #endif int keepAlive = 1; - setsockopt(remotefd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive)); - setsockopt(remotefd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive)); + ss_setsockopt(remotefd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE - setsockopt(remotefd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); #endif // Enable out-of-tree MPTCP if (listener->mptcp > 1) { - int err = setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); + int err = ss_setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); if (err == -1) { ERROR("failed to enable out-of-tree multipath TCP"); } } else if (listener->mptcp == 1) { int i = 0; while ((listener->mptcp = mptcp_enabled_values[i]) > 0) { - int err = setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); + int err = ss_setsockopt(remotefd, SOL_TCP, listener->mptcp, &opt, sizeof(opt)); if (err != -1) { break; } @@ -835,11 +835,11 @@ accept_cb(EV_P_ ev_io *w, int revents) } if (tcp_outgoing_sndbuf > 0) { - setsockopt(remotefd, SOL_SOCKET, SO_SNDBUF, &tcp_outgoing_sndbuf, sizeof(int)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_SNDBUF, &tcp_outgoing_sndbuf, sizeof(int)); } if (tcp_outgoing_rcvbuf > 0) { - setsockopt(remotefd, SOL_SOCKET, SO_RCVBUF, &tcp_outgoing_rcvbuf, sizeof(int)); + ss_setsockopt(remotefd, SOL_SOCKET, SO_RCVBUF, &tcp_outgoing_rcvbuf, sizeof(int)); } // Setup @@ -862,7 +862,7 @@ accept_cb(EV_P_ ev_io *w, int revents) } else { int r = connect(remotefd, remote_addr, get_sockaddr_len(remote_addr)); - if (r == -1 && errno != CONNECT_IN_PROGRESS) { + if (r == -1 && ss_socket_error() != CONNECT_IN_PROGRESS) { ERROR("connect"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); @@ -907,7 +907,7 @@ static void plugin_watcher_cb(EV_P_ ev_io *w, int revents) { char buf[1]; - SOCKET fd = accept(plugin_watcher.fd, NULL, NULL); + SOCKET fd = ss_accept(plugin_watcher.fd, NULL, NULL); if (fd == INVALID_SOCKET) { return; } @@ -1334,7 +1334,7 @@ main(int argc, char **argv) // Listen on plugin control port if (plugin != NULL && plugin_watcher.port != 0) { SOCKET fd; - fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + fd = ss_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fd != INVALID_SOCKET) { plugin_watcher.valid = 0; do { diff --git a/src/tunnel.h b/src/tunnel.h index 4e9fbf82b..871e8b6b0 100644 --- a/src/tunnel.h +++ b/src/tunnel.h @@ -30,7 +30,7 @@ #endif #ifdef __MINGW32__ -#include "winsock.h" +#include "ss_windows.h" #endif #include "crypto.h" diff --git a/src/udprelay.c b/src/udprelay.c index d90fd2a8a..252633336 100644 --- a/src/udprelay.c +++ b/src/udprelay.c @@ -46,13 +46,13 @@ #define SET_INTERFACE #endif -#include +#include "core.h" #include "utils.h" #include "netutils.h" #include "cache.h" #include "udprelay.h" -#include "winsock.h" +#include "ss_windows.h" #ifdef MODULE_REMOTE #define MAX_UDP_CONN_NUM 512 @@ -105,7 +105,10 @@ extern struct sockaddr_storage local_addr_v6; #endif static int packet_size = DEFAULT_PACKET_SIZE; -static int buf_size = DEFAULT_PACKET_SIZE * 2; +/* UDP preserves datagram boundaries. MTU is a transmission hint, not a + * receive limit: a smaller buffer silently truncates valid fragmented packets + * and makes authentication fail. Covers the full IPv4/IPv6 UDP datagram. */ +static const int buf_size = 65536; static int server_num = 0; static server_ctx_t *server_ctx_list[MAX_REMOTE_NUM] = { NULL }; @@ -129,7 +132,7 @@ static int set_broadcast(int socket_fd) { int opt = 1; - return setsockopt(socket_fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt)); + return ss_setsockopt(socket_fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt)); } #endif @@ -139,7 +142,7 @@ static int set_nosigpipe(int socket_fd) { int opt = 1; - return setsockopt(socket_fd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + return ss_setsockopt(socket_fd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); } #endif @@ -264,7 +267,7 @@ parse_udprelay_header(const char *buf, const size_t buf_len, memcpy(&addr->sin_port, buf + offset + in_addr_len, sizeof(uint16_t)); } if (host != NULL) { - inet_ntop(AF_INET, (const void *)(buf + offset), + ss_inet_ntop(AF_INET, (const void *)(buf + offset), host, INET_ADDRSTRLEN); } offset += in_addr_len; @@ -278,10 +281,10 @@ parse_udprelay_header(const char *buf, const size_t buf_len, if (name_len + 4 <= buf_len) { if (storage != NULL) { char tmp[MAX_HOSTNAME_LEN] = { 0 }; - struct cork_ip ip; + struct ss_ip ip; memcpy(tmp, buf + offset + 1, name_len); tmp[name_len] = '\0'; - if (cork_ip_init(&ip, tmp) != -1) { + if (ss_ip_init(&ip, tmp) != -1) { if (ip.version == 4) { struct sockaddr_in *addr = (struct sockaddr_in *)storage; inet_pton(AF_INET, tmp, &(addr->sin_addr)); @@ -315,7 +318,7 @@ parse_udprelay_header(const char *buf, const size_t buf_len, memcpy(&addr->sin6_port, buf + offset + in6_addr_len, sizeof(uint16_t)); } if (host != NULL) { - inet_ntop(AF_INET6, (const void *)(buf + offset), + ss_inet_ntop(AF_INET6, (const void *)(buf + offset), host, INET6_ADDRSTRLEN); } offset += in6_addr_len; @@ -349,14 +352,14 @@ get_addr_str(const struct sockaddr *sa, bool has_port) switch (sa->sa_family) { case AF_INET: memcpy(&sa_in, sa, sizeof(struct sockaddr_in)); - inet_ntop(AF_INET, &sa_in.sin_addr, addr, INET_ADDRSTRLEN); + ss_inet_ntop(AF_INET, &sa_in.sin_addr, addr, INET_ADDRSTRLEN); p = ntohs(sa_in.sin_port); snprintf(port, sizeof(port), "%d", p); break; case AF_INET6: memcpy(&sa_in6, sa, sizeof(struct sockaddr_in6)); - inet_ntop(AF_INET6, &sa_in6.sin6_addr, addr, INET6_ADDRSTRLEN); + ss_inet_ntop(AF_INET6, &sa_in6.sin6_addr, addr, INET6_ADDRSTRLEN); p = ntohs(sa_in6.sin6_port); snprintf(port, sizeof(port), "%d", p); break; @@ -390,7 +393,7 @@ create_remote_socket(int ipv6) addr.sin6_family = AF_INET6; addr.sin6_addr = in6addr_any; addr.sin6_port = 0; - remote_sock = socket(AF_INET6, SOCK_DGRAM, 0); + remote_sock = ss_socket(AF_INET6, SOCK_DGRAM, 0); if (remote_sock == -1) { ERROR("[udp] cannot create socket"); return -1; @@ -420,7 +423,7 @@ create_remote_socket(int ipv6) addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = 0; - remote_sock = socket(AF_INET, SOCK_DGRAM, 0); + remote_sock = ss_socket(AF_INET, SOCK_DGRAM, 0); if (remote_sock == -1) { ERROR("[udp] cannot create socket"); return -1; @@ -448,7 +451,7 @@ create_remote_socket(int ipv6) #if defined(__linux__) // Disable fragmentation int val = IP_PMTUDISC_DO; - setsockopt(remote_sock, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)); + ss_setsockopt(remote_sock, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)); #endif return remote_sock; @@ -500,18 +503,18 @@ create_server_socket(const char *host, const char *port) } for (/*rp = result*/; rp != NULL; rp = rp->ai_next) { - server_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + server_sock = ss_socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (server_sock == -1) { continue; } if (rp->ai_family == AF_INET6) { int ipv6only = host ? 1 : 0; - setsockopt(server_sock, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, sizeof(ipv6only)); + ss_setsockopt(server_sock, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, sizeof(ipv6only)); } int opt = 1; - setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ss_setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); #ifdef SO_NOSIGPIPE set_nosigpipe(server_sock); #endif @@ -524,14 +527,14 @@ create_server_socket(const char *host, const char *port) #ifdef IP_TOS // Set QoS flag int tos = 46 << 2; - int rc = setsockopt(server_sock, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv4 dscp failed: %d", errno); + int rc = ss_setsockopt(server_sock, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv4 dscp failed: %d", ss_socket_error()); } #ifdef IPV6_TCLASS - rc = setsockopt(server_sock, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv6 dscp failed: %d", errno); + rc = ss_setsockopt(server_sock, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv6 dscp failed: %d", ss_socket_error()); } #endif #endif @@ -541,12 +544,12 @@ create_server_socket(const char *host, const char *port) int flag_t = rp->ai_family == AF_INET ? IP_TRANSPARENT : IPV6_TRANSPARENT; int flag_r = rp->ai_family == AF_INET ? IP_RECVORIGDSTADDR : IPV6_RECVORIGDSTADDR; - if (setsockopt(server_sock, sol, flag_t, &opt, sizeof(opt))) { + if (ss_setsockopt(server_sock, sol, flag_t, &opt, sizeof(opt))) { ERROR("[udp] setsockopt IP_TRANSPARENT"); exit(EXIT_FAILURE); } - if (setsockopt(server_sock, sol, flag_r, &opt, sizeof(opt))) { + if (ss_setsockopt(server_sock, sol, flag_r, &opt, sizeof(opt))) { FATAL("[udp] setsockopt IP_RECVORIGDSTADDR"); } #endif @@ -559,7 +562,7 @@ create_server_socket(const char *host, const char *port) ERROR("[udp] bind"); } - close(server_sock); + ss_socket_close(server_sock); server_sock = -1; } @@ -568,7 +571,7 @@ create_server_socket(const char *host, const char *port) #if defined(__linux__) // Disable fragmentation int val = IP_PMTUDISC_DO; - setsockopt(server_sock, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)); + ss_setsockopt(server_sock, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)); #endif return server_sock; @@ -637,7 +640,7 @@ close_and_free_remote(EV_P_ remote_ctx_t *ctx) if (ctx != NULL) { ev_timer_stop(EV_A_ & ctx->watcher); ev_io_stop(EV_A_ & ctx->io); - close(ctx->fd); + ss_socket_close(ctx->fd); if (ctx->udp_session != NULL && ctx->server_ctx != NULL && ctx->server_ctx->crypto->udp_session_release != NULL) { @@ -651,7 +654,7 @@ static void remote_timeout_cb(EV_P_ ev_timer *watcher, int revents) { remote_ctx_t *remote_ctx - = cork_container_of(watcher, remote_ctx_t, watcher); + = ss_container_of(watcher, remote_ctx_t, watcher); if (verbose) { LOGI("[udp] connection timeout"); @@ -704,14 +707,14 @@ resolv_cb(struct sockaddr *addr, void *data) #ifdef IP_TOS // Set QoS flag int tos = 46 << 2; - int rc = setsockopt(remotefd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv4 dscp failed: %d", errno); + int rc = ss_setsockopt(remotefd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv4 dscp failed: %d", ss_socket_error()); } #ifdef IPV6_TCLASS - rc = setsockopt(remotefd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv6 dscp failed: %d", errno); + rc = ss_setsockopt(remotefd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv6 dscp failed: %d", ss_socket_error()); } #endif #endif @@ -774,7 +777,7 @@ void convert_ipv4_mapped_ipv6(struct sockaddr_storage* addr) { uint8_t* ipv6_raw_addr = mapped_ipv6_addr->sin6_addr.s6_addr; ipv6_raw_addr[10] = 0xff; ipv6_raw_addr[11] = 0xff; - in_addr_t ipv4_raw_addr = ntohl(ipv4_addr->sin_addr.s_addr); + uint32_t ipv4_raw_addr = ntohl(ipv4_addr->sin_addr.s_addr); ipv6_raw_addr[12] = (ipv4_raw_addr >> 24) & 0xff; ipv6_raw_addr[13] = (ipv4_raw_addr >> 16) & 0xff; ipv6_raw_addr[14] = (ipv4_raw_addr >> 8) & 0xff; @@ -901,7 +904,7 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) if (buf->len > packet_size) { if (verbose) { - LOGI("[udp] remote_recv_sendto fragmentation, MTU at least be: " SSIZE_FMT, buf->len + PACKET_HEADER_SIZE); + LOGI("[udp] remote_recv_sendto fragmentation, MTU at least be: " SIZE_FMT, buf->len + PACKET_HEADER_SIZE); } } @@ -911,20 +914,20 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) size_t remote_dst_addr_len = get_sockaddr_len((struct sockaddr *)&dst_addr); - int src_fd = socket(AF_INET6, SOCK_DGRAM, 0); + int src_fd = ss_socket(AF_INET6, SOCK_DGRAM, 0); if (src_fd < 0) { ERROR("[udp] remote_recv_socket"); goto CLEAN_UP; } int opt = 1; - if (setsockopt(src_fd, SOL_IPV6, IPV6_TRANSPARENT, &opt, sizeof(opt))) { + if (ss_setsockopt(src_fd, SOL_IPV6, IPV6_TRANSPARENT, &opt, sizeof(opt))) { ERROR("[udp] remote_recv_setsockopt"); - close(src_fd); + ss_socket_close(src_fd); goto CLEAN_UP; } - if (setsockopt(src_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { + if (ss_setsockopt(src_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { ERROR("[udp] remote_recv_setsockopt"); - close(src_fd); + ss_socket_close(src_fd); goto CLEAN_UP; } if (reuse_port) { @@ -935,20 +938,20 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) #ifdef IP_TOS // Set QoS flag int tos = 46 << 2; - int rc = setsockopt(src_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv4 dscp failed: %d", errno); + int rc = ss_setsockopt(src_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv4 dscp failed: %d", ss_socket_error()); } #ifdef IPV6_TCLASS - rc = setsockopt(src_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv6 dscp failed: %d", errno); + rc = ss_setsockopt(src_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv6 dscp failed: %d", ss_socket_error()); } #endif #endif if (bind(src_fd, (struct sockaddr *)&dst_addr, remote_dst_addr_len) != 0) { ERROR("[udp] remote_recv_bind"); - close(src_fd); + ss_socket_close(src_fd); goto CLEAN_UP; } @@ -960,19 +963,19 @@ remote_recv_cb(EV_P_ ev_io *w, int revents) int s = sendto(src_fd, buf->data, buf->len, 0, (struct sockaddr *)&mapped_src_addr, remote_src_addr_len); - if (s == -1 && !(errno == EAGAIN || errno == EWOULDBLOCK)) { + if (s == -1 && !(ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK)) { ERROR("[udp] remote_recv_sendto"); - close(src_fd); + ss_socket_close(src_fd); goto CLEAN_UP; } - close(src_fd); + ss_socket_close(src_fd); #else size_t remote_src_addr_len = get_sockaddr_len((struct sockaddr *)&remote_ctx->src_addr); int s = sendto(server_ctx->fd, buf->data, buf->len, 0, (struct sockaddr *)&remote_ctx->src_addr, remote_src_addr_len); - if (s == -1 && !(errno == EAGAIN || errno == EWOULDBLOCK)) { + if (s == -1 && !(ss_socket_error() == EAGAIN || ss_socket_error() == EWOULDBLOCK)) { ERROR("[udp] remote_recv_sendto"); goto CLEAN_UP; } @@ -1026,7 +1029,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) goto CLEAN_UP; } else if (buf->len > packet_size) { if (verbose) { - LOGI("[udp] UDP server_recv_recvmsg fragmentation, MTU at least be: " SSIZE_FMT, + LOGI("[udp] UDP server_recv_recvmsg fragmentation, MTU at least be: " SIZE_FMT, buf->len + PACKET_HEADER_SIZE); } } @@ -1160,8 +1163,8 @@ server_recv_cb(EV_P_ ev_io *w, int revents) uint16_t port_net_num = htons(port_num); int addr_header_len = 0; - struct cork_ip ip; - if (cork_ip_init(&ip, host) != -1) { + struct ss_ip ip; + if (ss_ip_init(&ip, host) != -1) { if (ip.version == 4) { // send as IPv4 struct in_addr host_addr; @@ -1305,14 +1308,14 @@ server_recv_cb(EV_P_ ev_io *w, int revents) #ifdef IP_TOS // Set QoS flag int tos = 46 << 2; - int rc = setsockopt(remotefd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv4 dscp failed: %d", errno); + int rc = ss_setsockopt(remotefd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv4 dscp failed: %d", ss_socket_error()); } #ifdef IPV6_TCLASS - rc = setsockopt(remotefd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv6 dscp failed: %d", errno); + rc = ss_setsockopt(remotefd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv6 dscp failed: %d", ss_socket_error()); } #endif #endif @@ -1327,7 +1330,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) if (vpn) { if (protect_socket(remotefd) == -1) { ERROR("protect_socket"); - close(remotefd); + ss_socket_close(remotefd); goto CLEAN_UP; } } @@ -1363,7 +1366,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) if (buf->len > packet_size) { if (verbose) { - LOGI("[udp] server_recv_sendto fragmentation, MTU at least be: " SSIZE_FMT, buf->len + PACKET_HEADER_SIZE); + LOGI("[udp] server_recv_sendto fragmentation, MTU at least be: " SIZE_FMT, buf->len + PACKET_HEADER_SIZE); } } @@ -1382,7 +1385,7 @@ server_recv_cb(EV_P_ ev_io *w, int revents) if (buf->len - addr_header_len > packet_size) { if (verbose) { - LOGI("[udp] server_recv_sendto fragmentation, MTU at least be: " SSIZE_FMT, + LOGI("[udp] server_recv_sendto fragmentation, MTU at least be: " SIZE_FMT, buf->len - addr_header_len + PACKET_HEADER_SIZE); } } @@ -1406,14 +1409,14 @@ server_recv_cb(EV_P_ ev_io *w, int revents) #ifdef IP_TOS // Set QoS flag int tos = 46 << 2; - int rc = setsockopt(remotefd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv4 dscp failed: %d", errno); + int rc = ss_setsockopt(remotefd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv4 dscp failed: %d", ss_socket_error()); } #ifdef IPV6_TCLASS - rc = setsockopt(remotefd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); - if (rc < 0 && errno != ENOPROTOOPT) { - LOGE("setting ipv6 dscp failed: %d", errno); + rc = ss_setsockopt(remotefd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); + if (rc < 0 && ss_socket_error() != ENOPROTOOPT) { + LOGE("setting ipv6 dscp failed: %d", ss_socket_error()); } #endif #endif @@ -1521,7 +1524,6 @@ init_udprelay(const char *server_host, const char *server_port, return -1; } packet_size = mtu - PACKET_HEADER_SIZE; - buf_size = packet_size * 2; } // //////////////////////////////////////////////// @@ -1568,7 +1570,7 @@ free_udprelay() while (server_num > 0) { server_ctx_t *server_ctx = server_ctx_list[--server_num]; ev_io_stop(loop, &server_ctx->io); - close(server_ctx->fd); + ss_socket_close(server_ctx->fd); cache_delete(server_ctx->conn_cache, 0); #ifdef MODULE_LOCAL free((char*) server_ctx->remote_addr); diff --git a/src/utils.h b/src/utils.h index 1358d14ef..9469f8a66 100644 --- a/src/utils.h +++ b/src/utils.h @@ -192,14 +192,9 @@ extern int use_syslog; #endif // if __ANDROID__ -// Workaround for "%z" in Windows printf -#ifdef __MINGW32__ -#define SSIZE_FMT "%Id" -#define SIZE_FMT "%Iu" -#else +/* C99 size formats; MinGW targets explicitly enable its ANSI stdio adapter. */ #define SSIZE_FMT "%zd" #define SIZE_FMT "%zu" -#endif #ifdef __MINGW32__ // Override Windows built-in functions diff --git a/src/winsock.c b/src/winsock.c index 26ea34431..ef6e7cb58 100644 --- a/src/winsock.c +++ b/src/winsock.c @@ -22,7 +22,7 @@ #ifdef __MINGW32__ -#include "winsock.h" +#include "ss_windows.h" #include "utils.h" #ifndef ENABLE_QUICK_EDIT diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 92836649b..80fbf2eab 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,24 +6,28 @@ include_directories(${SODIUM_INCLUDE_DIRS}) include_directories(${PCRE2_INCLUDE_DIRS}) include_directories(${CARES_INCLUDE_DIRS}) -if(WITH_EMBEDDED_SRC) - include_directories(${PROJECT_SOURCE_DIR}/libcork/include) - include_directories(${PROJECT_SOURCE_DIR}/libipset/include) - include_directories(${PROJECT_SOURCE_DIR}/libbloom/murmur2) - include_directories(${PROJECT_SOURCE_DIR}/libbloom) -endif() - if(CMAKE_SYSTEM_NAME STREQUAL "Linux") find_program(VALGRIND_EXECUTABLE NAMES valgrind) endif() # Helper function to add a unit test function(ss_add_test name sources libs) + if(WIN32 AND "${sources}" MATCHES "utils.c") + list(APPEND sources ${PROJECT_SOURCE_DIR}/src/winsock.c) + endif() add_executable(${name} ${sources}) target_compile_definitions(${name} PRIVATE -DHAVE_CONFIG_H) target_link_libraries(${name} ${libs}) + if(WIN32) + target_link_libraries(${name} ws2_32) + endif() # Keep assertions enabled for tests even in Release builds. - target_compile_options(${name} PRIVATE -UNDEBUG) + ss_project_options(${name}) + if(MSVC) + target_compile_options(${name} PRIVATE /UNDEBUG) + else() + target_compile_options(${name} PRIVATE -UNDEBUG) + endif() add_test(NAME ${name} COMMAND ${name}) set_tests_properties(${name} PROPERTIES LABELS "unit") @@ -53,88 +57,67 @@ ss_add_test(test_base64 # test_utils - needs sodium (for ss_malloc -> uses sodium in utils.c) ss_add_test(test_utils "test_utils.c;${PROJECT_SOURCE_DIR}/src/utils.c" - "${LIBSODIUM_SHARED}") + "ss_sodium") # test_json - standalone json parser ss_add_test(test_json "test_json.c;${PROJECT_SOURCE_DIR}/src/json.c" - "m") + "${SS_MATH_LIBRARY}") # test_ssurl - ss:// URI parsing (SIP002 and legacy) ss_add_test(test_ssurl "test_ssurl.c;${PROJECT_SOURCE_DIR}/src/ssurl.c;${PROJECT_SOURCE_DIR}/src/base64.c;${PROJECT_SOURCE_DIR}/src/utils.c" - "${LIBSODIUM_SHARED}") + "ss_sodium") -# test_netutils - needs cork for ip address parsing +# test_netutils - address parsing and resolver integration set(TEST_NETUTILS_LIBS - ${LIBSODIUM_SHARED} - ${LIBEV_SHARED} - ${LIBUDNS_SHARED} + ss_sodium + ss_ev + ss_cares ) -if(WITH_EMBEDDED_SRC) - list(APPEND TEST_NETUTILS_LIBS cork) -else() - list(APPEND TEST_NETUTILS_LIBS ${LIBCORK_SHARED}) -endif() ss_add_test(test_netutils "test_netutils.c;${PROJECT_SOURCE_DIR}/src/netutils.c;${PROJECT_SOURCE_DIR}/src/utils.c" "${TEST_NETUTILS_LIBS}") -# test_cache - needs libev for ev_time() +# test_cache - expiration uses a platform monotonic clock ss_add_test(test_cache "test_cache.c;${PROJECT_SOURCE_DIR}/src/cache.c;${PROJECT_SOURCE_DIR}/src/utils.c" - "${LIBEV_SHARED};${LIBSODIUM_SHARED}") + "ss_sodium") # test_ppbloom - needs bloom set(TEST_PPBLOOM_LIBS "") -if(WITH_EMBEDDED_SRC) - list(APPEND TEST_PPBLOOM_LIBS bloom) -else() - list(APPEND TEST_PPBLOOM_LIBS ${LIBBLOOM_SHARED}) -endif() +list(APPEND TEST_PPBLOOM_LIBS bloom) ss_add_test(test_ppbloom "test_ppbloom.c;${PROJECT_SOURCE_DIR}/src/ppbloom.c;${PROJECT_SOURCE_DIR}/src/utils.c" - "${TEST_PPBLOOM_LIBS};${LIBSODIUM_SHARED};m") + "${TEST_PPBLOOM_LIBS};ss_sodium;${SS_MATH_LIBRARY}") -# test_rule - needs pcre and cork +if(SS_ENABLE_REGEX) +# test_rule - regex matching set(TEST_RULE_LIBS - ${LIBPCRE2_SHARED} - ${LIBSODIUM_SHARED} + ss_pcre2 + ss_sodium ) -if(WITH_EMBEDDED_SRC) - list(APPEND TEST_RULE_LIBS cork) -else() - list(APPEND TEST_RULE_LIBS ${LIBCORK_SHARED}) -endif() ss_add_test(test_rule "test_rule.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" "${TEST_RULE_LIBS}") -# test_acl - needs pcre, cork, and ipset +# test_acl - ACL precedence and prefix matching set(TEST_ACL_LIBS ${TEST_RULE_LIBS}) -if(WITH_EMBEDDED_SRC) - list(APPEND TEST_ACL_LIBS ipset) -else() - list(APPEND TEST_ACL_LIBS ${LIBCORKIPSET_SHARED}) -endif() ss_add_test(test_acl - "test_acl.c;${PROJECT_SOURCE_DIR}/src/acl.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_acl.c;${PROJECT_SOURCE_DIR}/src/acl.c;${PROJECT_SOURCE_DIR}/src/ipset.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" "${TEST_ACL_LIBS}") -# test_jconf - needs cork and json +endif() + +# test_jconf - configuration parsing set(TEST_JCONF_LIBS - ${LIBSODIUM_SHARED} - ${LIBEV_SHARED} - ${LIBUDNS_SHARED} + ss_sodium + ss_ev + ss_cares ) -if(WITH_EMBEDDED_SRC) - list(APPEND TEST_JCONF_LIBS cork) -else() - list(APPEND TEST_JCONF_LIBS ${LIBCORK_SHARED}) -endif() ss_add_test(test_jconf "test_jconf.c;${PROJECT_SOURCE_DIR}/src/jconf.c;${PROJECT_SOURCE_DIR}/src/json.c;${PROJECT_SOURCE_DIR}/src/utils.c;${PROJECT_SOURCE_DIR}/src/netutils.c" - "${TEST_JCONF_LIBS};m") + "${TEST_JCONF_LIBS};${SS_MATH_LIBRARY}") # Crypto sources shared by the tests that link the crypto layer. The AEAD-2022 # ciphers pull in BLAKE3 (linked as the `blake3` target) and the session-based @@ -144,40 +127,39 @@ set(SS_CRYPTO_TEST_SOURCES ${PROJECT_SOURCE_DIR}/src/aead.c ${PROJECT_SOURCE_DIR}/src/aead2022.c ${PROJECT_SOURCE_DIR}/src/aead2022_udp.c - ${PROJECT_SOURCE_DIR}/src/stream.c ${PROJECT_SOURCE_DIR}/src/ppbloom.c ${PROJECT_SOURCE_DIR}/src/cache.c ${PROJECT_SOURCE_DIR}/src/base64.c ${PROJECT_SOURCE_DIR}/src/utils.c ) +if(SS_ENABLE_LEGACY) + list(APPEND SS_CRYPTO_TEST_SOURCES ${PROJECT_SOURCE_DIR}/src/stream.c) +endif() + # test_buffer - needs sodium and mbedtls for crypto.c buffer functions ss_add_test(test_buffer "test_buffer.c;${SS_CRYPTO_TEST_SOURCES}" - "${LIBSODIUM_SHARED};${LIBMBEDTLS_SHARED};${LIBMBEDCRYPTO_SHARED};${LIBEV_SHARED};${TEST_PPBLOOM_LIBS};blake3;m") + "ss_sodium;;ss_mbedcrypto;ss_ev;${TEST_PPBLOOM_LIBS};blake3;${SS_MATH_LIBRARY}") # test_crypto - full crypto test set(TEST_CRYPTO_LIBS - ${LIBSODIUM_SHARED} - ${LIBMBEDTLS_SHARED} - ${LIBMBEDCRYPTO_SHARED} - ${LIBEV_SHARED} + ss_sodium + + ss_mbedcrypto + ss_ev ) -if(WITH_EMBEDDED_SRC) - list(APPEND TEST_CRYPTO_LIBS bloom) -else() - list(APPEND TEST_CRYPTO_LIBS ${LIBBLOOM_SHARED}) -endif() +list(APPEND TEST_CRYPTO_LIBS bloom) # BLAKE3 backs the 2022 ciphers' key derivation list(APPEND TEST_CRYPTO_LIBS blake3) ss_add_test(test_crypto "test_crypto.c;${SS_CRYPTO_TEST_SOURCES}" - "${TEST_CRYPTO_LIBS};m") + "${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}") # test_aead2022 - Shadowsocks 2022 Edition (SIP022) ciphers ss_add_test(test_aead2022 "test_aead2022.c;${SS_CRYPTO_TEST_SOURCES}" - "${TEST_CRYPTO_LIBS};m") + "${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}") # test_interop_rust - interoperability against shadowsocks-rust. # Skips itself when shadowsocks-rust or the built binaries are absent. @@ -187,10 +169,33 @@ add_test(NAME test_interop_rust set_tests_properties(test_interop_rust PROPERTIES LABELS "integration" TIMEOUT 300 - ENVIRONMENT "SS_BIN_DIR=${RUNTIME_SHARED_OUTPUT_DIRECTORY}") + SKIP_RETURN_CODE 77 + ENVIRONMENT "SS_BIN_DIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") # test_ss_setup - bash unit tests for the ss-setup TUI tool add_test(NAME test_ss_setup COMMAND bash "${PROJECT_SOURCE_DIR}/tests/test_ss_setup.sh" WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}") set_tests_properties(test_ss_setup PROPERTIES LABELS "integration") + +ss_add_test(test_ipset + "test_ipset.c;${PROJECT_SOURCE_DIR}/src/ipset.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "ss_sodium") + +ss_add_test(test_process + "test_process.c;${PROJECT_SOURCE_DIR}/src/process.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "ss_sodium") +set_tests_properties(test_process PROPERTIES TIMEOUT 20) + +ss_add_test(test_resolv + "test_resolv.c;${PROJECT_SOURCE_DIR}/src/resolv.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "ss_ev;ss_cares;ss_sodium") +set_tests_properties(test_resolv PROPERTIES TIMEOUT 20) + +set(TEST_LITERAL_LIBS ss_sodium) +if(SS_ENABLE_REGEX) + list(APPEND TEST_LITERAL_LIBS ss_pcre2) +endif() +ss_add_test(test_acl_literal + "test_acl_literal.c;${PROJECT_SOURCE_DIR}/src/acl.c;${PROJECT_SOURCE_DIR}/src/ipset.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "${TEST_LITERAL_LIBS}") diff --git a/tests/check_static_runtime.py b/tests/check_static_runtime.py new file mode 100644 index 000000000..3ff4d93b3 --- /dev/null +++ b/tests/check_static_runtime.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Reject non-system runtime dependencies in static macOS/Windows outputs.""" +import argparse +import platform +import re +import subprocess +from pathlib import Path + +WINDOWS_SYSTEM_DLLS = { + 'advapi32.dll', 'bcrypt.dll', 'crypt32.dll', 'dnsapi.dll', 'iphlpapi.dll', + 'kernel32.dll', 'msvcrt.dll', 'ntdll.dll', 'ole32.dll', 'psapi.dll', + 'rpcrt4.dll', 'secur32.dll', 'shell32.dll', 'ucrtbase.dll', 'user32.dll', + 'userenv.dll', 'winmm.dll', 'ws2_32.dll', +} + + +def dependencies(binary, system): + if system == 'Darwin': + output = subprocess.check_output(['otool', '-L', str(binary)], text=True) + return [line.strip().split(' (compatibility version', 1)[0] + for line in output.splitlines() if line.startswith('\t')] + output = subprocess.check_output( + ['llvm-readobj', '--coff-imports', str(binary)], text=True) + return re.findall(r'^\s+Name: (.+)$', output, re.MULTILINE) + + +def is_system_dependency(name, system): + if system == 'Darwin': + return name.startswith(('/usr/lib/', '/System/Library/')) + name = name.strip().lower() + return name in WINDOWS_SYSTEM_DLLS or ( + name.startswith(('api-ms-win-', 'ext-ms-win-')) and name.endswith('.dll')) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('binaries', type=Path, nargs='+') + args = parser.parse_args() + system = platform.system() + if system not in ('Darwin', 'Windows'): + parser.error('this check supports macOS and native Windows Python') + for binary in args.binaries: + names = dependencies(binary, system) + if not names: + raise SystemExit(f'{binary}: no OS imports found; check file format/tool output') + unexpected = [name for name in names if not is_system_dependency(name, system)] + if unexpected: + raise SystemExit(f'{binary}: non-system dependencies: {unexpected}') + print(f'PASS {binary}: {", ".join(names)}') + + +if __name__ == '__main__': + main() diff --git a/tests/consumer/CMakeLists.txt b/tests/consumer/CMakeLists.txt new file mode 100644 index 000000000..6f635b879 --- /dev/null +++ b/tests/consumer/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(shadowsocks_consumer LANGUAGES C) +find_package(shadowsocks-libev CONFIG REQUIRED) +foreach(kind shared static) + if(TARGET shadowsocks::${kind}) + add_executable(consumer_${kind} main.c) + target_link_libraries(consumer_${kind} PRIVATE shadowsocks::${kind}) + endif() +endforeach() diff --git a/tests/consumer/main.c b/tests/consumer/main.c new file mode 100644 index 000000000..109dd61b8 --- /dev/null +++ b/tests/consumer/main.c @@ -0,0 +1,7 @@ +#include +#include +/* Force the linker to resolve the complete embedding API and its dependencies + * without starting an unbounded event loop in this installation smoke test. */ +static int (*volatile start)(profile_t) = start_ss_local_server; +static int (*volatile callback_start)(profile_t, ss_local_callback, void *) = start_ss_local_server_with_callback; +int main(void) { return start == NULL || callback_start == NULL; } diff --git a/tests/interop.py b/tests/interop.py new file mode 100644 index 000000000..1d03a0e4d --- /dev/null +++ b/tests/interop.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +"""Real TCP/UDP interoperability, independent of HTTP proxy environment variables.""" +import argparse +import base64 +import concurrent.futures +import contextlib +import os +from pathlib import Path +import shutil +import socket +import socketserver +import struct +import subprocess +import tempfile +import threading +import time + + +class TCPHandler(socketserver.BaseRequestHandler): + def handle(self): + self.request.settimeout(15) + try: + while chunk := self.request.recv(16384): + self.request.sendall(chunk) + except (OSError, TimeoutError): + pass + + +class UDPHandler(socketserver.BaseRequestHandler): + def handle(self): + data, sock = self.request + sock.sendto(data, self.client_address) + + +class TCPOrigin(socketserver.ThreadingTCPServer): + daemon_threads = True + + +class UDPOrigin(socketserver.ThreadingUDPServer): + daemon_threads = True + + +def receive(sock, size): + data = bytearray() + while len(data) < size: + chunk = sock.recv(size - len(data)) + if not chunk: + raise AssertionError(f"unexpected EOF after {len(data)}/{size} bytes") + data.extend(chunk) + return bytes(data) + + +def address(sock, kind): + if kind == 1: + host = socket.inet_ntop(socket.AF_INET, receive(sock, 4)) + elif kind == 4: + host = socket.inet_ntop(socket.AF_INET6, receive(sock, 16)) + elif kind == 3: + host = receive(sock, receive(sock, 1)[0]).decode("ascii") + else: + raise AssertionError(f"invalid SOCKS address type {kind}") + return host, struct.unpack("!H", receive(sock, 2))[0] + + +def socks(proxy_port, command, destination): + sock = socket.create_connection(("127.0.0.1", proxy_port), timeout=10) + try: + sock.sendall(b"\x05\x01\x00") + assert receive(sock, 2) == b"\x05\x00", "SOCKS authentication failed" + host, port = destination + encoded = host.encode("ascii") + sock.sendall(bytes([5, command, 0, 3, len(encoded)]) + encoded + struct.pack("!H", port)) + header = receive(sock, 4) + assert header[:3] == b"\x05\x00\x00", f"SOCKS request failed: {header.hex()}" + relay = address(sock, header[3]) + return sock, relay + except BaseException: + sock.close() + raise + + +def tcp_case(proxy_port, origin_port, size): + sock, _ = socks(proxy_port, 1, ("localhost", origin_port)) + payload = os.urandom(size) + with sock, concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + # Upload and download concurrently to exercise backpressure in both directions. + upload = pool.submit(sock.sendall, payload) + assert receive(sock, len(payload)) == payload, "TCP payload mismatch" + upload.result(timeout=10) + + +def udp_case(proxy_port, origin_port): + control, relay = socks(proxy_port, 3, ("0.0.0.0", 0)) + host, port = relay + if host in ("0.0.0.0", "::"): + host = "127.0.0.1" + with control, socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.settimeout(10) + for size in (1, 128, 1200, 4096): + payload = os.urandom(size) + header = b"\x00\x00\x00\x01" + socket.inet_aton("127.0.0.1") + struct.pack("!H", origin_port) + sock.sendto(header + payload, (host, port)) + try: + reply, sender = sock.recvfrom(65535) + except TimeoutError as error: + raise AssertionError(f"UDP timeout for {size}-byte payload") from error + assert sender[1] == port, "UDP reply bypassed relay" + assert reply[:3] == b"\x00\x00\x00", "fragmented or invalid SOCKS UDP reply" + if reply[3] == 1: + offset = 10 + elif reply[3] == 4: + offset = 22 + elif reply[3] == 3: + offset = 7 + reply[4] + else: + raise AssertionError("invalid UDP address type") + assert reply[offset:] == payload, "UDP payload mismatch" + + +def free_port(): + # TCP and UDP have separate port reservations, especially on Windows. + # Probe both while holding the TCP socket so a TCP-only free port cannot + # select a reserved or occupied UDP endpoint. + for _ in range(100): + with socket.socket() as tcp, socket.socket(type=socket.SOCK_DGRAM) as udp: + tcp.bind(("127.0.0.1", 0)) + port = tcp.getsockname()[1] + try: + udp.bind(("127.0.0.1", port)) + except OSError: + continue + return port + raise RuntimeError("could not find a free TCP/UDP port") + + +def wait_ready(process, port): + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + if process.poll() is not None: + raise AssertionError(f"peer exited with {process.returncode}") + try: + with socket.create_connection(("127.0.0.1", port), timeout=.1): + return + except OSError: + time.sleep(.03) + raise AssertionError(f"peer failed to listen on port {port}") + + +@contextlib.contextmanager +def peers(commands, ports, environment=None): + processes = [] + with tempfile.TemporaryDirectory(prefix="ss-interop-") as temp, contextlib.ExitStack() as stack: + logs = [] + try: + for index, (command, port) in enumerate(zip(commands, ports)): + path = Path(temp) / f"peer-{index}.log" + log = stack.enter_context(path.open("wb")) + logs.append(path) + process = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT, env=environment) + processes.append(process) + wait_ready(process, port) + yield + except BaseException: + for path in logs: + print(path.name + ":\n" + path.read_text(errors="replace")[-6000:], flush=True) + raise + finally: + for process in reversed(processes): + if process.poll() is None: + process.terminate() + for process in reversed(processes): + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--self", action="store_true", help="use this build for both peers") + parser.add_argument("--method", help="run one cipher") + parser.add_argument("--bin", default=os.environ.get("SS_BIN_DIR", "build/bin"), help="program directory") + parser.add_argument("--plugin", help="SIP003 fixture executable (requires --self)") + parser.add_argument("--isolate-windows-runtime", action="store_true", + help="remove MSYS2/toolchain DLL directories from child PATH") + args = parser.parse_args() + if args.plugin and not args.self: + parser.error("--plugin requires --self") + environment = None + if args.isolate_windows_runtime: + if os.name != "nt": + parser.error("--isolate-windows-runtime requires Windows") + environment = os.environ.copy() + system_root = os.environ["SYSTEMROOT"] + environment["PATH"] = os.pathsep.join((system_root, os.path.join(system_root, "System32"))) + binary_dir = Path(args.bin).resolve() + suffix = ".exe" if os.name == "nt" else "" + local, server = [str(binary_dir / (name + suffix)) for name in ("ss-local", "ss-server")] + rust_local, rust_server = shutil.which("sslocal"), shutil.which("ssserver") + missing = [p for p in (local, server) if not os.path.isfile(p)] + if not args.self: + missing += [name for name, found in (("sslocal", rust_local), ("ssserver", rust_server)) if not found] + if missing: + print("Missing interoperability prerequisites: " + ", ".join(missing)) + return 1 if os.environ.get("SS_REQUIRE_INTEROP") == "1" else 77 + methods = ["2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", + "2022-blake3-chacha20-poly1305", "aes-128-gcm", "aes-256-gcm", + "chacha20-ietf-poly1305"] + if args.method: + methods = [args.method] + failures = 0 + with TCPOrigin(("127.0.0.1", 0), TCPHandler) as tcp, UDPOrigin(("127.0.0.1", 0), UDPHandler) as udp: + for origin in (tcp, udp): + threading.Thread(target=origin.serve_forever, daemon=True).start() + try: + for method in methods: + key_size = 16 if method == "2022-blake3-aes-128-gcm" else 32 + psk = base64.b64encode(os.urandom(key_size)).decode("ascii") + directions = ("self",) if args.self else ("C client -> Rust server", "Rust client -> C server") + for direction in directions: + label = f"{method}: {direction}" + print("RUN " + label, flush=True) + server_port, local_port = free_port(), free_port() + c_server = [server, "-s", "127.0.0.1", "-p", str(server_port), "-k", psk, "-m", method, "-u"] + c_local = [local, "-s", "127.0.0.1", "-p", str(server_port), "-l", str(local_port), "-k", psk, "-m", method, "-u"] + commands = [c_server, c_local] + plugin_temp = tempfile.TemporaryDirectory(prefix="ss-plugin-") if args.plugin else None + markers = [] + if args.plugin: + for mode, command in zip(("server", "client"), commands): + marker = Path(plugin_temp.name) / mode + markers.append(marker) + command.extend(["--plugin", str(Path(args.plugin).resolve()), + "--plugin-opts", f"{mode};marker={marker}"]) + if direction.startswith("C client"): + commands[0] = [rust_server, "-s", f"127.0.0.1:{server_port}", "-k", psk, "-m", method, "-U"] + elif direction.startswith("Rust client"): + commands[1] = [rust_local, "-b", f"127.0.0.1:{local_port}", "-s", f"127.0.0.1:{server_port}", "-k", psk, "-m", method, "-U"] + try: + with peers(commands, [server_port, local_port], environment): + for marker in markers: + deadline = time.monotonic() + 10 + while not marker.exists() and time.monotonic() < deadline: + time.sleep(.03) + assert marker.exists(), "plugin did not become ready" + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool: + cases = [pool.submit(tcp_case, local_port, tcp.server_address[1], size) + for size in (1, 65536, 1048576)] + for case in cases: + case.result(timeout=30) + udp_case(local_port, udp.server_address[1]) + for marker in markers: + pid = int(marker.read_text()) + try: + os.kill(pid, 0) + except ProcessLookupError: + pass + else: + raise AssertionError(f"plugin child {pid} survived parent shutdown") + print("PASS TCP (3 concurrent streams) + UDP: " + label, flush=True) + except (OSError, AssertionError, TimeoutError) as error: + failures += 1 + print(f"FAIL {label}: {error}", flush=True) + finally: + if plugin_temp is not None: + plugin_temp.cleanup() + finally: + tcp.shutdown() + udp.shutdown() + return int(failures != 0) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/measure_udp.py b/tests/measure_udp.py new file mode 100644 index 000000000..17028d517 --- /dev/null +++ b/tests/measure_udp.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Loopback UDP echo measurement; includes Python/SOCKS overhead, not a capacity test.""" +import argparse +import base64 +import json +import os +from pathlib import Path +import socket +import struct +import threading +import time +from interop import UDPOrigin, UDPHandler, free_port, peers, socks + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--bin", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--count", type=int, default=2000) + parser.add_argument("--repeat", type=int, default=3) + args = parser.parse_args() + results = [] + with UDPOrigin(("127.0.0.1", 0), UDPHandler) as origin: + threading.Thread(target=origin.serve_forever, daemon=True).start() + try: + for method in ("aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"): + server_port, local_port = free_port(), free_port() + key = base64.b64encode(os.urandom(32)).decode("ascii") + common = ["-s", "127.0.0.1", "-p", str(server_port), "-m", method, "-k", key, "-u"] + commands = [[str(args.bin.resolve() / "ss-server"), *common], + [str(args.bin.resolve() / "ss-local"), *common, "-l", str(local_port)]] + with peers(commands, [server_port, local_port]): + control, relay = socks(local_port, 3, ("0.0.0.0", 0)) + with control, socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp: + udp.settimeout(5) + relay = ("127.0.0.1", relay[1]) + prefix = b"\0\0\0\x01\x7f\0\0\x01" + struct.pack("!H", origin.server_address[1]) + for trial in range(args.repeat): + start = time.perf_counter() + for sequence in range(args.count): + payload = struct.pack("!I", sequence) + bytes(1196) + udp.sendto(prefix + payload, relay) + response, sender = udp.recvfrom(65536) + assert sender[1] == relay[1] and response[10:] == payload + elapsed = time.perf_counter() - start + results.append(dict(cipher=method, trial=trial, packets=args.count, + payload_bytes=1200, seconds=elapsed, + payload_mbps=args.count * 1200 * 8 / elapsed / 1e6)) + finally: + origin.shutdown() + args.output.write_text(json.dumps(results, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/tests/sip003_fixture.py b/tests/sip003_fixture.py new file mode 100755 index 000000000..9381c4d06 --- /dev/null +++ b/tests/sip003_fixture.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Transparent SIP003 test transport; never used as a production plugin.""" +import os +from pathlib import Path +import socket +import socketserver +import threading + +mode, marker = os.environ["SS_PLUGIN_OPTIONS"].split(";marker=", 1) +local = (os.environ["SS_LOCAL_HOST"], int(os.environ["SS_LOCAL_PORT"])) +remote = (os.environ["SS_REMOTE_HOST"], int(os.environ["SS_REMOTE_PORT"])) +listen, destination = (remote, local) if mode == "server" else (local, remote) + + +def copy(source, target): + try: + while data := source.recv(16384): + target.sendall(data) + target.shutdown(socket.SHUT_WR) + except OSError: + pass + + +class Handler(socketserver.BaseRequestHandler): + def handle(self): + try: + with socket.create_connection(destination, timeout=10) as upstream: + self.request.settimeout(10) + thread = threading.Thread(target=copy, args=(self.request, upstream), daemon=True) + thread.start() + copy(upstream, self.request) + thread.join(timeout=10) + except OSError: + pass + + +class Server(socketserver.ThreadingTCPServer): + daemon_threads = True + + +with Server(listen, Handler) as server: + Path(marker).write_text(str(os.getpid()), encoding="ascii") + server.serve_forever() diff --git a/tests/test_acl.c b/tests/test_acl.c index 080bd95eb..cdf718af9 100644 --- a/tests/test_acl.c +++ b/tests/test_acl.c @@ -1,7 +1,7 @@ #include #include #include -#include +#include "test_helpers.h" #include "acl.h" @@ -10,11 +10,8 @@ int verbose = 0; static void test_invalid_regex_is_not_installed(void) { - char path[] = "/tmp/ss_acl_test.XXXXXX"; - int fd = mkstemp(path); - assert(fd >= 0); - - FILE *f = fdopen(fd, "w"); + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); assert(f != NULL); fprintf(f, "[black_list]\n"); fprintf(f, "[invalid\n"); @@ -31,17 +28,14 @@ test_invalid_regex_is_not_installed(void) assert(outbound_block_match_host("outbound.example") == 1); free_acl(); - unlink(path); + remove(path); } static void test_long_acl_line_is_discarded(void) { - char path[] = "/tmp/ss_acl_test.XXXXXX"; - int fd = mkstemp(path); - assert(fd >= 0); - - FILE *f = fdopen(fd, "w"); + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); assert(f != NULL); fprintf(f, "[black_list]\n"); for (int i = 0; i < 400; i++) { @@ -55,17 +49,14 @@ test_long_acl_line_is_discarded(void) assert(acl_match_host("kept.example") == 1); free_acl(); - unlink(path); + remove(path); } static void test_invalid_cidr_is_discarded(void) { - char path[] = "/tmp/ss_acl_test.XXXXXX"; - int fd = mkstemp(path); - assert(fd >= 0); - - FILE *f = fdopen(fd, "w"); + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); assert(f != NULL); fprintf(f, "[black_list]\n"); fprintf(f, "1.2.3.0/999\n"); @@ -83,7 +74,7 @@ test_invalid_cidr_is_discarded(void) assert(acl_match_host("path/with/slash.example") == 1); free_acl(); - unlink(path); + remove(path); } int diff --git a/tests/test_acl_literal.c b/tests/test_acl_literal.c new file mode 100644 index 000000000..9cf4ac6a1 --- /dev/null +++ b/tests/test_acl_literal.c @@ -0,0 +1,50 @@ +#include +#include +#include +#include "acl.h" +#include "rule.h" +int verbose = 0; + +int +main(int argc, char **argv) +{ + (void)argc; + char path[4096]; + assert(snprintf(path, sizeof(path), "%s.acl", argv[0]) < (int)sizeof(path)); + FILE *file = fopen(path, "w"); + assert(file != NULL); + fputs("[black_list]\nfull:Exact.example\nsuffix:blocked.example\n10.0.0.0/8\n" + "2001:db8::/32\n[white_list]\nsuffix:allowed.example\n" + "[outbound_block_list]\nfull:outbound.example\n", file); + fclose(file); + assert(init_acl(path) == 0); + assert(acl_match_host("EXACT.example") == 1); + assert(acl_match_host("sub.exact.example") == 0); + assert(acl_match_host("blocked.example") == 1); + assert(acl_match_host("a.blocked.example") == 1); + assert(acl_match_host("notblocked.example") == 0); + assert(acl_match_host("blocked.example.evil") == 0); + assert(acl_match_host("a.allowed.example") == -1); + assert(outbound_block_match_host("outbound.example") == 1); + assert(outbound_block_match_host("a.outbound.example") == 0); + assert(acl_match_host("10.42.1.2") == 1); + assert(acl_remove_ip("10.42.1.2") == 0); + assert(acl_match_host("10.42.1.2") == 0); + assert(acl_match_host("10.42.1.3") == 1); + assert(acl_add_ip("10.42.1.2") == 0); + assert(acl_match_host("10.42.1.2") == 1); + assert(acl_remove_ip("2001:db8::42") == 0); + assert(acl_match_host("2001:db8::42") == 0); + assert(acl_match_host("2001:db8::43") == 1); + free_acl(); +#if !SS_ENABLE_REGEX + file = fopen(path, "w"); + assert(file != NULL); + fputs("[black_list]\n10.0.0.0/8\n^old\\.regex$\n", file); + fclose(file); + assert(init_acl(path) == -1); + assert(acl_match_host("10.1.1.1") == 0); +#endif + remove(path); + return 0; +} diff --git a/tests/test_crypto.c b/tests/test_crypto.c index 1de032367..c09e54483 100644 --- a/tests/test_crypto.c +++ b/tests/test_crypto.c @@ -181,6 +181,7 @@ test_aead_repeat_salt_rejection_releases_context(void) * the first segment carries the nonce, so a codec that mishandles the * post-nonce steady state still round-trips a single segment correctly. */ +#if SS_ENABLE_LEGACY static void test_stream_multi_segment_roundtrip(const char *method) { @@ -222,6 +223,8 @@ test_stream_multi_segment_roundtrip(const char *method) ss_free(crypto); } +#endif + int main(void) { @@ -236,6 +239,7 @@ main(void) test_crypto_parse_key(); test_aead_repeat_salt_rejection_releases_context(); +#if SS_ENABLE_LEGACY /* mbedTLS-backed ciphers and libsodium-backed ciphers use different * code paths in stream.c, so cover both. */ test_stream_multi_segment_roundtrip("aes-256-cfb"); @@ -243,5 +247,8 @@ main(void) test_stream_multi_segment_roundtrip("camellia-128-cfb"); test_stream_multi_segment_roundtrip("chacha20-ietf"); test_stream_multi_segment_roundtrip("salsa20"); +#else + assert(crypto_init("password", NULL, "aes-256-cfb") == NULL); +#endif return 0; } diff --git a/tests/test_helpers.h b/tests/test_helpers.h new file mode 100644 index 000000000..b37e80946 --- /dev/null +++ b/tests/test_helpers.h @@ -0,0 +1,38 @@ +#ifndef SS_TEST_HELPERS_H +#define SS_TEST_HELPERS_H +#include +#include +#include +#include "platform.h" + +static inline void test_network_cleanup(void) +{ +#ifdef _WIN32 + WSACleanup(); +#endif +} +static inline void test_network_init(void) +{ +#ifdef _WIN32 + WSADATA data; + assert(WSAStartup(MAKEWORD(2, 2), &data) == 0); + assert(atexit(test_network_cleanup) == 0); +#endif +} +static inline FILE *test_tempfile(char *path, size_t size) +{ +#ifdef _WIN32 + char directory[MAX_PATH]; + DWORD length = GetTempPathA(sizeof(directory), directory); + assert(length != 0 && length < sizeof(directory)); + assert(size >= MAX_PATH); + assert(GetTempFileNameA(directory, "ssc", 0, path) != 0); + return fopen(path, "w"); +#else + assert(snprintf(path, size, "/tmp/ss-test.XXXXXX") < (int)size); + int fd = mkstemp(path); + assert(fd >= 0); + return fdopen(fd, "w"); +#endif +} +#endif diff --git a/tests/test_interop_rust.sh b/tests/test_interop_rust.sh index 08cf7d2cd..9025c0b49 100755 --- a/tests/test_interop_rust.sh +++ b/tests/test_interop_rust.sh @@ -1,121 +1,9 @@ -#!/bin/bash -# -# Interoperability tests against shadowsocks-rust. -# -# The 2022 (SIP022) ciphers are only useful if they interoperate with other -# implementations, and a spec detail can be got wrong in a way that still -# round-trips against ourselves. This drives our binaries against -# shadowsocks-rust's in both directions. -# -# Skips (exit 0) when shadowsocks-rust is not installed, so it is safe to -# run everywhere. Install it with `brew install shadowsocks-rust` or -# `cargo install shadowsocks-rust`. - -set -u - -BIN_DIR="${SS_BIN_DIR:-build/shared/bin}" -ORIGIN_PORT=${ORIGIN_PORT:-18999} -SERVER_PORT=${SERVER_PORT:-18388} -LOCAL_PORT=${LOCAL_PORT:-18081} - -result=0 -SPID="" -LPID="" -HPID="" - -cleanup() { - [ -n "$SPID" ] && kill -9 "$SPID" 2>/dev/null - [ -n "$LPID" ] && kill -9 "$LPID" 2>/dev/null - [ -n "$HPID" ] && kill -9 "$HPID" 2>/dev/null - wait 2>/dev/null - return 0 -} -trap cleanup EXIT - -for tool in sslocal ssserver; do - if ! command -v "$tool" > /dev/null 2>&1; then - echo "SKIP: shadowsocks-rust ($tool) not installed" - exit 0 - fi -done - -for tool in curl python3 openssl; do - if ! command -v "$tool" > /dev/null 2>&1; then - echo "SKIP: $tool not available" - exit 0 - fi -done - -if [ ! -x "$BIN_DIR/ss-local" ] || [ ! -x "$BIN_DIR/ss-server" ]; then - echo "SKIP: shadowsocks-libev binaries not found in $BIN_DIR" - echo " (set SS_BIN_DIR to override)" - exit 0 +#!/bin/sh +# Compatibility entry point. Python uses explicit SOCKS sockets, never curl +# or HTTP proxy environment variables. Required CI treats missing peers as errors. +if ! command -v python3 >/dev/null 2>&1; then + echo 'Missing interoperability prerequisite: python3' >&2 + if [ "${SS_REQUIRE_INTEROP:-0}" = 1 ]; then exit 1; fi + exit 77 fi - -# A local origin server keeps the test off the network. -WWW_DIR="$(mktemp -d)" -echo "interop-ok" > "$WWW_DIR/index.html" -(cd "$WWW_DIR" && python3 -m http.server "$ORIGIN_PORT" --bind 127.0.0.1) \ - > /dev/null 2>&1 & -HPID=$! -sleep 1 - -# Fetch through the SOCKS5 proxy and check we got the origin's content back. -fetch_through_proxy() { - curl -s --max-time 15 --socks5-hostname "127.0.0.1:$LOCAL_PORT" \ - "http://127.0.0.1:$ORIGIN_PORT/" 2>/dev/null | grep -q "interop-ok" -} - -run_case() { - local desc="$1" server_cmd="$2" client_cmd="$3" - - printf '\e[0;36mrunning: %s\e[0m\n' "$desc" - - $server_cmd > /dev/null 2>&1 & - SPID=$! - sleep 1 - $client_cmd > /dev/null 2>&1 & - LPID=$! - sleep 2 - - if fetch_through_proxy; then - printf '\e[0;32mOK: %s\e[0m\n' "$desc" - else - printf '\e[0;31mFAILED: %s\e[0m\n' "$desc" - result=1 - fi - - kill -9 "$SPID" "$LPID" 2>/dev/null - wait "$SPID" "$LPID" 2>/dev/null - SPID="" - LPID="" - # let the listening sockets go away before the next case binds them - sleep 2 -} - -test_method() { - local method="$1" key_size="$2" - local psk - - psk="$(openssl rand -base64 "$key_size")" - - run_case "$method: libev client -> rust server" \ - "ssserver -s 127.0.0.1:$SERVER_PORT -k $psk -m $method" \ - "$BIN_DIR/ss-local -s 127.0.0.1 -p $SERVER_PORT -l $LOCAL_PORT -k $psk -m $method" - - run_case "$method: rust client -> libev server" \ - "$BIN_DIR/ss-server -s 127.0.0.1 -p $SERVER_PORT -k $psk -m $method" \ - "sslocal -b 127.0.0.1:$LOCAL_PORT -s 127.0.0.1:$SERVER_PORT -k $psk -m $method" -} - -# SIP022. The key sizes are fixed by the spec. -test_method 2022-blake3-aes-128-gcm 16 -test_method 2022-blake3-aes-256-gcm 32 -test_method 2022-blake3-chacha20-poly1305 32 - -# A legacy AEAD cipher, to catch a regression that only affects the old path. -test_method aes-256-gcm 32 - -rm -rf "$WWW_DIR" - -exit $result +exec python3 "$(dirname "$0")/interop.py" "$@" diff --git a/tests/test_ipset.c b/tests/test_ipset.c new file mode 100644 index 000000000..e0146a1fe --- /dev/null +++ b/tests/test_ipset.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include "ipset.h" +int verbose = 0; + +static uint32_t state = 42; +static uint32_t +next_random(void) +{ + state = state * 1664525u + 1013904223u; + return state; +} + +static void +check_family(unsigned width) +{ + struct ss_ipset set = {0}; + uint8_t address[16] = {0}; + bool oracle[256] = {false}; + unsigned last = width / 8 - 1; + /* Independent exhaustive bitmap oracle over a /24 or /120. Prefix writes + * can cover the whole space; host deletion must punch holes in them. */ + for (unsigned round = 0; round < 2000; round++) { + unsigned host = next_random() >> 24; + unsigned bits = next_random() % 9; + bool present = (next_random() >> 31) != 0; + unsigned mask = bits == 0 ? 0 : (255u << (8 - bits)) & 255u; + address[last] = (uint8_t)host; + ss_ipset_assign(&set, address, width, width - 8 + bits, present); + for (unsigned i = 0; i < 256; i++) { + if ((i & mask) == (host & mask)) { + oracle[i] = present; + } + address[last] = (uint8_t)i; + assert(ss_ipset_contains(&set, address, width) == oracle[i]); + } + } + ss_ipset_assign(&set, address, width, 0, true); + memset(address, 255, sizeof(address)); + assert(ss_ipset_contains(&set, address, width)); + ss_ipset_assign(&set, address, width, width, false); + assert(!ss_ipset_contains(&set, address, width)); + address[last]--; + assert(ss_ipset_contains(&set, address, width)); + ss_ipset_assign(&set, address, width, width + 1, false); + assert(ss_ipset_contains(&set, address, width)); + ss_ipset_clear(&set); + ss_ipset_clear(&set); + assert(!ss_ipset_contains(&set, address, width)); +} + +int +main(void) +{ + check_family(32); + check_family(128); + return 0; +} diff --git a/tests/test_ipset_reference.c b/tests/test_ipset_reference.c new file mode 100644 index 000000000..b8a3405a0 --- /dev/null +++ b/tests/test_ipset_reference.c @@ -0,0 +1,59 @@ +/* Optional migration differential test. Link against the pre-migration ipset + * and cork archives; these are intentionally not production dependencies. */ +#include +#include +#include +#include +#include "ipset.h" +int verbose = 0; +static uint32_t state = 0x725cab91; +static uint32_t next_random(void) +{ + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + return state; +} +static void check_family(unsigned width) +{ + struct ss_ipset current = {0}; + struct ip_set reference; + ipset_init(&reference); + for (unsigned round = 0; round < 2000; round++) { + uint8_t bytes[16]; + for (unsigned i = 0; i < sizeof(bytes); i++) bytes[i] = (uint8_t)next_random(); + unsigned prefix = next_random() % (width + 1); + bool present = (next_random() & 1) != 0; + ss_ipset_assign(¤t, bytes, width, prefix, present); + struct cork_ipv4 v4; + struct cork_ipv6 v6; + memcpy(v4._.u8, bytes, sizeof(v4._.u8)); + memcpy(v6._.u8, bytes, sizeof(v6._.u8)); + if (width == 32) { + if (present) ipset_ipv4_add_network(&reference, &v4, prefix); + else ipset_ipv4_remove_network(&reference, &v4, prefix); + } else { + if (present) ipset_ipv6_add_network(&reference, &v6, prefix); + else ipset_ipv6_remove_network(&reference, &v6, prefix); + } + for (unsigned sample = 0; sample < 257; sample++) { + if (sample != 0) { + for (unsigned i = 0; i < sizeof(bytes); i++) bytes[i] = (uint8_t)next_random(); + } + memcpy(v4._.u8, bytes, sizeof(v4._.u8)); + memcpy(v6._.u8, bytes, sizeof(v6._.u8)); + bool old = width == 32 ? ipset_contains_ipv4(&reference, &v4) : ipset_contains_ipv6(&reference, &v6); + assert(old == ss_ipset_contains(¤t, bytes, width)); + } + } + ipset_done(&reference); + ss_ipset_clear(¤t); +} +int main(void) +{ + ipset_init_library(); + check_family(32); + check_family(128); + puts("1,028,000 IPv4/IPv6 membership comparisons matched the original libipset"); + return 0; +} diff --git a/tests/test_jconf.c b/tests/test_jconf.c index bda256964..03f2a5f93 100644 --- a/tests/test_jconf.c +++ b/tests/test_jconf.c @@ -6,8 +6,10 @@ #include #include #include -#include +#include "test_helpers.h" +#ifndef _WIN32 #include +#endif int verbose = 0; @@ -103,11 +105,8 @@ test_parse_addr_invalid_ipv6_like_host_is_not_rewritten(void) static void test_read_jconf_preserves_64bit_integer_strings(void) { - char path[] = "/tmp/ss_jconf_test.XXXXXX"; - int fd = mkstemp(path); - assert(fd >= 0); - - FILE *f = fdopen(fd, "w"); + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); assert(f != NULL); fprintf(f, "{\n"); fprintf(f, "\"server\":\"127.0.0.1\",\n"); @@ -122,7 +121,7 @@ test_read_jconf_preserves_64bit_integer_strings(void) assert(conf->remote_port != NULL); assert(strcmp(conf->remote_port, "4294967296") == 0); - unlink(path); + remove(path); } static void @@ -139,19 +138,33 @@ write_minimal_config(FILE *f, const char *extra_line) fprintf(f, "\n}\n"); } -#ifndef __MINGW32__ +static const char *test_executable; static void test_read_jconf_rejects_out_of_range_int_options(void) { - char path[] = "/tmp/ss_jconf_test.XXXXXX"; - int fd = mkstemp(path); - assert(fd >= 0); - - FILE *f = fdopen(fd, "w"); + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); assert(f != NULL); write_minimal_config(f, "\"tcp_incoming_sndbuf\":2147483648"); fclose(f); +#ifdef _WIN32 + char command[3 * 4096]; + assert(snprintf(command, sizeof(command), "\"%s\" --read-config \"%s\"", + test_executable, path) < (int)sizeof(command)); + STARTUPINFOA startup = {0}; + PROCESS_INFORMATION child = {0}; + startup.cb = sizeof(startup); + assert(CreateProcessA(test_executable, command, NULL, NULL, FALSE, 0, + NULL, NULL, &startup, &child)); + CloseHandle(child.hThread); + assert(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0); + DWORD status; + assert(GetExitCodeProcess(child.hProcess, &status)); + CloseHandle(child.hProcess); + /* FATAL exits with -1, which is a valid nonzero DWORD exit status. */ + assert(status != 0); +#else pid_t pid = fork(); assert(pid >= 0); if (pid == 0) { @@ -163,14 +176,20 @@ test_read_jconf_rejects_out_of_range_int_options(void) assert(waitpid(pid, &status, 0) == pid); assert(WIFEXITED(status)); assert(WEXITSTATUS(status) != 0); +#endif - unlink(path); + remove(path); } -#endif int -main(void) +main(int argc, char **argv) { + test_executable = argv[0]; + test_network_init(); + if (argc == 3 && strcmp(argv[1], "--read-config") == 0) { + read_jconf(argv[2]); + return 0; + } test_parse_addr_ipv4_with_port(); test_parse_addr_ipv6_with_port(); test_parse_addr_hostname_with_port(); @@ -179,8 +198,6 @@ main(void) test_parse_addr_malformed_bracketed_ipv6_is_not_rewritten(); test_parse_addr_invalid_ipv6_like_host_is_not_rewritten(); test_read_jconf_preserves_64bit_integer_strings(); -#ifndef __MINGW32__ test_read_jconf_rejects_out_of_range_int_options(); -#endif return 0; } diff --git a/tests/test_json.c b/tests/test_json.c index 7dc065493..64f87e23b 100644 --- a/tests/test_json.c +++ b/tests/test_json.c @@ -139,6 +139,15 @@ test_parse_empty_array(void) int main(void) { + const char *wide_numbers = "[4294967296,9223372036854775807,-4294967296,0.4294967296]"; + json_value *wide = json_parse(wide_numbers, strlen(wide_numbers)); + assert(wide != NULL && wide->type == json_array && wide->u.array.length == 4); + assert(wide->u.array.values[0]->u.integer == INT64_C(4294967296)); + assert(wide->u.array.values[1]->u.integer == INT64_MAX); + assert(wide->u.array.values[2]->u.integer == -INT64_C(4294967296)); + assert(wide->u.array.values[3]->u.dbl > 0.4294967295 && wide->u.array.values[3]->u.dbl < 0.4294967297); + json_value_free(wide); + assert(json_parse("9223372036854775808", 19) == NULL); test_parse_simple_object(); test_parse_array(); test_parse_nested(); diff --git a/tests/test_netutils.c b/tests/test_netutils.c index d75153d82..b47000de6 100644 --- a/tests/test_netutils.c +++ b/tests/test_netutils.c @@ -5,9 +5,7 @@ #include #include #include -#include -#include -#include +#include "test_helpers.h" int verbose = 0; @@ -129,6 +127,7 @@ test_get_sockaddr_rejects_invalid_ip_literal_ports(void) int main(void) { + test_network_init(); test_get_sockaddr_len(); test_sockaddr_cmp(); test_sockaddr_cmp_addr(); diff --git a/tests/test_process.c b/tests/test_process.c new file mode 100644 index 000000000..a4d888409 --- /dev/null +++ b/tests/test_process.c @@ -0,0 +1,85 @@ +#include +#include +#include +#include +#include "ss_process.h" +#ifdef _WIN32 +#include +static void pause_tick(void) { Sleep(10); } +#else +#include +static void pause_tick(void) { struct timespec delay = {0, 10000000}; nanosleep(&delay, NULL); } +#endif +int verbose = 0; + +int +main(int argc, char **argv) +{ + if (argc > 1 && strcmp(argv[1], "--child") == 0) { + assert(argc == 5); + FILE *file = fopen(argv[2], "w"); + assert(file != NULL); + assert(strcmp(argv[3], "spaces and \"quotes\" with \\") == 0); + assert(strcmp(argv[4], "") == 0); + assert(strcmp(getenv("SS_PROCESS_TEST"), "child value") == 0); + fputs("ready\n", file); + fclose(file); + for (unsigned i = 0; i < 1000; i++) pause_tick(); + return 0; + } + if (argc > 1 && strcmp(argv[1], "--exit") == 0) return 17; + + struct ss_process *process = ss_process_new("ss-no-such-program-ef91ac"); + ss_process_arg(process, "ss-no-such-program-ef91ac"); + int started = ss_process_start(process, 0); + assert(started == -1 || started == 0); + /* POSIX permits exec failure to be reported by child exit (notably under + * Valgrind's spawn wrapper), rather than synchronously by posix_spawnp. */ + if (started == 0) { + for (unsigned i = 0; i < 500 && ss_process_running(process); i++) pause_tick(); + assert(!ss_process_running(process)); + } + ss_process_free(process); + + char path[4096]; + assert(snprintf(path, sizeof(path), "%s.child-output", argv[0]) < (int)sizeof(path)); + remove(path); + process = ss_process_new(argv[0]); + ss_process_arg(process, argv[0]); + ss_process_arg(process, "--child"); + ss_process_arg(process, path); + ss_process_arg(process, "spaces and \"quotes\" with \\"); + ss_process_arg(process, ""); + ss_process_env(process, "SS_PROCESS_TEST", "child value"); + const char *before = getenv("SS_PROCESS_TEST"); + char *saved = before == NULL ? NULL : strdup(before); + assert(ss_process_start(process, 0) == 0); + bool ready = false; + for (unsigned i = 0; i < 500; i++) { + FILE *file = fopen(path, "r"); + if (file != NULL) { + char line[16]; + ready = fgets(line, sizeof(line), file) != NULL && strcmp(line, "ready\n") == 0; + fclose(file); + } + if (ready) break; + pause_tick(); + } + assert(ready); + assert(ss_process_running(process)); + const char *after = getenv("SS_PROCESS_TEST"); + assert(saved == NULL ? after == NULL : after != NULL && strcmp(after, saved) == 0); + free(saved); + ss_process_free(process); + remove(path); + + process = ss_process_new(argv[0]); + ss_process_arg(process, argv[0]); + ss_process_arg(process, "--exit"); + assert(ss_process_start(process, 0) == 0); + for (unsigned i = 0; i < 500 && ss_process_running(process); i++) pause_tick(); + assert(!ss_process_running(process)); + ss_process_free(process); + ss_process_free(NULL); + return 0; +} diff --git a/tests/test_redir_qemu.sh b/tests/test_redir_qemu.sh index 2681fab14..947b2ec49 100755 --- a/tests/test_redir_qemu.sh +++ b/tests/test_redir_qemu.sh @@ -11,7 +11,7 @@ # # Usage: bash tests/test_redir_qemu.sh [BIN_DIR] # BIN_DIR: directory containing ss-server and ss-redir binaries -# (default: build/shared/bin/) +# (default: build/bin/) # # Requirements (Linux only): # - qemu-system-x86_64 @@ -22,7 +22,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -BIN_DIR="${1:-$PROJECT_DIR/build/shared/bin}" +BIN_DIR="${1:-$PROJECT_DIR/build/bin}" # Resolve to absolute path BIN_DIR="$(cd "$BIN_DIR" && pwd)" diff --git a/tests/test_resolv.c b/tests/test_resolv.c new file mode 100644 index 000000000..87b2d9b29 --- /dev/null +++ b/tests/test_resolv.c @@ -0,0 +1,56 @@ +/* Exercise real c-ares cancellation with outstanding A and AAAA requests. */ +#include "test_helpers.h" +#include +#include "resolv.h" + +int verbose = 0; +struct result { unsigned called, freed; }; +static void resolved(struct sockaddr *address, void *data) +{ + struct result *result = data; + assert(address == NULL); + assert(result->freed == 0); + result->called++; +} +static void released(void *data) +{ + struct result *result = data; + assert(result->called == 1); + result->freed++; +} +int main(void) +{ + test_network_init(); + /* A bound but unread UDP socket prevents DNS responses or ICMP rejection; + * the test needs no external DNS server or connectivity. */ + ss_socket_t sink = socket(AF_INET, SOCK_DGRAM, 0); + struct sockaddr_in address = {0}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + assert(bind(sink, (struct sockaddr *)&address, sizeof(address)) == 0); + socklen_t size = sizeof(address); + assert(getsockname(sink, (struct sockaddr *)&address, &size) == 0); + char nameserver[64]; + snprintf(nameserver, sizeof(nameserver), "127.0.0.1:%u", ntohs(address.sin_port)); + struct ev_loop *loop = ev_loop_new(0); + assert(loop != NULL); + for (unsigned round = 0; round < 3; round++) { + struct result results[64] = {{0}}; + assert(resolv_init(loop, nameserver, 0) == 0); + for (unsigned i = 0; i < 64; i++) { + char hostname[64]; + snprintf(hostname, sizeof(hostname), "pending-%u.invalid", i); + resolv_start(hostname, htons(443), resolved, released, &results[i]); + } + ev_run(loop, EVRUN_NOWAIT); + resolv_shutdown(loop); + for (unsigned i = 0; i < 64; i++) { + assert(results[i].called == 1); + assert(results[i].freed == 1); + } + assert(ev_run(loop, EVRUN_NOWAIT) == 0); + } + ev_loop_destroy(loop); + ss_socket_close(sink); + return 0; +} diff --git a/tests/test_rule.c b/tests/test_rule.c index 2bfde205a..82670ad39 100644 --- a/tests/test_rule.c +++ b/tests/test_rule.c @@ -70,8 +70,8 @@ test_init_rule_invalid(void) static void test_lookup_rule(void) { - struct cork_dllist rules; - cork_dllist_init(&rules); + struct ss_list rules; + ss_list_init(&rules); rule_t *rule1 = new_rule(); accept_rule_arg(rule1, "^google\\.com$"); diff --git a/tests/test_ssurl.c b/tests/test_ssurl.c index b937b7752..9ebbce61b 100644 --- a/tests/test_ssurl.c +++ b/tests/test_ssurl.c @@ -157,6 +157,7 @@ test_rejects_malformed(void) }; for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + memset(&u, 0xa5, sizeof(u)); assert(ss_url_parse(bad[i], &u) == -1); /* a rejected parse must leave nothing behind to free */ assert(u.method == NULL && u.host == NULL && u.port == NULL); diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt new file mode 100644 index 000000000..9c2413dd0 --- /dev/null +++ b/third_party/CMakeLists.txt @@ -0,0 +1,82 @@ +# Sources are included in the checkout/release archive. Configuration never +# accesses the network. Extract into the build tree, verified before use. +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/manifest.json" manifest) +string(JSON count LENGTH "${manifest}") +math(EXPR last "${count} - 1") +foreach(i RANGE ${last}) + foreach(field name archive sha256) + string(JSON ${field} GET "${manifest}" ${i} ${field}) + endforeach() + if(name STREQUAL "pcre2" AND NOT SS_ENABLE_REGEX) + continue() + endif() + set(archive_path "${CMAKE_CURRENT_SOURCE_DIR}/dist/${archive}") + file(SHA256 "${archive_path}" actual) + if(NOT actual STREQUAL sha256) + message(FATAL_ERROR "Bundled ${name}: archive checksum mismatch") + endif() + set(destination "${CMAKE_CURRENT_BINARY_DIR}/${name}-${sha256}") + if(NOT EXISTS "${destination}/.extracted") + file(MAKE_DIRECTORY "${destination}") + file(ARCHIVE_EXTRACT INPUT "${archive_path}" DESTINATION "${destination}") + file(WRITE "${destination}/.extracted" "${sha256}\n") + endif() + file(GLOB children LIST_DIRECTORIES TRUE "${destination}/*") + list(FILTER children EXCLUDE REGEX "/\\.extracted$") + list(GET children 0 source) + set(${name}_SOURCE "${source}") +endforeach() + +set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE) +set(ENABLE_TESTING OFF CACHE BOOL "" FORCE) +set(USE_SHARED_MBEDTLS_LIBRARY OFF CACHE BOOL "" FORCE) +set(USE_STATIC_MBEDTLS_LIBRARY ON CACHE BOOL "" FORCE) +set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE) +set(MBEDTLS_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/mbedtls_config.h" CACHE FILEPATH "Crypto-only configuration" FORCE) +add_subdirectory("${mbedtls_SOURCE}" mbedtls EXCLUDE_FROM_ALL) +add_library(ss_mbedcrypto ALIAS mbedcrypto) +set(MBEDTLS_INCLUDE_DIRS "${mbedtls_SOURCE}/include" PARENT_SCOPE) + +set(CARES_STATIC ON CACHE BOOL "" FORCE) +set(CARES_SHARED OFF CACHE BOOL "" FORCE) +set(CARES_STATIC_PIC ON CACHE BOOL "" FORCE) +set(CARES_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(CARES_BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(CARES_INSTALL OFF CACHE BOOL "" FORCE) +add_subdirectory("${cares_SOURCE}" cares EXCLUDE_FROM_ALL) +get_target_property(cares_target c-ares::cares ALIASED_TARGET) +add_library(ss_cares ALIAS ${cares_target}) +set(CARES_INCLUDE_DIRS "${cares_SOURCE}/include;${CMAKE_CURRENT_BINARY_DIR}/cares" PARENT_SCOPE) + +if(SS_ENABLE_REGEX) +set(BUILD_SHARED_LIBS OFF) +set(PCRE2_BUILD_PCRE2_8 ON CACHE BOOL "" FORCE) +set(PCRE2_BUILD_PCRE2_16 OFF CACHE BOOL "" FORCE) +set(PCRE2_BUILD_PCRE2_32 OFF CACHE BOOL "" FORCE) +set(PCRE2_BUILD_PCRE2GREP OFF CACHE BOOL "" FORCE) +set(PCRE2_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(PCRE2_SUPPORT_JIT OFF CACHE BOOL "" FORCE) +set(PCRE2_INSTALL OFF CACHE BOOL "" FORCE) +add_subdirectory("${pcre2_SOURCE}" pcre2 EXCLUDE_FROM_ALL) +add_library(ss_pcre2 ALIAS pcre2-8-static) +set(PCRE2_INCLUDE_DIRS "${CMAKE_CURRENT_BINARY_DIR}/pcre2" PARENT_SCOPE) + +endif() + +add_library(ss_ev STATIC "${ev_SOURCE}/ev.c") +target_compile_definitions(ss_ev PRIVATE EV_STANDALONE=1) +target_include_directories(ss_ev PUBLIC "${ev_SOURCE}") +if(WIN32) + target_link_libraries(ss_ev PUBLIC ws2_32) + target_compile_options(ss_ev PRIVATE -include "${CMAKE_CURRENT_SOURCE_DIR}/libev_windows.h") +endif() + +include(Sodium.cmake) + +foreach(name sodium ev mbedtls) + install(FILES "${${name}_SOURCE}/LICENSE" DESTINATION "${CMAKE_INSTALL_DATADIR}/licenses/shadowsocks-libev/${name}") +endforeach() +install(FILES "${cares_SOURCE}/LICENSE.md" DESTINATION "${CMAKE_INSTALL_DATADIR}/licenses/shadowsocks-libev/cares") +if(SS_ENABLE_REGEX) + install(FILES "${pcre2_SOURCE}/LICENCE.md" DESTINATION "${CMAKE_INSTALL_DATADIR}/licenses/shadowsocks-libev/pcre2") +endif() diff --git a/third_party/README.md b/third_party/README.md new file mode 100644 index 000000000..41f6725e2 --- /dev/null +++ b/third_party/README.md @@ -0,0 +1,37 @@ +# Bundled sources + +`dist/` contains unmodified upstream source release archives. `manifest.json` +records upstream URLs, versions and SHA-256 hashes; CMake verifies hashes before +extracting into the build tree. Configuration never downloads dependencies. +These hashes pin downloaded content; they are not a claim of an independent +signature verification. + +- Mbed TLS 3.6.7: Apache-2.0 OR GPL-2.0-or-later; see the archive's LICENSE files. +- libsodium 1.0.22: ISC; see LICENSE in the archive. +- c-ares 1.34.8: MIT; see LICENSE.md in the archive. +- PCRE2 10.48: BSD-3-Clause; see LICENCE.md in the archive, including third-party notices. +- libev 4.33: BSD-2-Clause OR GPL-2.0-or-later; see LICENSE and source notices. +- bloom: copied unchanged from the former shadowsocks/libbloom submodule, + commit 437e1add5a2b9a87797d8c648df7cf5f3ee155a8. BSD-2-Clause; + license and MurmurHash2 source notices retained alongside the sources. +- BLAKE3 remains in src/blake3; its existing license files remain there. +- uthash remains in src/uthash.h with its upstream copyright/license notice. + +Mbed TLS, c-ares and PCRE2 use their upstream CMake builds. The local libsodium +CMake adapter compiles the portable C backends listed in upstream Makefile.am; +it is maintained by this project, not upstream. Upstream known-answer tests +are built and run under the `vendor` CTest label. SIMD acceleration needs its +own compiler probes and validation before enabling additional backends. +On Windows, `libev_windows.h` selects raw Winsock handles rather than CRT file +descriptors, consistently for application I/O and libev's wakeup sockets. +Windows therefore requires bundled mode; an arbitrary system libev build does +not necessarily use the same descriptor convention. + +To update an archive: select a supported upstream release, download its release +archive from the upstream source, verify published signatures/checksums where +available, update manifest.json (including version-specific configuration in +Sodium.cmake), and run bundled builds, vendor tests, project tests, sanitizers, +and independent TCP/UDP interoperability on the supported platform matrix. +Keep release archives and notices intact. Review upstream advisories and local +adapter changes when selecting an update. System mode remains available to +packagers who update libraries separately. diff --git a/third_party/Sodium.cmake b/third_party/Sodium.cmake new file mode 100644 index 000000000..646cec1ce --- /dev/null +++ b/third_party/Sodium.cmake @@ -0,0 +1,53 @@ +# Portable C backend: no assembler or platform build scripts needed. SIMD +# implementations compile to empty translation units unless enabled by probes. +file(READ "${sodium_SOURCE}/src/libsodium/Makefile.am" sodium_makefile) +string(REGEX MATCHALL "[a-zA-Z0-9_/-]+\\.c([ \n\r]|$)" sodium_relative_sources "${sodium_makefile}") +set(sodium_sources) +foreach(source IN LISTS sodium_relative_sources) + string(STRIP "${source}" source) + list(APPEND sodium_sources "${sodium_SOURCE}/src/libsodium/${source}") +endforeach() +list(REMOVE_DUPLICATES sodium_sources) +set(VERSION 1.0.22) +set(SODIUM_LIBRARY_VERSION_MAJOR 26) +set(SODIUM_LIBRARY_VERSION_MINOR 4) +set(SODIUM_LIBRARY_MINIMAL_DEF "") +configure_file("${sodium_SOURCE}/src/libsodium/include/sodium/version.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/sodium-include/sodium/version.h" @ONLY) +configure_file("${sodium_SOURCE}/src/libsodium/include/sodium/export.h" + "${CMAKE_CURRENT_BINARY_DIR}/sodium-include/sodium/export.h" COPYONLY) +add_library(ss_sodium STATIC ${sodium_sources}) +target_include_directories(ss_sodium PUBLIC + "${CMAKE_CURRENT_BINARY_DIR}/sodium-include" + "${sodium_SOURCE}/src/libsodium/include" + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/sodium-include/sodium" "${sodium_SOURCE}/src/libsodium/include/sodium") +target_compile_definitions(ss_sodium PUBLIC SODIUM_STATIC PRIVATE DEV_MODE=1) +if(NOT MSVC) + target_compile_options(ss_sodium PRIVATE -fno-strict-aliasing -fwrapv) +endif() +if(WIN32) + target_link_libraries(ss_sodium PUBLIC bcrypt) +endif() +set(SODIUM_INCLUDE_DIRS "${CMAKE_CURRENT_BINARY_DIR}/sodium-include;${sodium_SOURCE}/src/libsodium/include" PARENT_SCOPE) + +if(CMAKE_USE_PTHREADS_INIT) + target_compile_definitions(ss_sodium PRIVATE HAVE_PTHREAD=1) + target_link_libraries(ss_sodium PUBLIC Threads::Threads) +endif() + +# Upstream known-answer tests validate this project's portable CMake adapter. +if(BUILD_TESTING) + foreach(name aead_chacha20poly1305 aead_xchacha20poly1305 auth auth2 + chacha20 stream stream2 stream3 stream4 randombytes + sodium_core sodium_utils sodium_utils2 sodium_utils3) + if(EXISTS "${sodium_SOURCE}/test/default/${name}.c") + add_executable(sodium_test_${name} "${sodium_SOURCE}/test/default/${name}.c") + target_link_libraries(sodium_test_${name} PRIVATE ss_sodium) + target_include_directories(sodium_test_${name} PRIVATE "${sodium_SOURCE}/test/quirks") + target_compile_definitions(sodium_test_${name} PRIVATE + TEST_SRCDIR="${sodium_SOURCE}/test/default") + add_test(NAME sodium_${name} COMMAND sodium_test_${name}) + set_tests_properties(sodium_${name} PROPERTIES LABELS "vendor" TIMEOUT 120) + endif() + endforeach() +endif() diff --git a/third_party/bloom/LICENSE b/third_party/bloom/LICENSE new file mode 100644 index 000000000..a3b17c14c --- /dev/null +++ b/third_party/bloom/LICENSE @@ -0,0 +1,26 @@ + +Copyright (c) 2012, Jyri J. Virkki +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/bloom/bloom.c b/third_party/bloom/bloom.c new file mode 100644 index 000000000..8ae8167b3 --- /dev/null +++ b/third_party/bloom/bloom.c @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2012-2016, Jyri J. Virkki + * All rights reserved. + * + * This file is under BSD license. See LICENSE file. + */ + +/* + * Refer to bloom.h for documentation on the public interfaces. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bloom.h" +#include "murmurhash2.h" + +#define MAKESTRING(n) STRING(n) +#define STRING(n) #n + + +inline static int test_bit_set_bit(unsigned char * buf, + unsigned int x, int set_bit) +{ + unsigned int byte = x >> 3; + unsigned char c = buf[byte]; // expensive memory access + unsigned int mask = 1 << (x % 8); + + if (c & mask) { + return 1; + } else { + if (set_bit) { + buf[byte] = c | mask; + } + return 0; + } +} + + +static int bloom_check_add(struct bloom * bloom, + const void * buffer, int len, int add) +{ + if (bloom->ready == 0) { + printf("bloom at %p not initialized!\n", (void *)bloom); + return -1; + } + + int hits = 0; + register unsigned int a = murmurhash2(buffer, len, 0x9747b28c); + register unsigned int b = murmurhash2(buffer, len, a); + register unsigned int x; + register unsigned int i; + + for (i = 0; i < bloom->hashes; i++) { + x = (a + i*b) % bloom->bits; + if (test_bit_set_bit(bloom->bf, x, add)) { + hits++; + } else if (!add) { + // Don't care about the presence of all the bits. Just our own. + return 0; + } + } + + if (hits == bloom->hashes) { + return 1; // 1 == element already in (or collision) + } + + return 0; +} + + +int bloom_init_size(struct bloom * bloom, int entries, double error, + unsigned int cache_size) +{ + return bloom_init(bloom, entries, error); +} + + +int bloom_init(struct bloom * bloom, int entries, double error) +{ + bloom->ready = 0; + + if (entries < 1 || error == 0) { + return 1; + } + + bloom->entries = entries; + bloom->error = error; + + double num = log(bloom->error); + double denom = 0.480453013918201; // ln(2)^2 + bloom->bpe = -(num / denom); + + double dentries = (double)entries; + bloom->bits = (int)(dentries * bloom->bpe); + + if (bloom->bits % 8) { + bloom->bytes = (bloom->bits / 8) + 1; + } else { + bloom->bytes = bloom->bits / 8; + } + + bloom->hashes = (int)ceil(0.693147180559945 * bloom->bpe); // ln(2) + + bloom->bf = (unsigned char *)calloc(bloom->bytes, sizeof(unsigned char)); + if (bloom->bf == NULL) { + return 1; + } + + bloom->ready = 1; + return 0; +} + + +int bloom_check(struct bloom * bloom, const void * buffer, int len) +{ + return bloom_check_add(bloom, buffer, len, 0); +} + + +int bloom_add(struct bloom * bloom, const void * buffer, int len) +{ + return bloom_check_add(bloom, buffer, len, 1); +} + + +void bloom_print(struct bloom * bloom) +{ + printf("bloom at %p\n", (void *)bloom); + printf(" ->entries = %d\n", bloom->entries); + printf(" ->error = %f\n", bloom->error); + printf(" ->bits = %d\n", bloom->bits); + printf(" ->bits per elem = %f\n", bloom->bpe); + printf(" ->bytes = %d\n", bloom->bytes); + printf(" ->hash functions = %d\n", bloom->hashes); +} + + +void bloom_free(struct bloom * bloom) +{ + if (bloom->ready) { + free(bloom->bf); + } + bloom->ready = 0; +} + + +int bloom_reset(struct bloom * bloom) +{ + if (!bloom->ready) return 1; + memset(bloom->bf, 0, bloom->bytes); + return 0; +} + + +const char * bloom_version() +{ + return MAKESTRING(BLOOM_VERSION); +} diff --git a/third_party/bloom/bloom.h b/third_party/bloom/bloom.h new file mode 100644 index 000000000..532e0e348 --- /dev/null +++ b/third_party/bloom/bloom.h @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2012-2016, Jyri J. Virkki + * All rights reserved. + * + * This file is under BSD license. See LICENSE file. + */ + +#ifndef _BLOOM_H +#define _BLOOM_H + +#ifdef __cplusplus +extern "C" { +#endif + + +/** *************************************************************************** + * Structure to keep track of one bloom filter. Caller needs to + * allocate this and pass it to the functions below. First call for + * every struct must be to bloom_init(). + * + */ +struct bloom +{ + // These fields are part of the public interface of this structure. + // Client code may read these values if desired. Client code MUST NOT + // modify any of these. + int entries; + double error; + int bits; + int bytes; + int hashes; + + // Fields below are private to the implementation. These may go away or + // change incompatibly at any moment. Client code MUST NOT access or rely + // on these. + double bpe; + unsigned char * bf; + int ready; +}; + + +/** *************************************************************************** + * Initialize the bloom filter for use. + * + * The filter is initialized with a bit field and number of hash functions + * according to the computations from the wikipedia entry: + * http://en.wikipedia.org/wiki/Bloom_filter + * + * Optimal number of bits is: + * bits = (entries * ln(error)) / ln(2)^2 + * + * Optimal number of hash functions is: + * hashes = bpe * ln(2) + * + * Parameters: + * ----------- + * bloom - Pointer to an allocated struct bloom (see above). + * entries - The expected number of entries which will be inserted. + * error - Probability of collision (as long as entries are not + * exceeded). + * + * Return: + * ------- + * 0 - on success + * 1 - on failure + * + */ +int bloom_init(struct bloom * bloom, int entries, double error); + + +/** *************************************************************************** + * Deprecated, use bloom_init() + * + */ +int bloom_init_size(struct bloom * bloom, int entries, double error, + unsigned int cache_size); + + +/** *************************************************************************** + * Check if the given element is in the bloom filter. Remember this may + * return false positive if a collision occured. + * + * Parameters: + * ----------- + * bloom - Pointer to an allocated struct bloom (see above). + * buffer - Pointer to buffer containing element to check. + * len - Size of 'buffer'. + * + * Return: + * ------- + * 0 - element is not present + * 1 - element is present (or false positive due to collision) + * -1 - bloom not initialized + * + */ +int bloom_check(struct bloom * bloom, const void * buffer, int len); + + +/** *************************************************************************** + * Add the given element to the bloom filter. + * The return code indicates if the element (or a collision) was already in, + * so for the common check+add use case, no need to call check separately. + * + * Parameters: + * ----------- + * bloom - Pointer to an allocated struct bloom (see above). + * buffer - Pointer to buffer containing element to add. + * len - Size of 'buffer'. + * + * Return: + * ------- + * 0 - element was not present and was added + * 1 - element (or a collision) had already been added previously + * -1 - bloom not initialized + * + */ +int bloom_add(struct bloom * bloom, const void * buffer, int len); + + +/** *************************************************************************** + * Print (to stdout) info about this bloom filter. Debugging aid. + * + */ +void bloom_print(struct bloom * bloom); + + +/** *************************************************************************** + * Deallocate internal storage. + * + * Upon return, the bloom struct is no longer usable. You may call bloom_init + * again on the same struct to reinitialize it again. + * + * Parameters: + * ----------- + * bloom - Pointer to an allocated struct bloom (see above). + * + * Return: none + * + */ +void bloom_free(struct bloom * bloom); + +/** *************************************************************************** + * Erase internal storage. + * + * Erases all elements. Upon return, the bloom struct returns to its initial + * (initialized) state. + * + * Parameters: + * ----------- + * bloom - Pointer to an allocated struct bloom (see above). + * + * Return: + * 0 - on success + * 1 - on failure + * + */ +int bloom_reset(struct bloom * bloom); + + +/** *************************************************************************** + * Returns version string compiled into library. + * + * Return: version string + * + */ +const char * bloom_version(); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/third_party/bloom/murmur2/MurmurHash2.c b/third_party/bloom/murmur2/MurmurHash2.c new file mode 100644 index 000000000..32c4c32b0 --- /dev/null +++ b/third_party/bloom/murmur2/MurmurHash2.c @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------- +// MurmurHash2, by Austin Appleby + +// Note - This code makes a few assumptions about how your machine behaves - + +// 1. We can read a 4-byte value from any address without crashing +// 2. sizeof(int) == 4 + +// And it has a few limitations - + +// 1. It will not work incrementally. +// 2. It will not produce the same results on little-endian and big-endian +// machines. + +unsigned int murmurhash2(const void * key, int len, const unsigned int seed) +{ + // 'm' and 'r' are mixing constants generated offline. + // They're not really 'magic', they just happen to work well. + + const unsigned int m = 0x5bd1e995; + const int r = 24; + + // Initialize the hash to a 'random' value + + unsigned int h = seed ^ len; + + // Mix 4 bytes at a time into the hash + + const unsigned char * data = (const unsigned char *)key; + + while(len >= 4) + { + unsigned int k = *(unsigned int *)data; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + data += 4; + len -= 4; + } + + // Handle the last few bytes of the input array + + switch(len) + { + case 3: h ^= data[2] << 16; + case 2: h ^= data[1] << 8; + case 1: h ^= data[0]; + h *= m; + }; + + // Do a few final mixes of the hash to ensure the last few + // bytes are well-incorporated. + + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return h; +} diff --git a/third_party/bloom/murmur2/murmurhash2.h b/third_party/bloom/murmur2/murmurhash2.h new file mode 100644 index 000000000..e607381e4 --- /dev/null +++ b/third_party/bloom/murmur2/murmurhash2.h @@ -0,0 +1,7 @@ + +#ifndef _BLOOM_MURMURHASH2 +#define _BLOOM_MURMURHASH2 + +unsigned int murmurhash2(const void * key, int len, const unsigned int seed); + +#endif diff --git a/third_party/dist/c-ares-1.34.8.tar.gz b/third_party/dist/c-ares-1.34.8.tar.gz new file mode 100644 index 000000000..3f41ac733 Binary files /dev/null and b/third_party/dist/c-ares-1.34.8.tar.gz differ diff --git a/third_party/dist/libev-4.33.tar.gz b/third_party/dist/libev-4.33.tar.gz new file mode 100644 index 000000000..4cb43d84d Binary files /dev/null and b/third_party/dist/libev-4.33.tar.gz differ diff --git a/third_party/dist/libsodium-1.0.22.tar.gz b/third_party/dist/libsodium-1.0.22.tar.gz new file mode 100644 index 000000000..7fe08df16 Binary files /dev/null and b/third_party/dist/libsodium-1.0.22.tar.gz differ diff --git a/third_party/dist/mbedtls-3.6.7.tar.bz2 b/third_party/dist/mbedtls-3.6.7.tar.bz2 new file mode 100644 index 000000000..79ed5d5f6 Binary files /dev/null and b/third_party/dist/mbedtls-3.6.7.tar.bz2 differ diff --git a/third_party/dist/pcre2-10.48.tar.gz b/third_party/dist/pcre2-10.48.tar.gz new file mode 100644 index 000000000..43c0b73c0 Binary files /dev/null and b/third_party/dist/pcre2-10.48.tar.gz differ diff --git a/third_party/libev_windows.h b/third_party/libev_windows.h new file mode 100644 index 000000000..67bf6224e --- /dev/null +++ b/third_party/libev_windows.h @@ -0,0 +1,8 @@ +/* libev's int watcher descriptors carry Winsock handles in this application. + * Keep the same conversion for its wakeup socket pair and application I/O. + * The default CRT descriptor conversion is incompatible with raw sockets. + */ +#include +#define EV_FD_TO_WIN32_HANDLE(fd) ((SOCKET)(unsigned int)(fd)) +#define EV_WIN32_HANDLE_TO_FD(handle) ((int)(handle)) +#define EV_WIN32_CLOSE_FD(fd) closesocket(EV_FD_TO_WIN32_HANDLE(fd)) diff --git a/third_party/manifest.json b/third_party/manifest.json new file mode 100644 index 000000000..c2435a89c --- /dev/null +++ b/third_party/manifest.json @@ -0,0 +1,37 @@ +[ + { + "name": "mbedtls", + "version": "3.6.7", + "url": "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.7/mbedtls-3.6.7.tar.bz2", + "archive": "mbedtls-3.6.7.tar.bz2", + "sha256": "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6" + }, + { + "name": "cares", + "version": "1.34.8", + "url": "https://github.com/c-ares/c-ares/releases/download/v1.34.8/c-ares-1.34.8.tar.gz", + "archive": "c-ares-1.34.8.tar.gz", + "sha256": "c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78" + }, + { + "name": "pcre2", + "version": "10.48", + "url": "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.48/pcre2-10.48.tar.gz", + "archive": "pcre2-10.48.tar.gz", + "sha256": "ebcc25aadf2a51fa1fefa9b8bc9e7a79b3dae86870a0f1152a22e42befd46888" + }, + { + "name": "sodium", + "version": "1.0.22", + "url": "https://download.libsodium.org/libsodium/releases/libsodium-1.0.22.tar.gz", + "archive": "libsodium-1.0.22.tar.gz", + "sha256": "adbdd8f16149e81ac6078a03aca6fc03b592b89ef7b5ed83841c086191be3349" + }, + { + "name": "ev", + "version": "4.33", + "url": "https://dist.schmorp.de/libev/libev-4.33.tar.gz", + "archive": "libev-4.33.tar.gz", + "sha256": "507eb7b8d1015fbec5b935f34ebed15bf346bed04a11ab82b8eee848c4205aea" + } +] diff --git a/third_party/mbedtls_config.h b/third_party/mbedtls_config.h new file mode 100644 index 000000000..2c6dcb606 --- /dev/null +++ b/third_party/mbedtls_config.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#ifndef SS_MBEDTLS_CONFIG_H +#define SS_MBEDTLS_CONFIG_H +/* Only the primitives used by Shadowsocks. No TLS, X.509, networking, public + * key operations, PSA storage or independent entropy subsystem. Randomness + * is supplied by libsodium. MD5/SHA1 remain required by legacy AEAD key + * derivation even when legacy stream ciphers are disabled. */ +#define MBEDTLS_AES_C +#define MBEDTLS_CIPHER_C +#define MBEDTLS_GCM_C +#define MBEDTLS_MD_C +#define MBEDTLS_MD5_C +#define MBEDTLS_SHA1_C +#define MBEDTLS_SHA256_C +#define MBEDTLS_SHA512_C +#define MBEDTLS_PLATFORM_C +#define MBEDTLS_VERSION_C +#define MBEDTLS_VERSION_FEATURES +#define MBEDTLS_CIPHER_MODE_CTR +#define MBEDTLS_CIPHER_MODE_CFB +#define MBEDTLS_CAMELLIA_C +#if defined(__x86_64__) || defined(_M_X64) +#define MBEDTLS_AESNI_C +#if defined(__GNUC__) || defined(__clang__) +#define MBEDTLS_HAVE_ASM +#endif +#endif +#if defined(__aarch64__) +#define MBEDTLS_AESCE_C +#endif +#endif