diff --git a/.clang-format b/.clang-format index 3321916..6101830 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,4 @@ -# TetherKit code formatting configuration. +# Tethernet code formatting configuration. # Google style as the baseline, with line width relaxed to 100 columns to fit # protocol constant tables and long type names. --- @@ -12,7 +12,7 @@ TabWidth: 2 UseTab: Never AccessModifierOffset: -1 -# Pointer/reference hugs the type: TetherKit writes `Foo* p`, not `Foo *p` +# Pointer/reference hugs the type: Tethernet writes `Foo* p`, not `Foo *p` DerivePointerAlignment: false PointerAlignment: Left ReferenceAlignment: Left @@ -34,7 +34,7 @@ PackConstructorInitializers: NextLine SortIncludes: CaseSensitive IncludeBlocks: Regroup IncludeCategories: - - Regex: '^"tetherkit/' + - Regex: '^"tethernet/' Priority: 1 - Regex: '^<(doctest|libusb)' Priority: 2 diff --git a/.clang-tidy b/.clang-tidy index 3f0efc2..b24bc4a 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,4 +1,4 @@ -# TetherKit static analysis configuration. +# Tethernet static analysis configuration. # # Trade-offs: # * This project is a low-level driver dealing with kernel ABIs and USB wire @@ -53,7 +53,7 @@ Checks: > -readability-uppercase-literal-suffix WarningsAsErrors: '' -HeaderFilterRegex: 'tetherkit/.*' +HeaderFilterRegex: 'tethernet/.*' FormatStyle: file CheckOptions: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 72bf1e8..73426c6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -54,7 +54,7 @@ # That is expected: Dependabot cannot approve its own PR. You bypass it # the same way you bypass your own PRs, because the "Require review" # ruleset lists the admin role as a bypass actor: -# gh api repos/justsrc/TetherKit/pulls//merge -X PUT -f merge_method=merge +# gh api repos/justsrc/Tethernet/pulls//merge -X PUT -f merge_method=merge # The REST endpoint makes the mechanism explicit. `gh pr merge` refuses # at its own pre-flight when the PR is BLOCKED (cli/cli#13388, still # open) and suggests --auto or --admin; --admin only suppresses that diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 70838e2..25286cc 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -12,7 +12,7 @@ name: Bench on: - # No inputs: tetherkit_bench never reads argv (benchmarks/ has no argv or + # No inputs: tethernet_bench never reads argv (benchmarks/ has no argv or # argc use), so the old args input was dead plumbing. workflow_dispatch: @@ -32,22 +32,22 @@ jobs: run: brew install libusb # Sanitizers crush throughput; benchmarks must build Release with none of - # the TETHERKIT_ENABLE_*SAN options (the cmake/Sanitizers.cmake contract; + # the TETHERNET_ENABLE_*SAN options (the cmake/Sanitizers.cmake contract; # all default off, so not passing them is already correct). - name: Configure & Build run: | cmake -S . -B build-rel -DCMAKE_BUILD_TYPE=Release - cmake --build build-rel --target tetherkit_bench -j"$(sysctl -n hw.ncpu)" + cmake --build build-rel --target tethernet_bench -j"$(sysctl -n hw.ncpu)" - name: Run # pipefail: keep tee from swallowing the benchmark's exit code. run: | set -o pipefail - ./build-rel/bin/tetherkit_bench | tee bench-report.md + ./build-rel/bin/tethernet_bench | tee bench-report.md - name: Upload the bench report uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: tetherkit-bench-${{ github.run_number }} + name: tethernet-bench-${{ github.run_number }} path: bench-report.md retention-days: 14 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73c1650..e5e73e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ # Build: a manually triggered full build with artifact upload. # -# Dispatch-only full build: C++ (warnings as errors) + the libtetherkit shared +# Dispatch-only full build: C++ (warnings as errors) + the libtethernet shared # library + GUI packaging. Artifacts are uploaded for each run. name: Build @@ -81,54 +81,54 @@ jobs: run: | cmake -S . -B build \ -DCMAKE_BUILD_TYPE="${DISPATCHED_BUILD_TYPE}" \ - -DTETHERKIT_WARNINGS_AS_ERRORS=ON + -DTETHERNET_WARNINGS_AS_ERRORS=ON - name: Build run: cmake --build build -j"$(sysctl -n hw.ncpu)" - # Basic tetherkit-cli smoke checks; --list only enumerates devices and + # Basic tethernet-cli smoke checks; --list only enumerates devices and # state, no root needed. - name: Smoke test (--list needs no root) run: | - ./build/bin/tetherkit-cli --version - ./build/bin/tetherkit-cli --list + ./build/bin/tethernet-cli --version + ./build/bin/tethernet-cli --list - - name: Assemble TetherKit.app + - name: Assemble Tethernet.app run: cmake --build build --target gui - name: Self-checks run: | - codesign --verify --deep --strict dist/TetherKit.app + codesign --verify --deep --strict dist/Tethernet.app # The non-root path of --install must fail loudly instead of # half-installing. - payload="dist/TetherKit.app/Contents/Library/HelperTools" - if "${payload}/com.tetherkit.helper" --install; then + payload="dist/Tethernet.app/Contents/Library/HelperTools" + if "${payload}/com.tethernet.helper" --install; then echo "::error::helper --install unexpectedly succeeded without root" exit 1 fi # The Finder-alias install mode always exits 0 (result on stdout). - ./dist/TetherKit.app/Contents/MacOS/TetherKit --install-finder-alias + ./dist/Tethernet.app/Contents/MacOS/Tethernet --install-finder-alias - - name: Package TetherKit.app + - name: Package Tethernet.app run: | # ditto preserves the bundle structure and extended attributes; it is # the standard way to package a .app. - name="TetherKit-${GITHUB_RUN_NUMBER}-${DISPATCHED_OS}-${DISPATCHED_BUILD_TYPE}.zip" - ditto -c -k --keepParent dist/TetherKit.app "$name" + name="Tethernet-${GITHUB_RUN_NUMBER}-${DISPATCHED_OS}-${DISPATCHED_BUILD_TYPE}.zip" + ditto -c -k --keepParent dist/Tethernet.app "$name" # upload-artifact v7 supports single-file direct upload (archive: false): # the downloaded artifact IS the ditto zip above, not a zip inside another # zip. With archive: false the name input is ignored (the file name wins). - - name: Upload TetherKit.app artifact + - name: Upload Tethernet.app artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - path: TetherKit-${{ github.run_number }}-${{ env.DISPATCHED_OS }}-${{ env.DISPATCHED_BUILD_TYPE }}.zip + path: Tethernet-${{ github.run_number }}-${{ env.DISPATCHED_OS }}-${{ env.DISPATCHED_BUILD_TYPE }}.zip archive: false retention-days: 14 - - name: Upload libtetherkit shared-library artifact + - name: Upload libtethernet shared-library artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: tetherkit-libs-${{ github.run_number }}-${{ env.DISPATCHED_OS }}-${{ env.DISPATCHED_BUILD_TYPE }} - path: build/lib/libtetherkit*.dylib + name: tethernet-libs-${{ github.run_number }}-${{ env.DISPATCHED_OS }}-${{ env.DISPATCHED_BUILD_TYPE }} + path: build/lib/libtethernet*.dylib retention-days: 14 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b9119fb..b80e8c2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,7 @@ # The review requirement on the bot PR is bypassed BY YOU, the same way it # is for your own PRs: the "Require review" ruleset lists the admin role as # a bypass actor with bypass_mode pull_request, so -# gh api repos/justsrc/TetherKit/pulls//merge -X PUT -f merge_method=merge +# gh api repos/justsrc/Tethernet/pulls//merge -X PUT -f merge_method=merge # engages the bypass. (`gh pr merge` refuses at its own pre-flight when # mergeStateStatus is BLOCKED; that is cli/cli#13388, still open. The REST # endpoint does the right thing.) Do NOT add Dependabot to the bypass list: @@ -109,8 +109,8 @@ jobs: # # So the build decides the analysis set, and this project already # has the switch: doctest is an INTERFACE library linked only into - # tetherkit_tests (tests/CMakeLists.txt), so with - # TETHERKIT_BUILD_TESTS=OFF no test TU is compiled, doctest.h is + # tethernet_tests (tests/CMakeLists.txt), so with + # TETHERNET_BUILD_TESTS=OFF no test TU is compiled, doctest.h is # never parsed, and third_party/doctest never reaches the database. # # That retires cpp/use-after-free at third_party/doctest/doctest.h:3710 @@ -140,7 +140,7 @@ jobs: cmake -S . -B build \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_OSX_ARCHITECTURES=arm64 \ - -DTETHERKIT_BUILD_TESTS=OFF + -DTETHERNET_BUILD_TESTS=OFF cmake --build build -j"$(sysctl -n hw.ncpu)" # The upload is skipped for a commit authored by Dependabot. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4d1113d..c7b37e6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -129,6 +129,6 @@ jobs: uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: always() with: - name: tetherkit-tidy-log + name: tethernet-tidy-log path: tidy.log retention-days: 14 diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 21976c6..9377b08 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -1,7 +1,7 @@ # Pre-release: pushing a pre-release v* tag issues a pre-release # automatically. # -# git tag -a v0.2.0-rc.1 -m "TetherKit 0.2.0 rc 1" && git push origin v0.2.0-rc.1 +# git tag -a v0.2.0-rc.1 -m "Tethernet 0.2.0 rc 1" && git push origin v0.2.0-rc.1 # # Cut rc tags only from current main: tag-filtered workflows are resolved # from the TAGGED commit, not from main, so an rc tag on a commit older @@ -52,7 +52,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Same check as release.yml: the tag and CMakeLists' VERSION must - # agree, otherwise `tetherkit-cli --version` will not match the + # agree, otherwise `tethernet-cli --version` will not match the # release. A pre-release tag carries a suffix (v0.2.0-rc.1) that # CMake's project() VERSION cannot hold (components are numeric # only), so the comparison uses the base version: everything before @@ -77,7 +77,7 @@ jobs: run: | cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ - -DTETHERKIT_WARNINGS_AS_ERRORS=ON + -DTETHERNET_WARNINGS_AS_ERRORS=ON cmake --build build -j"$(sysctl -n hw.ncpu)" ctest --test-dir build --output-on-failure @@ -87,20 +87,21 @@ jobs: - name: Package the command-line binary id: pkg run: | - name="tetherkit-cli-${GITHUB_REF_NAME}-macos-arm64.tar.gz" - tar -C build/bin -czf "$name" tetherkit-cli + name="tethernet-cli-${GITHUB_REF_NAME}-macos-arm64.tar.gz" + cp THIRD_PARTY_NOTICES.md build/bin/ + tar -C build/bin -czf "$name" tethernet-cli THIRD_PARTY_NOTICES.md { echo "name=${name}" echo "sha256=$(shasum -a 256 "$name" | awk '{print $1}')" } >> "$GITHUB_OUTPUT" - - name: Package TetherKit.app + - name: Package Tethernet.app id: app run: | - name="TetherKit-${GITHUB_REF_NAME}-macos-arm64.zip" + name="Tethernet-${GITHUB_REF_NAME}-macos-arm64.zip" # ditto preserves the bundle structure and extended attributes; it is # the standard way to package a .app. - ditto -c -k --keepParent dist/TetherKit.app "$name" + ditto -c -k --keepParent dist/Tethernet.app "$name" { echo "name=${name}" echo "sha256=$(shasum -a 256 "$name" | awk '{print $1}')" @@ -160,15 +161,15 @@ jobs: built artifacts do not carry the flag): ```bash - xattr -d com.apple.quarantine tetherkit-cli # CLI - xattr -dr com.apple.quarantine TetherKit.app # GUI (recursive) + xattr -d com.apple.quarantine tethernet-cli # CLI + xattr -dr com.apple.quarantine Tethernet.app # GUI (recursive) ``` ## Command line ```bash - tetherkit-cli --list # list recognized devices, no root needed - sudo tetherkit-cli # start the driver, needs root + tethernet-cli --list # list recognized devices, no root needed + sudo tethernet-cli # start the driver, needs root ``` ## Requirements @@ -200,7 +201,7 @@ jobs: APP_NAME: ${{ steps.app.outputs.name }} run: | gh release create "${GITHUB_REF_NAME}" \ - --title "TetherKit ${GITHUB_REF_NAME}" \ + --title "Tethernet ${GITHUB_REF_NAME}" \ --prerelease \ --notes-file notes.md \ --generate-notes \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b5ec06..42bb122 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ # Release: pushing a stable v* tag issues a release automatically. # -# git tag -a v0.2.0 -m "TetherKit 0.2.0" && git push origin v0.2.0 +# git tag -a v0.2.0 -m "Tethernet 0.2.0" && git push origin v0.2.0 # # Pre-release tags (a hyphen marks the semver pre-release part, e.g. # v0.2.0-rc.1) are excluded below and handled by pre-release.yml, which @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # The tag and CMakeLists' VERSION must agree, otherwise - # `tetherkit-cli --version` will not match the version on the Release / + # `tethernet-cli --version` will not match the version on the Release / # Homebrew. Catch that before the release. - name: Verify the tag matches the CMakeLists VERSION run: | @@ -56,7 +56,7 @@ jobs: run: | cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ - -DTETHERKIT_WARNINGS_AS_ERRORS=ON + -DTETHERNET_WARNINGS_AS_ERRORS=ON cmake --build build -j"$(sysctl -n hw.ncpu)" ctest --test-dir build --output-on-failure @@ -66,20 +66,21 @@ jobs: - name: Package the command-line binary id: pkg run: | - name="tetherkit-cli-${GITHUB_REF_NAME}-macos-arm64.tar.gz" - tar -C build/bin -czf "$name" tetherkit-cli + name="tethernet-cli-${GITHUB_REF_NAME}-macos-arm64.tar.gz" + cp THIRD_PARTY_NOTICES.md build/bin/ + tar -C build/bin -czf "$name" tethernet-cli THIRD_PARTY_NOTICES.md { echo "name=${name}" echo "sha256=$(shasum -a 256 "$name" | awk '{print $1}')" } >> "$GITHUB_OUTPUT" - - name: Package TetherKit.app + - name: Package Tethernet.app id: app run: | - name="TetherKit-${GITHUB_REF_NAME}-macos-arm64.zip" + name="Tethernet-${GITHUB_REF_NAME}-macos-arm64.zip" # ditto preserves the bundle structure and extended attributes; it is # the standard way to package a .app. - ditto -c -k --keepParent dist/TetherKit.app "$name" + ditto -c -k --keepParent dist/Tethernet.app "$name" { echo "name=${name}" echo "sha256=$(shasum -a 256 "$name" | awk '{print $1}')" @@ -120,26 +121,20 @@ jobs: Homebrew is recommended (built from source, no Gatekeeper prompt): ```bash - brew install justsrc/tap/tetherkit # GUI, TetherKit.app - brew install justsrc/tap/tetherkit-cli # command-line tool + brew install justsrc/tap/tethernet # GUI, Tethernet.app + brew install justsrc/tap/tethernet-cli # command-line tool ``` - After installing the GUI, `open "$(brew --prefix)/opt/tetherkit/TetherKit.app"`. + After installing the GUI, `open "$(brew --prefix)/opt/tethernet/Tethernet.app"`. On first run, click "Install privileged components" and enter the admin password once; the first launch also creates the Finder alias - automatically, after which TetherKit turns up in Spotlight. - - > Note for users of older versions: starting with this release the - > formula name `tetherkit` belongs to the GUI, and the CLI is renamed - > `tetherkit-cli` (the binary carries that name too). If you - > previously installed the CLI, run - > `brew uninstall tetherkit && brew install tetherkit-cli`. + automatically, after which Tethernet turns up in Spotlight. ## Command line ```bash - tetherkit-cli --list # list recognized devices, no root needed - sudo tetherkit-cli # start the driver, needs root + tethernet-cli --list # list recognized devices, no root needed + sudo tethernet-cli # start the driver, needs root ``` ## Requirements @@ -154,8 +149,8 @@ jobs: browser download. Remove the quarantine manually to run them: ```bash - xattr -d com.apple.quarantine tetherkit-cli # CLI - xattr -dr com.apple.quarantine TetherKit.app # GUI (recursive) + xattr -d com.apple.quarantine tethernet-cli # CLI + xattr -dr com.apple.quarantine Tethernet.app # GUI (recursive) ``` If that bothers you, use the Homebrew route above or build from @@ -179,7 +174,7 @@ jobs: APP_NAME: ${{ steps.app.outputs.name }} run: | gh release create "${GITHUB_REF_NAME}" \ - --title "TetherKit ${GITHUB_REF_NAME}" \ + --title "Tethernet ${GITHUB_REF_NAME}" \ --notes-file notes.md \ --generate-notes \ --verify-tag \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3b828b..58caefa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -114,7 +114,7 @@ jobs: run: | cmake -S . -B build \ -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ - -DTETHERKIT_WARNINGS_AS_ERRORS=ON + -DTETHERNET_WARNINGS_AS_ERRORS=ON - name: Build run: cmake --build build -j"$(sysctl -n hw.ncpu)" @@ -124,8 +124,8 @@ jobs: - name: Smoke test (--list needs no root) run: | - ./build/bin/tetherkit-cli --version - ./build/bin/tetherkit-cli --list + ./build/bin/tethernet-cli --version + ./build/bin/tethernet-cli --list # ---------------------------------------------------------------------------- # Verify the macOS 13.3 floor the README claims. @@ -219,30 +219,30 @@ jobs: - name: Install libusb run: brew install libusb - - name: Build C++ (the GUI links libtetherkit) + - name: Build C++ (the GUI links libtethernet) run: | cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DTETHERKIT_BUILD_TESTS=OFF -DTETHERKIT_BUILD_BENCHMARKS=OFF + -DTETHERNET_BUILD_TESTS=OFF -DTETHERNET_BUILD_BENCHMARKS=OFF cmake --build build -j"$(sysctl -n hw.ncpu)" - name: Swift unit tests - run: TETHERKIT_LIB_DIR="$PWD/build/lib" swift test --package-path gui + run: TETHERNET_LIB_DIR="$PWD/build/lib" swift test --package-path gui - - name: Assemble TetherKit.app + - name: Assemble Tethernet.app run: cmake --build build --target gui - name: Self-checks run: | - codesign --verify --deep --strict dist/TetherKit.app + codesign --verify --deep --strict dist/Tethernet.app # The non-root path of --install must fail loudly instead of # half-installing. - payload="dist/TetherKit.app/Contents/Library/HelperTools" - if "${payload}/com.tetherkit.helper" --install; then + payload="dist/Tethernet.app/Contents/Library/HelperTools" + if "${payload}/com.tethernet.helper" --install; then echo "::error::helper --install unexpectedly succeeded without root" exit 1 fi # The Finder-alias install mode always exits 0 (result on stdout). - ./dist/TetherKit.app/Contents/MacOS/TetherKit --install-finder-alias + ./dist/Tethernet.app/Contents/MacOS/Tethernet --install-finder-alias # ---------------------------------------------------------------------------- # ThreadSanitizer: the correctness backstop for the lock-free queue and the @@ -260,7 +260,7 @@ jobs: - name: Configure & Build run: | - cmake -S . -B build-tsan -DTETHERKIT_ENABLE_TSAN=ON + cmake -S . -B build-tsan -DTETHERNET_ENABLE_TSAN=ON cmake --build build-tsan -j"$(sysctl -n hw.ncpu)" - name: Test @@ -293,8 +293,8 @@ jobs: - name: Configure & Build run: | cmake -S . -B build-asan \ - -DTETHERKIT_ENABLE_ASAN=ON \ - -DTETHERKIT_ENABLE_UBSAN=ON + -DTETHERNET_ENABLE_ASAN=ON \ + -DTETHERNET_ENABLE_UBSAN=ON cmake --build build-asan -j"$(sysctl -n hw.ncpu)" - name: Disable feth hardware checksum offload @@ -305,7 +305,7 @@ jobs: - name: Test (suite as the runner user) # The suite runs unprivileged on purpose: its capi permission cases - # assert the non-root contract (TK_ERR_PERMISSION without root). As + # assert the non-root contract (TN_ERR_PERMISSION without root). As # root those gates legitimately pass, so the same CHECKs that used to # verify -4 now see 0 or -3 and fail. Feth/BPF root cases skip # quietly here and get their own root step below. @@ -324,14 +324,14 @@ jobs: # The grep strings match text printed by tests/test_net_link.cc # (ReportSkip and the pair-creation MESSAGE); keep them in sync. set -o pipefail - sudo env TETHERKIT_ROOT_TESTS=1 \ + sudo env TETHERNET_ROOT_TESTS=1 \ UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ ctest --test-dir build-asan -V -R 'net\.' 2>&1 | tee abi-asan.log # Root cases SKIP (not fail) when preconditions are unmet, and # skipped still shows green. The name filter would hide an # unexpected skip from the exit code; these guards make it loud. - if grep -q "requires root and TETHERKIT_ROOT_TESTS" abi-asan.log; then + if grep -q "requires root and TETHERNET_ROOT_TESTS" abi-asan.log; then echo "::error::feth/BPF cases were skipped under the sanitizer build" exit 1 fi @@ -394,13 +394,13 @@ jobs: # so this pipeline's status was tee's (always 0); a test failing # after the pair-creation MESSAGE would still look green. set -o pipefail - sudo env TETHERKIT_ROOT_TESTS=1 \ + sudo env TETHERNET_ROOT_TESTS=1 \ ctest --test-dir build -V -R 'net\.' 2>&1 | tee abi.log # Critical: root cases SKIP (not fail) when preconditions are unmet, # and skipped still shows green. Without this guard the job could sit # permanently green while having checked nothing. - if grep -q "requires root and TETHERKIT_ROOT_TESTS" abi.log; then + if grep -q "requires root and TETHERNET_ROOT_TESTS" abi.log; then echo "::error::feth/BPF cases were skipped; this ABI check did not actually run" exit 1 fi diff --git a/.github/workflows/update-tap.yml b/.github/workflows/update-tap.yml index 6f814f9..8dba291 100644 --- a/.github/workflows/update-tap.yml +++ b/.github/workflows/update-tap.yml @@ -105,11 +105,11 @@ jobs: run: | # A missing formula must stop everything before any rewrite starts, # with a clear message, instead of failing perl mid-loop. - for f in Formula/tetherkit.rb Formula/tetherkit-cli.rb; do + for f in Formula/tethernet.rb Formula/tethernet-cli.rb; do test -f "$f" || { echo "::error::$f is missing in justsrc/homebrew-tap"; exit 1; } done - - name: Rewrite the formulas (tetherkit and tetherkit-cli) + - name: Rewrite the formulas (tethernet and tethernet-cli) working-directory: tap env: FORMULA_URL: ${{ steps.src.outputs.url }} @@ -119,7 +119,7 @@ jobs: # rewritten together. # perl instead of sed -i: BSD and GNU sed disagree about -i # semantics, perl behaves the same on both. - for f in Formula/tetherkit.rb Formula/tetherkit-cli.rb; do + for f in Formula/tethernet.rb Formula/tethernet-cli.rb; do perl -pi -e 's|^ url ".*"| url "$ENV{FORMULA_URL}"|' "$f" perl -pi -e 's|^ sha256 ".*"| sha256 "$ENV{FORMULA_SHA256}"|' "$f" @@ -144,20 +144,20 @@ jobs: mkdir -p "$(dirname "$tap_dir")" ln -s "${GITHUB_WORKSPACE}/tap" "$tap_dir" - brew audit --strict --online justsrc/tap/tetherkit-cli - brew audit --strict --online justsrc/tap/tetherkit + brew audit --strict --online justsrc/tap/tethernet-cli + brew audit --strict --online justsrc/tap/tethernet - brew install --build-from-source justsrc/tap/tetherkit-cli - test "$(tetherkit-cli --version | head -1)" \ - = "TetherKit ${TAP_VERSION} (C++23, macOS 13.3+)" + brew install --build-from-source justsrc/tap/tethernet-cli + test "$(tethernet-cli --version | head -1)" \ + = "Tethernet ${TAP_VERSION} (C++23, macOS 13.3+)" - brew install --build-from-source justsrc/tap/tetherkit - app="$(brew --prefix)/opt/tetherkit/TetherKit.app" + brew install --build-from-source justsrc/tap/tethernet + app="$(brew --prefix)/opt/tethernet/Tethernet.app" test -d "$app" codesign --verify --deep --strict "$app" - "$app/Contents/MacOS/TetherKit" --install-finder-alias + "$app/Contents/MacOS/Tethernet" --install-finder-alias - brew uninstall tetherkit tetherkit-cli + brew uninstall tethernet tethernet-cli # A branch and a pull request, never a push to main. The tap is what # users install from, so its contents get a human read before they @@ -168,7 +168,7 @@ jobs: env: TAP_TAG: ${{ inputs.tag }} TAP_VERSION: ${{ steps.src.outputs.version }} - TAP_BRANCH: bump-tetherkit-${{ inputs.tag }} + TAP_BRANCH: bump-tethernet-${{ inputs.tag }} # The credential helper reads the token from the environment when # git asks for it, so it is passed as GH_TOKEN and never as part of # a command line. This workflow's own GITHUB_TOKEN cannot write to @@ -195,7 +195,7 @@ jobs: # The branch name carries the tag, so a re-run for the same version # lands on the same branch instead of stacking a second one. git switch -c "${TAP_BRANCH}" - git commit -am "tetherkit + tetherkit-cli ${TAP_VERSION}" + git commit -am "tethernet + tethernet-cli ${TAP_VERSION}" # Authenticate the push with a credential helper rather than by # putting the token in the remote URL. A credential in the URL is # readable from .git/config, shows up in the process argument list, @@ -227,7 +227,7 @@ jobs: env: TAP_TAG: ${{ inputs.tag }} TAP_VERSION: ${{ steps.src.outputs.version }} - TAP_BRANCH: bump-tetherkit-${{ inputs.tag }} + TAP_BRANCH: bump-tethernet-${{ inputs.tag }} GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} run: | # env passage, not direct expansion: a secret expression must never @@ -262,8 +262,8 @@ jobs: gh pr create --repo justsrc/homebrew-tap \ --base main \ --head "${TAP_BRANCH}" \ - --title "tetherkit + tetherkit-cli ${TAP_VERSION}" \ - --body "Automated tap bump for TetherKit ${TAP_TAG}. + --title "tethernet + tethernet-cli ${TAP_VERSION}" \ + --body "Automated tap bump for Tethernet ${TAP_TAG}. The formulas point at the ${TAP_TAG} source tarball; the sha256 was recomputed from that tarball and both formulas were installed from @@ -272,7 +272,7 @@ jobs: on the app). Checklist after merging: dispatch the tap's bottle.yml - (workflow_dispatch, formula: tetherkit, then tetherkit-cli) so the + (workflow_dispatch, formula: tethernet, then tethernet-cli) so the new version's bottles are built, pushed to GHCR, and the formulas' bottle blocks are updated. Until that runs, brew install for the new version fails with a 404 on the GHCR manifest -- use diff --git a/.gitignore b/.gitignore index a4017c7..08132db 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,7 @@ _deps/ # ---- GUI packaging artifacts ---- # -# dist/ is produced by gui/Scripts/build-gui.sh (TetherKit.app + helper). +# dist/ is produced by gui/Scripts/build-gui.sh (Tethernet.app + helper). # It is a pure derivative and does not belong in version control. dist/ diff --git a/AGENTS.md b/AGENTS.md index dc56468..8e7254c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# AGENTS.md -- TetherKit implementation notes +# AGENTS.md -- Tethernet implementation notes > This file is **working memory** for AI agents (and for any human taking over). > Update the relevant section after every commit. Read this file before starting any work to avoid stepping on the same pitfalls twice. @@ -86,7 +86,7 @@ Measurement machine: macOS 26.5.1 (Darwin 25.5.0), Apple Silicon arm64, Apple cl ### 2.5 Development-environment constraints (affect the test strategy) - **No root**: the current session runs as uid 501 and cannot create feth or open `/dev/bpf*`. - -> Tests that need root must be **optional and skippable**, controlled in CI and locally by a switch such as `TETHERKIT_ROOT_TESTS=1`. + -> Tests that need root must be **optional and skippable**, controlled in CI and locally by a switch such as `TETHERNET_ROOT_TESTS=1`. - **No guarantee of a USB device**: at project start the development machine had none, and a real device was only connected later for verification. -> The USB backend must be abstracted behind an interface with an in-memory loopback mock; end-to-end tests and throughput benchmarks run on the mock. Automated tests **must not depend on a device being present**. @@ -97,7 +97,7 @@ Measurement machine: macOS 26.5.1 (Darwin 25.5.0), Apple Silicon arm64, Apple cl ## 3. Directory structure ``` -TetherKit/ +Tethernet/ ├── AGENTS.md This file: agent working memory ├── README.md User-facing documentation (English, the default shown on GitHub) ├── CMakeLists.txt Top-level build script @@ -108,7 +108,7 @@ TetherKit/ │ ├── CompilerWarnings.cmake Warning options as an INTERFACE target │ ├── Sanitizers.cmake ASan/UBSan/TSan switches │ └── Optimizations.cmake Data-path optimization options and trade-off notes -├── include/tetherkit/ Public headers (subdirectories by module) +├── include/tethernet/ Public headers (subdirectories by module) │ ├── common/messages.def **All user-facing strings** (X-macro, single English column) │ └── capi/ C ABI -- the only extern "C" boundary in the project ├── src/ Implementation @@ -116,12 +116,12 @@ TetherKit/ │ ├── capi/ C ABI implementation (session, interface config, logging, orphan cleanup) │ └── app/ Command-line entry point ├── gui/ SwiftUI graphical interface (SwiftPM project, see docs/GUI-ARCHITECTURE.md) -│ ├── Sources/CTetherKit/ Module map for the C ABI (headers are symlinks) -│ ├── Sources/TetherKitIPC/ Shared by App and helper: XPC protocol, models, authorization -│ ├── Sources/TetherKitUIModel/ Sidebar page model (separate target, @testable import in tests) -│ ├── Sources/TetherKitCore/ Swift wrapper around the C ABI -│ ├── Sources/TetherKitHelper/ Privileged helper (root, launched by launchd) -│ ├── Sources/TetherKitApp/ SwiftUI interface +│ ├── Sources/CTethernet/ Module map for the C ABI (headers are symlinks) +│ ├── Sources/TethernetIPC/ Shared by App and helper: XPC protocol, models, authorization +│ ├── Sources/TethernetUIModel/ Sidebar page model (separate target, @testable import in tests) +│ ├── Sources/TethernetCore/ Swift wrapper around the C ABI +│ ├── Sources/TethernetHelper/ Privileged helper (root, launched by launchd) +│ ├── Sources/TethernetApp/ SwiftUI interface │ ├── Resources/ Info.plist, LaunchDaemon plist │ └── Scripts/ Build / install / uninstall scripts ├── tests/ doctest unit tests (single binary + test-suite filtering) @@ -133,19 +133,19 @@ TetherKit/ Dependencies are **strictly one-way**; reverse or cyclic dependencies are forbidden: ``` -tk_common <- (no dependencies) +tn_common <- (no dependencies) ^ -tk_rndis / tk_net / tk_usb <- tk_common +tn_rndis / tn_net / tn_usb <- tn_common ^ -tk_core <- common + rndis + net + usb +tn_core <- common + rndis + net + usb ^ -tetherkit (executable) <- core -tk_capi (C ABI) <- core -> packaged as the shared library libtetherkit +tethernet (executable) <- core +tn_capi (C ABI) <- core -> packaged as the shared library libtethernet ^ -gui/ (Swift) <- libtetherkit +gui/ (Swift) <- libtethernet ``` -The Swift side can only see `libtetherkit`, never any C++ symbol (the export symbol allowlist is pinned to `_tk_*`). The C++ layer's interfaces use `std::expected` / `std::span` / abstract base classes, which Swift's C++ interop cannot consume, so the C ABI layer is not optional. +The Swift side can only see `libtethernet`, never any C++ symbol (the export symbol allowlist is pinned to `_tn_*`). The C++ layer's interfaces use `std::expected` / `std::span` / abstract base classes, which Swift's C++ interop cannot consume, so the C ABI layer is not optional. --- @@ -153,7 +153,7 @@ The Swift side can only see `libtetherkit`, never any C++ symbol (the export sym | Category | Convention | Example | |---|---|---| -| Namespace | `lower_case` | `tetherkit`, `tetherkit::rndis` | +| Namespace | `lower_case` | `tethernet`, `tethernet::rndis` | | Types (class/struct/enum/alias) | `CamelCase` | `RndisStateMachine`, `BpfLink` | | Functions and methods | `CamelCase` | `SendEncapsulatedCommand()` | | Local variables and parameters | `lower_case` | `frame_len`, `max_transfer_size` | @@ -174,9 +174,9 @@ Error handling by layer: | | C++ (library + CLI) | Swift (GUI + helper) | |---|---|---| -| Message table | `include/tetherkit/common/messages.def` (X-macro, expanded into an enum + one table) | `gui/Sources/TetherKitIPC/LocalizedStrings.swift` (exhaustive switch) | +| Message table | `include/tethernet/common/messages.def` (X-macro, expanded into an enum + one table) | `gui/Sources/TethernetIPC/LocalizedStrings.swift` (exhaustive switch) | | Look up a string | `Tr(Msg::kFoo, args...)`, arguments match `std::format` | `L(.foo, args...)`, `String(format:)` printf style | -| Log | The `TETHERKIT_INFO_TR(Msg::kFoo, ...)` family of macros | -- | +| Log | The `TETHERNET_INFO_TR(Msg::kFoo, ...)` family of macros | -- | | No arguments | `Text(Msg::kFoo)` returns `string_view`, no allocation | `L10n.text(.foo)` | | Missing a language | Table-length assertion + `common.i18n` case | **Fails to compile** (exhaustive switch) | | Placeholder mismatch | `common.i18n` checks indices and types per entry | `LocalizationTests` checks position and type per entry | @@ -189,12 +189,12 @@ Three hard rules: Where the language comes from: -- CLI: `--lang en|auto`; when omitted, inferred in order from `TETHERKIT_LANG` -> `LC_ALL` -> `LC_MESSAGES` -> `LANG`. **The language is fixed before arguments are parsed**, so help text and argument errors themselves are in the target language. Warning: sudo does not necessarily pass these variables through; in that case pass `--lang` explicitly. Any other value is rejected with "unknown language ...; English is the only language shipped today". -- GUI: the language switch in the App menu and the menu bar panel (Follow system / English), stored in the `TetherKitLanguagePreference` UserDefaults key. +- CLI: `--lang en|auto`; when omitted, inferred in order from `TETHERNET_LANG` -> `LC_ALL` -> `LC_MESSAGES` -> `LANG`. **The language is fixed before arguments are parsed**, so help text and argument errors themselves are in the target language. Warning: sudo does not necessarily pass these variables through; in that case pass `--lang` explicitly. Any other value is rejected with "unknown language ...; English is the only language shipped today". +- GUI: the language switch in the App menu and the menu bar panel (Follow system / English), stored in the `TethernetLanguagePreference` UserDefaults key. -**All three places must be updated together when the language changes; missing any one leaves "the UI in English, the logs in Chinese"**: the Swift string table (`L10n.apply`), libtetherkit (`tk_set_language`), and the helper (XPC `setLanguage` -- it runs as root under launchd and cannot see the user preference). All three are done together in `AppModel.applyLanguage`; do not call any one of them alone elsewhere. +**All three places must be updated together when the language changes; missing any one leaves "the UI in English, the logs in Chinese"**: the Swift string table (`L10n.apply`), libtethernet (`tn_set_language`), and the helper (XPC `setLanguage` -- it runs as root under launchd and cannot see the user preference). All three are done together in `AppModel.applyLanguage`; do not call any one of them alone elsewhere. -The string table currently has one column (English). The CLI accepts `--lang en` (and `auto`, which also resolves to English) and rejects every other value. The GUI menu offers "Follow system" and "English". The multi-language plumbing (the `Language` enum, `TextIn`, `ParseLanguage`, the C ABI `tk_set_language`, the helper's `setLanguage` XPC method, `L10n.apply`) is deliberately kept so that adding a language later stays a bounded five-touch change; the checklist lives at the top of `include/tetherkit/common/messages.def`. +The string table currently has one column (English). The CLI accepts `--lang en` (and `auto`, which also resolves to English) and rejects every other value. The GUI menu offers "Follow system" and "English". The multi-language plumbing (the `Language` enum, `TextIn`, `ParseLanguage`, the C ABI `tn_set_language`, the helper's `setLanguage` XPC method, `L10n.apply`) is deliberately kept so that adding a language later stays a bounded five-touch change; the checklist lives at the top of `include/tethernet/common/messages.def`. --- @@ -219,7 +219,7 @@ Legend: `Done` `In progress` `Not started` | 13 | `refactor(core): Runtime changed to a self-owned control thread` | Done | Non-blocking Start, consistent snapshots, event drain | | 14 | `feat(capi): session lifecycle, status snapshots, and event polling` | Done | Enum alignment pinned down with static_assert | | 15 | `feat(capi): interface network-access configuration and orphan interface cleanup` | Done | DHCP / static IP; persistent feth registration | -| 16 | `build: output shared library libtetherkit` | Done | tk_capi changed to an OBJECT library; export symbol allowlist | +| 16 | `build: output shared library libtethernet` | Done | tn_capi changed to an OBJECT library; export symbol allowlist | | 17 | `feat(gui): SwiftPM project skeleton, C interop, and XPC protocol` | Done | Headers use symlinks so they never fall out of sync | | 18 | `feat(gui): privileged helper and authorization credential recheck` | Done | LaunchDaemon + AuthorizationRef | | 19 | `feat(gui): SwiftUI design system and main interface` | Done | Status card/device/throughput/logs | @@ -229,11 +229,11 @@ Legend: `Done` `In progress` `Not started` | 23 | `feat(gui): one-click install and uninstall of the privileged helper inside the App` | Done | AEWP + helper binary setuid(0); payload embedded in .app, dist/helper removed; the "bash drops privileges when euid != ruid" pitfall recorded in SPIKE | | 24 | `feat(gui): app icon` | Done | Flood-fill transparency from the outer white background by edge connectivity (inner white elements preserved); icns at all sizes; verified in the Dock on real hardware | | 25 | `feat(gui): check for updates (check only, no replacement)` | Done | GitHub releases/latest + semantic comparison; daily silent + manual from menu; automatic replacement is blocked by Gatekeeper for unsigned distribution, so check-only | -| 26 | `feat!: rename CLI to tetherkit-cli; App automatically maintains a Finder alias` | Done | OUTPUT_NAME changes the product name (the `tetherkit` formula name ceded to the GUI); alias uses the bookmark API (symlinks are not recognized by Spotlight), created automatically on first launch -- **brew's postinstall runs in a sandbox and cannot write to /Applications (measured on real hardware)**; local Spotlight index was damaged (mdutil unknown state), indexing effect to be rechecked after the index is rebuilt | +| 26 | `feat!: rename CLI to tethernet-cli; App automatically maintains a Finder alias` | Done | OUTPUT_NAME changes the product name (the `tethernet` formula name ceded to the GUI); alias uses the bookmark API (symlinks are not recognized by Spotlight), created automatically on first launch -- **brew's postinstall runs in a sandbox and cannot write to /Applications (measured on real hardware)**; local Spotlight index was damaged (mdutil unknown state), indexing effect to be rechecked after the index is rebuilt | | 27 | `build(ci): add GUI build to CI; attach .app to releases; keep tap formulas in sync` | Done | GUI job (macos-14/26); build-gui.sh supports --swift-build-flags=--disable-sandbox (SwiftPM sandbox cannot nest inside brew's sandbox) | | 28 | `chore(release): v0.1.2 -- README icon/screenshots/dual-formula install instructions` | Done | At that point the project shipped Chinese and English; docs/assets | | 29 | `chore(release): v0.1.3 -- device name no longer lost due to session occupancy` | Done | capi string-memory backfill + helper occupancy check fix (pitfall 17 in section 7) | -| 30 | `feat(i18n): externalize all user-facing strings, Chinese and English` | Done | ~390 C++ strings + 220 Swift strings at that point; CLI `--lang`, C ABI `tk_set_language`, GUI language menu; XPC protocol revision 2 -> 3 | +| 30 | `feat(i18n): externalize all user-facing strings, Chinese and English` | Done | ~390 C++ strings + 220 Swift strings at that point; CLI `--lang`, C ABI `tn_set_language`, GUI language menu; XPC protocol revision 2 -> 3 | | 31 | `chore(release): v0.1.4 -- Chinese and English` | Done | At that point the project shipped Chinese and English; the English README became the default (HoRNDIS search-intent SEO); **after upgrading, users had to update the privileged helper once inside the App** (protocol revision changed) | | 32 | `feat(gui): show an update entry when the privileged helper version does not match the App` | Done | Compares the **library** version, not Info.plist (`swift run` has no bundle); different build configurations do not count as a mismatch; when the protocol revision did not change there was previously no prompt at all | | 33 | `chore(release): v0.1.5 -- one-click update when component versions do not match` | Done | Protocol revision still 3, the old helper continues to work; after upgrading the management row prompts for an update, but not updating does not break anything | @@ -241,41 +241,41 @@ Legend: `Done` `In progress` `Not started` | 35 | `build(ci): split CI into tests, build, and lint workflows` | Done | ci.yml's five jobs moved into tests.yml with their steps and checks unchanged, triggers narrowed to PR + push main + manual (tag pushes covered by the self-sufficient release.yml; the swift-app branch is retired); new build.yml (workflow_dispatch only, runner-image and build-type inputs, .app and shared-library artifacts) and lint.yml (clang-format as a hard gate; clang-tidy advisory, since WarningsAsErrors: '' in .clang-tidy is an existing choice, promote it once the backlog is zero); all actions pinned to the latest major, checkout@v7 / upload-artifact@v7, with the .app uploaded via v7 single-file direct upload so the ditto zip arrives as itself | | 36 | `build(ci): add weekly regression, ASan/UBSan, CodeQL, benchmarks, and hardening` | Done | tests.yml gains a Monday cron (catches runner-image / new Xcode / latest libusb / xnu ABI drift before the next PR) and an ASan+UBSan job (splits the work with TSan: memory and UB vs concurrency; Sanitizers.cmake only forbids ASan+TSan together); new codeql.yml (github/codeql-action@v4, manual C++ build analysis, push main + cron, a hard complement to advisory lint) and bench.yml (dispatch-only benchmarks, Release without sanitizers, Markdown artifact, hosted-runner noise caveat); dependabot.yml bumps actions weekly in one grouped PR (backs the latest-major convention); tests/lint/build get permissions: contents: read; Xcode selection filters out beta versions | | 37 | `build(ci): translate release and tap workflows to English` | Done | release.yml and update-tap.yml translated wholesale (comments, step names, error echoes, and the user-facing Release notes heredoc); release.yml's checkout pin bumped v5 -> v7 per the latest-major convention; the two runtime strings the abi-check greps depend on (ReportSkip's root-skip message and the pair-creation MESSAGE in tests/test_net_link.cc) are now English literals, and tests.yml greps the matching strings byte-exactly (MESSAGE prints on success verified empirically against the vendored doctest 2.4.12) | -| 38 | `chore: point the tap, README links, and update checks at justsrc` | Done | update-tap.yml (clone url, local tap path, audit/install taps) and release.yml's Release-notes brew lines repointed to justsrc/homebrew-tap; README.md and README.zh-CN.md install lines and tap link follow at that point; UpdateChecker's releases/latest endpoint and the READMEs' Releases links switched to fork releases per user decision, so the fork becomes the update source (pre-first-release the API 404s and the checker already surfaces that as Failure.noReleases); user-side steps outstanding at that time: create justsrc/homebrew-tap (since done), copy both formulas from the old tap and point their homepage at justsrc/TetherKit (still outstanding), and add the public key as a write-scoped deploy key + HOMEBREW_TAP_DEPLOY_KEY (superseded by row 60's one-token model: the tap now authenticates with the HOMEBREW_TAP_TOKEN repo secret) | +| 38 | `chore: point the tap, README links, and update checks at justsrc` | Done | update-tap.yml (clone url, local tap path, audit/install taps) and release.yml's Release-notes brew lines repointed to justsrc/homebrew-tap; README.md and README.zh-CN.md install lines and tap link follow at that point; UpdateChecker's releases/latest endpoint and the READMEs' Releases links switched to fork releases per user decision, so the fork becomes the update source (pre-first-release the API 404s and the checker already surfaces that as Failure.noReleases); user-side steps outstanding at that time: create justsrc/homebrew-tap (since done), copy both formulas from the old tap and point their homepage at justsrc/Tethernet (still outstanding), and add the public key as a write-scoped deploy key + HOMEBREW_TAP_DEPLOY_KEY (superseded by row 60's one-token model: the tap now authenticates with the HOMEBREW_TAP_TOKEN repo secret) | | 39 | `fix(ci): survive the macOS 14 image retirement and rescope the format gate` | Done | Diagnosed PR #2's first runs (Tests 42487700197, Lint 42487700062, 2026-11-08): all four macos-14 jobs cancelled ~60s in with zero steps and the check-run annotation "The configuration 'macos-14-arm64-us-default' is not supported" -- GitHub retired the macos-14 runner image on 2026-11-02 (runner-images #13518); not fail-fast (matrices set fail-fast: false), not concurrency (no second run existed); Lint's clang-format job ran a repo-wide gate the repo could never satisfy -- the baseline is unformatted under current clang-format (~65 files, 491 violation lines in tests/test_net_link.cc alone). Fixes (commit b6135fd): macos-14 -> macos-15 in the three tests.yml matrices and build.yml's dispatch choice, with honest comment updates (macos-14 runtime-floor coverage ends; the minos-14.0 link flag and the Compile-for-13.3 job carry what they can); clang-format gate rescoped from repo-wide files to changed lines via vendored LLVM clang-format-diff.py (.github/scripts, run `-style file` explicitly because recent clang-format releases, 23.1.5 included, already default to file style and the flag survives a future default change (in sync with lint.yml's corrected comment)), diff base = PR base tip / event.before / HEAD~1 with a conditional base fetch and fetch-depth 2, plus an advisory repo-wide backlog count in the step summary; verified locally: positive probe (injected 200-col literal) exits 1, clean run exits 0, the branch's own hunks pass, and --dry-run without -Werror exits 0 so the summary always lands. A whole-file reformat of test_net_link.cc was tried and reverted: it moved 63 upstream Chinese lines (test titles/comments) into the diff for zero functional gain at the time | | 40 | `feat(ci): add xcode-27 image legs to the Tests matrices` | Done | Xcode 27 is not an /Applications entry on the macos-26 image -- under GitHub's one-major-Xcode-per-image model it ships as the separate label `xcode-27` (macOS 27 base, default toolchain Xcode 27.x; preview 2026-07-16, GA 2026-10-15, runner-images #14404; the macos-26 image tops out at 26.6 and macos-15 at 26.3, verified against both image Readmes); build-test, gui, and abi-check matrices therefore gain `xcode-27` as a third `os` value -- `runs-on: ${{ matrix.os }}` carries the label directly, no select step needed, names render as `xcode-27 · Release` / `GUI · xcode-27`; gui's select-newest step picked 27.x non-beta automatically at the time (the next commit replaces discovery on xcode-27 legs with a deliberate Xcode 27.1 pin: the image also ships Xcode 27.2.0 as a name-clean release-candidate symlink); abi-check gains it on purpose: the macOS 27 base means a new kernel/xnu, the exact surface where the FethPair private ABI could drift (the job's own long-term concern); tsan/asan-ubsan/deployment-target stay on macos-26 defaults with a pointed comment; new legs cost +4 macOS jobs per Tests run (11 -> 15), deliberately riding every PR (10x-billed macOS minutes accepted as the price of review-time drift catching); matrix entries documented with label provenance | -| 41 | `fix(ci): make UBSan fatal and run the feth cases under it` | Done | Round-two review, B3. UBSan was report-and-continue: a diagnostic printed, the process kept going, ctest exited 0 through real undefined behavior. cmake/Sanitizers.cmake now appends -fno-sanitize-recover=undefined when TETHERKIT_ENABLE_UBSAN is set; the variable stays empty otherwise, so ASan-only and TSan builds get no extra flag (verified: a full configure with ASan+UBSan on writes -fno-sanitize-recover=undefined into compile_commands.json and flags.make, and c++ compiles a prober with the flag). The asan-ubsan Test step passes UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 for sanitizer runtime pieces the compiler cannot touch, and runs the feth/BPF cases as root after net.link.fake.hwcsum=0; skipped root cases read green while the BPF buffer walk (bh_hdrlen, BPF_WORDALIGN) and the reinterpret_cast-heavy if_fake bridge never reach the sanitizer | +| 41 | `fix(ci): make UBSan fatal and run the feth cases under it` | Done | Round-two review, B3. UBSan was report-and-continue: a diagnostic printed, the process kept going, ctest exited 0 through real undefined behavior. cmake/Sanitizers.cmake now appends -fno-sanitize-recover=undefined when TETHERNET_ENABLE_UBSAN is set; the variable stays empty otherwise, so ASan-only and TSan builds get no extra flag (verified: a full configure with ASan+UBSan on writes -fno-sanitize-recover=undefined into compile_commands.json and flags.make, and c++ compiles a prober with the flag). The asan-ubsan Test step passes UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 for sanitizer runtime pieces the compiler cannot touch, and runs the feth/BPF cases as root after net.link.fake.hwcsum=0; skipped root cases read green while the BPF buffer walk (bh_hdrlen, BPF_WORDALIGN) and the reinterpret_cast-heavy if_fake bridge never reach the sanitizer | | 42 | `fix(ci): pin Xcode 27.1 on xcode-27 legs; filter RCs elsewhere` | Done | Round-two review, M1. The xcode-27 image ships Xcode_27.2.0 as a release-candidate symlink whose clean name no filter can call out: live run 42491763714 (on e06208a) picked it through a beta-only filter and the leg went green on a pre-release toolchain. build-test and gui now run a Pin step on xcode-27 legs: test -d /Applications/Xcode_27.1.app or error out asking for a deliberate bump, then xcode-select it. macos-15 and macos-26 keep discovery but the filter widens to release candidates (grep -ivE 'beta|release.?candidate'); build.yml's dispatch os choice gains xcode-27 with the same select/pin split. abi-check needs no pin: it never selects an Xcode, so its xcode-27 leg rides the image default | | 43 | `fix(ci): stop tee and diff from masking the exit status` | Done | Round-two review, B2. GitHub runs shell steps as bash -e with no pipefail, so a pipeline's exit code is its last command's. The abi-check forensic job read tee's always-zero status, so a ctest failure after the pair-creation MESSAGE could hide behind passing evidence; the lint format gate would feed empty stdin to clang-format-diff.py when git diff failed and read the checker's 0 as a formatting pass. Both steps set -o pipefail first, with a comment on each site saying why; lint's base-resolution step already fails the job under -e, the flag closes the same hole independently | | 44 | `fix(ci): pass inputs by env; preflight the tap; cap the bench` | Done | Round-two review, B1 plus related minors. Inputs and step outputs never interpolate into run: text anymore: bench_args travels as BENCH_ARGS, build.yml reads its dispatch os/build_type through job env DISPATCHED_OS and DISPATCHED_BUILD_TYPE (the upload with: blocks switch to env context so artifact names stay in lockstep with what the shell builds), and update-tap.yml routes inputs.tag and the src step outputs through TAP_TAG/TAP_VERSION/FORMULA_URL/FORMULA_SHA256 in its four shell steps. update-tap gains a Preflight step verifying both formulas exist in justsrc/homebrew-tap before any perl rewrite starts (a missing formula previously died mid-loop with a perl message); release.yml drops secrets: inherit for the explicit HOMEBREW_TAP_DEPLOY_KEY map, keeping every other caller secret out of the callee's scope; bench.yml gets permissions: contents: read and timeout-minutes: 30 | | 45 | `fix(ci): scope concurrency by event; sync stale comments` | Done | Round-two review minors, plus the ledger corrections. The concurrency group gains github.event_name: a push to main can no longer cancel the in-flight Monday cron, the run that watches runner-image, libusb, and xnu drift. Comments now match what runs: tests.yml's header names four deviations from the ancestor ci.yml (checkout pin, RC filtering, image labels, English abi-check strings) and its trigger rationale says what tag pushes cover and what they skip; the "All 168 cases" claim stopped hard-coding the suite size after the count measured 210; the cron comment documents GitHub's 60-day auto-disable of silent schedules; release.yml's header no longer claims full self-sufficiency and names what a tag run covers and leaves out; AGENTS rows 39 and 40 now reference the landed commits b6135fd and e06208a | | 46 | `fix(ci): block-scalar the asan-ubsan test command` | Done | Found by the fourth-round ASan+UBSan run: exit 127 on the Test step before a single test ran, with the error reading an env: ctest: No such file or directory whose leading space is the tell. The step had been authored as a folded plain scalar (run: with no \|, three physical lines joined by trailing backslashes), and YAML folds plain-scalar newlines into spaces, so the script became one line where each backslash escapes the following space and glues neighboring words into single tokens; ctest reached sudo env as a command named " ctest". Shell-style line continuations are only meaningful inside a block scalar, which every other continuation-bearing step used already, abi-check included. The step now writes the three lines as a run: \| block mirroring abi-check; verified YAML parses and no other workflow carries a plain-scalar continuation (scan vacuous). The build with -fno-sanitize-recover=undefined succeeded in the same run, the failure was purely this step's syntax | -| 47 | `fix(ci): keep permission cases non-root in asan-ubsan` | Done | Fifth round, run 42504799177 (on 3688329): the asan-ubsan job ran the whole suite as root and the capi permission cases failed on their contract assertions (tests/test_capi.cc:402 and 494): tk_session_start returned 0 and tk_net_apply/tk_net_clear returned -3 where the fixtures assert the documented non-root behavior, TK_ERR_PERMISSION (-4); as root those gates legitimately pass, and tk_cleanup_orphan_interfaces outright succeeded. Copilot's review suggestion to make the tests expect the executing user was rejected: the non-root contract is product behavior (it is what the CLI turns into the please-run-as-root hint), and teaching the CHECKs to accept root would make a future regression to these gates read green. The Test step split in two: the suite as the runner user with UBSAN_OPTIONS as step env (permission cases hold, feth/BPF root cases skip quietly), then a filtered sudo run, ctest -V -R 'net\.' 2>&1 with tee and pipefail, carrying abi-check's guard pair verbatim (skip-marker grep and the created-feth confirmation; strings verified against tests/test_net_link.cc's ReportSkip line 83 and the pair-creation MESSAGE line 435), so a silent skip cannot read green and a genuine sanitizer finding in the BPF buffer walk or the private-ABI bridge fails the step loudly | +| 47 | `fix(ci): keep permission cases non-root in asan-ubsan` | Done | Fifth round, run 42504799177 (on 3688329): the asan-ubsan job ran the whole suite as root and the capi permission cases failed on their contract assertions (tests/test_capi.cc:402 and 494): tn_session_start returned 0 and tn_net_apply/tn_net_clear returned -3 where the fixtures assert the documented non-root behavior, TN_ERR_PERMISSION (-4); as root those gates legitimately pass, and tn_cleanup_orphan_interfaces outright succeeded. Copilot's review suggestion to make the tests expect the executing user was rejected: the non-root contract is product behavior (it is what the CLI turns into the please-run-as-root hint), and teaching the CHECKs to accept root would make a future regression to these gates read green. The Test step split in two: the suite as the runner user with UBSAN_OPTIONS as step env (permission cases hold, feth/BPF root cases skip quietly), then a filtered sudo run, ctest -V -R 'net\.' 2>&1 with tee and pipefail, carrying abi-check's guard pair verbatim (skip-marker grep and the created-feth confirmation; strings verified against tests/test_net_link.cc's ReportSkip line 83 and the pair-creation MESSAGE line 435), so a silent skip cannot read green and a genuine sanitizer finding in the BPF buffer walk or the private-ABI bridge fails the step loudly | | 48 | `fix(ci): declare the tap deploy key on the callee` | Done | Third-round review, N1, referee-verified. release.yml passes the reusable update-tap.yml a named secrets map, which declared only inputs.tag, yet its Set-up-the-deploy-key step expands secrets.HOMEBREW_TAP_DEPLOY_KEY: GitHub validates a named map against on.workflow_call.secrets and an unmatched name fails the run at startup with zero jobs and no step logs (annotation on the caller: Invalid secret, HOMEBREW_TAP_DEPLOY_KEY is not defined in the referenced workflow; Stack Overflow 69493352/76799186); the old secrets: inherit skips that validation entirely, which is why the one-way narrowing broke it, and PR CI cannot see this because workflow_dispatch faces no such declaration: the first real tag push is exactly where it dies. workflow_call now declares HOMEBREW_TAP_DEPLOY_KEY: required: true (dispatch declares no secrets; a workflow_call declaration is where the caller's map is checked) | | 49 | `fix(ci): scope update-tap's token; key travels by env` | Done | Third-round residuals for update-tap.yml, the one workflow left without permissions or a timeout: it now sets workflow-level permissions: contents: read (the tap push rides the scoped deploy key, so its GITHUB_TOKEN needs no write access anywhere in this repo) and job-level timeout-minutes: 30, matching bench.yml's cap. The Set-up-the-deploy-key step no longer expands the secret raw inside run: text: it travels as step env DEPLOY_KEY and the printf reads "$DEPLOY_KEY", the same env-passage doctrine the src step's comment already stated | | 50 | `fix(ci): scope lint's concurrency group by event` | Done | Third-round residual: lint.yml's concurrency group was workflow-ref with no event_name, so a push to main and a manual dispatch on the same ref could cancel each other's in-flight lint; tests.yml had gained event_name in f0fbc1d and lint was the holdout. The group now appends github.event_name exactly as tests.yml writes it, with the two-event rationale folded into the concurrency comment | -| 51 | `fix(ci): drop the dead bench args dispatch input` | Done | Third-round residual, addressed rather than left moot: the Run step carried the args dispatch input through an unquoted BENCH_ARGS (intentional word-splitting), but the input itself was dead: benchmarks/ contains no argv or argc use at all (grep-verified before the comment that says so was written), so no dispatch text could ever change the run. workflow_dispatch now declares no inputs, the Run step invokes tetherkit_bench bare behind pipefail+tee, and the inputs.args env-passage comment went with the plumbing it described | +| 51 | `fix(ci): drop the dead bench args dispatch input` | Done | Third-round residual, addressed rather than left moot: the Run step carried the args dispatch input through an unquoted BENCH_ARGS (intentional word-splitting), but the input itself was dead: benchmarks/ contains no argv or argc use at all (grep-verified before the comment that says so was written), so no dispatch text could ever change the run. workflow_dispatch now declares no inputs, the Run step invokes tethernet_bench bare behind pipefail+tee, and the inputs.args env-passage comment went with the plumbing it described | | 52 | `fix(ci): sync the root-leg and style-default comments` | Done | Third-round comment syncs. tests.yml's sanitized root step claimed the net\. filter selects the root-gated feth/BPF cases only, but ctest picks up all three net.* suites (live run 42506526442 executed net.abi, net.loopback, and net.feth there): the comment now says exactly that, net.feth is the one root gates and the other two ride along under the sanitizer build. AGENTS row 39 said the format gate runs `-style file` "since the binary default is LLVM style", contradicting lint.yml's corrected comment (recent clang-format already defaults to file style; the flag survives a future default change): row 39 now carries the corrected rationale. Delegation record: three antigravity/gemini-3.8-flash delegates applied the lint, bench, and tests edits byte-exact within scope; the update-tap edits were applied directly, because the workspace guard blocks non-primary providers from a secret-bearing file, and update-tap.yml is exactly such a file | | 53 | `docs: correct row 50's event_name provenance to tests.yml` | Done | Unanimous round-three reviewer finding, applied verbatim after re-verification at the source: row 50 credited tests/build with gaining event_name in f0fbc1d, but that commit touched release.yml and tests.yml only, and build.yml has never carried a concurrency block at all (pickaxe for event_name and for concurrency: both empty; it is dispatch-only with nothing to cancel). Row 50 now attributes the earlier scoping to tests.yml alone; the lint.yml workflow change itself was always correct and needed no second look. Non-blocking doc-only fix | | 54 | `fix(ci): force arm64 target architecture in codeql build` | Done | Diagnosed CodeQL run 42513807606 (2026-11-08): job failed with linker error "Undefined symbols for architecture x86_64" and "found architecture 'arm64', required architecture 'x86_64'" against Homebrew's /opt/homebrew/Cellar/libusb/1.0.30/lib/libusb-1.0.dylib. CodeQL's macOS tracer forces traced child processes onto the x86_64 slice under Rosetta; without an explicit target architecture, AppleClang emits x86_64 objects that cannot link against arm64-only Homebrew dependencies. `codeql.yml` pins `CMAKE_OSX_ARCHITECTURES=arm64` so clang cross-emits arm64 regardless of driver process architecture; `arch -arm64` cannot be used instead because child builds forced native bypass CodeQL tracing. Hardened `codeql.yml` with a `pull_request` trigger scoped to `.github/**` so workflow edits self-verify. Verified green by two follow-up runs on the branch -- 42578919426 (pull_request event) and 42578945162 (workflow_dispatch; 11m59s C++-analysis job span, 12m5s run wall, Build and analyze clean against arm64 Homebrew libusb) -- sealing the failure-plus-verification pairing exactly like rows 39/41/42/47. | | 55 | `fix(ci): narrow codeql pr trigger, event-scope concurrency` | Done | Review round on row 54, five findings, four applied in codeql.yml plus this row's amendment: (1, consensus) pull_request paths narrowed `.github/**` -> `.github/workflows/**` - dependabot.yml and .github/scripts/ edits never reach the analyzer, while Dependabot's weekly grouped action-bump PR still matches by construction (it edits workflow files) and the header comment now states that ride-along as deliberate; (2, sharpest catch) the header's "No concurrency block: canceling would throw away the whole analysis" sentence predated the PR trigger and no longer held - a superseded PR commit's SARIF is simply replaced by the newer run - so an event-scoped concurrency group mirroring tests.yml landed (group `${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}`, cancel-in-progress: true) with a comment explaining why cancellation is safe within one event and why event_name in the key keeps a push to main from canceling the Monday cron; (3) row 54 amended (above) to cite verification runs 42578919426 and 42578945162, sealing the failure-plus-verification pairing the ledger's audit value depends on; (4, nit) the Build step comment now carries the hard-coded-on-purpose guard - under the tracer `uname -m` reports x86_64, so a `-DCMAKE_OSX_ARCHITECTURES=$(uname -m)` "simplification" would silently reintroduce the bug (the deliberate-choices-need-a-magnitude rule from pitfall 16); (5, INFO) the first-ever green analysis surfaced one pre-existing `cpp/use-after-free` at vendored third_party/doctest/doctest.h:3710 - resolved as a documented decline rather than config: `paths-ignore` is verifiably inert for C/C++ manual builds (codeql team statement in github/codeql-action#3890; GitHub docs confine paths filtering to build-mode none; the traced run would warn "path-based filtering not supported"), and SARIF post-filtering via advanced-security/filter-sarif means a new third-party action for exactly one benign vendored alert - the workflow header records the accepted baseline instead; the fork-PR-403 finding was retracted by the reviewers against current GitHub docs (SARIF upload on pull_request events is permitted under read-only fork tokens). Two disjoint antigravity/gemini-3.8-flash delegates applied the codeql.yml and AGENTS.md edits byte-exact within disjoint file scopes. Verification: the PR-triggered analysis run 42583242299 is green on 75634ad, and the new group's cancel-in-progress proved itself on this very push - the in-flight run 42582860934 for the superseded commit 84d8151 was canceled and replaced by the 75634ad run. | -| 56 | `feat(i18n)!: drop the Chinese column, keep the single-language wiring` (this branch) | Done | Maintainer decision: English-only, but preserving the multi-language plumbing so adding a language stays a bounded five-touch change (checklist now lives at the top of messages.def). messages.def went 843 -> 548 lines via a string-aware perl splice (TETHERKIT_MESSAGE(id, zh, en) -> (id, en)), 310 records; guards on kept column: no Han, no full-width punctuation. i18n.h/i18n.cc rewritten in English: Language enum reduced to kEnglish=0, single kMessageTable, TextIn kept as the designed per-language dispatch point, ParseLanguage no longer accepts zh/Chinese (returns false, CLI rejects with new English message), LanguageFromLocaleString parked at "always English" with the prefix rule to return there, ContextSeparator fixed to ": ". C ABI signature-stable: TK_LANGUAGE_CHINESE removed; ToLanguage returns nullopt for unknown values so a caller still sending the old Chinese value 1 is ignored and stays in English; helper XPC setLanguage method and protocolRevision 3 untouched (still applied, inert). Four ScopedLanguage guard sites flipped kChinese->kEnglish with English expectations (bridge stats "queue depth", CONDIS reject "connection-oriented", keepalive fatal "keep-alive", MalformedReason "none"); test_net_link's sysctl-why bilingual loop collapsed to the single table. test_common_i18n.cc rewritten: per-string placeholder well-formedness, no mixed indexing, contiguous indices 0..n-1; the cross-language parity checks were retired and are documented to come back when a second language lands. CLI help no longer advertises zh. Verification: cmake -Werror build clean, ctest 23/23 suites pass, smokes green (--version; --help shows the reworded --lang lines; --lang zh --list errors in English; --lang en --list and LC_ALL=zh_CN --list render English). GUI side deliberately untouched this phase, so `swift test` still exercises bilingual Swift tables and must be green throughout; the Swift strip is the next commit. Splice/validation scripts live in the session temp dir (not in the repo). Untracked docs/icon_resources/ and draft_icons/ predate this work and were left alone. | -| 57 | `feat(i18n)!: strip the Swift side to English-only, keep the add-a-language wiring` (this branch) | Done | Phase 2 complement to row 56. LocalizedStrings.swift went 743 -> 685 lines via a string-aware perl splice: every `localizations` switch row lost its Chinese first argument, with two guards - the dropped arg must contain Han or full-width punctuation OR be identical to the kept one (the languageEnglish native-name row needs the second rule), and the kept column must be Han- and fullwidth-free; kept spans are preserved byte-exact, including multi-line `+`-concatenation layouts (installer/update/permission strings); the file header and 42 MARK/comment lines went to English via an exact-line map; the `languageChinese` key is gone entirely (both Pickers hard-code `Text(verbatim: "English")` under the tag, so the key had no non-test consumers). Language enum is `.english` only with `cValue` constant 0; LanguagePreference loses `.chinese`, so a stale saved rawValue "chinese" decodes to nil and falls back to `.system` (graceful, symmetric with the C ABI ignoring unknown values); `L10n.apply` and `text(_:in:)` survive as the add-a-language dispatch points; new internal `L10n.localeLanguage` is parked always-English, exactly mirroring LanguageFromLocaleString on the C++ side, and `systemLanguage` still reads Locale.preferredLanguages through it; HelperService.setLanguage keeps its ignore-unknown semantics (an old App still sending "chinese" leaves the helper in English); both language menus now offer Follow system + English. **Swift constraint determined empirically** (tiny probe program): single-element labeled tuples are illegal - `cannot create a single-element tuple with an element label` - so the planned `(english: String)` one-member tuple is unrepresentable and rows are bare strings; when a language is added the property becomes a real labeled tuple and every case appends one labeled argument, the same way C++ rows `(id, en)` grow. LocalizationTests rewritten for the single table, still 7 cases: per-key placeholder contiguity, the %ld-integer rule, language resolution incl. localeLanguage, and formatting substitution; the cross-language placeholder/parity checks retire until a second language and the suite header says they return then. Verification: swift build green (3.3s, only the pre-existing libtetherkit dylib-version warning); swift test 30/30 - TetherKitIPCTests 17/17 (Authorization 2 + HelperConstants 8 + the 7 rewritten LocalizationTests) and TetherKitUIModelTests 13/13; the current-state snapshot below says "17 IPC + 7 UIModel = 24", which predates the sidebar test additions, not this work. Also folded in here: the row-56 typo fix (i18n.cci -> i18n.h). Splice/unwrap scripts live in the session temp dir. | -| 58 | `feat(i18n)!: translate every Chinese comment and user-facing benchmark string to English` (this branch) | Done | P3 = the comment-and-doc-string half of the English-only migration (P1 = C++ message table, P2 = Swift string table). Scope: ~100 files, every Han comment in `include/`, `src/`, `tests/`, `benchmarks/`, `gui/` plus the benchmark tool's own report output; `docs/` + `README` + `AGENTS.md` are P4. Work was delegated in disjoint batches through the T3 `delegate_task` tool on the three OpenCode models the maintainer named (iceman-free, minimax-m3.1-flash-free, mimo-v2.6-flash-free; MiMo hit a free-tier limit mid-run and its remainder was re-dispatched to MiniMax); every batch was verified before acceptance, and three were sent back or fixed by hand. Verification method, all in-session: a comment-only scope proof (comments stripped from HEAD and from the tree, then the code token streams compared -- byte-identical for every file except the deliberately allowed literals), a comment-line-count drop check (a translation must not shrink a file's prose), a repo-wide Han census, the CI lint gate reproduced locally (vendored `.github/scripts/clang-format-diff.py` over the changed hunks, base HEAD), a full `-Werror` build plus 23/23 ctest, and `swift build`/`swift test` for the GUI batches. Findings worth keeping: (1) the house style is **pure ASCII in comments** -- the earlier crew had already converted box-drawing to `+ - |` in `feth_device.h` and dropped the `star`/`warning` markers, so two batches that kept them were normalized afterwards (emphasis now lives in the prose); (2) the CI format gate is **line-scoped**, so touching a test title drags its whole `TEST_SUITE` block into scope and clang-format then wants it re-indented by 2 -- the Chinese-era test files had their cases at column 0 and were never format-clean; the reformat was applied with an invariant that the **code token stream** is unchanged (a re-indent or a re-wrap may move a line break, never a token) and that the **comment word stream** is unchanged (a long comment may be reflowed, never reworded); (3) `src/app/main.cc` had to have its include blocks reordered (project headers before system headers, per `.clang-format`) -- pre-existing baseline debt that only became visible once a comment above it changed; (4) `docs/BENCHMARKS.md` is **half generated**: its first part is the literal output of `tetherkit_bench`, so the tool's report strings had to be translated too (they are user-facing) and the document's generated part must use exactly the wording the tool now prints -- but the document must NOT be regenerated, because that would drop the root-measured feth/BPF numbers and the hand-written analysis; (5) the `device` fixture in `tests/test_capi.cc` was **swapped, not translated**: it is a byte-pattern fixture (two 3-byte UTF-8 characters) and became two U+20AC characters (the euro sign, 3 bytes each, 6 bytes total) with every byte count in the assertions preserved; (6) the two CI grep anchors in `tests/test_net_link.cc` (the `ReportSkip` format string and the `created {} <-> {}` message) are byte-exact and must stay so; (7) a *passing* test is not proof -- a log-text needle such as `find("libusb initialized")` must match the production message table exactly or the assertion passes vacuously; (8) the Chinese strings in `tests/test_common_error.cc` are **paired** (a context string and the assertion on it) and were translated on both sides with identical wording, which is why the brief for that batch had to be corrected before dispatch. The GUI language plumbing, the XPC protocol revision 3 and the C ABI are untouched by this phase | -| 59 | `docs(i18n)!: translate the documentation to English and drop the Chinese README` (this branch) | Done | P4 = the documentation half of the English-only migration (P3 = code comments). Scope: `AGENTS.md` (857 -> 488 lines), the six `docs/*.md` files, `README.md`, and the deletion of `README.zh-CN.md`. Delegated in four disjoint batches through the T3 `delegate_task` tool on the three OpenCode models the maintainer named; the README edits and the Chinese-README deletion were done by the maintainer. Verification, per file: a numeric-token multiset comparison against HEAD (every measurement preserved; the only differences are three correct `wan`-unit conversions in BENCHMARKS.md -- `124 wan` -> "1.24 million frames per second", `2.7 wan` -> "27,000 frames per second", `46 wan` -> "460,000 frames per second"), a fence-aware structural comparison (headings, table row counts and per-table pipe counts identical in every file), a link-resolution check, and a repo-wide Han scan that reaches zero. Findings worth keeping: (1) `docs/BENCHMARKS.md` is **half generated** -- its first part is the literal output of `tetherkit_bench`, so it must use exactly the wording the tool now prints, and it must **not** be regenerated (that would drop the root-measured feth/BPF numbers and the hand-written analysis); the tool's English output was captured and used as the reference; (2) a document that quotes GUI strings must quote them **verbatim**: four quotes in GUI-ARCHITECTURE.md did not match the table ("Check for Updates..." vs the real "Check for Updates...", "Uninstall the privileged helper..." vs "...helper?", the invented "Install / update the privileged helper" button, and a `%ld` template rendered as a literal) and were corrected against `LocalizedStrings.swift`; (3) **ASCII-only yields to fidelity** when the text is a quotation of a real string or a literal that carries meaning: GUI-SPIKE.md keeps a full-width parenthesis in its bash snippet because the documented `0xEF` lead-byte trap depends on it (the reproduction was re-verified: `IFACE?: unbound variable`), and the two UI titles above keep the app's real ellipsis and question mark; (4) the AGENTS.md ledger rows and the pitfall list keep their exact numbering (the 58 pre-existing rows, plus this one; pitfalls 1..24 plus 4b), so the code's citation "AGENTS.md section 7, pitfalls 12 and 13" still resolves to the same two pitfalls; the emoji status marks became the word "Done" table-wide (row 58 included, its prose untouched), and the stale test count was corrected from 24 to the measured 30 (17 TetherKitIPC + 13 TetherKitUIModel); (5) the AGENTS.md directory tree no longer lists the deleted `README.zh-CN.md`, while ledger row 38 keeps the mention as a historical record of what that commit did; (6) the i18n section was adapted to the single-language reality while naming the retained plumbing and its five-touch checklist. Nothing pushed | -| 60 | `build(ci): pin actions, widen CodeQL, open a PR for tap updates` (this branch) | Done | Supply-chain and release-gate hardening, all three parts researched against current upstream docs before writing. **SHA pinning**: every external action reference (18 across 6 workflow files) is now a full 40-char commit SHA with the release tag in a trailing comment (actions/checkout@3d3c42e # v7.0.1, actions/upload-artifact@cf430e0 # v7.0.2, github/codeql-action@33d7b85 # v4.39.3); SHAs were resolved through the git-ref API, dereferenced for annotated tags, then each confirmed to BE the release commit (checkout = 'prep v7.0.1 release', upload-artifact = the v7.0.2 merge, codeql-action v4 == v4.39.3 by compare). Rationale: a mutable tag can be repointed at malicious code and our workflows hold a token that writes the public tap users install from; SHA pinning is what GitHub's hardening guide calls the most secure option and what the OpenSSF Scorecard Pinned-Dependencies check measures. **The documented cost, recorded so it is not rediscovered**: Dependabot ALERTS are only generated for actions referenced with semver, not SHA (docs: 'alerts are only generated for actions that use semantic versioning, not SHA versioning'), so a compromised release of a pinned action now arrives as a version-update PR rather than a security alert, and dependabot_security_updates has nothing to open for the github-actions ecosystem. dependabot.yml was rewritten to state the new convention and that trade. The local reusable-workflow call (uses: ./.github/workflows/update-tap.yml) stays a path on purpose. **CodeQL**: the pull_request paths filter was removed (row 55 had narrowed it to .github/workflows/**); it now runs on every PR, because the check's value is catching a defect before it lands, and with the filter 'C++ analysis' could not be a required status check without deadlocking PRs that never trigger it -- macOS minutes are free for public repos, so the cost is wall-clock, not money. The vendored doctest is now excluded from the analysis set by BUILDING less, not by path filters: -DTETHERKIT_BUILD_TESTS=OFF, which is the mechanism the docs prescribe for compiled languages ('configure your build process to compile only the specified files'); paths-ignore is inert for C++ manual builds (codeql-action#3890, maintainer statement) and SARIF post-filtering via advanced-security/filter-sarif was declined rather than unavailable: the action is public, but it is one more third-party action to trust and pin on the analysis path, and it would need the workflow to upload the SARIF itself rather than letting the action do it -- not worth it for one benign vendored alert. That retires cpp/use-after-free at third_party/doctest/doctest.h:3710 at the SOURCE instead of dismissing it -- it is a union false positive (the buffer is freed and reassigned before the read) in a test framework that never ships, and upstream doctest 2.5.3 carries the identical code, so upgrading would not clear it. Cost: tests/ drops out of the analysis set with doctest; src/ and include/ stay fully analyzed, which is where the BPF buffer walk and the reinterpret_cast-heavy private ABI bridge live. **Release gate**: the tap update no longer pushes to main. It pushes a bump branch and opens a PR, because the tap is what users brew install from and its contents should get a human read. One credential, a fine-grained token scoped to justsrc/homebrew-tap with contents:write and pull_requests:write: it clones the tap over https, pushes the bump branch, and opens the PR (GITHUB_TOKEN cannot cross repositories, which is why a token is needed at all). The token reaches git through a credential helper rather than through the remote URL: the push step configures `credential.https://github.com.helper = !gh auth git-credential` in the clone's local config, and the helper reads the token from GH_TOKEN in the environment at the moment git asks for it, so the token never appears in a command line, in .git/config, or in a failure message. The 'release' environment on TetherKit carries a required-reviewer rule (justsrc), so the job waits for an approval before it even creates the branch; the tap repo got its own ruleset requiring a PR on main, so a direct push is impossible there, not merely avoided -- the token can write the tap's main directly and only the ruleset stops it. Verification, local only: every workflow parses (PyYAML), actionlint reports zero findings on the three substantively edited files and no new findings against the pre-edit baseline on the others (the xcode-27 runner-label and shellcheck notes are pre-existing), all eight run: blocks pass bash -n, and a cmake configure with -DTETHERKIT_BUILD_TESTS=OFF succeeds. Findings worth keeping: (1) 'commit.signoff' is NOT a git config key -- git 2.56 only knows format.signoff, which governs format-patch; git commit -s adds the trailer, the config does not, and the setting reads back fine while doing nothing, so the check that matters is the trailer's presence, not the config's value; (2) 'web_commit_signoff_required' governs commits made through the WEB interface only, so it never gated CLI pushes; (3) the repo's own history is 145 direct commits to main against 5 merges, so a required-checks rule on main changes the working style: direct pushes are rejected, and 'strict' (branch must be up to date with main) compounds that, which is why strict is off and the maintainer chose PR-only; (4) an open 'critical' code-scanning alert on vendored code is a reputational cost even when it is a false positive, which is the second reason to fix it at the source rather than dismiss it. Committed and pushed as feat/ci-hardening; see row 61 for the follow-up | +| 56 | `feat(i18n)!: drop the Chinese column, keep the single-language wiring` (this branch) | Done | Maintainer decision: English-only, but preserving the multi-language plumbing so adding a language stays a bounded five-touch change (checklist now lives at the top of messages.def). messages.def went 843 -> 548 lines via a string-aware perl splice (TETHERNET_MESSAGE(id, zh, en) -> (id, en)), 310 records; guards on kept column: no Han, no full-width punctuation. i18n.h/i18n.cc rewritten in English: Language enum reduced to kEnglish=0, single kMessageTable, TextIn kept as the designed per-language dispatch point, ParseLanguage no longer accepts zh/Chinese (returns false, CLI rejects with new English message), LanguageFromLocaleString parked at "always English" with the prefix rule to return there, ContextSeparator fixed to ": ". C ABI signature-stable: TN_LANGUAGE_CHINESE removed; ToLanguage returns nullopt for unknown values so a caller still sending the old Chinese value 1 is ignored and stays in English; helper XPC setLanguage method and protocolRevision 3 untouched (still applied, inert). Four ScopedLanguage guard sites flipped kChinese->kEnglish with English expectations (bridge stats "queue depth", CONDIS reject "connection-oriented", keepalive fatal "keep-alive", MalformedReason "none"); test_net_link's sysctl-why bilingual loop collapsed to the single table. test_common_i18n.cc rewritten: per-string placeholder well-formedness, no mixed indexing, contiguous indices 0..n-1; the cross-language parity checks were retired and are documented to come back when a second language lands. CLI help no longer advertises zh. Verification: cmake -Werror build clean, ctest 23/23 suites pass, smokes green (--version; --help shows the reworded --lang lines; --lang zh --list errors in English; --lang en --list and LC_ALL=zh_CN --list render English). GUI side deliberately untouched this phase, so `swift test` still exercises bilingual Swift tables and must be green throughout; the Swift strip is the next commit. Splice/validation scripts live in the session temp dir (not in the repo). Untracked docs/icon_resources/ and draft_icons/ predate this work and were left alone. | +| 57 | `feat(i18n)!: strip the Swift side to English-only, keep the add-a-language wiring` (this branch) | Done | Phase 2 complement to row 56. LocalizedStrings.swift went 743 -> 685 lines via a string-aware perl splice: every `localizations` switch row lost its Chinese first argument, with two guards - the dropped arg must contain Han or full-width punctuation OR be identical to the kept one (the languageEnglish native-name row needs the second rule), and the kept column must be Han- and fullwidth-free; kept spans are preserved byte-exact, including multi-line `+`-concatenation layouts (installer/update/permission strings); the file header and 42 MARK/comment lines went to English via an exact-line map; the `languageChinese` key is gone entirely (both Pickers hard-code `Text(verbatim: "English")` under the tag, so the key had no non-test consumers). Language enum is `.english` only with `cValue` constant 0; LanguagePreference loses `.chinese`, so a stale saved rawValue "chinese" decodes to nil and falls back to `.system` (graceful, symmetric with the C ABI ignoring unknown values); `L10n.apply` and `text(_:in:)` survive as the add-a-language dispatch points; new internal `L10n.localeLanguage` is parked always-English, exactly mirroring LanguageFromLocaleString on the C++ side, and `systemLanguage` still reads Locale.preferredLanguages through it; HelperService.setLanguage keeps its ignore-unknown semantics (an old App still sending "chinese" leaves the helper in English); both language menus now offer Follow system + English. **Swift constraint determined empirically** (tiny probe program): single-element labeled tuples are illegal - `cannot create a single-element tuple with an element label` - so the planned `(english: String)` one-member tuple is unrepresentable and rows are bare strings; when a language is added the property becomes a real labeled tuple and every case appends one labeled argument, the same way C++ rows `(id, en)` grow. LocalizationTests rewritten for the single table, still 7 cases: per-key placeholder contiguity, the %ld-integer rule, language resolution incl. localeLanguage, and formatting substitution; the cross-language placeholder/parity checks retire until a second language and the suite header says they return then. Verification: swift build green (3.3s, only the pre-existing libtethernet dylib-version warning); swift test 30/30 - TethernetIPCTests 17/17 (Authorization 2 + HelperConstants 8 + the 7 rewritten LocalizationTests) and TethernetUIModelTests 13/13; the current-state snapshot below says "17 IPC + 7 UIModel = 24", which predates the sidebar test additions, not this work. Also folded in here: the row-56 typo fix (i18n.cci -> i18n.h). Splice/unwrap scripts live in the session temp dir. | +| 58 | `feat(i18n)!: translate every Chinese comment and user-facing benchmark string to English` (this branch) | Done | P3 = the comment-and-doc-string half of the English-only migration (P1 = C++ message table, P2 = Swift string table). Scope: ~100 files, every Han comment in `include/`, `src/`, `tests/`, `benchmarks/`, `gui/` plus the benchmark tool's own report output; `docs/` + `README` + `AGENTS.md` are P4. Work was delegated in disjoint batches through the T3 `delegate_task` tool on the three OpenCode models the maintainer named (iceman-free, minimax-m3.1-flash-free, mimo-v2.6-flash-free; MiMo hit a free-tier limit mid-run and its remainder was re-dispatched to MiniMax); every batch was verified before acceptance, and three were sent back or fixed by hand. Verification method, all in-session: a comment-only scope proof (comments stripped from HEAD and from the tree, then the code token streams compared -- byte-identical for every file except the deliberately allowed literals), a comment-line-count drop check (a translation must not shrink a file's prose), a repo-wide Han census, the CI lint gate reproduced locally (vendored `.github/scripts/clang-format-diff.py` over the changed hunks, base HEAD), a full `-Werror` build plus 23/23 ctest, and `swift build`/`swift test` for the GUI batches. Findings worth keeping: (1) the house style is **pure ASCII in comments** -- the earlier crew had already converted box-drawing to `+ - |` in `feth_device.h` and dropped the `star`/`warning` markers, so two batches that kept them were normalized afterwards (emphasis now lives in the prose); (2) the CI format gate is **line-scoped**, so touching a test title drags its whole `TEST_SUITE` block into scope and clang-format then wants it re-indented by 2 -- the Chinese-era test files had their cases at column 0 and were never format-clean; the reformat was applied with an invariant that the **code token stream** is unchanged (a re-indent or a re-wrap may move a line break, never a token) and that the **comment word stream** is unchanged (a long comment may be reflowed, never reworded); (3) `src/app/main.cc` had to have its include blocks reordered (project headers before system headers, per `.clang-format`) -- pre-existing baseline debt that only became visible once a comment above it changed; (4) `docs/BENCHMARKS.md` is **half generated**: its first part is the literal output of `tethernet_bench`, so the tool's report strings had to be translated too (they are user-facing) and the document's generated part must use exactly the wording the tool now prints -- but the document must NOT be regenerated, because that would drop the root-measured feth/BPF numbers and the hand-written analysis; (5) the `device` fixture in `tests/test_capi.cc` was **swapped, not translated**: it is a byte-pattern fixture (two 3-byte UTF-8 characters) and became two U+20AC characters (the euro sign, 3 bytes each, 6 bytes total) with every byte count in the assertions preserved; (6) the two CI grep anchors in `tests/test_net_link.cc` (the `ReportSkip` format string and the `created {} <-> {}` message) are byte-exact and must stay so; (7) a *passing* test is not proof -- a log-text needle such as `find("libusb initialized")` must match the production message table exactly or the assertion passes vacuously; (8) the Chinese strings in `tests/test_common_error.cc` are **paired** (a context string and the assertion on it) and were translated on both sides with identical wording, which is why the brief for that batch had to be corrected before dispatch. The GUI language plumbing, the XPC protocol revision 3 and the C ABI are untouched by this phase | +| 59 | `docs(i18n)!: translate the documentation to English and drop the Chinese README` (this branch) | Done | P4 = the documentation half of the English-only migration (P3 = code comments). Scope: `AGENTS.md` (857 -> 488 lines), the six `docs/*.md` files, `README.md`, and the deletion of `README.zh-CN.md`. Delegated in four disjoint batches through the T3 `delegate_task` tool on the three OpenCode models the maintainer named; the README edits and the Chinese-README deletion were done by the maintainer. Verification, per file: a numeric-token multiset comparison against HEAD (every measurement preserved; the only differences are three correct `wan`-unit conversions in BENCHMARKS.md -- `124 wan` -> "1.24 million frames per second", `2.7 wan` -> "27,000 frames per second", `46 wan` -> "460,000 frames per second"), a fence-aware structural comparison (headings, table row counts and per-table pipe counts identical in every file), a link-resolution check, and a repo-wide Han scan that reaches zero. Findings worth keeping: (1) `docs/BENCHMARKS.md` is **half generated** -- its first part is the literal output of `tethernet_bench`, so it must use exactly the wording the tool now prints, and it must **not** be regenerated (that would drop the root-measured feth/BPF numbers and the hand-written analysis); the tool's English output was captured and used as the reference; (2) a document that quotes GUI strings must quote them **verbatim**: four quotes in GUI-ARCHITECTURE.md did not match the table ("Check for Updates..." vs the real "Check for Updates...", "Uninstall the privileged helper..." vs "...helper?", the invented "Install / update the privileged helper" button, and a `%ld` template rendered as a literal) and were corrected against `LocalizedStrings.swift`; (3) **ASCII-only yields to fidelity** when the text is a quotation of a real string or a literal that carries meaning: GUI-SPIKE.md keeps a full-width parenthesis in its bash snippet because the documented `0xEF` lead-byte trap depends on it (the reproduction was re-verified: `IFACE?: unbound variable`), and the two UI titles above keep the app's real ellipsis and question mark; (4) the AGENTS.md ledger rows and the pitfall list keep their exact numbering (the 58 pre-existing rows, plus this one; pitfalls 1..24 plus 4b), so the code's citation "AGENTS.md section 7, pitfalls 12 and 13" still resolves to the same two pitfalls; the emoji status marks became the word "Done" table-wide (row 58 included, its prose untouched), and the stale test count was corrected from 24 to the measured 30 (17 TethernetIPC + 13 TethernetUIModel); (5) the AGENTS.md directory tree no longer lists the deleted `README.zh-CN.md`, while ledger row 38 keeps the mention as a historical record of what that commit did; (6) the i18n section was adapted to the single-language reality while naming the retained plumbing and its five-touch checklist. Nothing pushed | +| 60 | `build(ci): pin actions, widen CodeQL, open a PR for tap updates` (this branch) | Done | Supply-chain and release-gate hardening, all three parts researched against current upstream docs before writing. **SHA pinning**: every external action reference (18 across 6 workflow files) is now a full 40-char commit SHA with the release tag in a trailing comment (actions/checkout@3d3c42e # v7.0.1, actions/upload-artifact@cf430e0 # v7.0.2, github/codeql-action@33d7b85 # v4.39.3); SHAs were resolved through the git-ref API, dereferenced for annotated tags, then each confirmed to BE the release commit (checkout = 'prep v7.0.1 release', upload-artifact = the v7.0.2 merge, codeql-action v4 == v4.39.3 by compare). Rationale: a mutable tag can be repointed at malicious code and our workflows hold a token that writes the public tap users install from; SHA pinning is what GitHub's hardening guide calls the most secure option and what the OpenSSF Scorecard Pinned-Dependencies check measures. **The documented cost, recorded so it is not rediscovered**: Dependabot ALERTS are only generated for actions referenced with semver, not SHA (docs: 'alerts are only generated for actions that use semantic versioning, not SHA versioning'), so a compromised release of a pinned action now arrives as a version-update PR rather than a security alert, and dependabot_security_updates has nothing to open for the github-actions ecosystem. dependabot.yml was rewritten to state the new convention and that trade. The local reusable-workflow call (uses: ./.github/workflows/update-tap.yml) stays a path on purpose. **CodeQL**: the pull_request paths filter was removed (row 55 had narrowed it to .github/workflows/**); it now runs on every PR, because the check's value is catching a defect before it lands, and with the filter 'C++ analysis' could not be a required status check without deadlocking PRs that never trigger it -- macOS minutes are free for public repos, so the cost is wall-clock, not money. The vendored doctest is now excluded from the analysis set by BUILDING less, not by path filters: -DTETHERNET_BUILD_TESTS=OFF, which is the mechanism the docs prescribe for compiled languages ('configure your build process to compile only the specified files'); paths-ignore is inert for C++ manual builds (codeql-action#3890, maintainer statement) and SARIF post-filtering via advanced-security/filter-sarif was declined rather than unavailable: the action is public, but it is one more third-party action to trust and pin on the analysis path, and it would need the workflow to upload the SARIF itself rather than letting the action do it -- not worth it for one benign vendored alert. That retires cpp/use-after-free at third_party/doctest/doctest.h:3710 at the SOURCE instead of dismissing it -- it is a union false positive (the buffer is freed and reassigned before the read) in a test framework that never ships, and upstream doctest 2.5.3 carries the identical code, so upgrading would not clear it. Cost: tests/ drops out of the analysis set with doctest; src/ and include/ stay fully analyzed, which is where the BPF buffer walk and the reinterpret_cast-heavy private ABI bridge live. **Release gate**: the tap update no longer pushes to main. It pushes a bump branch and opens a PR, because the tap is what users brew install from and its contents should get a human read. One credential, a fine-grained token scoped to justsrc/homebrew-tap with contents:write and pull_requests:write: it clones the tap over https, pushes the bump branch, and opens the PR (GITHUB_TOKEN cannot cross repositories, which is why a token is needed at all). The token reaches git through a credential helper rather than through the remote URL: the push step configures `credential.https://github.com.helper = !gh auth git-credential` in the clone's local config, and the helper reads the token from GH_TOKEN in the environment at the moment git asks for it, so the token never appears in a command line, in .git/config, or in a failure message. The 'release' environment on Tethernet carries a required-reviewer rule (justsrc), so the job waits for an approval before it even creates the branch; the tap repo got its own ruleset requiring a PR on main, so a direct push is impossible there, not merely avoided -- the token can write the tap's main directly and only the ruleset stops it. Verification, local only: every workflow parses (PyYAML), actionlint reports zero findings on the three substantively edited files and no new findings against the pre-edit baseline on the others (the xcode-27 runner-label and shellcheck notes are pre-existing), all eight run: blocks pass bash -n, and a cmake configure with -DTETHERNET_BUILD_TESTS=OFF succeeds. Findings worth keeping: (1) 'commit.signoff' is NOT a git config key -- git 2.56 only knows format.signoff, which governs format-patch; git commit -s adds the trailer, the config does not, and the setting reads back fine while doing nothing, so the check that matters is the trailer's presence, not the config's value; (2) 'web_commit_signoff_required' governs commits made through the WEB interface only, so it never gated CLI pushes; (3) the repo's own history is 145 direct commits to main against 5 merges, so a required-checks rule on main changes the working style: direct pushes are rejected, and 'strict' (branch must be up to date with main) compounds that, which is why strict is off and the maintainer chose PR-only; (4) an open 'critical' code-scanning alert on vendored code is a reputational cost even when it is a false positive, which is the second reason to fix it at the source rather than dismiss it. Committed and pushed as feat/ci-hardening; see row 61 for the follow-up | | 61 | `fix(ci): keep the CodeQL upload alive on Dependabot merges` (this branch) | Done | A Dependabot PR merged with SQUASH produces a commit on main that is AUTHORED BY DEPENDABOT, and any workflow on such a commit gets a read-only GITHUB_TOKEN, so the SARIF upload 403s ("Resource not accessible by integration"). The upload library logs a warning and rethrows (upload-lib.ts's 403 handler); the action's outer catch then fails the step via `core.setFailed`, so the run goes red. The analyze step now sets the action's own `upload` input to `never` for a Dependabot-authored push and `always` otherwise, so the run stays green while the analysis still runs; only the publish is skipped. The action's `getUploadValue` returns exactly always/failure-only/never (legacy `true`/`false` inputs map onto them; unknown values warn and default to always), and the 'never' branch skips the upload call and returns normally (read out of the action source at the pinned v4.39.3, not just the docs). **The pull_request path is deliberately NOT guarded**, which corrects an earlier assumption in this session: code scanning always allows uploading results when the pull_request event triggers the run, because the results are scoped to that one PR, and upload-lib.ts carries an explicit pull_request branch for it. Guarding it would be a regression, and a regression with teeth: no upload means no result, and a required check that never reports blocks the PR forever. **Two wrong turns worth recording**: the input is `upload`, not `upload-sarif` -- that boolean belongs to OSV-Scanner, and copying it here would have been silently ignored; and both `paths-ignore` and a `branches`+`branches-ignore` pair were wrong -- actionlint rejects a `branches`+`branches-ignore` PAIR on one event (paths filters combine with branches fine -- probe: exit 0 -- but paths-ignore is inert for C++ manual builds), and a squash-merged bot commit lands on main, so a dependabot/** branch filter cannot match it anyway. Verification, local only: the expression's four event/author combinations evaluated by hand, the workflow parses, actionlint is clean, the input name and its accepted values read out of the action.yml at the pinned SHA, and getUploadValue plus the 'never' branch read in the action source. Committed and pushed as feat/ci-hardening | -| 62 | `docs(ci): document the Dependabot PR merge procedure` (this branch) | Done | The SHA pinning made Dependabot a weekly event, so the procedure for merging its PR is now part of the repo's operating instructions rather than tribal knowledge. It lives in .github/dependabot.yml, where a maintainer opening a bot PR will look, and it covers: merge-commit rather than squash or rebase (both leave the tip authored by Dependabot), because a squash or rebase commit is authored by Dependabot and loses write scope (the merge commit is authored by the human, so results keep publishing); the REST bypass command for the review gate, `gh api repos/justsrc/TetherKit/pulls//merge -X PUT -f merge_method=merge`, because `gh pr merge` refuses at its own pre-flight when the PR is BLOCKED (cli/cli#13388, still open) and suggests --auto or --admin; --admin only suppresses that client-side refusal -- no admin flag travels to the server (gh's mergePayload has no such field) and the 'Protect main' ruleset has no bypass actor at all, so nothing can override the required status checks. The merge succeeds because the 'Require review' ruleset lists the admin role as a bypass actor with bypass_mode pull_request, which engages server-side on any PR merge by the admin, REST or gh; and the reminder that the version-update PR is the ONLY human checkpoint a compromised action release would meet, since SHA pinning deliberately kills the Dependabot alert channel (row 60). **The review requirement needed no settings change**, and the reason is the non-obvious part: the 'Require review' ruleset already lists the admin role as a bypass actor with bypass_mode pull_request, so the human who merges the bot PR bypasses the review exactly as for their own PRs. Adding Dependabot to the bypass list would have been actively wrong -- it would let the bot merge its own PR unreviewed, defeating the SHA pinning. That warning is carried in both dependabot.yml's step 4 and codeql.yml's header, so a maintainer meets it whichever of the two files they open. Also confirmed while writing it: no PR-triggered workflow (tests, lint, codeql) references a secret, so Dependabot's no-secrets rule is a non-issue for them. Committed and pushed as feat/ci-hardening | +| 62 | `docs(ci): document the Dependabot PR merge procedure` (this branch) | Done | The SHA pinning made Dependabot a weekly event, so the procedure for merging its PR is now part of the repo's operating instructions rather than tribal knowledge. It lives in .github/dependabot.yml, where a maintainer opening a bot PR will look, and it covers: merge-commit rather than squash or rebase (both leave the tip authored by Dependabot), because a squash or rebase commit is authored by Dependabot and loses write scope (the merge commit is authored by the human, so results keep publishing); the REST bypass command for the review gate, `gh api repos/justsrc/Tethernet/pulls//merge -X PUT -f merge_method=merge`, because `gh pr merge` refuses at its own pre-flight when the PR is BLOCKED (cli/cli#13388, still open) and suggests --auto or --admin; --admin only suppresses that client-side refusal -- no admin flag travels to the server (gh's mergePayload has no such field) and the 'Protect main' ruleset has no bypass actor at all, so nothing can override the required status checks. The merge succeeds because the 'Require review' ruleset lists the admin role as a bypass actor with bypass_mode pull_request, which engages server-side on any PR merge by the admin, REST or gh; and the reminder that the version-update PR is the ONLY human checkpoint a compromised action release would meet, since SHA pinning deliberately kills the Dependabot alert channel (row 60). **The review requirement needed no settings change**, and the reason is the non-obvious part: the 'Require review' ruleset already lists the admin role as a bypass actor with bypass_mode pull_request, so the human who merges the bot PR bypasses the review exactly as for their own PRs. Adding Dependabot to the bypass list would have been actively wrong -- it would let the bot merge its own PR unreviewed, defeating the SHA pinning. That warning is carried in both dependabot.yml's step 4 and codeql.yml's header, so a maintainer meets it whichever of the two files they open. Also confirmed while writing it: no PR-triggered workflow (tests, lint, codeql) references a secret, so Dependabot's no-secrets rule is a non-issue for them. Committed and pushed as feat/ci-hardening | | 63 | `docs(ci): apply the second-review text corrections` (this branch) | Done | A second, line-by-line review of rows 60-62 and the three CI files. Verdict: no defect in the workflows themselves -- every change is comment, prose, or a PR body -- but nine places where the ledger described the mechanism inaccurately or claimed something untrue, one addition pointing at where a warning now lives, and one check that turned out to be correct only by accident. **Every claim in the review was verified against upstream source before being applied, not taken on trust**, and one of them turned out to be overstated: the review says `gh pr merge --admin` "only suppresses that client-side refusal", but gh's own help and source show it also skips the merge queue client-side (the help text: "To bypass a merge queue and merge directly, pass the `--admin` flag"); the wording keeps the server-side half, which is the part that matters, and the load-bearing finding stands either way -- gh's `mergePayload` carries no admin field, so nothing about privileges travels to the server, and the 'Protect main' ruleset has `bypass_actors: []` with `current_user_can_bypass: never`, so no admin can override the required status checks. What merges the PR is the 'Require review' ruleset, which lists the admin role as a bypass actor with `bypass_mode: pull_request`. Corrections applied: row 38's open-loop deploy-key instruction became a closed loop (the tap repo is since done, the formula copy is still outstanding, and the deploy key is superseded by row 60's one-token model, so an onboarding agent can no longer act on a credential that is no longer used); row 60's claim that SARIF post-filtering "requires an Enterprise-owned repository" was **wrong on the facts** (the action is public) and was rewritten as a decision rather than a constraint -- declined, because it is one more third-party action to trust and pin on the analysis path; row 61's three clause fixes were corrected to the two-layer failure path (upload-lib.ts warns and rethrows, the action's outer catch then calls `core.setFailed`, read at analyze-action.ts:451 at the pinned v4.39.3), to `getUploadValue`'s real contract (exactly always/failure-only/never, with legacy `true`/`false` inputs mapped onto them and an unknown value warning and defaulting to always), and to actionlint's real rule (a `branches`+`branches-ignore` **pair** on one event is rejected, exit 1, while paths filters combine with branches fine, exit 0 -- both probed on actionlint 1.7.12); rows 61 and 62 ended with "Nothing pushed" although their commits are on origin/feat/ci-hardening (350eb0e and c34783e), so both now read "Committed and pushed as feat/ci-hardening"; row 62's merge strategy now says merge-commit rather than squash **or rebase** (both leave the tip authored by Dependabot) and its `--admin` paragraph was replaced with the verified mechanism. The one behavior change: update-tap.yml's existing-PR check piped `gh pr list --jq '.[0].number'` into `grep -q .`, and gh prints a bare newline when nothing matches -- **the old form was not actually broken** (probed: BSD grep, which the runner uses, does not match `.` against a bare newline, so the old check took the right branch in both cases), but it worked only by that accident, so the check now tests for a non-empty number explicitly and is independent of the grep implementation; probed live against a branch with an open PR (yields the number) and one without (yields empty). The rewrite also changes one failure mode: under `bash -e` (GitHub's default for `run:` blocks) a failing `gh` call now ends the step at the check, because the assignment takes the non-zero status, where the old pipeline fell through to `gh pr create` -- the error surfaces at the check instead of at a later command. Delegation: two disjoint scopes went to iceman-free subagents (codeql.yml, dependabot.yml), each verified independently afterwards by diffing the comment-stripped payload against HEAD (byte-identical), so no YAML key, expression, or action pin moved; update-tap.yml was edited directly, because it is the secret-bearing file and row 52's workspace-guard rule keeps non-primary providers out of it. Also documented, not fixed: the tap token's `contents:write` also reaches the tap's tags and releases, which no ruleset governs (pitfall 26). Committed, not pushed | | 64 | `feat(ci): add a pre-release workflow` (this branch) | Done | New `.github/workflows/pre-release.yml`: pushing a pre-release tag (`git tag -a v0.2.0-rc.1 ...`) publishes a marked pre-release. A step-for-step mirror of release.yml on purpose: same checkout pin (3d3c42e # v7.0.1), same verify/libusb/build+ctest/GUI/package/checksum steps, same GITHUB_REF_NAME env-var doctrine, no environment gate (release.yml's release job has none either -- the `release` environment guards only the tap) and no timeout. The ruling that a pre-release never touches the tap required TWO changes, not one: (a) the new workflow simply has no update-tap job and holds no tap secret (a closing comment block marks the deliberate absence where the job would sit), but that alone would have been worthless; (b) release.yml's `on.push.tags: ["v*"]` ALSO matched `v0.2.0-rc.1` (`*` crosses the hyphen), so pushing an rc tag would have fired BOTH workflows, and the stable one would have published a full Release AND bumped the tap anyway -- release.yml is now `tags: ["v*", "!v*-*"]`, the documented include-then-exclude form for tags (a negative pattern after a positive match excludes the ref, and a negative must be accompanied by at least one positive pattern), so exactly one of the two workflows runs per tag push: the hyphen is semver's pre-release marker and doubles as the routing bit, verified with an fnmatch probe (`v0.2.0-rc.1` matches `v*` and `v*-*`; `v0.2.0` matches only `v*`). Four deliberate differences from release.yml, each commented in the file: (1) `gh release create --prerelease`, with the reason recorded there: GitHub never serves a marked pre-release from `releases/latest`, the endpoint UpdateChecker.swift queries, so the app's Check for Updates can never offer an rc build and the Latest badge stays on the stable release; (2) the tag/version verify step compares only the BASE version (`base_ver="${tag_ver%%-*}"`, everything before the first hyphen), because CMake's `project(VERSION)` rejects pre-release suffixes -- probed locally: `project(Probe VERSION 0.2.0-rc.1)` errors with `VERSION "0.2.0-rc.1" format invalid.` -- so `v0.2.0-rc.1` requires CMakeLists.txt to say `0.2.0` and fails fast otherwise (operational consequence: bump VERSION before cutting an rc; the suffix stays tag metadata, the binaries still report the base version); (3) the release notes drop the Homebrew section entirely (the tap does not carry this version, so pointing readers at it is a dead end) and state plainly that the tap is not updated, keeping the quarantine/xattr instructions, CLI, requirements, and the checksum table; (4) one accuracy improvement over the stable notes: only the CLI asset needs `brew install libusb` -- the .app embeds its own copy via build-gui.sh's `embed_libusb` (`@loader_path`, verified in gui/Scripts/build-gui.sh), so the asset route needs no extra install for the GUI. Verification, all local: actionlint clean on both files (its shellcheck integration active), YAML parse of both, all 9 `run:` blocks per file pass `bash -n`, and comments ASCII-only (the single non-ASCII char is the ellipsis in the retry echo copied verbatim from release.yml's operational line, not a comment). Committed and pushed as feat/pre-release-workflow. | | 65 | `fix(ci): unbreak the ledger table, pass step outputs as env` (this branch) | Done | Four review findings on row 64's work, all fixed in one commit. (1, must fix) Row 64's insertion anchored on `### Current state` and landed BELOW the blank line that already separated row 63 from that heading, so a blank line sat between rows 63 and 64 -- in GFM a blank line terminates a table, and row 64 rendered as a paragraph of pipes (the reviewer verified by rendering the region: one table of 64 rows, row 64 outside it). The blank line is deleted; rows 63 and 64 are contiguous and the blank line after row 64 still separates the table from the heading. Recorded as pitfall 29; the check is a structural scan of the table region (no blank line between the table header and the next heading, every row line starting with the pipe character). (2) Tagging an OLD commit bypasses the guard: on.push.tags workflows are resolved from the TAGGED commit, not from main, so an rc tag on a commit older than this change runs the OLD release.yml -- its tags: ["v*"] matches every v* tag -- and would publish a stable Release AND bump the tap anyway. Inherent to Actions and unfixable from the new file (old commits do not carry it), so it is an operating rule, stated next to the tag-cutting example in pre-release.yml's header: cut rc tags only from current main after this merges. (3) The new workflow cannot be exercised by PR CI (tag-only trigger; this fork has never run release.yml either -- zero runs, zero releases), so row 64's verification stays structural until a tag exists: after merge, cut one throwaway rc whose base matches CMakeLists.txt VERSION (v0.1.5-rc.1, or bump to 0.1.6 first) and confirm the Pre-release badge, no tap PR, Latest unchanged, assets and checksums present, and no stable Release run. (4) Step outputs were interpolated into run: text in BOTH files -- a row-44 doctrine violation, pre-existing in release.yml and copied verbatim into pre-release.yml. Both notes heredocs and both gh release create steps now take the five outputs through env (SRC_SHA256, PKG_NAME, PKG_SHA256, APP_NAME, APP_SHA256 -- the update-tap.yml FORMULA_URL/FORMULA_SHA256 pattern), the heredocs stay quoted, and the checksum rows are appended with printf so the values are arguments -- data, never shell text. Two wrong turns worth recording: the printf format was first single-quoted, and shellcheck's SC2016 (info) fired on the markdown backticks inside it -- actionlint exits 1 even on info findings, so that is not a cosmetic grade; the format is now double-quoted with escaped backticks (a literal backtick inside double quotes), which keeps the table's code spans. And the first printf attempt backticked BOTH columns; executing the notes step locally against the old heredoc caught that the Source-tarball row's label is plain text in the published table -- its format now leaves the label column plain while the asset-name rows keep both columns in code spans, and the assembled notes are byte-identical to the old output under identical inputs. The unquoted-heredoc alternative was rejected: the notes contain markdown backticks, which an unquoted heredoc evaluates as command substitution. Also fixed the count mismatch the reviewer flagged as a nit: pre-release.yml's header no longer says "two deliberate differences" (the step-level ones are commented at their steps). Verification, all local: actionlint clean on both files (exit 0, zero findings), YAML parse, all 9 run: blocks per file pass bash -n, no ${{ }} remains inside any run block of either file, and each notes step was executed locally with dummy env values and byte-compared against the old heredoc output. Committed on feat/pre-release-workflow, not pushed. | ### Current state -- **Tests**: all 23 ctest cases pass; on the GUI side `swift test` has 30 cases passing (TetherKitIPCTests 17 and TetherKitUIModelTests 13, including LocalizationTests and HelperConstantsTests). +- **Tests**: all 23 ctest cases pass; on the GUI side `swift test` has 30 cases passing (TethernetIPCTests 17 and TethernetUIModelTests 13, including LocalizationTests and HelperConstantsTests). - **Build**: zero warnings under `-Werror`; the CLI, shared library, and GUI products all build. - **CI**: legacy ci.yml split into tests.yml (five jobs migrated with their steps and checks unchanged, triggers PR + push main + Monday cron + ASan/UBSan), build.yml (manual trigger only, full build + artifacts) and lint.yml (clang-format gate + advisory clang-tidy); new codeql.yml (weekly + push main + every-PR C++ scanning), bench.yml (manual benchmarks) and dependabot.yml (weekly grouped action bumps); permissions: contents: read across tests/lint/build; Xcode selection filters beta; every external action is pinned to a full commit SHA with the release tag in a trailing comment (see ledger row 60 for why SHA rather than a major tag); releases split by tag shape: release.yml takes stable `v*` tags only, and pre-release.yml takes hyphenated `v*-*` tags and never touches the tap (row 64). -- **Runnable**: `--version` / `--help` / `--list` all work; starting without root prints a clear hint and returns exit code 1; `TetherKit.app` and `tetherkit-helper` both start after packaging; the privileged helper can be installed / uninstalled inside the App with one click (the install path has been exercised on real hardware); when the helper version does not match the App, the management row lights up "Update the helper" (it compares the **library** version, not Info.plist -- `brew upgrade` only replaces the .app, the copy installed under /Library does not follow; when the protocol revision did not change there was previously no prompt at all, and the only thing the user could notice was "the issue described as fixed in the release notes is still there"; the windows were screenshot-checked); English-only behavior is verified: CLI `--help` / `--list` / argument errors are correct, and switching the language in the GUI menu updates the main window and the App menu **immediately** (no restart required, screenshot-checked). +- **Runnable**: `--version` / `--help` / `--list` all work; starting without root prints a clear hint and returns exit code 1; `Tethernet.app` and `tethernet-helper` both start after packaging; the privileged helper can be installed / uninstalled inside the App with one click (the install path has been exercised on real hardware); when the helper version does not match the App, the management row lights up "Update the helper" (it compares the **library** version, not Info.plist -- `brew upgrade` only replaces the .app, the copy installed under /Library does not follow; when the protocol revision did not change there was previously no prompt at all, and the only thing the user could notice was "the issue described as fixed in the release notes is still there"; the windows were screenshot-checked); English-only behavior is verified: CLI `--help` / `--list` / argument errors are correct, and switching the language in the GUI menu updates the main window and the App menu **immediately** (no restart required, screenshot-checked). - **Verified**: the USB side works on a real RNDIS device (enumeration, interface claim, RNDIS handshake), and this USB side **does not require root**; the feth private ABI, the two private BPF ioctls, and the core assumption that "a BPF `write()` lets the peer IP stack receive a frame" have all been confirmed on real hardware. See section 6. - **Benchmarked** (2026-07-28, Android USB tethering): end-to-end iperf3 measured **RX 324-327 Mbps / TX 234-241 Mbps** (USB 2.0 High Speed, theoretical ceiling 426 Mbps); the full GUI-to-real-device link works (plug in -> connect -> obtain a DHCP address -> bidirectional ping). See section 6.4 and [docs/BENCHMARKS.md](docs/BENCHMARKS.md). - **Fixed**: the root cause of TX frame drops under high load has been identified -- the bridge layer dropped the rest of the batch immediately when the bulk OUT transfer pool was full, even though a slot would become available in a few hundred microseconds. After changing to wait: TX frame drops 1889 -> 0, TCP TX retransmits 3829 -> 0, bidirectional concurrent TX 4.8 -> 87 Mbps; Ctrl-C graceful shutdown was rechecked after the fix (printed "stopped", no feth residue). See section 6.5. @@ -319,7 +319,7 @@ How to reproduce: use `--list` to see recognition results; for interface-level d ### 6.2 Verified: feth / BPF side (**requires root**) -How to reproduce (3)(4)(7): `sudo TETHERKIT_ROOT_TESTS=1 build/bin/tetherkit_tests --test-suite=net.feth` (when that environment variable is not set, these cases **skip rather than fail**, and the skip reason is printed), or run `sudo build/bin/tetherkit-cli` directly once. (5)(6) are **not** in the test suite yet; they were measured with one-off probes (the method is described in each item; the gap for (6) was recorded in 6.3). +How to reproduce (3)(4)(7): `sudo TETHERNET_ROOT_TESTS=1 build/bin/tethernet_tests --test-suite=net.feth` (when that environment variable is not set, these cases **skip rather than fail**, and the skip reason is printed), or run `sudo build/bin/tethernet-cli` directly once. (5)(6) are **not** in the test suite yet; they were measured with one-off probes (the method is described in each item; the gap for (6) was recorded in 6.3). **(3) The private feth-pairing ABI works on macOS 26.** @@ -368,7 +368,7 @@ The original three items are **all complete**; conclusions are folded into 6.4: - [x] Android RNDIS quirk -> see "device-reported parameters" in 6.4. - [x] **Root cause** of TX frame drops -- identified and fixed, see 6.5. -- [x] Graceful shutdown after the fix -- in a real terminal, `sudo build/bin/tetherkit-cli` then Ctrl-C (SIGINT, sharing the same handler as SIGTERM / SIGHUP): printed "stopped", `ifconfig` showed no feth residue. **The newly added backpressure wait did not block the shutdown path.** Note this can only be verified in a real terminal; an elevation script swallows the signal (see pitfall 15 in section 7). +- [x] Graceful shutdown after the fix -- in a real terminal, `sudo build/bin/tethernet-cli` then Ctrl-C (SIGINT, sharing the same handler as SIGTERM / SIGHUP): printed "stopped", `ifconfig` showed no feth residue. **The newly added backpressure wait did not block the shutdown path.** Note this can only be verified in a real terminal; an elevation script swallows the signal (see pitfall 15 in section 7). Still unresolved: @@ -392,7 +392,7 @@ Warning: **the device reports a different MAC every session** (Android randomize **Throughput**: RX 326-327 Mbps (0 retransmits) / TX 239-302 Mbps (0 retransmits, after the fix). The full evidence chain, A/B data, and mechanism analysis are in the "End-to-end measurements on a real device" section of [docs/BENCHMARKS.md](docs/BENCHMARKS.md). -Warning -- **debugging trap**: the `rndis0` `rx_errs` counter in `/proc/net/dev` on the phone side is **unreliable** -- it records an error for every frame (50 pings all received replies, yet `rx_pkts` and `rx_errs` both increased by 51). When investigating TX issues, rely on iperf3's actual loss rate and TetherKit's own statistics. +Warning -- **debugging trap**: the `rndis0` `rx_errs` counter in `/proc/net/dev` on the phone side is **unreliable** -- it records an error for every frame (50 pings all received replies, yet `rx_pkts` and `rx_errs` both increased by 51). When investigating TX issues, rely on iperf3's actual loss rate and Tethernet's own statistics. ### 6.5 Fixed: TX frame drops under high load @@ -404,7 +404,7 @@ The old code did `AddDroppedFull(remaining) + break` when `SendFrames` returned | Metric | Before fix | After fix | |---|---:|---:| -| TetherKit's own TX frame drops | 1889 | **0** | +| Tethernet's own TX frame drops | 1889 | **0** | | TCP TX retransmits | 3829 | **0** | | Bidirectional concurrent TX | 4.8 Mbps | **87 Mbps** | | Unidirectional TCP RX (regression check) | 324-327 | 326-327 | @@ -428,7 +428,7 @@ Warning: two statistics miscounts were fixed along with this: oversized frames a 6. **BPF traversal must use `bh_hdrlen`**, not `sizeof(struct bpf_hdr)` -- the header is followed by alignment padding. 7. **libusb has no `detach_kernel_driver` on macOS**. If an interface is held by a kernel driver, the only options are a codeless kext / dext or re-enumeration; libusb cannot solve it. 8. **Do not assume a device or root is present when tests run**, so "it does not run" does not mean "the code is wrong"; everything verifiable offline must be covered by mock tests, and tests that need root must be able to skip cleanly. -9. **Never put quotes around arguments in ctest's `add_test`.** Writing `COMMAND tetherkit_tests --test-suite="${_suite}"` causes CMake to pass the quotes as part of the argument content (`ctest -V` shows `"--test-suite="foo""`), doctest matches no cases -> 0 cases run -> doctest returns exit code 0 for zero matches -> ctest reports Passed. **The entire test suite silently became a no-op while staying all green**, actually hiding 4 real failures. The correct form is `--test-suite=${_suite}` (no quotes). +9. **Never put quotes around arguments in ctest's `add_test`.** Writing `COMMAND tethernet_tests --test-suite="${_suite}"` causes CMake to pass the quotes as part of the argument content (`ctest -V` shows `"--test-suite="foo""`), doctest matches no cases -> 0 cases run -> doctest returns exit code 0 for zero matches -> ctest reports Passed. **The entire test suite silently became a no-op while staying all green**, actually hiding 4 real failures. The correct form is `--test-suite=${_suite}` (no quotes). 10. **When adding a "must hit at least one assertion" guard to a ctest case, use `FAIL_REGULAR_EXPRESSION`, not `PASS_REGULAR_EXPRESSION`.** The latter **replaces** exit-code evaluation (CMake's documented behavior); once set, a real assertion failure is ignored instead -- digging the previous pitfall right back open. `FAIL_` is additive on top of the exit code. 11. **Time-related classes must not read the clock in their constructor.** `PeriodicTimer` used to do that, which meant the instant read in the constructor was slightly later than the caller's `now`, so "now + period" had not yet elapsed and unit tests failed randomly and non-reproducibly. Changed to take the timing origin explicitly (the default value keeps the convenient form). 12. **Warning: synchronous libusb interrupt transfers block forever on macOS; the timeout parameter is not honored at all.** Once hit, the symptoms are a stuck control thread, no more statistics output, no response even to SIGTERM, and leftover feth interfaces on exit. The stack looks like this: @@ -440,14 +440,14 @@ Warning: two statistics miscounts were fixed along with this: oversized frames a 13. Also remember: `timeout = 0` means **wait forever** in libusb/darwin, not "do not wait". To "just probe", use a non-zero minimum value (`kProbeOnlyTimeoutMillis`), and as noted above this still does not work for interrupt endpoints. 14. **Without setting `BIOCSHDRCMPLT = 1`, a BPF `write()` to feth returns `ENXIO` ("Device not configured") directly -- it is not as mild as "the frame header gets rewritten".** A controlled experiment confirmed this: same code, same interface, the only difference was whether this ioctl was set -- without it `ENXIO` every time, with it `write` succeeds immediately. The original comment in `bpf_link.cc` only said `hdrcmplt=0` would "strip the first 14 bytes and rebuild the frame header"; that is the **semantics** of `hdrcmplt`. The **actual consequence** on feth is that the write does not work at all (`if_fake`'s output path cannot handle the `AF_UNSPEC` rebuild branch). When debugging `ENXIO` it is easy to assume the interface is not up or the name is wrong -- check this ioctl first. In ordering it must come before `BIOCSETIF`, and batch write (`BIOCSBATCHWRITE`) requires it to be 1. -15. **Warning: `osascript ... with administrator privileges` swallows inter-process SIGTERM delivery.** When using it for elevation testing, `kill -TERM $pid` inside the script returns 0, the target process's `blocked` and `pending` masks are both 0, and the handler installed by `sigaction` is present (sample shows the handler address is not `SIG_IGN`), yet **the signal never arrives** and the process can only be `kill -9`'d. A 15-line C program served as a control: plain shell OK, plain `osascript` OK, add `with administrator privileges` fails. -> Consequence: **do not use this elevation path to verify graceful shutdown**; it produces a false "shutdown hangs" failure that has nothing to do with the code under test (the run logs before and after the fix were all `Killed: 9`, which was once mistaken for a newly introduced regression). To verify end-to-end shutdown, run `sudo build/bin/tetherkit-cli` in a real terminal and press Ctrl-C -- that has been verified to print "stopped" with no feth residue. +15. **Warning: `osascript ... with administrator privileges` swallows inter-process SIGTERM delivery.** When using it for elevation testing, `kill -TERM $pid` inside the script returns 0, the target process's `blocked` and `pending` masks are both 0, and the handler installed by `sigaction` is present (sample shows the handler address is not `SIG_IGN`), yet **the signal never arrives** and the process can only be `kill -9`'d. A 15-line C program served as a control: plain shell OK, plain `osascript` OK, add `with administrator privileges` fails. -> Consequence: **do not use this elevation path to verify graceful shutdown**; it produces a false "shutdown hangs" failure that has nothing to do with the code under test (the run logs before and after the fix were all `Killed: 9`, which was once mistaken for a newly introduced regression). To verify end-to-end shutdown, run `sudo build/bin/tethernet-cli` in a real terminal and press Ctrl-C -- that has been verified to print "stopped" with no feth residue. 16. **A "deliberately done this way" comment can still be wrong -- especially when it sounds convincing.** The root cause of TX frame drops (section 6.5) hid for an entire release because that drop logic was accompanied by a confident comment: "waiting would let the kernel buffer pile up into invisible drops". Both premises were wrong: `bs_drop` is read back and shown in the statistics line on every batch; and the 4 MiB kernel buffer is exactly the level that should absorb bursts. -> Lesson: **when writing "deliberate", be able to state the magnitude**. Here, a quick calculation of "one 256-frame slice needs about 26 transfer slots while the pool has only 4" would have made the problem obvious immediately. When reviewing "deliberately / on purpose", first check its quantitative assumptions. -17. **For "best-effort" fields, an empty value must not overwrite a known good one.** The defect where the device name degraded to VID:PID after connecting was three layers deep: string descriptors cannot be read while the device is occupied (empty string), the GUI happened to refresh the device list once in the window between "connect clicked" and "status polled to running" (empty string overwrote good data), and the list froze after running (the degraded name was fixed for the rest of the session). The fix does not try to block the race window -- there is always a gap across an XPC round trip, and it cannot be blocked completely -- instead it makes empty strings stop appearing: `tk_list_devices` remembers the last successfully read strings for each device keyed by "bus + address + VID:PID" and backfills when a read comes up empty; when the device leaves, the memory is cleared (to prevent an old name from attaching to a new device that takes the same address). **"Could not get it this time" is not the same as "the device has no name"**; the same applies to all best-effort fields -- either the consumer falls back to the old value or the producer backfills, pick one, otherwise a transient failure becomes a persistent data degradation (pure logic lives in `ReconcileDeviceStrings`, with a unit test in capi.support). The helper's occupancy check was fixed along with this: the old "session is non-nil" rule was wrong in both directions -- during handshake the session is not registered yet (a concurrent enumeration would insert transfers into the control endpoint mid-handshake), while a failed/stopped dead session has already released the device (after unplug/replug the name could never be read again). Changed to "actually holds it": the sessionStarting flag + runState in {starting, running, stopping}. +17. **For "best-effort" fields, an empty value must not overwrite a known good one.** The defect where the device name degraded to VID:PID after connecting was three layers deep: string descriptors cannot be read while the device is occupied (empty string), the GUI happened to refresh the device list once in the window between "connect clicked" and "status polled to running" (empty string overwrote good data), and the list froze after running (the degraded name was fixed for the rest of the session). The fix does not try to block the race window -- there is always a gap across an XPC round trip, and it cannot be blocked completely -- instead it makes empty strings stop appearing: `tn_list_devices` remembers the last successfully read strings for each device keyed by "bus + address + VID:PID" and backfills when a read comes up empty; when the device leaves, the memory is cleared (to prevent an old name from attaching to a new device that takes the same address). **"Could not get it this time" is not the same as "the device has no name"**; the same applies to all best-effort fields -- either the consumer falls back to the old value or the producer backfills, pick one, otherwise a transient failure becomes a persistent data degradation (pure logic lives in `ReconcileDeviceStrings`, with a unit test in capi.support). The helper's occupancy check was fixed along with this: the old "session is non-nil" rule was wrong in both directions -- during handshake the session is not registered yet (a concurrent enumeration would insert transfers into the control endpoint mid-handshake), while a failed/stopped dead session has already released the device (after unplug/replug the name could never be read again). Changed to "actually holds it": the sessionStarting flag + runState in {starting, running, stopping}. 18. **SwiftUI does not know that "a global table lookup" has changed.** After moving strings to runtime lookup, changing the language does not make any `@Observable` property appear to change, so view bodies naturally do not recompute -- the UI just stays in the old language. The fix is to make the language change itself an observable value (`AppModel.languageRevision` increments on every switch) and use it as an identity: the main window and the menu-bar panel both attach `.id(model.languageRevision)` to force the entire tree to rebuild. **`.commands { }` is more subtle**: every `Button` label inside is its own independent expression, and in practice (verified by reading menu item names via AppleScript) after switching language only the language menu itself changed -- because it read `languagePreference` and therefore had a dependency -- while the neighboring "Check for Updates…" stayed in the old language. The fix is to wrap that group in **one View** and read revision once in its body so the whole group recomputes together (see `AppMenuItems`). A variant of the same problem: **a `static let` that stores text is evaluated only once** and never updates after a language switch. Any static member that stores text must be a computed property (the three authorization hints on `AppModel` and `NetworkCard.dnsHint` both hit this). -19. **On macOS 26, window chrome is determined by `LC_BUILD_VERSION`'s `sdk`.** Measured: a `swift build` product was linked by SwiftPM as `sdk 14.0` (`-target arm64-apple-macos14.0` folded into LC_BUILD_VERSION), so on an all-Tahoe machine it took **legacy chrome** -- the traffic lights were 14x16 pt button frames, 20 pt spacing, drawing only ~12 pt circles inside; by contrast Hand Mirror (minos 14.4 / sdk 26.5) and Dropover (minos 13.0 / sdk 27.0) both use 14x14 pt button frames, 23 pt spacing, full-size circles. All AppKit windows draw the same `_NSTheme*Widget` set; the difference is only SDK-gated. Fix: add `-platform_version macos 14.0.0 26.5.0` when linking the `TetherKitApp` target (gui/Package.swift), **keeping minos at 14.0 so only chrome is affected**. Every release package must be checked: `otool -l | rg -A4 LC_BUILD_VERSION` -- `sdk` must be >= 26. The main window was also changed to an AppKit-managed `NSWindow` (MainWindowManager): it is presented deterministically at startup and no longer depends on SwiftUI scene instantiation timing. Traffic-light style is independent of window ownership and is determined by the `sdk` above. `build-gui.sh`'s self-check now enforces this verification (LC_BUILD_VERSION sdk stamp >= 26); one packaging run verifies it. -20. **Window presentation for `.prohibited` / dev runs**: the main window no longer comes from a SwiftUI `Window` scene but is created and `makeKeyAndOrderFront`'d by the delegate in `applicationDidFinishLaunching` via `@NSApplicationDelegateAdaptor` -- a bare run still pops the window and no longer needs manual rescue from the menu bar; gatekeeping code such as `isWindowPresentationExpected` was removed entirely (window presentation is managed by MainWindowManager, and SwiftUI has no scene left to recreate on its own). The `.prohibited -> .regular` self-check in `TetherKitApplication.init()` is kept: it does not control window presentation, it controls normal activation semantics for an unactivated process. +19. **On macOS 26, window chrome is determined by `LC_BUILD_VERSION`'s `sdk`.** Measured: a `swift build` product was linked by SwiftPM as `sdk 14.0` (`-target arm64-apple-macos14.0` folded into LC_BUILD_VERSION), so on an all-Tahoe machine it took **legacy chrome** -- the traffic lights were 14x16 pt button frames, 20 pt spacing, drawing only ~12 pt circles inside; by contrast Hand Mirror (minos 14.4 / sdk 26.5) and Dropover (minos 13.0 / sdk 27.0) both use 14x14 pt button frames, 23 pt spacing, full-size circles. All AppKit windows draw the same `_NSTheme*Widget` set; the difference is only SDK-gated. Fix: add `-platform_version macos 14.0.0 26.5.0` when linking the `TethernetApp` target (gui/Package.swift), **keeping minos at 14.0 so only chrome is affected**. Every release package must be checked: `otool -l | rg -A4 LC_BUILD_VERSION` -- `sdk` must be >= 26. The main window was also changed to an AppKit-managed `NSWindow` (MainWindowManager): it is presented deterministically at startup and no longer depends on SwiftUI scene instantiation timing. Traffic-light style is independent of window ownership and is determined by the `sdk` above. `build-gui.sh`'s self-check now enforces this verification (LC_BUILD_VERSION sdk stamp >= 26); one packaging run verifies it. +20. **Window presentation for `.prohibited` / dev runs**: the main window no longer comes from a SwiftUI `Window` scene but is created and `makeKeyAndOrderFront`'d by the delegate in `applicationDidFinishLaunching` via `@NSApplicationDelegateAdaptor` -- a bare run still pops the window and no longer needs manual rescue from the menu bar; gatekeeping code such as `isWindowPresentationExpected` was removed entirely (window presentation is managed by MainWindowManager, and SwiftUI has no scene left to recreate on its own). The `.prohibited -> .regular` self-check in `TethernetApplication.init()` is kept: it does not control window presentation, it controls normal activation semantics for an unactivated process. 21. **SwiftPM pinning `sdk` to the deployment target is a systemic issue.** Any SwiftPM project built without `-platform_version` will be treated as a legacy app on Tahoe; Homebrew builds are affected the same way -- if a formula build omits that flag, users still get legacy traffic lights. 22. **macOS 26 wraps view updates from Button actions in an animated transaction (button activation context).** Clicking the sidebar to switch pages made the new page briefly scale up by about 8-10% and then settle, triggered only by clicks; the same state change driven by a timer was pixel-clean. `Transaction.disablesAnimations` only prevents new implicit `.animation` writes into the transaction and **does not suppress transitions** -- a transition interpolates only when the transaction is animated (objc.io's Transaction / transitions posts) -- so a page write wrapped in an activation transaction is always interpolated, and `.transition(.identity)` / an instant transaction only make the jump faster without removing the root cause. Fixed in two places (source comment "triple clamp"): `SidebarRow` switches to `InstantButtonStyle` (`PrimitiveButtonStyle`): `.transaction { animation = nil }` + `.contentShape(Rectangle)` + `.simultaneousGesture(TapGesture().onEnded { trigger() })`, so no activation context is created at all (call `trigger()` on the configuration, not `action()`); `SidebarShell.select` moves the state write to the next runloop tick (`DispatchQueue.main.async`) with an instant-transaction backstop. See "Why page switches have zero animation" in docs/GUI-ARCHITECTURE.md. @@ -456,8 +456,8 @@ Warning: two statistics miscounts were fixed along with this: oversized frames a 24. **macOS 26.5 discards every block scheduled onto an async queue inside an "accessibility press turn".** The five-page sidebar broke under VoiceOver: `AXUIElementPerformAction(kAXPressAction)` returned success, but the body of the block written inside `select()` via `DispatchQueue.main.async` never ran (`Task { @MainActor }` died the same way), and only synchronous writes in the same turn survived. Bisection (isolated repro) ruled out `InstantButtonStyle` (all four button styles worked fine in the isolated repro), the `isSelected` trait, `onHover`, and `@AppStorage` -- the "jump to the next runloop tick" itself was the cause. `NSApp.currentEvent == nil` at press time is the only reliable branching signal: `select()` now decides whether to defer based on event type (defer on mouse/keyboard turns as before; write directly on accessibility turns). Direct writes on keyboard turns have a second pitfall: re-rendering the page after the switch can re-dispatch the same key event, stepping twice on one key press -- so keyboard turns must also defer. The probe needs no permissions: a self-process `AXUIElementCreateApplication` traversal + `performAction` + AppStorage key readback can reproduce the loop. Live VO/FKA recheck (2026-11-07): arrow keys and `AXPress` activation work; Space/Return were confirmed as **real defects** (live presses had the same effect, ruling out synthetic-event artifacts). (Note: an early attempt had the sidebar container intercept Space/Return and call `activateFocusedRow`; that approach was completely reworked in Round 11: the row itself declares `.accessibilityAction(.default)` with a direct write, the window intercepts Return to claim it + row-level `.onKeyPress(.return)` as backstop.) Two rejected approaches (do not repeat): (1) giving each row button its own `.focusable()` traps keyboard events in the focus engine so the container's arrow-key handler never receives them and arrow keys stop working entirely; (2) programmatic focus via `@FocusState` is a silent no-op, and the AX readback shows no focus, so "ring follows selection" cannot be done that way. Another machine-level fact: with FKA on, arrow keys are taken over by the system focus engine (the "Move Focus" keys) and no in-app handler can intercept them -- without FKA the container arrow-key direct switch is unaffected. Implementation in `SidebarShell.rowSelect`, `SidebarRow`, and `SelfAccessibility`; the shared write/deferral exit is in `ButtonTurn.write`, and the LogCard level filter and clear button use the same path (clicking filter/clear no longer interpolates with an activation transaction). Round 6 (after three-way system-level investigation, the live activation pipeline was finalized): under real FKA, Space never reaches the app as an NSEvent -- universalaccessd intercepts Space and arrow keys in the HID layer via a CGEventTap, converts Space into an out-of-process `AXUIElementPerformAction` delivered to the focused element, while Return passes through unchanged; the synthetic probe injected NSEvents into `NSApp.sendEvent`, bypassing that tap entirely -- this is the root of the "all pass headless, all fail live" divergence. Three corresponding pipelines: (1) the row AXButton explicitly attaches `.accessibilityAction(.default)` (the AXPress landing point for Space), with the write taking the new `ButtonTurn.writeDirect` path -- in an out-of-process AX turn AppKit does not clear `NSApp.currentEvent`, so a stale event left over from the previous click can trick `write` into deferring, and 26.5 discards every async block scheduled inside an accessibility turn, making a direct write the only path that does not depend on turn detection; (2) Return / numpad Enter are intercepted in `SidebarMainWindow.performKeyEquivalent` (AppKit's key-equivalent phase runs before firstResponder dispatch and avoids installing/uninstalling a listener lifecycle), conditioned on the system-level AX focus resolving to a row title and the field editor not being active, otherwise passed through; (3) the focused row's own `.onKeyPress(.return)` is kept as a backstop (Round 11 finalization; the container handles only arrow keys). At the same time the `model.fullKeyboardAccess` gate was removed (AppModel's polling and property rolled back with it): `NSApp.isFullKeyboardAccessEnabled` reads the old preference `AppleKeyboardUIMode` (Keyboard > Keyboard navigation), not the FKA toggle in the Accessibility panel, so using it as a gate silently dead-ends users who have only the latter on; matching AX focus to itself is self-limiting. Rejected/deferred approaches: `.keyboardShortcut` is a window-global equivalent -- attaching it to multiple rows means only the first takes effect and it hijacks text input (rejected); reshaping the row `.accessibilityRole` into a radio group or unlocking FKA Tab traversal would change the AX structure (both the title-matching resolver and the probes depend on it), deferred for now; the NSEvent local monitor was replaced by the window subclass (the latter avoids install/uninstall); changing `InstantButtonStyle` back to `Button(configuration)` risks regressing arrow keys (measured in r5), left as-is. - Round 7 (two live FKA-off regressions + headless probe matrix finalized): (1) the true FKA value in the Accessibility panel is `CFPreferencesCopyAppValue("keyboardAccess Enabled", "com.apple.universalaccess")` (`SelfAccessibility.isFullKeyboardAccessOn`, read fresh each time); on the test machine `AppleKeyboardUIMode=2` (keyboard navigation on) and the panel toggle off coexisted for long periods, so both Space/Return key pipelines (the focused row's `.onKeyPress` and the window's `performKeyEquivalent`) are now gated on this true value -- when the panel toggle is off, AX focus on a row is just "residual from the last browse" (it lingers after clicking a row or touching it with Tab), and a bare Space/Return would silently re-activate that row (live symptom: pressing anywhere jumps to the Management page), so it must stay inert. The row's AXPress path has **no gate** (VoiceOver is independent of this toggle). (2) (exploration history / superseded -- the current arrow-key contract is Round 11 (2)): under keyboard navigation (`AppleKeyboardUIMode=2`), arrow keys did not reach the SwiftUI container because AppKit key navigation consumed them before firstResponder dispatch (live symptom: arrow keys only worked after clicking the sidebar once). Fixed by claiming bare arrow keys 125/126 in `SidebarMainWindow.performKeyEquivalent` (before the firstResponder phase), conditioned on FKA-off + first responder being "empty" -- nil (the normal SwiftUI window case; probe measured `NSHostingView.acceptsFirstResponder == false`, so the previous round's `initialFirstResponder` and the reassert in present were dead code and were removed) or the window/contentView itself; when a text view (log page and form editors), segmented control, or other real control holds focus, pass through as usual. Stepping matches `SidebarShell.stepSelection` and wraps; a claimed return is handled, so the container `.onKeyPress` does not step twice. (3) Warning -- modifier-flag trap: arrow-key keyDown events always carry the `.function` bit (NSModifierFlags' device-independent hardware-key marker, present even with no physical modifier held); treating the entire `deviceIndependentFlagsMask` as a chord prohibition silently rejects every bare arrow key (`TETHERKIT_FKA_OVERRIDE` override + PROBE print evidence: the key entered `performKeyEquivalent` but the arrow branch never ran); the chord prohibition must be the explicit set `[.capsLock, .shift, .control, .option, .command]`. (4) Headless verification matrix (injecting into `NSApp.sendEvent`, which cannot bypass the real HID tap, so the final verdict on real-FKA-on Space/arrow keys remains on live hardware): with UA off, bare down/up arrow keys switch pages immediately (including wrap-around), bare Space/Return and "Tab-residual focus + Space/Return" are all inert, AXPress on a non-selected row switches correctly, all three keys pass through when an inserted NSTextView or segmented control holds focus, and Command chords pass through; with forced UA-on (DEBUG override of the true value), row activation switches to the focused row, Return correctly claims and switches on the focused row, and text views are safe; synthetic arrow keys still switching directly through the container under forced UA is a headless hybrid state (under real FKA-on the HID layer has already eaten the NSEvent and it does not happen), not a defect. The probe scaffolding AxProbe.swift was removed after Round 7 verification; the DEBUG fkaOverride is kept as a regression guard. - Round 8 (focus acknowledgement, Tab ownership, and picker wrap-up): (1) the only supported in-process channel for moving AX focus is SwiftUI state wrappers: `.focused` / `.accessibilityFocused` bound per row, glued with `onChange(of: focusedRow)` (writing the latter emits `kAXFocusedUIElementChanged`); calling `AXUIElementSetAttributeValue` on your own `kAXFocusedUIElement` from the outside is a confirmed no-op (err=0, focus still nil, measured by probe). (2) (exploration history / superseded): Round 8 attempted to self-heal by re-reading the system answer after poll-driven re-renders via `.onChange(of: model.status.monotonicNanos)` and an involvement gate. That approach was fully retired in the Round 11 native architecture (the self-heal mechanisms were removed; focus is now carried entirely by SwiftUI's native engine and window arrow-key self-healing). (3) the row AXPress is carried by `.accessibilityAction(.default)`; the probe machine proved "stimulus lands on = focused row"; the real HID path remains on the hardware checklist (the universalaccessd division of labor finalized in round 7 is unchanged). (4) a segmented Picker does not respond to keys under keyboard navigation (`AppleKeyboardUIMode=2`) or real FKA (r8-B); the `.menu` pop-up style is the keyboard-operable orthodox replacement; the popped menu runs in `NSEventTrackingRunLoopMode`, so `NSApp.sendEvent` / firstResponder / local event monitors cannot reach it and a headless environment cannot synthesize open-menu/submit -- menu existence was evidenced by a CGWindowList window diff (layer>0, own pid), AXMenu counts are polluted by the menu-bar baseline; writing a value to `AXPopUpButton` is a ghost success (err=0, `valueSettable=false`, value unchanged). (5) FKA-off Tab ownership: r8 used `.focusable(false)`, later reworked in r9/r11. (6) `ButtonTurn` moved into TetherKitUIModel (executable targets cannot be imported by `swift test`), and the commit semantics of activation escape have 6 unit tests (TetherKitUIModelTests/ButtonTurnTests). + Round 7 (two live FKA-off regressions + headless probe matrix finalized): (1) the true FKA value in the Accessibility panel is `CFPreferencesCopyAppValue("keyboardAccess Enabled", "com.apple.universalaccess")` (`SelfAccessibility.isFullKeyboardAccessOn`, read fresh each time); on the test machine `AppleKeyboardUIMode=2` (keyboard navigation on) and the panel toggle off coexisted for long periods, so both Space/Return key pipelines (the focused row's `.onKeyPress` and the window's `performKeyEquivalent`) are now gated on this true value -- when the panel toggle is off, AX focus on a row is just "residual from the last browse" (it lingers after clicking a row or touching it with Tab), and a bare Space/Return would silently re-activate that row (live symptom: pressing anywhere jumps to the Management page), so it must stay inert. The row's AXPress path has **no gate** (VoiceOver is independent of this toggle). (2) (exploration history / superseded -- the current arrow-key contract is Round 11 (2)): under keyboard navigation (`AppleKeyboardUIMode=2`), arrow keys did not reach the SwiftUI container because AppKit key navigation consumed them before firstResponder dispatch (live symptom: arrow keys only worked after clicking the sidebar once). Fixed by claiming bare arrow keys 125/126 in `SidebarMainWindow.performKeyEquivalent` (before the firstResponder phase), conditioned on FKA-off + first responder being "empty" -- nil (the normal SwiftUI window case; probe measured `NSHostingView.acceptsFirstResponder == false`, so the previous round's `initialFirstResponder` and the reassert in present were dead code and were removed) or the window/contentView itself; when a text view (log page and form editors), segmented control, or other real control holds focus, pass through as usual. Stepping matches `SidebarShell.stepSelection` and wraps; a claimed return is handled, so the container `.onKeyPress` does not step twice. (3) Warning -- modifier-flag trap: arrow-key keyDown events always carry the `.function` bit (NSModifierFlags' device-independent hardware-key marker, present even with no physical modifier held); treating the entire `deviceIndependentFlagsMask` as a chord prohibition silently rejects every bare arrow key (`TETHERNET_FKA_OVERRIDE` override + PROBE print evidence: the key entered `performKeyEquivalent` but the arrow branch never ran); the chord prohibition must be the explicit set `[.capsLock, .shift, .control, .option, .command]`. (4) Headless verification matrix (injecting into `NSApp.sendEvent`, which cannot bypass the real HID tap, so the final verdict on real-FKA-on Space/arrow keys remains on live hardware): with UA off, bare down/up arrow keys switch pages immediately (including wrap-around), bare Space/Return and "Tab-residual focus + Space/Return" are all inert, AXPress on a non-selected row switches correctly, all three keys pass through when an inserted NSTextView or segmented control holds focus, and Command chords pass through; with forced UA-on (DEBUG override of the true value), row activation switches to the focused row, Return correctly claims and switches on the focused row, and text views are safe; synthetic arrow keys still switching directly through the container under forced UA is a headless hybrid state (under real FKA-on the HID layer has already eaten the NSEvent and it does not happen), not a defect. The probe scaffolding AxProbe.swift was removed after Round 7 verification; the DEBUG fkaOverride is kept as a regression guard. + Round 8 (focus acknowledgement, Tab ownership, and picker wrap-up): (1) the only supported in-process channel for moving AX focus is SwiftUI state wrappers: `.focused` / `.accessibilityFocused` bound per row, glued with `onChange(of: focusedRow)` (writing the latter emits `kAXFocusedUIElementChanged`); calling `AXUIElementSetAttributeValue` on your own `kAXFocusedUIElement` from the outside is a confirmed no-op (err=0, focus still nil, measured by probe). (2) (exploration history / superseded): Round 8 attempted to self-heal by re-reading the system answer after poll-driven re-renders via `.onChange(of: model.status.monotonicNanos)` and an involvement gate. That approach was fully retired in the Round 11 native architecture (the self-heal mechanisms were removed; focus is now carried entirely by SwiftUI's native engine and window arrow-key self-healing). (3) the row AXPress is carried by `.accessibilityAction(.default)`; the probe machine proved "stimulus lands on = focused row"; the real HID path remains on the hardware checklist (the universalaccessd division of labor finalized in round 7 is unchanged). (4) a segmented Picker does not respond to keys under keyboard navigation (`AppleKeyboardUIMode=2`) or real FKA (r8-B); the `.menu` pop-up style is the keyboard-operable orthodox replacement; the popped menu runs in `NSEventTrackingRunLoopMode`, so `NSApp.sendEvent` / firstResponder / local event monitors cannot reach it and a headless environment cannot synthesize open-menu/submit -- menu existence was evidenced by a CGWindowList window diff (layer>0, own pid), AXMenu counts are polluted by the menu-bar baseline; writing a value to `AXPopUpButton` is a ghost success (err=0, `valueSettable=false`, value unchanged). (5) FKA-off Tab ownership: r8 used `.focusable(false)`, later reworked in r9/r11. (6) `ButtonTurn` moved into TethernetUIModel (executable targets cannot be imported by `swift test`), and the commit semantics of activation escape have 6 unit tests (TethernetUIModelTests/ButtonTurnTests). Round 9 (live recheck overturned r8's FKA-off Tab contract): (1) under keyboard navigation the Tab cycle originally did not enter the sidebar (row `.focusable(false)`), so as long as any control held focus, the "focus is empty/window itself" precondition for the window arrow-key claim failed -> Tab could not reach the sidebar and arrow keys were stranded (user live report). The contract reversed: row `.focusable(true)` became constant -- the r8 probe-matrix expectation "Tab never lands on a row" is obsolete from this point and the probe matrix must update its expectations first; FKA-off inertness no longer comes from the modifier chain but from activation-action branching: inside the FKA-off gate, native activation driven by the keyboard (live keyDown) is discarded and mouse events pass through, distinguished by `NSApp.currentEvent` type. Warning: `currentEvent` is read in two places (this gate and `ButtonTurn.write`'s defer decision), both only as a negative/branching signal -- an AXPress turn can leave a stale old event behind (this pitfall round 6), so do not use it as a positive predicate. (2) Return live-failure follow-up: the AX self-read claimed by the window's `performKeyEquivalent` could resolve a row in the synthetic probe but could not resolve one under the user's live FKA -> Return silently fell through. Added the focused row's own `.onKeyPress(.return)` (FKA-on gate, calling `rowSelect` directly), with zero AX-read dependency -- a headless environment cannot bypass the HID layer, so which link in this chain breaks on live hardware can only be verified on live hardware; the two claim levels are first-to-consume, and at worst the duplicate is an idempotent page switch. Numpad Enter (keycode 76) is still covered only by the window claim (SwiftUI `KeyPress.Key` has no numpad variant). Round 10 (self-driven keyboard-ring exploration, superseded in Round 11): Warning: Round 10 explored claiming bare Tab at the AppKit window layer, maintaining a self-driven keyboard ring in `AppModel.sidebarRingRow`, and using `SidebarFocusBridge` for probe sampling. That design was proven over-engineered and blocked the normal focus order in Round 11 and has been completely removed; no residue such as `sidebarRingRow` remains in the code, do not reintroduce it. Round 11 (final architecture: native focus-cycle traversal and arrow-key activation in all states): (1) Root cause identified: the dead end that kept the probe from entering the sidebar was the `.focusable(SelfAccessibility.isFullKeyboardAccessOn)` bound on the `rows` container (which marked the whole group unfocusable when FKA-off); focusable removed from the container, only the selected row declares `.focusable(isSelected)` when FKA-off (all rows focusable when FKA-on). The Tab key now forms a "selected sidebar row <-> content buttons" bidirectional native Tab cycle directly. (2) Arrow-key activation: the `handleArrowKey` guard was changed to pass through for text editing (cursor movement) only when `isFirstResponderEditableText` is true; when a button/card/scroll area has focus, arrow keys step pages directly at any time, page switches self-heal focus, and the user is never stuck on the current page. The container `.onKeyPress` keeps up/down arrow keys as a backstop. With FKA-on, arrow keys are taken over by the system focus engine (the HID-layer "Move Focus" keys), so neither the window claim nor the container backstop receives the events; arrow keys move key focus itself -- expected behavior, not a defect. (3) Return claim and row-level backstop: when the first responder is not a field editor, the window's `performKeyEquivalent` attempts an in-process system-level AX focus resolution (requires TCC trust); if untrusted or no row is resolved, the event falls through to the focused row's own `.onKeyPress(.return)` backstop to switch pages. (4) Explicit focus highlight: when any row has focus and the window is active (`controlActiveState == .key`) it shows an accent border and a light-gray background; when the window loses focus to the background the border is removed automatically. The selection follows arrow-key movement in sync. @@ -511,26 +511,26 @@ Warning: two statistics miscounts were fixed along with this: oversized frames a ```bash # Configure + build (warnings as errors) -cmake -S . -B build -DTETHERKIT_WARNINGS_AS_ERRORS=ON && cmake --build build -j10 +cmake -S . -B build -DTETHERNET_WARNINGS_AS_ERRORS=ON && cmake --build build -j10 # Run tests ctest --test-dir build --output-on-failure # Verify the lock-free queue with ThreadSanitizer (important!) -cmake -S . -B build-tsan -DTETHERKIT_ENABLE_TSAN=ON && cmake --build build-tsan -j10 \ +cmake -S . -B build-tsan -DTETHERNET_ENABLE_TSAN=ON && cmake --build build-tsan -j10 \ && ctest --test-dir build-tsan --output-on-failure # Performance benchmarks (be sure to use a Release build with no sanitizers) cmake -S . -B build-rel -DCMAKE_BUILD_TYPE=Release && cmake --build build-rel -j10 \ - && ./build-rel/bin/tetherkit_bench + && ./build-rel/bin/tethernet_bench # GUI: build, test, package -TETHERKIT_LIB_DIR=$PWD/build/lib swift build --package-path gui -TETHERKIT_LIB_DIR=$PWD/build/lib swift test --package-path gui +TETHERNET_LIB_DIR=$PWD/build/lib swift build --package-path gui +TETHERNET_LIB_DIR=$PWD/build/lib swift test --package-path gui ./gui/Scripts/build-gui.sh # Spot-check language behavior (run after changing user-facing strings) -./build/bin/tetherkit-cli --lang en --help -./build/bin/tetherkit-cli --lang en --list +./build/bin/tethernet-cli --lang en --help +./build/bin/tethernet-cli --lang en --list ctest --test-dir build -R common.i18n --output-on-failure ``` diff --git a/CMakeLists.txt b/CMakeLists.txt index e3376da..7e1825f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,8 @@ cmake_minimum_required(VERSION 3.24) project( - TetherKit - VERSION 0.1.5 + Tethernet + VERSION 0.1.0 DESCRIPTION "macOS userspace RNDIS driver: libusb + feth + BPF" LANGUAGES CXX) @@ -13,7 +13,7 @@ project( # and Darwin BPF; it cannot be portable. # ------------------------------------------------------------------------------ if(NOT APPLE) - message(FATAL_ERROR "TetherKit supports macOS only: it depends on feth (if_fake) and Darwin BPF.") + message(FATAL_ERROR "Tethernet supports macOS only: it depends on feth (if_fake) and Darwin BPF.") endif() # ------------------------------------------------------------------------------ @@ -37,14 +37,14 @@ set(CMAKE_CXX_EXTENSIONS OFF) # * But the 13.3 floor must be enforced: libc++'s std::to_chars (the # underlying implementation of std::format's float formatting) carries # availability annotations, and anything lower simply fails to compile. -set(TETHERKIT_MIN_MACOS_VERSION "13.3") +set(TETHERNET_MIN_MACOS_VERSION "13.3") if(CMAKE_OSX_DEPLOYMENT_TARGET AND CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS - TETHERKIT_MIN_MACOS_VERSION) + TETHERNET_MIN_MACOS_VERSION) message( FATAL_ERROR "CMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} is too low; " - "at least ${TETHERKIT_MIN_MACOS_VERSION} is required (libc++ std::to_chars carries availability annotations).") + "at least ${TETHERNET_MIN_MACOS_VERSION} is required (libc++ std::to_chars carries availability annotations).") endif() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) @@ -59,7 +59,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Position-independent code, globally. # # The feature modules are all static libraries, but they end up linked into -# the shared lib libtetherkit -- non-PIC object files cannot link into a +# the shared lib libtethernet -- non-PIC object files cannot link into a # dylib. On macOS/clang, PIC is the default anyway; spelling it out here keeps # the "why is PIC mandatory" reasoning documented in the build script itself. set(CMAKE_POSITION_INDEPENDENT_CODE ON) @@ -74,15 +74,15 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # ------------------------------------------------------------------------------ # Build options # ------------------------------------------------------------------------------ -option(TETHERKIT_BUILD_TESTS "Build unit tests" ON) -option(TETHERKIT_BUILD_BENCHMARKS "Build performance benchmarks" ON) -option(TETHERKIT_BUILD_SHARED_LIB "Build shared lib libtetherkit (needed by the GUI and other language bindings)" ON) -option(TETHERKIT_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF) -option(TETHERKIT_NATIVE_ARCH "Optimize for the host CPU microarchitecture (-mcpu=native; binary not portable)" OFF) -option(TETHERKIT_ENABLE_LTO "Enable link-time optimization (Release-style builds)" OFF) -option(TETHERKIT_ENABLE_ASAN "Enable AddressSanitizer" OFF) -option(TETHERKIT_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF) -option(TETHERKIT_ENABLE_TSAN "Enable ThreadSanitizer (validates the lock-free queues and thread model)" OFF) +option(TETHERNET_BUILD_TESTS "Build unit tests" ON) +option(TETHERNET_BUILD_BENCHMARKS "Build performance benchmarks" ON) +option(TETHERNET_BUILD_SHARED_LIB "Build shared lib libtethernet (needed by the GUI and other language bindings)" ON) +option(TETHERNET_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF) +option(TETHERNET_NATIVE_ARCH "Optimize for the host CPU microarchitecture (-mcpu=native; binary not portable)" OFF) +option(TETHERNET_ENABLE_LTO "Enable link-time optimization (Release-style builds)" OFF) +option(TETHERNET_ENABLE_ASAN "Enable AddressSanitizer" OFF) +option(TETHERNET_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF) +option(TETHERNET_ENABLE_TSAN "Enable ThreadSanitizer (validates the lock-free queues and thread model)" OFF) include(CompilerWarnings) include(Sanitizers) @@ -101,12 +101,12 @@ find_package(LibUSB REQUIRED) add_subdirectory(third_party) add_subdirectory(src) -if(TETHERKIT_BUILD_TESTS) +if(TETHERNET_BUILD_TESTS) enable_testing() add_subdirectory(tests) endif() -if(TETHERKIT_BUILD_BENCHMARKS) +if(TETHERNET_BUILD_BENCHMARKS) add_subdirectory(benchmarks) endif() @@ -123,17 +123,17 @@ endif() # needs the Xcode toolchain, and someone who only wants the command line should # not be forced to install it. # ------------------------------------------------------------------------------ -if(TETHERKIT_BUILD_SHARED_LIB AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gui/Package.swift") +if(TETHERNET_BUILD_SHARED_LIB AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gui/Package.swift") # Pass the current build directory to the script explicitly (by default it - # looks for /build), so libtetherkit is also found correctly in + # looks for /build), so libtethernet is also found correctly in # non-default build dirs such as build-rel / build-tsan. add_custom_target( gui - COMMAND "${CMAKE_COMMAND}" -E env "TETHERKIT_BUILD_DIR=${CMAKE_BINARY_DIR}" + COMMAND "${CMAKE_COMMAND}" -E env "TETHERNET_BUILD_DIR=${CMAKE_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/gui/Scripts/build-gui.sh" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" - DEPENDS tetherkit_shared - COMMENT "Building TetherKit.app and tetherkit-helper" + DEPENDS tethernet_shared + COMMENT "Building Tethernet.app and tethernet-helper" USES_TERMINAL VERBATIM) set_property(TARGET gui APPEND PROPERTY @@ -144,21 +144,21 @@ endif() # Build configuration summary # ------------------------------------------------------------------------------ message(STATUS "") -message(STATUS "==== TetherKit ${PROJECT_VERSION} build configuration ====") +message(STATUS "==== Tethernet ${PROJECT_VERSION} build configuration ====") message(STATUS " Build type : ${CMAKE_BUILD_TYPE}") message(STATUS " C++ standard : C++${CMAKE_CXX_STANDARD}") if(CMAKE_OSX_DEPLOYMENT_TARGET) - set(_tk_deploy "macOS ${CMAKE_OSX_DEPLOYMENT_TARGET}") + set(_tn_deploy "macOS ${CMAKE_OSX_DEPLOYMENT_TARGET}") else() - set(_tk_deploy "host default") + set(_tn_deploy "host default") endif() -message(STATUS " Deployment target : ${_tk_deploy} (floor ${TETHERKIT_MIN_MACOS_VERSION})") +message(STATUS " Deployment target : ${_tn_deploy} (floor ${TETHERNET_MIN_MACOS_VERSION})") message(STATUS " Compiler : ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}") message(STATUS " libusb : ${LibUSB_VERSION} (${LibUSB_LIBRARY})") -message(STATUS " Unit tests : ${TETHERKIT_BUILD_TESTS}") -message(STATUS " Benchmarks : ${TETHERKIT_BUILD_BENCHMARKS}") -message(STATUS " Host microarch : ${TETHERKIT_NATIVE_ARCH}") -message(STATUS " LTO : ${TETHERKIT_ENABLE_LTO}") -message(STATUS " ASan/UBSan/TSan : ${TETHERKIT_ENABLE_ASAN}/${TETHERKIT_ENABLE_UBSAN}/${TETHERKIT_ENABLE_TSAN}") +message(STATUS " Unit tests : ${TETHERNET_BUILD_TESTS}") +message(STATUS " Benchmarks : ${TETHERNET_BUILD_BENCHMARKS}") +message(STATUS " Host microarch : ${TETHERNET_NATIVE_ARCH}") +message(STATUS " LTO : ${TETHERNET_ENABLE_LTO}") +message(STATUS " ASan/UBSan/TSan : ${TETHERNET_ENABLE_ASAN}/${TETHERNET_ENABLE_UBSAN}/${TETHERNET_ENABLE_TSAN}") message(STATUS "=========================================") message(STATUS "") diff --git a/LICENSE b/LICENSE index 7ff0238..cde6f8b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2026 TetherKit contributors +Copyright (c) 2026 Tethernet contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 31f55ad..c910755 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,12 @@ -

- TetherKit icon -

- -# TetherKit +# Tethernet **A kext-free HoRNDIS alternative that brings Android USB tethering on macOS back to life.** -TetherKit is a **user-space RNDIS driver for macOS**: it turns an RNDIS device (Android USB +Tethernet is a **user-space RNDIS driver for macOS**: it turns an RNDIS device (Android USB tethering, Windows Phone, embedded Linux gadgets, …) into a network interface that macOS can see and use — with **no kernel extension, no DriverKit, no SIP changes and no developer account**. Works the same on Apple Silicon and Intel. Ships with a GUI — pick a device, -connect, configure IP in three clicks — plus a command-line tool, `tetherkit-cli`. - -![TetherKit main window](docs/assets/screenshot-main-en.jpg) +connect, configure IP in three clicks — plus a command-line tool, `tethernet-cli`. - **USB side**: asynchronous bulk transfers via [libusb](https://libusb.info/), with a complete host-side RNDIS state machine. @@ -33,12 +27,12 @@ connect, configure IP in three clicks — plus a command-line tool, `tetherkit-c If you landed here because **HoRNDIS stopped working after a macOS upgrade**, or because it shows up as blocked/"Disabled Software" with no *Allow* button on your Apple Silicon Mac — -that is what TetherKit is for. +that is what Tethernet is for. [HoRNDIS](https://github.com/jwise/HoRNDIS) was the answer for years, and it is good software. But it is a **kernel extension**, and that is the part macOS keeps tightening: -| | HoRNDIS | TetherKit | +| | HoRNDIS | Tethernet | |---|---|---| | Kind of software | Kernel extension (kext) | Plain user-space program | | Apple Silicon | Needs *Reduced Security* set in Recovery + a reboot before a third-party kext can load at all | Nothing to change — it is not a driver as far as the OS is concerned | @@ -47,7 +41,7 @@ But it is a **kernel extension**, and that is the part macOS keeps tightening: | Worst case failure | Kernel panic | The process exits | | Upstream status | Latest release 9.2 (Aug 2024); ["MacOS Sonoma not supported"](https://github.com/jwise/HoRNDIS/issues/169) still open | Actively developed | -TetherKit reaches the same goal from the other side: instead of teaching the kernel to speak +Tethernet reaches the same goal from the other side: instead of teaching the kernel to speak RNDIS, it speaks RNDIS **in user space** over libusb, and hands the resulting Ethernet frames to macOS through a `feth` virtual interface pair. The kernel never loads any of our code. @@ -64,7 +58,7 @@ The macOS kernel has **no** RNDIS driver. Plug in an Android phone with USB teth and no new interface shows up. Existing approaches either ship a kext (requires weakening SIP or Apple Silicon's security policy, a high signing bar, and breaks easily on system updates) or go through a Network Extension (requires a developer account plus system-extension -approval). TetherKit takes a third route: **act as both the USB host and the network driver, +approval). Tethernet takes a third route: **act as both the USB host and the network driver, entirely in user space**. --- @@ -76,44 +70,34 @@ Intel. The command-line tool goes back to macOS 13.3 Ventura. Nothing needs to b SIP stays on, the Apple Silicon security policy stays at *Full Security*. ```bash -# The GUI, TetherKit.app (macOS 14+) -brew install justsrc/tap/tetherkit +# The GUI, Tethernet.app (macOS 14+) +brew install justsrc/tap/tethernet -# The command-line tool, tetherkit-cli (macOS 13.3+) -brew install justsrc/tap/tetherkit-cli +# The command-line tool, tethernet-cli (macOS 13.3+) +brew install justsrc/tap/tethernet-cli ``` Both formulae build from source and pull in `libusb` automatically. The tap lives at [justsrc/homebrew-tap](https://github.com/justsrc/homebrew-tap). Launch the GUI like this (on first launch the app drops a Finder alias into -/Applications, so Spotlight can find and launch TetherKit from then on): +/Applications, so Spotlight can find and launch Tethernet from then on): ```bash -open "$(brew --prefix)/opt/tetherkit/TetherKit.app" +open "$(brew --prefix)/opt/tethernet/Tethernet.app" ``` On first run the app walks you through installing the privileged helper — one click, one admin password prompt. -To upgrade: - -```bash -brew upgrade tetherkit tetherkit-cli -``` - -> **Note for existing users**: as of v0.1.2 the formula name `tetherkit` belongs -> to the GUI; the CLI was renamed `tetherkit-cli` (binary included). If you had -> the CLI installed, run `brew uninstall tetherkit && brew install tetherkit-cli`. - You can also grab prebuilt artifacts straight from -[Releases](https://github.com/justsrc/TetherKit/releases) (arm64 only). They are +[Releases](https://github.com/justsrc/Tethernet/releases) (arm64 only). They are **unsigned**, so a browser download gets quarantined by Gatekeeper and you have to clear the attribute yourself: ```bash -xattr -d com.apple.quarantine tetherkit-cli # CLI -xattr -dr com.apple.quarantine TetherKit.app # GUI (recursive) +xattr -d com.apple.quarantine tethernet-cli # CLI +xattr -dr com.apple.quarantine Tethernet.app # GUI (recursive) ``` If that bothers you, use Homebrew above, or build it yourself as described in @@ -124,11 +108,11 @@ quarantine attribute. ## Graphical interface -TetherKit.app (SwiftUI) reduces the whole flow to three clicks — pick a device, +Tethernet.app (SwiftUI) reduces the whole flow to three clicks — pick a device, connect, configure IP — and shows live throughput and logs. -It comes in two pieces: `TetherKit.app` runs as a **normal user**, and anything -that needs root is handed to `tetherkit-helper`, a privileged component launched +It comes in two pieces: `Tethernet.app` runs as a **normal user**, and anything +that needs root is handed to `tethernet-helper`, a privileged component launched on demand by launchd. Every privileged call carries an authorization credential the user has just confirmed. The app itself needs no entitlements. @@ -161,16 +145,16 @@ primary service on its own. for a compact panel with shortcuts back to the main window. The connection itself lives in the privileged helper, so quitting the UI never drops it. -**Update check**: "TetherKit → Check for Updates…" checks on demand; the app also +**Update check**: "Tethernet → Check for Updates…" checks on demand; the app also checks once a day on its own (it only hits GitHub's public Releases API and reports nothing), lighting up a notice on the Manage page when a new version is out. The update itself still goes through `brew upgrade` or a source rebuild — with certificate-free distribution, auto-replacing the .app would be blocked by Gatekeeper, so the app deliberately only checks, never swaps. To disable the automatic check: -`defaults write com.tetherkit.app updateCheckDisabled -bool YES`. +`defaults write com.tethernet.app updateCheckDisabled -bool YES`. -**Interface language**: "TetherKit → Language" offers Follow system / English, +**Interface language**: "Tethernet → Language" offers Follow system / English, and the menu bar panel carries the same switch. Changes take effect **immediately** — no restart — and the log lines coming out of the library follow along (the language is pushed to the privileged helper too). The default @@ -183,22 +167,22 @@ trade-offs are in [docs/GUI-ARCHITECTURE.md](docs/GUI-ARCHITECTURE.md). ## Command-line tool -Besides the GUI there is a command-line tool, `tetherkit-cli` (installation above): +Besides the GUI there is a command-line tool, `tethernet-cli` (installation above): ```bash # First check whether the device is recognized (**no root needed**) -tetherkit-cli --list +tethernet-cli --list # Start the driver -sudo tetherkit-cli +sudo tethernet-cli # In another terminal, configure the new interface (RNDIS devices usually run a DHCP server) sudo ipconfig set feth0 DHCP ipconfig getifaddr feth0 ``` -> The commands above assume an installed `tetherkit-cli` (on your `PATH`). If you -> built from source, substitute `./build/bin/tetherkit-cli`. +> The commands above assume an installed `tethernet-cli` (on your `PATH`). If you +> built from source, substitute `./build/bin/tethernet-cli`. On a successful start the program prints the interface it created along with the follow-up commands. `Ctrl-C` shuts down gracefully (the device is taken out of RNDIS first, then the @@ -215,18 +199,18 @@ Common options (see `--help` for the full list): | `--max-transfer-kb` | The main throughput tuning knob — see [docs/PERFORMANCE.md](docs/PERFORMANCE.md) | | `--lang en\|auto` | Interface language, default `auto` (see below) | -**Language**: by default the tool checks `TETHERKIT_LANG`, `LC_ALL`, +**Language**: by default the tool checks `TETHERNET_LANG`, `LC_ALL`, `LC_MESSAGES` and `LANG` in that order and takes the first non-empty value. English is the only language shipped today, so `en` and `auto` both select it and any other value is rejected with an error. ```bash -tetherkit-cli --lang en --help # pick explicitly -TETHERKIT_LANG=en tetherkit-cli --list +tethernet-cli --lang en --help # pick explicitly +TETHERNET_LANG=en tethernet-cli --list ``` > ⚠️ Whether `sudo` passes `LANG` through depends on your sudoers `env_keep`, so -> `sudo tetherkit-cli` may not see the same environment as your terminal — pass +> `sudo tethernet-cli` may not see the same environment as your terminal — pass > `--lang` explicitly in that case. ### Why root is needed @@ -259,17 +243,17 @@ In other words, root is a requirement of the interface side, not the USB side **Does Android USB tethering work on macOS at all?** Not out of the box — macOS ships no RNDIS driver, so an Android phone in USB tethering mode -simply produces no network interface. That is what TetherKit adds. +simply produces no network interface. That is what Tethernet adds. **Do I have to disable SIP, or lower the security policy on Apple Silicon?** -No. Those are requirements of *kernel extensions*. TetherKit is an ordinary program; the +No. Those are requirements of *kernel extensions*. Tethernet is an ordinary program; the kernel never loads any of its code. The privileged part is a small background component that needs an admin password once, at install time. **HoRNDIS shows as "Disabled Software" / has no *Allow* button. Can I fix it?** That is macOS refusing to load a third-party kext, not something the kext can fix from its side. On Apple Silicon, loading one at all requires booting into Recovery and lowering the -security policy. TetherKit avoids the whole category of problem. +security policy. Tethernet avoids the whole category of problem. **Does it work on M1 / M2 / M3 / M4?** Yes — Apple Silicon and Intel take the same code path. There is nothing architecture-specific @@ -278,10 +262,10 @@ about it beyond ordinary compilation. **Is my device supported?** Anything that presents an RNDIS interface: most Android phones with USB tethering enabled, Windows Phone, and Linux `g_ether`/`u_ether` USB gadgets (Raspberry Pi Zero, BeagleBone …). -Run `tetherkit-cli --list` to check — it needs no root. +Run `tethernet-cli --list` to check — it needs no root. **Does iPhone tethering need this?** -No. macOS supports iPhone USB tethering natively; TetherKit is for the devices it does not +No. macOS supports iPhone USB tethering natively; Tethernet is for the devices it does not cover. --- @@ -302,7 +286,7 @@ cover. └────────────────────────────────────────────┼─────────────────────────────────┘ │ read() / write() raw Ethernet frames ┌────────────────────────────────────────────┼─────────────────────────────────┐ - │ TetherKit (user space) │ │ + │ Tethernet (user space) │ │ │ ▼ │ │ ┌───────────────────────── data path bridge ─────────────────────────┐ │ │ │ TX: BPF batch read → coalesce frames → RNDIS_PACKET_MSG → bulk OUT │ │ @@ -343,7 +327,7 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build -j ``` -Output: `build/bin/tetherkit-cli`. +Output: `build/bin/tethernet-cli`. ### Building the GUI @@ -351,21 +335,21 @@ Requires the full Xcode toolchain. On top of the build directory above: ```bash cmake --build build --target gui # same as ./gui/Scripts/build-gui.sh -open dist/TetherKit.app +open dist/Tethernet.app ``` ### Build options | Option | Default | Description | |---|---|---| -| `TETHERKIT_BUILD_TESTS` | `ON` | Build unit tests | -| `TETHERKIT_BUILD_BENCHMARKS` | `ON` | Build benchmarks | -| `TETHERKIT_WARNINGS_AS_ERRORS` | `OFF` | Treat warnings as errors | -| `TETHERKIT_NATIVE_ARCH` | `OFF` | `-mcpu=native`; the binary is no longer portable | -| `TETHERKIT_ENABLE_LTO` | `OFF` | Link-time optimization | -| `TETHERKIT_ENABLE_ASAN` | `OFF` | AddressSanitizer | -| `TETHERKIT_ENABLE_UBSAN` | `OFF` | UndefinedBehaviorSanitizer | -| `TETHERKIT_ENABLE_TSAN` | `OFF` | ThreadSanitizer (**required to validate the lock-free structures**) | +| `TETHERNET_BUILD_TESTS` | `ON` | Build unit tests | +| `TETHERNET_BUILD_BENCHMARKS` | `ON` | Build benchmarks | +| `TETHERNET_WARNINGS_AS_ERRORS` | `OFF` | Treat warnings as errors | +| `TETHERNET_NATIVE_ARCH` | `OFF` | `-mcpu=native`; the binary is no longer portable | +| `TETHERNET_ENABLE_LTO` | `OFF` | Link-time optimization | +| `TETHERNET_ENABLE_ASAN` | `OFF` | AddressSanitizer | +| `TETHERNET_ENABLE_UBSAN` | `OFF` | UndefinedBehaviorSanitizer | +| `TETHERNET_ENABLE_TSAN` | `OFF` | ThreadSanitizer (**required to validate the lock-free structures**) | ### Tests @@ -377,7 +361,7 @@ The lock-free queue and the multi-threaded data path are validated separately un ThreadSanitizer: ```bash -cmake -S . -B build-tsan -DTETHERKIT_ENABLE_TSAN=ON +cmake -S . -B build-tsan -DTETHERNET_ENABLE_TSAN=ON cmake --build build-tsan -j ctest --test-dir build-tsan --output-on-failure ``` @@ -387,7 +371,7 @@ ctest --test-dir build-tsan --output-on-failure ```bash cmake -S . -B build-rel -DCMAKE_BUILD_TYPE=Release cmake --build build-rel -j -./build-rel/bin/tetherkit_bench +./build-rel/bin/tethernet_bench ``` Aggregated results: [docs/BENCHMARKS.md](docs/BENCHMARKS.md). @@ -410,6 +394,6 @@ The documents below are written in English. --- -## License +## Licenses -See [LICENSE](LICENSE). +Tethernet is released under the [MIT License](LICENSE). Third-party components and their licenses (including libusb under LGPL-2.1) are documented in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..b5e34df --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,27 @@ +## libusb 1.0.30 — LGPL-2.1 + +Copyright (c) 2001 Johannes Erdfelt +Copyright (c) 2007-2009 Daniel Drake +Copyright (c) 2010-2012 Peter Stuge +Copyright (c) 2008-2016 Nathan Hjelm +Copyright (c) 2009-2013 Pete Batard +Copyright (c) 2009-2013 Ludovic Rousseau +Copyright (c) 2010-2012 Michael Plante +Copyright (c) 2011-2013 Hans de Goede +Copyright (c) 2012-2013 Martin Pieuchot +Copyright (c) 2012-2013 Toby Gray +Copyright (c) 2013-2018 Chris Dickens +and other contributors (see libusb AUTHORS). + +Tethernet links libusb dynamically and redistributes an unmodified copy of +libusb-1.0.0.dylib inside Tethernet.app, so that the app runs without Homebrew +installed. The library is loaded via dyld dynamic linking and the +copy on disk can be replaced by the user. + +Source for libusb 1.0.30: https://github.com/libusb/libusb/releases/tag/v1.0.30 +Full license text: docs/licenses/libusb-COPYING (also shipped in the app bundle). + +## doctest 2.4.12 — MIT + +Copyright (c) 2016-2023 Viktor Kirilov. Vendored at third_party/doctest/ for the +test suite only; it is not part of any shipped binary. diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index d49a246..5548412 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -6,13 +6,13 @@ # docs/BENCHMARKS.md, saving a parsing layer. add_executable( - tetherkit_bench + tethernet_bench bench_main.cc bench_common.cc bench_net.cc bench_rndis.cc harness.cc) -# tk_core PUBLIC-links all other modules (the benchmarks need the RNDIS codec and net loopback). -target_link_libraries(tetherkit_bench PRIVATE tetherkit_base tk_version tk_core) -target_include_directories(tetherkit_bench PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +# tn_core PUBLIC-links all other modules (the benchmarks need the RNDIS codec and net loopback). +target_link_libraries(tethernet_bench PRIVATE tethernet_base tn_version tn_core) +target_include_directories(tethernet_bench PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/benchmarks/bench_common.cc b/benchmarks/bench_common.cc index 936ab55..9930174 100644 --- a/benchmarks/bench_common.cc +++ b/benchmarks/bench_common.cc @@ -8,22 +8,23 @@ // of the data path. #include "bench_common.h" +#include "tethernet/common/byte_order.h" +#include "tethernet/common/frame_ring.h" +#include "tethernet/common/spsc_ring.h" +#include "tethernet/common/stats.h" + #include #include -#include #include #include +#include #include #include #include #include "harness.h" -#include "tetherkit/common/byte_order.h" -#include "tetherkit/common/frame_ring.h" -#include "tetherkit/common/spsc_ring.h" -#include "tetherkit/common/stats.h" -namespace tetherkit::bench { +namespace tethernet::bench { namespace { /// The two most common frame lengths on Ethernet: a full MTU frame and a small TCP ACK frame. @@ -304,4 +305,4 @@ void RegisterCommonBenchmarks(Runner& runner) { [](std::uint64_t n) { return BenchCounterUpdateFetchAdd(n); }); } -} // namespace tetherkit::bench +} // namespace tethernet::bench diff --git a/benchmarks/bench_common.h b/benchmarks/bench_common.h index c821d22..45a3295 100644 --- a/benchmarks/bench_common.h +++ b/benchmarks/bench_common.h @@ -1,11 +1,11 @@ // Registration entry point for infrastructure-layer micro-benchmarks. #pragma once -namespace tetherkit::bench { +namespace tethernet::bench { class Runner; -/// Registers all tk_common micro-benchmarks with the runner. +/// Registers all tn_common micro-benchmarks with the runner. void RegisterCommonBenchmarks(Runner& runner); -} // namespace tetherkit::bench +} // namespace tethernet::bench diff --git a/benchmarks/bench_main.cc b/benchmarks/bench_main.cc index 667b4f9..8a88010 100644 --- a/benchmarks/bench_main.cc +++ b/benchmarks/bench_main.cc @@ -3,9 +3,12 @@ // The output is a complete Markdown document (stdout) that can be redirected directly into // docs/BENCHMARKS.md: // -// ./build-rel/bin/tetherkit_bench > docs/BENCHMARKS.md +// ./build-rel/bin/tethernet_bench > docs/BENCHMARKS.md // // Progress messages go to stderr and do not pollute the report. +#include "tethernet/common/scheduling.h" +#include "tethernet/version.h" + #include #include #include @@ -14,51 +17,49 @@ #include "bench_net.h" #include "bench_rndis.h" #include "harness.h" -#include "tetherkit/common/scheduling.h" -#include "tetherkit/version.h" int main() { // The benchmark thread itself runs at data-path QoS; otherwise it may be scheduled onto an // efficiency core and the measured numbers would be worse than at actual runtime. - tetherkit::ConfigureCurrentThread("bench", tetherkit::ThreadRole::kDataPath); + tethernet::ConfigureCurrentThread("bench", tethernet::ThreadRole::kDataPath); - const std::string version{tetherkit::GetVersionString()}; - std::printf("# TetherKit Performance Benchmarks\n\n"); - std::printf("Automatically generated by `tetherkit_bench` -- %s\n\n", version.c_str()); - std::printf("> Regenerate: `./build-rel/bin/tetherkit_bench > docs/BENCHMARKS.md`\n"); + const std::string version{tethernet::GetVersionString()}; + std::printf("# Tethernet Performance Benchmarks\n\n"); + std::printf("Automatically generated by `tethernet_bench` -- %s\n\n", version.c_str()); + std::printf("> Regenerate: `./build-rel/bin/tethernet_bench > docs/BENCHMARKS.md`\n"); std::printf( "> Must use a **Release build without sanitizers**, otherwise the numbers are not " "meaningful.\n\n"); - tetherkit::bench::PrintEnvironment(); + tethernet::bench::PrintEnvironment(); std::fprintf(stderr, "Starting measurements...\n"); { - tetherkit::bench::Runner runner; - tetherkit::bench::RegisterCommonBenchmarks(runner); - tetherkit::bench::PrintMarkdownReport(runner.RunAll(), "Infrastructure (tk_common)"); + tethernet::bench::Runner runner; + tethernet::bench::RegisterCommonBenchmarks(runner); + tethernet::bench::PrintMarkdownReport(runner.RunAll(), "Infrastructure (tn_common)"); } { - tetherkit::bench::Runner runner; - tetherkit::bench::RegisterRndisBenchmarks(runner); - tetherkit::bench::PrintMarkdownReport(runner.RunAll(), "RNDIS encode/decode (tk_rndis)"); + tethernet::bench::Runner runner; + tethernet::bench::RegisterRndisBenchmarks(runner); + tethernet::bench::PrintMarkdownReport(runner.RunAll(), "RNDIS encode/decode (tn_rndis)"); } { // The link layer requires root. When skipped, leave a trace in the report -- otherwise // readers cannot tell whether this report "measured it and found no problem" or "never // measured it at all". - tetherkit::bench::Runner runner; + tethernet::bench::Runner runner; std::string skip_reason; - if (tetherkit::bench::RegisterNetBenchmarks(runner, skip_reason)) { - tetherkit::bench::PrintMarkdownReport(runner.RunAll(), - "feth / BPF link layer (tk_net, requires root)"); + if (tethernet::bench::RegisterNetBenchmarks(runner, skip_reason)) { + tethernet::bench::PrintMarkdownReport(runner.RunAll(), + "feth / BPF link layer (tn_net, requires root)"); // Must be torn down here, not left to static destruction -- see bench_net.h for the reason. - tetherkit::bench::ShutdownNetBenchmarks(); + tethernet::bench::ShutdownNetBenchmarks(); } else { - std::printf("## feth / BPF link layer (tk_net, requires root)\n\n"); + std::printf("## feth / BPF link layer (tn_net, requires root)\n\n"); std::printf("**Not measured in this run** -- %s.\n\n", skip_reason.c_str()); - std::printf("To measure: `sudo ./build/bin/tetherkit_bench > docs/BENCHMARKS.md`\n\n"); + std::printf("To measure: `sudo ./build/bin/tethernet_bench > docs/BENCHMARKS.md`\n\n"); std::fprintf(stderr, " Skipping link-layer benchmarks: %s\n", skip_reason.c_str()); } } diff --git a/benchmarks/bench_net.cc b/benchmarks/bench_net.cc index 0efa9cd..1be4231 100644 --- a/benchmarks/bench_net.cc +++ b/benchmarks/bench_net.cc @@ -17,6 +17,17 @@ // this way is correct and should not be deliberately avoided. #include "bench_net.h" +#include "tethernet/net/bpf_link.h" +#include "tethernet/net/feth_device.h" + +#include +#include +#include +#include +#include +#include + +#include #include #include #include @@ -26,19 +37,9 @@ #include #include -#include -#include -#include -#include -#include -#include -#include - #include "harness.h" -#include "tetherkit/net/bpf_link.h" -#include "tetherkit/net/feth_device.h" -namespace tetherkit::bench { +namespace tethernet::bench { namespace { constexpr std::uint32_t kFullFrameBytes = 1514; @@ -351,4 +352,4 @@ void ShutdownNetBenchmarks() noexcept { g_fixture = nullptr; } -} // namespace tetherkit::bench +} // namespace tethernet::bench diff --git a/benchmarks/bench_net.h b/benchmarks/bench_net.h index 1918dd1..8ed9634 100644 --- a/benchmarks/bench_net.h +++ b/benchmarks/bench_net.h @@ -8,7 +8,7 @@ #include -namespace tetherkit::bench { +namespace tethernet::bench { class Runner; @@ -28,4 +28,4 @@ bool RegisterNetBenchmarks(Runner& runner, std::string& skip_reason); /// is a failure. void ShutdownNetBenchmarks() noexcept; -} // namespace tetherkit::bench +} // namespace tethernet::bench diff --git a/benchmarks/bench_rndis.cc b/benchmarks/bench_rndis.cc index 64578e4..1686697 100644 --- a/benchmarks/bench_rndis.cc +++ b/benchmarks/bench_rndis.cc @@ -6,6 +6,9 @@ // RX: PacketMessageReader extracts frames one by one from a bulk IN transfer #include "bench_rndis.h" +#include "tethernet/rndis/packet_codec.h" +#include "tethernet/rndis/protocol.h" + #include #include #include @@ -13,10 +16,8 @@ #include #include "harness.h" -#include "tetherkit/rndis/packet_codec.h" -#include "tetherkit/rndis/protocol.h" -namespace tetherkit::bench { +namespace tethernet::bench { namespace { constexpr std::uint32_t kFullFrameBytes = 1514; @@ -210,4 +211,4 @@ void RegisterRndisBenchmarks(Runner& runner) { [](std::uint64_t n) { return BenchRoundTrip(n, kSmallFrameBytes, 10); }); } -} // namespace tetherkit::bench +} // namespace tethernet::bench diff --git a/benchmarks/bench_rndis.h b/benchmarks/bench_rndis.h index 7efe09c..c3adf8b 100644 --- a/benchmarks/bench_rndis.h +++ b/benchmarks/bench_rndis.h @@ -1,11 +1,11 @@ // Registration entry point for RNDIS codec micro-benchmarks. #pragma once -namespace tetherkit::bench { +namespace tethernet::bench { class Runner; /// Registers RNDIS codec micro-benchmarks with the runner. void RegisterRndisBenchmarks(Runner& runner); -} // namespace tetherkit::bench +} // namespace tethernet::bench diff --git a/benchmarks/harness.cc b/benchmarks/harness.cc index b67cc69..61e8fa1 100644 --- a/benchmarks/harness.cc +++ b/benchmarks/harness.cc @@ -1,5 +1,8 @@ #include "harness.h" +#include "tethernet/common/cache.h" +#include "tethernet/version.h" + #include #include @@ -8,10 +11,7 @@ #include #include -#include "tetherkit/common/cache.h" -#include "tetherkit/version.h" - -namespace tetherkit::bench { +namespace tethernet::bench { namespace { /// Reads a string-typed sysctl; returns "unknown" on failure. @@ -121,4 +121,4 @@ void PrintMarkdownReport(const std::vector& results, std::string_view ti "indicative only.\n\n"); } -} // namespace tetherkit::bench +} // namespace tethernet::bench diff --git a/benchmarks/harness.h b/benchmarks/harness.h index 0fa6fe3..67eb5cf 100644 --- a/benchmarks/harness.h +++ b/benchmarks/harness.h @@ -18,19 +18,19 @@ // measurement is trustworthy. #pragma once +#include "tethernet/common/time.h" + #include -#include #include #include #include +#include #include #include #include #include -#include "tetherkit/common/time.h" - -namespace tetherkit::bench { +namespace tethernet::bench { /// Prevents the compiler from optimizing away the code under measurement. /// @@ -170,4 +170,4 @@ void PrintMarkdownReport(const std::vector& results, std::string_view ti /// benchmark results are only meaningful with context. void PrintEnvironment(); -} // namespace tetherkit::bench +} // namespace tethernet::bench diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake index dca1afe..4f8aba9 100644 --- a/cmake/CompilerWarnings.cmake +++ b/cmake/CompilerWarnings.cmake @@ -1,11 +1,11 @@ # CompilerWarnings.cmake -- centralized warning options. # -# Provides the INTERFACE target tetherkit_warnings; every project-owned target +# Provides the INTERFACE target tethernet_warnings; every project-owned target # links it, while third_party/ code does not (keeps the noise down). -add_library(tetherkit_warnings INTERFACE) +add_library(tethernet_warnings INTERFACE) -set(_tk_warnings +set(_tn_warnings -Wall -Wextra -Wpedantic @@ -31,8 +31,8 @@ set(_tk_warnings # pins sizes with static_assert(sizeof(...)). ) -target_compile_options(tetherkit_warnings INTERFACE ${_tk_warnings}) +target_compile_options(tethernet_warnings INTERFACE ${_tn_warnings}) -if(TETHERKIT_WARNINGS_AS_ERRORS) - target_compile_options(tetherkit_warnings INTERFACE -Werror) +if(TETHERNET_WARNINGS_AS_ERRORS) + target_compile_options(tethernet_warnings INTERFACE -Werror) endif() diff --git a/cmake/Optimizations.cmake b/cmake/Optimizations.cmake index 9754a93..7c482e5 100644 --- a/cmake/Optimizations.cmake +++ b/cmake/Optimizations.cmake @@ -1,12 +1,12 @@ # Optimizations.cmake -- data path optimization options. # -# Provides the INTERFACE target tetherkit_optimizations. +# Provides the INTERFACE target tethernet_optimizations. # # Design trade-offs: # * No -march/-mcpu by default: the Apple Silicon baseline (armv8.4-a for M1) # already covers everything we need (LSE atomic instructions, NEON); the # extra benefit of -mcpu=native is marginal while tying the binary to a -# specific chip generation. Turn it on with -DTETHERKIT_NATIVE_ARCH=ON +# specific chip generation. Turn it on with -DTETHERNET_NATIVE_ARCH=ON # when needed. # * No -O3: the hot paths here are dominated by memcpy and syscalls, where # -O3's aggressive loop unrolling and vectorization add almost nothing while @@ -14,10 +14,10 @@ # default) is the better choice. # * -fno-plt / -fvisibility=hidden trim indirect jumps and symbol table size. -add_library(tetherkit_optimizations INTERFACE) +add_library(tethernet_optimizations INTERFACE) target_compile_options( - tetherkit_optimizations + tethernet_optimizations INTERFACE # Hide internal symbols by default: this project exports no stable ABI, # and hidden symbols leave the optimizer freer to inline. @@ -28,23 +28,23 @@ target_compile_options( # the hot paths' real bottleneck is syscalls, not alias analysis. -fno-strict-aliasing) -if(TETHERKIT_NATIVE_ARCH) +if(TETHERNET_NATIVE_ARCH) include(CheckCXXCompilerFlag) - check_cxx_compiler_flag(-mcpu=native TK_HAS_MCPU_NATIVE) - if(TK_HAS_MCPU_NATIVE) - target_compile_options(tetherkit_optimizations INTERFACE -mcpu=native) + check_cxx_compiler_flag(-mcpu=native TN_HAS_MCPU_NATIVE) + if(TN_HAS_MCPU_NATIVE) + target_compile_options(tethernet_optimizations INTERFACE -mcpu=native) else() message(WARNING "The compiler does not support -mcpu=native; microarchitecture optimization skipped.") endif() endif() -if(TETHERKIT_ENABLE_LTO) +if(TETHERNET_ENABLE_LTO) include(CheckIPOSupported) - check_ipo_supported(RESULT TK_IPO_OK OUTPUT TK_IPO_MSG) - if(TK_IPO_OK) + check_ipo_supported(RESULT TN_IPO_OK OUTPUT TN_IPO_MSG) + if(TN_IPO_OK) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON PARENT_SCOPE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON PARENT_SCOPE) else() - message(WARNING "LTO not supported, skipped: ${TK_IPO_MSG}") + message(WARNING "LTO not supported, skipped: ${TN_IPO_MSG}") endif() endif() diff --git a/cmake/Sanitizers.cmake b/cmake/Sanitizers.cmake index e98e43d..a6fdd22 100644 --- a/cmake/Sanitizers.cmake +++ b/cmake/Sanitizers.cmake @@ -1,57 +1,57 @@ # Sanitizers.cmake -- sanitizer switches. # -# Provides the INTERFACE target tetherkit_sanitizers. +# Provides the INTERFACE target tethernet_sanitizers. # # Notes: # * ASan and TSan are mutually exclusive; they cannot be on together. # * TSan is this project's most important sanitizer -- memory-ordering # correctness between the lock-free SPSC ring, the libusb event thread, and # the BPF reader/writer threads can only be validated by it. Run at least: -# cmake -B build-tsan -DTETHERKIT_ENABLE_TSAN=ON && ctest --test-dir build-tsan +# cmake -B build-tsan -DTETHERNET_ENABLE_TSAN=ON && ctest --test-dir build-tsan # * Sanitizers cut throughput substantially; benchmarks must run on a # Release build without any sanitizer enabled. -add_library(tetherkit_sanitizers INTERFACE) +add_library(tethernet_sanitizers INTERFACE) -if(TETHERKIT_ENABLE_ASAN AND TETHERKIT_ENABLE_TSAN) +if(TETHERNET_ENABLE_ASAN AND TETHERNET_ENABLE_TSAN) message(FATAL_ERROR "AddressSanitizer and ThreadSanitizer cannot be enabled together.") endif() -set(_tk_san_list "") +set(_tn_san_list "") -if(TETHERKIT_ENABLE_ASAN) - list(APPEND _tk_san_list address) +if(TETHERNET_ENABLE_ASAN) + list(APPEND _tn_san_list address) endif() -if(TETHERKIT_ENABLE_TSAN) - list(APPEND _tk_san_list thread) +if(TETHERNET_ENABLE_TSAN) + list(APPEND _tn_san_list thread) endif() -if(TETHERKIT_ENABLE_UBSAN) - list(APPEND _tk_san_list undefined) +if(TETHERNET_ENABLE_UBSAN) + list(APPEND _tn_san_list undefined) # Do we ever make unaligned accesses on hand-packed wire-format structs? # No -- every read is byte-wise, so the alignment check stays on to catch # genuine bugs. endif() -set(_tk_ubsan_recover_opts "") -if(TETHERKIT_ENABLE_UBSAN) +set(_tn_ubsan_recover_opts "") +if(TETHERNET_ENABLE_UBSAN) # UBSan defaults to report-and-continue: a diagnostic prints and the # process keeps running, so ctest still exits 0 and a sanitizer job stays # green through real UB. Compile every UBSan diagnostic fatal instead; # the CI job that enables UBSan additionally sets UBSAN_OPTIONS= # halt_on_error=1 to cover sanitizers inside code we cannot recompile. - list(APPEND _tk_ubsan_recover_opts -fno-sanitize-recover=undefined) + list(APPEND _tn_ubsan_recover_opts -fno-sanitize-recover=undefined) endif() -if(_tk_san_list) - list(JOIN _tk_san_list "," _tk_san_flags) - set(TETHERKIT_SANITIZER_SUMMARY "${_tk_san_flags}") - target_compile_options(tetherkit_sanitizers INTERFACE -fsanitize=${_tk_san_flags} +if(_tn_san_list) + list(JOIN _tn_san_list "," _tn_san_flags) + set(TETHERNET_SANITIZER_SUMMARY "${_tn_san_flags}") + target_compile_options(tethernet_sanitizers INTERFACE -fsanitize=${_tn_san_flags} -fno-omit-frame-pointer -g - ${_tk_ubsan_recover_opts}) - target_link_options(tetherkit_sanitizers INTERFACE -fsanitize=${_tk_san_flags}) - message(STATUS "Sanitizers enabled: ${_tk_san_flags}") + ${_tn_ubsan_recover_opts}) + target_link_options(tethernet_sanitizers INTERFACE -fsanitize=${_tn_san_flags}) + message(STATUS "Sanitizers enabled: ${_tn_san_flags}") else() - set(TETHERKIT_SANITIZER_SUMMARY "none") + set(TETHERNET_SANITIZER_SUMMARY "none") endif() diff --git a/cmake/tetherkit_exports.txt b/cmake/tethernet_exports.txt similarity index 69% rename from cmake/tetherkit_exports.txt rename to cmake/tethernet_exports.txt index 20f9744..e67911b 100644 --- a/cmake/tetherkit_exports.txt +++ b/cmake/tethernet_exports.txt @@ -1,13 +1,13 @@ -# Exported-symbol allowlist for libtetherkit.dylib (-exported_symbols_list). +# Exported-symbol allowlist for libtethernet.dylib (-exported_symbols_list). # -# Only the C ABI is exported. C++-side symbols (mangled tetherkit::* names and +# Only the C ABI is exported. C++-side symbols (mangled tethernet::* names and # the static libusb copy) are hidden, for two reasons: # 1. The host process may already have another libusb loaded; exporting # identically named symbols would let them pollute each other. # 2. C++ symbols carry an ABI fingerprint: once they leak out, that is a # promise of their stability, and we only want to promise the -# tetherkit_c.h layer. +# tethernet_c.h layer. # # The leading underscore is the Mach-O naming convention for C symbols -# (C's tk_version -> _tk_version). -_tk_* +# (C's tn_version -> _tn_version). +_tn_* diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index d85ea4b..4a2ddff 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -1,8 +1,8 @@ -# TetherKit Performance Benchmarks +# Tethernet Performance Benchmarks -Automatically generated by `tetherkit_bench` -- TetherKit 0.1.2 (C++23, macOS 13.3+) +Automatically generated by `tethernet_bench` (C++23, macOS 13.3+) -> Regenerate: `./build-rel/bin/tetherkit_bench > docs/BENCHMARKS.md` +> Regenerate: `./build-rel/bin/tethernet_bench > docs/BENCHMARKS.md` > Must use a **Release build without sanitizers**, otherwise the numbers are not meaningful. ## Environment @@ -15,7 +15,7 @@ Automatically generated by `tetherkit_bench` -- TetherKit 0.1.2 (C++23, macOS 13 | OS | macOS 26.5.1 (25F80) | | Build | Release, AppleClang 21.0.0.21000101, C++23, native-arch=OFF, LTO=OFF, sanitizers=none | -## Infrastructure (tk_common) +## Infrastructure (tn_common) | Group | Benchmark | Median ns/op | Best ns/op | Rate | Throughput | Spread | |---|---|---:|---:|---:|---:|---:| @@ -39,7 +39,7 @@ Automatically generated by `tetherkit_bench` -- TetherKit 0.1.2 (C++23, macOS 13 > Spread = (worst round - best round) / median. Above 30% is flagged, indicating > background interference or scheduling onto an efficiency core; treat absolute values as indicative only. -## RNDIS encode/decode (tk_rndis) +## RNDIS encode/decode (tn_rndis) | Group | Benchmark | Median ns/op | Best ns/op | Rate | Throughput | Spread | |---|---|---:|---:|---:|---:|---:| @@ -58,7 +58,7 @@ Automatically generated by `tetherkit_bench` -- TetherKit 0.1.2 (C++23, macOS 13 > Spread = (worst round - best round) / median. Above 30% is flagged, indicating > background interference or scheduling onto an efficiency core; treat absolute values as indicative only. -## feth / BPF link layer (tk_net, requires root) +## feth / BPF link layer (tn_net, requires root) | Group | Benchmark | Median ns/op | Best ns/op | Rate | Throughput | Spread | |---|---|---:|---:|---:|---:|---:| @@ -225,7 +225,7 @@ about **426 Mbps**; RX reached **77%** of it. configurations with zero retransmits and zero frame drops; TX reached only ~235 Mbps on the same link with thousands of TCP retransmits. -**2) TX frame drops happen on TetherKit's own TX path, not due to device +**2) TX frame drops happen on Tethernet's own TX path, not due to device rejection.** Runtime statistics provide direct evidence (with `--tx-transfers 4`, the default): @@ -327,7 +327,7 @@ only when full and remains visible in statistics. | Metric | Before | After | |---|---:|---:| -| TetherKit TX frame drops (6-8 s window) | 1889 | **0** | +| Tethernet TX frame drops (6-8 s window) | 1889 | **0** | | TCP TX retransmits | 3829 | **0** | | TCP TX throughput | 234~241 Mbps | 239~302 Mbps | | **Bidirectional TX** | **4.8 Mbps** | **87 Mbps** | @@ -373,7 +373,7 @@ low-rate counterexample proves it immediately: Every frame is counted as both "received" and "error", yet replies are completely normal. **When debugging TX issues, rely on iperf3's actual drop -rate and TetherKit's own statistics, not the phone-side `rx_errs`.** +rate and Tethernet's own statistics, not the phone-side `rx_errs`.** --- @@ -402,7 +402,7 @@ rate and TetherKit's own statistics, not the phone-side `rx_errs`.** (`thread_affinity_policy` is effectively useless on Apple Silicon). - **The link-layer benchmarks require root.** When run without root, the group does not disappear silently; the report records "Not measured in this run". - To measure: `sudo ./build/bin/tetherkit_bench > docs/BENCHMARKS.md`. Note + To measure: `sudo ./build/bin/tethernet_bench > docs/BENCHMARKS.md`. Note that the fixture must be torn down explicitly before `main` returns (`ShutdownNetBenchmarks`) -- leaving it to static destructors throws because the logging mutex has already been destroyed, which shows up as "all @@ -432,11 +432,11 @@ The four items previously listed here are **all complete**; see above: - [x] **Root cause** of TX frame drops -- found and fixed: the bridge dropped immediately when the transfer pool was full instead of waiting, see - "Root cause and fix for TX frame drops". TetherKit's own TX drops fell + "Root cause and fix for TX frame drops". Tethernet's own TX drops fell from 1889 to 0. - [x] Graceful shutdown after the fix -- confirmed in a real terminal: - `sudo build/bin/tetherkit-cli` then Ctrl-C prints "stopped" with no feth + `sudo build/bin/tethernet-cli` then Ctrl-C prints "stopped" with no feth residue in `ifconfig`. The added backpressure wait (`WaitForSendCapacity`) does not block the shutdown path. Note: this can **only be verified in a real terminal** -- the privilege-escalation @@ -467,7 +467,7 @@ the target process shows `blocked` / `pending` masks of 0, and the address is not `SIG_IGN`), yet the signal never arrives and the process can only be killed with `kill -9`. -A 15-line C program confirmed this independently of TetherKit: +A 15-line C program confirmed this independently of Tethernet: | Execution environment | Receives SIGTERM? | |---|---| @@ -478,5 +478,5 @@ A 15-line C program confirmed this independently of TetherKit: **Consequence: do not use this chain to verify graceful shutdown.** It produces a "shutdown hangs" false failure that has nothing to do with the code under test -- logs before and after the fix were all `Killed: 9`. To verify -end-to-end shutdown, run `sudo build/bin/tetherkit-cli` in a real terminal and +end-to-end shutdown, run `sudo build/bin/tethernet-cli` in a real terminal and press Ctrl-C (verified this way, as noted above). diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 292a7b8..68843d2 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,4 +1,4 @@ -# TetherKit Design Document +# Tethernet Design Document This document explains **why it is designed this way**. For field offsets and constants, see [RNDIS-PROTOCOL.md](RNDIS-PROTOCOL.md); for measured numbers, @@ -66,7 +66,7 @@ copy per direction, and TX has a "whole frame <= MTU + 18" length limit. +---------------------------------------+---------------------+ | +---------------------------------------+---------------------+ - | TetherKit (userspace) v | + | Tethernet (userspace) v | | +---------- core::Bridge (3 data-path threads) ---------+ | | | RX: bulk IN callback -> FrameRing -> batched BPF write| | | | TX: batched BPF read -> RNDIS aggregation -> bulk OUT | | @@ -91,23 +91,23 @@ copy per direction, and TX has a "whole frame <= MTU + 18" length limit. Dependencies are **strictly one-way**; reverse edges and cycles are forbidden: ``` -tk_common <- (no dependencies) errors, logging, byte order, lock-free queue, statistics, thread QoS +tn_common <- (no dependencies) errors, logging, byte order, lock-free queue, statistics, thread QoS ^ -tk_rndis <- common RNDIS wire format + state machine (**pure logic, no I/O**) -tk_net <- common feth lifetime + BPF link -tk_usb <- common, rndis libusb wrapper (rndis is used only for protocol constants) +tn_rndis <- common RNDIS wire format + state machine (**pure logic, no I/O**) +tn_net <- common feth lifetime + BPF link +tn_usb <- common, rndis libusb wrapper (rndis is used only for protocol constants) ^ -tk_core <- all of the above data-path bridge + runtime orchestration +tn_core <- all of the above data-path bridge + runtime orchestration ^ -tetherkit <- core command line +tethernet <- core command line ``` -**`tk_rndis` not touching I/O is a deliberate constraint.** It lets the entire +**`tn_rndis` not touching I/O is a deliberate constraint.** It lets the entire RNDIS implementation (including the state machine) be fully unit-tested on a machine with neither a USB device nor root -- and this project's development machine is exactly such a machine. For that reason the `ControlChannel` interface lives in the `rndis` layer (it describes RNDIS protocol semantics, not -USB semantics) and is implemented by `tk_usb`. +USB semantics) and is implemented by `tn_usb`. --- @@ -254,7 +254,7 @@ The error messages themselves all come from the message table (see section 6b), not from string literals. **The RNDIS status-code-to-name mapping lives in exactly one place, -`rndis/protocol.cc`** (a single source of truth); `tk_common` does not know +`rndis/protocol.cc`** (a single source of truth); `tn_common` does not know RNDIS and only outputs the hexadecimal value, while the symbolic name is spliced into the context string by the rndis layer. (There used to be a second copy in `error.cc` with 5 wrong values; it has been removed.) @@ -265,20 +265,20 @@ copy in `error.cc` with 5 wrong values; it has been removed.) User-facing text (errors, logs, help, status names) **is never written as a string literal**; it all lives in -`include/tetherkit/common/messages.def`. That file is an X-macro list expanded +`include/tethernet/common/messages.def`. That file is an X-macro list expanded to generate the `Msg` enum and the English string table -- all from the same source, so **a message cannot be structurally missing from the table**. The project is currently English-only, but the multi-language plumbing (the -`Language` enum, `TextIn`, `ParseLanguage`, the C ABI `tk_set_language`, the +`Language` enum, `TextIn`, `ParseLanguage`, the C ABI `tn_set_language`, the helper's `setLanguage` XPC method, and `L10n.apply`) is deliberately kept so that adding a language later stays a bounded change; the checklist for that -change is at the top of `include/tetherkit/common/messages.def`. +change is at the top of `include/tethernet/common/messages.def`. | Access form | Use | Allocates? | |---|---|---| | `Tr(Msg::kFoo, args...)` | with arguments, same semantics as `std::format` | yes (returns `std::string`) | | `Text(Msg::kFoo)` | no arguments, returns `string_view` | no, `noexcept` | -| `TETHERKIT_INFO_TR(Msg::kFoo, ...)` etc. | logging | not evaluated when the level is off | +| `TETHERNET_INFO_TR(Msg::kFoo, ...)` etc. | logging | not evaluated when the level is off | **Why not gettext**: it would mean pulling in libintl, running msgfmt at build time, and looking up directories at runtime, in exchange for runtime failures of @@ -300,7 +300,7 @@ instead of rendering as a half-formed log line on a user's machine. The language is decided by the host: the command line takes `--lang en` (and reads environment variables for defaults where applicable), and the GUI passes -its choice through the C ABI `tk_set_language`. The GUI language menu offers +its choice through the C ABI `tn_set_language`. The GUI language menu offers "Follow system" and "English". This is a **single process-wide state**, not one per session -- logs and errors are produced from multiple threads, and making it thread-local would let a single session emit output in two different languages. @@ -380,7 +380,7 @@ keep-alive failures, and reset replay) and every bridge path (including bidirectional concurrency, backpressure, pause, and shutdown under traffic) be tested offline and verified under ThreadSanitizer. feth/BPF cases that need root are **skipped rather than failed** by default and can be enabled explicitly -with `TETHERKIT_ROOT_TESTS=1`. +with `TETHERNET_ROOT_TESTS=1`. --- @@ -404,7 +404,7 @@ take the wrong path.** | Category | Convention | Example | |---|---|---| -| namespace | `lower_case` | `tetherkit::rndis` | +| namespace | `lower_case` | `tethernet::rndis` | | type | `CamelCase` | `PacketMessageWriter` | | function and method (including accessors) | `CamelCase` | `MaxFrameBytes()` | | local variable and parameter | `lower_case` | `frame_length` | diff --git a/docs/GUI-ARCHITECTURE.md b/docs/GUI-ARCHITECTURE.md index 9ec64ea..bd07c14 100644 --- a/docs/GUI-ARCHITECTURE.md +++ b/docs/GUI-ARCHITECTURE.md @@ -1,6 +1,6 @@ # GUI implementation notes -This document records what the TetherKit graphical interface **has +This document records what the Tethernet graphical interface **has implemented, why it is implemented that way, and which parts must not be touched**. @@ -16,7 +16,7 @@ pitfalls the hard way. ``` +---------------------------------------------------+ +-------------------------------------------+ -| TetherKit.app (uid 501) | | tetherkit-helper (uid 0) | +| Tethernet.app (uid 501) | | tethernet-helper (uid 0) | | | XPC | | | - SwiftUI interface | ------> | - holds the RNDIS session | | - pops the system auth dialog for a credential | | - creates/destroys feth, opens BPF | @@ -56,7 +56,7 @@ is equally safe. ## 2. Directory layout ``` -include/tetherkit/capi/tetherkit_c.h the only extern "C" boundary in the project +include/tethernet/capi/tethernet_c.h the only extern "C" boundary in the project src/capi/ C ABI implementation |-- capi_support.{h,cc} fixed-size buffer copies, error translation, interface name checks |-- core_foundation_support.{h,cc} minimal RAII wrappers for CF / SCDynamicStore @@ -70,17 +70,17 @@ src/capi/ C ABI implementation gui/ |-- Package.swift SwiftPM project |-- Sources/ -| |-- CTetherKit/ module map for the C ABI (the headers are symlinks) -| |-- TetherKitIPC/ shared by the App and the helper: protocol, models, authorization, string table -| |-- TetherKitCore/ Swift wrapper of the C ABI -| |-- TetherKitHelper/ privileged helper -| |-- TetherKitUIModel/ main window page model (SidebarPage: fixed colors and +| |-- CTethernet/ module map for the C ABI (the headers are symlinks) +| |-- TethernetIPC/ shared by the App and the helper: protocol, models, authorization, string table +| |-- TethernetCore/ Swift wrapper of the C ABI +| |-- TethernetHelper/ privileged helper +| |-- TethernetUIModel/ main window page model (SidebarPage: fixed colors and | | session state colors, persisted selection parsing) -- a | | separate library so that it can be unit tested (an executable | | target cannot be @testable imported) -| `-- TetherKitApp/ SwiftUI interface (main window management and the page views) -|-- Tests/TetherKitIPCTests/ authorization credential lifecycle + string table placeholder consistency -|-- Tests/TetherKitUIModelTests/ page tint semantics + persisted selection parsing +| `-- TethernetApp/ SwiftUI interface (main window management and the page views) +|-- Tests/TethernetIPCTests/ authorization credential lifecycle + string table placeholder consistency +|-- Tests/TethernetUIModelTests/ page tint semantics + persisted selection parsing |-- Resources/ Info.plist, LaunchDaemon plist `-- Scripts/ build / install / uninstall scripts ``` @@ -91,9 +91,9 @@ gui/ | Direction | Mechanism | Period | |---|---|---| -| library -> helper: status | `tk_session_status_get` copies a snapshot | on demand | -| library -> helper: events | `tk_session_poll_events` takes from the ring queue | rides along with the status query | -| library -> helper: logs | `tk_drain_logs` takes from the ring queue | every `drainFeed` | +| library -> helper: status | `tn_session_status_get` copies a snapshot | on demand | +| library -> helper: events | `tn_session_poll_events` takes from the ring queue | rides along with the status query | +| library -> helper: logs | `tn_drain_logs` takes from the ring queue | every `drainFeed` | | helper -> App | XPC, JSON-encoded Codable | 500 ms | **Not a single callback crosses the language boundary anywhere in the chain.** @@ -110,7 +110,7 @@ inside the library, the host polls". ### 4.1 C ABI - **No ownership crosses the boundary.** Output always goes into the - fixed-length struct the caller provides. The only exception is `tk_session_t*`, + fixed-length struct the caller provides. The only exception is `tn_session_t*`, whose create / destroy pairing is explicit. - **Truncation of a fixed-length buffer must land on a UTF-8 character boundary.** The error messages can contain non-ASCII characters, and cutting @@ -121,7 +121,7 @@ inside the library, the host polls". matching order**, and a set of `static_assert`s in `session.cc` welds that down. Inserting an enum value in the middle shifts them silently, with no error at all. -- **The member declaration order of `tk_session` is constrained**: `events` must +- **The member declaration order of `tn_session` is constrained**: `events` must come before `runtime`, so that on destruction `runtime` joins the control thread first. The other way round is a use-after-free. @@ -166,7 +166,7 @@ interface responds immediately. hard-to-diagnose phenomenon "the write returns success, the read comes back empty". -### 4.4 Authorization re-check (`TetherKitIPC/Authorization.swift`) +### 4.4 Authorization re-check (`TethernetIPC/Authorization.swift`) Three details that turn into a vulnerability the moment they are written wrong; all of them are documented in the comments of that file: @@ -203,7 +203,7 @@ A fourth one, **actually hit in practice**: value of `requestAuthorization()` and use it afterwards** -- ARC is free to release the token right after the last read of `externalForm`. - `gui/Tests/TetherKitIPCTests/AuthorizationTests.swift` nails this behaviour + `gui/Tests/TethernetIPCTests/AuthorizationTests.swift` nails this behaviour down; neither of the two cases pops an authorization dialog, so it can go into CI. @@ -262,7 +262,7 @@ password box, it just asks less often. ### 4.6 XPC interface revision number `HelperConstants.protocolRevision` must be incremented every time -`TetherKitHelperProtocol` changes. It is currently **3** (1: the first +`TethernetHelperProtocol` changes. It is currently **3** (1: the first version; 2: the reply of a privileged method carries "did the authorization fail"; 3: adds `setLanguage`). The helper encodes it in the `helperVersion` reply, and the App compares it as soon as it connects. @@ -321,7 +321,7 @@ needs updating". the delegate in `applicationDidFinishLaunching` and `makeKeyAndOrderFront`-ed, no longer depending on the instantiation timing of SwiftUI and no longer needing a manual rescue from the menu bar. The `.prohibited -> .regular` - self-check in `TetherKitApplication.init()` is kept: it governs the normal + self-check in `TethernetApplication.init()` is kept: it governs the normal activation semantics of a process that is not active yet. - **The polling is adaptive**: a running session / starting or stopping / the window open -> 500 ms; purely idle in the background -> 2 seconds. When the @@ -331,7 +331,7 @@ needs updating". ### 4.7b Interface language The string table is compiled into the binary, it does not go through `.lproj`. -The reason is written at the top of `TetherKitIPC/Localization.swift`, and its +The reason is written at the top of `TethernetIPC/Localization.swift`, and its core is that **the helper is a bare executable installed in `/Library/PrivilegedHelperTools`**, with no resource bundle next to it, and there should not be one, yet it still has to produce user-facing text of its @@ -341,7 +341,7 @@ The table currently holds a single column (English) and the GUI language menu offers "Follow system" and "English"; the multi-language plumbing is deliberately kept so that adding a language later stays a bounded five-touch change (the checklist lives at the top of -`include/tetherkit/common/messages.def`). +`include/tethernet/common/messages.def`). When the language changes, **all three** places must be updated together, and missing one of them produces "the interface in one language, the log in @@ -350,7 +350,7 @@ another": | Place | How to change it | Consequence of not changing it | |---|---|---| | Swift string table | `L10n.apply(_:)` | the interface does not change | -| libtetherkit | `TetherKitLibrary.setLanguage(_:)` -> `tk_set_language` | the library logs in the log card stay in the old language | +| libtethernet | `TethernetLibrary.setLanguage(_:)` -> `tn_set_language` | the library logs in the log card stay in the old language | | helper | the XPC `setLanguage(_:)` | the helper's messages and the library logs on its side stay in the old language | All three are done together in `AppModel.applyLanguage`; do not call one of @@ -375,8 +375,8 @@ Two SwiftUI specific pitfalls (both hit in practice): ### 4.8 Packaging -- **Copying the dylib must use `cp -a`, never `install`.** `libtetherkit.dylib` - and `libtetherkit.0.dylib` are symlinks; `install` copies a separate real +- **Copying the dylib must use `cp -a`, never `install`.** `libtethernet.dylib` + and `libtethernet.0.dylib` are symlinks; `install` copies a separate real file for each, and afterwards `install_name_tool` only changes one of them -- while the one that is loaded via `@rpath` is exactly the one that was not changed. @@ -384,7 +384,7 @@ Two SwiftUI specific pitfalls (both hit in practice): redone.** arm64 requires a valid signature; changing the bytes invalidates the original signature and the load is then rejected by the kernel outright (`Killed: 9`), with no reason visible in the log. -- **libusb is a dependency of `libtetherkit.dylib`, not of the executable.** +- **libusb is a dependency of `libtethernet.dylib`, not of the executable.** Running `otool -L` against the executable yields an empty result, the whole step is silently skipped -- after the install everything looks normal, yet it still depends on Homebrew. @@ -470,7 +470,7 @@ source. launches (defaults) and goes out by itself after the upgrade because of the version comparison. - Privacy: only the public GitHub API is accessed, and the failures are - silent; `defaults write com.tetherkit.app updateCheckDisabled -bool YES` + silent; `defaults write com.tethernet.app updateCheckDisabled -bool YES` turns it off completely. - The bare executable of `swift run` has no Info.plist version number, so the check skips itself automatically. @@ -597,7 +597,7 @@ orthodox keyboard-operable alternative; the popup runs in opening and committing it (a hardware checklist item), so the existence of the menu is proven by a CGWindowList diff (pitfall 24 round 8 (4)); the filter binding now goes through `ButtonTurn.routed` (`ButtonTurn` was moved into -TetherKitUIModel as well, its commit semantics have unit tests). +TethernetUIModel as well, its commit semantics have unit tests). **Tab re-entering the sidebar and the row level fallback for the Return (round 9, real machine re-check)**: the user's real machine re-check overturned the @@ -714,27 +714,27 @@ affect them (verified in practice). | Capability | Where | |---|---| -| Environment preflight (root, feth sysctls, MTU ceiling) | `tk_check_environment` | -| Device enumeration + vendor name / product name / serial number | `tk_list_devices` | -| Session lifecycle (non-blocking start, status snapshot, events) | `tk_session_*` | -| Log capture and polling | `tk_drain_logs` | -| DHCP / static IP / clearing | `tk_net_apply`, `tk_net_clear` | -| Read-back of the really effective state (address, gateway, DNS, main default route) | `tk_net_query` | -| Orphan feth cleanup | `tk_cleanup_orphan_interfaces` | -| Privileged helper + credential re-check | `gui/Sources/TetherKitHelper` | -| One-click install / update / uninstall of the privileged helper inside the App | `gui/Sources/TetherKitApp/HelperInstaller.swift` | -| Update check (check only, never replace; daily automatic + manual menu) | `gui/Sources/TetherKitApp/UpdateChecker.swift` | -| Automatic Finder alias maintenance (created on the first launch, searchable and launchable in Spotlight; the brew postinstall cannot create it because of the sandbox) | `gui/Sources/TetherKitApp/FinderAlias.swift` | -| SwiftUI interface (five sidebar pages: Connect / Network / Throughput / Log / Manage, paged by `SidebarShell`; the list of the log page fills the page height) | `gui/Sources/TetherKitApp/Views/SidebarShell.swift` | -| Live rate in the menu bar + background running (menu bar only mode) | `gui/Sources/TetherKitApp/Views/MenuBarPanel.swift` | -| Dev self capture tool (the App captures its own window into a PNG, so the iteration loop needs no screen recording permission) | `gui/Sources/TetherKitApp/Views/UiSelfCapture.swift` | -| English user-facing strings (the runtime language switch is kept, and the interface / the library log / the helper messages are switched together) | `gui/Sources/TetherKitIPC/Localization.swift`, `tk_set_language` | +| Environment preflight (root, feth sysctls, MTU ceiling) | `tn_check_environment` | +| Device enumeration + vendor name / product name / serial number | `tn_list_devices` | +| Session lifecycle (non-blocking start, status snapshot, events) | `tn_session_*` | +| Log capture and polling | `tn_drain_logs` | +| DHCP / static IP / clearing | `tn_net_apply`, `tn_net_clear` | +| Read-back of the really effective state (address, gateway, DNS, main default route) | `tn_net_query` | +| Orphan feth cleanup | `tn_cleanup_orphan_interfaces` | +| Privileged helper + credential re-check | `gui/Sources/TethernetHelper` | +| One-click install / update / uninstall of the privileged helper inside the App | `gui/Sources/TethernetApp/HelperInstaller.swift` | +| Update check (check only, never replace; daily automatic + manual menu) | `gui/Sources/TethernetApp/UpdateChecker.swift` | +| Automatic Finder alias maintenance (created on the first launch, searchable and launchable in Spotlight; the brew postinstall cannot create it because of the sandbox) | `gui/Sources/TethernetApp/FinderAlias.swift` | +| SwiftUI interface (five sidebar pages: Connect / Network / Throughput / Log / Manage, paged by `SidebarShell`; the list of the log page fills the page height) | `gui/Sources/TethernetApp/Views/SidebarShell.swift` | +| Live rate in the menu bar + background running (menu bar only mode) | `gui/Sources/TethernetApp/Views/MenuBarPanel.swift` | +| Dev self capture tool (the App captures its own window into a PNG, so the iteration loop needs no screen recording permission) | `gui/Sources/TethernetApp/Views/UiSelfCapture.swift` | +| English user-facing strings (the runtime language switch is kept, and the interface / the library log / the helper messages are switched together) | `gui/Sources/TethernetIPC/Localization.swift`, `tn_set_language` | ### Not implemented / known limitations | Item | Explanation | |---|---| -| **The DNS of the static IP is not verified on a real device** | IPConfiguration publishes DNS only in DHCP mode. In static mode we add a DNS key to the service it created, and whether IPMonitor adopts it has not been verified. Therefore `tk_net_query` always **reads back** the resolver that is really in effect, and the interface shows the read-back result -- if we guessed wrong, at worst the DNS does not take effect, nothing is broken. | +| **The DNS of the static IP is not verified on a real device** | IPConfiguration publishes DNS only in DHCP mode. In static mode we add a DNS key to the service it created, and whether IPMonitor adopts it has not been verified. Therefore `tn_net_query` always **reads back** the resolver that is really in effect, and the interface shows the read-back result -- if we guessed wrong, at worst the DNS does not take effect, nothing is broken. | | **End to end already works on a real device (the DHCP path)** | Verified on 2026-07-28 with an Android phone: plug it in -> the App connects -> feth0 gets an address in the phone's subnet via DHCP -> the host and the phone can ping each other in both directions -> iperf3 measures RX 325 / TX 235 Mbps. **The step "can it reach the public internet" could not be verified** -- a VPN on this machine holds the default route and hijacks the tethered traffic, which is a problem of the test environment and not of the GUI. After the App quits the session is still held by the helper (background running mode, as designed). | | **The authorization can only take a password, no fingerprint** | A limitation of the system API, not a defect. Verified and recorded in section 4.5. The mitigation is the token cache: it asks only once every 5 minutes. | | **Hot plug** | `Context::SupportsHotplug()` exists but is not wired up; for now the interface re-enumerates every 2 seconds. | @@ -748,7 +748,7 @@ affect them (verified in practice). ## 6. Build and install ```bash -# 1. The C++ part (produces libtetherkit.dylib) +# 1. The C++ part (produces libtethernet.dylib) cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build -j @@ -759,16 +759,16 @@ cmake --build build --target gui # helper: click the button, type the administrator password once (see # section 4.9). # Terminal alternative: sudo ./gui/Scripts/install-helper.sh -open dist/TetherKit.app +open dist/Tethernet.app ``` During development you can also use SwiftPM directly, without going through the packaging script: ```bash -export TETHERKIT_LIB_DIR="$PWD/build/lib" +export TETHERNET_LIB_DIR="$PWD/build/lib" swift build --package-path gui -swift test --package-path gui # the unit tests of TetherKitIPC + TetherKitUIModel +swift test --package-path gui # the unit tests of TethernetIPC + TethernetUIModel ``` Uninstall: the "Uninstall the privileged helper?" button on the "Manage" page @@ -782,13 +782,13 @@ Troubleshooting entry points: | Symptom | Where to look first | |---|---| -| The interface keeps showing "The privileged helper must be installed" | `launchctl print system/com.tetherkit.helper` | -| The helper does not come up | `/var/log/tetherkit-helper.log` | +| The interface keeps showing "The privileged helper must be installed" | `launchctl print system/com.tethernet.helper` | +| The helper does not come up | `/var/log/tethernet-helper.log` | | The connection fails | the "Log" panel in the interface (one click to copy) | -| After the install it still depends on Homebrew | `otool -L dist/TetherKit.app/Contents/Frameworks/libtetherkit.0.*.dylib` | +| After the install it still depends on Homebrew | `otool -L dist/Tethernet.app/Contents/Frameworks/libtethernet.0.*.dylib` | | Clicking "Connect" reports "Cannot restore the authorization credential (-60005)" | The App side released the AuthorizationRef too early, see item 4 in section 4.4 | | The interface shows "The privileged helper needs updating" | The XPC protocol was changed but the helper was not reinstalled; click "Update the helper" (or run the install script) | -| Clicking "Install the helper" reports "This copy of TetherKit carries no installer payload" | The bare executable of `swift run` is running; the payload exists only in the .app assembled by build-gui.sh; install it with the terminal script | +| Clicking "Install the helper" reports "This copy of Tethernet carries no installer payload" | The bare executable of `swift run` is running; the payload exists only in the .app assembled by build-gui.sh; install it with the terminal script | | The authorization box only takes a password, no fingerprint | This is a limitation of the system API, not a defect, see section 4.5 | | After unplugging and replugging the device, clicking "Connect" reports "A session is already running" | The helper is an old version (a failed state old session was not cleaned up); reinstall the helper | | Double-clicking the Finder icon does nothing | Already fixed: the main window is presented again in `applicationShouldHandleReopen` (this callback only became available once AppKit managed the window itself) | diff --git a/docs/GUI-SPIKE.md b/docs/GUI-SPIKE.md index 9936ade..33686ed 100644 --- a/docs/GUI-SPIKE.md +++ b/docs/GUI-SPIKE.md @@ -1,7 +1,7 @@ # GUI and privilege escalation feasibility spike This document records the conclusions of an exploratory spike: **can a native -Swift GUI be built on top of TetherKit, and can it obtain the root privileges +Swift GUI be built on top of Tethernet, and can it obtain the root privileges needed to configure the network in an appropriate way?** The answer is yes, but three routes that looked more "orthodox" were ruled out @@ -128,12 +128,12 @@ password box, is deprecated. ## 4. The final design ``` -TetherKit.app (uid 501, no root needed) +Tethernet.app (uid 501, no root needed) | 1. call the C API directly for the root-free work: environment preflight, USB enumeration | 2. AuthorizationCopyRights -> pop a fingerprint/password -> AuthorizationRef | 3. AuthorizationMakeExternalForm -> 32-byte credential | - +-- XPC (com.tetherkit.helper, .privileged) --> tetherkit-helper (uid 0) + +-- XPC (com.tethernet.helper, .privileged) --> tethernet-helper (uid 0) | | 4. CreateFromExternalForm restores it | | 5. CopyRights re-checks it | | (without interactionAllowed) @@ -218,7 +218,7 @@ user confirmed at some moment with a password / fingerprint that they agree to grant a certain right". Not one of the uid and euid of the process moves. Measured: on the line right after the authorization succeeds, the App calls -`tk_feth_create()` directly and still gets `EPERM`; the same function called +`tn_feth_create()` directly and still gets `EPERM`; the same function called through the helper succeeds. The Authorization Services have always been an **advisory** authorization @@ -323,7 +323,7 @@ the command does not even have to run. ### 6.5 The impact on the implementation -**The `tk_net_*` group of C APIs is basically not needed.** The DHCP client +**The `tn_net_*` group of C APIs is basically not needed.** The DHCP client originally planned to be implemented by hand (opening BPF on feth0 to run DISCOVER/OFFER/REQUEST/ACK, and coupling its timing to the RNDIS state machine) is saved as a whole. The helper only needs: @@ -356,7 +356,7 @@ Under the Hardened Runtime macOS refuses to load a library whose Team ID differs from the main program's. Homebrew's libusb is ad-hoc signed (no Team ID), while the App is signed with a developer certificate (it has a Team ID). -**Any signed and distributed TetherKit.app must embed libusb** and re-sign it +**Any signed and distributed Tethernet.app must embed libusb** and re-sign it with the same identity. The other road is to add the `com.apple.security.cs.disable-library-validation` exemption, but that amounts to turning off the library validation of the whole process, which is not worth @@ -373,7 +373,7 @@ kernel (`Killed: 9`), with no reason visible in the launchd log. ### `install` breaks the symlinks into separate files -`libtetherkit.dylib` and `libtetherkit.0.dylib` are symlinks to +`libtethernet.dylib` and `libtethernet.0.dylib` are symlinks to `.0.1.1.dylib`. `install` does not preserve the symlinks and copies a **separate real file** for each. The later `install_name_tool` only changes one of them, and the one that gets loaded via `@rpath` is exactly the one that was @@ -382,7 +382,7 @@ there, and with no warning whatsoever**. Use `cp -a`. ### Look up the dependencies against the right object -`libusb` is a dependency of `libtetherkit.dylib`, not of the helper +`libusb` is a dependency of `libtethernet.dylib`, not of the helper executable. Running `otool -L` against the helper yields an empty result, the whole step is silently skipped -- after the install everything looks perfectly normal, yet it still depends on Homebrew. @@ -501,7 +501,7 @@ sudo ipconfig set feth9 NONE && sudo ifconfig feth9 destroy # Do not take its absence as evidence that "IPConfiguration does not # recognize this interface" -- I stepped on that one. -# the complete verification on a real link (needs the device plugged in and tetherkit running) +# the complete verification on a real link (needs the device plugged in and tethernet running) sudo ipconfig set feth0 DHCP ipconfig getifaddr feth0 # the lease scutil --dns | grep -A3 feth0 # scoped DNS (it is in the later part, do not cut it with head) diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 7da58f3..01fbb0a 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -112,7 +112,7 @@ cmake -S . -B build-rel -DCMAKE_BUILD_TYPE=Release - **Do not enable `-O3`**: the hot path is dominated by memcpy and system calls; `-O3`'s aggressive loop unrolling and vectorization gain almost nothing, and instead increase code size and hurt I-cache hits. -- **`-DTETHERKIT_NATIVE_ARCH=ON` gains very little**: the arm64 baseline already +- **`-DTETHERNET_NATIVE_ARCH=ON` gains very little**: the arm64 baseline already emits LSE atomic instructions (`ldadd`); `-mcpu=` only brings scheduling-model differences, not an instruction-set unlock. Also note that `-mcpu=apple-m5` **is rejected by Apple clang 21** (`native` resolves to diff --git a/docs/RNDIS-PROTOCOL.md b/docs/RNDIS-PROTOCOL.md index 3888a56..06c51fc 100644 --- a/docs/RNDIS-PROTOCOL.md +++ b/docs/RNDIS-PROTOCOL.md @@ -2,7 +2,7 @@ This document is the complete wire-format detail needed to implement the RNDIS **host side**. All values are consistent with -[`include/tetherkit/rndis/protocol.h`](../include/tetherkit/rndis/protocol.h) +[`include/tethernet/rndis/protocol.h`](../include/tethernet/rndis/protocol.h) (the single source of truth in the code) and were cross-checked against the Linux kernel implementation: `drivers/net/usb/rndis_host.c`, `include/linux/usb/rndis_host.h`, `drivers/usb/gadget/function/rndis.c`. diff --git a/docs/assets/icon.png b/docs/assets/icon.png deleted file mode 100644 index bef9eff..0000000 Binary files a/docs/assets/icon.png and /dev/null differ diff --git a/docs/assets/screenshot-main-en.jpg b/docs/assets/screenshot-main-en.jpg deleted file mode 100644 index ed9be2f..0000000 Binary files a/docs/assets/screenshot-main-en.jpg and /dev/null differ diff --git a/docs/licenses/libusb-COPYING b/docs/licenses/libusb-COPYING new file mode 100644 index 0000000..5ab7695 --- /dev/null +++ b/docs/licenses/libusb-COPYING @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/gui/.clang-tidy b/gui/.clang-tidy index b502b7f..b880831 100644 --- a/gui/.clang-tidy +++ b/gui/.clang-tidy @@ -1,7 +1,7 @@ # gui/ is a Swift package, not part of the CMake build; clang-tidy has no # business here. # -# The only C file is CTetherKit/shim.c, which exists purely to satisfy a +# The only C file is CTethernet/shim.c, which exists purely to satisfy a # SwiftPM formality (every C target needs at least one source file). Applying # the C++ project's naming and modernization rules to it is meaningless. InheritParentConfig: false diff --git a/gui/Package.swift b/gui/Package.swift index 9c92e1b..2109452 100644 --- a/gui/Package.swift +++ b/gui/Package.swift @@ -1,6 +1,6 @@ // swift-tools-version: 5.9 // -// The graphical interface of TetherKit. +// The graphical interface of Tethernet. // // Why SwiftPM rather than CMake or an Xcode project: // @@ -17,12 +17,12 @@ // produces the executable; the Info.plist, the icon and the embedded // dylib are all put together in the script. // -// How libtetherkit is found: +// How libtethernet is found: // // The C++ side is built first, and its product lands in // /build/lib. The path comes in through an environment variable: // -// export TETHERKIT_LIB_DIR=$PWD/build/lib +// export TETHERNET_LIB_DIR=$PWD/build/lib // swift build --package-path gui // // Scripts/build-gui.sh sets it for you. Without it the repository @@ -31,13 +31,13 @@ import Foundation import PackageDescription -/// The directory libtetherkit lives in. +/// The directory libtethernet lives in. /// /// An absolute path: SwiftPM's working directory depends on how it is /// invoked, and a relative path would give different results between /// `swift build --package-path gui` and `cd gui && swift build`. let libraryDirectory: String = { - if let fromEnvironment = ProcessInfo.processInfo.environment["TETHERKIT_LIB_DIR"], + if let fromEnvironment = ProcessInfo.processInfo.environment["TETHERNET_LIB_DIR"], !fromEnvironment.isEmpty { return fromEnvironment } @@ -50,13 +50,13 @@ let libraryDirectory: String = { .path }() -/// All the flags needed to link libtetherkit. +/// All the flags needed to link libtethernet. /// /// This has to use unsafeFlags -- SwiftPM has no safe interface for /// "add a library search path". This package is a root package and will /// not be depended on by others, so the unsafeFlags restriction does not /// apply. -let tetherkitLinkerSettings: [LinkerSetting] = [ +let tethernetLinkerSettings: [LinkerSetting] = [ .unsafeFlags([ "-L\(libraryDirectory)", // The rpath has to be passed to the linker in pieces via -Xlinker. @@ -85,7 +85,7 @@ let tetherkitLinkerSettings: [LinkerSetting] = [ "-Xlinker", "-rpath", "-Xlinker", "@executable_path", "-Xlinker", "-rpath", "-Xlinker", libraryDirectory, ]), - .linkedLibrary("tetherkit"), + .linkedLibrary("tethernet"), ] /// Tahoe (macOS 26+) selects the window chrome from the executable's @@ -97,7 +97,7 @@ let tetherkitLinkerSettings: [LinkerSetting] = [ /// Do the same here; minos stays 14.0 and the sdk stamp gates only /// the Tahoe appearance (verified: sdk 14.0 -> legacy lights, /// sdk 26.5 -> full-size glossy ones, same minos). -let tetherkitAppLinkerSettings: [LinkerSetting] = [ +let tethernetAppLinkerSettings: [LinkerSetting] = [ .unsafeFlags([ "-Xlinker", "-platform_version", "-Xlinker", "macos", "-Xlinker", "14.0.0", "-Xlinker", "26.5.0", @@ -105,55 +105,55 @@ let tetherkitAppLinkerSettings: [LinkerSetting] = [ ] let package = Package( - name: "TetherKitGUI", + name: "TethernetGUI", // macOS 14: @Observable and ContentUnavailableView need it. The // command line part still supports 13.3; the two are independent // artifacts and do not have to be aligned. platforms: [.macOS(.v14)], products: [ - .executable(name: "TetherKitApp", targets: ["TetherKitApp"]), - .executable(name: "tetherkit-helper", targets: ["TetherKitHelper"]), + .executable(name: "TethernetApp", targets: ["TethernetApp"]), + .executable(name: "tethernet-helper", targets: ["TethernetHelper"]), ], targets: [ // The module map for the C ABI. The header is a symbolic link - // to include/tetherkit/capi/tetherkit_c.h, so it stays in sync + // to include/tethernet/capi/tethernet_c.h, so it stays in sync // with the C++ side forever and needs no generation step. - .target(name: "CTetherKit"), + .target(name: "CTethernet"), // The XPC protocol and the data models shared by the app and // the helper. Both sides stay consistent through one shared // source rather than each keeping its own copy. - .target(name: "TetherKitIPC"), + .target(name: "TethernetIPC"), - // The Swift wrapper of the C ABI: it turns tk_* into Swift types + // The Swift wrapper of the C ABI: it turns tn_* into Swift types // and errors. - .target(name: "TetherKitCore", - dependencies: ["CTetherKit", "TetherKitIPC"], - linkerSettings: tetherkitLinkerSettings), + .target(name: "TethernetCore", + dependencies: ["CTethernet", "TethernetIPC"], + linkerSettings: tethernetLinkerSettings), // The privileged helper, running as root. - .executableTarget(name: "TetherKitHelper", - dependencies: ["TetherKitCore", "TetherKitIPC"]), + .executableTarget(name: "TethernetHelper", + dependencies: ["TethernetCore", "TethernetIPC"]), // The SwiftUI app the user sees (running as an ordinary user). - .executableTarget(name: "TetherKitApp", - dependencies: ["TetherKitCore", "TetherKitIPC", "TetherKitUIModel"], - linkerSettings: tetherkitAppLinkerSettings), + .executableTarget(name: "TethernetApp", + dependencies: ["TethernetCore", "TethernetIPC", "TethernetUIModel"], + linkerSettings: tethernetAppLinkerSettings), // Sidebar page model (SidebarPage: titles, icons, fixed and // session-state tints, persisted-selection resolution). A // library target of its own rather than folded into - // TetherKitApp: an executable target cannot be + // TethernetApp: an executable target cannot be // @testable-imported, so the tint semantics are only lockable - // from outside it (see TetherKitUIModelTests). - .target(name: "TetherKitUIModel", dependencies: ["TetherKitIPC"]), + // from outside it (see TethernetUIModelTests). + .target(name: "TethernetUIModel", dependencies: ["TethernetIPC"]), - // TetherKitIPC: pure logic layer, no hardware access, no root. + // TethernetIPC: pure logic layer, no hardware access, no root. // Session and interface config tests live on the C++ side // (tests/test_capi.cc), not duplicated here. - .testTarget(name: "TetherKitIPCTests", dependencies: ["TetherKitIPC"]), + .testTarget(name: "TethernetIPCTests", dependencies: ["TethernetIPC"]), - // Pure logic of the page model (TetherKitUIModel): tint + // Pure logic of the page model (TethernetUIModel): tint // decision and persisted selection. - .testTarget(name: "TetherKitUIModelTests", dependencies: ["TetherKitUIModel"]), + .testTarget(name: "TethernetUIModelTests", dependencies: ["TethernetUIModel"]), ]) diff --git a/gui/Resources/App-Info.plist b/gui/Resources/App-Info.plist index c2c55d5..13d4aa7 100644 --- a/gui/Resources/App-Info.plist +++ b/gui/Resources/App-Info.plist @@ -1,26 +1,26 @@ CFBundleName - TetherKit + Tethernet CFBundleDisplayName - TetherKit + Tethernet CFBundleIdentifier - com.tetherkit.app + com.tethernet.app CFBundleExecutable - TetherKit + Tethernet CFBundlePackageType APPL diff --git a/gui/Resources/com.tetherkit.helper.plist b/gui/Resources/com.tethernet.helper.plist similarity index 82% rename from gui/Resources/com.tetherkit.helper.plist rename to gui/Resources/com.tethernet.helper.plist index 2c0d746..ad83198 100644 --- a/gui/Resources/com.tetherkit.helper.plist +++ b/gui/Resources/com.tethernet.helper.plist @@ -1,8 +1,8 @@ MachServices - com.tetherkit.helper + com.tethernet.helper StandardOutPath - /var/log/tetherkit-helper.log + /var/log/tethernet-helper.log StandardErrorPath - /var/log/tetherkit-helper.log + /var/log/tethernet-helper.log diff --git a/gui/Scripts/build-gui.sh b/gui/Scripts/build-gui.sh index 6f9a226..cee4e68 100755 --- a/gui/Scripts/build-gui.sh +++ b/gui/Scripts/build-gui.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # -# Build the TetherKit GUI: TetherKit.app and tetherkit-helper. +# Build the Tethernet GUI: Tethernet.app and tethernet-helper. # # Usage: # ./gui/Scripts/build-gui.sh # uses the default build/ directory -# TETHERKIT_BUILD_DIR=/path ./gui/Scripts/build-gui.sh +# TETHERNET_BUILD_DIR=/path ./gui/Scripts/build-gui.sh # ./gui/Scripts/build-gui.sh --debug # debug build -- faster compile, breakpoints # # There is exactly one artifact: -# dist/TetherKit.app -- the double-clickable application +# dist/Tethernet.app -- the double-clickable application # # The privileged-helper install payload (helper binary + dylibs + plist + # install script) is embedded under the .app's Contents/Library/HelperTools/; @@ -29,7 +29,7 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" GUI_DIR="${REPO_ROOT}/gui" -BUILD_DIR="${TETHERKIT_BUILD_DIR:-${REPO_ROOT}/build}" +BUILD_DIR="${TETHERNET_BUILD_DIR:-${REPO_ROOT}/build}" DIST_DIR="${REPO_ROOT}/dist" SWIFT_CONFIGURATION="release" @@ -56,8 +56,8 @@ die() { printf '\033[31merror: \033[0m%s\n' "$1" >&2; exit 1; } command -v swift >/dev/null || die "swift not found; install Xcode or the command line tools first" LIB_DIR="${BUILD_DIR}/lib" -if [[ ! -f "${LIB_DIR}/libtetherkit.dylib" ]]; then - die "cannot find ${LIB_DIR}/libtetherkit.dylib +if [[ ! -f "${LIB_DIR}/libtethernet.dylib" ]]; then + die "cannot find ${LIB_DIR}/libtethernet.dylib build the C++ part first: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build -j" @@ -66,7 +66,7 @@ fi VERSION="$(sed -n 's/^ VERSION \([0-9.]*\)$/\1/p' "${REPO_ROOT}/CMakeLists.txt" | head -1)" [[ -n "${VERSION}" ]] || die "could not parse the version from CMakeLists.txt" -log "TetherKit ${VERSION} (Swift ${SWIFT_CONFIGURATION} build)" +log "Tethernet ${VERSION} (Swift ${SWIFT_CONFIGURATION} build)" # ------------------------------------------------------------------------------ # Compile Swift @@ -75,12 +75,12 @@ log "Compiling the Swift targets" # An empty array conflicts with set -u under macOS's bundled bash 3.2 # ("${arr[@]}" reports unbound variable), so the expansion must use the # ${arr[@]+...} guard form. -TETHERKIT_LIB_DIR="${LIB_DIR}" swift build \ +TETHERNET_LIB_DIR="${LIB_DIR}" swift build \ --package-path "${GUI_DIR}" \ --configuration "${SWIFT_CONFIGURATION}" \ ${SWIFT_BUILD_FLAGS[@]+"${SWIFT_BUILD_FLAGS[@]}"} -SWIFT_BIN_DIR="$(TETHERKIT_LIB_DIR="${LIB_DIR}" swift build \ +SWIFT_BIN_DIR="$(TETHERNET_LIB_DIR="${LIB_DIR}" swift build \ --package-path "${GUI_DIR}" \ --configuration "${SWIFT_CONFIGURATION}" \ ${SWIFT_BUILD_FLAGS[@]+"${SWIFT_BUILD_FLAGS[@]}"} \ @@ -89,26 +89,26 @@ SWIFT_BIN_DIR="$(TETHERKIT_LIB_DIR="${LIB_DIR}" swift build \ # ------------------------------------------------------------------------------ # Assemble the .app # ------------------------------------------------------------------------------ -APP_DIR="${DIST_DIR}/TetherKit.app" +APP_DIR="${DIST_DIR}/Tethernet.app" log "Assembling ${APP_DIR}" rm -rf "${APP_DIR}" mkdir -p "${APP_DIR}/Contents/MacOS" "${APP_DIR}/Contents/Frameworks" \ "${APP_DIR}/Contents/Resources" -cp "${SWIFT_BIN_DIR}/TetherKitApp" "${APP_DIR}/Contents/MacOS/TetherKit" -sed "s/__TETHERKIT_VERSION__/${VERSION}/g" \ +cp "${SWIFT_BIN_DIR}/TethernetApp" "${APP_DIR}/Contents/MacOS/Tethernet" +sed "s/__TETHERNET_VERSION__/${VERSION}/g" \ "${GUI_DIR}/Resources/App-Info.plist" > "${APP_DIR}/Contents/Info.plist" cp "${GUI_DIR}/Resources/AppIcon.icns" "${APP_DIR}/Contents/Resources/" # cp -a here is mandatory, never install: -# libtetherkit.dylib and libtetherkit.0.dylib are both symlinks to +# libtethernet.dylib and libtethernet.0.dylib are both symlinks to # .0.1.1.dylib. install does not preserve symlinks -- it copies each as an # independent real file, and install_name_tool then gets applied to only # one of them while @rpath happens to resolve to the untouched one: a copy # that is never used gets fixed, the problem persists, and nothing warns. # Recorded in docs/GUI-SPIKE.md section 7. -cp -a "${LIB_DIR}"/libtetherkit*.dylib "${APP_DIR}/Contents/Frameworks/" +cp -a "${LIB_DIR}"/libtethernet*.dylib "${APP_DIR}/Contents/Frameworks/" # ------------------------------------------------------------------------------ # Assemble the privileged-helper payload (embedded into the .app) @@ -129,9 +129,9 @@ PAYLOAD_DIR="${APP_DIR}/Contents/Library/HelperTools" log "Assembling ${PAYLOAD_DIR}" mkdir -p "${PAYLOAD_DIR}" -cp "${SWIFT_BIN_DIR}/tetherkit-helper" "${PAYLOAD_DIR}/com.tetherkit.helper" -cp -a "${LIB_DIR}"/libtetherkit*.dylib "${PAYLOAD_DIR}/" -cp "${GUI_DIR}/Resources/com.tetherkit.helper.plist" "${PAYLOAD_DIR}/" +cp "${SWIFT_BIN_DIR}/tethernet-helper" "${PAYLOAD_DIR}/com.tethernet.helper" +cp -a "${LIB_DIR}"/libtethernet*.dylib "${PAYLOAD_DIR}/" +cp "${GUI_DIR}/Resources/com.tethernet.helper.plist" "${PAYLOAD_DIR}/" # The install/uninstall scripts also ride in the payload, so a machine that # has only the .app (no repo) can install and uninstall. cp "${SCRIPT_DIR}/install-helper.sh" "${SCRIPT_DIR}/uninstall-helper.sh" "${PAYLOAD_DIR}/" @@ -152,7 +152,7 @@ rm -rf "${DIST_DIR}/helper" # (docs/GUI-SPIKE.md section 7). # # Which file gets patched: -# libusb is a dependency of **libtetherkit.dylib**, not of the executable. +# libusb is a dependency of **libtethernet.dylib**, not of the executable. # Running otool -L on the executable returns nothing and the whole section # is silently skipped: the install looks perfect while the binary still # depends on Homebrew. Recorded in that same section 7. @@ -161,36 +161,55 @@ embed_libusb() { local destination_dir="$1" # The real file (not a symlink) is the patch target. local real_dylib - real_dylib="$(ls "${destination_dir}"/libtetherkit.*.*.*.dylib 2>/dev/null | head -1)" - [[ -n "${real_dylib}" ]] || die "no real libtetherkit file found in ${destination_dir}" + real_dylib="$(ls "${destination_dir}"/libtethernet.*.*.*.dylib 2>/dev/null | head -1)" + [[ -n "${real_dylib}" ]] || die "no real libtethernet file found in ${destination_dir}" local libusb_path libusb_path="$(otool -L "${real_dylib}" | awk '/libusb-1\.0/ {print $1; exit}')" if [[ -z "${libusb_path}" || "${libusb_path}" == @* ]]; then return 0 # no absolute-path dependency, nothing to do fi - [[ -f "${libusb_path}" ]] || die "libtetherkit depends on ${libusb_path}, but the file does not exist" + [[ -f "${libusb_path}" ]] || die "libtethernet depends on ${libusb_path}, but the file does not exist" + + embedded_libusb_path="${libusb_path}" local libusb_name libusb_name="$(basename "${libusb_path}")" cp -a "${libusb_path}" "${destination_dir}/${libusb_name}" - chmod u+w "${destination_dir}/${libusb_name}" install_name_tool -change "${libusb_path}" "@loader_path/${libusb_name}" "${real_dylib}" - install_name_tool -id "@loader_path/${libusb_name}" "${destination_dir}/${libusb_name}" # Re-signing after byte edits is mandatory. # arm64 requires valid signatures on executables and dylibs; # install_name_tool invalidates the original one, after which load is # rejected outright by the kernel (Killed: 9) with no reason in any log. - codesign --force --sign - --timestamp=none "${destination_dir}/${libusb_name}" >/dev/null + # Note: libusb itself is left unmodified and keeps its original ad-hoc signature. codesign --force --sign - --timestamp=none "${real_dylib}" >/dev/null } +embedded_libusb_path="" log "Embedding libusb" embed_libusb "${APP_DIR}/Contents/Frameworks" embed_libusb "${PAYLOAD_DIR}" +# ------------------------------------------------------------------------------ +# Third-party notices and licenses +# ------------------------------------------------------------------------------ +log "Shipping third-party licenses" +LICENSES_DIR="${APP_DIR}/Contents/Resources/Licenses" +mkdir -p "${LICENSES_DIR}" +cp "${REPO_ROOT}/THIRD_PARTY_NOTICES.md" "${LICENSES_DIR}/" + +# Copy the LGPL from the SAME tree as the dylib that was just embedded, so the +# license version can never drift from the binary it describes. +if [[ -n "${embedded_libusb_path}" ]]; then + libusb_root="$(dirname "$(dirname "$(realpath "${embedded_libusb_path}")")")" + [[ -f "${libusb_root}/COPYING" ]] || die "no COPYING next to ${embedded_libusb_path}" + cp "${libusb_root}/COPYING" "${LICENSES_DIR}/libusb-COPYING" +elif [[ -f "${REPO_ROOT}/docs/licenses/libusb-COPYING" ]]; then + cp "${REPO_ROOT}/docs/licenses/libusb-COPYING" "${LICENSES_DIR}/libusb-COPYING" +fi + # ------------------------------------------------------------------------------ # Strip the rpath that points at the build directory # @@ -210,8 +229,8 @@ strip_build_rpath() { codesign --force --sign - --timestamp=none "${binary}" >/dev/null fi } -strip_build_rpath "${APP_DIR}/Contents/MacOS/TetherKit" -strip_build_rpath "${PAYLOAD_DIR}/com.tetherkit.helper" +strip_build_rpath "${APP_DIR}/Contents/MacOS/Tethernet" +strip_build_rpath "${PAYLOAD_DIR}/com.tethernet.helper" # ------------------------------------------------------------------------------ # Signing @@ -223,7 +242,7 @@ strip_build_rpath "${PAYLOAD_DIR}/com.tetherkit.helper" # the other way around, any inner change invalidates the outer signature. # ------------------------------------------------------------------------------ log "Ad-hoc signing" -codesign --force --sign - --timestamp=none "${PAYLOAD_DIR}/com.tetherkit.helper" >/dev/null +codesign --force --sign - --timestamp=none "${PAYLOAD_DIR}/com.tethernet.helper" >/dev/null codesign --force --sign - --timestamp=none --deep "${APP_DIR}" >/dev/null # ------------------------------------------------------------------------------ @@ -235,13 +254,13 @@ codesign --verify --deep --strict "${APP_DIR}" || die ".app signature verificati # LC_BUILD_VERSION's sdk stamp must be >= 26, otherwise the whole app # renders legacy traffic lights on Tahoe (AGENTS.md pitfall 19). Enforce # the "recheck on every release" rule in the script, not in memory. -sdk_stamp="$(otool -l "${APP_DIR}/Contents/MacOS/TetherKit" | awk '/LC_BUILD_VERSION/{s=1} s && / sdk /{print $NF; exit}')" +sdk_stamp="$(otool -l "${APP_DIR}/Contents/MacOS/Tethernet" | awk '/LC_BUILD_VERSION/{s=1} s && / sdk /{print $NF; exit}')" if [[ "$(printf '%s' "${sdk_stamp}" | cut -d. -f1)" -lt 26 ]]; then die "sdk stamp is ${sdk_stamp:-missing}; Tahoe would render legacy chrome (need >= 26, see -platform_version in Package.swift)" fi echo " OK LC_BUILD_VERSION sdk = ${sdk_stamp}" -if otool -L "${APP_DIR}/Contents/Frameworks"/libtetherkit.*.*.*.dylib | grep -q '/opt/homebrew'; then +if otool -L "${APP_DIR}/Contents/Frameworks"/libtethernet.*.*.*.dylib | grep -q '/opt/homebrew'; then echo " WARNING the artifact still depends on /opt/homebrew; it will not run once copied to another machine" else echo " OK no absolute-path Homebrew dependency" diff --git a/gui/Scripts/install-helper.sh b/gui/Scripts/install-helper.sh index fd73c00..18a9789 100755 --- a/gui/Scripts/install-helper.sh +++ b/gui/Scripts/install-helper.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Install tetherkit-helper and register the LaunchDaemon. **Must run as root.** +# Install tethernet-helper and register the LaunchDaemon. **Must run as root.** # # sudo ./gui/Scripts/install-helper.sh # @@ -21,20 +21,20 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # Locate the payload directory by priority: -# 1. TETHERKIT_HELPER_DIR -- packaging / CI can point it wherever it wants; +# 1. TETHERNET_HELPER_DIR -- packaging / CI can point it wherever it wants; # 2. the script's own directory -- the copy packed into the .app payload # takes this branch: the payload sits right next to it; # 3. the repo's default artifact location -- running # sudo ./gui/Scripts/install-helper.sh inside the repo takes this one. -if [[ -n "${TETHERKIT_HELPER_DIR:-}" ]]; then - SOURCE_DIR="${TETHERKIT_HELPER_DIR}" -elif [[ -f "${SCRIPT_DIR}/com.tetherkit.helper" ]]; then +if [[ -n "${TETHERNET_HELPER_DIR:-}" ]]; then + SOURCE_DIR="${TETHERNET_HELPER_DIR}" +elif [[ -f "${SCRIPT_DIR}/com.tethernet.helper" ]]; then SOURCE_DIR="${SCRIPT_DIR}" else - SOURCE_DIR="$(cd -- "${SCRIPT_DIR}/../.." && pwd)/dist/TetherKit.app/Contents/Library/HelperTools" + SOURCE_DIR="$(cd -- "${SCRIPT_DIR}/../.." && pwd)/dist/Tethernet.app/Contents/Library/HelperTools" fi -LABEL="com.tetherkit.helper" +LABEL="com.tethernet.helper" TOOLS_DIR="/Library/PrivilegedHelperTools" INSTALL_PATH="${TOOLS_DIR}/${LABEL}" PLIST_PATH="/Library/LaunchDaemons/${LABEL}.plist" @@ -64,11 +64,11 @@ fi log "Installing to ${INSTALL_PATH}" mkdir -p "${TOOLS_DIR}" -# cp -a rather than install: libtetherkit's two symlinks must survive as +# cp -a rather than install: libtethernet's two symlinks must survive as # symlinks. install copies each of them into an independent real file, and # after that there is no telling which copy @rpath ends up loading. cp -a "${SOURCE_DIR}/${LABEL}" "${INSTALL_PATH}" -cp -a "${SOURCE_DIR}"/libtetherkit*.dylib "${TOOLS_DIR}/" +cp -a "${SOURCE_DIR}"/libtethernet*.dylib "${TOOLS_DIR}/" if compgen -G "${SOURCE_DIR}/libusb-*.dylib" >/dev/null; then cp -a "${SOURCE_DIR}"/libusb-*.dylib "${TOOLS_DIR}/" fi @@ -97,9 +97,9 @@ launchctl bootstrap system "${PLIST_PATH}" # registration there is no process yet -- that is normal, not a failure. # Validating means checking that the service is registered. if launchctl print "system/${LABEL}" >/dev/null 2>&1; then - log "Done. TetherKit.app picks it up automatically; restarting the App is unnecessary." + log "Done. Tethernet.app picks it up automatically; restarting the App is unnecessary." else - die "Service still not found after registration; check /var/log/tetherkit-helper.log" + die "Service still not found after registration; check /var/log/tethernet-helper.log" fi cat <\033[0m %s\n' "$1"; } @@ -27,7 +27,7 @@ fi log "Deleting files" rm -f "${PLIST_PATH}" "${INSTALL_PATH}" -rm -f "${TOOLS_DIR}"/libtetherkit*.dylib "${TOOLS_DIR}"/libusb-*.dylib +rm -f "${TOOLS_DIR}"/libtethernet*.dylib "${TOOLS_DIR}"/libusb-*.dylib # Backstop: if the helper was ever force-killed, the state file may still # list interfaces it never destroyed. diff --git a/gui/Sources/CTetherKit/include/tetherkit_c.h b/gui/Sources/CTetherKit/include/tetherkit_c.h deleted file mode 120000 index 6ff53f8..0000000 --- a/gui/Sources/CTetherKit/include/tetherkit_c.h +++ /dev/null @@ -1 +0,0 @@ -../../../../include/tetherkit/capi/tetherkit_c.h \ No newline at end of file diff --git a/gui/Sources/CTethernet/include/tethernet_c.h b/gui/Sources/CTethernet/include/tethernet_c.h new file mode 120000 index 0000000..4928e77 --- /dev/null +++ b/gui/Sources/CTethernet/include/tethernet_c.h @@ -0,0 +1 @@ +../../../../include/tethernet/capi/tethernet_c.h \ No newline at end of file diff --git a/gui/Sources/CTetherKit/shim.c b/gui/Sources/CTethernet/shim.c similarity index 64% rename from gui/Sources/CTetherKit/shim.c rename to gui/Sources/CTethernet/shim.c index 3c57b34..8765581 100644 --- a/gui/Sources/CTetherKit/shim.c +++ b/gui/Sources/CTethernet/shim.c @@ -1,18 +1,18 @@ // SwiftPM requires every C target to have at least one source file, otherwise // it refuses to build. // -// The entire content of this target is actually include/tetherkit_c.h (a +// The entire content of this target is actually include/tethernet_c.h (a // symlink to the C++-side header) -- the real implementation lives in -// libtetherkit.dylib. This empty translation unit exists only to satisfy +// libtethernet.dylib. This empty translation unit exists only to satisfy // SwiftPM's formal requirement. // // It also does something useful along the way: wrapping the header catches // problems like "the header contains syntax only C++ accepts" at compile time, // rather than waiting for the Swift-side import to fail. -#include "tetherkit_c.h" +#include "tethernet_c.h" // A non-exported dummy symbol to prevent some linkers from warning about a // completely empty object file. -static const int tetherkit_c_shim_anchor = 0; -const int* tetherkit_c_shim_unused(void); -const int* tetherkit_c_shim_unused(void) { return &tetherkit_c_shim_anchor; } +static const int tethernet_c_shim_anchor = 0; +const int* tethernet_c_shim_unused(void); +const int* tethernet_c_shim_unused(void) { return &tethernet_c_shim_anchor; } diff --git a/gui/Sources/TetherKitApp/AppModel.swift b/gui/Sources/TethernetApp/AppModel.swift similarity index 98% rename from gui/Sources/TetherKitApp/AppModel.swift rename to gui/Sources/TethernetApp/AppModel.swift index 640b033..9202407 100644 --- a/gui/Sources/TetherKitApp/AppModel.swift +++ b/gui/Sources/TethernetApp/AppModel.swift @@ -1,8 +1,8 @@ import Foundation import Observation import SwiftUI -import TetherKitCore -import TetherKitIPC +import TethernetCore +import TethernetIPC /// Instantaneous rate measured by one sample. struct ThroughputSample: Identifiable { @@ -81,7 +81,7 @@ enum HelperAvailability: Equatable { /// why it has to be blocked on the install guide page. This one means /// **the protocol is still compatible but the versions differ** -- the /// helper works as usual, it just still carries the pre-upgrade -/// libtetherkit, so none of the problems the new version fixes are fixed +/// libtethernet, so none of the problems the new version fixes are fixed /// on the side that actually does the work (`brew upgrade` only swaps the /// .app and cannot touch /Library/PrivilegedHelperTools). /// @@ -147,7 +147,7 @@ final class AppModel { /// Interface language preference. Changing it does **three** things at /// once: switch the Swift-side string table, push the language to - /// libtetherkit (otherwise another language leaks into the log card), + /// libtethernet (otherwise another language leaks into the log card), /// and push it to the helper (it runs as root under launchd and cannot /// see the user's language preference). /// @@ -206,7 +206,7 @@ final class AppModel { /// The explanation shown in the system authorization dialog. /// - /// Without this sentence the box only says "TetherKit wants to make + /// Without this sentence the box only says "Tethernet wants to make /// changes" and the user has no way to judge whether to approve. A /// computed property rather than `static let`: `static let` is /// evaluated only once, when the type is first used, so a later language @@ -256,7 +256,7 @@ final class AppModel { // MARK: - Lifecycle - /// Apply a language preference: this process -> libtetherkit -> helper, + /// Apply a language preference: this process -> libtethernet -> helper, /// none of them may be missed. /// /// UserDefaults is written only here and read in @@ -264,7 +264,7 @@ final class AppModel { /// constant, so renaming it cannot miss one of them. func applyLanguage(_ preference: LanguagePreference) { let resolved = L10n.apply(preference) - TetherKitLibrary.setLanguage(resolved) + TethernetLibrary.setLanguage(resolved) UserDefaults.standard.set(preference.rawValue, forKey: Self.languageDefaultsKey) languageRevision += 1 Task { await client.setLanguage(resolved) } @@ -285,7 +285,7 @@ final class AppModel { } } - private static let languageDefaultsKey = "TetherKitLanguagePreference" + private static let languageDefaultsKey = "TethernetLanguagePreference" func start() { guard pollingTask == nil else { return } @@ -612,7 +612,7 @@ final class AppModel { /// /// The **library** version rather than the app version in Info.plist: /// what gets installed into /Library/PrivilegedHelperTools is a copy of - /// the libtetherkit inside the .app (see the payload assembly in + /// the libtethernet inside the .app (see the payload assembly in /// build-gui.sh), and only same-origin values compare meaningfully. /// Moreover the bare executable from `swift run` has no bundle at all, /// so on that path the Info.plist check would fail outright. @@ -624,7 +624,7 @@ final class AppModel { /// 500 ms polling loop, and there is no reason to re-run the regex /// every time. private static let bundledVersion = - HelperConstants.semanticVersion(of: TetherKitLibrary.versionInfo.version) + HelperConstants.semanticVersion(of: TethernetLibrary.versionInfo.version) /// Whether the installed helper is the same version as the one the app /// ships. Returns nil when they match. @@ -750,7 +750,7 @@ final class AppModel { /// version only lights up the hint in the management row -- it never /// interrupts with a dialog, since knowing about an update is a "by the /// way" matter that does not deserve a modal box. - /// `defaults write com.tetherkit.app updateCheckDisabled -bool YES` + /// `defaults write com.tethernet.app updateCheckDisabled -bool YES` /// turns it off. private func checkForUpdatesQuietly() async { guard let current = UpdateChecker.currentVersion else { return } diff --git a/gui/Sources/TetherKitApp/DesignSystem.swift b/gui/Sources/TethernetApp/DesignSystem.swift similarity index 99% rename from gui/Sources/TetherKitApp/DesignSystem.swift rename to gui/Sources/TethernetApp/DesignSystem.swift index 5a7a28f..9696853 100644 --- a/gui/Sources/TetherKitApp/DesignSystem.swift +++ b/gui/Sources/TethernetApp/DesignSystem.swift @@ -1,5 +1,5 @@ import SwiftUI -import TetherKitIPC +import TethernetIPC /// The design language of the UI. /// diff --git a/gui/Sources/TetherKitApp/FinderAlias.swift b/gui/Sources/TethernetApp/FinderAlias.swift similarity index 98% rename from gui/Sources/TetherKitApp/FinderAlias.swift rename to gui/Sources/TethernetApp/FinderAlias.swift index be1cc8a..854dc8d 100644 --- a/gui/Sources/TetherKitApp/FinderAlias.swift +++ b/gui/Sources/TethernetApp/FinderAlias.swift @@ -1,5 +1,5 @@ import Foundation -import TetherKitIPC +import TethernetIPC /// Maintain a Finder alias pointing at this .app in the Applications /// directory, so Spotlight can find it. @@ -24,7 +24,7 @@ import TetherKitIPC enum FinderAlias { /// The alias file name. Without .app -- it is a bookmark file, not a /// bundle; the name Spotlight shows is exactly this. - private static let aliasName = "TetherKit" + private static let aliasName = "Tethernet" /// Make sure the alias exists and points at bundleURL. Returns a /// plain-language result (the --install-finder-alias mode prints it diff --git a/gui/Sources/TetherKitApp/HelperClient.swift b/gui/Sources/TethernetApp/HelperClient.swift similarity index 97% rename from gui/Sources/TetherKitApp/HelperClient.swift rename to gui/Sources/TethernetApp/HelperClient.swift index e617337..6aa8ed5 100644 --- a/gui/Sources/TetherKitApp/HelperClient.swift +++ b/gui/Sources/TethernetApp/HelperClient.swift @@ -1,7 +1,7 @@ import Foundation -import TetherKitIPC +import TethernetIPC -/// The client that talks to tetherkit-helper. +/// The client that talks to tethernet-helper. /// /// It wraps "reply block based XPC" into async/await. It looks fiddly, but /// one thing has to be handled with care: the NSXPCConnection error block @@ -94,7 +94,7 @@ final class HelperClient { } let created = NSXPCConnection(machServiceName: HelperConstants.machServiceName, options: .privileged) - created.remoteObjectInterface = NSXPCInterface(with: TetherKitHelperProtocol.self) + created.remoteObjectInterface = NSXPCInterface(with: TethernetHelperProtocol.self) // The cached connection has to be discarded once it breaks, // otherwise every later call hits a dead connection and it @@ -114,14 +114,14 @@ final class HelperClient { /// Issue one call. `body` receives the proxy and the guard, and is /// responsible for starting the request and delivering the result. private func invoke( - _ body: @escaping (TetherKitHelperProtocol, ContinuationGuard) -> Void + _ body: @escaping (TethernetHelperProtocol, ContinuationGuard) -> Void ) async throws -> T { try await withCheckedThrowingContinuation { continuation in let guarded = ContinuationGuard(continuation) let proxy = connection().remoteObjectProxyWithErrorHandler { error in guarded.resume(throwing: Failure.unreachable(error.localizedDescription)) } - guard let typed = proxy as? TetherKitHelperProtocol else { + guard let typed = proxy as? TethernetHelperProtocol else { guarded.resume(throwing: Failure.malformedResponse) return } @@ -241,7 +241,7 @@ final class HelperClient { } private func invokeVoid( - _ body: @escaping (TetherKitHelperProtocol, ContinuationGuard) -> Void + _ body: @escaping (TethernetHelperProtocol, ContinuationGuard) -> Void ) async throws { _ = try await invoke(body) } diff --git a/gui/Sources/TetherKitApp/HelperInstaller.swift b/gui/Sources/TethernetApp/HelperInstaller.swift similarity index 99% rename from gui/Sources/TetherKitApp/HelperInstaller.swift rename to gui/Sources/TethernetApp/HelperInstaller.swift index 9bd59b3..b309a68 100644 --- a/gui/Sources/TetherKitApp/HelperInstaller.swift +++ b/gui/Sources/TethernetApp/HelperInstaller.swift @@ -1,6 +1,6 @@ import Foundation import Security -import TetherKitIPC +import TethernetIPC /// Install the privileged helper payload embedded in the .app as a /// LaunchDaemon, or uninstall it cleanly again -- the whole mechanism @@ -46,7 +46,7 @@ import TetherKitIPC /// the credential into exactly the same true root as sudo, merges /// stderr into stdout (AEWP's pipe only takes stdout), and then execs /// the install-helper.sh in the same directory. -/// See TetherKitHelper/InstallerMode.swift. +/// See TethernetHelper/InstallerMode.swift. /// /// Why success and failure are not read from the exit code: /// diff --git a/gui/Sources/TetherKitApp/KeyboardProbe.swift b/gui/Sources/TethernetApp/KeyboardProbe.swift similarity index 97% rename from gui/Sources/TetherKitApp/KeyboardProbe.swift rename to gui/Sources/TethernetApp/KeyboardProbe.swift index 20ab76c..3ebedf9 100644 --- a/gui/Sources/TetherKitApp/KeyboardProbe.swift +++ b/gui/Sources/TethernetApp/KeyboardProbe.swift @@ -1,9 +1,9 @@ #if DEBUG import AppKit -import TetherKitUIModel +import TethernetUIModel /// Headless keyboard probe (DEBUG only; opt-in via -/// `TETHERKIT_SB_PROBE=walk|matrix|fka_on|full`; any other value +/// `TETHERNET_SB_PROBE=walk|matrix|fka_on|full`; any other value /// fails with exit 1 rather than falling back to a mode). /// /// Walks the keyboard contract through the REAL dispatch chain — @@ -22,11 +22,11 @@ import TetherKitUIModel /// * `matrix` — one pass per sidebar page: two Tabs (recorded), a /// bare Down that must step to the next page, an Up that must step /// back, then one Shift+Tab (recorded). -/// * `fka_on` — FKA forced on via `TETHERKIT_FKA_OVERRIDE=1`: one +/// * `fka_on` — FKA forced on via `TETHERNET_FKA_OVERRIDE=1`: one /// Return then one Tab, recorded; only the override's liveness is /// asserted. /// * `full` — the FKA-off contract suite (FKA forced off via -/// `TETHERKIT_FKA_OVERRIDE=0`), round 11. Space and Return are sent before any Tab, so no Tab can +/// `TETHERNET_FKA_OVERRIDE=0`), round 11. Space and Return are sent before any Tab, so no Tab can /// first park a content button under them (the run-1 hazard: Space /// landing on a real button once hijacked the whole probe run): /// @@ -78,7 +78,7 @@ enum KeyboardProbe { static func armIfNeeded() { guard !armed, - let mode = ProcessInfo.processInfo.environment["TETHERKIT_SB_PROBE"] else { return } + let mode = ProcessInfo.processInfo.environment["TETHERNET_SB_PROBE"] else { return } armed = true print("PROBE armed mode=\(mode) fka=\(SelfAccessibility.isFullKeyboardAccessOn)") scheduleWatchdog() @@ -95,14 +95,14 @@ enum KeyboardProbe { case "full": runFullSuite() default: - failures.append("unknown TETHERKIT_SB_PROBE mode \(mode)") + failures.append("unknown TETHERNET_SB_PROBE mode \(mode)") finish() } } private static func runFkaOnSuite() { if !SelfAccessibility.isFullKeyboardAccessOn { - failures.append("TETHERKIT_FKA_OVERRIDE did not force FKA on") + failures.append("TETHERNET_FKA_OVERRIDE did not force FKA on") } after(initialSettleSeconds) { _ = observe("fka-on-base") @@ -199,7 +199,7 @@ enum KeyboardProbe { private static func runFullSuite() { if SelfAccessibility.isFullKeyboardAccessOn { - failures.append("full suite requires FKA off (set TETHERKIT_FKA_OVERRIDE=0 or disable Full Keyboard Access in System Settings)") + failures.append("full suite requires FKA off (set TETHERNET_FKA_OVERRIDE=0 or disable Full Keyboard Access in System Settings)") } after(initialSettleSeconds) { // Capture the pinned selection BEFORE sending any key: the diff --git a/gui/Sources/TetherKitApp/MainWindowManager.swift b/gui/Sources/TethernetApp/MainWindowManager.swift similarity index 99% rename from gui/Sources/TetherKitApp/MainWindowManager.swift rename to gui/Sources/TethernetApp/MainWindowManager.swift index 7ab0dd0..ebc1966 100644 --- a/gui/Sources/TetherKitApp/MainWindowManager.swift +++ b/gui/Sources/TethernetApp/MainWindowManager.swift @@ -1,7 +1,7 @@ import AppKit import SwiftUI -import TetherKitIPC -import TetherKitUIModel +import TethernetIPC +import TethernetUIModel /// The main window. `performKeyEquivalent` is AppKit's window-level /// key-equivalent phase: it runs BEFORE the raw key reaches the first diff --git a/gui/Sources/TetherKitApp/SelfAccessibility.swift b/gui/Sources/TethernetApp/SelfAccessibility.swift similarity index 99% rename from gui/Sources/TetherKitApp/SelfAccessibility.swift rename to gui/Sources/TethernetApp/SelfAccessibility.swift index e551157..3a62632 100644 --- a/gui/Sources/TetherKitApp/SelfAccessibility.swift +++ b/gui/Sources/TethernetApp/SelfAccessibility.swift @@ -19,12 +19,12 @@ enum SelfAccessibility { #if DEBUG /// Probe override for the pane toggle - /// (`TETHERKIT_FKA_OVERRIDE=0|1`). Headless keyboard probes must + /// (`TETHERNET_FKA_OVERRIDE=0|1`). Headless keyboard probes must /// pin the pane state regardless of what System Settings currently /// shows; DEBUG-only so shipped builds always read the real /// preference. private static var fullKeyboardAccessOverride: Bool? { - guard let raw = ProcessInfo.processInfo.environment["TETHERKIT_FKA_OVERRIDE"] else { + guard let raw = ProcessInfo.processInfo.environment["TETHERNET_FKA_OVERRIDE"] else { return nil } return raw == "1" diff --git a/gui/Sources/TetherKitApp/TetherKitApp.swift b/gui/Sources/TethernetApp/TethernetApp.swift similarity index 96% rename from gui/Sources/TetherKitApp/TetherKitApp.swift rename to gui/Sources/TethernetApp/TethernetApp.swift index 53d6853..3055efb 100644 --- a/gui/Sources/TetherKitApp/TetherKitApp.swift +++ b/gui/Sources/TethernetApp/TethernetApp.swift @@ -1,12 +1,12 @@ import AppKit import SwiftUI -import TetherKitIPC +import TethernetIPC -/// The graphical interface entry point of TetherKit. +/// The graphical interface entry point of Tethernet. /// /// The app runs as an **ordinary user** and needs no privileges of its own. /// Everything that requires root (creating the virtual interface, opening -/// BPF, configuring the IP) is handed to tetherkit-helper, each call +/// BPF, configuring the IP) is handed to tethernet-helper, each call /// carrying an authorization credential the user confirmed. See /// docs/GUI-ARCHITECTURE.md. /// @@ -31,9 +31,9 @@ import TetherKitIPC /// `sdk` stamp, not from which layer owns the window; see /// `MainWindowManager` and the linker settings in `Package.swift`. @main -struct TetherKitApplication: App { +struct TethernetApplication: App { @State private var model = AppModel() - @NSApplicationDelegateAdaptor(TetherKitAppDelegate.self) private var appDelegate + @NSApplicationDelegateAdaptor(TethernetAppDelegate.self) private var appDelegate init() { // The mode used by the CI smoke test and for manual rebuilds: @@ -57,7 +57,7 @@ struct TetherKitApplication: App { if NSApplication.shared.activationPolicy() == .prohibited { NSApplication.shared.setActivationPolicy(.regular) } - TetherKitAppDelegate.launchModel = model + TethernetAppDelegate.launchModel = model // Normal launch: check it in the background as a side task -- add // it if missing, and rewrite it if the target drifted after brew // upgrade changed the Cellar path. @@ -96,7 +96,7 @@ struct TetherKitApplication: App { } } -/// TetherKit's own items in the app menu. +/// Tethernet's own items in the app menu. /// /// Why this is wrapped in a View instead of spreading the Buttons directly /// inside the CommandGroup: @@ -203,7 +203,7 @@ struct MainWindowRoot: View { // us an instance, but setting instance state on it from App.init // is init-order dependent, so the launch slot is the deterministic // channel. -final class TetherKitAppDelegate: NSObject, NSApplicationDelegate { +final class TethernetAppDelegate: NSObject, NSApplicationDelegate { static var launchModel: AppModel? func applicationDidFinishLaunching(_ notification: Notification) { diff --git a/gui/Sources/TetherKitApp/UpdateChecker.swift b/gui/Sources/TethernetApp/UpdateChecker.swift similarity index 97% rename from gui/Sources/TetherKitApp/UpdateChecker.swift rename to gui/Sources/TethernetApp/UpdateChecker.swift index affcc66..385fdc0 100644 --- a/gui/Sources/TetherKitApp/UpdateChecker.swift +++ b/gui/Sources/TethernetApp/UpdateChecker.swift @@ -1,5 +1,5 @@ import Foundation -import TetherKitIPC +import TethernetIPC /// Ask GitHub Releases whether a newer version exists. /// @@ -16,7 +16,7 @@ import TetherKitIPC /// It only queries GitHub's public REST API (releases/latest) and /// carries no information about this machine. The automatic check runs /// at most once a day and is silent on failure; -/// `defaults write com.tetherkit.app updateCheckDisabled -bool YES` turns +/// `defaults write com.tethernet.app updateCheckDisabled -bool YES` turns /// it off completely. enum UpdateChecker { struct Release: Equatable { @@ -88,7 +88,7 @@ enum UpdateChecker { // MARK: - Implementation private static let endpoint = - URL(string: "https://api.github.com/repos/justsrc/TetherKit/releases/latest")! + URL(string: "https://api.github.com/repos/justsrc/Tethernet/releases/latest")! private struct Payload: Decodable { let tagName: String diff --git a/gui/Sources/TetherKitApp/Views/ConnectionPage.swift b/gui/Sources/TethernetApp/Views/ConnectionPage.swift similarity index 98% rename from gui/Sources/TetherKitApp/Views/ConnectionPage.swift rename to gui/Sources/TethernetApp/Views/ConnectionPage.swift index 4371732..084f38a 100644 --- a/gui/Sources/TetherKitApp/Views/ConnectionPage.swift +++ b/gui/Sources/TethernetApp/Views/ConnectionPage.swift @@ -1,5 +1,5 @@ import SwiftUI -import TetherKitIPC +import TethernetIPC /// The "Connection" page: the app's single point of action. Status /// hero, environment warning (only on failure), device selection. diff --git a/gui/Sources/TetherKitApp/Views/ContentView.swift b/gui/Sources/TethernetApp/Views/ContentView.swift similarity index 98% rename from gui/Sources/TetherKitApp/Views/ContentView.swift rename to gui/Sources/TethernetApp/Views/ContentView.swift index 8b98c8d..1854e56 100644 --- a/gui/Sources/TetherKitApp/Views/ContentView.swift +++ b/gui/Sources/TethernetApp/Views/ContentView.swift @@ -1,5 +1,5 @@ import SwiftUI -import TetherKitIPC +import TethernetIPC /// Main window root view: gates on helper availability, then shows /// the sidebar shell once the helper is ready. @@ -48,7 +48,7 @@ struct ContentView: View { Button(L(.openReleasePage)) { NSWorkspace.shared.open(release.pageURL) } Button(L(.copyBrewUpgradeCommand)) { NSPasteboard.general.clearContents() - NSPasteboard.general.setString("brew upgrade tetherkit", forType: .string) + NSPasteboard.general.setString("brew upgrade tethernet", forType: .string) } Button(L(.ok), role: .cancel) {} } else { @@ -78,7 +78,7 @@ struct ContentView: View { /// The body of the "Check for Updates..." popup. /// /// The brew command name matches the GUI formula in the tap - /// (tetherkit) -- when the CLI was renamed to tetherkit-cli this name + /// (tethernet) -- when the CLI was renamed to tethernet-cli this name /// went to the GUI; users who build by hand follow the second /// sentence. private var updateCheckDescription: String { diff --git a/gui/Sources/TetherKitApp/Views/DeviceCard.swift b/gui/Sources/TethernetApp/Views/DeviceCard.swift similarity index 99% rename from gui/Sources/TetherKitApp/Views/DeviceCard.swift rename to gui/Sources/TethernetApp/Views/DeviceCard.swift index fdc183a..287d0ae 100644 --- a/gui/Sources/TetherKitApp/Views/DeviceCard.swift +++ b/gui/Sources/TethernetApp/Views/DeviceCard.swift @@ -1,5 +1,5 @@ import SwiftUI -import TetherKitIPC +import TethernetIPC /// Device selection and session parameters. /// diff --git a/gui/Sources/TetherKitApp/Views/LogCard.swift b/gui/Sources/TethernetApp/Views/LogCard.swift similarity index 99% rename from gui/Sources/TetherKitApp/Views/LogCard.swift rename to gui/Sources/TethernetApp/Views/LogCard.swift index d0a7670..0164f66 100644 --- a/gui/Sources/TetherKitApp/Views/LogCard.swift +++ b/gui/Sources/TethernetApp/Views/LogCard.swift @@ -1,6 +1,6 @@ import SwiftUI -import TetherKitIPC -import TetherKitUIModel +import TethernetIPC +import TethernetUIModel /// Log panel. /// diff --git a/gui/Sources/TetherKitApp/Views/LogPage.swift b/gui/Sources/TethernetApp/Views/LogPage.swift similarity index 98% rename from gui/Sources/TetherKitApp/Views/LogPage.swift rename to gui/Sources/TethernetApp/Views/LogPage.swift index 7b6058b..3ac9f47 100644 --- a/gui/Sources/TetherKitApp/Views/LogPage.swift +++ b/gui/Sources/TethernetApp/Views/LogPage.swift @@ -1,5 +1,5 @@ import SwiftUI -import TetherKitIPC +import TethernetIPC /// The "Log" page. /// diff --git a/gui/Sources/TetherKitApp/Views/ManagePage.swift b/gui/Sources/TethernetApp/Views/ManagePage.swift similarity index 99% rename from gui/Sources/TetherKitApp/Views/ManagePage.swift rename to gui/Sources/TethernetApp/Views/ManagePage.swift index 2f74764..c0b4fa9 100644 --- a/gui/Sources/TetherKitApp/Views/ManagePage.swift +++ b/gui/Sources/TethernetApp/Views/ManagePage.swift @@ -1,5 +1,5 @@ import SwiftUI -import TetherKitIPC +import TethernetIPC /// The "Manage" page: privileged helper status and maintenance actions. struct ManagePage: View { diff --git a/gui/Sources/TetherKitApp/Views/MenuBarPanel.swift b/gui/Sources/TethernetApp/Views/MenuBarPanel.swift similarity index 99% rename from gui/Sources/TetherKitApp/Views/MenuBarPanel.swift rename to gui/Sources/TethernetApp/Views/MenuBarPanel.swift index 181eeff..b6f1ac0 100644 --- a/gui/Sources/TetherKitApp/Views/MenuBarPanel.swift +++ b/gui/Sources/TethernetApp/Views/MenuBarPanel.swift @@ -1,6 +1,6 @@ import AppKit import SwiftUI -import TetherKitIPC +import TethernetIPC /// The permanent menu bar label: a status icon plus live rates. /// diff --git a/gui/Sources/TetherKitApp/Views/NetworkCard.swift b/gui/Sources/TethernetApp/Views/NetworkCard.swift similarity index 99% rename from gui/Sources/TetherKitApp/Views/NetworkCard.swift rename to gui/Sources/TethernetApp/Views/NetworkCard.swift index a0abb42..8e9a8e1 100644 --- a/gui/Sources/TetherKitApp/Views/NetworkCard.swift +++ b/gui/Sources/TethernetApp/Views/NetworkCard.swift @@ -1,7 +1,7 @@ import SwiftUI -import TetherKitCore -import TetherKitIPC -import TetherKitUIModel +import TethernetCore +import TethernetIPC +import TethernetUIModel /// Network access mode configuration: DHCP or a static IP. /// @@ -26,7 +26,7 @@ struct NetworkCard: View { private var interfaceReady: Bool { !model.status.systemInterface.isEmpty } private var canAddDNS: Bool { - // The cap matches TK_DNS_MAX in the C ABI; extra entries would + // The cap matches TN_DNS_MAX in the C ABI; extra entries would // be dropped, so it is better not to allow adding them at all. model.networkConfiguration.dnsServers.count < 4 } diff --git a/gui/Sources/TetherKitApp/Views/SidebarShell.swift b/gui/Sources/TethernetApp/Views/SidebarShell.swift similarity index 98% rename from gui/Sources/TetherKitApp/Views/SidebarShell.swift rename to gui/Sources/TethernetApp/Views/SidebarShell.swift index 7633ddc..771583e 100644 --- a/gui/Sources/TetherKitApp/Views/SidebarShell.swift +++ b/gui/Sources/TethernetApp/Views/SidebarShell.swift @@ -1,6 +1,6 @@ import SwiftUI -import TetherKitIPC -import TetherKitUIModel +import TethernetIPC +import TethernetUIModel /// Main window frame: sidebar plus content area. /// @@ -49,10 +49,10 @@ struct SidebarShell: View { init(model: AppModel) { self.model = model #if DEBUG - // Keyboard-probe pin (`TETHERKIT_SB_SELECT=`): the + // Keyboard-probe pin (`TETHERNET_SB_SELECT=`): the // probe must not depend on the persisted selection — real pages // carry destructive buttons a stray synthetic Space would fire. - if let pinned = ProcessInfo.processInfo.environment["TETHERKIT_SB_SELECT"], + if let pinned = ProcessInfo.processInfo.environment["TETHERNET_SB_SELECT"], UserDefaults.standard.string(forKey: SidebarPage.storageKey) != pinned { UserDefaults.standard.set(pinned, forKey: SidebarPage.storageKey) } @@ -204,7 +204,7 @@ struct SidebarShell: View { .frame(width: 28, height: 28) } VStack(alignment: .leading, spacing: 1) { - Text(verbatim: "TetherKit") + Text(verbatim: "Tethernet") .font(.callout.weight(.medium)) if let version = Self.bundleVersion { Text(verbatim: version) @@ -506,7 +506,7 @@ private struct InstantButtonStyle: PrimitiveButtonStyle { /// and the window-level Return handler in MainWindowManager — one /// literal, two writers of the same default. extension SidebarPage { - static let storageKey = "tetherkit.sidebar.page" + static let storageKey = "tethernet.sidebar.page" /// The page whose row the SYSTEM currently reports as the keyboard /// focused element — read on the system-wide element and accepted diff --git a/gui/Sources/TetherKitApp/Views/StatusHeroCard.swift b/gui/Sources/TethernetApp/Views/StatusHeroCard.swift similarity index 99% rename from gui/Sources/TetherKitApp/Views/StatusHeroCard.swift rename to gui/Sources/TethernetApp/Views/StatusHeroCard.swift index 3affe69..f667412 100644 --- a/gui/Sources/TetherKitApp/Views/StatusHeroCard.swift +++ b/gui/Sources/TethernetApp/Views/StatusHeroCard.swift @@ -1,5 +1,5 @@ import SwiftUI -import TetherKitIPC +import TethernetIPC /// The main status card: the visual and operational center of the whole /// interface. diff --git a/gui/Sources/TetherKitApp/Views/ThroughputCard.swift b/gui/Sources/TethernetApp/Views/ThroughputCard.swift similarity index 99% rename from gui/Sources/TetherKitApp/Views/ThroughputCard.swift rename to gui/Sources/TethernetApp/Views/ThroughputCard.swift index a18e9b3..637ebc3 100644 --- a/gui/Sources/TetherKitApp/Views/ThroughputCard.swift +++ b/gui/Sources/TethernetApp/Views/ThroughputCard.swift @@ -1,6 +1,6 @@ import Charts import SwiftUI -import TetherKitIPC +import TethernetIPC /// Throughput and counters. /// diff --git a/gui/Sources/TetherKitApp/Views/UiSelfCapture.swift b/gui/Sources/TethernetApp/Views/UiSelfCapture.swift similarity index 96% rename from gui/Sources/TetherKitApp/Views/UiSelfCapture.swift rename to gui/Sources/TethernetApp/Views/UiSelfCapture.swift index da9cd2f..b8febe5 100644 --- a/gui/Sources/TetherKitApp/Views/UiSelfCapture.swift +++ b/gui/Sources/TethernetApp/Views/UiSelfCapture.swift @@ -13,12 +13,12 @@ import SwiftUI // // Invocation (debug launch arguments): // -// ./TetherKit -SelfCapture /tmp/out.png -TetherKitLanguagePreference english +// ./Tethernet -SelfCapture /tmp/out.png -TethernetLanguagePreference english // // * -SelfCapture : after the window settles, capture it to PNG // and terminate. // * Language is forced through the argument domain of UserDefaults -// (-TetherKitLanguagePreference ); the argument +// (-TethernetLanguagePreference ); the argument // domain outranks normal storage. // // A few dozen lines here buy the "edit UI, auto-capture, view PNG" diff --git a/gui/Sources/TetherKitCore/CInterop.swift b/gui/Sources/TethernetCore/CInterop.swift similarity index 99% rename from gui/Sources/TetherKitCore/CInterop.swift rename to gui/Sources/TethernetCore/CInterop.swift index e5c7b0c..0a3bd90 100644 --- a/gui/Sources/TetherKitCore/CInterop.swift +++ b/gui/Sources/TethernetCore/CInterop.swift @@ -1,4 +1,4 @@ -import CTetherKit +import CTethernet import Foundation // C fixed-size char arrays are imported into Swift as tuples (`char[16]` -> a diff --git a/gui/Sources/TetherKitCore/NetworkConfigurator.swift b/gui/Sources/TethernetCore/NetworkConfigurator.swift similarity index 89% rename from gui/Sources/TetherKitCore/NetworkConfigurator.swift rename to gui/Sources/TethernetCore/NetworkConfigurator.swift index 0c2965c..07ece7c 100644 --- a/gui/Sources/TetherKitCore/NetworkConfigurator.swift +++ b/gui/Sources/TethernetCore/NetworkConfigurator.swift @@ -1,15 +1,15 @@ -import CTetherKit +import CTethernet import Foundation -import TetherKitIPC +import TethernetIPC /// Network access configuration for a virtual interface. /// /// `apply` / `clear` require root (called only from the helper); `query` does not. public enum NetworkConfigurator { - /// Capacity of each address field in tk_ip_config_t, used to locate rows in + /// Capacity of each address field in tn_ip_config_t, used to locate rows in /// the 2D array `dns[4][46]`. - private static let addressStride = Int(TK_ADDRESS_CAPACITY) - private static let maxDNSServers = Int(TK_DNS_MAX) + private static let addressStride = Int(TN_ADDRESS_CAPACITY) + private static let maxDNSServers = Int(TN_DNS_MAX) /// Apply configuration. /// @@ -18,8 +18,8 @@ public enum NetworkConfigurator { /// yourself" cannot give the UI meaningful success/failure feedback. The /// caller must ensure this is not called on the main thread. public static func apply(_ configuration: NetworkConfiguration, to interface: String) throws { - var raw = tk_ip_config_t() - tk_ip_config_init(&raw) + var raw = tn_ip_config_t() + tn_ip_config_init(&raw) raw.mode = configuration.mode.rawValue raw.set_default_route = configuration.setDefaultRoute @@ -38,15 +38,15 @@ public enum NetworkConfigurator { raw.dns_count = Int32(servers.count) } - var error = tk_error_t() - let result = interface.withCString { tk_net_apply($0, &raw, &error) } + var error = tn_error_t() + let result = interface.withCString { tn_net_apply($0, &raw, &error) } try check(result, error) } /// Revoke the IP configuration on an interface. public static func clear(interface: String) throws { - var error = tk_error_t() - let result = interface.withCString { tk_net_clear($0, &error) } + var error = tn_error_t() + let result = interface.withCString { tn_net_clear($0, &error) } try check(result, error) } @@ -56,9 +56,9 @@ public enum NetworkConfigurator { /// yet exist -- the UI also refreshes before a session is up, and surfacing a /// spurious error there would only mislead. public static func query(interface: String) throws -> NetworkState { - var raw = tk_net_state_t() - var error = tk_error_t() - let result = interface.withCString { tk_net_query($0, &raw, &error) } + var raw = tn_net_state_t() + var error = tn_error_t() + let result = interface.withCString { tn_net_query($0, &raw, &error) } try check(result, error) var servers: [String] = [] diff --git a/gui/Sources/TetherKitCore/TetherKitError.swift b/gui/Sources/TethernetCore/TethernetError.swift similarity index 77% rename from gui/Sources/TetherKitCore/TetherKitError.swift rename to gui/Sources/TethernetCore/TethernetError.swift index d2f5fd7..598e003 100644 --- a/gui/Sources/TetherKitCore/TetherKitError.swift +++ b/gui/Sources/TethernetCore/TethernetError.swift @@ -1,12 +1,12 @@ -import CTetherKit -import TetherKitIPC +import CTethernet +import TethernetIPC import Foundation /// Error from the C ABI. /// /// `message` is already text that can be shown directly to the user (the library /// writes it to that standard), so the UI does not need to translate it again. -public struct TetherKitError: LocalizedError, Sendable { +public struct TethernetError: LocalizedError, Sendable { public enum Domain: Int32, Sendable { case generic = 0 case errno = 1 @@ -23,9 +23,9 @@ public struct TetherKitError: LocalizedError, Sendable { /// Whether this is "permission denied" -- the UI uses this to decide between /// requesting authorization and reporting an error. - public var isPermissionDenied: Bool { result == TK_ERR_PERMISSION.rawValue } + public var isPermissionDenied: Bool { result == TN_ERR_PERMISSION.rawValue } - init(result: Int32, error: tk_error_t) { + init(result: Int32, error: tn_error_t) { self.result = result self.domain = Domain(rawValue: error.domain) ?? .generic self.code = error.code @@ -38,7 +38,7 @@ public struct TetherKitError: LocalizedError, Sendable { } public init(message: String) { - self.result = TK_ERR_FAILED.rawValue + self.result = TN_ERR_FAILED.rawValue self.domain = .generic self.code = 0 self.message = message @@ -46,7 +46,7 @@ public struct TetherKitError: LocalizedError, Sendable { } /// Translate a C result code + error struct into a Swift throw. -func check(_ result: tk_result_t, _ error: tk_error_t) throws { - guard result != TK_OK else { return } - throw TetherKitError(result: result.rawValue, error: error) +func check(_ result: tn_result_t, _ error: tn_error_t) throws { + guard result != TN_OK else { return } + throw TethernetError(result: result.rawValue, error: error) } diff --git a/gui/Sources/TetherKitCore/TetherKitLibrary.swift b/gui/Sources/TethernetCore/TethernetLibrary.swift similarity index 82% rename from gui/Sources/TetherKitCore/TetherKitLibrary.swift rename to gui/Sources/TethernetCore/TethernetLibrary.swift index 396d70e..54c6f4b 100644 --- a/gui/Sources/TetherKitCore/TetherKitLibrary.swift +++ b/gui/Sources/TethernetCore/TethernetLibrary.swift @@ -1,17 +1,17 @@ -import CTetherKit +import CTethernet import Foundation -import TetherKitIPC +import TethernetIPC -/// Process-wide interface to libtetherkit: version, environment preflight, +/// Process-wide interface to libtethernet: version, environment preflight, /// device enumeration, and logging. /// /// This group does not require root; both the App and the helper can call it. -public enum TetherKitLibrary { +public enum TethernetLibrary { /// Maximum number of devices enumerated in one call. /// /// 16 is a limit that will never be hit: 16 RNDIS devices plugged into one /// machine at the same time is already absurd, and a fixed array buys "no - /// memory management". If it really is exceeded, tk_list_devices reports the + /// memory management". If it really is exceeded, tn_list_devices reports the /// true total; we simply do not show the extra entries. private static let deviceCapacity = 16 @@ -21,7 +21,7 @@ public enum TetherKitLibrary { /// drain a backlog; larger is meaningless. private static let logCapacity = 256 - /// Sync the UI language to libtetherkit. + /// Sync the UI language to libtethernet. /// /// Must be called -- otherwise the UI would be in one language while library /// logs in the log card would be in another. Logs already queued are not @@ -29,12 +29,12 @@ public enum TetherKitLibrary { /// sooner this is called the better; after a language switch, newly produced /// logs are immediately in the new language. public static func setLanguage(_ language: Language) { - tk_set_language(language.cValue) + tn_set_language(language.cValue) } public static var versionInfo: (version: String, build: String, libusb: String) { - var info = tk_version_info_t() - tk_version(&info) + var info = tn_version_info_t() + tn_version(&info) return (String(fixedCArray: info.text), String(fixedCArray: info.build), String(fixedCArray: info.libusb)) @@ -43,8 +43,8 @@ public enum TetherKitLibrary { /// Runtime environment preflight. Always succeeds -- "environment not /// acceptable" is a result, not a failure. public static func checkEnvironment() -> EnvironmentReport { - var raw = tk_environment_t() - _ = tk_check_environment(&raw) + var raw = tn_environment_t() + _ = tn_check_environment(&raw) let version = versionInfo return EnvironmentReport( isRoot: raw.is_root, @@ -62,11 +62,11 @@ public enum TetherKitLibrary { /// This requires opening the device; while a session is running the device /// is held exclusively, so pass false then to avoid a pointless attempt. public static func listDevices(readStrings: Bool = true) throws -> [DeviceDescriptor] { - var buffer = [tk_device_info_t](repeating: tk_device_info_t(), count: deviceCapacity) + var buffer = [tn_device_info_t](repeating: tn_device_info_t(), count: deviceCapacity) var count = 0 - var error = tk_error_t() + var error = tn_error_t() - let result = tk_list_devices(&buffer, deviceCapacity, &count, readStrings, &error) + let result = tn_list_devices(&buffer, deviceCapacity, &count, readStrings, &error) try check(result, error) return buffer.prefix(min(count, deviceCapacity)).map { raw in @@ -87,20 +87,20 @@ public enum TetherKitLibrary { /// Enable log capture and set the level. public static func startLogCapture(level: LogLevel = .info) { - tk_set_log_level(level.rawValue) - tk_enable_log_capture(true) + tn_set_log_level(level.rawValue) + tn_enable_log_capture(true) } public static func stopLogCapture() { - tk_enable_log_capture(false) + tn_enable_log_capture(false) } /// Drain buffered logs. Returns the entries and the number dropped because /// the buffer was full. public static func drainLogs() -> (entries: [LogEntry], dropped: UInt64) { - var buffer = [tk_log_record_t](repeating: tk_log_record_t(), count: logCapacity) + var buffer = [tn_log_record_t](repeating: tn_log_record_t(), count: logCapacity) var dropped: UInt64 = 0 - let taken = tk_drain_logs(&buffer, logCapacity, &dropped) + let taken = tn_drain_logs(&buffer, logCapacity, &dropped) let entries = buffer.prefix(taken).map { raw in LogEntry( @@ -123,8 +123,8 @@ public enum TetherKitLibrary { @discardableResult public static func cleanupOrphanInterfaces() throws -> Int { var removed = 0 - var error = tk_error_t() - let result = tk_cleanup_orphan_interfaces(&removed, &error) + var error = tn_error_t() + let result = tn_cleanup_orphan_interfaces(&removed, &error) try check(result, error) return removed } diff --git a/gui/Sources/TetherKitCore/TetherKitSession.swift b/gui/Sources/TethernetCore/TethernetSession.swift similarity index 79% rename from gui/Sources/TetherKitCore/TetherKitSession.swift rename to gui/Sources/TethernetCore/TethernetSession.swift index 04b880c..dc1d56f 100644 --- a/gui/Sources/TetherKitCore/TetherKitSession.swift +++ b/gui/Sources/TethernetCore/TethernetSession.swift @@ -1,6 +1,6 @@ -import CTetherKit +import CTethernet import Foundation -import TetherKitIPC +import TethernetIPC /// An RNDIS session. Requires root, so it is used only inside the helper /// process. @@ -9,7 +9,7 @@ import TetherKitIPC /// the event queue are both locked), but the caller must ensure `start` / `stop` /// are not called concurrently. The helper satisfies this by serializing all /// lifecycle calls on a single serial queue. -public final class TetherKitSession { +public final class TethernetSession { private let handle: OpaquePointer /// Maximum number of events drained in one call, matching the C-side ring @@ -17,10 +17,10 @@ public final class TetherKitSession { private static let eventCapacity = 128 public init(configuration: SessionConfiguration) throws { - var raw = tk_session_config_t() + var raw = tn_session_config_t() // Must init before mutating: the struct carries many tuning knobs, and // zero-initialization would produce an invalid configuration. - tk_session_config_init(&raw) + tn_session_config_init(&raw) raw.vendor_id = configuration.vendorID raw.product_id = configuration.productID raw.bus_number = configuration.busNumber @@ -28,36 +28,36 @@ public final class TetherKitSession { raw.mtu = configuration.mtu raw.adopt_device_mac = configuration.adoptDeviceMAC - var error = tk_error_t() - guard let created = tk_session_create(&raw, &error) else { - throw TetherKitError(result: TK_ERR_FAILED.rawValue, error: error) + var error = tn_error_t() + guard let created = tn_session_create(&raw, &error) else { + throw TethernetError(result: TN_ERR_FAILED.rawValue, error: error) } handle = created } deinit { - // tk_session_destroy stops internally first (idempotent), so there is no + // tn_session_destroy stops internally first (idempotent), so there is no // need to call stop here. - tk_session_destroy(handle) + tn_session_destroy(handle) } /// Start. Non-blocking -- returning only means the request was accepted; /// success or failure is reflected in subsequent status. public func start() throws { - var error = tk_error_t() - let result = tk_session_start(handle, &error) + var error = tn_error_t() + let result = tn_session_start(handle, &error) try check(result, error) } /// Stop and wait for all teardown to complete. Idempotent. public func stop() { - _ = tk_session_stop(handle) + _ = tn_session_stop(handle) } /// Take a status snapshot. Callable from any thread. public func status() -> SessionStatus { - var raw = tk_session_status_t() - guard tk_session_status_get(handle, &raw) == TK_OK else { + var raw = tn_session_status_t() + guard tn_session_status_get(handle, &raw) == TN_OK else { return .idle } return SessionStatus( @@ -92,21 +92,21 @@ public final class TetherKitSession { /// transitions are already shown through status in the UI; surfacing them as /// notices again would just be noise. public func drainNotices() -> [String] { - var buffer = [tk_event_t](repeating: tk_event_t(), count: Self.eventCapacity) - let taken = tk_session_poll_events(handle, &buffer, Self.eventCapacity) + var buffer = [tn_event_t](repeating: tn_event_t(), count: Self.eventCapacity) + let taken = tn_session_poll_events(handle, &buffer, Self.eventCapacity) return buffer.prefix(taken).compactMap { event -> String? in - // On the C side tk_event_kind values are all non-negative and are + // On the C side tn_event_kind values are all non-negative and are // imported into Swift as UInt32, while the struct field is int32_t // -- they cannot be compared directly, so an explicit cast is needed. switch event.kind { - case Int32(TK_EVENT_LINK.rawValue): + case Int32(TN_EVENT_LINK.rawValue): return L(event.a == 1 ? .eventLinkUp : .eventLinkDown) - case Int32(TK_EVENT_DEVICE_RESET.rawValue): + case Int32(TN_EVENT_DEVICE_RESET.rawValue): return L(event.a == 1 ? .eventDeviceResetReplayed : .eventDeviceReset) - case Int32(TK_EVENT_FATAL.rawValue): + case Int32(TN_EVENT_FATAL.rawValue): return String(fixedCArray: event.text) - case Int32(TK_EVENT_NEGOTIATED.rawValue): + case Int32(TN_EVENT_NEGOTIATED.rawValue): return L(.eventNegotiated, Int(event.a), Int(event.b)) default: return nil diff --git a/gui/Sources/TetherKitHelper/HelperService.swift b/gui/Sources/TethernetHelper/HelperService.swift similarity index 94% rename from gui/Sources/TetherKitHelper/HelperService.swift rename to gui/Sources/TethernetHelper/HelperService.swift index b033bc6..a305370 100644 --- a/gui/Sources/TetherKitHelper/HelperService.swift +++ b/gui/Sources/TethernetHelper/HelperService.swift @@ -1,6 +1,6 @@ import Foundation -import TetherKitCore -import TetherKitIPC +import TethernetCore +import TethernetIPC /// XPC service implementation for the helper. /// @@ -18,14 +18,14 @@ import TetherKitIPC /// (concurrent start/stop or concurrent ipconfig calls would be disastrous). /// Status queries are not queued and go straight to the C layer -- snapshots /// there are already thread-safe. -final class HelperService: NSObject, TetherKitHelperProtocol { - private let lifecycleQueue = DispatchQueue(label: "com.tetherkit.helper.lifecycle") - private let networkQueue = DispatchQueue(label: "com.tetherkit.helper.network") +final class HelperService: NSObject, TethernetHelperProtocol { + private let lifecycleQueue = DispatchQueue(label: "com.tethernet.helper.lifecycle") + private let networkQueue = DispatchQueue(label: "com.tethernet.helper.network") /// Protects session and notices. Hold times are very short, so a plain lock /// is sufficient. private let stateLock = NSLock() - private var session: TetherKitSession? + private var session: TethernetSession? /// startSession is constructing / handshaking. In this window the session is /// not yet registered, but the device has already been opened by this process /// -- enumeration's "in use" check must count it, otherwise string reads would @@ -39,11 +39,11 @@ final class HelperService: NSObject, TetherKitHelperProtocol { func helperVersion(reply: @escaping (String) -> Void) { // Include the XPC interface revision so the App can detect "the helper is // an old pre-upgrade version". - reply(HelperConstants.encodeVersion(TetherKitLibrary.versionInfo.version)) + reply(HelperConstants.encodeVersion(TethernetLibrary.versionInfo.version)) } func environment(reply: @escaping (Data?, String?) -> Void) { - respond(with: TetherKitLibrary.checkEnvironment(), reply: reply) + respond(with: TethernetLibrary.checkEnvironment(), reply: reply) } func listDevices(reply: @escaping (Data?, String?) -> Void) { @@ -65,7 +65,7 @@ final class HelperService: NSObject, TetherKitHelperProtocol { } } do { - let devices = try TetherKitLibrary.listDevices(readStrings: !deviceHeld) + let devices = try TethernetLibrary.listDevices(readStrings: !deviceHeld) respond(with: devices, reply: reply) } catch { reply(nil, error.localizedDescription) @@ -91,7 +91,7 @@ final class HelperService: NSObject, TetherKitHelperProtocol { } func drainFeed(reply: @escaping (Data?) -> Void) { - let drained = TetherKitLibrary.drainLogs() + let drained = TethernetLibrary.drainLogs() let notices = takeNotices() let feed = HelperFeed(logs: drained.entries, droppedLogs: drained.dropped, notices: notices) reply(try? JSONEncoder().encode(feed)) @@ -105,7 +105,7 @@ final class HelperService: NSObject, TetherKitHelperProtocol { // the helper simply stays in English. if let language = Language(rawValue: rawValue) { L10n.apply(.english) - TetherKitLibrary.setLanguage(language) + TethernetLibrary.setLanguage(language) } reply() } @@ -148,7 +148,7 @@ final class HelperService: NSObject, TetherKitHelperProtocol { self.stateLock.withLock { self.sessionStarting = true } defer { self.stateLock.withLock { self.sessionStarting = false } } do { - let session = try TetherKitSession(configuration: configuration) + let session = try TethernetSession(configuration: configuration) try session.start() self.stateLock.withLock { self.session = session } reply(nil, false) @@ -166,7 +166,7 @@ final class HelperService: NSObject, TetherKitHelperProtocol { // Remove the session from state before stopping it: stopping joins // the control thread and may take hundreds of milliseconds, during // which we must not continue to report "session is running". - let session = self.stateLock.withLock { () -> TetherKitSession? in + let session = self.stateLock.withLock { () -> TethernetSession? in defer { self.session = nil } return self.session } @@ -212,7 +212,7 @@ final class HelperService: NSObject, TetherKitHelperProtocol { /// catches SIGKILL, but when a graceful exit is possible it should be taken /// -- that way even the "clean up on next launch" step is unnecessary. func shutdown() { - let session = stateLock.withLock { () -> TetherKitSession? in + let session = stateLock.withLock { () -> TethernetSession? in defer { self.session = nil } return self.session } @@ -252,7 +252,7 @@ final class HelperService: NSObject, TetherKitHelperProtocol { } } - private func withState(_ body: (TetherKitSession?) -> T) -> T { + private func withState(_ body: (TethernetSession?) -> T) -> T { stateLock.withLock { body(session) } } @@ -299,7 +299,7 @@ final class HelperListenerDelegate: NSObject, NSXPCListenerDelegate { // For an open-source, source-distributed tool, credential verification // is the more appropriate model: it is equally safe regardless of who // compiled it. - connection.exportedInterface = NSXPCInterface(with: TetherKitHelperProtocol.self) + connection.exportedInterface = NSXPCInterface(with: TethernetHelperProtocol.self) connection.exportedObject = service connection.resume() return true diff --git a/gui/Sources/TetherKitHelper/InstallerMode.swift b/gui/Sources/TethernetHelper/InstallerMode.swift similarity index 99% rename from gui/Sources/TetherKitHelper/InstallerMode.swift rename to gui/Sources/TethernetHelper/InstallerMode.swift index b7c3dab..e44e03d 100644 --- a/gui/Sources/TetherKitHelper/InstallerMode.swift +++ b/gui/Sources/TethernetHelper/InstallerMode.swift @@ -1,5 +1,5 @@ import Darwin -import TetherKitIPC +import TethernetIPC import Foundation /// Execution entry for "one-click install / uninstall from the App": when this diff --git a/gui/Sources/TetherKitHelper/main.swift b/gui/Sources/TethernetHelper/main.swift similarity index 89% rename from gui/Sources/TetherKitHelper/main.swift rename to gui/Sources/TethernetHelper/main.swift index d7d76ab..2f2a2f5 100644 --- a/gui/Sources/TetherKitHelper/main.swift +++ b/gui/Sources/TethernetHelper/main.swift @@ -1,11 +1,11 @@ -// tetherkit-helper -- the privileged helper running as root. +// tethernet-helper -- the privileged helper running as root. // // Where its root comes from: -// From launchd: /Library/LaunchDaemons/com.tetherkit.helper.plist declares +// From launchd: /Library/LaunchDaemons/com.tethernet.helper.plist declares // MachServices, so when the App connects to this Mach service, launchd launches // it on demand, and it starts as root. This has nothing to do with whether the // user pressed a fingerprint -- so "who is calling" must be verified by the -// helper itself on every request; see TetherKitIPC/Authorization.swift. +// helper itself on every request; see TethernetIPC/Authorization.swift. // // Why not SMAppService: // SMPrivilegedExecutables / SMAuthorizedClients rely on a code-signature @@ -13,8 +13,8 @@ // (Homebrew formula), the cdhash produced by each machine's build differs, so a // DR hardcoded in the plist can never match. See docs/GUI-SPIKE.md section 3.3. import Foundation -import TetherKitCore -import TetherKitIPC +import TethernetCore +import TethernetIPC // ---- Installer mode ---- // @@ -34,7 +34,7 @@ func writeToStandardError(_ message: String) { // // Enable capture so the App can see the library's internal logs in the UI. // Output to stderr is unaffected. -TetherKitLibrary.startLogCapture(level: .info) +TethernetLibrary.startLogCapture(level: .info) // ---- Backstop cleanup ---- // @@ -42,7 +42,7 @@ TetherKitLibrary.startLogCapture(level: .info) // interfaces were left in the kernel. This is the only place that can recover // from that -- signal handlers cannot catch SIGKILL. do { - let removed = try TetherKitLibrary.cleanupOrphanInterfaces() + let removed = try TethernetLibrary.cleanupOrphanInterfaces() if removed > 0 { writeToStandardError(L(.helperOrphansCleaned, Int(removed))) } @@ -80,7 +80,7 @@ let listener = NSXPCListener(machServiceName: HelperConstants.machServiceName) listener.delegate = delegate listener.resume() -writeToStandardError(L(.helperReady, TetherKitLibrary.versionInfo.version)) +writeToStandardError(L(.helperReady, TethernetLibrary.versionInfo.version)) // Block on the main runloop. // diff --git a/gui/Sources/TetherKitIPC/Authorization.swift b/gui/Sources/TethernetIPC/Authorization.swift similarity index 100% rename from gui/Sources/TetherKitIPC/Authorization.swift rename to gui/Sources/TethernetIPC/Authorization.swift diff --git a/gui/Sources/TetherKitIPC/HelperConstants.swift b/gui/Sources/TethernetIPC/HelperConstants.swift similarity index 95% rename from gui/Sources/TetherKitIPC/HelperConstants.swift rename to gui/Sources/TethernetIPC/HelperConstants.swift index 9f50a03..7025db1 100644 --- a/gui/Sources/TetherKitIPC/HelperConstants.swift +++ b/gui/Sources/TethernetIPC/HelperConstants.swift @@ -9,16 +9,16 @@ import Foundation public enum HelperConstants { /// Mach service name. Must match the MachServices key in /// /Library/LaunchDaemons/*.plist. - public static let machServiceName = "com.tetherkit.helper" + public static let machServiceName = "com.tethernet.helper" /// Install path of the helper executable. /// /// /Library/PrivilegedHelperTools is Apple's conventional location for /// privileged helpers; only root can write there. - public static let helperExecutablePath = "/Library/PrivilegedHelperTools/com.tetherkit.helper" + public static let helperExecutablePath = "/Library/PrivilegedHelperTools/com.tethernet.helper" /// LaunchDaemon configuration file path. - public static let launchDaemonPlistPath = "/Library/LaunchDaemons/com.tetherkit.helper.plist" + public static let launchDaemonPlistPath = "/Library/LaunchDaemons/com.tethernet.helper.plist" /// Authorization right required for privileged operations. /// @@ -33,7 +33,7 @@ public enum HelperConstants { public static let privilegedRightName = "system.privilege.admin" /// XPC interface revision. Increment this on every change to - /// TetherKitHelperProtocol. + /// TethernetHelperProtocol. /// /// Why this is needed: the helper is installed in a system directory, so if /// the App is upgraded but the helper is not reinstalled, the method @@ -72,7 +72,7 @@ public enum HelperConstants { } /// Extract the semantic version number from the library's version string: - /// `"TetherKit 0.1.4 (C++23, macOS 13.3+)"` -> `"0.1.4"`. + /// `"Tethernet 0.1.4 (C++23, macOS 13.3+)"` -> `"0.1.4"`. /// /// Why not compare the whole string: /// Besides the version number, the string also carries the C++ standard and diff --git a/gui/Sources/TetherKitIPC/HelperProtocol.swift b/gui/Sources/TethernetIPC/HelperProtocol.swift similarity index 97% rename from gui/Sources/TetherKitIPC/HelperProtocol.swift rename to gui/Sources/TethernetIPC/HelperProtocol.swift index 53332eb..d14526c 100644 --- a/gui/Sources/TetherKitIPC/HelperProtocol.swift +++ b/gui/Sources/TethernetIPC/HelperProtocol.swift @@ -29,7 +29,7 @@ import Foundation /// if that contains a non-encodable object it becomes a runtime exception. And /// what we need to show the user is already just a message string, so passing /// a plain string is simple and cannot fail. nil means success. -@objc public protocol TetherKitHelperProtocol { +@objc public protocol TethernetHelperProtocol { /// Connectivity probe. Does not require authorization -- otherwise "not /// installed" and "not authorized" would be indistinguishable. func helperVersion(reply: @escaping (String) -> Void) @@ -79,7 +79,7 @@ import Foundation /// /// Why this is necessary: the helper runs as root under launchd and cannot /// see the user's language preference, yet the notices it produces and the - /// libtetherkit logs are shown as-is in the App's log card. Without keeping + /// libtethernet logs are shown as-is in the App's log card. Without keeping /// them in sync, the UI and the logs would speak different languages. /// /// The parameter is the rawValue of `Language` (currently `"english"`). diff --git a/gui/Sources/TetherKitIPC/Localization.swift b/gui/Sources/TethernetIPC/Localization.swift similarity index 96% rename from gui/Sources/TetherKitIPC/Localization.swift rename to gui/Sources/TethernetIPC/Localization.swift index 1a34416..328fa95 100644 --- a/gui/Sources/TetherKitIPC/Localization.swift +++ b/gui/Sources/TethernetIPC/Localization.swift @@ -7,7 +7,7 @@ import Foundation // The standard approach bundles .lproj directories, and this project ships // three independent artifact shapes: // * the bare executable that `swift build` produces (run during dev); -// * the TetherKit.app assembled by Scripts/build-gui.sh; +// * the Tethernet.app assembled by Scripts/build-gui.sh; // * the **bare** helper executable that lives in // /Library/PrivilegedHelperTools. // The last one is fatal: no resource bundle sits next to the helper, and @@ -19,7 +19,7 @@ import Foundation // three shapes behave identically, install scripts stay untouched, and // there is no "installed on another machine, .lproj missing, everything // falls back to the development language" class of runtime failure. It also -// mirrors the C++ side (include/tetherkit/common/messages.def): one mental +// mirrors the C++ side (include/tethernet/common/messages.def): one mental // model. // // The cost is Xcode's string-catalog editor -- this repo has no Xcode @@ -43,7 +43,7 @@ public enum LanguagePreference: String, CaseIterable, Codable, Sendable { public enum Language: String, CaseIterable, Codable, Sendable { case english - /// Aligned with the C ABI's tk_language_t (TK_LANGUAGE_ENGLISH = 0). A + /// Aligned with the C ABI's tn_language_t (TN_LANGUAGE_ENGLISH = 0). A /// language is one more enum value; on the wire, foreign values are /// ignored by every consumer, so callers keep working and stay in English. public var cValue: Int32 { 0 } @@ -77,7 +77,7 @@ public enum L10n { /// Applies a preference and returns the language that becomes effective. /// /// After taking the return value, the caller must **also** push it into - /// libtetherkit (see `TetherKitLibrary.setLanguage`), or library logs and + /// libtethernet (see `TethernetLibrary.setLanguage`), or library logs and /// the interface language drift apart. @discardableResult public static func apply(_ preference: LanguagePreference) -> Language { diff --git a/gui/Sources/TetherKitIPC/LocalizedStrings.swift b/gui/Sources/TethernetIPC/LocalizedStrings.swift similarity index 97% rename from gui/Sources/TetherKitIPC/LocalizedStrings.swift rename to gui/Sources/TethernetIPC/LocalizedStrings.swift index c4585e1..d9d5925 100644 --- a/gui/Sources/TetherKitIPC/LocalizedStrings.swift +++ b/gui/Sources/TethernetIPC/LocalizedStrings.swift @@ -10,7 +10,7 @@ import Foundation // language, so this is a bounded change, not a re-architecture): // // 1. Mirror the C++ side: add the value to the `Language` enum here in the -// package, to tk_language_t (include/tetherkit/capi/tetherkit_c.h), and +// package, to tn_language_t (include/tethernet/capi/tethernet_c.h), and // to the add-a-language checklist at the top of messages.def. // 2. Give every row of the `localizations` switch one more labeled argument. // 3. Dispatch on the language in L10n.text(_:in:). The helper already @@ -70,11 +70,11 @@ public enum L10nKey: String, CaseIterable, Sendable { case authorizationRestoreFailed case authorizationRightMissing - // MARK: - Library errors (TetherKitError.swift) + // MARK: - Library errors (TethernetError.swift) case libraryGenericFailure - // MARK: - Session events (TetherKitSession.swift) + // MARK: - Session events (TethernetSession.swift) case eventLinkUp case eventLinkDown @@ -389,7 +389,7 @@ extension L10nKey { case .helperOrphanCleanupFailed: return "Failed to clean up leftover virtual interfaces: %@" case .helperSigtermReceived: return "Received SIGTERM; shutting down" - case .helperReady: return "tetherkit-helper ready: %@" + case .helperReady: return "tethernet-helper ready: %@" case .installerNeedsRoot: return "Error: cannot obtain root (euid=%1$ld, ruid=%2$ld)." case .installerUseTerminal: @@ -405,7 +405,7 @@ extension L10nKey { // Both of these two messages end with "use the terminal instead", so they // share one row instead of repeating the same closing clause. case .installerNoPayload: - return "This copy of TetherKit carries no installer payload (likely a dev build). " + return "This copy of Tethernet carries no installer payload (likely a dev build). " + "Run this in a terminal: sudo ./gui/Scripts/install-helper.sh" case .installerAPIUnavailable: return "This version of macOS no longer offers the API in-app installation needs. " @@ -443,14 +443,14 @@ extension L10nKey { // MARK: App model case .authPromptSession: - return "TetherKit needs administrator privileges to create the virtual interface, " + return "Tethernet needs administrator privileges to create the virtual interface, " + "open the data link and configure IP addresses." case .authPromptInstall: - return "TetherKit needs administrator privileges to install the privileged helper " + return "Tethernet needs administrator privileges to install the privileged helper " + "(copy it to /Library/PrivilegedHelperTools and register the system " + "service)." case .authPromptUninstall: - return "TetherKit needs administrator privileges to uninstall the privileged helper " + return "Tethernet needs administrator privileges to uninstall the privileged helper " + "(unregister the system service and delete its files under /Library)." case .interfaceNotReadyYet: return "The virtual interface does not exist yet -- connect a device first" @@ -474,7 +474,7 @@ extension L10nKey { return "You are on the latest version (v%@)." case .updateAvailable: return "Version v%@ is available. If you installed via Homebrew, run " - + "brew upgrade tetherkit in a terminal; if you build from source, pull the " + + "brew upgrade tethernet in a terminal; if you build from source, pull the " + "latest code and rebuild." case .updateCheckFailed: return "The check could not be completed: %@" case .updateDevBuild: @@ -504,7 +504,7 @@ extension L10nKey { case .needInstallTitle: return "The privileged helper must be installed" case .needInstallBody: return "Creating the virtual interface and opening the data link require\n" - + "administrator privileges. TetherKit keeps that work in a separate\n" + + "administrator privileges. Tethernet keeps that work in a separate\n" + "background component so the app itself runs as a normal user." case .installHelperButton: return "Install the helper" case .installHelperDetail: diff --git a/gui/Sources/TetherKitIPC/Models.swift b/gui/Sources/TethernetIPC/Models.swift similarity index 98% rename from gui/Sources/TetherKitIPC/Models.swift rename to gui/Sources/TethernetIPC/Models.swift index 8bbf674..ae28840 100644 --- a/gui/Sources/TetherKitIPC/Models.swift +++ b/gui/Sources/TethernetIPC/Models.swift @@ -13,7 +13,7 @@ import Foundation // MARK: - Session /// Startup configuration for an RNDIS session. Fields correspond one-to-one with -/// tk_session_config_t in the C ABI. +/// tn_session_config_t in the C ABI. public struct SessionConfiguration: Codable, Hashable, Sendable { /// Device filter. All zeros mean use the first RNDIS device found. public var vendorID: UInt16 @@ -44,7 +44,7 @@ public struct SessionConfiguration: Codable, Hashable, Sendable { } } -/// Session lifecycle state. Raw values align with C's tk_run_state_t. +/// Session lifecycle state. Raw values align with C's tn_run_state_t. public enum RunState: Int32, Codable, Sendable { case idle = 0 case starting = 1 @@ -58,7 +58,7 @@ public enum RunState: Int32, Codable, Sendable { public var isTransitional: Bool { self == .starting || self == .stopping } } -/// RNDIS host-side state. Raw values align with C's tk_rndis_state_t. +/// RNDIS host-side state. Raw values align with C's tn_rndis_state_t. public enum RndisState: Int32, Codable, Sendable { case uninitialized = 0 case initializing = 1 @@ -199,7 +199,7 @@ public struct DeviceDescriptor: Codable, Hashable, Identifiable, Sendable { // MARK: - Network configuration -/// Network access mode. Raw values align with C's tk_ip_mode_t. +/// Network access mode. Raw values align with C's tn_ip_mode_t. public enum IPMode: Int32, Codable, CaseIterable, Sendable { case dhcp = 0 case manual = 1 diff --git a/gui/Sources/TetherKitUIModel/ButtonTurn.swift b/gui/Sources/TethernetUIModel/ButtonTurn.swift similarity index 97% rename from gui/Sources/TetherKitUIModel/ButtonTurn.swift rename to gui/Sources/TethernetUIModel/ButtonTurn.swift index 139b027..9df6da5 100644 --- a/gui/Sources/TetherKitUIModel/ButtonTurn.swift +++ b/gui/Sources/TethernetUIModel/ButtonTurn.swift @@ -34,7 +34,7 @@ import SwiftUI /// /// Lives in this target (the sidebar page model) so both its callers /// and its commit semantics are unit-testable: the App target is an -/// executable, which `swift test` cannot import (see TetherKitUIModelTests). +/// executable, which `swift test` cannot import (see TethernetUIModelTests). public enum ButtonTurn { /// Event types that identify an activation turn requiring the @@ -108,7 +108,7 @@ public enum ButtonTurn { /// write through `write` escapes the turn, so the affected /// subtrees swap without interpolation. The commit semantics are /// this escape's whole contract and are asserted in - /// TetherKitUIModelTests: outside a user turn (where a unit test + /// TethernetUIModelTests: outside a user turn (where a unit test /// runs, `NSApp.currentEvent == nil`) the write lands synchronously. public static func routed(get: @escaping () -> T, set: @escaping (T) -> Void) -> Binding { diff --git a/gui/Sources/TetherKitUIModel/SidebarPage.swift b/gui/Sources/TethernetUIModel/SidebarPage.swift similarity index 97% rename from gui/Sources/TetherKitUIModel/SidebarPage.swift rename to gui/Sources/TethernetUIModel/SidebarPage.swift index bd39265..890b53d 100644 --- a/gui/Sources/TetherKitUIModel/SidebarPage.swift +++ b/gui/Sources/TethernetUIModel/SidebarPage.swift @@ -1,9 +1,9 @@ import SwiftUI -import TetherKitIPC +import TethernetIPC /// One page of the main window's sidebar. /// -/// Lives in `TetherKitUIModel` because a SwiftPM executable cannot be +/// Lives in `TethernetUIModel` because a SwiftPM executable cannot be /// `@testable`-imported and the tint semantics deserve unit tests. /// Order is by how often the user reaches for a page: Connection is /// where actions happen, Throughput/Log are for watching, Manage is diff --git a/gui/Tests/TetherKitIPCTests/AuthorizationTests.swift b/gui/Tests/TethernetIPCTests/AuthorizationTests.swift similarity index 99% rename from gui/Tests/TetherKitIPCTests/AuthorizationTests.swift rename to gui/Tests/TethernetIPCTests/AuthorizationTests.swift index 4135b56..4e1fbb2 100644 --- a/gui/Tests/TetherKitIPCTests/AuthorizationTests.swift +++ b/gui/Tests/TethernetIPCTests/AuthorizationTests.swift @@ -1,7 +1,7 @@ import Security import XCTest -@testable import TetherKitIPC +@testable import TethernetIPC /// Regression tests for the authorization credential lifecycle. /// diff --git a/gui/Tests/TetherKitIPCTests/HelperConstantsTests.swift b/gui/Tests/TethernetIPCTests/HelperConstantsTests.swift similarity index 85% rename from gui/Tests/TetherKitIPCTests/HelperConstantsTests.swift rename to gui/Tests/TethernetIPCTests/HelperConstantsTests.swift index 6dcc59a..d749e29 100644 --- a/gui/Tests/TetherKitIPCTests/HelperConstantsTests.swift +++ b/gui/Tests/TethernetIPCTests/HelperConstantsTests.swift @@ -1,6 +1,6 @@ import XCTest -@testable import TetherKitIPC +@testable import TethernetIPC /// Encoding/decoding and comparison of the helper version string. /// @@ -19,21 +19,21 @@ final class HelperConstantsTests: XCTestCase { // MARK: - Encode/decode func testEncodeDecodeRoundTrip() { - let encoded = HelperConstants.encodeVersion("TetherKit 0.1.4 (C++23, macOS 13.3+)") + let encoded = HelperConstants.encodeVersion("Tethernet 0.1.4 (C++23, macOS 13.3+)") let (revision, version) = HelperConstants.decodeVersion(encoded) XCTAssertEqual(revision, HelperConstants.protocolRevision) - XCTAssertEqual(version, "TetherKit 0.1.4 (C++23, macOS 13.3+)") + XCTAssertEqual(version, "Tethernet 0.1.4 (C++23, macOS 13.3+)") } /// A string returned by an old component has no separator -- exactly the kind /// of component that must be identified, recorded as revision 0. func testDecodeLegacyVersionWithoutRevision() { let (revision, version) = - HelperConstants.decodeVersion("TetherKit 0.1.2 (C++23, macOS 13.3+)") + HelperConstants.decodeVersion("Tethernet 0.1.2 (C++23, macOS 13.3+)") XCTAssertEqual(revision, 0) - XCTAssertEqual(version, "TetherKit 0.1.2 (C++23, macOS 13.3+)") + XCTAssertEqual(version, "Tethernet 0.1.2 (C++23, macOS 13.3+)") XCTAssertNotEqual(revision, HelperConstants.protocolRevision, "0 must differ from every real revision, otherwise an old component would be treated as matching") } @@ -42,14 +42,14 @@ final class HelperConstantsTests: XCTestCase { func testSemanticVersionFromLibraryVersionString() { XCTAssertEqual( - HelperConstants.semanticVersion(of: "TetherKit 0.1.4 (C++23, macOS 13.3+)"), "0.1.4") + HelperConstants.semanticVersion(of: "Tethernet 0.1.4 (C++23, macOS 13.3+)"), "0.1.4") } /// The other two numbers in the string (C++ standard, minimum macOS version) /// must not be taken as the version number: the former has no dot, the latter /// comes after the version number. func testSemanticVersionIgnoresBuildConfigurationNumbers() { - let text = "TetherKit 1.0.0 (C++23, macOS 13.3+)" + let text = "Tethernet 1.0.0 (C++23, macOS 13.3+)" XCTAssertEqual(HelperConstants.semanticVersion(of: text), "1.0.0") } @@ -58,15 +58,15 @@ final class HelperConstantsTests: XCTestCase { /// why the whole string is not compared directly: otherwise a spurious /// warning would appear that never goes away when clicked. func testSemanticVersionEqualAcrossBuildConfigurations() { - let installed = HelperConstants.semanticVersion(of: "TetherKit 0.1.4 (C++23, macOS 13.3+)") - let bundled = HelperConstants.semanticVersion(of: "TetherKit 0.1.4 (C++26, macOS 15.0+)") + let installed = HelperConstants.semanticVersion(of: "Tethernet 0.1.4 (C++23, macOS 13.3+)") + let bundled = HelperConstants.semanticVersion(of: "Tethernet 0.1.4 (C++26, macOS 15.0+)") XCTAssertEqual(installed, bundled) } func testSemanticVersionDetectsDifferentReleases() { - let installed = HelperConstants.semanticVersion(of: "TetherKit 0.1.3 (C++23, macOS 13.3+)") - let bundled = HelperConstants.semanticVersion(of: "TetherKit 0.1.4 (C++23, macOS 13.3+)") + let installed = HelperConstants.semanticVersion(of: "Tethernet 0.1.3 (C++23, macOS 13.3+)") + let bundled = HelperConstants.semanticVersion(of: "Tethernet 0.1.4 (C++23, macOS 13.3+)") XCTAssertNotEqual(installed, bundled) XCTAssertEqual(installed, "0.1.3") @@ -77,16 +77,16 @@ final class HelperConstantsTests: XCTestCase { /// false positive (a spurious update prompt) than a missed one (the user keeps /// running an old component without knowing it). func testSemanticVersionFallsBackToWholeString() { - XCTAssertEqual(HelperConstants.semanticVersion(of: " TetherKit dev "), "TetherKit dev") - XCTAssertNotEqual(HelperConstants.semanticVersion(of: "TetherKit dev-a"), - HelperConstants.semanticVersion(of: "TetherKit dev-b")) + XCTAssertEqual(HelperConstants.semanticVersion(of: " Tethernet dev "), "Tethernet dev") + XCTAssertNotEqual(HelperConstants.semanticVersion(of: "Tethernet dev-a"), + HelperConstants.semanticVersion(of: "Tethernet dev-b")) } /// Comparing a version to itself after installation must match -- if this /// turns red, the UI will show a "component needs updating" prompt that stays /// in an endless loop even after updating. func testSemanticVersionIsStableForSameInput() { - let text = "TetherKit 0.1.4 (C++23, macOS 13.3+)" + let text = "Tethernet 0.1.4 (C++23, macOS 13.3+)" XCTAssertEqual(HelperConstants.semanticVersion(of: text), HelperConstants.semanticVersion(of: text)) } diff --git a/gui/Tests/TetherKitIPCTests/LocalizationTests.swift b/gui/Tests/TethernetIPCTests/LocalizationTests.swift similarity index 96% rename from gui/Tests/TetherKitIPCTests/LocalizationTests.swift rename to gui/Tests/TethernetIPCTests/LocalizationTests.swift index 2289370..c73125a 100644 --- a/gui/Tests/TetherKitIPCTests/LocalizationTests.swift +++ b/gui/Tests/TethernetIPCTests/LocalizationTests.swift @@ -1,6 +1,6 @@ import XCTest -@testable import TetherKitIPC +@testable import TethernetIPC /// Consistency checks for the string table. /// @@ -146,12 +146,12 @@ final class LocalizationTests: XCTestCase { // The positional entries must substitute by position, not by order of // appearance. - let alias = L(.aliasCreated, in: .english, "/Applications/TetherKit", "/opt/TetherKit.app") - XCTAssertTrue(alias.contains("/Applications/TetherKit")) - XCTAssertTrue(alias.contains("/opt/TetherKit.app")) + let alias = L(.aliasCreated, in: .english, "/Applications/Tethernet", "/opt/Tethernet.app") + XCTAssertTrue(alias.contains("/Applications/Tethernet")) + XCTAssertTrue(alias.contains("/opt/Tethernet.app")) } - /// Language tags must match the C ABI's tk_language_t -- if they drift, + /// Language tags must match the C ABI's tn_language_t -- if they drift, /// the GUI speaks one language and library logs speak another, with no /// error anywhere. func testCValueMatchesCABI() { diff --git a/gui/Tests/TetherKitUIModelTests/ButtonTurnTests.swift b/gui/Tests/TethernetUIModelTests/ButtonTurnTests.swift similarity index 98% rename from gui/Tests/TetherKitUIModelTests/ButtonTurnTests.swift rename to gui/Tests/TethernetUIModelTests/ButtonTurnTests.swift index bb7271a..b3e1036 100644 --- a/gui/Tests/TetherKitUIModelTests/ButtonTurnTests.swift +++ b/gui/Tests/TethernetUIModelTests/ButtonTurnTests.swift @@ -1,6 +1,6 @@ import XCTest -import TetherKitIPC -@testable import TetherKitUIModel +import TethernetIPC +@testable import TethernetUIModel /// Locks the activation-turn write escape's commit semantics — the /// mechanics the log-level picker (and every future routed control diff --git a/gui/Tests/TetherKitUIModelTests/SidebarPageTests.swift b/gui/Tests/TethernetUIModelTests/SidebarPageTests.swift similarity index 98% rename from gui/Tests/TetherKitUIModelTests/SidebarPageTests.swift rename to gui/Tests/TethernetUIModelTests/SidebarPageTests.swift index 7695970..af40d7a 100644 --- a/gui/Tests/TetherKitUIModelTests/SidebarPageTests.swift +++ b/gui/Tests/TethernetUIModelTests/SidebarPageTests.swift @@ -1,7 +1,7 @@ import XCTest import SwiftUI -import TetherKitIPC -@testable import TetherKitUIModel +import TethernetIPC +@testable import TethernetUIModel /// Locks the sidebar page semantics: the per-run-state color of the /// Connection tile ("the icon backs the glance") and the persisted diff --git a/include/tetherkit/common/messages.def b/include/tetherkit/common/messages.def deleted file mode 100644 index 72bd6e6..0000000 --- a/include/tetherkit/common/messages.def +++ /dev/null @@ -1,548 +0,0 @@ -// All user-facing strings. This file deliberately has no include guard: it is an -// X-macro list, expanded into the Msg enum (by tetherkit/common/i18n.h) and the -// message table (by src/common/i18n.cc). -// -// Each row takes exactly two arguments: -// -// TETHERKIT_MESSAGE(identifier, English) -// -// Rules for adding a message: -// -// 1. Prefix the identifier with its module: kCommon* / kRndis* / kNet* / kUsb* / -// kCore* / kCapi* / kCli*; CamelCase for the rest; do not paraphrase the prose. -// 2. Placeholders: implicit {} are numbered in order of appearance (0, 1, 2, ...); -// implicit and explicit numbering must not be mixed within one string -// (std::format's rule). Reorder with explicit indices when needed, e.g. -// "write {1} bytes to {0}". -// 3. Insert new entries into their module's section, roughly in call order. -// -// Rule: every message's placeholders must parse cleanly and use contiguous -// indices starting at 0. Enforced entry by entry by tests/test_common_i18n.cc; -// a bad row turns `ctest -R common.i18n` red on the spot. -// -// How to add a language (the wiring deliberately stays in place with one -// language, so this is a bounded five-touch change, not a re-architecture): -// -// 1. Add a value (kSpanish = 1, say) to enum class Language in -// tetherkit/common/i18n.h, mirror it as TK_LANGUAGE_SPANISH in -// tk_language_t (include/tetherkit/capi/tetherkit_c.h), and mirror it in -// the Swift `Language` enum (gui/Sources/TetherKitIPC/Localization.swift). -// 2. Give every row of this file one more argument: the macro signature -// becomes (id, en, es) at the three extraction sites (the Msg enum in -// i18n.h, the table(s) in src/common/i18n.cc). -// 3. Emit one more table in src/common/i18n.cc next to kMessageTable and let -// TextIn() select it by Language. -// 4. Accept the new spelling and locale prefix in ParseLanguage() and -// LanguageFromLocaleString(). -// 5. Add the GUI menu entry and the per-key column in -// gui/Sources/TetherKitIPC/LocalizedStrings.swift, then extend the checks -// in tests/test_common_i18n.cc and the GUI LocalizationTests so that every -// language covers every message. - -// ============================================================================= -// common -- infrastructure layer -// ============================================================================= - -TETHERKIT_MESSAGE(kCommonLogFormatFailed, "") -TETHERKIT_MESSAGE(kCommonThreadQosFailed, "failed to set thread QoS (qos={}, rc={}); falling back to the default " - "scheduling policy") - -// ============================================================================= -// net -- the feth virtual interface and the BPF link -// ============================================================================= - -TETHERKIT_MESSAGE(kNetLoopbackInjectedFailure, "loopback backend failed on write #{} as configured by the test") - -// --- shared (used by both feth and BPF) --- -TETHERKIT_MESSAGE(kNetInterfaceNameTooLong, "interface name \"{}\" is too long") -TETHERKIT_MESSAGE(kNetInterfaceNameOverLimit, "interface name \"{}\" exceeds the {}-byte limit") -TETHERKIT_MESSAGE(kNetSysctlReadFailed, "failed to read sysctl {}") -TETHERKIT_MESSAGE(kNetUnknownValue, "unknown") - -// --- BPF link --- -TETHERKIT_MESSAGE(kNetOptionalIoctlUnavailable, "optional ioctl {} is unavailable (errno={}); skipped") -TETHERKIT_MESSAGE(kNetFrameLimitBelowMinimum, "max_frame_bytes={} is smaller than the minimum Ethernet frame length {}") -TETHERKIT_MESSAGE(kNetBpfOpenDenied, "permission denied opening {}. /dev/bpf* is 0600 root:wheel and macOS has no " - "equivalent of FreeBSD's access_bpf group, so this must run as root") -TETHERKIT_MESSAGE(kNetBpfNoDeviceAvailable, "no usable /dev/bpf* device found (all busy, or insufficient permissions)") -TETHERKIT_MESSAGE(kNetBpfBufferClamped, "the kernel clamped the BPF buffer from the requested {} bytes to {} bytes " - "(limited by sysctl debug.bpf_bufsize_cap)") -TETHERKIT_MESSAGE(kNetBpfInterfaceMissing, "interface {} does not exist; cannot attach BPF") -TETHERKIT_MESSAGE(kNetBpfBindFailed, "failed to attach BPF to {}") -TETHERKIT_MESSAGE(kNetBpfWrongDataLinkType, "interface {} has data link type {}; this driver only supports Ethernet " - "(DLT_EN10MB={})") -TETHERKIT_MESSAGE(kNetBpfReady, "BPF ready: {} -> {} (kernel buffer {} KiB, max frame {} bytes, batch write {}, " - "timestamps {})") -TETHERKIT_MESSAGE(kNetBpfBatchWriteEnabled, "enabled") -TETHERKIT_MESSAGE(kNetBpfBatchWriteUnavailable, "unavailable (writing one frame at a time)") -TETHERKIT_MESSAGE(kNetBpfTimestampDisabled, "off") -TETHERKIT_MESSAGE(kNetBpfTimestampEnabled, "left on") -TETHERKIT_MESSAGE(kNetBpfReadFailed, "failed to read from {}") -TETHERKIT_MESSAGE(kNetBpfHeaderTooShort, "BPF record has bh_hdrlen={}, below the minimum of {}; discarding the rest of " - "this read") -TETHERKIT_MESSAGE(kNetBpfRecordOutOfBounds, "BPF record runs past the buffer (offset {} + {} > {}); discarding the rest") -TETHERKIT_MESSAGE(kNetBpfSkipOversizedRecord, "skipping BPF record: caplen={} datalen={} (limit {})") -TETHERKIT_MESSAGE(kNetBpfBatchFrameLimit, "hit the per-batch frame limit of {}; the remaining {} bytes of this read were " - "dropped. Increase max_frames_per_batch") -TETHERKIT_MESSAGE(kNetBpfWriteFailed, "failed to write a {1}-byte frame to {0}") -TETHERKIT_MESSAGE(kNetBpfBatchWriteFailed, "failed to batch-write {1} frames ({2} bytes) to {0}") - -// --- feth virtual interface --- -TETHERKIT_MESSAGE(kNetIoctlSocketFailed, "failed to create the socket used for interface ioctls") -TETHERKIT_MESSAGE(kNetFethMaxMtuInvalid, "net.link.fake.max_mtu returned the invalid value {}") -TETHERKIT_MESSAGE(kNetSysctlUnreadableSkipped, "sysctl {} is unreadable ({}); skipping the check") -TETHERKIT_MESSAGE(kNetSysctlMismatch, "sysctl {0} is currently {1} but must be {2}. Reason: {3}.\n" - " feth snapshots this switch into the interface at creation time, so changing " - "it afterwards has no effect. Fix it first:\n" - " sudo sysctl -w {0}={2}") -TETHERKIT_MESSAGE(kNetSysctlWhyHwcsum, "when non-zero, IP/TCP/UDP checksums on outbound frames are placeholders left " - "for hardware to compute, so the frames we read have invalid checksums") -TETHERKIT_MESSAGE(kNetSysctlWhyFcs, "when non-zero, a 4-byte CRC is appended to each frame and would have to be " - "stripped before forwarding over USB") -TETHERKIT_MESSAGE(kNetSysctlWhyTso, "when non-zero, TSO super-packets of up to 64 KB arrive, exceeding the RNDIS " - "per-frame limit") -TETHERKIT_MESSAGE(kNetSysctlWhyLro, "when non-zero, coalesced oversized receive frames arrive") -TETHERKIT_MESSAGE(kNetSysctlWhyTrailer, "when non-zero, frames carry extra trailing padding") -TETHERKIT_MESSAGE(kNetSysctlWhySeparateHeader, "when non-zero, the frame header is separated from the payload, breaking the " - "contiguous-memory assumption") -TETHERKIT_MESSAGE(kNetDestroyOrphanFailed, "failed to destroy leftover virtual interface {}") -TETHERKIT_MESSAGE(kNetDestroyedOrphan, "destroyed leftover virtual interface {}") -TETHERKIT_MESSAGE(kNetDestroyFailed, "failed to destroy {}: {}") -TETHERKIT_MESSAGE(kNetDestroyed, "destroyed virtual interface {}") -TETHERKIT_MESSAGE(kNetFethCreateNeedsRoot, "creating a feth virtual interface requires root; run with sudo") -TETHERKIT_MESSAGE(kNetInterfaceExists, "interface {} already exists; pick another name or destroy it first") -TETHERKIT_MESSAGE(kNetFethCreateFailed, "failed to create feth virtual interface {}") -TETHERKIT_MESSAGE(kNetCreated, "created virtual interface {}") -TETHERKIT_MESSAGE(kNetPeerInvalidObject, "pairing failed: invalid interface object") -TETHERKIT_MESSAGE(kNetPeerNameTooLong, "peer interface name \"{}\" is too long") -TETHERKIT_MESSAGE(kNetPeerNeedsRoot, "pairing feth interfaces requires root; run with sudo") -TETHERKIT_MESSAGE(kNetPeerRejected, "the kernel refused to pair {} with {}. Likely causes: one side already has a " - "peer, the peer is not a feth interface, or the private if_fake ABI changed in " - "this macOS version") -TETHERKIT_MESSAGE(kNetPeerFailed, "failed to pair {} with {}") -TETHERKIT_MESSAGE(kNetPeered, "paired {} with {}") -TETHERKIT_MESSAGE(kNetUnpeerInvalidObject, "unpairing failed: invalid interface object") -TETHERKIT_MESSAGE(kNetUnpeerIoctlWhat, "ioctl(SIOCSDRVSPEC, IF_FAKE_S_CMD_SET_PEER) to unpair") -TETHERKIT_MESSAGE(kNetQueryPeerInvalidObject, "peer query failed: invalid interface object") -TETHERKIT_MESSAGE(kNetSetMtuInvalidObject, "setting the MTU failed: invalid interface object") -TETHERKIT_MESSAGE(kNetSetMtuRejected, "refused to set the MTU of {} to {} (the feth MTU ceiling is {}, set by sysctl " - "net.link.fake.max_mtu and snapshotted when the interface was created)") -TETHERKIT_MESSAGE(kNetSetMtuFailed, "failed to set the MTU of {} to {}") -TETHERKIT_MESSAGE(kNetSetMacInvalidObject, "setting the MAC failed: invalid interface object") -TETHERKIT_MESSAGE(kNetSetMacNeedsRoot, "changing an interface MAC requires root; run with sudo") -TETHERKIT_MESSAGE(kNetSetMacFailed, "failed to set the MAC of {} to {}") -TETHERKIT_MESSAGE(kNetMacSet, "set the MAC of {} to {}") -TETHERKIT_MESSAGE(kNetGetifaddrsFailed, "getifaddrs failed") -TETHERKIT_MESSAGE(kNetLinkAddressWrongLength, "the link address of {} is {} bytes, not a 6-byte MAC") -TETHERKIT_MESSAGE(kNetLinkAddressNotFound, "no link address for interface {} in the getifaddrs results") -TETHERKIT_MESSAGE(kNetSetUpInvalidObject, "changing the interface state failed: invalid interface object") -TETHERKIT_MESSAGE(kNetSetUpNeedsRoot, "bringing an interface up or down requires root; run with sudo") -TETHERKIT_MESSAGE(kNetSetUpFailed, "failed to bring {} {}") -TETHERKIT_MESSAGE(kNetFethPairNeedsRoot, "creating feth virtual interfaces requires root. Run TetherKit with sudo.") -TETHERKIT_MESSAGE(kNetMtuExceedsFethLimit, "the requested MTU {} exceeds the feth ceiling of {} (sysctl " - "net.link.fake.max_mtu)") -TETHERKIT_MESSAGE(kNetSameMacDadConflict, "both sides have the same MAC ({}), which would trigger an IPv6 DAD address " - "conflict") -TETHERKIT_MESSAGE(kNetFethPairReady, "virtual interface pair ready: system side {} <-> driver side {} (MTU {})") - -// ============================================================================= -// rndis -- protocol codec and state machine -// ============================================================================= - -// --- generic placeholders --- -TETHERKIT_MESSAGE(kRndisUnknownStatus, "unknown status") -TETHERKIT_MESSAGE(kRndisUnknownMessage, "unknown message") -TETHERKIT_MESSAGE(kRndisUnknownMessageType, "unknown message type") -TETHERKIT_MESSAGE(kRndisUnknown, "unknown") -TETHERKIT_MESSAGE(kRndisResponse, "a response") - -// --- state names --- -TETHERKIT_MESSAGE(kRndisStateUninitialized, "uninitialized") -TETHERKIT_MESSAGE(kRndisStateInitializing, "initializing") -TETHERKIT_MESSAGE(kRndisStateInitialized, "initialized") -TETHERKIT_MESSAGE(kRndisStateDataInitialized, "data ready") -TETHERKIT_MESSAGE(kRndisStateHalting, "halting") -TETHERKIT_MESSAGE(kRndisStateUnknown, "unknown state") - -// --- data packet decode rejection reasons --- -TETHERKIT_MESSAGE(kRndisMalformedNone, "none") -TETHERKIT_MESSAGE(kRndisMalformedNotPacketMessage, "MessageType is not PACKET_MSG") -TETHERKIT_MESSAGE(kRndisMalformedMessageTooShort, "MessageLength is below 44") -TETHERKIT_MESSAGE(kRndisMalformedMessageOverruns, "MessageLength runs past the transfer") -TETHERKIT_MESSAGE(kRndisMalformedDataOutOfBounds, "DataOffset+DataLength is out of bounds") -TETHERKIT_MESSAGE(kRndisMalformedFrameTooShort, "DataLength is below the Ethernet header length") -TETHERKIT_MESSAGE(kRndisMalformedFrameTooLong, "DataLength exceeds the per-frame limit") -TETHERKIT_MESSAGE(kRndisMalformedUnknown, "unknown") - -// --- control message codec --- -TETHERKIT_MESSAGE(kRndisBufferTooShort, "{} is too short: need {} bytes, got {}") -TETHERKIT_MESSAGE(kRndisEncodeBufferTooSmall, "the buffer for encoding {} is too small: need {} bytes, got {}") -TETHERKIT_MESSAGE(kRndisUnexpectedMessageType, "expected {}, got {} ({:#010x})") -TETHERKIT_MESSAGE(kRndisInconsistentMessageLength, "MessageLength={1} of {0} is inconsistent (buffer is {2} bytes, minimum is {3})") -TETHERKIT_MESSAGE(kRndisInlineBufferOutOfBounds, "{} is out of bounds: offset {} (relative {}) + length {} exceeds MessageLength " - "{}") -TETHERKIT_MESSAGE(kRndisWhatMessageHeader, "the RNDIS message header") -TETHERKIT_MESSAGE(kRndisWhatQueryCmpltInfoBuffer, "the InformationBuffer of QUERY_CMPLT") -TETHERKIT_MESSAGE(kRndisWhatOidUint32, "the LE32 return value of an OID") -TETHERKIT_MESSAGE(kRndisWhatOidMac, "the MAC return value of an OID") -TETHERKIT_MESSAGE(kRndisStatusSuffix, "{}: {}") -TETHERKIT_MESSAGE(kRndisInitRejected, "the device rejected RNDIS initialization") -TETHERKIT_MESSAGE(kRndisUnsupportedMedium, "the device reports medium {:#x}; this driver only supports 802.3 Ethernet (0)") -TETHERKIT_MESSAGE(kRndisConnectionOriented, "the device is connection-oriented (CONDIS); this driver only supports " - "connectionless 802.3 devices") -TETHERKIT_MESSAGE(kRndisNoConnectionlessFlag, "the device has DeviceFlags={:#x} without the CONNECTIONLESS bit; continuing as " - "if it were connectionless") -TETHERKIT_MESSAGE(kRndisUnsupportedMajorVersion, "the device reports RNDIS major version {}; this driver only implements {}") -TETHERKIT_MESSAGE(kRndisAlignmentFactorClamped, "the device reports PacketAlignmentFactor={}, above the legal ceiling of {}; " - "clamped") -TETHERKIT_MESSAGE(kRndisMaxTransferTooSmall, "the device reports MaxTransferSize={}, too small for the {}-byte RNDIS + " - "Ethernet headers") -TETHERKIT_MESSAGE(kRndisMaxTransferClampedToHost, "the device reports MaxTransferSize={}, above the host buffer ceiling of {}; " - "using the host ceiling") -TETHERKIT_MESSAGE(kRndisMtuLoweredForTransferSize, "the device's MaxTransferSize={} is below the {} needed for a full frame; " - "lowering the MTU from {} to {}") -TETHERKIT_MESSAGE(kRndisExpectedIndicateStatus, "expected REMOTE_NDIS_INDICATE_STATUS_MSG, got {:#010x}") -TETHERKIT_MESSAGE(kRndisOidCounterBadLength, "the OID counter returned {} bytes, which is neither 4 nor 8") -TETHERKIT_MESSAGE(kRndisIndicateStatusBufferOutOfBounds, "the StatusBufferOffset={} / Length={} of INDICATE_STATUS is out of bounds under " - "both base-point interpretations (MessageLength={}); ignoring the status buffer") - -// --- state machine --- -TETHERKIT_MESSAGE(kRndisStateTransition, "RNDIS state: {} -> {}") -TETHERKIT_MESSAGE(kRndisControlMessageReceived, "control channel received {} ({:#010x}), {} bytes") -TETHERKIT_MESSAGE(kRndisCondisMessageReceived, "the device sent the connection-oriented (CONDIS) message {}; this driver only " - "supports connectionless 802.3 devices") -TETHERKIT_MESSAGE(kRndisDiscardUnexpectedMessage, "discarding an unexpected control message {} ({:#010x})") -TETHERKIT_MESSAGE(kRndisNotificationReceived, "received a RESPONSE_AVAILABLE notification") -TETHERKIT_MESSAGE(kRndisWaitReplyTimeout, "timed out waiting for {} ({} polls x {} ms)") -TETHERKIT_MESSAGE(kRndisIndicateStatusParseFailed, "failed to parse INDICATE_STATUS: {}") -TETHERKIT_MESSAGE(kRndisDeviceIndicatedWithDiagnostics, "the device reported {} ({:#010x}); diagnostics: DiagStatus={:#010x}, error " - "offset={}") -TETHERKIT_MESSAGE(kRndisDeviceIndicated, "the device reported {} ({:#010x})") -TETHERKIT_MESSAGE(kRndisKeepAliveMsgTooShort, "the KEEPALIVE_MSG from the device is shorter than 12 bytes") -TETHERKIT_MESSAGE(kRndisDeviceKeepAlive, "the device initiated a keep-alive (RequestId={}); replying with KEEPALIVE_CMPLT") -TETHERKIT_MESSAGE(kRndisOptionalOidQueryFailed, "failed to query optional OID {} (not fatal): {}") -TETHERKIT_MESSAGE(kRndisOidQueryFailed, "failed to query OID {}") -TETHERKIT_MESSAGE(kRndisOptionalOidUnsupported, "the device does not support optional OID {} ({})") -TETHERKIT_MESSAGE(kRndisOidQueryRejected, "query of OID {} was rejected: {}") -TETHERKIT_MESSAGE(kRndisOidSetRejected, "setting OID {} = {:#x} was rejected: {}") -TETHERKIT_MESSAGE(kRndisOidSet, "set OID {} = {:#x}") -TETHERKIT_MESSAGE(kRndisPermanentMac, "device permanent MAC: {}") -TETHERKIT_MESSAGE(kRndisMtuLoweredToDeviceFrameSize, "the device reports a maximum frame size of {}, below the negotiated MTU of {}; " - "lowering the MTU") -TETHERKIT_MESSAGE(kRndisDeviceInfo, "device info: MAC {}, link {:.0f} Mbps, max frame {}, medium {}, vendor {:#x} " - "\"{}\"") -TETHERKIT_MESSAGE(kRndisStartWrongState, "Start() may only be called from the uninitialized state; the current state is " - "\"{}\"") -TETHERKIT_MESSAGE(kRndisInitFailed, "RNDIS initialization failed") -TETHERKIT_MESSAGE(kRndisMaxPacketsClamped, "the device reports MaxPacketsPerMessage={}; clamping to {} as the caller " - "requested") -TETHERKIT_MESSAGE(kRndisNegotiated, "RNDIS negotiation complete: version {}.{}, MTU {}, device aggregation limit {} " - "bytes / {} packets, TX alignment {} bytes") -TETHERKIT_MESSAGE(kRndisPacketFilterFailed, "failed to set the packet filter; the data channel cannot be opened") -TETHERKIT_MESSAGE(kRndisDrainControlFailed, "failed to drain unsolicited control-channel messages") -TETHERKIT_MESSAGE(kRndisKeepAliveFailed, "keep-alive failed ({} consecutive of {}): {}") -TETHERKIT_MESSAGE(kRndisKeepAliveDeadLink, "{} consecutive keep-alive failures; treating the link as dead") -TETHERKIT_MESSAGE(kRndisKeepAliveRejected, "the device's KEEPALIVE_CMPLT returned {}; attempting a soft reset") -TETHERKIT_MESSAGE(kRndisKeepAliveOk, "keep-alive OK") -TETHERKIT_MESSAGE(kRndisResetStarted, "starting an RNDIS soft reset") -TETHERKIT_MESSAGE(kRndisResetRejected, "the soft reset was rejected: {}") -TETHERKIT_MESSAGE(kRndisResetDone, "soft reset complete; addressing information {}") -TETHERKIT_MESSAGE(kRndisAddressingLost, "was lost and must be replayed") -TETHERKIT_MESSAGE(kRndisAddressingKept, "is still valid") -TETHERKIT_MESSAGE(kRndisPacketFilterReplayed, "replayed the packet filter setting {:#x}") -TETHERKIT_MESSAGE(kRndisEncodeHaltFailed, "failed to encode HALT_MSG: {}") -TETHERKIT_MESSAGE(kRndisSendHaltFailed, "failed to send HALT_MSG (the device may already be disconnected): {}") -TETHERKIT_MESSAGE(kRndisHaltSent, "sent HALT_MSG") -TETHERKIT_MESSAGE(kRndisClearPacketFilterFailed, "failed to clear the packet filter (sending HALT anyway): {}") - -// ============================================================================= -// usb -- context, device discovery, and transfer channels -// ============================================================================= - -// --- context --- -TETHERKIT_MESSAGE(kUsbInitFailed, "libusb_init failed") -TETHERKIT_MESSAGE(kUsbInitialized, "libusb initialized: {} (hotplug {})") -TETHERKIT_MESSAGE(kUsbHotplugSupported, "supported") -TETHERKIT_MESSAGE(kUsbHotplugUnsupported, "unsupported") -TETHERKIT_MESSAGE(kUsbContextReleased, "released the libusb context") -TETHERKIT_MESSAGE(kUsbUnknownVersion, "unknown version") -TETHERKIT_MESSAGE(kUsbEventThreadStarted, "libusb event thread started") -TETHERKIT_MESSAGE(kUsbEventLoopNoDevice, "the libusb event loop got NO_DEVICE; continuing to wait") -TETHERKIT_MESSAGE(kUsbHandleEventsFailed, "libusb_handle_events returned {} ({}); the event thread is exiting") -TETHERKIT_MESSAGE(kUsbEventThreadExited, "libusb event thread exited") - -// --- device discovery and claiming --- -TETHERKIT_MESSAGE(kUsbGetDeviceListFailed, "libusb_get_device_list failed") -TETHERKIT_MESSAGE(kUsbSkippedAcmModem, "{:04x}:{:04x} interface {} looks like RNDIS by signature but carries non-zero " - "ACM bmCapabilities; treating it as a real cdc-acm modem and skipping it") -TETHERKIT_MESSAGE(kUsbNoPairedDataInterface, "interface {1} of {0} is an RNDIS control interface but has no matching data " - "interface; skipping it") -TETHERKIT_MESSAGE(kUsbDeviceFound, "found RNDIS device {}: control interface {}, data interface {}, signature " - "{:02x}/{:02x}/{:02x}{}") -TETHERKIT_MESSAGE(kUsbViaAndroidQuirk, " (matched via the Android quirk fallback)") -TETHERKIT_MESSAGE(kUsbDeviceGone, "device {} is no longer on the bus") -TETHERKIT_MESSAGE(kUsbOpenFailed, "failed to open device {}") -TETHERKIT_MESSAGE(kUsbClaimFailed, "failed to claim {} (interface {})") -TETHERKIT_MESSAGE(kUsbClaimBusyHint, "the interface is held by a kernel driver. macOS has no RNDIS driver of its own, " - "so this usually means a third-party kext such as HoRNDIS is installed, or " - "another user-space program already claimed the interface") -TETHERKIT_MESSAGE(kUsbClaimNotFoundHint, "the interface does not exist. The device may have switched configuration, or " - "the descriptors were parsed incorrectly") -TETHERKIT_MESSAGE(kUsbControlInterface, "the RNDIS control interface") -TETHERKIT_MESSAGE(kUsbDataInterface, "the RNDIS data interface") -TETHERKIT_MESSAGE(kUsbReadActiveConfigFailed, "failed to read the active configuration descriptor") -TETHERKIT_MESSAGE(kUsbClaimed, "claimed {} ({}): bulk IN 0x{:02x} / OUT 0x{:02x} (wMaxPacketSize {}), " - "interrupt IN {}") -TETHERKIT_MESSAGE(kUsbNoInterruptEndpointShort, "none (falling back to polling the control endpoint)") -TETHERKIT_MESSAGE(kUsbClosed, "closed device {}") -TETHERKIT_MESSAGE(kUsbBulkEndpointsMissing, "no matching pair of bulk endpoints on data interface {} (IN=0x{:02x}, " - "OUT=0x{:02x})") -TETHERKIT_MESSAGE(kUsbBulkMaxPacketSizeZero, "the bulk endpoint has wMaxPacketSize 0") -TETHERKIT_MESSAGE(kUsbUnknownSpeed, "unknown speed") -TETHERKIT_MESSAGE(kUsbClearHaltFailed, "failed to clear the halt condition on endpoint 0x{:02x}") - -// --- interrupt notification --- -TETHERKIT_MESSAGE(kUsbNoInterruptEndpoint, "the device has no interrupt endpoint; skipping the notification listener and " - "falling back to polling the control endpoint") -TETHERKIT_MESSAGE(kUsbNotificationAlreadyRunning, "the notification listener is already running") -TETHERKIT_MESSAGE(kUsbAllocInterruptTransferFailed, "failed to allocate a libusb transfer for interrupt notifications") -TETHERKIT_MESSAGE(kUsbSubmitInterruptFailed, "failed to submit the interrupt notification transfer") -TETHERKIT_MESSAGE(kUsbNotificationStarted, "interrupt notification listener started (endpoint 0x{:02x})") -TETHERKIT_MESSAGE(kUsbCancelInterruptReturned, "cancelling the interrupt notification transfer returned {}") -TETHERKIT_MESSAGE(kUsbInterruptReclaimTimeout, "timed out waiting for the interrupt notification transfer to be reclaimed; " - "leaving it allocated to avoid a use-after-free") -TETHERKIT_MESSAGE(kUsbUnknownNotification, "the interrupt endpoint received an unknown notification {:#010x}; ignoring it") -TETHERKIT_MESSAGE(kUsbInterruptHaltClearFailed, "failed to clear the halt on the interrupt endpoint; falling back to polling the " - "control endpoint: {}") -TETHERKIT_MESSAGE(kUsbInterruptTransferStatus, "interrupt notification transfer status={}; continuing to listen") - -// --- control channel --- -TETHERKIT_MESSAGE(kUsbControlMessageLengthInvalid, "invalid control message length {}") -TETHERKIT_MESSAGE(kUsbSendEncapsulatedFailed, "SEND_ENCAPSULATED_COMMAND failed") -TETHERKIT_MESSAGE(kUsbSendEncapsulatedShort, "SEND_ENCAPSULATED_COMMAND only sent {} of {} bytes") -TETHERKIT_MESSAGE(kUsbGetEncapsulatedFailed, "GET_ENCAPSULATED_RESPONSE failed") - -// --- data channel --- -TETHERKIT_MESSAGE(kUsbTxBufferTooSmall, "the {}-byte TX transfer buffer cannot hold a full frame ({} bytes needed)") -TETHERKIT_MESSAGE(kUsbTxSlotQueueInitFailed, "failed to initialize the TX free-slot queue") -TETHERKIT_MESSAGE(kUsbDataChannelReady, "data channel ready: RX {} x {} KiB ({} KiB in flight), TX {} x {} KiB; device " - "aggregation limit {} bytes / {} packets, alignment {} bytes") -TETHERKIT_MESSAGE(kUsbTransferAllocFailed, "failed to allocate {} bytes ({}-byte aligned) for a USB transfer") -TETHERKIT_MESSAGE(kUsbAllocTransferFailed, "libusb_alloc_transfer failed") -TETHERKIT_MESSAGE(kUsbBulkInSubmitted, "submitted {} bulk IN transfers") -TETHERKIT_MESSAGE(kUsbSubmitBulkInFailed, "failed to submit a bulk IN transfer") -TETHERKIT_MESSAGE(kUsbMalformedRndisMessage, "malformed RNDIS message in a bulk IN transfer: {} ({} frames decoded so far)") -TETHERKIT_MESSAGE(kUsbBulkInStall, "bulk IN endpoint STALL; clearing the halt and continuing") -TETHERKIT_MESSAGE(kUsbBulkInClearHaltFailed, "failed to clear the halt on bulk IN: {}") -TETHERKIT_MESSAGE(kUsbBulkInDeviceGone, "a bulk IN transfer reported that the device is disconnected") -TETHERKIT_MESSAGE(kUsbBulkInFailed, "bulk IN transfer failed: status={}; retrying") -TETHERKIT_MESSAGE(kUsbBulkInResubmitFailed, "failed to resubmit a bulk IN transfer: {}") -TETHERKIT_MESSAGE(kUsbDataChannelShuttingDown, "the data channel is shutting down; refusing to send") -TETHERKIT_MESSAGE(kUsbReturnTxSlotFailed, "failed to return TX slot {} (this should not happen)") -TETHERKIT_MESSAGE(kUsbSubmitBulkOutFailed, "failed to submit a bulk OUT transfer") -TETHERKIT_MESSAGE(kUsbBulkOutSubmitted, "bulk OUT submitted {} frames / {} payload bytes / {} transfer bytes") -TETHERKIT_MESSAGE(kUsbBulkOutStall, "bulk OUT endpoint STALL; clearing the halt") -TETHERKIT_MESSAGE(kUsbBulkOutClearHaltFailed, "failed to clear the halt on bulk OUT: {}") -TETHERKIT_MESSAGE(kUsbBulkOutFailed, "bulk OUT transfer failed: status={}") -TETHERKIT_MESSAGE(kUsbCancelBulkInReturned, "cancelling a bulk IN transfer returned {}") -TETHERKIT_MESSAGE(kUsbCancelBulkOutReturned, "cancelling a bulk OUT transfer returned {}") -TETHERKIT_MESSAGE(kUsbTransferReclaimTimeout, "timed out waiting for {} in-flight USB transfers to be reclaimed. Their memory " - "is deliberately not freed to avoid a use-after-free (leaking about {} KiB). " - "This usually means the device is in a bad state, or the libusb event thread has " - "stopped.") -TETHERKIT_MESSAGE(kUsbDataChannelStopped, "data channel stopped") - -// ============================================================================= -// core -- data-path bridge and runtime orchestration -// ============================================================================= - -// --- bridge --- -TETHERKIT_MESSAGE(kCoreBridgeAlreadyRunning, "the bridge is already running") -TETHERKIT_MESSAGE(kCoreDataPathStarted, "data path started: RX queue {} frames ({} KiB), RX write batch {} frames, TX " - "submit batch {} frames, link batch write {}") -TETHERKIT_MESSAGE(kCoreBatchWriteAvailable, "available") -TETHERKIT_MESSAGE(kCoreBatchWriteUnavailable, "unavailable (writing one frame at a time)") -TETHERKIT_MESSAGE(kCoreDataPathStopped, "data path stopped") -TETHERKIT_MESSAGE(kCoreRxInjectorStarted, "RX injector thread started") -TETHERKIT_MESSAGE(kCoreRxInjectorExited, "RX injector thread exited") -TETHERKIT_MESSAGE(kCoreLinkWriteFailed, "failed to write {} frames to the link: {}") -TETHERKIT_MESSAGE(kCoreTxExtractorStarted, "TX extractor thread started") -TETHERKIT_MESSAGE(kCoreTxExtractorExited, "TX extractor thread exited") -TETHERKIT_MESSAGE(kCoreLinkReadFailed, "failed to read from the link: {}") -TETHERKIT_MESSAGE(kCoreUsbSubmitFailed, "failed to submit {} frames to USB: {}") -TETHERKIT_MESSAGE(kCoreStatsLine, "RX {:>8.0f} pps / {:>8.2f} Mbps (dropped {}) | " - "TX {:>8.0f} pps / {:>8.2f} Mbps (dropped {}) | " - "queue depth {} kernel drops {} backpressure {}") - -// --- runtime state names --- -TETHERKIT_MESSAGE(kCoreRunStateIdle, "idle") -TETHERKIT_MESSAGE(kCoreRunStateStarting, "starting") -TETHERKIT_MESSAGE(kCoreRunStateRunning, "running") -TETHERKIT_MESSAGE(kCoreRunStateStopping, "stopping") -TETHERKIT_MESSAGE(kCoreRunStateStopped, "stopped") -TETHERKIT_MESSAGE(kCoreRunStateFailed, "failed") -TETHERKIT_MESSAGE(kCoreRunStateUnknown, "unknown") - -// --- runtime orchestration --- -TETHERKIT_MESSAGE(kCoreRuntimeAlreadyStarted, "the runtime is already started") -TETHERKIT_MESSAGE(kCoreNeedsRoot, "TetherKit needs root to create the feth virtual interface and open /dev/bpf*.\n" - " Run it with sudo: sudo tetherkit-cli\n" - " (The USB side itself does not need root -- macOS has no RNDIS kernel driver, " - "so libusb can claim the interface directly.)") -TETHERKIT_MESSAGE(kCoreNoDeviceFound, "No RNDIS device found. Check that:\n" - " 1. the device is connected with a USB data cable (not a charge-only one);\n" - " 2. \"USB tethering\" is enabled on the device;\n" - " 3. the phone is unlocked and trusts this Mac (some devices hide the RNDIS " - "interface while locked).\n" - " Run `system_profiler SPUSBDataType` to confirm the system sees the device.") -TETHERKIT_MESSAGE(kCoreMultipleDevices, "found {} RNDIS devices; using the first one (use VID/PID to pick another)") -TETHERKIT_MESSAGE(kCoreExitingOnFatal, "exiting because of an unrecoverable error") -TETHERKIT_MESSAGE(kCoreInterfaceReady, "==== interface ready: {} ====") -TETHERKIT_MESSAGE(kCoreNextStepsAssignIp, "Next, give it an IP address (RNDIS devices usually run their own DHCP server):") -TETHERKIT_MESSAGE(kCoreNextStepsVerify, "Verify with:") -TETHERKIT_MESSAGE(kCoreNextStepsDefaultRoute, "To route traffic through it by default (this replaces the current default " - "route -- be careful):") -TETHERKIT_MESSAGE(kCoreNextStepsTemporaryNote1, "Note: ipconfig set creates a *temporary* service that only lives until the") -TETHERKIT_MESSAGE(kCoreNextStepsTemporaryNote2, " next network configuration change, and never appears in System Settings > " - "Network.") -TETHERKIT_MESSAGE(kCoreStopping, "stopping...") -TETHERKIT_MESSAGE(kCoreStopped, "stopped") -TETHERKIT_MESSAGE(kCoreRndisReady, "RNDIS ready: device MAC {}, MTU {}, link {:.0f} Mbps") -TETHERKIT_MESSAGE(kCoreLinkState, "link state: {}") -TETHERKIT_MESSAGE(kCoreLinkConnected, "connected") -TETHERKIT_MESSAGE(kCoreLinkDisconnected, "disconnected") -TETHERKIT_MESSAGE(kCoreDeviceReset, "the device performed a soft reset (addressing information {})") -TETHERKIT_MESSAGE(kCoreAddressingLost, "was lost and has been replayed") -TETHERKIT_MESSAGE(kCoreAddressingKept, "was preserved") -TETHERKIT_MESSAGE(kCoreLinkUnrecoverable, "the link is unrecoverable") - -// ============================================================================= -// capi -- C ABI layer (interface configuration, sessions, orphan cleanup, subprocesses) -// ============================================================================= - -// --- subprocess --- -TETHERKIT_MESSAGE(kCapiPipeFailed, "failed to create a pipe") -TETHERKIT_MESSAGE(kCapiSpawnFileActionsFailed, "posix_spawn_file_actions_init failed") -TETHERKIT_MESSAGE(kCapiExecFailed, "cannot execute {}") -TETHERKIT_MESSAGE(kCapiWaitFailed, "failed to wait for {} to finish") - -// --- sessions --- -TETHERKIT_MESSAGE(kCapiSessionConfigNull, "tk_session_create: config must not be null") -TETHERKIT_MESSAGE(kCapiSessionOutOfMemory, "out of memory; cannot create the session") - -// --- orphaned interface cleanup --- -TETHERKIT_MESSAGE(kCapiCleanupNeedsRoot, "cleaning up leftover virtual interfaces requires root") -TETHERKIT_MESSAGE(kCapiOrphanAlreadyGone, "leftover interface {} is already gone, or cannot be destroyed") - -// --- interface IP configuration --- -TETHERKIT_MESSAGE(kCapiInterfaceNameNull, "the interface name must not be empty") -TETHERKIT_MESSAGE(kCapiInterfaceNotOurs, "refusing to touch interface \"{}\": only feth interfaces created by " - "TetherKit are accepted.\n" - " This guard exists so a mistake cannot wipe the configuration of a real " - "interface such as en0.") -TETHERKIT_MESSAGE(kCapiCommandFailed, "{} failed (exit code {}): {}") -TETHERKIT_MESSAGE(kCapiCommandNoOutput, "the command printed no explanation") -TETHERKIT_MESSAGE(kCapiDnsPublishFailed, "failed to write the DNS key (service {}); DNS may not take effect in manual " - "mode") -TETHERKIT_MESSAGE(kCapiWhatAddScopedRoute, "adding a scoped default route") -TETHERKIT_MESSAGE(kCapiWhatUpdateScopedRoute, "updating the scoped default route") -TETHERKIT_MESSAGE(kCapiWhatSwitchGlobalRoute, "switching the global default route") -TETHERKIT_MESSAGE(kCapiWhatAddGlobalRoute, "adding a global default route") -TETHERKIT_MESSAGE(kCapiWhatStartDhcp, "starting DHCP") -TETHERKIT_MESSAGE(kCapiDhcpTimeout, "DHCP did not obtain a lease within {} seconds.\n" - " Common causes: tethering is not enabled on the device, or the peer runs no " - "DHCP server.\n" - " The service keeps retrying in the background, so an address may still appear " - "if you refresh later; you can also switch to a static IP.") -TETHERKIT_MESSAGE(kCapiDhcpNoRouter, "got a DHCP lease, but it carries no router address, so the global default route " - "cannot be pointed at it.") -TETHERKIT_MESSAGE(kCapiInvalidAddress, "\"{}\" is not a valid IPv4 address") -TETHERKIT_MESSAGE(kCapiInvalidNetmask, "subnet mask \"{}\" is not a valid IPv4 address") -TETHERKIT_MESSAGE(kCapiInvalidRouter, "router \"{}\" is not a valid IPv4 address") -TETHERKIT_MESSAGE(kCapiDefaultRouteNeedsRouter, "\"Use as default route\" requires a router address") -TETHERKIT_MESSAGE(kCapiInvalidDns, "DNS server \"{}\" is not a valid IPv4 address") -TETHERKIT_MESSAGE(kCapiWhatApplyManual, "applying a static IP") -TETHERKIT_MESSAGE(kCapiNoServiceForDns, "no network service found for {}; DNS cannot be published") -TETHERKIT_MESSAGE(kCapiWhatClearConfig, "clearing the interface configuration") -TETHERKIT_MESSAGE(kCapiApplyConfigNull, "tk_net_apply: config must not be null") -TETHERKIT_MESSAGE(kCapiApplyNeedsRoot, "configuring an interface IP requires root") -TETHERKIT_MESSAGE(kCapiUnknownIpMode, "unknown IP mode {}") -TETHERKIT_MESSAGE(kCapiClearNeedsRoot, "clearing an interface configuration requires root") - -// ============================================================================= -// cli -- the tetherkit-cli command line -// ============================================================================= - -// The CLI help text lives here as whole sentences rather than split into tiny pieces: -// the text must align as fixed layout, and dozens of tiny strings cannot keep the widths consistent. -TETHERKIT_MESSAGE(kCliUsage, "Usage: sudo tetherkit-cli [options]\n" - "\n" - " A user-space RNDIS driver for macOS: turns an RNDIS device (Android USB\n" - " tethering and friends) into an interface the system can see.\n" - "\n" - "Device selection:\n" - " --vid only match this USB vendor ID (e.g. 18d1)\n" - " --pid only match this USB product ID (e.g. 4ee4)\n" - " --list list the RNDIS devices found and exit (**no root needed**)\n" - "\n" - "Interface:\n" - " --mtu desired MTU, default 1500 (lowered automatically if the\n" - " device cannot carry it; capped by sysctl\n" - " net.link.fake.max_mtu)\n" - " --keep-feth-mac do not set the system-side MAC to the address the device\n" - " reports\n" - "\n" - "Performance tuning:\n" - " --rx-transfers bulk IN transfers in flight, default 16\n" - " --tx-transfers bulk OUT transfers in flight, default 4. Measured 4/8/16\n" - " with no discernible difference (a full pool waits for a\n" - " slot instead of dropping frames), so you rarely need this\n" - " --rx-transfer-kb KiB per bulk IN buffer, default 16\n" - " --max-transfer-kb MaxTransferSize announced in INITIALIZE (KiB), default 16.\n" - " This is the only way to make the device aggregate packets\n" - " and is the main throughput lever; larger is usually faster\n" - " --max-tx-packets how many RNDIS packets to aggregate per host -> device\n" - " transfer. 0 (default) means honour the device's reported\n" - " MaxPacketsPerMessage. Diagnostic: if you suspect the device\n" - " claims aggregation but only unpacks the first packet, set 1\n" - " and compare. Note that aggregation worked on every Android\n" - " device tested and clamping to 1 was slower -- do not reach\n" - " for this as a default cure for dropped frames\n" - " --bpf-buffer-kb KiB for the BPF kernel capture buffer, default 4096\n" - "\n" - "Other:\n" - " --stats statistics reporting interval, 0 disables, default 5000\n" - " --log trace|debug|info|warn|error|off, default info\n" - " --lang interface language, default auto -- checks\n" - " TETHERKIT_LANG, LC_ALL, LC_MESSAGES and LANG in that order\n" - " and takes the first non-empty value. English is the only\n" - " language shipped today; en and auto both select it. sudo\n" - " does not always pass those through, so pass --lang\n" - " explicitly then\n" - " --no-color disable coloured log output\n" - " -h, --help show this help\n" - " -V, --version show the version\n" - "\n" - "Why root is required: creating the feth virtual interface and opening /dev/bpf*\n" - "both need root. The USB side does not -- macOS ships no RNDIS kernel driver, so\n" - "libusb can claim the interface directly.\n") - -TETHERKIT_MESSAGE(kCliInstallSignalHandlerFailed, "failed to install a handler for signal {}") -TETHERKIT_MESSAGE(kCliIgnoreSigpipeFailed, "failed to ignore SIGPIPE") -TETHERKIT_MESSAGE(kCliParseNumberFailed, "cannot parse the number \"{}\"") -TETHERKIT_MESSAGE(kCliUnknownLogLevel, "unknown log level \"{}\"") -TETHERKIT_MESSAGE(kCliUnknownLanguage, "unknown language \"{}\"; English is the only language shipped today") -TETHERKIT_MESSAGE(kCliMissingOptionValue, "option {} is missing its argument") -TETHERKIT_MESSAGE(kCliUnknownOption, "unknown option \"{}\"; run --help for usage") -TETHERKIT_MESSAGE(kCliArgumentError, "argument error: {}\n") -TETHERKIT_MESSAGE(kCliNoDevices, "No RNDIS device found.\n") -TETHERKIT_MESSAGE(kCliNoDevicesHint, "Check that: (1) the cable carries data and is not charge-only; (2) USB\n" - " tethering is enabled on the device; (3) the phone is unlocked\n" - " and trusts this Mac.\n") -TETHERKIT_MESSAGE(kCliDevicesHeader, "RNDIS devices found:\n") -TETHERKIT_MESSAGE(kCliDeviceLine, " {} control interface {} / data interface {} signature " - "{:02x}/{:02x}/{:02x}{}\n") -TETHERKIT_MESSAGE(kCliDeviceAndroidQuirk, " (matched via the Android quirk fallback)") -TETHERKIT_MESSAGE(kCliEnumerateFailed, "failed to enumerate devices: {}\n") -TETHERKIT_MESSAGE(kCliStartFailed, "failed to start: {}") -TETHERKIT_MESSAGE(kCliStopSignalReceived, "received a shutdown signal") diff --git a/include/tetherkit/capi/.clang-tidy b/include/tethernet/capi/.clang-tidy similarity index 92% rename from include/tetherkit/capi/.clang-tidy rename to include/tethernet/capi/.clang-tidy index ef64954..f46c7f3 100644 --- a/include/tetherkit/capi/.clang-tidy +++ b/include/tethernet/capi/.clang-tidy @@ -1,7 +1,7 @@ # Check exemptions for the C ABI layer # # This layer is **for C and Swift callers**, so the names must follow the C -# convention (`tk_snake_case`) rather than the CamelCase of the project's +# convention (`tn_snake_case`) rather than the CamelCase of the project's # internal C++ code. Forcing the upper-level rules on it would only produce a # pile of warnings nobody could fix, so the whole naming group is off here. # diff --git a/include/tetherkit/capi/tetherkit_c.h b/include/tethernet/capi/tethernet_c.h similarity index 73% rename from include/tetherkit/capi/tetherkit_c.h rename to include/tethernet/capi/tethernet_c.h index 214ed3e..888d6bb 100644 --- a/include/tetherkit/capi/tetherkit_c.h +++ b/include/tethernet/capi/tethernet_c.h @@ -1,4 +1,4 @@ -// The C ABI of TetherKit -- the **only** extern "C" boundary in the whole +// The C ABI of Tethernet -- the **only** extern "C" boundary in the whole // project. // // Why it exists: Swift's C++ interop cannot swallow this project's C++23 @@ -12,8 +12,8 @@ // 1. **No ownership crosses the boundary.** Every output is written into a // fixed-length struct supplied by the caller, and the library never // allocates memory that the caller would have to free. The only -// exception is the opaque handle tk_session_t, which is created by -// `tk_session_create` and destroyed by `tk_session_destroy` -- a clear +// exception is the opaque handle tn_session_t, which is created by +// `tn_session_create` and destroyed by `tn_session_destroy` -- a clear // pairing. // // 2. **No callbacks, an event queue instead.** The observer callbacks @@ -22,11 +22,11 @@ // worse is reentrancy -- calling stop from inside a callback deadlocks // on itself (Stop() joins the control thread). So everything became // "the library queues, the host polls". The same goes for the logs, see -// `tk_drain_logs`. +// `tn_drain_logs`. // -// 3. **Every function returns `tk_result_t` (0 is success, negative is +// 3. **Every function returns `tn_result_t` (0 is success, negative is // failure)**, and the reason for the failure goes into the optional -// `tk_error_t*`. Passing NULL means the reason does not matter. No +// `tn_error_t*`. Passing NULL means the reason does not matter. No // errno, no thread-local last-error -- both are easy to misread in a // multithreaded host. // @@ -38,17 +38,17 @@ // // Permissions (which functions need root) // -// No root required: `tk_version`, `tk_check_environment`, `tk_list_devices`, -// `tk_drain_logs`, `tk_net_query` -// Root required : `tk_session_*` (it creates the feth and opens -// /dev/bpf*), `tk_net_apply`, `tk_net_clear`, -// `tk_cleanup_orphan_interfaces` +// No root required: `tn_version`, `tn_check_environment`, `tn_list_devices`, +// `tn_drain_logs`, `tn_net_query` +// Root required : `tn_session_*` (it creates the feth and opens +// /dev/bpf*), `tn_net_apply`, `tn_net_clear`, +// `tn_cleanup_orphan_interfaces` // // What the GUI does: the app itself only calls the functions that need no -// root, and hands the ones that do to `tetherkit-helper` (launched by +// root, and hands the ones that do to `tethernet-helper` (launched by // launchd as root). See docs/GUI-ARCHITECTURE.md. -#ifndef TETHERKIT_CAPI_TETHERKIT_C_H_ -#define TETHERKIT_CAPI_TETHERKIT_C_H_ +#ifndef TETHERNET_CAPI_TETHERNET_C_H_ +#define TETHERNET_CAPI_TETHERNET_C_H_ #include #include @@ -59,9 +59,9 @@ extern "C" { #endif #if defined(_WIN32) -#define TK_API +#define TN_API #else -#define TK_API __attribute__((visibility("default"))) +#define TN_API __attribute__((visibility("default"))) #endif // ============================================================================= @@ -75,57 +75,57 @@ extern "C" { /// Error message buffer capacity. The underlying error strings carry a /// multi-line "what to do next" hint, so 1 KiB is enough. -#define TK_ERROR_MESSAGE_CAPACITY 1024 +#define TN_ERROR_MESSAGE_CAPACITY 1024 /// Interface name capacity. Matches the kernel's IFNAMSIZ (16, terminator /// included). -#define TK_INTERFACE_NAME_CAPACITY 16 +#define TN_INTERFACE_NAME_CAPACITY 16 /// USB string descriptor capacity (manufacturer name / product name / serial /// number). -#define TK_USB_STRING_CAPACITY 128 +#define TN_USB_STRING_CAPACITY 128 /// IP address text capacity. Matches INET6_ADDRSTRLEN (46). -#define TK_ADDRESS_CAPACITY 46 +#define TN_ADDRESS_CAPACITY 46 /// How many DNS servers one configuration accepts at most. -#define TK_DNS_MAX 4 +#define TN_DNS_MAX 4 /// Text capacity of a single log line / event. -#define TK_TEXT_CAPACITY 512 +#define TN_TEXT_CAPACITY 512 /// Thread name capacity, matching the real limit of pthread_setname_np. -#define TK_THREAD_NAME_CAPACITY 16 +#define TN_THREAD_NAME_CAPACITY 16 // ============================================================================= // Result codes and errors // ============================================================================= -typedef enum tk_result { - TK_OK = 0, ///< Success. - TK_ERR_INVALID_ARGUMENT = -1, ///< Invalid argument (null pointer, out-of-range +typedef enum tn_result { + TN_OK = 0, ///< Success. + TN_ERR_INVALID_ARGUMENT = -1, ///< Invalid argument (null pointer, out-of-range ///< enum, malformed address). - TK_ERR_INVALID_STATE = -2, ///< The operation is not allowed in the current + TN_ERR_INVALID_STATE = -2, ///< The operation is not allowed in the current ///< state (e.g. a repeated start). - TK_ERR_FAILED = -3, ///< The underlying operation failed; the - ///< details are in tk_error_t. - TK_ERR_PERMISSION = -4, ///< Root is required but the process is not root. - TK_ERR_NOT_FOUND = -5, ///< The target does not exist (no device, no + TN_ERR_FAILED = -3, ///< The underlying operation failed; the + ///< details are in tn_error_t. + TN_ERR_PERMISSION = -4, ///< Root is required but the process is not root. + TN_ERR_NOT_FOUND = -5, ///< The target does not exist (no device, no ///< such interface). -} tk_result_t; +} tn_result_t; /// The domain an error code comes from; it decides how the `code` field is to -/// be interpreted. It corresponds one to one with `tetherkit::ErrorDomain` on +/// be interpreted. It corresponds one to one with `tethernet::ErrorDomain` on /// the C++ side. -typedef enum tk_error_domain { - TK_ERROR_DOMAIN_GENERIC = 0, ///< A purely logical error; `code` is +typedef enum tn_error_domain { + TN_ERROR_DOMAIN_GENERIC = 0, ///< A purely logical error; `code` is ///< meaningless. - TK_ERROR_DOMAIN_ERRNO = 1, ///< A POSIX errno. - TK_ERROR_DOMAIN_LIBUSB = 2, ///< libusb's enum libusb_error (negative). - TK_ERROR_DOMAIN_RNDIS = 3, ///< RNDIS_STATUS_*. -} tk_error_domain_t; + TN_ERROR_DOMAIN_ERRNO = 1, ///< A POSIX errno. + TN_ERROR_DOMAIN_LIBUSB = 2, ///< libusb's enum libusb_error (negative). + TN_ERROR_DOMAIN_RNDIS = 3, ///< RNDIS_STATUS_*. +} tn_error_domain_t; /// The full description of one failure. `message` is already a text that can /// be shown to the user as is. -typedef struct tk_error { - int32_t domain; ///< tk_error_domain_t +typedef struct tn_error { + int32_t domain; ///< tn_error_domain_t int64_t code; - char message[TK_ERROR_MESSAGE_CAPACITY]; -} tk_error_t; + char message[TN_ERROR_MESSAGE_CAPACITY]; +} tn_error_t; // ============================================================================= // Interface language @@ -145,9 +145,9 @@ typedef struct tk_error { // messages.def), this enum gains a value; old callers compiled against the // narrower enum keep working because unknown values are ignored at set // and degrade to English output by design. -typedef enum tk_language { - TK_LANGUAGE_ENGLISH = 0, -} tk_language_t; +typedef enum tn_language { + TN_LANGUAGE_ENGLISH = 0, +} tn_language_t; /// Sets the interface language. Any thread, any time. /// @@ -156,44 +156,44 @@ typedef enum tk_language { /// when they were produced. The GUI should call this once early at startup /// and then only when the user switches the language. /// -/// A `language` value outside tk_language_t is ignored by design, so a +/// A `language` value outside tn_language_t is ignored by design, so a /// caller built against an older (or future) enumeration keeps working and /// simply stays in English. -TK_API void tk_set_language(int32_t language); +TN_API void tn_set_language(int32_t language); -/// Reads the current interface language as a tk_language_t value. -TK_API int32_t tk_get_language(void); +/// Reads the current interface language as a tn_language_t value. +TN_API int32_t tn_get_language(void); -/// Infers the language from environment variables, checking TETHERKIT_LANG, +/// Infers the language from environment variables, checking TETHERNET_LANG, /// LC_ALL, LC_MESSAGES, and LANG in that order. /// /// Infers only; does not change the current setting. The GUI normally does /// not need it (macOS language preference belongs to NSLocale); it exists /// for command-line hosts and third-party bindings. -TK_API int32_t tk_detect_system_language(void); +TN_API int32_t tn_detect_system_language(void); // ============================================================================= // Version and environment preflight (neither needs root) // ============================================================================= -typedef struct tk_version_info { +typedef struct tn_version_info { uint16_t major; uint16_t minor; uint16_t patch; - /// Of the form "TetherKit 0.1.1 (C++23, macOS 13.3+)". + /// Of the form "Tethernet 0.1.1 (C++23, macOS 13.3+)". char text[64]; /// The build configuration description, of the form "RelWithDebInfo, /// AppleClang 21.0.0, ...". char build[192]; /// The libusb version string. char libusb[64]; -} tk_version_info_t; +} tn_version_info_t; -TK_API void tk_version(tk_version_info_t* out_version); +TN_API void tn_version(tn_version_info_t* out_version); /// The result of the environment preflight. The GUI shows it before startup /// to make "why it will not run" clear up front. -typedef struct tk_environment { +typedef struct tn_environment { /// Whether the current process runs as root. bool is_root; /// Whether the feth creation-time sysctls all hold the values we require. @@ -204,23 +204,23 @@ typedef struct tk_environment { bool sysctls_ok; /// The specifics when the sysctls do not qualify; an empty string when /// they do. - char sysctl_detail[TK_ERROR_MESSAGE_CAPACITY]; + char sysctl_detail[TN_ERROR_MESSAGE_CAPACITY]; /// The MTU ceiling feth supports (sysctl net.link.fake.max_mtu). 0 when /// the query failed. uint32_t feth_max_mtu; -} tk_environment_t; +} tn_environment_t; /// Collects the environment information. **This function always returns -/// TK_OK** -- "the environment does not qualify" is not a call failure but a +/// TN_OK** -- "the environment does not qualify" is not a call failure but a /// result to show to the user. -TK_API tk_result_t tk_check_environment(tk_environment_t* out_environment); +TN_API tn_result_t tn_check_environment(tn_environment_t* out_environment); // ============================================================================= // Device enumeration (no root required) // ============================================================================= /// A candidate device recognized as RNDIS. -typedef struct tk_device_info { +typedef struct tn_device_info { uint16_t vendor_id; uint16_t product_id; uint8_t bus_number; @@ -246,12 +246,12 @@ typedef struct tk_device_info { /// only when there is no previous value either is it an empty string, and /// then the GUI should fall back to showing `description` (the VID:PID /// form). - char manufacturer[TK_USB_STRING_CAPACITY]; - char product[TK_USB_STRING_CAPACITY]; - char serial[TK_USB_STRING_CAPACITY]; + char manufacturer[TN_USB_STRING_CAPACITY]; + char product[TN_USB_STRING_CAPACITY]; + char serial[TN_USB_STRING_CAPACITY]; /// Of the form "Bus 020 Device 003: 18d1:4ee4", available in every case. char description[64]; -} tk_device_info_t; +} tn_device_info_t; /// Enumerates the RNDIS devices currently connected. /// @@ -271,40 +271,40 @@ typedef struct tk_device_info { /// the read fails, the values last read successfully in /// this process are filled back in, so the names will /// not turn into empty strings because of it. -TK_API tk_result_t tk_list_devices(tk_device_info_t* out_devices, size_t capacity, - size_t* out_count, bool read_strings, tk_error_t* out_error); +TN_API tn_result_t tn_list_devices(tn_device_info_t* out_devices, size_t capacity, + size_t* out_count, bool read_strings, tn_error_t* out_error); // ============================================================================= // Logs // ============================================================================= -typedef enum tk_log_level { - TK_LOG_TRACE = 0, - TK_LOG_DEBUG = 1, - TK_LOG_INFO = 2, - TK_LOG_WARN = 3, - TK_LOG_ERROR = 4, - TK_LOG_OFF = 5, -} tk_log_level_t; - -typedef struct tk_log_record { - int32_t level; ///< tk_log_level_t +typedef enum tn_log_level { + TN_LOG_TRACE = 0, + TN_LOG_DEBUG = 1, + TN_LOG_INFO = 2, + TN_LOG_WARN = 3, + TN_LOG_ERROR = 4, + TN_LOG_OFF = 5, +} tn_log_level_t; + +typedef struct tn_log_record { + int32_t level; ///< tn_log_level_t /// Wall clock time (nanoseconds since the Unix epoch). Wall clock rather /// than monotonic time: the logs have to line up with the system logs. int64_t wall_nanos; - char thread[TK_THREAD_NAME_CAPACITY]; - char message[TK_TEXT_CAPACITY]; -} tk_log_record_t; + char thread[TN_THREAD_NAME_CAPACITY]; + char message[TN_TEXT_CAPACITY]; +} tn_log_record_t; -TK_API void tk_set_log_level(int32_t level); +TN_API void tn_set_log_level(int32_t level); /// Turns log capture on / off. /// /// **Off** by default -- a command line program does not need it, and having /// it on only costs an extra copy. A GUI host calls -/// `tk_enable_log_capture(true)` at startup and then periodically takes the -/// records away with `tk_drain_logs`. -TK_API void tk_enable_log_capture(bool enabled); +/// `tn_enable_log_capture(true)` at startup and then periodically takes the +/// records away with `tn_drain_logs`. +TN_API void tn_enable_log_capture(bool enabled); /// Takes the buffered log lines away (first in, first out) and returns how /// many were actually taken. @@ -314,40 +314,39 @@ TK_API void tk_enable_log_capture(bool enabled); /// more useful than keeping the beginning. The number of dropped records is /// reported through `out_dropped`, so the GUI can hint "there is a gap in the /// logs". -TK_API size_t tk_drain_logs(tk_log_record_t* out_records, size_t capacity, - uint64_t* out_dropped); +TN_API size_t tn_drain_logs(tn_log_record_t* out_records, size_t capacity, uint64_t* out_dropped); // ============================================================================= // Sessions (root required) // ============================================================================= /// The host-side RNDIS state, corresponding one to one with -/// `tetherkit::rndis::State` on the C++ side. -typedef enum tk_rndis_state { - TK_RNDIS_UNINITIALIZED = 0, - TK_RNDIS_INITIALIZING = 1, - TK_RNDIS_INITIALIZED = 2, - TK_RNDIS_DATA_INITIALIZED = 3, ///< The data starts flowing. - TK_RNDIS_HALTING = 4, -} tk_rndis_state_t; +/// `tethernet::rndis::State` on the C++ side. +typedef enum tn_rndis_state { + TN_RNDIS_UNINITIALIZED = 0, + TN_RNDIS_INITIALIZING = 1, + TN_RNDIS_INITIALIZED = 2, + TN_RNDIS_DATA_INITIALIZED = 3, ///< The data starts flowing. + TN_RNDIS_HALTING = 4, +} tn_rndis_state_t; /// The lifecycle state of a session. This is the state the GUI main window /// binds to directly. -typedef enum tk_run_state { - TK_RUN_IDLE = 0, ///< Created, not yet started. - TK_RUN_STARTING = 1, ///< Start sequence: enumerate, handshake, create the +typedef enum tn_run_state { + TN_RUN_IDLE = 0, ///< Created, not yet started. + TN_RUN_STARTING = 1, ///< Start sequence: enumerate, handshake, create the ///< interface, open the bridge. - TK_RUN_RUNNING = 2, ///< The data paths are up. - TK_RUN_STOPPING = 3, - TK_RUN_STOPPED = 4, - TK_RUN_FAILED = 5, ///< The start failed or an unrecoverable error + TN_RUN_RUNNING = 2, ///< The data paths are up. + TN_RUN_STOPPING = 3, + TN_RUN_STOPPED = 4, + TN_RUN_FAILED = 5, ///< The start failed or an unrecoverable error ///< occurred; the reason is in status.fatal. -} tk_run_state_t; +} tn_run_state_t; -/// Session configuration. Fill in the defaults with `tk_session_config_init` +/// Session configuration. Fill in the defaults with `tn_session_config_init` /// and then change the fields you care about -- a plain `{0}` initialization /// leaves a pile of invalid zeros. -typedef struct tk_session_config { +typedef struct tn_session_config { /// The device filter. All-zero means use the first RNDIS device found. uint16_t vendor_id; uint16_t product_id; @@ -374,54 +373,54 @@ typedef struct tk_session_config { uint32_t max_transfer_kib; /// The KiB size of the kernel BPF capture buffer. uint32_t bpf_buffer_kib; -} tk_session_config_t; +} tn_session_config_t; /// Fills the configuration with the recommended defaults. -TK_API void tk_session_config_init(tk_session_config_t* out_config); +TN_API void tn_session_config_init(tn_session_config_t* out_config); /// Session events. -typedef enum tk_event_kind { +typedef enum tn_event_kind { /// An RNDIS state transition. `a` = the state before the transition, - /// `b` = the state after it (both tk_rndis_state_t). - TK_EVENT_RNDIS_STATE = 0, + /// `b` = the state after it (both tn_rndis_state_t). + TN_EVENT_RNDIS_STATE = 0, /// Negotiation complete. `a` = the final MTU, `b` = the link speed (Mbps). - TK_EVENT_NEGOTIATED = 1, + TN_EVENT_NEGOTIATED = 1, /// Link up / down. `a` = 1 means connected. - TK_EVENT_LINK = 2, + TN_EVENT_LINK = 2, /// Device soft reset. `a` = 1 means the addressing information was lost /// and replayed. - TK_EVENT_DEVICE_RESET = 3, + TN_EVENT_DEVICE_RESET = 3, /// An unrecoverable error; `text` is the reason. The session then enters - /// TK_RUN_FAILED. - TK_EVENT_FATAL = 4, + /// TN_RUN_FAILED. + TN_EVENT_FATAL = 4, /// A lifecycle state change of the session. `a` = before the transition, - /// `b` = after it (both tk_run_state_t). - TK_EVENT_RUN_STATE = 5, -} tk_event_kind_t; + /// `b` = after it (both tn_run_state_t). + TN_EVENT_RUN_STATE = 5, +} tn_event_kind_t; -typedef struct tk_event { - int32_t kind; ///< tk_event_kind_t +typedef struct tn_event { + int32_t kind; ///< tn_event_kind_t int64_t wall_nanos; int64_t a; int64_t b; - char text[TK_TEXT_CAPACITY]; -} tk_event_t; + char text[TN_TEXT_CAPACITY]; +} tn_event_t; /// A complete snapshot of the data paths and the link. The GUI pulls it /// periodically (500 ms to 1 s is recommended) to refresh the UI. -typedef struct tk_session_status { - int32_t run_state; ///< tk_run_state_t - int32_t rndis_state; ///< tk_rndis_state_t +typedef struct tn_session_status { + int32_t run_state; ///< tn_run_state_t + int32_t rndis_state; ///< tn_rndis_state_t bool link_up; bool paused; ///< Whether the data movement is paused (link down or during a ///< device reset). /// The system-side interface name (the host configures the IP on this /// one). An empty string until it is created. - char system_interface[TK_INTERFACE_NAME_CAPACITY]; - /// The driver-side interface name (TetherKit's BPF attaches to this one). + char system_interface[TN_INTERFACE_NAME_CAPACITY]; + /// The driver-side interface name (Tethernet's BPF attaches to this one). /// For diagnostics display only. - char driver_interface[TK_INTERFACE_NAME_CAPACITY]; + char driver_interface[TN_INTERFACE_NAME_CAPACITY]; /// The permanent MAC the device reports. All zero when not negotiated. uint8_t device_mac[6]; @@ -456,23 +455,23 @@ typedef struct tk_session_status { /// between two pulls can be stretched by scheduling. int64_t monotonic_nanos; - /// The reason when run_state == TK_RUN_FAILED; an empty string otherwise. - char fatal[TK_ERROR_MESSAGE_CAPACITY]; -} tk_session_status_t; + /// The reason when run_state == TN_RUN_FAILED; an empty string otherwise. + char fatal[TN_ERROR_MESSAGE_CAPACITY]; +} tn_session_status_t; /// An opaque session handle. -typedef struct tk_session tk_session_t; +typedef struct tn_session tn_session_t; /// Creates a session. **Does no I/O at all**, it only records the /// configuration. -TK_API tk_session_t* tk_session_create(const tk_session_config_t* config, tk_error_t* out_error); +TN_API tn_session_t* tn_session_create(const tn_session_config_t* config, tn_error_t* out_error); /// Starts the session. **Non-blocking**: internally it starts one control /// thread that runs the start sequence and the keep-alive loop, and this /// function returns immediately. /// -/// Returning TK_OK only means "the start request has been accepted"; the real -/// outcome has to be observed through the run_state of `tk_session_status` and +/// Returning TN_OK only means "the start request has been accepted"; the real +/// outcome has to be observed through the run_state of `tn_session_status` and /// the event queue. The reason for this design is that the start sequence /// contains a USB handshake, which on a slow device can take hundreds of /// milliseconds to seconds, and blocking the GUI main thread is @@ -482,7 +481,7 @@ TK_API tk_session_t* tk_session_create(const tk_session_config_t* config, tk_err /// synchronous API returns LIBUSB_ERROR_BUSY on the event thread); the /// library guarantees this internally, so the caller does not have to care /// about threads. -TK_API tk_result_t tk_session_start(tk_session_t* session, tk_error_t* out_error); +TN_API tn_result_t tn_session_start(tn_session_t* session, tn_error_t* out_error); /// Requests the shutdown and waits for the control thread to exit. /// Idempotent. @@ -491,13 +490,13 @@ TK_API tk_result_t tk_session_start(tk_session_t* session, tk_error_t* out_error /// function joins the control thread. The C ABI layer has no callbacks, so /// as long as the caller does not hold a lock in its own event handling that /// would cause reentrancy, it is fine. -TK_API tk_result_t tk_session_stop(tk_session_t* session); +TN_API tn_result_t tn_session_stop(tn_session_t* session); /// Stops and destroys. Passing NULL is a no-op. -TK_API void tk_session_destroy(tk_session_t* session); +TN_API void tn_session_destroy(tn_session_t* session); /// Takes a status snapshot. Callable from any thread. -TK_API tk_result_t tk_session_status_get(tk_session_t* session, tk_session_status_t* out_status); +TN_API tn_result_t tn_session_status_get(tn_session_t* session, tn_session_status_t* out_status); /// Takes the queued events away (first in, first out) and returns how many /// were actually taken. @@ -505,8 +504,8 @@ TK_API tk_result_t tk_session_status_get(tk_session_t* session, tk_session_statu /// Like the logs it is a fixed-length ring queue; when it fills up the oldest /// are dropped. Missing a few state transition events does not affect /// correctness -- the authoritative state is always the snapshot from -/// `tk_session_status_get`; the events are only for animations and hints. -TK_API size_t tk_session_poll_events(tk_session_t* session, tk_event_t* out_events, +/// `tn_session_status_get`; the events are only for animations and hints. +TN_API size_t tn_session_poll_events(tn_session_t* session, tn_event_t* out_events, size_t capacity); // ============================================================================= @@ -514,32 +513,32 @@ TK_API size_t tk_session_poll_events(tk_session_t* session, tk_event_t* out_even // ============================================================================= /// The network access mode. -typedef enum tk_ip_mode { +typedef enum tn_ip_mode { /// Hand it to the system's IPConfiguration to run DHCP. It takes care of /// four things: getting the lease, setting up the scoped DNS, installing /// the scoped default route, and publishing the service into the dynamic /// store. - TK_IP_MODE_DHCP = 0, + TN_IP_MODE_DHCP = 0, /// A static IP. The address is still delivered through IPConfiguration /// (rather than a bare SIOCAIFADDR), because only that way does one get a /// service "registered through the proper path" -- writing entries into /// the dynamic store by hand is **not adopted by IPMonitor**, which has /// been measured and confirmed. - TK_IP_MODE_MANUAL = 1, + TN_IP_MODE_MANUAL = 1, /// Revokes the configuration (equivalent to `ipconfig set NONE`). - TK_IP_MODE_NONE = 2, -} tk_ip_mode_t; + TN_IP_MODE_NONE = 2, +} tn_ip_mode_t; -typedef struct tk_ip_config { - int32_t mode; ///< tk_ip_mode_t +typedef struct tn_ip_config { + int32_t mode; ///< tn_ip_mode_t - /// The four items below are only used when mode == TK_IP_MODE_MANUAL. - char address[TK_ADDRESS_CAPACITY]; - char netmask[TK_ADDRESS_CAPACITY]; + /// The four items below are only used when mode == TN_IP_MODE_MANUAL. + char address[TN_ADDRESS_CAPACITY]; + char netmask[TN_ADDRESS_CAPACITY]; /// The router (gateway). An empty string means no default route is /// configured at all. - char router[TK_ADDRESS_CAPACITY]; - char dns[TK_DNS_MAX][TK_ADDRESS_CAPACITY]; + char router[TN_ADDRESS_CAPACITY]; + char dns[TN_DNS_MAX][TN_ADDRESS_CAPACITY]; int32_t dns_count; /// Whether to point the **global** default route at this interface as well. @@ -552,10 +551,10 @@ typedef struct tk_ip_config { /// other network is available, so this interface naturally is the primary /// service and the switch does not have to be touched. bool set_default_route; -} tk_ip_config_t; +} tn_ip_config_t; /// Fills the configuration with the defaults (DHCP). -TK_API void tk_ip_config_init(tk_ip_config_t* out_config); +TN_API void tn_ip_config_init(tn_ip_config_t* out_config); /// The IP state the interface **really has in effect** right now. /// @@ -563,12 +562,12 @@ TK_API void tk_ip_config_init(tk_ip_config_t* out_config); /// state back: in static mode whether the DNS takes effect depends on /// IPMonitor accepting the service, and only a readback can give the user /// accurate feedback. -typedef struct tk_net_state { +typedef struct tn_net_state { bool has_address; - char address[TK_ADDRESS_CAPACITY]; - char netmask[TK_ADDRESS_CAPACITY]; - char router[TK_ADDRESS_CAPACITY]; - char dns[TK_DNS_MAX][TK_ADDRESS_CAPACITY]; + char address[TN_ADDRESS_CAPACITY]; + char netmask[TN_ADDRESS_CAPACITY]; + char router[TN_ADDRESS_CAPACITY]; + char dns[TN_DNS_MAX][TN_ADDRESS_CAPACITY]; int32_t dns_count; /// The configuration method IPConfiguration reports ("DHCP" / "MANUAL" / /// "NONE" / ""). @@ -579,21 +578,21 @@ typedef struct tk_net_state { bool has_default_route; /// Whether the global default route currently points at this interface. bool is_primary_default_route; -} tk_net_state_t; +} tn_net_state_t; /// Configures the IP of an interface according to the configuration. Needs /// root. -TK_API tk_result_t tk_net_apply(const char* interface_name, const tk_ip_config_t* config, - tk_error_t* out_error); +TN_API tn_result_t tn_net_apply(const char* interface_name, const tn_ip_config_t* config, + tn_error_t* out_error); /// Revokes the IP configuration on an interface (`ipconfig set NONE`). /// Needs root. -TK_API tk_result_t tk_net_clear(const char* interface_name, tk_error_t* out_error); +TN_API tn_result_t tn_net_clear(const char* interface_name, tn_error_t* out_error); /// Reads back the IP state really in effect on an interface. **Does not need /// root**. -TK_API tk_result_t tk_net_query(const char* interface_name, tk_net_state_t* out_state, - tk_error_t* out_error); +TN_API tn_result_t tn_net_query(const char* interface_name, tn_net_state_t* out_state, + tn_error_t* out_error); // ============================================================================= // Orphan interface cleanup (root required) @@ -605,16 +604,16 @@ TK_API tk_result_t tk_net_query(const char* interface_name, tk_net_state_t* out_ /// Why it is needed: when the process is SIGKILLed the C++ destructors do /// not run and the feth stays in the kernel; a signal handler cannot catch /// SIGKILL, so the only option is a **cleanup backstop at the next start**. -/// The session records the interface names in `/var/run/tetherkit-interfaces` +/// The session records the interface names in `/var/run/tethernet-interfaces` /// when it creates the feth; this function reads it and destroys them one by /// one. /// /// @param out_removed Receives the number of interfaces actually destroyed; /// may be NULL. -TK_API tk_result_t tk_cleanup_orphan_interfaces(size_t* out_removed, tk_error_t* out_error); +TN_API tn_result_t tn_cleanup_orphan_interfaces(size_t* out_removed, tn_error_t* out_error); #if defined(__cplusplus) } // extern "C" #endif -#endif // TETHERKIT_CAPI_TETHERKIT_C_H_ +#endif // TETHERNET_CAPI_TETHERNET_C_H_ diff --git a/include/tetherkit/common/byte_order.h b/include/tethernet/common/byte_order.h similarity index 98% rename from include/tetherkit/common/byte_order.h rename to include/tethernet/common/byte_order.h index ac6c656..9df77ca 100644 --- a/include/tetherkit/common/byte_order.h +++ b/include/tethernet/common/byte_order.h @@ -21,7 +21,7 @@ #include #include -namespace tetherkit { +namespace tethernet { /// Unsigned integer types that appear in wire formats. template @@ -114,4 +114,4 @@ template return value != 0 && (value & (value - 1)) == 0; } -} // namespace tetherkit +} // namespace tethernet diff --git a/include/tetherkit/common/cache.h b/include/tethernet/common/cache.h similarity index 90% rename from include/tetherkit/common/cache.h rename to include/tethernet/common/cache.h index dd3c2e2..86eb822 100644 --- a/include/tetherkit/common/cache.h +++ b/include/tethernet/common/cache.h @@ -3,7 +3,7 @@ #include -namespace tetherkit { +namespace tethernet { /// Cache line size. Hardcoded to 128. /// @@ -22,6 +22,6 @@ namespace tetherkit { inline constexpr std::size_t kCacheLineSize = 128; /// Alignment specifier that gives a member its own cache line. -#define TETHERKIT_CACHE_ALIGNED alignas(::tetherkit::kCacheLineSize) +#define TETHERNET_CACHE_ALIGNED alignas(::tethernet::kCacheLineSize) -} // namespace tetherkit +} // namespace tethernet diff --git a/include/tetherkit/common/error.h b/include/tethernet/common/error.h similarity index 83% rename from include/tetherkit/common/error.h rename to include/tethernet/common/error.h index 18183f2..9e3546b 100644 --- a/include/tetherkit/common/error.h +++ b/include/tethernet/common/error.h @@ -17,7 +17,7 @@ #include #include -namespace tetherkit { +namespace tethernet { namespace detail { @@ -114,35 +114,35 @@ inline Status Ok() noexcept { namespace detail { // Generate unique temporary variable names to avoid shadowing during macro expansion. -#define TETHERKIT_CONCAT_INNER(a, b) a##b -#define TETHERKIT_CONCAT(a, b) TETHERKIT_CONCAT_INNER(a, b) -#define TETHERKIT_UNIQUE(base) TETHERKIT_CONCAT(base, __LINE__) +#define TETHERNET_CONCAT_INNER(a, b) a##b +#define TETHERNET_CONCAT(a, b) TETHERNET_CONCAT_INNER(a, b) +#define TETHERNET_UNIQUE(base) TETHERNET_CONCAT(base, __LINE__) } // namespace detail /// Propagate the error upward immediately if the expression fails. /// -/// Usage: `TETHERKIT_RETURN_IF_ERROR(link.Configure(mtu));` -#define TETHERKIT_RETURN_IF_ERROR(expr) \ - do { \ - auto TETHERKIT_UNIQUE(tk_status_) = (expr); \ - if (!TETHERKIT_UNIQUE(tk_status_)) { \ - return std::unexpected(std::move(TETHERKIT_UNIQUE(tk_status_)).error()); \ - } \ +/// Usage: `TETHERNET_RETURN_IF_ERROR(link.Configure(mtu));` +#define TETHERNET_RETURN_IF_ERROR(expr) \ + do { \ + auto TETHERNET_UNIQUE(tn_status_) = (expr); \ + if (!TETHERNET_UNIQUE(tn_status_)) { \ + return std::unexpected(std::move(TETHERNET_UNIQUE(tn_status_)).error()); \ + } \ } while (false) /// Assign the success value to a new variable, or propagate the error upward on failure. /// -/// Usage: `TETHERKIT_ASSIGN_OR_RETURN(auto fd, OpenBpfDevice());` +/// Usage: `TETHERNET_ASSIGN_OR_RETURN(auto fd, OpenBpfDevice());` /// /// `decl` is a declaration rather than an expression and cannot be parenthesized, so the /// macro-parentheses check is suppressed here. // NOLINTNEXTLINE(bugprone-macro-parentheses) -#define TETHERKIT_ASSIGN_OR_RETURN(decl, expr) \ - auto TETHERKIT_UNIQUE(tk_result_) = (expr); \ - if (!TETHERKIT_UNIQUE(tk_result_)) { \ - return std::unexpected(std::move(TETHERKIT_UNIQUE(tk_result_)).error()); \ - } \ - decl = std::move(TETHERKIT_UNIQUE(tk_result_)).value() - -} // namespace tetherkit +#define TETHERNET_ASSIGN_OR_RETURN(decl, expr) \ + auto TETHERNET_UNIQUE(tn_result_) = (expr); \ + if (!TETHERNET_UNIQUE(tn_result_)) { \ + return std::unexpected(std::move(TETHERNET_UNIQUE(tn_result_)).error()); \ + } \ + decl = std::move(TETHERNET_UNIQUE(tn_result_)).value() + +} // namespace tethernet diff --git a/include/tetherkit/common/frame_ring.h b/include/tethernet/common/frame_ring.h similarity index 98% rename from include/tetherkit/common/frame_ring.h rename to include/tethernet/common/frame_ring.h index a9d75e6..c012026 100644 --- a/include/tetherkit/common/frame_ring.h +++ b/include/tethernet/common/frame_ring.h @@ -27,6 +27,9 @@ // two and is slower. #pragma once +#include "tethernet/common/cache.h" +#include "tethernet/common/spsc_ring.h" + #include #include #include @@ -34,10 +37,7 @@ #include #include -#include "tetherkit/common/cache.h" -#include "tetherkit/common/spsc_ring.h" - -namespace tetherkit { +namespace tethernet { /// Maximum bytes of an Ethernet frame (excluding FCS). /// @@ -398,8 +398,8 @@ class FrameRing { // respectively and **must** live on different cache lines -- otherwise every frame's // BeginWrite/BeginRead would invalidate each other, and all the cache-line isolation // done for SpscCursor would be wasted. - TETHERKIT_CACHE_ALIGNED std::size_t pending_write_index_ = 0; - TETHERKIT_CACHE_ALIGNED std::size_t pending_read_index_ = 0; + TETHERNET_CACHE_ALIGNED std::size_t pending_write_index_ = 0; + TETHERNET_CACHE_ALIGNED std::size_t pending_read_index_ = 0; }; -} // namespace tetherkit +} // namespace tethernet diff --git a/include/tetherkit/common/i18n.h b/include/tethernet/common/i18n.h similarity index 95% rename from include/tetherkit/common/i18n.h rename to include/tethernet/common/i18n.h index f3373c1..62ebca9 100644 --- a/include/tetherkit/common/i18n.h +++ b/include/tethernet/common/i18n.h @@ -13,7 +13,7 @@ // // All strings live in messages.def, one row each: // -// TETHERKIT_MESSAGE(kCliUnknownOption, "unknown option \"{}\"") +// TETHERNET_MESSAGE(kCliUnknownOption, "unknown option \"{}\"") // // The file is expanded by X-macros, producing the `Msg` enum (here) and the // message table (in i18n.cc) from the same source, so a missing entry is @@ -24,7 +24,7 @@ // // * Plain strings: Tr(Msg::kCliUnknownOption, argument) -- the arguments are // exactly std::format's; the result is a std::string. -// * Logging: the TETHERKIT_INFO_TR family (logging.h) keeps the +// * Logging: the TETHERNET_INFO_TR family (logging.h) keeps the // do-not-evaluate-when-disabled laziness. // * Errors: Error::Generic(Tr(Msg::kFoo, x)). // @@ -45,7 +45,7 @@ #include #include -namespace tetherkit { +namespace tethernet { /// The interface languages the library can serve. /// @@ -61,9 +61,9 @@ enum class Language : std::uint8_t { /// Message identifiers. Values are generated from messages.def. enum class Msg : std::uint16_t { -#define TETHERKIT_MESSAGE(id, en) id, -#include "tetherkit/common/messages.def" // NOLINT(bugprone-suspicious-include) -#undef TETHERKIT_MESSAGE +#define TETHERNET_MESSAGE(id, en) id, +#include "tethernet/common/messages.def" // NOLINT(bugprone-suspicious-include) +#undef TETHERNET_MESSAGE /// Sentinel: the number of messages. For table-length assertions and test /// loops only, not a real message. kMessageCount, @@ -82,7 +82,7 @@ void SetLanguage(Language language) noexcept; [[nodiscard]] Language GetLanguage() noexcept; -/// Infers the language from environment variables, checking `TETHERKIT_LANG`, +/// Infers the language from environment variables, checking `TETHERNET_LANG`, /// `LC_ALL`, `LC_MESSAGES`, and `LANG` in that order. /// /// The first non-empty value wins (POSIX's LC_ALL > LC_MESSAGES > LANG order), @@ -154,4 +154,4 @@ template return detail::FormatMessage(id, std::make_format_args(args...)); } -} // namespace tetherkit +} // namespace tethernet diff --git a/include/tetherkit/common/logging.h b/include/tethernet/common/logging.h similarity index 77% rename from include/tetherkit/common/logging.h rename to include/tethernet/common/logging.h index a071ead..ddc7f9c 100644 --- a/include/tetherkit/common/logging.h +++ b/include/tethernet/common/logging.h @@ -3,7 +3,7 @@ // Design constraints: // 1. **Must not appear on data hot paths.** Trace/Debug levels are removed entirely by // the preprocessor in Release builds (even argument evaluation does not happen), so -// writing TETHERKIT_TRACE on a hot path is safe; but Info and above really do +// writing TETHERNET_TRACE on a hot path is safe; but Info and above really do // formatting and a locked stderr write, so hot paths must not use them. // 2. Thread-safe: concurrent writes to stderr from multiple threads would interleave, // so each full line is written under a mutex. Logging is not on hot paths, so lock @@ -12,14 +12,14 @@ // is all that is needed, and std::format is enough. #pragma once +#include "tethernet/common/i18n.h" + #include #include #include #include -#include "tetherkit/common/i18n.h" - -namespace tetherkit { +namespace tethernet { enum class LogLevel : std::uint8_t { kTrace = 0, ///< Per-frame details, removed at compile time in Release builds. @@ -36,13 +36,13 @@ enum class LogLevel : std::uint8_t { /// Compile-time log floor. Log calls below this level are removed entirely by the /// preprocessor. /// -/// In Release builds (NDEBUG), Trace/Debug are removed so that TETHERKIT_TRACE on hot +/// In Release builds (NDEBUG), Trace/Debug are removed so that TETHERNET_TRACE on hot /// paths does not even evaluate its arguments. -#ifndef TETHERKIT_COMPILED_MIN_LOG_LEVEL +#ifndef TETHERNET_COMPILED_MIN_LOG_LEVEL #ifdef NDEBUG -#define TETHERKIT_COMPILED_MIN_LOG_LEVEL 2 // kInfo +#define TETHERNET_COMPILED_MIN_LOG_LEVEL 2 // kInfo #else -#define TETHERKIT_COMPILED_MIN_LOG_LEVEL 0 // kTrace +#define TETHERNET_COMPILED_MIN_LOG_LEVEL 0 // kTrace #endif #endif @@ -111,42 +111,42 @@ void LogFormatted(LogLevel level, std::string_view file, unsigned line, /// Logging macros. Do compile-time level trimming first, then the runtime level check /// (branch-prediction friendly). -#define TETHERKIT_LOG(level, ...) \ - do { \ - if constexpr (static_cast(level) >= TETHERKIT_COMPILED_MIN_LOG_LEVEL) { \ - if (::tetherkit::detail::IsLogLevelEnabled(level)) [[unlikely]] { \ - ::tetherkit::detail::LogFormatted((level), ::tetherkit::detail::BaseName(__FILE__), \ +#define TETHERNET_LOG(level, ...) \ + do { \ + if constexpr (static_cast(level) >= TETHERNET_COMPILED_MIN_LOG_LEVEL) { \ + if (::tethernet::detail::IsLogLevelEnabled(level)) [[unlikely]] { \ + ::tethernet::detail::LogFormatted((level), ::tethernet::detail::BaseName(__FILE__), \ __LINE__, __VA_ARGS__); \ } \ } \ } while (false) -#define TETHERKIT_TRACE(...) TETHERKIT_LOG(::tetherkit::LogLevel::kTrace, __VA_ARGS__) -#define TETHERKIT_DEBUG(...) TETHERKIT_LOG(::tetherkit::LogLevel::kDebug, __VA_ARGS__) -#define TETHERKIT_INFO(...) TETHERKIT_LOG(::tetherkit::LogLevel::kInfo, __VA_ARGS__) -#define TETHERKIT_WARN(...) TETHERKIT_LOG(::tetherkit::LogLevel::kWarn, __VA_ARGS__) -#define TETHERKIT_ERROR(...) TETHERKIT_LOG(::tetherkit::LogLevel::kError, __VA_ARGS__) +#define TETHERNET_TRACE(...) TETHERNET_LOG(::tethernet::LogLevel::kTrace, __VA_ARGS__) +#define TETHERNET_DEBUG(...) TETHERNET_LOG(::tethernet::LogLevel::kDebug, __VA_ARGS__) +#define TETHERNET_INFO(...) TETHERNET_LOG(::tethernet::LogLevel::kInfo, __VA_ARGS__) +#define TETHERNET_WARN(...) TETHERNET_LOG(::tethernet::LogLevel::kWarn, __VA_ARGS__) +#define TETHERNET_ERROR(...) TETHERNET_LOG(::tethernet::LogLevel::kError, __VA_ARGS__) /// Log a **translatable** message. `id` is a `Msg` enum value; the remaining arguments /// match std::format. /// -/// Why this family exists instead of writing `TETHERKIT_INFO("{}", Tr(id, ...))` +/// Why this family exists instead of writing `TETHERNET_INFO("{}", Tr(id, ...))` /// directly: as macros, Tr() stays **inside** the level check, so when the level is /// disabled both the table lookup and formatting are skipped -- the handwritten form /// above makes it easy to accidentally put Tr() outside the macro and pay for it on /// every log line. -#define TETHERKIT_LOG_TR(level, id, ...) \ - TETHERKIT_LOG(level, "{}", ::tetherkit::Tr((id)__VA_OPT__(, ) __VA_ARGS__)) - -#define TETHERKIT_TRACE_TR(id, ...) \ - TETHERKIT_LOG_TR(::tetherkit::LogLevel::kTrace, id __VA_OPT__(, ) __VA_ARGS__) -#define TETHERKIT_DEBUG_TR(id, ...) \ - TETHERKIT_LOG_TR(::tetherkit::LogLevel::kDebug, id __VA_OPT__(, ) __VA_ARGS__) -#define TETHERKIT_INFO_TR(id, ...) \ - TETHERKIT_LOG_TR(::tetherkit::LogLevel::kInfo, id __VA_OPT__(, ) __VA_ARGS__) -#define TETHERKIT_WARN_TR(id, ...) \ - TETHERKIT_LOG_TR(::tetherkit::LogLevel::kWarn, id __VA_OPT__(, ) __VA_ARGS__) -#define TETHERKIT_ERROR_TR(id, ...) \ - TETHERKIT_LOG_TR(::tetherkit::LogLevel::kError, id __VA_OPT__(, ) __VA_ARGS__) - -} // namespace tetherkit +#define TETHERNET_LOG_TR(level, id, ...) \ + TETHERNET_LOG(level, "{}", ::tethernet::Tr((id)__VA_OPT__(, ) __VA_ARGS__)) + +#define TETHERNET_TRACE_TR(id, ...) \ + TETHERNET_LOG_TR(::tethernet::LogLevel::kTrace, id __VA_OPT__(, ) __VA_ARGS__) +#define TETHERNET_DEBUG_TR(id, ...) \ + TETHERNET_LOG_TR(::tethernet::LogLevel::kDebug, id __VA_OPT__(, ) __VA_ARGS__) +#define TETHERNET_INFO_TR(id, ...) \ + TETHERNET_LOG_TR(::tethernet::LogLevel::kInfo, id __VA_OPT__(, ) __VA_ARGS__) +#define TETHERNET_WARN_TR(id, ...) \ + TETHERNET_LOG_TR(::tethernet::LogLevel::kWarn, id __VA_OPT__(, ) __VA_ARGS__) +#define TETHERNET_ERROR_TR(id, ...) \ + TETHERNET_LOG_TR(::tethernet::LogLevel::kError, id __VA_OPT__(, ) __VA_ARGS__) + +} // namespace tethernet diff --git a/include/tethernet/common/messages.def b/include/tethernet/common/messages.def new file mode 100644 index 0000000..5bbdba3 --- /dev/null +++ b/include/tethernet/common/messages.def @@ -0,0 +1,548 @@ +// All user-facing strings. This file deliberately has no include guard: it is an +// X-macro list, expanded into the Msg enum (by tethernet/common/i18n.h) and the +// message table (by src/common/i18n.cc). +// +// Each row takes exactly two arguments: +// +// TETHERNET_MESSAGE(identifier, English) +// +// Rules for adding a message: +// +// 1. Prefix the identifier with its module: kCommon* / kRndis* / kNet* / kUsb* / +// kCore* / kCapi* / kCli*; CamelCase for the rest; do not paraphrase the prose. +// 2. Placeholders: implicit {} are numbered in order of appearance (0, 1, 2, ...); +// implicit and explicit numbering must not be mixed within one string +// (std::format's rule). Reorder with explicit indices when needed, e.g. +// "write {1} bytes to {0}". +// 3. Insert new entries into their module's section, roughly in call order. +// +// Rule: every message's placeholders must parse cleanly and use contiguous +// indices starting at 0. Enforced entry by entry by tests/test_common_i18n.cc; +// a bad row turns `ctest -R common.i18n` red on the spot. +// +// How to add a language (the wiring deliberately stays in place with one +// language, so this is a bounded five-touch change, not a re-architecture): +// +// 1. Add a value (kSpanish = 1, say) to enum class Language in +// tethernet/common/i18n.h, mirror it as TN_LANGUAGE_SPANISH in +// tn_language_t (include/tethernet/capi/tethernet_c.h), and mirror it in +// the Swift `Language` enum (gui/Sources/TethernetIPC/Localization.swift). +// 2. Give every row of this file one more argument: the macro signature +// becomes (id, en, es) at the three extraction sites (the Msg enum in +// i18n.h, the table(s) in src/common/i18n.cc). +// 3. Emit one more table in src/common/i18n.cc next to kMessageTable and let +// TextIn() select it by Language. +// 4. Accept the new spelling and locale prefix in ParseLanguage() and +// LanguageFromLocaleString(). +// 5. Add the GUI menu entry and the per-key column in +// gui/Sources/TethernetIPC/LocalizedStrings.swift, then extend the checks +// in tests/test_common_i18n.cc and the GUI LocalizationTests so that every +// language covers every message. + +// ============================================================================= +// common -- infrastructure layer +// ============================================================================= + +TETHERNET_MESSAGE(kCommonLogFormatFailed, "") +TETHERNET_MESSAGE(kCommonThreadQosFailed, "failed to set thread QoS (qos={}, rc={}); falling back to the default " + "scheduling policy") + +// ============================================================================= +// net -- the feth virtual interface and the BPF link +// ============================================================================= + +TETHERNET_MESSAGE(kNetLoopbackInjectedFailure, "loopback backend failed on write #{} as configured by the test") + +// --- shared (used by both feth and BPF) --- +TETHERNET_MESSAGE(kNetInterfaceNameTooLong, "interface name \"{}\" is too long") +TETHERNET_MESSAGE(kNetInterfaceNameOverLimit, "interface name \"{}\" exceeds the {}-byte limit") +TETHERNET_MESSAGE(kNetSysctlReadFailed, "failed to read sysctl {}") +TETHERNET_MESSAGE(kNetUnknownValue, "unknown") + +// --- BPF link --- +TETHERNET_MESSAGE(kNetOptionalIoctlUnavailable, "optional ioctl {} is unavailable (errno={}); skipped") +TETHERNET_MESSAGE(kNetFrameLimitBelowMinimum, "max_frame_bytes={} is smaller than the minimum Ethernet frame length {}") +TETHERNET_MESSAGE(kNetBpfOpenDenied, "permission denied opening {}. /dev/bpf* is 0600 root:wheel and macOS has no " + "equivalent of FreeBSD's access_bpf group, so this must run as root") +TETHERNET_MESSAGE(kNetBpfNoDeviceAvailable, "no usable /dev/bpf* device found (all busy, or insufficient permissions)") +TETHERNET_MESSAGE(kNetBpfBufferClamped, "the kernel clamped the BPF buffer from the requested {} bytes to {} bytes " + "(limited by sysctl debug.bpf_bufsize_cap)") +TETHERNET_MESSAGE(kNetBpfInterfaceMissing, "interface {} does not exist; cannot attach BPF") +TETHERNET_MESSAGE(kNetBpfBindFailed, "failed to attach BPF to {}") +TETHERNET_MESSAGE(kNetBpfWrongDataLinkType, "interface {} has data link type {}; this driver only supports Ethernet " + "(DLT_EN10MB={})") +TETHERNET_MESSAGE(kNetBpfReady, "BPF ready: {} -> {} (kernel buffer {} KiB, max frame {} bytes, batch write {}, " + "timestamps {})") +TETHERNET_MESSAGE(kNetBpfBatchWriteEnabled, "enabled") +TETHERNET_MESSAGE(kNetBpfBatchWriteUnavailable, "unavailable (writing one frame at a time)") +TETHERNET_MESSAGE(kNetBpfTimestampDisabled, "off") +TETHERNET_MESSAGE(kNetBpfTimestampEnabled, "left on") +TETHERNET_MESSAGE(kNetBpfReadFailed, "failed to read from {}") +TETHERNET_MESSAGE(kNetBpfHeaderTooShort, "BPF record has bh_hdrlen={}, below the minimum of {}; discarding the rest of " + "this read") +TETHERNET_MESSAGE(kNetBpfRecordOutOfBounds, "BPF record runs past the buffer (offset {} + {} > {}); discarding the rest") +TETHERNET_MESSAGE(kNetBpfSkipOversizedRecord, "skipping BPF record: caplen={} datalen={} (limit {})") +TETHERNET_MESSAGE(kNetBpfBatchFrameLimit, "hit the per-batch frame limit of {}; the remaining {} bytes of this read were " + "dropped. Increase max_frames_per_batch") +TETHERNET_MESSAGE(kNetBpfWriteFailed, "failed to write a {1}-byte frame to {0}") +TETHERNET_MESSAGE(kNetBpfBatchWriteFailed, "failed to batch-write {1} frames ({2} bytes) to {0}") + +// --- feth virtual interface --- +TETHERNET_MESSAGE(kNetIoctlSocketFailed, "failed to create the socket used for interface ioctls") +TETHERNET_MESSAGE(kNetFethMaxMtuInvalid, "net.link.fake.max_mtu returned the invalid value {}") +TETHERNET_MESSAGE(kNetSysctlUnreadableSkipped, "sysctl {} is unreadable ({}); skipping the check") +TETHERNET_MESSAGE(kNetSysctlMismatch, "sysctl {0} is currently {1} but must be {2}. Reason: {3}.\n" + " feth snapshots this switch into the interface at creation time, so changing " + "it afterwards has no effect. Fix it first:\n" + " sudo sysctl -w {0}={2}") +TETHERNET_MESSAGE(kNetSysctlWhyHwcsum, "when non-zero, IP/TCP/UDP checksums on outbound frames are placeholders left " + "for hardware to compute, so the frames we read have invalid checksums") +TETHERNET_MESSAGE(kNetSysctlWhyFcs, "when non-zero, a 4-byte CRC is appended to each frame and would have to be " + "stripped before forwarding over USB") +TETHERNET_MESSAGE(kNetSysctlWhyTso, "when non-zero, TSO super-packets of up to 64 KB arrive, exceeding the RNDIS " + "per-frame limit") +TETHERNET_MESSAGE(kNetSysctlWhyLro, "when non-zero, coalesced oversized receive frames arrive") +TETHERNET_MESSAGE(kNetSysctlWhyTrailer, "when non-zero, frames carry extra trailing padding") +TETHERNET_MESSAGE(kNetSysctlWhySeparateHeader, "when non-zero, the frame header is separated from the payload, breaking the " + "contiguous-memory assumption") +TETHERNET_MESSAGE(kNetDestroyOrphanFailed, "failed to destroy leftover virtual interface {}") +TETHERNET_MESSAGE(kNetDestroyedOrphan, "destroyed leftover virtual interface {}") +TETHERNET_MESSAGE(kNetDestroyFailed, "failed to destroy {}: {}") +TETHERNET_MESSAGE(kNetDestroyed, "destroyed virtual interface {}") +TETHERNET_MESSAGE(kNetFethCreateNeedsRoot, "creating a feth virtual interface requires root; run with sudo") +TETHERNET_MESSAGE(kNetInterfaceExists, "interface {} already exists; pick another name or destroy it first") +TETHERNET_MESSAGE(kNetFethCreateFailed, "failed to create feth virtual interface {}") +TETHERNET_MESSAGE(kNetCreated, "created virtual interface {}") +TETHERNET_MESSAGE(kNetPeerInvalidObject, "pairing failed: invalid interface object") +TETHERNET_MESSAGE(kNetPeerNameTooLong, "peer interface name \"{}\" is too long") +TETHERNET_MESSAGE(kNetPeerNeedsRoot, "pairing feth interfaces requires root; run with sudo") +TETHERNET_MESSAGE(kNetPeerRejected, "the kernel refused to pair {} with {}. Likely causes: one side already has a " + "peer, the peer is not a feth interface, or the private if_fake ABI changed in " + "this macOS version") +TETHERNET_MESSAGE(kNetPeerFailed, "failed to pair {} with {}") +TETHERNET_MESSAGE(kNetPeered, "paired {} with {}") +TETHERNET_MESSAGE(kNetUnpeerInvalidObject, "unpairing failed: invalid interface object") +TETHERNET_MESSAGE(kNetUnpeerIoctlWhat, "ioctl(SIOCSDRVSPEC, IF_FAKE_S_CMD_SET_PEER) to unpair") +TETHERNET_MESSAGE(kNetQueryPeerInvalidObject, "peer query failed: invalid interface object") +TETHERNET_MESSAGE(kNetSetMtuInvalidObject, "setting the MTU failed: invalid interface object") +TETHERNET_MESSAGE(kNetSetMtuRejected, "refused to set the MTU of {} to {} (the feth MTU ceiling is {}, set by sysctl " + "net.link.fake.max_mtu and snapshotted when the interface was created)") +TETHERNET_MESSAGE(kNetSetMtuFailed, "failed to set the MTU of {} to {}") +TETHERNET_MESSAGE(kNetSetMacInvalidObject, "setting the MAC failed: invalid interface object") +TETHERNET_MESSAGE(kNetSetMacNeedsRoot, "changing an interface MAC requires root; run with sudo") +TETHERNET_MESSAGE(kNetSetMacFailed, "failed to set the MAC of {} to {}") +TETHERNET_MESSAGE(kNetMacSet, "set the MAC of {} to {}") +TETHERNET_MESSAGE(kNetGetifaddrsFailed, "getifaddrs failed") +TETHERNET_MESSAGE(kNetLinkAddressWrongLength, "the link address of {} is {} bytes, not a 6-byte MAC") +TETHERNET_MESSAGE(kNetLinkAddressNotFound, "no link address for interface {} in the getifaddrs results") +TETHERNET_MESSAGE(kNetSetUpInvalidObject, "changing the interface state failed: invalid interface object") +TETHERNET_MESSAGE(kNetSetUpNeedsRoot, "bringing an interface up or down requires root; run with sudo") +TETHERNET_MESSAGE(kNetSetUpFailed, "failed to bring {} {}") +TETHERNET_MESSAGE(kNetFethPairNeedsRoot, "creating feth virtual interfaces requires root. Run Tethernet with sudo.") +TETHERNET_MESSAGE(kNetMtuExceedsFethLimit, "the requested MTU {} exceeds the feth ceiling of {} (sysctl " + "net.link.fake.max_mtu)") +TETHERNET_MESSAGE(kNetSameMacDadConflict, "both sides have the same MAC ({}), which would trigger an IPv6 DAD address " + "conflict") +TETHERNET_MESSAGE(kNetFethPairReady, "virtual interface pair ready: system side {} <-> driver side {} (MTU {})") + +// ============================================================================= +// rndis -- protocol codec and state machine +// ============================================================================= + +// --- generic placeholders --- +TETHERNET_MESSAGE(kRndisUnknownStatus, "unknown status") +TETHERNET_MESSAGE(kRndisUnknownMessage, "unknown message") +TETHERNET_MESSAGE(kRndisUnknownMessageType, "unknown message type") +TETHERNET_MESSAGE(kRndisUnknown, "unknown") +TETHERNET_MESSAGE(kRndisResponse, "a response") + +// --- state names --- +TETHERNET_MESSAGE(kRndisStateUninitialized, "uninitialized") +TETHERNET_MESSAGE(kRndisStateInitializing, "initializing") +TETHERNET_MESSAGE(kRndisStateInitialized, "initialized") +TETHERNET_MESSAGE(kRndisStateDataInitialized, "data ready") +TETHERNET_MESSAGE(kRndisStateHalting, "halting") +TETHERNET_MESSAGE(kRndisStateUnknown, "unknown state") + +// --- data packet decode rejection reasons --- +TETHERNET_MESSAGE(kRndisMalformedNone, "none") +TETHERNET_MESSAGE(kRndisMalformedNotPacketMessage, "MessageType is not PACKET_MSG") +TETHERNET_MESSAGE(kRndisMalformedMessageTooShort, "MessageLength is below 44") +TETHERNET_MESSAGE(kRndisMalformedMessageOverruns, "MessageLength runs past the transfer") +TETHERNET_MESSAGE(kRndisMalformedDataOutOfBounds, "DataOffset+DataLength is out of bounds") +TETHERNET_MESSAGE(kRndisMalformedFrameTooShort, "DataLength is below the Ethernet header length") +TETHERNET_MESSAGE(kRndisMalformedFrameTooLong, "DataLength exceeds the per-frame limit") +TETHERNET_MESSAGE(kRndisMalformedUnknown, "unknown") + +// --- control message codec --- +TETHERNET_MESSAGE(kRndisBufferTooShort, "{} is too short: need {} bytes, got {}") +TETHERNET_MESSAGE(kRndisEncodeBufferTooSmall, "the buffer for encoding {} is too small: need {} bytes, got {}") +TETHERNET_MESSAGE(kRndisUnexpectedMessageType, "expected {}, got {} ({:#010x})") +TETHERNET_MESSAGE(kRndisInconsistentMessageLength, "MessageLength={1} of {0} is inconsistent (buffer is {2} bytes, minimum is {3})") +TETHERNET_MESSAGE(kRndisInlineBufferOutOfBounds, "{} is out of bounds: offset {} (relative {}) + length {} exceeds MessageLength " + "{}") +TETHERNET_MESSAGE(kRndisWhatMessageHeader, "the RNDIS message header") +TETHERNET_MESSAGE(kRndisWhatQueryCmpltInfoBuffer, "the InformationBuffer of QUERY_CMPLT") +TETHERNET_MESSAGE(kRndisWhatOidUint32, "the LE32 return value of an OID") +TETHERNET_MESSAGE(kRndisWhatOidMac, "the MAC return value of an OID") +TETHERNET_MESSAGE(kRndisStatusSuffix, "{}: {}") +TETHERNET_MESSAGE(kRndisInitRejected, "the device rejected RNDIS initialization") +TETHERNET_MESSAGE(kRndisUnsupportedMedium, "the device reports medium {:#x}; this driver only supports 802.3 Ethernet (0)") +TETHERNET_MESSAGE(kRndisConnectionOriented, "the device is connection-oriented (CONDIS); this driver only supports " + "connectionless 802.3 devices") +TETHERNET_MESSAGE(kRndisNoConnectionlessFlag, "the device has DeviceFlags={:#x} without the CONNECTIONLESS bit; continuing as " + "if it were connectionless") +TETHERNET_MESSAGE(kRndisUnsupportedMajorVersion, "the device reports RNDIS major version {}; this driver only implements {}") +TETHERNET_MESSAGE(kRndisAlignmentFactorClamped, "the device reports PacketAlignmentFactor={}, above the legal ceiling of {}; " + "clamped") +TETHERNET_MESSAGE(kRndisMaxTransferTooSmall, "the device reports MaxTransferSize={}, too small for the {}-byte RNDIS + " + "Ethernet headers") +TETHERNET_MESSAGE(kRndisMaxTransferClampedToHost, "the device reports MaxTransferSize={}, above the host buffer ceiling of {}; " + "using the host ceiling") +TETHERNET_MESSAGE(kRndisMtuLoweredForTransferSize, "the device's MaxTransferSize={} is below the {} needed for a full frame; " + "lowering the MTU from {} to {}") +TETHERNET_MESSAGE(kRndisExpectedIndicateStatus, "expected REMOTE_NDIS_INDICATE_STATUS_MSG, got {:#010x}") +TETHERNET_MESSAGE(kRndisOidCounterBadLength, "the OID counter returned {} bytes, which is neither 4 nor 8") +TETHERNET_MESSAGE(kRndisIndicateStatusBufferOutOfBounds, "the StatusBufferOffset={} / Length={} of INDICATE_STATUS is out of bounds under " + "both base-point interpretations (MessageLength={}); ignoring the status buffer") + +// --- state machine --- +TETHERNET_MESSAGE(kRndisStateTransition, "RNDIS state: {} -> {}") +TETHERNET_MESSAGE(kRndisControlMessageReceived, "control channel received {} ({:#010x}), {} bytes") +TETHERNET_MESSAGE(kRndisCondisMessageReceived, "the device sent the connection-oriented (CONDIS) message {}; this driver only " + "supports connectionless 802.3 devices") +TETHERNET_MESSAGE(kRndisDiscardUnexpectedMessage, "discarding an unexpected control message {} ({:#010x})") +TETHERNET_MESSAGE(kRndisNotificationReceived, "received a RESPONSE_AVAILABLE notification") +TETHERNET_MESSAGE(kRndisWaitReplyTimeout, "timed out waiting for {} ({} polls x {} ms)") +TETHERNET_MESSAGE(kRndisIndicateStatusParseFailed, "failed to parse INDICATE_STATUS: {}") +TETHERNET_MESSAGE(kRndisDeviceIndicatedWithDiagnostics, "the device reported {} ({:#010x}); diagnostics: DiagStatus={:#010x}, error " + "offset={}") +TETHERNET_MESSAGE(kRndisDeviceIndicated, "the device reported {} ({:#010x})") +TETHERNET_MESSAGE(kRndisKeepAliveMsgTooShort, "the KEEPALIVE_MSG from the device is shorter than 12 bytes") +TETHERNET_MESSAGE(kRndisDeviceKeepAlive, "the device initiated a keep-alive (RequestId={}); replying with KEEPALIVE_CMPLT") +TETHERNET_MESSAGE(kRndisOptionalOidQueryFailed, "failed to query optional OID {} (not fatal): {}") +TETHERNET_MESSAGE(kRndisOidQueryFailed, "failed to query OID {}") +TETHERNET_MESSAGE(kRndisOptionalOidUnsupported, "the device does not support optional OID {} ({})") +TETHERNET_MESSAGE(kRndisOidQueryRejected, "query of OID {} was rejected: {}") +TETHERNET_MESSAGE(kRndisOidSetRejected, "setting OID {} = {:#x} was rejected: {}") +TETHERNET_MESSAGE(kRndisOidSet, "set OID {} = {:#x}") +TETHERNET_MESSAGE(kRndisPermanentMac, "device permanent MAC: {}") +TETHERNET_MESSAGE(kRndisMtuLoweredToDeviceFrameSize, "the device reports a maximum frame size of {}, below the negotiated MTU of {}; " + "lowering the MTU") +TETHERNET_MESSAGE(kRndisDeviceInfo, "device info: MAC {}, link {:.0f} Mbps, max frame {}, medium {}, vendor {:#x} " + "\"{}\"") +TETHERNET_MESSAGE(kRndisStartWrongState, "Start() may only be called from the uninitialized state; the current state is " + "\"{}\"") +TETHERNET_MESSAGE(kRndisInitFailed, "RNDIS initialization failed") +TETHERNET_MESSAGE(kRndisMaxPacketsClamped, "the device reports MaxPacketsPerMessage={}; clamping to {} as the caller " + "requested") +TETHERNET_MESSAGE(kRndisNegotiated, "RNDIS negotiation complete: version {}.{}, MTU {}, device aggregation limit {} " + "bytes / {} packets, TX alignment {} bytes") +TETHERNET_MESSAGE(kRndisPacketFilterFailed, "failed to set the packet filter; the data channel cannot be opened") +TETHERNET_MESSAGE(kRndisDrainControlFailed, "failed to drain unsolicited control-channel messages") +TETHERNET_MESSAGE(kRndisKeepAliveFailed, "keep-alive failed ({} consecutive of {}): {}") +TETHERNET_MESSAGE(kRndisKeepAliveDeadLink, "{} consecutive keep-alive failures; treating the link as dead") +TETHERNET_MESSAGE(kRndisKeepAliveRejected, "the device's KEEPALIVE_CMPLT returned {}; attempting a soft reset") +TETHERNET_MESSAGE(kRndisKeepAliveOk, "keep-alive OK") +TETHERNET_MESSAGE(kRndisResetStarted, "starting an RNDIS soft reset") +TETHERNET_MESSAGE(kRndisResetRejected, "the soft reset was rejected: {}") +TETHERNET_MESSAGE(kRndisResetDone, "soft reset complete; addressing information {}") +TETHERNET_MESSAGE(kRndisAddressingLost, "was lost and must be replayed") +TETHERNET_MESSAGE(kRndisAddressingKept, "is still valid") +TETHERNET_MESSAGE(kRndisPacketFilterReplayed, "replayed the packet filter setting {:#x}") +TETHERNET_MESSAGE(kRndisEncodeHaltFailed, "failed to encode HALT_MSG: {}") +TETHERNET_MESSAGE(kRndisSendHaltFailed, "failed to send HALT_MSG (the device may already be disconnected): {}") +TETHERNET_MESSAGE(kRndisHaltSent, "sent HALT_MSG") +TETHERNET_MESSAGE(kRndisClearPacketFilterFailed, "failed to clear the packet filter (sending HALT anyway): {}") + +// ============================================================================= +// usb -- context, device discovery, and transfer channels +// ============================================================================= + +// --- context --- +TETHERNET_MESSAGE(kUsbInitFailed, "libusb_init failed") +TETHERNET_MESSAGE(kUsbInitialized, "libusb initialized: {} (hotplug {})") +TETHERNET_MESSAGE(kUsbHotplugSupported, "supported") +TETHERNET_MESSAGE(kUsbHotplugUnsupported, "unsupported") +TETHERNET_MESSAGE(kUsbContextReleased, "released the libusb context") +TETHERNET_MESSAGE(kUsbUnknownVersion, "unknown version") +TETHERNET_MESSAGE(kUsbEventThreadStarted, "libusb event thread started") +TETHERNET_MESSAGE(kUsbEventLoopNoDevice, "the libusb event loop got NO_DEVICE; continuing to wait") +TETHERNET_MESSAGE(kUsbHandleEventsFailed, "libusb_handle_events returned {} ({}); the event thread is exiting") +TETHERNET_MESSAGE(kUsbEventThreadExited, "libusb event thread exited") + +// --- device discovery and claiming --- +TETHERNET_MESSAGE(kUsbGetDeviceListFailed, "libusb_get_device_list failed") +TETHERNET_MESSAGE(kUsbSkippedAcmModem, "{:04x}:{:04x} interface {} looks like RNDIS by signature but carries non-zero " + "ACM bmCapabilities; treating it as a real cdc-acm modem and skipping it") +TETHERNET_MESSAGE(kUsbNoPairedDataInterface, "interface {1} of {0} is an RNDIS control interface but has no matching data " + "interface; skipping it") +TETHERNET_MESSAGE(kUsbDeviceFound, "found RNDIS device {}: control interface {}, data interface {}, signature " + "{:02x}/{:02x}/{:02x}{}") +TETHERNET_MESSAGE(kUsbViaAndroidQuirk, " (matched via the Android quirk fallback)") +TETHERNET_MESSAGE(kUsbDeviceGone, "device {} is no longer on the bus") +TETHERNET_MESSAGE(kUsbOpenFailed, "failed to open device {}") +TETHERNET_MESSAGE(kUsbClaimFailed, "failed to claim {} (interface {})") +TETHERNET_MESSAGE(kUsbClaimBusyHint, "the interface is held by a kernel driver. macOS has no RNDIS driver of its own, " + "so this usually means a third-party kext such as HoRNDIS is installed, or " + "another user-space program already claimed the interface") +TETHERNET_MESSAGE(kUsbClaimNotFoundHint, "the interface does not exist. The device may have switched configuration, or " + "the descriptors were parsed incorrectly") +TETHERNET_MESSAGE(kUsbControlInterface, "the RNDIS control interface") +TETHERNET_MESSAGE(kUsbDataInterface, "the RNDIS data interface") +TETHERNET_MESSAGE(kUsbReadActiveConfigFailed, "failed to read the active configuration descriptor") +TETHERNET_MESSAGE(kUsbClaimed, "claimed {} ({}): bulk IN 0x{:02x} / OUT 0x{:02x} (wMaxPacketSize {}), " + "interrupt IN {}") +TETHERNET_MESSAGE(kUsbNoInterruptEndpointShort, "none (falling back to polling the control endpoint)") +TETHERNET_MESSAGE(kUsbClosed, "closed device {}") +TETHERNET_MESSAGE(kUsbBulkEndpointsMissing, "no matching pair of bulk endpoints on data interface {} (IN=0x{:02x}, " + "OUT=0x{:02x})") +TETHERNET_MESSAGE(kUsbBulkMaxPacketSizeZero, "the bulk endpoint has wMaxPacketSize 0") +TETHERNET_MESSAGE(kUsbUnknownSpeed, "unknown speed") +TETHERNET_MESSAGE(kUsbClearHaltFailed, "failed to clear the halt condition on endpoint 0x{:02x}") + +// --- interrupt notification --- +TETHERNET_MESSAGE(kUsbNoInterruptEndpoint, "the device has no interrupt endpoint; skipping the notification listener and " + "falling back to polling the control endpoint") +TETHERNET_MESSAGE(kUsbNotificationAlreadyRunning, "the notification listener is already running") +TETHERNET_MESSAGE(kUsbAllocInterruptTransferFailed, "failed to allocate a libusb transfer for interrupt notifications") +TETHERNET_MESSAGE(kUsbSubmitInterruptFailed, "failed to submit the interrupt notification transfer") +TETHERNET_MESSAGE(kUsbNotificationStarted, "interrupt notification listener started (endpoint 0x{:02x})") +TETHERNET_MESSAGE(kUsbCancelInterruptReturned, "cancelling the interrupt notification transfer returned {}") +TETHERNET_MESSAGE(kUsbInterruptReclaimTimeout, "timed out waiting for the interrupt notification transfer to be reclaimed; " + "leaving it allocated to avoid a use-after-free") +TETHERNET_MESSAGE(kUsbUnknownNotification, "the interrupt endpoint received an unknown notification {:#010x}; ignoring it") +TETHERNET_MESSAGE(kUsbInterruptHaltClearFailed, "failed to clear the halt on the interrupt endpoint; falling back to polling the " + "control endpoint: {}") +TETHERNET_MESSAGE(kUsbInterruptTransferStatus, "interrupt notification transfer status={}; continuing to listen") + +// --- control channel --- +TETHERNET_MESSAGE(kUsbControlMessageLengthInvalid, "invalid control message length {}") +TETHERNET_MESSAGE(kUsbSendEncapsulatedFailed, "SEND_ENCAPSULATED_COMMAND failed") +TETHERNET_MESSAGE(kUsbSendEncapsulatedShort, "SEND_ENCAPSULATED_COMMAND only sent {} of {} bytes") +TETHERNET_MESSAGE(kUsbGetEncapsulatedFailed, "GET_ENCAPSULATED_RESPONSE failed") + +// --- data channel --- +TETHERNET_MESSAGE(kUsbTxBufferTooSmall, "the {}-byte TX transfer buffer cannot hold a full frame ({} bytes needed)") +TETHERNET_MESSAGE(kUsbTxSlotQueueInitFailed, "failed to initialize the TX free-slot queue") +TETHERNET_MESSAGE(kUsbDataChannelReady, "data channel ready: RX {} x {} KiB ({} KiB in flight), TX {} x {} KiB; device " + "aggregation limit {} bytes / {} packets, alignment {} bytes") +TETHERNET_MESSAGE(kUsbTransferAllocFailed, "failed to allocate {} bytes ({}-byte aligned) for a USB transfer") +TETHERNET_MESSAGE(kUsbAllocTransferFailed, "libusb_alloc_transfer failed") +TETHERNET_MESSAGE(kUsbBulkInSubmitted, "submitted {} bulk IN transfers") +TETHERNET_MESSAGE(kUsbSubmitBulkInFailed, "failed to submit a bulk IN transfer") +TETHERNET_MESSAGE(kUsbMalformedRndisMessage, "malformed RNDIS message in a bulk IN transfer: {} ({} frames decoded so far)") +TETHERNET_MESSAGE(kUsbBulkInStall, "bulk IN endpoint STALL; clearing the halt and continuing") +TETHERNET_MESSAGE(kUsbBulkInClearHaltFailed, "failed to clear the halt on bulk IN: {}") +TETHERNET_MESSAGE(kUsbBulkInDeviceGone, "a bulk IN transfer reported that the device is disconnected") +TETHERNET_MESSAGE(kUsbBulkInFailed, "bulk IN transfer failed: status={}; retrying") +TETHERNET_MESSAGE(kUsbBulkInResubmitFailed, "failed to resubmit a bulk IN transfer: {}") +TETHERNET_MESSAGE(kUsbDataChannelShuttingDown, "the data channel is shutting down; refusing to send") +TETHERNET_MESSAGE(kUsbReturnTxSlotFailed, "failed to return TX slot {} (this should not happen)") +TETHERNET_MESSAGE(kUsbSubmitBulkOutFailed, "failed to submit a bulk OUT transfer") +TETHERNET_MESSAGE(kUsbBulkOutSubmitted, "bulk OUT submitted {} frames / {} payload bytes / {} transfer bytes") +TETHERNET_MESSAGE(kUsbBulkOutStall, "bulk OUT endpoint STALL; clearing the halt") +TETHERNET_MESSAGE(kUsbBulkOutClearHaltFailed, "failed to clear the halt on bulk OUT: {}") +TETHERNET_MESSAGE(kUsbBulkOutFailed, "bulk OUT transfer failed: status={}") +TETHERNET_MESSAGE(kUsbCancelBulkInReturned, "cancelling a bulk IN transfer returned {}") +TETHERNET_MESSAGE(kUsbCancelBulkOutReturned, "cancelling a bulk OUT transfer returned {}") +TETHERNET_MESSAGE(kUsbTransferReclaimTimeout, "timed out waiting for {} in-flight USB transfers to be reclaimed. Their memory " + "is deliberately not freed to avoid a use-after-free (leaking about {} KiB). " + "This usually means the device is in a bad state, or the libusb event thread has " + "stopped.") +TETHERNET_MESSAGE(kUsbDataChannelStopped, "data channel stopped") + +// ============================================================================= +// core -- data-path bridge and runtime orchestration +// ============================================================================= + +// --- bridge --- +TETHERNET_MESSAGE(kCoreBridgeAlreadyRunning, "the bridge is already running") +TETHERNET_MESSAGE(kCoreDataPathStarted, "data path started: RX queue {} frames ({} KiB), RX write batch {} frames, TX " + "submit batch {} frames, link batch write {}") +TETHERNET_MESSAGE(kCoreBatchWriteAvailable, "available") +TETHERNET_MESSAGE(kCoreBatchWriteUnavailable, "unavailable (writing one frame at a time)") +TETHERNET_MESSAGE(kCoreDataPathStopped, "data path stopped") +TETHERNET_MESSAGE(kCoreRxInjectorStarted, "RX injector thread started") +TETHERNET_MESSAGE(kCoreRxInjectorExited, "RX injector thread exited") +TETHERNET_MESSAGE(kCoreLinkWriteFailed, "failed to write {} frames to the link: {}") +TETHERNET_MESSAGE(kCoreTxExtractorStarted, "TX extractor thread started") +TETHERNET_MESSAGE(kCoreTxExtractorExited, "TX extractor thread exited") +TETHERNET_MESSAGE(kCoreLinkReadFailed, "failed to read from the link: {}") +TETHERNET_MESSAGE(kCoreUsbSubmitFailed, "failed to submit {} frames to USB: {}") +TETHERNET_MESSAGE(kCoreStatsLine, "RX {:>8.0f} pps / {:>8.2f} Mbps (dropped {}) | " + "TX {:>8.0f} pps / {:>8.2f} Mbps (dropped {}) | " + "queue depth {} kernel drops {} backpressure {}") + +// --- runtime state names --- +TETHERNET_MESSAGE(kCoreRunStateIdle, "idle") +TETHERNET_MESSAGE(kCoreRunStateStarting, "starting") +TETHERNET_MESSAGE(kCoreRunStateRunning, "running") +TETHERNET_MESSAGE(kCoreRunStateStopping, "stopping") +TETHERNET_MESSAGE(kCoreRunStateStopped, "stopped") +TETHERNET_MESSAGE(kCoreRunStateFailed, "failed") +TETHERNET_MESSAGE(kCoreRunStateUnknown, "unknown") + +// --- runtime orchestration --- +TETHERNET_MESSAGE(kCoreRuntimeAlreadyStarted, "the runtime is already started") +TETHERNET_MESSAGE(kCoreNeedsRoot, "Tethernet needs root to create the feth virtual interface and open /dev/bpf*.\n" + " Run it with sudo: sudo tethernet-cli\n" + " (The USB side itself does not need root -- macOS has no RNDIS kernel driver, " + "so libusb can claim the interface directly.)") +TETHERNET_MESSAGE(kCoreNoDeviceFound, "No RNDIS device found. Check that:\n" + " 1. the device is connected with a USB data cable (not a charge-only one);\n" + " 2. \"USB tethering\" is enabled on the device;\n" + " 3. the phone is unlocked and trusts this Mac (some devices hide the RNDIS " + "interface while locked).\n" + " Run `system_profiler SPUSBDataType` to confirm the system sees the device.") +TETHERNET_MESSAGE(kCoreMultipleDevices, "found {} RNDIS devices; using the first one (use VID/PID to pick another)") +TETHERNET_MESSAGE(kCoreExitingOnFatal, "exiting because of an unrecoverable error") +TETHERNET_MESSAGE(kCoreInterfaceReady, "==== interface ready: {} ====") +TETHERNET_MESSAGE(kCoreNextStepsAssignIp, "Next, give it an IP address (RNDIS devices usually run their own DHCP server):") +TETHERNET_MESSAGE(kCoreNextStepsVerify, "Verify with:") +TETHERNET_MESSAGE(kCoreNextStepsDefaultRoute, "To route traffic through it by default (this replaces the current default " + "route -- be careful):") +TETHERNET_MESSAGE(kCoreNextStepsTemporaryNote1, "Note: ipconfig set creates a *temporary* service that only lives until the") +TETHERNET_MESSAGE(kCoreNextStepsTemporaryNote2, " next network configuration change, and never appears in System Settings > " + "Network.") +TETHERNET_MESSAGE(kCoreStopping, "stopping...") +TETHERNET_MESSAGE(kCoreStopped, "stopped") +TETHERNET_MESSAGE(kCoreRndisReady, "RNDIS ready: device MAC {}, MTU {}, link {:.0f} Mbps") +TETHERNET_MESSAGE(kCoreLinkState, "link state: {}") +TETHERNET_MESSAGE(kCoreLinkConnected, "connected") +TETHERNET_MESSAGE(kCoreLinkDisconnected, "disconnected") +TETHERNET_MESSAGE(kCoreDeviceReset, "the device performed a soft reset (addressing information {})") +TETHERNET_MESSAGE(kCoreAddressingLost, "was lost and has been replayed") +TETHERNET_MESSAGE(kCoreAddressingKept, "was preserved") +TETHERNET_MESSAGE(kCoreLinkUnrecoverable, "the link is unrecoverable") + +// ============================================================================= +// capi -- C ABI layer (interface configuration, sessions, orphan cleanup, subprocesses) +// ============================================================================= + +// --- subprocess --- +TETHERNET_MESSAGE(kCapiPipeFailed, "failed to create a pipe") +TETHERNET_MESSAGE(kCapiSpawnFileActionsFailed, "posix_spawn_file_actions_init failed") +TETHERNET_MESSAGE(kCapiExecFailed, "cannot execute {}") +TETHERNET_MESSAGE(kCapiWaitFailed, "failed to wait for {} to finish") + +// --- sessions --- +TETHERNET_MESSAGE(kCapiSessionConfigNull, "tn_session_create: config must not be null") +TETHERNET_MESSAGE(kCapiSessionOutOfMemory, "out of memory; cannot create the session") + +// --- orphaned interface cleanup --- +TETHERNET_MESSAGE(kCapiCleanupNeedsRoot, "cleaning up leftover virtual interfaces requires root") +TETHERNET_MESSAGE(kCapiOrphanAlreadyGone, "leftover interface {} is already gone, or cannot be destroyed") + +// --- interface IP configuration --- +TETHERNET_MESSAGE(kCapiInterfaceNameNull, "the interface name must not be empty") +TETHERNET_MESSAGE(kCapiInterfaceNotOurs, "refusing to touch interface \"{}\": only feth interfaces created by " + "Tethernet are accepted.\n" + " This guard exists so a mistake cannot wipe the configuration of a real " + "interface such as en0.") +TETHERNET_MESSAGE(kCapiCommandFailed, "{} failed (exit code {}): {}") +TETHERNET_MESSAGE(kCapiCommandNoOutput, "the command printed no explanation") +TETHERNET_MESSAGE(kCapiDnsPublishFailed, "failed to write the DNS key (service {}); DNS may not take effect in manual " + "mode") +TETHERNET_MESSAGE(kCapiWhatAddScopedRoute, "adding a scoped default route") +TETHERNET_MESSAGE(kCapiWhatUpdateScopedRoute, "updating the scoped default route") +TETHERNET_MESSAGE(kCapiWhatSwitchGlobalRoute, "switching the global default route") +TETHERNET_MESSAGE(kCapiWhatAddGlobalRoute, "adding a global default route") +TETHERNET_MESSAGE(kCapiWhatStartDhcp, "starting DHCP") +TETHERNET_MESSAGE(kCapiDhcpTimeout, "DHCP did not obtain a lease within {} seconds.\n" + " Common causes: tethering is not enabled on the device, or the peer runs no " + "DHCP server.\n" + " The service keeps retrying in the background, so an address may still appear " + "if you refresh later; you can also switch to a static IP.") +TETHERNET_MESSAGE(kCapiDhcpNoRouter, "got a DHCP lease, but it carries no router address, so the global default route " + "cannot be pointed at it.") +TETHERNET_MESSAGE(kCapiInvalidAddress, "\"{}\" is not a valid IPv4 address") +TETHERNET_MESSAGE(kCapiInvalidNetmask, "subnet mask \"{}\" is not a valid IPv4 address") +TETHERNET_MESSAGE(kCapiInvalidRouter, "router \"{}\" is not a valid IPv4 address") +TETHERNET_MESSAGE(kCapiDefaultRouteNeedsRouter, "\"Use as default route\" requires a router address") +TETHERNET_MESSAGE(kCapiInvalidDns, "DNS server \"{}\" is not a valid IPv4 address") +TETHERNET_MESSAGE(kCapiWhatApplyManual, "applying a static IP") +TETHERNET_MESSAGE(kCapiNoServiceForDns, "no network service found for {}; DNS cannot be published") +TETHERNET_MESSAGE(kCapiWhatClearConfig, "clearing the interface configuration") +TETHERNET_MESSAGE(kCapiApplyConfigNull, "tn_net_apply: config must not be null") +TETHERNET_MESSAGE(kCapiApplyNeedsRoot, "configuring an interface IP requires root") +TETHERNET_MESSAGE(kCapiUnknownIpMode, "unknown IP mode {}") +TETHERNET_MESSAGE(kCapiClearNeedsRoot, "clearing an interface configuration requires root") + +// ============================================================================= +// cli -- the tethernet-cli command line +// ============================================================================= + +// The CLI help text lives here as whole sentences rather than split into tiny pieces: +// the text must align as fixed layout, and dozens of tiny strings cannot keep the widths consistent. +TETHERNET_MESSAGE(kCliUsage, "Usage: sudo tethernet-cli [options]\n" + "\n" + " A user-space RNDIS driver for macOS: turns an RNDIS device (Android USB\n" + " tethering and friends) into an interface the system can see.\n" + "\n" + "Device selection:\n" + " --vid only match this USB vendor ID (e.g. 18d1)\n" + " --pid only match this USB product ID (e.g. 4ee4)\n" + " --list list the RNDIS devices found and exit (**no root needed**)\n" + "\n" + "Interface:\n" + " --mtu desired MTU, default 1500 (lowered automatically if the\n" + " device cannot carry it; capped by sysctl\n" + " net.link.fake.max_mtu)\n" + " --keep-feth-mac do not set the system-side MAC to the address the device\n" + " reports\n" + "\n" + "Performance tuning:\n" + " --rx-transfers bulk IN transfers in flight, default 16\n" + " --tx-transfers bulk OUT transfers in flight, default 4. Measured 4/8/16\n" + " with no discernible difference (a full pool waits for a\n" + " slot instead of dropping frames), so you rarely need this\n" + " --rx-transfer-kb KiB per bulk IN buffer, default 16\n" + " --max-transfer-kb MaxTransferSize announced in INITIALIZE (KiB), default 16.\n" + " This is the only way to make the device aggregate packets\n" + " and is the main throughput lever; larger is usually faster\n" + " --max-tx-packets how many RNDIS packets to aggregate per host -> device\n" + " transfer. 0 (default) means honour the device's reported\n" + " MaxPacketsPerMessage. Diagnostic: if you suspect the device\n" + " claims aggregation but only unpacks the first packet, set 1\n" + " and compare. Note that aggregation worked on every Android\n" + " device tested and clamping to 1 was slower -- do not reach\n" + " for this as a default cure for dropped frames\n" + " --bpf-buffer-kb KiB for the BPF kernel capture buffer, default 4096\n" + "\n" + "Other:\n" + " --stats statistics reporting interval, 0 disables, default 5000\n" + " --log trace|debug|info|warn|error|off, default info\n" + " --lang interface language, default auto -- checks\n" + " TETHERNET_LANG, LC_ALL, LC_MESSAGES and LANG in that order\n" + " and takes the first non-empty value. English is the only\n" + " language shipped today; en and auto both select it. sudo\n" + " does not always pass those through, so pass --lang\n" + " explicitly then\n" + " --no-color disable coloured log output\n" + " -h, --help show this help\n" + " -V, --version show the version\n" + "\n" + "Why root is required: creating the feth virtual interface and opening /dev/bpf*\n" + "both need root. The USB side does not -- macOS ships no RNDIS kernel driver, so\n" + "libusb can claim the interface directly.\n") + +TETHERNET_MESSAGE(kCliInstallSignalHandlerFailed, "failed to install a handler for signal {}") +TETHERNET_MESSAGE(kCliIgnoreSigpipeFailed, "failed to ignore SIGPIPE") +TETHERNET_MESSAGE(kCliParseNumberFailed, "cannot parse the number \"{}\"") +TETHERNET_MESSAGE(kCliUnknownLogLevel, "unknown log level \"{}\"") +TETHERNET_MESSAGE(kCliUnknownLanguage, "unknown language \"{}\"; English is the only language shipped today") +TETHERNET_MESSAGE(kCliMissingOptionValue, "option {} is missing its argument") +TETHERNET_MESSAGE(kCliUnknownOption, "unknown option \"{}\"; run --help for usage") +TETHERNET_MESSAGE(kCliArgumentError, "argument error: {}\n") +TETHERNET_MESSAGE(kCliNoDevices, "No RNDIS device found.\n") +TETHERNET_MESSAGE(kCliNoDevicesHint, "Check that: (1) the cable carries data and is not charge-only; (2) USB\n" + " tethering is enabled on the device; (3) the phone is unlocked\n" + " and trusts this Mac.\n") +TETHERNET_MESSAGE(kCliDevicesHeader, "RNDIS devices found:\n") +TETHERNET_MESSAGE(kCliDeviceLine, " {} control interface {} / data interface {} signature " + "{:02x}/{:02x}/{:02x}{}\n") +TETHERNET_MESSAGE(kCliDeviceAndroidQuirk, " (matched via the Android quirk fallback)") +TETHERNET_MESSAGE(kCliEnumerateFailed, "failed to enumerate devices: {}\n") +TETHERNET_MESSAGE(kCliStartFailed, "failed to start: {}") +TETHERNET_MESSAGE(kCliStopSignalReceived, "received a shutdown signal") diff --git a/include/tetherkit/common/scheduling.h b/include/tethernet/common/scheduling.h similarity index 97% rename from include/tetherkit/common/scheduling.h rename to include/tethernet/common/scheduling.h index aefb354..1f03c01 100644 --- a/include/tetherkit/common/scheduling.h +++ b/include/tethernet/common/scheduling.h @@ -28,7 +28,7 @@ #include #include -namespace tetherkit { +namespace tethernet { /// Thread purpose, which determines the QoS level. enum class ThreadRole : std::uint8_t { @@ -50,4 +50,4 @@ void ConfigureCurrentThread(std::string_view name, ThreadRole role) noexcept; /// log output. [[nodiscard]] unsigned int QosClassFor(ThreadRole role) noexcept; -} // namespace tetherkit +} // namespace tethernet diff --git a/include/tetherkit/common/spsc_ring.h b/include/tethernet/common/spsc_ring.h similarity index 95% rename from include/tetherkit/common/spsc_ring.h rename to include/tethernet/common/spsc_ring.h index 66ee223..1715d0e 100644 --- a/include/tetherkit/common/spsc_ring.h +++ b/include/tethernet/common/spsc_ring.h @@ -15,18 +15,18 @@ // same SpscCursor. // // Correctness is verified with ThreadSanitizer: -// cmake -B build-tsan -DTETHERKIT_ENABLE_TSAN=ON && ctest --test-dir build-tsan +// cmake -B build-tsan -DTETHERNET_ENABLE_TSAN=ON && ctest --test-dir build-tsan #pragma once +#include "tethernet/common/cache.h" + #include #include #include #include #include -#include "tetherkit/common/cache.h" - -namespace tetherkit { +namespace tethernet { /// Index cursor for an SPSC queue: manages "where written / where read" and /// cross-thread memory ordering. @@ -188,13 +188,13 @@ class SpscCursor { private: // Read-only shared data gets its own cache line to avoid sharing with frequently // modified indices. - TETHERKIT_CACHE_ALIGNED const std::size_t capacity_; + TETHERNET_CACHE_ALIGNED const std::size_t capacity_; const std::size_t mask_; - TETHERKIT_CACHE_ALIGNED std::atomic write_pos_{0}; - TETHERKIT_CACHE_ALIGNED std::uint64_t cached_read_pos_{0}; - TETHERKIT_CACHE_ALIGNED std::atomic read_pos_{0}; - TETHERKIT_CACHE_ALIGNED std::uint64_t cached_write_pos_{0}; + TETHERNET_CACHE_ALIGNED std::atomic write_pos_{0}; + TETHERNET_CACHE_ALIGNED std::uint64_t cached_read_pos_{0}; + TETHERNET_CACHE_ALIGNED std::atomic read_pos_{0}; + TETHERNET_CACHE_ALIGNED std::uint64_t cached_write_pos_{0}; }; /// SPSC lock-free bounded queue of fixed-size elements. @@ -258,4 +258,4 @@ class SpscRing { std::unique_ptr slots_; }; -} // namespace tetherkit +} // namespace tethernet diff --git a/include/tetherkit/common/stats.h b/include/tethernet/common/stats.h similarity index 98% rename from include/tetherkit/common/stats.h rename to include/tethernet/common/stats.h index e014c58..f84aafe 100644 --- a/include/tetherkit/common/stats.h +++ b/include/tethernet/common/stats.h @@ -13,13 +13,13 @@ // Relaxed atomics on arm64 are ordinary ldr/str instructions with no extra cost. #pragma once +#include "tethernet/common/cache.h" +#include "tethernet/common/time.h" + #include #include -#include "tetherkit/common/cache.h" -#include "tetherkit/common/time.h" - -namespace tetherkit { +namespace tethernet { /// Data-path counters for one direction (RX or TX). /// @@ -182,4 +182,4 @@ class RateSampler { DirectionSnapshot last_tx_; }; -} // namespace tetherkit +} // namespace tethernet diff --git a/include/tetherkit/common/time.h b/include/tethernet/common/time.h similarity index 98% rename from include/tetherkit/common/time.h rename to include/tethernet/common/time.h index bc55d18..3b9e5c8 100644 --- a/include/tetherkit/common/time.h +++ b/include/tethernet/common/time.h @@ -11,7 +11,7 @@ #include #include -namespace tetherkit { +namespace tethernet { /// Nanosecond timestamp type alias, making interface signatures self-explanatory. using Nanos = std::uint64_t; @@ -104,4 +104,4 @@ class PeriodicTimer { Nanos next_deadline_; }; -} // namespace tetherkit +} // namespace tethernet diff --git a/include/tetherkit/core/bridge.h b/include/tethernet/core/bridge.h similarity index 97% rename from include/tetherkit/core/bridge.h rename to include/tethernet/core/bridge.h index b6adad6..8815cac 100644 --- a/include/tetherkit/core/bridge.h +++ b/include/tethernet/core/bridge.h @@ -54,19 +54,19 @@ // already has a natural blocking point, so merging gains nothing. #pragma once +#include "tethernet/common/error.h" +#include "tethernet/common/frame_ring.h" +#include "tethernet/common/stats.h" +#include "tethernet/net/link_backend.h" +#include "tethernet/usb/data_channel.h" + #include #include #include #include #include -#include "tetherkit/common/error.h" -#include "tetherkit/common/frame_ring.h" -#include "tetherkit/common/stats.h" -#include "tetherkit/net/link_backend.h" -#include "tetherkit/usb/data_channel.h" - -namespace tetherkit::core { +namespace tethernet::core { /// Bridge layer configuration. struct BridgeConfig { @@ -239,4 +239,4 @@ class Bridge { [[nodiscard]] std::string FormatStatsLine(const BridgeStats& previous, const BridgeStats& current, double seconds); -} // namespace tetherkit::core +} // namespace tethernet::core diff --git a/include/tetherkit/core/runtime.h b/include/tethernet/core/runtime.h similarity index 96% rename from include/tetherkit/core/runtime.h rename to include/tethernet/core/runtime.h index 53ec967..0d2d656 100644 --- a/include/tetherkit/core/runtime.h +++ b/include/tethernet/core/runtime.h @@ -35,6 +35,15 @@ // use-after-free (see usb/data_channel.h for details). #pragma once +#include "tethernet/common/error.h" +#include "tethernet/core/bridge.h" +#include "tethernet/net/bpf_link.h" +#include "tethernet/net/feth_device.h" +#include "tethernet/rndis/state_machine.h" +#include "tethernet/usb/context.h" +#include "tethernet/usb/data_channel.h" +#include "tethernet/usb/device.h" + #include #include #include @@ -44,16 +53,7 @@ #include #include -#include "tetherkit/common/error.h" -#include "tetherkit/core/bridge.h" -#include "tetherkit/net/bpf_link.h" -#include "tetherkit/net/feth_device.h" -#include "tetherkit/rndis/state_machine.h" -#include "tetherkit/usb/context.h" -#include "tetherkit/usb/data_channel.h" -#include "tetherkit/usb/device.h" - -namespace tetherkit::core { +namespace tethernet::core { /// Lifecycle state of the runtime. The only state dimension the host /// (command line / GUI) needs to care about. @@ -211,8 +211,8 @@ struct RuntimeConfig { /// /// Usage: /// ``` -/// TETHERKIT_ASSIGN_OR_RETURN(auto runtime, Runtime::Create(config)); -/// TETHERKIT_RETURN_IF_ERROR(runtime->Start()); // returns immediately +/// TETHERNET_ASSIGN_OR_RETURN(auto runtime, Runtime::Create(config)); +/// TETHERNET_RETURN_IF_ERROR(runtime->Start()); // returns immediately /// runtime->WaitUntilStopped(); // command line: park the /// // main thread /// runtime->Stop(); // idempotent @@ -349,4 +349,4 @@ class Runtime final : public rndis::StateMachineObserver { bool stopped_ = false; }; -} // namespace tetherkit::core +} // namespace tethernet::core diff --git a/include/tetherkit/net/bpf_link.h b/include/tethernet/net/bpf_link.h similarity index 98% rename from include/tetherkit/net/bpf_link.h rename to include/tethernet/net/bpf_link.h index 9682f08..d491b61 100644 --- a/include/tetherkit/net/bpf_link.h +++ b/include/tethernet/net/bpf_link.h @@ -64,6 +64,9 @@ // immediately. #pragma once +#include "tethernet/common/error.h" +#include "tethernet/net/link_backend.h" + #include #include #include @@ -72,10 +75,7 @@ #include #include -#include "tetherkit/common/error.h" -#include "tetherkit/net/link_backend.h" - -namespace tetherkit::net { +namespace tethernet::net { /// Configuration parameters for the BPF backend. struct BpfConfig { @@ -216,4 +216,4 @@ class BpfLink final : public LinkBackend { std::atomic interrupted_{false}; }; -} // namespace tetherkit::net +} // namespace tethernet::net diff --git a/include/tetherkit/net/darwin_abi.h b/include/tethernet/net/darwin_abi.h similarity index 99% rename from include/tetherkit/net/darwin_abi.h rename to include/tethernet/net/darwin_abi.h index d2b7982..1d076a5 100644 --- a/include/tetherkit/net/darwin_abi.h +++ b/include/tethernet/net/darwin_abi.h @@ -24,6 +24,8 @@ // Neither can silently take a wrong path. #pragma once +#include "tethernet/common/i18n.h" + #include #include #include @@ -33,9 +35,7 @@ #include #include -#include "tetherkit/common/i18n.h" - -namespace tetherkit::net { +namespace tethernet::net { // ============================================================================= // struct ifdrv -- parameter struct for SIOCSDRVSPEC / SIOCGDRVSPEC @@ -235,4 +235,4 @@ inline constexpr RequiredFethSysctl kRequiredFethSysctls[] = { {"net.link.fake.separate_frame_header", 0, Msg::kNetSysctlWhySeparateHeader}, }; -} // namespace tetherkit::net +} // namespace tethernet::net diff --git a/include/tetherkit/net/feth_device.h b/include/tethernet/net/feth_device.h similarity index 97% rename from include/tetherkit/net/feth_device.h rename to include/tethernet/net/feth_device.h index c7c586a..0bda0e4 100644 --- a/include/tetherkit/net/feth_device.h +++ b/include/tethernet/net/feth_device.h @@ -6,7 +6,7 @@ // | IP / routes / DHCP // v // +--------+ peer pairing +--------+ BPF -// | feth0 | <-----------> | feth1 | <----------> TetherKit +// | feth0 | <-----------> | feth1 | <----------> Tethernet // |(system)| |(driver)| // +--------+ +--------+ // @@ -24,14 +24,14 @@ // proc_suser check in the kernel). #pragma once +#include "tethernet/common/error.h" + #include #include #include #include -#include "tetherkit/common/error.h" - -namespace tetherkit::net { +namespace tethernet::net { /// A 6-byte MAC address. using MacAddress = std::array; @@ -203,7 +203,7 @@ class FethPair { /// routes on this one. [[nodiscard]] const FethInterface& SystemSide() const noexcept { return system_side_; } - /// The driver-side interface: TetherKit attaches BPF to it to receive and + /// The driver-side interface: Tethernet attaches BPF to it to receive and /// send raw frames. [[nodiscard]] const FethInterface& DriverSide() const noexcept { return driver_side_; } @@ -226,4 +226,4 @@ class FethPair { /// startup instead of a stream of EPERM. [[nodiscard]] bool IsRunningAsRoot() noexcept; -} // namespace tetherkit::net +} // namespace tethernet::net diff --git a/include/tetherkit/net/link_backend.h b/include/tethernet/net/link_backend.h similarity index 95% rename from include/tetherkit/net/link_backend.h rename to include/tethernet/net/link_backend.h index 59ae578..6ea0fb2 100644 --- a/include/tetherkit/net/link_backend.h +++ b/include/tethernet/net/link_backend.h @@ -13,14 +13,14 @@ // deliberately avoided here.) #pragma once +#include "tethernet/common/error.h" +#include "tethernet/common/frame_ring.h" + #include #include #include -#include "tetherkit/common/error.h" -#include "tetherkit/common/frame_ring.h" - -namespace tetherkit::net { +namespace tethernet::net { /// A batch of frames to send. using FrameBatch = std::span; @@ -78,4 +78,4 @@ class LinkBackend { virtual void Interrupt() noexcept = 0; }; -} // namespace tetherkit::net +} // namespace tethernet::net diff --git a/include/tetherkit/net/loopback_link.h b/include/tethernet/net/loopback_link.h similarity index 97% rename from include/tetherkit/net/loopback_link.h rename to include/tethernet/net/loopback_link.h index 0dc0aff..fad1d71 100644 --- a/include/tetherkit/net/loopback_link.h +++ b/include/tethernet/net/loopback_link.h @@ -16,15 +16,15 @@ // the same behavior as real BPF. #pragma once +#include "tethernet/common/frame_ring.h" +#include "tethernet/net/link_backend.h" + #include #include #include #include -#include "tetherkit/common/frame_ring.h" -#include "tetherkit/net/link_backend.h" - -namespace tetherkit::net { +namespace tethernet::net { /// Configuration of the loopback backend. struct LoopbackConfig { @@ -130,4 +130,4 @@ class LoopbackLink final : public LinkBackend { std::atomic fail_writes_after_{0}; }; -} // namespace tetherkit::net +} // namespace tethernet::net diff --git a/include/tetherkit/rndis/control_channel.h b/include/tethernet/rndis/control_channel.h similarity index 96% rename from include/tetherkit/rndis/control_channel.h rename to include/tethernet/rndis/control_channel.h index 5345820..7a4a5cd 100644 --- a/include/tetherkit/rndis/control_channel.h +++ b/include/tethernet/rndis/control_channel.h @@ -3,9 +3,9 @@ // Why this interface lives in the rndis layer rather than the usb layer: it describes the // **RNDIS protocol** control-channel semantics (send a message, fetch a message, wait // for a "response available" notification), not USB semantics. The state machine -// (tk_rndis) depends on it and the libusb implementation (tk_usb) provides it -- so the +// (tn_rndis) depends on it and the libusb implementation (tn_usb) provides it -- so the // dependency direction is the correct rndis <- usb, with no reverse dependency from -// tk_rndis on tk_usb. +// tn_rndis on tn_usb. // // RNDIS control messages do not go through bulk endpoints, but through two class // requests on the USB control endpoint (EP0): @@ -27,14 +27,14 @@ // (libusb issue #1288: control transfers on M2 are about 10x slower than on x64). #pragma once +#include "tethernet/common/error.h" + #include #include #include #include -#include "tetherkit/common/error.h" - -namespace tetherkit::rndis { +namespace tethernet::rndis { /// Timeout value to pass when "just probing, do not really wait". /// @@ -108,4 +108,4 @@ class ControlChannel { [[nodiscard]] virtual std::string_view Describe() const noexcept = 0; }; -} // namespace tetherkit::rndis +} // namespace tethernet::rndis diff --git a/include/tetherkit/rndis/messages.h b/include/tethernet/rndis/messages.h similarity index 98% rename from include/tetherkit/rndis/messages.h rename to include/tethernet/rndis/messages.h index 0555608..36dec66 100644 --- a/include/tetherkit/rndis/messages.h +++ b/include/tethernet/rndis/messages.h @@ -18,15 +18,15 @@ // of bounds. #pragma once +#include "tethernet/common/error.h" +#include "tethernet/rndis/protocol.h" + #include #include #include #include -#include "tetherkit/common/error.h" -#include "tetherkit/rndis/protocol.h" - -namespace tetherkit::rndis { +namespace tethernet::rndis { /// 6-byte Ethernet MAC address. using MacAddress = std::array; @@ -260,4 +260,4 @@ struct IndicateStatus { /// Read a 6-byte MAC from an information buffer. [[nodiscard]] Result ParseMac(std::span information); -} // namespace tetherkit::rndis +} // namespace tethernet::rndis diff --git a/include/tetherkit/rndis/packet_codec.h b/include/tethernet/rndis/packet_codec.h similarity index 98% rename from include/tetherkit/rndis/packet_codec.h rename to include/tethernet/rndis/packet_codec.h index c6c890a..c39e7be 100644 --- a/include/tetherkit/rndis/packet_codec.h +++ b/include/tethernet/rndis/packet_codec.h @@ -20,16 +20,16 @@ // determined by PacketAlignmentFactor. #pragma once +#include "tethernet/common/byte_order.h" +#include "tethernet/common/i18n.h" +#include "tethernet/rndis/protocol.h" + #include #include #include #include -#include "tetherkit/common/byte_order.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/rndis/protocol.h" - -namespace tetherkit::rndis { +namespace tethernet::rndis { // ============================================================================= // Decode: device -> host @@ -335,4 +335,4 @@ class PacketMessageWriter { bool zlp_padding_added_ = false; }; -} // namespace tetherkit::rndis +} // namespace tethernet::rndis diff --git a/include/tetherkit/rndis/protocol.h b/include/tethernet/rndis/protocol.h similarity index 99% rename from include/tetherkit/rndis/protocol.h rename to include/tethernet/rndis/protocol.h index 0d0d329..e9fcadc 100644 --- a/include/tetherkit/rndis/protocol.h +++ b/include/tethernet/rndis/protocol.h @@ -34,7 +34,7 @@ #include #include -namespace tetherkit::rndis { +namespace tethernet::rndis { // ============================================================================= // Version and timeouts @@ -267,9 +267,9 @@ inline constexpr std::uint32_t kMaxPacketAlignmentFactor = 7; /// RNDIS_STATUS_* status codes. /// -/// Deliberately named StatusCode rather than Status: `tetherkit::Status` is already an +/// Deliberately named StatusCode rather than Status: `tethernet::Status` is already an /// alias for `std::expected`, and defining another `Status` in -/// tetherkit::rndis would shadow it, making it impossible to write "may fail with no +/// tethernet::rndis would shadow it, making it impossible to write "may fail with no /// return value" signatures in this namespace. enum class StatusCode : std::uint32_t { // ---- Success / pending ---- @@ -602,4 +602,4 @@ static_assert(MaxTransferSizeFor(1500, 64) == 1600); /// Hard upper limit of MaxTransferSize for USB 1.1 devices (required by specification). inline constexpr std::uint32_t kUsb11MaxTransferSize = 0x4000; -} // namespace tetherkit::rndis +} // namespace tethernet::rndis diff --git a/include/tetherkit/rndis/state_machine.h b/include/tethernet/rndis/state_machine.h similarity index 98% rename from include/tetherkit/rndis/state_machine.h rename to include/tethernet/rndis/state_machine.h index 8a19ebd..d54338e 100644 --- a/include/tetherkit/rndis/state_machine.h +++ b/include/tethernet/rndis/state_machine.h @@ -63,18 +63,18 @@ // not flow again after reset. #pragma once +#include "tethernet/common/error.h" +#include "tethernet/common/time.h" +#include "tethernet/rndis/control_channel.h" +#include "tethernet/rndis/messages.h" +#include "tethernet/rndis/protocol.h" + #include #include #include #include -#include "tetherkit/common/error.h" -#include "tetherkit/common/time.h" -#include "tetherkit/rndis/messages.h" -#include "tetherkit/rndis/protocol.h" -#include "tetherkit/rndis/control_channel.h" - -namespace tetherkit::rndis { +namespace tethernet::rndis { /// Host-side state. enum class State : std::uint8_t { @@ -360,4 +360,4 @@ class StateMachine { bool link_connected_ = false; }; -} // namespace tetherkit::rndis +} // namespace tethernet::rndis diff --git a/include/tetherkit/usb/context.h b/include/tethernet/usb/context.h similarity index 97% rename from include/tetherkit/usb/context.h rename to include/tethernet/usb/context.h index b3920dd..a973197 100644 --- a/include/tetherkit/usb/context.h +++ b/include/tethernet/usb/context.h @@ -40,6 +40,8 @@ // key trick for keeping a USB pipe saturated. #pragma once +#include "tethernet/common/error.h" + #include #include @@ -47,9 +49,7 @@ #include #include -#include "tetherkit/common/error.h" - -namespace tetherkit::usb { +namespace tethernet::usb { /// A libusb context plus a dedicated event loop thread. /// @@ -96,4 +96,4 @@ class Context { std::atomic running_{false}; }; -} // namespace tetherkit::usb +} // namespace tethernet::usb diff --git a/include/tetherkit/usb/data_channel.h b/include/tethernet/usb/data_channel.h similarity index 98% rename from include/tetherkit/usb/data_channel.h rename to include/tethernet/usb/data_channel.h index 9bb3e27..40c4577 100644 --- a/include/tetherkit/usb/data_channel.h +++ b/include/tethernet/usb/data_channel.h @@ -49,6 +49,13 @@ // -> Conclusion: use fewer, larger transfers. #pragma once +#include "tethernet/common/error.h" +#include "tethernet/common/frame_ring.h" +#include "tethernet/common/spsc_ring.h" +#include "tethernet/common/stats.h" +#include "tethernet/rndis/messages.h" +#include "tethernet/usb/device.h" + #include #include @@ -62,14 +69,7 @@ #include #include -#include "tetherkit/common/error.h" -#include "tetherkit/common/frame_ring.h" -#include "tetherkit/common/spsc_ring.h" -#include "tetherkit/common/stats.h" -#include "tetherkit/rndis/messages.h" -#include "tetherkit/usb/device.h" - -namespace tetherkit::usb { +namespace tethernet::usb { /// Data channel configuration. struct DataChannelConfig { @@ -338,4 +338,4 @@ class UsbDataChannel final : public DataChannel { std::condition_variable send_capacity_cv_; }; -} // namespace tetherkit::usb +} // namespace tethernet::usb diff --git a/include/tetherkit/usb/device.h b/include/tethernet/usb/device.h similarity index 98% rename from include/tetherkit/usb/device.h rename to include/tethernet/usb/device.h index fe65338..59c8f91 100644 --- a/include/tetherkit/usb/device.h +++ b/include/tethernet/usb/device.h @@ -51,6 +51,11 @@ // begin with, so enabling it only introduces a destructive operation. #pragma once +#include "tethernet/common/error.h" +#include "tethernet/rndis/control_channel.h" +#include "tethernet/rndis/protocol.h" +#include "tethernet/usb/context.h" + #include #include @@ -60,12 +65,7 @@ #include #include -#include "tetherkit/common/error.h" -#include "tetherkit/rndis/protocol.h" -#include "tetherkit/usb/context.h" -#include "tetherkit/rndis/control_channel.h" - -namespace tetherkit::usb { +namespace tethernet::usb { /// A candidate device recognized as RNDIS. struct DeviceCandidate { @@ -302,4 +302,4 @@ class UsbControlChannel final : public rndis::ControlChannel { std::atomic notification_stopping_{false}; }; -} // namespace tetherkit::usb +} // namespace tethernet::usb diff --git a/include/tetherkit/version.h b/include/tethernet/version.h similarity index 85% rename from include/tetherkit/version.h rename to include/tethernet/version.h index 34b0925..acb2972 100644 --- a/include/tetherkit/version.h +++ b/include/tethernet/version.h @@ -1,4 +1,4 @@ -// TetherKit version information. +// Tethernet version information. // // The version number is injected into version.cc by CMake at configure time; // this header only exposes accessors, so bumping the version does not trigger @@ -8,7 +8,7 @@ #include #include -namespace tetherkit { +namespace tethernet { /// The three components of the semantic version number. struct Version { @@ -20,7 +20,7 @@ struct Version { /// Returns the version number burned in at compile time. Version GetVersion() noexcept; -/// Returns a human-readable version string like "TetherKit 0.1.0 (C++23, macOS 13.3+)". +/// Returns a human-readable version string like "Tethernet 0.1.0 (C++23, macOS 13.3+)". std::string_view GetVersionString() noexcept; /// Returns a build description like @@ -30,4 +30,4 @@ std::string_view GetVersionString() noexcept; /// configuration is meaningless. std::string_view GetBuildDescription() noexcept; -} // namespace tetherkit +} // namespace tethernet diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9875679..b2d6487 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,31 +3,31 @@ # The dependency direction is strictly one-way; reverse or circular # dependencies are forbidden: # -# tk_common <- no dependencies (logging, errors, lock-free containers, +# tn_common <- no dependencies (logging, errors, lock-free containers, # memory pool, statistics) # | # v -# tk_rndis <- common (RNDIS wire format and state machine, pure +# tn_rndis <- common (RNDIS wire format and state machine, pure # logic, no I/O) -# tk_net <- common (feth virtual interface + BPF link, macOS +# tn_net <- common (feth virtual interface + BPF link, macOS # syscalls only) -# tk_usb <- common (libusb wrapper) +# tn_usb <- common (libusb wrapper) # | # v -# tk_core <- common+rndis+net+usb (data-path bridging and runtime +# tn_core <- common+rndis+net+usb (data-path bridging and runtime # orchestration) # | # v -# tetherkit <- core (command-line executable) -# tk_capi <- core (C ABI, consumed by the Swift GUI) +# tethernet <- core (command-line executable) +# tn_capi <- core (C ABI, consumed by the Swift GUI) # ------------------------------------------------------------------------------ # Common compile properties inherited by every target in this project. # ------------------------------------------------------------------------------ -add_library(tetherkit_base INTERFACE) -target_include_directories(tetherkit_base INTERFACE "${PROJECT_SOURCE_DIR}/include") -target_link_libraries(tetherkit_base INTERFACE tetherkit_warnings tetherkit_sanitizers - tetherkit_optimizations) +add_library(tethernet_base INTERFACE) +target_include_directories(tethernet_base INTERFACE "${PROJECT_SOURCE_DIR}/include") +target_link_libraries(tethernet_base INTERFACE tethernet_warnings tethernet_sanitizers + tethernet_optimizations) # ------------------------------------------------------------------------------ # Version: generated from the template so that a version bump does not force a @@ -36,8 +36,8 @@ target_link_libraries(tetherkit_base INTERFACE tetherkit_warnings tetherkit_sani configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.cc.in" "${CMAKE_CURRENT_BINARY_DIR}/version.cc" @ONLY) -add_library(tk_version STATIC "${CMAKE_CURRENT_BINARY_DIR}/version.cc") -target_link_libraries(tk_version PUBLIC tetherkit_base) +add_library(tn_version STATIC "${CMAKE_CURRENT_BINARY_DIR}/version.cc") +target_link_libraries(tn_version PUBLIC tethernet_base) add_subdirectory(common) add_subdirectory(rndis) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 6b95ccd..870889d 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,13 +1,13 @@ -# app -- the command-line executable tetherkit-cli +# app -- the command-line executable tethernet-cli # -# The CMake target name stays tetherkit (the usage -# `cmake --build build --target tetherkit` and the CMake cache are unaffected); -# the **output file name** is tetherkit-cli: on Homebrew the `tetherkit` name -# was claimed by the GUI formula, so the CLI ships as tetherkit-cli. -add_executable(tetherkit main.cc) -set_target_properties(tetherkit PROPERTIES OUTPUT_NAME tetherkit-cli) +# The CMake target name stays tethernet (the usage +# `cmake --build build --target tethernet` and the CMake cache are unaffected); +# the **output file name** is tethernet-cli: on Homebrew the `tethernet` name +# was claimed by the GUI formula, so the CLI ships as tethernet-cli. +add_executable(tethernet main.cc) +set_target_properties(tethernet PROPERTIES OUTPUT_NAME tethernet-cli) -# tk_core PUBLIC-links all of the other modules. -target_link_libraries(tetherkit PRIVATE tetherkit_base tk_version tk_core) +# tn_core PUBLIC-links all of the other modules. +target_link_libraries(tethernet PRIVATE tethernet_base tn_version tn_core) -install(TARGETS tetherkit RUNTIME DESTINATION bin) +install(TARGETS tethernet RUNTIME DESTINATION bin) diff --git a/src/app/main.cc b/src/app/main.cc index da5bf60..cc328e5 100644 --- a/src/app/main.cc +++ b/src/app/main.cc @@ -1,12 +1,12 @@ -// TetherKit command-line entry point. +// Tethernet command-line entry point. // // Kept deliberately thin: parse arguments -> assemble a RuntimeConfig -> hand // it to core::Runtime -> handle signals. All substantive logic lives in the // library so it can be covered by unit tests. -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" -#include "tetherkit/core/runtime.h" -#include "tetherkit/version.h" +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" +#include "tethernet/core/runtime.h" +#include "tethernet/version.h" #include // NOLINT(modernize-deprecated-headers) -- sigaction lives only in this header #include @@ -23,14 +23,14 @@ namespace { -using tetherkit::Error; -using tetherkit::Language; -using tetherkit::LogLevel; -using tetherkit::Msg; -using tetherkit::Result; -using tetherkit::Status; -using tetherkit::Text; -using tetherkit::Tr; +using tethernet::Error; +using tethernet::Language; +using tethernet::LogLevel; +using tethernet::Msg; +using tethernet::Result; +using tethernet::Status; +using tethernet::Text; +using tethernet::Tr; /// Global shutdown flag. /// @@ -78,7 +78,7 @@ extern "C" void HandleSignal(int /*signal_number*/) { if (::sigaction(SIGPIPE, &ignore, nullptr) != 0) { return std::unexpected(Error::FromErrno(0, Tr(Msg::kCliIgnoreSigpipeFailed))); } - return tetherkit::Ok(); + return tethernet::Ok(); } // ============================================================================= @@ -116,8 +116,8 @@ void ApplyLanguageOption(int argc, char** argv) { if (std::string_view{argv[i]} != "--lang") { continue; } - if (Language language{}; tetherkit::ParseLanguage(argv[i + 1], &language)) { - tetherkit::SetLanguage(language); + if (Language language{}; tethernet::ParseLanguage(argv[i + 1], &language)) { + tethernet::SetLanguage(language); } return; } @@ -159,7 +159,7 @@ void ApplyLanguageOption(int argc, char** argv) { /// Result of parsing. struct ParsedArguments { - tetherkit::core::RuntimeConfig config; + tethernet::core::RuntimeConfig config; bool show_help = false; bool show_version = false; bool list_devices = false; @@ -198,78 +198,78 @@ struct ParsedArguments { continue; } if (argument == "--no-color") { - tetherkit::SetLogColorEnabled(false); + tethernet::SetLogColorEnabled(false); continue; } if (argument == "--vid") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(const auto value, ParseUint(text, 16)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(const auto value, ParseUint(text, 16)); parsed.config.device_filter.vendor_id = static_cast(value); continue; } if (argument == "--pid") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(const auto value, ParseUint(text, 16)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(const auto value, ParseUint(text, 16)); parsed.config.device_filter.product_id = static_cast(value); continue; } if (argument == "--mtu") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(parsed.config.mtu, ParseUint(text)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(parsed.config.mtu, ParseUint(text)); continue; } if (argument == "--rx-transfers") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(parsed.config.data_channel.rx_transfer_count, ParseUint(text)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(parsed.config.data_channel.rx_transfer_count, ParseUint(text)); continue; } if (argument == "--tx-transfers") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(parsed.config.data_channel.tx_transfer_count, ParseUint(text)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(parsed.config.data_channel.tx_transfer_count, ParseUint(text)); continue; } if (argument == "--rx-transfer-kb") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(const auto kilobytes, ParseUint(text)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(const auto kilobytes, ParseUint(text)); parsed.config.data_channel.rx_transfer_bytes = kilobytes * 1024; continue; } if (argument == "--max-transfer-kb") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(const auto kilobytes, ParseUint(text)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(const auto kilobytes, ParseUint(text)); parsed.config.rndis.host_max_transfer_size = kilobytes * 1024; continue; } if (argument == "--max-tx-packets") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(parsed.config.rndis.max_tx_packets_per_message, ParseUint(text)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(parsed.config.rndis.max_tx_packets_per_message, ParseUint(text)); continue; } if (argument == "--bpf-buffer-kb") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(const auto kilobytes, ParseUint(text)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(const auto kilobytes, ParseUint(text)); parsed.config.bpf.kernel_buffer_bytes = kilobytes * 1024; continue; } if (argument == "--stats") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(parsed.config.stats_interval_millis, ParseUint(text)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(parsed.config.stats_interval_millis, ParseUint(text)); continue; } if (argument == "--log") { - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); - TETHERKIT_ASSIGN_OR_RETURN(const auto level, ParseLogLevel(text)); - tetherkit::SetLogLevel(level); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(const auto level, ParseLogLevel(text)); + tethernet::SetLogLevel(level); continue; } if (argument == "--lang") { // The value was already applied by ApplyLanguageOption before parsing // began (so that even this round's errors come out in the target // language). Here we only consume it and re-check the spelling. - TETHERKIT_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); + TETHERNET_ASSIGN_OR_RETURN(const auto text, take_value(i, argument)); Language ignored{}; - if (!tetherkit::ParseLanguage(text, &ignored)) { + if (!tethernet::ParseLanguage(text, &ignored)) { return std::unexpected(Error::Generic(Tr(Msg::kCliUnknownLanguage, text))); } continue; @@ -281,15 +281,15 @@ struct ParsedArguments { } /// `--list`: print the recognized RNDIS devices. **Does not need root**. -[[nodiscard]] Status ListDevices(const tetherkit::usb::DeviceFilter& filter) { - TETHERKIT_ASSIGN_OR_RETURN(const auto context, tetherkit::usb::Context::Create()); - TETHERKIT_ASSIGN_OR_RETURN(const auto candidates, - tetherkit::usb::FindRndisDevices(*context, filter)); +[[nodiscard]] Status ListDevices(const tethernet::usb::DeviceFilter& filter) { + TETHERNET_ASSIGN_OR_RETURN(const auto context, tethernet::usb::Context::Create()); + TETHERNET_ASSIGN_OR_RETURN(const auto candidates, + tethernet::usb::FindRndisDevices(*context, filter)); if (candidates.empty()) { PrintText(Msg::kCliNoDevices); PrintText(Msg::kCliNoDevicesHint); - return tetherkit::Ok(); + return tethernet::Ok(); } PrintText(Msg::kCliDevicesHeader); @@ -301,7 +301,7 @@ struct ParsedArguments { candidate.used_android_quirk ? Text(Msg::kCliDeviceAndroidQuirk) : std::string_view{}); std::fputs(line.c_str(), stdout); } - return tetherkit::Ok(); + return tethernet::Ok(); } } // namespace @@ -310,7 +310,7 @@ int main(int argc, char** argv) { // The language must be settled **before any** output. Infer from the // environment first, then let an explicit --lang override -- that way even // argument parsing's own errors come out in the user's language. - tetherkit::SetLanguage(tetherkit::DetectLanguageFromEnvironment()); + tethernet::SetLanguage(tethernet::DetectLanguageFromEnvironment()); ApplyLanguageOption(argc, argv); auto parsed = ParseArguments(argc, argv); @@ -325,8 +325,8 @@ int main(int argc, char** argv) { return 0; } if (parsed->show_version) { - const std::string line = std::format("{}\n{}\n", tetherkit::GetVersionString(), - tetherkit::GetBuildDescription()); + const std::string line = + std::format("{}\n{}\n", tethernet::GetVersionString(), tethernet::GetBuildDescription()); std::fputs(line.c_str(), stdout); return 0; } @@ -341,20 +341,20 @@ int main(int argc, char** argv) { } if (const auto status = InstallSignalHandlers(); !status) { - TETHERKIT_ERROR("{}", status.error().ToString()); + TETHERNET_ERROR("{}", status.error().ToString()); return 1; } - TETHERKIT_INFO("{}", tetherkit::GetVersionString()); + TETHERNET_INFO("{}", tethernet::GetVersionString()); - auto runtime = tetherkit::core::Runtime::Create(parsed->config); + auto runtime = tethernet::core::Runtime::Create(parsed->config); if (!runtime) { - TETHERKIT_ERROR("{}", runtime.error().ToString()); + TETHERNET_ERROR("{}", runtime.error().ToString()); return 1; } if (const auto status = (*runtime)->Start(); !status) { - TETHERKIT_ERROR_TR(Msg::kCliStartFailed, status.error().ToString()); + TETHERNET_ERROR_TR(Msg::kCliStartFailed, status.error().ToString()); return 1; } @@ -366,7 +366,7 @@ int main(int argc, char** argv) { while (!g_stop_requested.load(std::memory_order_acquire)) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); } - TETHERKIT_INFO_TR(Msg::kCliStopSignalReceived); + TETHERNET_INFO_TR(Msg::kCliStopSignalReceived); (*runtime)->RequestStop(); }); diff --git a/src/capi/.clang-tidy b/src/capi/.clang-tidy index e9533b1..89ad7e9 100644 --- a/src/capi/.clang-tidy +++ b/src/capi/.clang-tidy @@ -1,7 +1,7 @@ # Check exemptions for the C ABI implementation layer # -# Same reasoning as include/tetherkit/capi/.clang-tidy: the exported function -# names must follow the C convention `tk_snake_case`, and the implementation +# Same reasoning as include/tethernet/capi/.clang-tidy: the exported function +# names must follow the C convention `tn_snake_case`, and the implementation # unavoidably contains C-style fixed-length buffer copies and raw pointers. InheritParentConfig: true diff --git a/src/capi/CMakeLists.txt b/src/capi/CMakeLists.txt index f62c8c5..5735974 100644 --- a/src/capi/CMakeLists.txt +++ b/src/capi/CMakeLists.txt @@ -1,4 +1,4 @@ -# tk_capi -- the C ABI layer +# tn_capi -- the C ABI layer # # The **only** extern "C" boundary of the whole project; it exists for one # reason: to serve the Swift GUI. @@ -6,7 +6,7 @@ # on core and everything below it. # # Built as an OBJECT library rather than a STATIC one, so that the shared -# library libtetherkit picks up **all** the object files. With a static +# library libtethernet picks up **all** the object files. With a static # library the linker only pulls in the members somebody references, and the # exported C ABI functions are referenced by nobody inside the library -- the # result is an empty dylib, with no error message whatsoever. @@ -14,7 +14,7 @@ # way to say it in CMake.) add_library( - tk_capi OBJECT + tn_capi OBJECT capi_support.cc core_foundation_support.cc environment.cc @@ -24,28 +24,28 @@ add_library( process_runner.cc session.cc) -target_link_libraries(tk_capi PUBLIC tetherkit_base tk_version tk_core) +target_link_libraries(tn_capi PUBLIC tethernet_base tn_version tn_core) # The interface IP configuration has to read back configd's dynamic store # (the gateway and DNS that are actually in effect), and that means going -# through SystemConfiguration. This is a dependency of tk_capi alone -- the +# through SystemConfiguration. This is a dependency of tn_capi alone -- the # lower layers never touch configd. -target_link_libraries(tk_capi PUBLIC "-framework CoreFoundation" +target_link_libraries(tn_capi PUBLIC "-framework CoreFoundation" "-framework SystemConfiguration") # ------------------------------------------------------------------------------ -# libtetherkit -- the shared library we distribute +# libtethernet -- the shared library we distribute # # Swift can only link shared libraries or system libraries, not a pile of CMake # static targets, so this layer has to exist. # ------------------------------------------------------------------------------ -if(TETHERKIT_BUILD_SHARED_LIB) - add_library(tetherkit_shared SHARED) - target_link_libraries(tetherkit_shared PRIVATE tk_capi) +if(TETHERNET_BUILD_SHARED_LIB) + add_library(tethernet_shared SHARED) + target_link_libraries(tethernet_shared PRIVATE tn_capi) set_target_properties( - tetherkit_shared - PROPERTIES OUTPUT_NAME tetherkit + tethernet_shared + PROPERTIES OUTPUT_NAME tethernet VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}" # Let the dependents load it by @rpath instead of baking in an @@ -56,15 +56,15 @@ if(TETHERKIT_BUILD_SHARED_LIB) # Export only the C ABI; every other symbol (including the C++ mangled # names) is hidden. The reason is documented in the allowlist file. - target_link_options(tetherkit_shared PRIVATE - "-Wl,-exported_symbols_list,${PROJECT_SOURCE_DIR}/cmake/tetherkit_exports.txt") + target_link_options(tethernet_shared PRIVATE + "-Wl,-exported_symbols_list,${PROJECT_SOURCE_DIR}/cmake/tethernet_exports.txt") - install(TARGETS tetherkit_shared LIBRARY DESTINATION lib) + install(TARGETS tethernet_shared LIBRARY DESTINATION lib) endif() # The C ABI header is installed for the outside: once the Homebrew formula is # installed, other projects can bind to it too. -install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/tetherkit/capi" - DESTINATION include/tetherkit +install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/tethernet/capi" + DESTINATION include/tethernet FILES_MATCHING PATTERN "*.h") diff --git a/src/capi/capi_support.cc b/src/capi/capi_support.cc index 0ff3749..2d056b5 100644 --- a/src/capi/capi_support.cc +++ b/src/capi/capi_support.cc @@ -3,7 +3,7 @@ #include #include -namespace tetherkit::capi { +namespace tethernet::capi { std::int64_t WallNanos() noexcept { ::timespec now{}; @@ -14,7 +14,7 @@ std::int64_t WallNanos() noexcept { void ReconcileDeviceStrings(std::vector& memory, std::span present, - std::span infos) { + std::span infos) { // Evict the devices that are no longer on the bus first -- the order // matters: eviction runs before the back-fill, so a "different device" // plugged back into the same address never picks up the previous one's @@ -23,7 +23,7 @@ void ReconcileDeviceStrings(std::vector& memory, return std::ranges::find(present, entry.identity) == present.end(); }); - for (tk_device_info_t& info : infos) { + for (tn_device_info_t& info : infos) { const bool read_succeeded = info.manufacturer[0] != '\0' || info.product[0] != '\0' || info.serial[0] != '\0'; const DeviceIdentity identity = IdentityOf(info); @@ -56,7 +56,7 @@ void ReconcileDeviceStrings(std::vector& memory, bool IsValidFethName(std::string_view name) noexcept { constexpr std::string_view kPrefix = "feth"; // The name also has to fit into the kernel's IFNAMSIZ buffer. - if (name.size() <= kPrefix.size() || name.size() >= TK_INTERFACE_NAME_CAPACITY) { + if (name.size() <= kPrefix.size() || name.size() >= TN_INTERFACE_NAME_CAPACITY) { return false; } if (!name.starts_with(kPrefix)) { @@ -66,4 +66,4 @@ bool IsValidFethName(std::string_view name) noexcept { [](char character) { return character >= '0' && character <= '9'; }); } -} // namespace tetherkit::capi +} // namespace tethernet::capi diff --git a/src/capi/capi_support.h b/src/capi/capi_support.h index a69b266..6db40e1 100644 --- a/src/capi/capi_support.h +++ b/src/capi/capi_support.h @@ -3,9 +3,12 @@ // // The three recurring chores of the "C++ world <-> C world" bridge live here: // copying a std::string_view into a fixed-size buffer, translating a -// tetherkit::Error into a tk_error_t, and reading the wall clock. +// tethernet::Error into a tn_error_t, and reading the wall clock. #pragma once +#include "tethernet/capi/tethernet_c.h" +#include "tethernet/common/error.h" + #include #include #include @@ -13,10 +16,7 @@ #include #include -#include "tetherkit/capi/tetherkit_c.h" -#include "tetherkit/common/error.h" - -namespace tetherkit::capi { +namespace tethernet::capi { /// Copies text into a fixed-size buffer, guarantees NUL termination, and /// truncates when it is too long. @@ -54,7 +54,7 @@ inline void CopyText(char (&destination)[N], std::string_view text) noexcept { /// Translates a C++-side error into the C struct. `out_error` may be nullptr /// (the caller does not care about the reason). -inline void FillError(tk_error_t* out_error, const Error& error) noexcept { +inline void FillError(tn_error_t* out_error, const Error& error) noexcept { if (out_error == nullptr) { return; } @@ -67,22 +67,22 @@ inline void FillError(tk_error_t* out_error, const Error& error) noexcept { } /// Fills in a purely logical error that carries no underlying error code. -inline void FillGenericError(tk_error_t* out_error, std::string_view message) noexcept { +inline void FillGenericError(tn_error_t* out_error, std::string_view message) noexcept { if (out_error == nullptr) { return; } - out_error->domain = TK_ERROR_DOMAIN_GENERIC; + out_error->domain = TN_ERROR_DOMAIN_GENERIC; out_error->code = 0; CopyText(out_error->message, message); } /// Clears the error struct to "no error". Called on the success path so the /// caller never reads a leftover from the previous call. -inline void ClearError(tk_error_t* out_error) noexcept { +inline void ClearError(tn_error_t* out_error) noexcept { if (out_error == nullptr) { return; } - out_error->domain = TK_ERROR_DOMAIN_GENERIC; + out_error->domain = TN_ERROR_DOMAIN_GENERIC; out_error->code = 0; out_error->message[0] = '\0'; } @@ -102,7 +102,7 @@ struct DeviceIdentity { }; /// Extracts the device identity from an enumeration result. -[[nodiscard]] inline DeviceIdentity IdentityOf(const tk_device_info_t& info) noexcept { +[[nodiscard]] inline DeviceIdentity IdentityOf(const tn_device_info_t& info) noexcept { return DeviceIdentity{.bus_number = info.bus_number, .device_address = info.device_address, .vendor_id = info.vendor_id, @@ -112,9 +112,9 @@ struct DeviceIdentity { /// The string descriptors **successfully read** from a device most recently. struct RememberedDeviceStrings { DeviceIdentity identity; - char manufacturer[TK_USB_STRING_CAPACITY]{}; - char product[TK_USB_STRING_CAPACITY]{}; - char serial[TK_USB_STRING_CAPACITY]{}; + char manufacturer[TN_USB_STRING_CAPACITY]{}; + char product[TN_USB_STRING_CAPACITY]{}; + char serial[TN_USB_STRING_CAPACITY]{}; }; /// Maintains the memory of string descriptors: remember what was read, and @@ -148,14 +148,14 @@ struct RememberedDeviceStrings { /// responsibility -- that is what makes it testable offline. void ReconcileDeviceStrings(std::vector& memory, std::span present, - std::span infos); + std::span infos); /// Wall clock time since the Unix epoch (nanoseconds). /// /// Events and logs use the wall clock rather than the monotonic clock: they /// are shown to people and have to line up with the system log in Console.app. /// Rate calculation uses the monotonic clock instead; see -/// tk_session_status.monotonic_nanos. +/// tn_session_status.monotonic_nanos. [[nodiscard]] std::int64_t WallNanos() noexcept; /// Validates the interface name: only `feth` is accepted. @@ -171,9 +171,9 @@ void ReconcileDeviceStrings(std::vector& memory, /// Installs the on-disk registry of feth interfaces (see orphan_cleanup.cc). /// Idempotent. /// -/// Called by tk_session_create -- only a session that really creates an +/// Called by tn_session_create -- only a session that really creates an /// interface needs to register, which keeps the root-free group of entry /// points (version, enumeration, preflight) free of side effects. void InstallInterfaceRegistry(); -} // namespace tetherkit::capi +} // namespace tethernet::capi diff --git a/src/capi/core_foundation_support.cc b/src/capi/core_foundation_support.cc index aaf6ccf..623e9fb 100644 --- a/src/capi/core_foundation_support.cc +++ b/src/capi/core_foundation_support.cc @@ -2,7 +2,7 @@ #include -namespace tetherkit::capi { +namespace tethernet::capi { ScopedCFRef MakeCFString(std::string_view text) { return ScopedCFRef{::CFStringCreateWithBytes( @@ -41,9 +41,9 @@ SCDynamicStoreRef SharedDynamicStore() { // No lock is needed -- since C++11 the initialization of a function-local // static is thread-safe (the magic static), so concurrent first calls let // only one thread actually run the initialization. - static SCDynamicStoreRef store = ::SCDynamicStoreCreate( - kCFAllocatorDefault, CFSTR("TetherKit"), /*callout=*/nullptr, /*context=*/nullptr); + static SCDynamicStoreRef store = ::SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("Tethernet"), + /*callout=*/nullptr, /*context=*/nullptr); return store; } -} // namespace tetherkit::capi +} // namespace tethernet::capi diff --git a/src/capi/core_foundation_support.h b/src/capi/core_foundation_support.h index 73461f9..b4e8d31 100644 --- a/src/capi/core_foundation_support.h +++ b/src/capi/core_foundation_support.h @@ -13,7 +13,7 @@ #include #include -namespace tetherkit::capi { +namespace tethernet::capi { /// Owns a CF object and CFReleases it on destruction. /// @@ -79,4 +79,4 @@ class ScopedCFRef { /// the helper are cleaned up automatically when it exits, leaving no litter. [[nodiscard]] SCDynamicStoreRef SharedDynamicStore(); -} // namespace tetherkit::capi +} // namespace tethernet::capi diff --git a/src/capi/environment.cc b/src/capi/environment.cc index 630f6cb..25e1ab7 100644 --- a/src/capi/environment.cc +++ b/src/capi/environment.cc @@ -2,8 +2,15 @@ // enumeration. // // The GUI itself (uid 501) calls only this group; the session and the -// interface configuration that need root are handed to tetherkit-helper. This +// interface configuration that need root are handed to tethernet-helper. This // split is one of the core conclusions of docs/GUI-SPIKE.md. +#include "tethernet/capi/tethernet_c.h" +#include "tethernet/common/i18n.h" +#include "tethernet/net/feth_device.h" +#include "tethernet/usb/context.h" +#include "tethernet/usb/device.h" +#include "tethernet/version.h" + #include #include @@ -18,18 +25,12 @@ #include #include "capi_support.h" -#include "tetherkit/capi/tetherkit_c.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/net/feth_device.h" -#include "tetherkit/usb/context.h" -#include "tetherkit/usb/device.h" -#include "tetherkit/version.h" namespace { -using tetherkit::capi::ClearError; -using tetherkit::capi::CopyText; -using tetherkit::capi::FillError; +using tethernet::capi::ClearError; +using tethernet::capi::CopyText; +using tethernet::capi::FillError; /// Reads a USB string descriptor into a fixed-size buffer. An index of 0 means /// the device provides no such string. @@ -44,7 +45,7 @@ void ReadStringDescriptor(::libusb_device_handle* handle, std::uint8_t index, ch if (index == 0) { return; } - std::array buffer{}; + std::array buffer{}; const int length = ::libusb_get_string_descriptor_ascii( handle, index, buffer.data(), static_cast(buffer.size())); if (length <= 0) { @@ -59,13 +60,13 @@ void ReadStringDescriptor(::libusb_device_handle* handle, std::uint8_t index, ch /// /// Why "best effort": reading a string descriptor requires libusb_open first, /// and the device may already be held exclusively by another process on this -/// machine (a running tetherkit-helper, say), in which case the darwin backend +/// machine (a running tethernet-helper, say), in which case the darwin backend /// returns LIBUSB_ERROR_ACCESS. That is not an error, it is just an /// unavailable name -- at that point the string memory back-fills the value -/// read last time (see the end of tk_list_devices), and only when there is no +/// read last time (see the end of tn_list_devices), and only when there is no /// memory either does it fall back to VID:PID. It must never make the whole /// enumeration fail. -void TryReadStrings(::libusb_context* context, tk_device_info_t& info) noexcept { +void TryReadStrings(::libusb_context* context, tn_device_info_t& info) noexcept { ::libusb_device** list = nullptr; const ssize_t count = ::libusb_get_device_list(context, &list); if (count < 0) { @@ -120,13 +121,13 @@ void TryReadStrings(::libusb_context* context, tk_device_info_t& info) noexcept /// /// On initialization failure this returns nullptr and fills in the error, and /// it does **not** cache the failure -- the next call retries. -tetherkit::usb::Context* SharedEnumerationContext(tk_error_t* out_error) { +tethernet::usb::Context* SharedEnumerationContext(tn_error_t* out_error) { static std::mutex mutex; - static std::unique_ptr context; + static std::unique_ptr context; const std::lock_guard guard(mutex); if (context == nullptr) { - auto created = tetherkit::usb::Context::Create(); + auto created = tethernet::usb::Context::Create(); if (!created) { FillError(out_error, created.error()); return nullptr; @@ -145,7 +146,7 @@ tetherkit::usb::Context* SharedEnumerationContext(tk_error_t* out_error) { /// ReconcileDeviceStrings. struct StringMemory { std::mutex mutex; - std::vector entries; + std::vector entries; }; StringMemory& SharedStringMemory() { @@ -153,9 +154,9 @@ StringMemory& SharedStringMemory() { return memory; } -void FillDeviceInfo(const tetherkit::usb::DeviceCandidate& candidate, - tk_device_info_t& info) noexcept { - info = tk_device_info_t{}; +void FillDeviceInfo(const tethernet::usb::DeviceCandidate& candidate, + tn_device_info_t& info) noexcept { + info = tn_device_info_t{}; info.vendor_id = candidate.vendor_id; info.product_id = candidate.product_id; info.bus_number = candidate.bus_number; @@ -173,105 +174,104 @@ void FillDeviceInfo(const tetherkit::usb::DeviceCandidate& candidate, namespace { -/// tk_language_t > C++ Language. Unknown values return nullopt, which makes +/// tn_language_t > C++ Language. Unknown values return nullopt, which makes /// the set call a no-op: a caller still passing a removed language id (the /// old Chinese value 1) keeps working and simply stays in English. -[[nodiscard]] std::optional ToLanguage(std::int32_t value) noexcept { +[[nodiscard]] std::optional ToLanguage(std::int32_t value) noexcept { switch (value) { - case TK_LANGUAGE_ENGLISH: - return tetherkit::Language::kEnglish; + case TN_LANGUAGE_ENGLISH: + return tethernet::Language::kEnglish; default: return std::nullopt; } } -[[nodiscard]] std::int32_t FromLanguage(tetherkit::Language /*language*/) noexcept { +[[nodiscard]] std::int32_t FromLanguage(tethernet::Language /*language*/) noexcept { // English is the only language shipped today; the parameter is kept so the // signature survives a second language unchanged. - return TK_LANGUAGE_ENGLISH; + return TN_LANGUAGE_ENGLISH; } } // namespace -void tk_set_language(int32_t language) { - if (const std::optional parsed = ToLanguage(language); - parsed.has_value()) { - tetherkit::SetLanguage(*parsed); +void tn_set_language(int32_t language) { + if (const std::optional parsed = ToLanguage(language); parsed.has_value()) { + tethernet::SetLanguage(*parsed); } } -int32_t tk_get_language(void) { - return FromLanguage(tetherkit::GetLanguage()); +int32_t tn_get_language(void) { + return FromLanguage(tethernet::GetLanguage()); } -int32_t tk_detect_system_language(void) { - return FromLanguage(tetherkit::DetectLanguageFromEnvironment()); +int32_t tn_detect_system_language(void) { + return FromLanguage(tethernet::DetectLanguageFromEnvironment()); } -void tk_version(tk_version_info_t* out_version) { +void tn_version(tn_version_info_t* out_version) { if (out_version == nullptr) { return; } - *out_version = tk_version_info_t{}; - const tetherkit::Version version = tetherkit::GetVersion(); + *out_version = tn_version_info_t{}; + const tethernet::Version version = tethernet::GetVersion(); out_version->major = version.major; out_version->minor = version.minor; out_version->patch = version.patch; - CopyText(out_version->text, tetherkit::GetVersionString()); - CopyText(out_version->build, tetherkit::GetBuildDescription()); - CopyText(out_version->libusb, tetherkit::usb::Context::VersionString()); + CopyText(out_version->text, tethernet::GetVersionString()); + CopyText(out_version->build, tethernet::GetBuildDescription()); + CopyText(out_version->libusb, tethernet::usb::Context::VersionString()); } -tk_result_t tk_check_environment(tk_environment_t* out_environment) { +tn_result_t tn_check_environment(tn_environment_t* out_environment) { if (out_environment == nullptr) { - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } - *out_environment = tk_environment_t{}; + *out_environment = tn_environment_t{}; - out_environment->is_root = tetherkit::net::IsRunningAsRoot(); + out_environment->is_root = tethernet::net::IsRunningAsRoot(); // These sysctls are a **creation-time snapshot** of feth; changing them // after the interface exists has no effect, so they must be queried up // front. When they do not qualify, the reason is handed to the GUI as-is -- // the user has to see which switch is turned on to know what to change. - if (const auto status = tetherkit::net::VerifyFethSysctls(); status) { + if (const auto status = tethernet::net::VerifyFethSysctls(); status) { out_environment->sysctls_ok = true; } else { out_environment->sysctls_ok = false; CopyText(out_environment->sysctl_detail, status.error().ToString()); } - if (const auto max_mtu = tetherkit::net::QueryFethMaxMtu(); max_mtu) { + if (const auto max_mtu = tethernet::net::QueryFethMaxMtu(); max_mtu) { out_environment->feth_max_mtu = *max_mtu; } // Deliberately always returns success: "the environment does not qualify" is // a **result** to be shown to the user, not a call failure. - return TK_OK; + return TN_OK; } -tk_result_t tk_list_devices(tk_device_info_t* out_devices, size_t capacity, size_t* out_count, - bool read_strings, tk_error_t* out_error) { +tn_result_t tn_list_devices(tn_device_info_t* out_devices, size_t capacity, size_t* out_count, + bool read_strings, tn_error_t* out_error) { if (out_count == nullptr) { - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } ClearError(out_error); *out_count = 0; - tetherkit::usb::Context* context = SharedEnumerationContext(out_error); + tethernet::usb::Context* context = SharedEnumerationContext(out_error); if (context == nullptr) { - return TK_ERR_FAILED; + return TN_ERR_FAILED; } - auto candidates = tetherkit::usb::FindRndisDevices(*context, {}); + auto candidates = tethernet::usb::FindRndisDevices(*context, {}); if (!candidates) { FillError(out_error, candidates.error()); - return TK_ERR_FAILED; + return TN_ERR_FAILED; } *out_count = candidates->size(); if (out_devices == nullptr) { - return TK_OK; + return TN_OK; } const std::size_t writable = std::min(capacity, candidates->size()); @@ -288,9 +288,9 @@ tk_result_t tk_list_devices(tk_device_info_t* out_devices, size_t capacity, size // writable prefix: when the capacity is too small the devices after that // are still present, and their memory must not be dropped as "unplugged". { - std::vector present; + std::vector present; present.reserve(candidates->size()); - for (const tetherkit::usb::DeviceCandidate& candidate : *candidates) { + for (const tethernet::usb::DeviceCandidate& candidate : *candidates) { present.push_back({.bus_number = candidate.bus_number, .device_address = candidate.device_address, .vendor_id = candidate.vendor_id, @@ -298,7 +298,7 @@ tk_result_t tk_list_devices(tk_device_info_t* out_devices, size_t capacity, size } StringMemory& memory = SharedStringMemory(); const std::lock_guard guard(memory.mutex); - tetherkit::capi::ReconcileDeviceStrings(memory.entries, present, {out_devices, writable}); + tethernet::capi::ReconcileDeviceStrings(memory.entries, present, {out_devices, writable}); } - return TK_OK; + return TN_OK; } diff --git a/src/capi/log_ring.cc b/src/capi/log_ring.cc index 40b8a74..da1c981 100644 --- a/src/capi/log_ring.cc +++ b/src/capi/log_ring.cc @@ -6,6 +6,9 @@ // context means both marshalling across threads and the landmine of "the // closure logs one more line -> self-deadlock". Buffering plus polling kills // both problems at once. +#include "tethernet/capi/tethernet_c.h" +#include "tethernet/common/logging.h" + #include #include #include @@ -13,13 +16,11 @@ #include #include "capi_support.h" -#include "tetherkit/capi/tetherkit_c.h" -#include "tetherkit/common/logging.h" namespace { -using tetherkit::capi::CopyText; -using tetherkit::capi::WallNanos; +using tethernet::capi::CopyText; +using tethernet::capi::WallNanos; /// Capacity of the ring buffer. /// @@ -33,8 +34,7 @@ constexpr std::size_t kLogRingCapacity = 256; /// when the host is stuck, the newest scene is more useful than the beginning. class LogRing { public: - void Push(tk_log_level_t level, std::string_view thread_name, - std::string_view message) noexcept { + void Push(tn_log_level_t level, std::string_view thread_name, std::string_view message) noexcept { const std::lock_guard guard(mutex_); if (count_ == kLogRingCapacity) { // Overwrite the oldest entry: advance the read pointer, which is @@ -43,7 +43,7 @@ class LogRing { --count_; ++dropped_; } - tk_log_record_t& record = records_[(head_ + count_) % kLogRingCapacity]; + tn_log_record_t& record = records_[(head_ + count_) % kLogRingCapacity]; record.level = static_cast(level); record.wall_nanos = WallNanos(); CopyText(record.thread, thread_name); @@ -51,7 +51,7 @@ class LogRing { ++count_; } - std::size_t Drain(tk_log_record_t* out_records, std::size_t capacity, + std::size_t Drain(tn_log_record_t* out_records, std::size_t capacity, std::uint64_t* out_dropped) noexcept { const std::lock_guard guard(mutex_); if (out_dropped != nullptr) { @@ -79,7 +79,7 @@ class LogRing { private: std::mutex mutex_; - std::array records_{}; + std::array records_{}; /// Index of the next record to read. std::size_t head_ = 0; std::size_t count_ = 0; @@ -94,28 +94,28 @@ LogRing& Ring() noexcept { return ring; } -/// Installs the callback for tetherkit::SetLogSink. +/// Installs the callback for tethernet::SetLogSink. /// /// It runs inside the log mutex, so the implementation may only "copy plus /// take one lock of its own", and must never log again (that would deadlock /// on itself). -void SinkTrampoline(tetherkit::LogLevel level, std::string_view thread_name, +void SinkTrampoline(tethernet::LogLevel level, std::string_view thread_name, std::string_view message, void* /*user*/) noexcept { - Ring().Push(static_cast(level), thread_name, message); + Ring().Push(static_cast(level), thread_name, message); } } // namespace -void tk_set_log_level(int32_t level) { - if (level < TK_LOG_TRACE || level > TK_LOG_OFF) { +void tn_set_log_level(int32_t level) { + if (level < TN_LOG_TRACE || level > TN_LOG_OFF) { return; } - tetherkit::SetLogLevel(static_cast(level)); + tethernet::SetLogLevel(static_cast(level)); } -void tk_enable_log_capture(bool enabled) { +void tn_enable_log_capture(bool enabled) { if (enabled) { - tetherkit::SetLogSink(&SinkTrampoline, nullptr); + tethernet::SetLogSink(&SinkTrampoline, nullptr); return; } @@ -130,11 +130,11 @@ void tk_enable_log_capture(bool enabled) { // SetLogSink -- SetLogSink takes the log mutex, and the log sink path is // "log lock -> LogRing lock", so taking them in the reverse order would // invert the lock order. - tetherkit::SetLogSink(nullptr, nullptr); + tethernet::SetLogSink(nullptr, nullptr); Ring().Clear(); } -size_t tk_drain_logs(tk_log_record_t* out_records, size_t capacity, uint64_t* out_dropped) { +size_t tn_drain_logs(tn_log_record_t* out_records, size_t capacity, uint64_t* out_dropped) { if (out_dropped != nullptr) { *out_dropped = 0; } diff --git a/src/capi/net_config.cc b/src/capi/net_config.cc index e957a36..ba5cce6 100644 --- a/src/capi/net_config.cc +++ b/src/capi/net_config.cc @@ -24,17 +24,21 @@ // options). MANUAL mode has no DNS source, so all we can do is add a DNS // key to the service it created. // That move is **not verified on a real device**, so we never promise -// success upward: tk_net_query always reads back the resolver that is +// success upward: tn_net_query always reads back the resolver that is // actually in effect in the system, and the GUI shows the read-back result, // not the value we pushed down. -#include -#include +#include "tethernet/capi/tethernet_c.h" +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" + #include #include #include #include +#include #include +#include #include #include #include @@ -48,29 +52,26 @@ #include "capi_support.h" #include "core_foundation_support.h" #include "process_runner.h" -#include "tetherkit/capi/tetherkit_c.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" namespace { -using tetherkit::Error; -using tetherkit::Msg; -using tetherkit::Result; -using tetherkit::Status; -using tetherkit::Text; -using tetherkit::Tr; -using tetherkit::capi::ClearError; -using tetherkit::capi::CopyText; -using tetherkit::capi::CopyToStdString; -using tetherkit::capi::FillError; -using tetherkit::capi::FillGenericError; -using tetherkit::capi::IsValidFethName; -using tetherkit::capi::MakeCFString; -using tetherkit::capi::ProcessResult; -using tetherkit::capi::RunTool; -using tetherkit::capi::ScopedCFRef; -using tetherkit::capi::SharedDynamicStore; +using tethernet::Error; +using tethernet::Msg; +using tethernet::Result; +using tethernet::Status; +using tethernet::Text; +using tethernet::Tr; +using tethernet::capi::ClearError; +using tethernet::capi::CopyText; +using tethernet::capi::CopyToStdString; +using tethernet::capi::FillError; +using tethernet::capi::FillGenericError; +using tethernet::capi::IsValidFethName; +using tethernet::capi::MakeCFString; +using tethernet::capi::ProcessResult; +using tethernet::capi::RunTool; +using tethernet::capi::ScopedCFRef; +using tethernet::capi::SharedDynamicStore; constexpr std::string_view kIpconfigPath = "/usr/sbin/ipconfig"; constexpr std::string_view kRoutePath = "/sbin/route"; @@ -89,7 +90,7 @@ constexpr std::chrono::milliseconds kDhcpPollInterval{250}; // --------------------------------------------------------------------------- [[nodiscard]] bool IsValidIpv4(std::string_view text) noexcept { - if (text.empty() || text.size() >= TK_ADDRESS_CAPACITY) { + if (text.empty() || text.size() >= TN_ADDRESS_CAPACITY) { return false; } const std::string owned{text}; @@ -105,7 +106,7 @@ constexpr std::chrono::milliseconds kDhcpPollInterval{250}; if (!IsValidFethName(interface_name)) { return std::unexpected(Error::Generic(Tr(Msg::kCapiInterfaceNotOurs, interface_name))); } - return tetherkit::Ok(); + return tethernet::Ok(); } // --------------------------------------------------------------------------- @@ -121,7 +122,7 @@ constexpr std::chrono::milliseconds kDhcpPollInterval{250}; [[nodiscard]] Status RunOrFail(std::string_view executable, const std::vector& arguments, std::string_view what) { - TETHERKIT_ASSIGN_OR_RETURN(const ProcessResult result, RunTool(executable, arguments)); + TETHERNET_ASSIGN_OR_RETURN(const ProcessResult result, RunTool(executable, arguments)); if (!result.Succeeded()) { std::string detail{result.output}; // Tool output usually carries a trailing newline; that reads badly when @@ -133,7 +134,7 @@ constexpr std::chrono::milliseconds kDhcpPollInterval{250}; Tr(Msg::kCapiCommandFailed, what, result.exit_code, detail.empty() ? std::string{Text(Msg::kCapiCommandNoOutput)} : detail))); } - return tetherkit::Ok(); + return tethernet::Ok(); } /// Reads the interface's current IPv4 address; returns std::nullopt when there @@ -276,7 +277,7 @@ constexpr std::chrono::milliseconds kDhcpPollInterval{250}; /// properly registered** service gets adopted". A wrong bet causes no damage /// -- the DNS simply does not take effect -- so a failure is only logged and /// never fails the whole static-IP configuration. The real result is reported -/// by the tk_net_query read-back. +/// by the tn_net_query read-back. /// /// One more thing to note: a value in SCDynamicStore is **owned by the session /// that set it**, and the value disappears when that session is released. @@ -308,7 +309,7 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, const ScopedCFRef key = MakeCFString(std::format("State:/Network/Service/{}/DNS", service_id)); if (::SCDynamicStoreSetValue(store, key.Get(), payload.Get()) == 0) { - TETHERKIT_WARN_TR(Msg::kCapiDnsPublishFailed, service_id); + TETHERNET_WARN_TR(Msg::kCapiDnsPublishFailed, service_id); } } @@ -337,7 +338,7 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, if (const auto status = RunOrFail(kRoutePath, add_arguments, Text(Msg::kCapiWhatAddScopedRoute)); status) { - return tetherkit::Ok(); + return tethernet::Ok(); } // When an identical route already exists, add fails with EEXIST -- retry // once with change. @@ -352,7 +353,7 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, const std::vector arguments{"-n", "change", "-inet", "default", std::string{router}}; if (const auto status = RunOrFail(kRoutePath, arguments, Text(Msg::kCapiWhatSwitchGlobalRoute)); status) { - return tetherkit::Ok(); + return tethernet::Ok(); } // The system may have no global default route at all (nothing is connected), // in which case change fails and add is the right call. That is exactly the @@ -376,9 +377,8 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, } [[nodiscard]] Status ApplyDhcp(std::string_view interface_name, bool set_default_route) { - TETHERKIT_RETURN_IF_ERROR( - RunOrFail(kIpconfigPath, {"set", std::string{interface_name}, "DHCP"}, - Text(Msg::kCapiWhatStartDhcp))); + TETHERNET_RETURN_IF_ERROR(RunOrFail(kIpconfigPath, {"set", std::string{interface_name}, "DHCP"}, + Text(Msg::kCapiWhatStartDhcp))); // Wait for the lease. IPConfiguration automatically does four things: get // the lease, set up the scoped DNS, install the scoped default route, and @@ -388,7 +388,7 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, } if (!set_default_route) { - return tetherkit::Ok(); + return tethernet::Ok(); } const std::optional router = QueryDhcpRouter(interface_name); if (!router.has_value()) { @@ -397,7 +397,7 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, return PromoteToGlobalDefaultRoute(*router); } -[[nodiscard]] Status ApplyManual(std::string_view interface_name, const tk_ip_config_t& config) { +[[nodiscard]] Status ApplyManual(std::string_view interface_name, const tn_ip_config_t& config) { if (!IsValidIpv4(config.address)) { return std::unexpected( Error::Generic(Tr(Msg::kCapiInvalidAddress, config.address))); @@ -416,7 +416,7 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, } std::vector dns_servers; - for (std::int32_t i = 0; i < config.dns_count && i < TK_DNS_MAX; ++i) { + for (std::int32_t i = 0; i < config.dns_count && i < TN_DNS_MAX; ++i) { const std::string_view server{config.dns[i]}; if (server.empty()) { continue; @@ -430,15 +430,14 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, // The address still goes out through IPConfiguration; see the file header // for the reason. - TETHERKIT_RETURN_IF_ERROR(RunOrFail( - kIpconfigPath, - {"set", std::string{interface_name}, "MANUAL", config.address, config.netmask}, + TETHERNET_RETURN_IF_ERROR(RunOrFail( + kIpconfigPath, {"set", std::string{interface_name}, "MANUAL", config.address, config.netmask}, Text(Msg::kCapiWhatApplyManual))); if (!router.empty()) { - TETHERKIT_RETURN_IF_ERROR(InstallScopedDefaultRoute(interface_name, router)); + TETHERNET_RETURN_IF_ERROR(InstallScopedDefaultRoute(interface_name, router)); if (config.set_default_route) { - TETHERKIT_RETURN_IF_ERROR(PromoteToGlobalDefaultRoute(router)); + TETHERNET_RETURN_IF_ERROR(PromoteToGlobalDefaultRoute(router)); } } @@ -449,11 +448,11 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, service_id.has_value()) { TryPublishDns(store, *service_id, dns_servers); } else { - TETHERKIT_WARN_TR(Msg::kCapiNoServiceForDns, interface_name); + TETHERNET_WARN_TR(Msg::kCapiNoServiceForDns, interface_name); } } } - return tetherkit::Ok(); + return tethernet::Ok(); } [[nodiscard]] Status ApplyNone(std::string_view interface_name) { @@ -463,15 +462,15 @@ void TryPublishDns(SCDynamicStoreRef store, std::string_view service_id, } // namespace -void tk_ip_config_init(tk_ip_config_t* out_config) { +void tn_ip_config_init(tn_ip_config_t* out_config) { if (out_config == nullptr) { return; } - *out_config = tk_ip_config_t{}; + *out_config = tn_ip_config_t{}; // Default is DHCP: an RNDIS device almost always ships with its own DHCP // server, and for the vast majority of users that is the only option they // need. - out_config->mode = TK_IP_MODE_DHCP; + out_config->mode = TN_IP_MODE_DHCP; // Default: do **not** grab the global default route. It is only needed when // a higher-priority connected service also exists, and the typical // USB-tethering scenario is precisely that no other network is available -- @@ -479,54 +478,54 @@ void tk_ip_config_init(tk_ip_config_t* out_config) { out_config->set_default_route = false; } -tk_result_t tk_net_apply(const char* interface_name, const tk_ip_config_t* config, - tk_error_t* out_error) { +tn_result_t tn_net_apply(const char* interface_name, const tn_ip_config_t* config, + tn_error_t* out_error) { ClearError(out_error); if (config == nullptr) { FillGenericError(out_error, Tr(Msg::kCapiApplyConfigNull)); - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } if (const auto status = ValidateInterface(interface_name); !status) { FillError(out_error, status.error()); - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } if (::geteuid() != 0) { FillGenericError(out_error, Tr(Msg::kCapiApplyNeedsRoot)); - return TK_ERR_PERMISSION; + return TN_ERR_PERMISSION; } - Status status = tetherkit::Ok(); + Status status = tethernet::Ok(); switch (config->mode) { - case TK_IP_MODE_DHCP: + case TN_IP_MODE_DHCP: status = ApplyDhcp(interface_name, config->set_default_route); break; - case TK_IP_MODE_MANUAL: + case TN_IP_MODE_MANUAL: status = ApplyManual(interface_name, *config); break; - case TK_IP_MODE_NONE: + case TN_IP_MODE_NONE: status = ApplyNone(interface_name); break; default: FillGenericError(out_error, Tr(Msg::kCapiUnknownIpMode, config->mode)); - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } if (!status) { FillError(out_error, status.error()); - return TK_ERR_FAILED; + return TN_ERR_FAILED; } - return TK_OK; + return TN_OK; } -tk_result_t tk_net_clear(const char* interface_name, tk_error_t* out_error) { +tn_result_t tn_net_clear(const char* interface_name, tn_error_t* out_error) { ClearError(out_error); if (const auto status = ValidateInterface(interface_name); !status) { FillError(out_error, status.error()); - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } if (::geteuid() != 0) { FillGenericError(out_error, Tr(Msg::kCapiClearNeedsRoot)); - return TK_ERR_PERMISSION; + return TN_ERR_PERMISSION; } // Remove the DNS key we wrote ourselves (if any) first, then let @@ -543,22 +542,22 @@ tk_result_t tk_net_clear(const char* interface_name, tk_error_t* out_error) { if (const auto status = ApplyNone(interface_name); !status) { FillError(out_error, status.error()); - return TK_ERR_FAILED; + return TN_ERR_FAILED; } - return TK_OK; + return TN_OK; } -tk_result_t tk_net_query(const char* interface_name, tk_net_state_t* out_state, - tk_error_t* out_error) { +tn_result_t tn_net_query(const char* interface_name, tn_net_state_t* out_state, + tn_error_t* out_error) { ClearError(out_error); if (out_state == nullptr) { - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } if (const auto status = ValidateInterface(interface_name); !status) { FillError(out_error, status.error()); - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } - *out_state = tk_net_state_t{}; + *out_state = tn_net_state_t{}; // ---- Address and netmask: the real state in the kernel ---- if (const std::optional address = QueryAddress(interface_name, SIOCGIFADDR); @@ -576,7 +575,7 @@ tk_result_t tk_net_query(const char* interface_name, tk_net_state_t* out_state, if (store == nullptr) { // Not being able to reach the dynamic store is not a query failure -- the // address half is already there, so return as usual. - return TK_OK; + return TN_OK; } const std::optional service_id = FindServiceId(store, interface_name); @@ -601,14 +600,14 @@ tk_result_t tk_net_query(const char* interface_name, tk_net_state_t* out_state, const ScopedCFRef dns = CopyServiceEntry(store, *service_id, "DNS"); const std::vector servers = StringArrayField(dns.Get(), CFSTR("ServerAddresses")); for (const std::string& server : servers) { - if (out_state->dns_count >= TK_DNS_MAX) { + if (out_state->dns_count >= TN_DNS_MAX) { break; } - CopyText(out_state->dns[out_state->dns_count], TK_ADDRESS_CAPACITY, server); + CopyText(out_state->dns[out_state->dns_count], TN_ADDRESS_CAPACITY, server); ++out_state->dns_count; } } out_state->is_primary_default_route = PrimaryInterface(store) == interface_name; - return TK_OK; + return TN_OK; } diff --git a/src/capi/orphan_cleanup.cc b/src/capi/orphan_cleanup.cc index cadf977..a813af5 100644 --- a/src/capi/orphan_cleanup.cc +++ b/src/capi/orphan_cleanup.cc @@ -15,6 +15,11 @@ // feth is a shared facility; other programs (or the user's own ifconfig // invocations) may be using it too. Only destroying the ones we registered // ourselves avoids collateral damage. +#include "tethernet/capi/tethernet_c.h" +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" +#include "tethernet/net/feth_device.h" + #include #include @@ -26,18 +31,14 @@ #include #include "capi_support.h" -#include "tetherkit/capi/tetherkit_c.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" -#include "tetherkit/net/feth_device.h" namespace { -using tetherkit::Msg; -using tetherkit::Tr; -using tetherkit::capi::ClearError; -using tetherkit::capi::FillGenericError; -using tetherkit::capi::IsValidFethName; +using tethernet::Msg; +using tethernet::Tr; +using tethernet::capi::ClearError; +using tethernet::capi::FillGenericError; +using tethernet::capi::IsValidFethName; /// Path of the registry file. /// @@ -49,7 +50,7 @@ using tetherkit::capi::IsValidFethName; /// /// A reboot empties /var/run, and a reboot also takes every feth with it -- /// the two lifetimes coincide, so stale entries need no extra handling. -constexpr const char* kRegistryPath = "/var/run/tetherkit-interfaces"; +constexpr const char* kRegistryPath = "/var/run/tethernet-interfaces"; std::mutex& RegistryMutex() { static std::mutex mutex; @@ -120,22 +121,22 @@ void OnInterfaceChanged(std::string_view name, bool created) noexcept { } // namespace -namespace tetherkit::capi { +namespace tethernet::capi { void InstallInterfaceRegistry() { net::SetInterfaceRegistry(&OnInterfaceChanged); } -} // namespace tetherkit::capi +} // namespace tethernet::capi -tk_result_t tk_cleanup_orphan_interfaces(size_t* out_removed, tk_error_t* out_error) { +tn_result_t tn_cleanup_orphan_interfaces(size_t* out_removed, tn_error_t* out_error) { ClearError(out_error); if (out_removed != nullptr) { *out_removed = 0; } if (::geteuid() != 0) { FillGenericError(out_error, Tr(Msg::kCapiCleanupNeedsRoot)); - return TK_ERR_PERMISSION; + return TN_ERR_PERMISSION; } // The destroy loop must **never** hold RegistryMutex: a successful @@ -150,7 +151,7 @@ tk_result_t tk_cleanup_orphan_interfaces(size_t* out_removed, tk_error_t* out_er names = ReadRegistry(); } if (names.empty()) { - return TK_OK; + return TN_OK; } std::size_t removed = 0; @@ -159,11 +160,11 @@ tk_result_t tk_cleanup_orphan_interfaces(size_t* out_removed, tk_error_t* out_er // longer exists (the system rebooted, say), which is exactly what we // wanted. A genuine failure is only logged and does not stop the // remaining entries. - if (const auto status = tetherkit::net::DestroyInterfaceByName(name); status) { + if (const auto status = tethernet::net::DestroyInterfaceByName(name); status) { ++removed; continue; } - TETHERKIT_DEBUG_TR(Msg::kCapiOrphanAlreadyGone, name); + TETHERNET_DEBUG_TR(Msg::kCapiOrphanAlreadyGone, name); } // Finalize: the ones that could be destroyed were already removed line by @@ -178,5 +179,5 @@ tk_result_t tk_cleanup_orphan_interfaces(size_t* out_removed, tk_error_t* out_er if (out_removed != nullptr) { *out_removed = removed; } - return TK_OK; + return TN_OK; } diff --git a/src/capi/process_runner.cc b/src/capi/process_runner.cc index d2e5536..8edfd38 100644 --- a/src/capi/process_runner.cc +++ b/src/capi/process_runner.cc @@ -1,6 +1,7 @@ -#include "tetherkit/common/i18n.h" #include "process_runner.h" +#include "tethernet/common/i18n.h" + #include #include #include @@ -12,7 +13,7 @@ extern char** environ; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -namespace tetherkit::capi { +namespace tethernet::capi { namespace { /// Reads a pipe until the far end closes it. @@ -129,4 +130,4 @@ Result RunTool(std::string_view executable, return Spawn(executable, arguments); } -} // namespace tetherkit::capi +} // namespace tethernet::capi diff --git a/src/capi/process_runner.h b/src/capi/process_runner.h index d58660e..69fcac7 100644 --- a/src/capi/process_runner.h +++ b/src/capi/process_runner.h @@ -16,14 +16,14 @@ // reverted to bare ioctls just to save one fork. #pragma once +#include "tethernet/common/error.h" + #include #include #include #include -#include "tetherkit/common/error.h" - -namespace tetherkit::capi { +namespace tethernet::capi { struct ProcessResult { int exit_code = 0; @@ -56,4 +56,4 @@ struct ProcessResult { [[nodiscard]] Result RunTool(std::string_view executable, const std::vector& arguments); -} // namespace tetherkit::capi +} // namespace tethernet::capi diff --git a/src/capi/session.cc b/src/capi/session.cc index 59766f3..0e6c7ce 100644 --- a/src/capi/session.cc +++ b/src/capi/session.cc @@ -3,6 +3,13 @@ // This layer holds almost no logic; it does three things: POD config -> // RuntimeConfig, RuntimeSnapshot -> POD status, RuntimeEvent -> ring queue. // The real orchestration lives in core::Runtime. +#include "tethernet/capi/tethernet_c.h" +#include "tethernet/common/i18n.h" +#include "tethernet/common/time.h" +#include "tethernet/core/runtime.h" +#include "tethernet/net/feth_device.h" +#include "tethernet/rndis/state_machine.h" + #include #include #include @@ -15,21 +22,15 @@ #include #include "capi_support.h" -#include "tetherkit/capi/tetherkit_c.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/time.h" -#include "tetherkit/core/runtime.h" -#include "tetherkit/net/feth_device.h" -#include "tetherkit/rndis/state_machine.h" namespace { -using tetherkit::capi::ClearError; -using tetherkit::capi::CopyText; -using tetherkit::capi::FillError; -using tetherkit::capi::WallNanos; -using tetherkit::core::RunState; -using tetherkit::core::RuntimeEvent; +using tethernet::capi::ClearError; +using tethernet::capi::CopyText; +using tethernet::capi::FillError; +using tethernet::capi::WallNanos; +using tethernet::core::RunState; +using tethernet::core::RuntimeEvent; // --------------------------------------------------------------------------- // Enum value alignment check @@ -40,26 +41,26 @@ using tetherkit::core::RuntimeEvent; // the wrong status, and nothing reports an error), so it is nailed down at // compile time. // --------------------------------------------------------------------------- -static_assert(static_cast(RunState::kIdle) == TK_RUN_IDLE); -static_assert(static_cast(RunState::kStarting) == TK_RUN_STARTING); -static_assert(static_cast(RunState::kRunning) == TK_RUN_RUNNING); -static_assert(static_cast(RunState::kStopping) == TK_RUN_STOPPING); -static_assert(static_cast(RunState::kStopped) == TK_RUN_STOPPED); -static_assert(static_cast(RunState::kFailed) == TK_RUN_FAILED); - -static_assert(static_cast(tetherkit::rndis::State::kUninitialized) == TK_RNDIS_UNINITIALIZED); -static_assert(static_cast(tetherkit::rndis::State::kInitializing) == TK_RNDIS_INITIALIZING); -static_assert(static_cast(tetherkit::rndis::State::kInitialized) == TK_RNDIS_INITIALIZED); -static_assert(static_cast(tetherkit::rndis::State::kDataInitialized) == - TK_RNDIS_DATA_INITIALIZED); -static_assert(static_cast(tetherkit::rndis::State::kHalting) == TK_RNDIS_HALTING); - -static_assert(static_cast(RuntimeEvent::Kind::kRndisState) == TK_EVENT_RNDIS_STATE); -static_assert(static_cast(RuntimeEvent::Kind::kNegotiated) == TK_EVENT_NEGOTIATED); -static_assert(static_cast(RuntimeEvent::Kind::kLink) == TK_EVENT_LINK); -static_assert(static_cast(RuntimeEvent::Kind::kDeviceReset) == TK_EVENT_DEVICE_RESET); -static_assert(static_cast(RuntimeEvent::Kind::kFatal) == TK_EVENT_FATAL); -static_assert(static_cast(RuntimeEvent::Kind::kRunState) == TK_EVENT_RUN_STATE); +static_assert(static_cast(RunState::kIdle) == TN_RUN_IDLE); +static_assert(static_cast(RunState::kStarting) == TN_RUN_STARTING); +static_assert(static_cast(RunState::kRunning) == TN_RUN_RUNNING); +static_assert(static_cast(RunState::kStopping) == TN_RUN_STOPPING); +static_assert(static_cast(RunState::kStopped) == TN_RUN_STOPPED); +static_assert(static_cast(RunState::kFailed) == TN_RUN_FAILED); + +static_assert(static_cast(tethernet::rndis::State::kUninitialized) == TN_RNDIS_UNINITIALIZED); +static_assert(static_cast(tethernet::rndis::State::kInitializing) == TN_RNDIS_INITIALIZING); +static_assert(static_cast(tethernet::rndis::State::kInitialized) == TN_RNDIS_INITIALIZED); +static_assert(static_cast(tethernet::rndis::State::kDataInitialized) == + TN_RNDIS_DATA_INITIALIZED); +static_assert(static_cast(tethernet::rndis::State::kHalting) == TN_RNDIS_HALTING); + +static_assert(static_cast(RuntimeEvent::Kind::kRndisState) == TN_EVENT_RNDIS_STATE); +static_assert(static_cast(RuntimeEvent::Kind::kNegotiated) == TN_EVENT_NEGOTIATED); +static_assert(static_cast(RuntimeEvent::Kind::kLink) == TN_EVENT_LINK); +static_assert(static_cast(RuntimeEvent::Kind::kDeviceReset) == TN_EVENT_DEVICE_RESET); +static_assert(static_cast(RuntimeEvent::Kind::kFatal) == TN_EVENT_FATAL); +static_assert(static_cast(RuntimeEvent::Kind::kRunState) == TN_EVENT_RUN_STATE); /// The event ring buffer. /// @@ -71,7 +72,7 @@ static_assert(static_cast(RuntimeEvent::Kind::kRunState) == TK_EVENT_RUN_ST /// in the snapshot, and missing a few does not affect correctness. constexpr std::size_t kEventRingCapacity = 128; -class EventRing final : public tetherkit::core::RuntimeEventSink { +class EventRing final : public tethernet::core::RuntimeEventSink { public: /// Called by the control thread. void OnRuntimeEvent(const RuntimeEvent& event) noexcept override { @@ -80,7 +81,7 @@ class EventRing final : public tetherkit::core::RuntimeEventSink { head_ = (head_ + 1) % kEventRingCapacity; --count_; } - tk_event_t& slot = events_[(head_ + count_) % kEventRingCapacity]; + tn_event_t& slot = events_[(head_ + count_) % kEventRingCapacity]; slot.kind = static_cast(event.kind); slot.wall_nanos = WallNanos(); slot.a = event.a; @@ -90,7 +91,7 @@ class EventRing final : public tetherkit::core::RuntimeEventSink { } /// Called by the host thread. - std::size_t Drain(tk_event_t* out_events, std::size_t capacity) noexcept { + std::size_t Drain(tn_event_t* out_events, std::size_t capacity) noexcept { const std::lock_guard guard(mutex_); std::size_t taken = 0; while (taken < capacity && count_ > 0) { @@ -106,7 +107,7 @@ class EventRing final : public tetherkit::core::RuntimeEventSink { private: std::mutex mutex_; - std::array events_{}; + std::array events_{}; std::size_t head_ = 0; std::size_t count_ = 0; }; @@ -121,10 +122,10 @@ class EventRing final : public tetherkit::core::RuntimeEventSink { return value != 0 ? value : fallback; } -[[nodiscard]] tetherkit::core::RuntimeConfig ToRuntimeConfig(const tk_session_config_t& config, +[[nodiscard]] tethernet::core::RuntimeConfig ToRuntimeConfig(const tn_session_config_t& config, EventRing& sink) { - const tetherkit::core::RuntimeConfig defaults; - tetherkit::core::RuntimeConfig result; + const tethernet::core::RuntimeConfig defaults; + tethernet::core::RuntimeConfig result; result.device_filter.vendor_id = config.vendor_id; result.device_filter.product_id = config.product_id; @@ -159,17 +160,17 @@ class EventRing final : public tetherkit::core::RuntimeEventSink { // first -- its destructor joins the control thread, and the control thread // may push events into events until the very last moment. // The other way around is a use-after-free. -struct tk_session { +struct tn_session { EventRing events; - std::unique_ptr runtime; + std::unique_ptr runtime; }; -void tk_session_config_init(tk_session_config_t* out_config) { +void tn_session_config_init(tn_session_config_t* out_config) { if (out_config == nullptr) { return; } - const tetherkit::core::RuntimeConfig defaults; - *out_config = tk_session_config_t{}; + const tethernet::core::RuntimeConfig defaults; + *out_config = tn_session_config_t{}; out_config->mtu = defaults.mtu; out_config->adopt_device_mac = defaults.adopt_device_mac; out_config->rx_transfer_count = defaults.data_channel.rx_transfer_count; @@ -179,11 +180,11 @@ void tk_session_config_init(tk_session_config_t* out_config) { out_config->bpf_buffer_kib = defaults.bpf.kernel_buffer_bytes / 1024; } -tk_session_t* tk_session_create(const tk_session_config_t* config, tk_error_t* out_error) { +tn_session_t* tn_session_create(const tn_session_config_t* config, tn_error_t* out_error) { ClearError(out_error); if (config == nullptr) { - tetherkit::capi::FillGenericError(out_error, - tetherkit::Tr(tetherkit::Msg::kCapiSessionConfigNull)); + tethernet::capi::FillGenericError(out_error, + tethernet::Tr(tethernet::Msg::kCapiSessionConfigNull)); return nullptr; } @@ -191,16 +192,16 @@ tk_session_t* tk_session_create(const tk_session_config_t* config, tk_error_t* o // the registry callback here is enough -- the root-free group of entry // points (version, enumeration, preflight) therefore stays free of side // effects and never touches /var/run. - tetherkit::capi::InstallInterfaceRegistry(); + tethernet::capi::InstallInterfaceRegistry(); - auto session = std::unique_ptr(new (std::nothrow) tk_session()); + auto session = std::unique_ptr(new (std::nothrow) tn_session()); if (session == nullptr) { - tetherkit::capi::FillGenericError(out_error, - tetherkit::Tr(tetherkit::Msg::kCapiSessionOutOfMemory)); + tethernet::capi::FillGenericError(out_error, + tethernet::Tr(tethernet::Msg::kCapiSessionOutOfMemory)); return nullptr; } - auto runtime = tetherkit::core::Runtime::Create(ToRuntimeConfig(*config, session->events)); + auto runtime = tethernet::core::Runtime::Create(ToRuntimeConfig(*config, session->events)); if (!runtime) { FillError(out_error, runtime.error()); return nullptr; @@ -209,10 +210,10 @@ tk_session_t* tk_session_create(const tk_session_config_t* config, tk_error_t* o return session.release(); } -tk_result_t tk_session_start(tk_session_t* session, tk_error_t* out_error) { +tn_result_t tn_session_start(tn_session_t* session, tn_error_t* out_error) { ClearError(out_error); if (session == nullptr) { - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } if (const auto status = session->runtime->Start(); !status) { @@ -220,32 +221,32 @@ tk_result_t tk_session_start(tk_session_t* session, tk_error_t* out_error) { // The only synchronous failure is the root check (see Runtime::Start), so // it gets a dedicated code -- that lets the GUI pop up "authorization // required" instead of a generic "start failed". - return tetherkit::net::IsRunningAsRoot() ? TK_ERR_FAILED : TK_ERR_PERMISSION; + return tethernet::net::IsRunningAsRoot() ? TN_ERR_FAILED : TN_ERR_PERMISSION; } - return TK_OK; + return TN_OK; } -tk_result_t tk_session_stop(tk_session_t* session) { +tn_result_t tn_session_stop(tn_session_t* session) { if (session == nullptr) { - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } session->runtime->Stop(); - return TK_OK; + return TN_OK; } -void tk_session_destroy(tk_session_t* session) { +void tn_session_destroy(tn_session_t* session) { // The Runtime destructor calls Stop() (idempotent), so a plain delete is // enough here. delete session; // NOLINT(cppcoreguidelines-owning-memory) } -tk_result_t tk_session_status_get(tk_session_t* session, tk_session_status_t* out_status) { +tn_result_t tn_session_status_get(tn_session_t* session, tn_session_status_t* out_status) { if (session == nullptr || out_status == nullptr) { - return TK_ERR_INVALID_ARGUMENT; + return TN_ERR_INVALID_ARGUMENT; } - const tetherkit::core::RuntimeSnapshot snapshot = session->runtime->Snapshot(); - *out_status = tk_session_status_t{}; + const tethernet::core::RuntimeSnapshot snapshot = session->runtime->Snapshot(); + *out_status = tn_session_status_t{}; out_status->run_state = static_cast(snapshot.run_state); out_status->rndis_state = static_cast(snapshot.rndis_state); @@ -261,8 +262,7 @@ tk_result_t tk_session_status_get(tk_session_t* session, tk_session_status_t* ou // the device *is* this interface, and the peer's ARP table and DHCP lease // are both built from the permanent address, so showing it in the UI is // what matches what the user sees on the router. - static_assert(sizeof(out_status->device_mac) == - std::tuple_size_v); + static_assert(sizeof(out_status->device_mac) == std::tuple_size_v); std::ranges::copy(snapshot.device_info.permanent_address, std::begin(out_status->device_mac)); out_status->mtu = snapshot.parameters.mtu; @@ -282,13 +282,13 @@ tk_result_t tk_session_status_get(tk_session_t* session, tk_session_status_t* ou // than letting the host difference its own clock readings: scheduling // stretches the real interval between two fetches, and using the host's // timer period as the denominator overstates the rate. - out_status->monotonic_nanos = static_cast(tetherkit::MonotonicNanos()); + out_status->monotonic_nanos = static_cast(tethernet::MonotonicNanos()); CopyText(out_status->fatal, snapshot.fatal_message); - return TK_OK; + return TN_OK; } -size_t tk_session_poll_events(tk_session_t* session, tk_event_t* out_events, size_t capacity) { +size_t tn_session_poll_events(tn_session_t* session, tn_event_t* out_events, size_t capacity) { if (session == nullptr || capacity == 0) { return 0; } diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index f307ba4..6d1b282 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -1,16 +1,16 @@ -# tk_common -- the infrastructure layer +# tn_common -- the infrastructure layer # # Depends on no higher-level module, nor on libusb / network syscalls. Its only # external dependency is libusb's headers (error.cc needs libusb_error_name to # translate error codes), so it is linked PRIVATEly and libusb is not propagated -# to tk_common's consumers. +# to tn_common's consumers. add_library( - tk_common STATIC + tn_common STATIC error.cc i18n.cc logging.cc scheduling.cc) -target_link_libraries(tk_common PUBLIC tetherkit_base) -target_link_libraries(tk_common PRIVATE LibUSB::LibUSB) +target_link_libraries(tn_common PUBLIC tethernet_base) +target_link_libraries(tn_common PRIVATE LibUSB::LibUSB) diff --git a/src/common/error.cc b/src/common/error.cc index 2bb6678..70e2d30 100644 --- a/src/common/error.cc +++ b/src/common/error.cc @@ -1,4 +1,4 @@ -#include "tetherkit/common/error.h" +#include "tethernet/common/error.h" #include @@ -6,7 +6,7 @@ #include #include -namespace tetherkit { +namespace tethernet { namespace { /// Thread-safe strerror. @@ -42,8 +42,8 @@ std::string Error::ToString() const { } case ErrorDomain::kRndis: { - // The RNDIS_STATUS_* to name mapping only lives in tk_rndis - // (rndis/protocol.cc); tk_common is not allowed to depend on upper + // The RNDIS_STATUS_* to name mapping only lives in tn_rndis + // (rndis/protocol.cc); tn_common is not allowed to depend on upper // modules, so this just prints the raw numeric value -- // the rndis layer bakes the status code's symbolic name into the // context string when it constructs the Error. @@ -54,4 +54,4 @@ std::string Error::ToString() const { return context_; } -} // namespace tetherkit +} // namespace tethernet diff --git a/src/common/i18n.cc b/src/common/i18n.cc index 6b376c7..5526920 100644 --- a/src/common/i18n.cc +++ b/src/common/i18n.cc @@ -1,9 +1,9 @@ -#include "tetherkit/common/i18n.h" +#include "tethernet/common/i18n.h" // ContextSeparator() is declared in error.h (which avoids pulling in // ); including it here lets the compiler check that declaration and // definition agree. -#include "tetherkit/common/error.h" +#include "tethernet/common/error.h" #include #include @@ -13,7 +13,7 @@ #include #include -namespace tetherkit { +namespace tethernet { namespace { /// Message count. The table is fixed-length, so "added an enum value but no @@ -26,9 +26,9 @@ constexpr std::size_t kMessageCount = static_cast(Msg::kMessageCoun /// this one, extracted from the new column, and a dispatch in TextIn() that /// picks by Language (see the checklist in messages.def). constexpr std::array kMessageTable{ -#define TETHERKIT_MESSAGE(id, en) en, -#include "tetherkit/common/messages.def" // NOLINT(bugprone-suspicious-include) -#undef TETHERKIT_MESSAGE +#define TETHERNET_MESSAGE(id, en) en, +#include "tethernet/common/messages.def" // NOLINT(bugprone-suspicious-include) +#undef TETHERNET_MESSAGE }; /// Current language. Process-wide single state, atomic rather than mutexed: @@ -91,10 +91,10 @@ Language LanguageFromLocaleString(std::string_view /*locale*/) noexcept { } Language DetectLanguageFromEnvironment() noexcept { - // POSIX order: LC_ALL beats LC_MESSAGES beats LANG. TETHERKIT_LANG comes + // POSIX order: LC_ALL beats LC_MESSAGES beats LANG. TETHERNET_LANG comes // first so a user can change just this program's language without touching // the system locale. - for (const char* name : {"TETHERKIT_LANG", "LC_ALL", "LC_MESSAGES", "LANG"}) { + for (const char* name : {"TETHERNET_LANG", "LC_ALL", "LC_MESSAGES", "LANG"}) { const std::string_view value = ReadEnvironment(name); if (value.empty()) { continue; @@ -164,4 +164,4 @@ std::string_view ContextSeparator() noexcept { } } // namespace detail -} // namespace tetherkit +} // namespace tethernet diff --git a/src/common/logging.cc b/src/common/logging.cc index de3b62a..fd5acf4 100644 --- a/src/common/logging.cc +++ b/src/common/logging.cc @@ -1,4 +1,4 @@ -#include "tetherkit/common/logging.h" +#include "tethernet/common/logging.h" #include #include @@ -11,7 +11,7 @@ #include #include -namespace tetherkit { +namespace tethernet { namespace { // The log configuration is process-wide singleton state, naturally a mutable @@ -173,4 +173,4 @@ void EmitLogLine(LogLevel level, std::string_view file, unsigned line, } } // namespace detail -} // namespace tetherkit +} // namespace tethernet diff --git a/src/common/scheduling.cc b/src/common/scheduling.cc index d43c084..3d7465c 100644 --- a/src/common/scheduling.cc +++ b/src/common/scheduling.cc @@ -1,10 +1,10 @@ -#include "tetherkit/common/scheduling.h" +#include "tethernet/common/scheduling.h" -#include +#include "tethernet/common/logging.h" -#include "tetherkit/common/logging.h" +#include -namespace tetherkit { +namespace tethernet { unsigned int QosClassFor(ThreadRole role) noexcept { switch (role) { @@ -27,8 +27,8 @@ void ConfigureCurrentThread(std::string_view name, ThreadRole role) noexcept { if (rc != 0) { // A QoS failure does not affect functionality, only performance, so this // just warns instead of returning an error. - TETHERKIT_WARN_TR(Msg::kCommonThreadQosFailed, static_cast(qos), rc); + TETHERNET_WARN_TR(Msg::kCommonThreadQosFailed, static_cast(qos), rc); } } -} // namespace tetherkit +} // namespace tethernet diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 8651805..b3f5c63 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,12 +1,12 @@ -# tk_core -- data-path bridging and runtime orchestration +# tn_core -- data-path bridging and runtime orchestration # -# Wires tk_usb's RNDIS data channel to tk_net's link backend and manages the +# Wires tn_usb's RNDIS data channel to tn_net's link backend and manages the # data-path threads. It is the only layer that depends on all three of # usb / net / rndis at once. add_library( - tk_core STATIC + tn_core STATIC bridge.cc runtime.cc) -target_link_libraries(tk_core PUBLIC tetherkit_base tk_common tk_rndis tk_net tk_usb) +target_link_libraries(tn_core PUBLIC tethernet_base tn_common tn_rndis tn_net tn_usb) diff --git a/src/core/bridge.cc b/src/core/bridge.cc index c8ed71a..991c193 100644 --- a/src/core/bridge.cc +++ b/src/core/bridge.cc @@ -1,15 +1,15 @@ -#include "tetherkit/core/bridge.h" +#include "tethernet/core/bridge.h" + +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" +#include "tethernet/common/scheduling.h" #include #include #include #include -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" -#include "tetherkit/common/scheduling.h" - -namespace tetherkit::core { +namespace tethernet::core { namespace { /// Upper bound on how long one TX-backpressure wait may block for a free @@ -61,13 +61,13 @@ Status Bridge::Start() { // matter (the queue is bounded, and it drops and counts when full), but // opening receive first lets us collect frames the moment the link is ready, // dropping a few fewer. - TETHERKIT_RETURN_IF_ERROR(data_channel_->StartReceiving(*rx_ring_, counters_.rx)); + TETHERNET_RETURN_IF_ERROR(data_channel_->StartReceiving(*rx_ring_, counters_.rx)); running_.store(true, std::memory_order_release); receive_injector_ = std::thread([this] { RunReceiveInjector(); }); transmit_extractor_ = std::thread([this] { RunTransmitExtractor(); }); - TETHERKIT_INFO_TR(Msg::kCoreDataPathStarted, rx_ring_->Capacity(), + TETHERNET_INFO_TR(Msg::kCoreDataPathStarted, rx_ring_->Capacity(), rx_ring_->StorageBytes() / 1024, config_.rx_write_batch, config_.tx_submit_batch, Text(link_->SupportsBatchWrite() ? Msg::kCoreBatchWriteAvailable @@ -112,7 +112,7 @@ void Bridge::Stop() { data_channel_->Shutdown(); - TETHERKIT_INFO_TR(Msg::kCoreDataPathStopped); + TETHERNET_INFO_TR(Msg::kCoreDataPathStopped); } // ============================================================================= @@ -142,7 +142,7 @@ void Bridge::SetPaused(bool paused) noexcept { void Bridge::RunReceiveInjector() noexcept { ConfigureCurrentThread("rx-inject", ThreadRole::kDataPath); - TETHERKIT_DEBUG_TR(Msg::kCoreRxInjectorStarted); + TETHERNET_DEBUG_TR(Msg::kCoreRxInjectorStarted); std::uint32_t idle_spins = 0; @@ -197,8 +197,7 @@ void Bridge::RunReceiveInjector() noexcept { const auto result = link_->WriteFrames(rx_batch_); if (!result) { counters_.rx.AddIoError(); - TETHERKIT_WARN_TR(Msg::kCoreLinkWriteFailed, rx_batch_.size(), - result.error().ToString()); + TETHERNET_WARN_TR(Msg::kCoreLinkWriteFailed, rx_batch_.size(), result.error().ToString()); // A write failure does not exit the thread -- the interface may just // be temporarily down. The batch session still releases its slots // normally (the frames cannot be delivered anymore; keeping them would @@ -222,7 +221,7 @@ void Bridge::RunReceiveInjector() noexcept { } // batch session destructs -> a single PublishRead(n) } - TETHERKIT_DEBUG_TR(Msg::kCoreRxInjectorExited); + TETHERNET_DEBUG_TR(Msg::kCoreRxInjectorExited); } // ============================================================================= @@ -231,7 +230,7 @@ void Bridge::RunReceiveInjector() noexcept { void Bridge::RunTransmitExtractor() noexcept { ConfigureCurrentThread("tx-extract", ThreadRole::kDataPath); - TETHERKIT_DEBUG_TR(Msg::kCoreTxExtractorStarted); + TETHERNET_DEBUG_TR(Msg::kCoreTxExtractorStarted); while (!stop_requested_.load(std::memory_order_acquire)) { // Blocking read. With BIOCIMMEDIATE=1 the BPF hands over whatever @@ -240,7 +239,7 @@ void Bridge::RunTransmitExtractor() noexcept { const auto batch = link_->ReadFrames(); if (!batch) { counters_.tx.AddIoError(); - TETHERKIT_WARN_TR(Msg::kCoreLinkReadFailed, batch.error().ToString()); + TETHERNET_WARN_TR(Msg::kCoreLinkReadFailed, batch.error().ToString()); // A read failure can mean the interface was torn down. Wait a bit and // retry to avoid a busy loop spamming the log. std::this_thread::sleep_for(std::chrono::milliseconds(10)); @@ -286,7 +285,7 @@ void Bridge::RunTransmitExtractor() noexcept { // break with only +1 io_error, leaving the rest out of every counter, // vanishing from the statistics entirely. counters_.tx.AddDroppedFull(batch->frames.size() - offset); - TETHERKIT_WARN_TR(Msg::kCoreUsbSubmitFailed, chunk, sent.error().ToString()); + TETHERNET_WARN_TR(Msg::kCoreUsbSubmitFailed, chunk, sent.error().ToString()); break; } @@ -333,7 +332,7 @@ void Bridge::RunTransmitExtractor() noexcept { } } - TETHERKIT_DEBUG_TR(Msg::kCoreTxExtractorExited); + TETHERNET_DEBUG_TR(Msg::kCoreTxExtractorExited); } // ============================================================================= @@ -341,7 +340,7 @@ void Bridge::RunTransmitExtractor() noexcept { // ============================================================================= BridgeStats Bridge::Snapshot() const { - DirectionSnapshot tx = tetherkit::Snapshot(counters_.tx); + DirectionSnapshot tx = tethernet::Snapshot(counters_.tx); // Merge the errors the data channel accumulated in its async completion // callbacks -- those are incremented by the libusb event thread and are // deliberately kept apart from the bridge's counters to preserve the @@ -349,7 +348,7 @@ BridgeStats Bridge::Snapshot() const { tx.io_errors += data_channel_->AsyncSendErrors(); return BridgeStats{ - .rx = tetherkit::Snapshot(counters_.rx), + .rx = tethernet::Snapshot(counters_.rx), .tx = tx, .rx_queue_depth = rx_ring_->SizeSnapshot(), .link_kernel_drops = link_kernel_drops_.load(std::memory_order_relaxed), @@ -372,4 +371,4 @@ std::string FormatStatsLine(const BridgeStats& previous, const BridgeStats& curr current.tx_backpressure_events - previous.tx_backpressure_events); } -} // namespace tetherkit::core +} // namespace tethernet::core diff --git a/src/core/runtime.cc b/src/core/runtime.cc index 0982e77..cbc1ce0 100644 --- a/src/core/runtime.cc +++ b/src/core/runtime.cc @@ -1,4 +1,9 @@ -#include "tetherkit/core/runtime.h" +#include "tethernet/core/runtime.h" + +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" +#include "tethernet/common/scheduling.h" +#include "tethernet/common/time.h" #include #include @@ -6,12 +11,7 @@ #include #include -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" -#include "tetherkit/common/scheduling.h" -#include "tetherkit/common/time.h" - -namespace tetherkit::core { +namespace tethernet::core { std::string_view RunStateName(RunState state) noexcept { switch (state) { @@ -143,18 +143,18 @@ Status Runtime::RunStartSequence() { // ---- Step 1: libusb context and event thread ---- // // (The root check was already done synchronously in Start().) - TETHERKIT_ASSIGN_OR_RETURN(usb_context_, usb::Context::Create()); + TETHERNET_ASSIGN_OR_RETURN(usb_context_, usb::Context::Create()); // ---- Step 2: discover and open the device ---- - TETHERKIT_ASSIGN_OR_RETURN(const auto candidates, + TETHERNET_ASSIGN_OR_RETURN(const auto candidates, usb::FindRndisDevices(*usb_context_, config_.device_filter)); if (candidates.empty()) { return std::unexpected(Error::Generic(Tr(Msg::kCoreNoDeviceFound))); } if (candidates.size() > 1) { - TETHERKIT_INFO_TR(Msg::kCoreMultipleDevices, candidates.size()); + TETHERNET_INFO_TR(Msg::kCoreMultipleDevices, candidates.size()); } - TETHERKIT_ASSIGN_OR_RETURN(device_, usb::Device::Open(*usb_context_, candidates.front())); + TETHERNET_ASSIGN_OR_RETURN(device_, usb::Device::Open(*usb_context_, candidates.front())); RefreshSnapshot(); // ---- Step 3: RNDIS control channel and state machine ---- @@ -167,10 +167,10 @@ Status Runtime::RunStartSequence() { // Interrupt notifications must go over an async transfer or the control // thread would wedge forever -- see the UsbControlChannel notes in - // include/tetherkit/usb/device.h. It must be started **before** the state + // include/tethernet/usb/device.h. It must be started **before** the state // machine's Start(), because the initialization handshake itself waits for // notifications. - TETHERKIT_RETURN_IF_ERROR(control_channel_->StartNotificationListener()); + TETHERNET_RETURN_IF_ERROR(control_channel_->StartNotificationListener()); // Let the RNDIS negotiation use the real endpoint max packet size (it // affects how MaxTransferSize is derived). @@ -178,7 +178,7 @@ Status Runtime::RunStartSequence() { rndis_config.requested_mtu = config_.mtu; state_machine_ = std::make_unique(*control_channel_, *this, rndis_config); - TETHERKIT_RETURN_IF_ERROR(state_machine_->Start()); + TETHERNET_RETURN_IF_ERROR(state_machine_->Start()); // Only now do we have the negotiation results: device MAC, final MTU, // aggregation caps, alignment requirements. @@ -197,7 +197,7 @@ Status Runtime::RunStartSequence() { std::ranges::copy(info.permanent_address, adopted.begin()); system_mac = &adopted; } - TETHERKIT_ASSIGN_OR_RETURN(auto pair, net::FethPair::Create(parameters.mtu, system_mac)); + TETHERNET_ASSIGN_OR_RETURN(auto pair, net::FethPair::Create(parameters.mtu, system_mac)); feth_pair_ = std::make_unique(std::move(pair)); } // Flush the interfaces into the snapshot as soon as they exist: if any later @@ -216,7 +216,7 @@ Status Runtime::RunStartSequence() { // Max frame = MTU + Ethernet header. Note the kernel bpfwrite hard cap is // MTU + 18, so this must never exceed it. bpf_config.max_frame_bytes = parameters.mtu + rndis::kEthernetHeaderBytes; - TETHERKIT_ASSIGN_OR_RETURN(bpf_link_, + TETHERNET_ASSIGN_OR_RETURN(bpf_link_, net::BpfLink::Open(feth_pair_->DriverSide().Name(), bpf_config)); } @@ -228,7 +228,7 @@ Status Runtime::RunStartSequence() { // or be truncated. data_config.rx_transfer_bytes = std::max(data_config.rx_transfer_bytes, config_.rndis.host_max_transfer_size); - TETHERKIT_ASSIGN_OR_RETURN(data_channel_, + TETHERNET_ASSIGN_OR_RETURN(data_channel_, usb::UsbDataChannel::Create(*device_, parameters, data_config)); } @@ -237,7 +237,7 @@ Status Runtime::RunStartSequence() { BridgeConfig bridge_config = config_.bridge; bridge_config.max_frame_bytes = parameters.mtu + rndis::kEthernetHeaderBytes; bridge_ = std::make_unique(*data_channel_, *bpf_link_, bridge_config); - TETHERKIT_RETURN_IF_ERROR(bridge_->Start()); + TETHERNET_RETURN_IF_ERROR(bridge_->Start()); } RefreshSnapshot(); @@ -274,7 +274,7 @@ void Runtime::RunControlLoop() { const BridgeStats current = bridge_->Snapshot(); const double seconds = static_cast(now - last_stats_nanos) / static_cast(kNanosPerSecond); - TETHERKIT_INFO("{}", FormatStatsLine(previous, current, seconds)); + TETHERNET_INFO("{}", FormatStatsLine(previous, current, seconds)); previous = current; last_stats_nanos = now; } @@ -304,24 +304,24 @@ void Runtime::RunControlLoop() { } if (fatal_error_.load(std::memory_order_acquire)) { - TETHERKIT_ERROR_TR(Msg::kCoreExitingOnFatal); + TETHERNET_ERROR_TR(Msg::kCoreExitingOnFatal); } } void Runtime::PrintNextSteps() const { const std::string_view name = feth_pair_->SystemSide().Name(); - TETHERKIT_INFO(""); - TETHERKIT_INFO_TR(Msg::kCoreInterfaceReady, name); - TETHERKIT_INFO_TR(Msg::kCoreNextStepsAssignIp); - TETHERKIT_INFO(" sudo ipconfig set {} DHCP", name); - TETHERKIT_INFO_TR(Msg::kCoreNextStepsVerify); - TETHERKIT_INFO(" ipconfig getifaddr {}", name); - TETHERKIT_INFO(" ipconfig getsummary {}", name); - TETHERKIT_INFO_TR(Msg::kCoreNextStepsDefaultRoute); - TETHERKIT_INFO(" sudo route -n change default $(ipconfig getoption {} router)", name); - TETHERKIT_INFO_TR(Msg::kCoreNextStepsTemporaryNote1); - TETHERKIT_INFO_TR(Msg::kCoreNextStepsTemporaryNote2); - TETHERKIT_INFO(""); + TETHERNET_INFO(""); + TETHERNET_INFO_TR(Msg::kCoreInterfaceReady, name); + TETHERNET_INFO_TR(Msg::kCoreNextStepsAssignIp); + TETHERNET_INFO(" sudo ipconfig set {} DHCP", name); + TETHERNET_INFO_TR(Msg::kCoreNextStepsVerify); + TETHERNET_INFO(" ipconfig getifaddr {}", name); + TETHERNET_INFO(" ipconfig getsummary {}", name); + TETHERNET_INFO_TR(Msg::kCoreNextStepsDefaultRoute); + TETHERNET_INFO(" sudo route -n change default $(ipconfig getoption {} router)", name); + TETHERNET_INFO_TR(Msg::kCoreNextStepsTemporaryNote1); + TETHERNET_INFO_TR(Msg::kCoreNextStepsTemporaryNote2); + TETHERNET_INFO(""); } // ============================================================================= @@ -329,7 +329,7 @@ void Runtime::PrintNextSteps() const { // ============================================================================= void Runtime::Teardown() { - TETHERKIT_INFO_TR(Msg::kCoreStopping); + TETHERNET_INFO_TR(Msg::kCoreStopping); // Teardown in the exact **reverse** of the start-up order. // @@ -385,7 +385,7 @@ void Runtime::Teardown() { snapshot_.link_up = false; } RefreshSnapshot(); - TETHERKIT_INFO_TR(Msg::kCoreStopped); + TETHERNET_INFO_TR(Msg::kCoreStopped); } // ============================================================================= @@ -440,7 +440,7 @@ void Runtime::SetRunState(RunState next) { void Runtime::RecordFatal(const Error& error) { const std::string message = error.ToString(); - TETHERKIT_ERROR("{}", message); + TETHERNET_ERROR("{}", message); { const std::lock_guard guard(snapshot_mutex_); @@ -475,7 +475,7 @@ void Runtime::OnStateChanged(rndis::State from, rndis::State to) { void Runtime::OnNegotiated(const rndis::NegotiatedParameters& parameters, const rndis::DeviceInfo& info) { - TETHERKIT_INFO_TR(Msg::kCoreRndisReady, rndis::FormatMac(info.permanent_address).data(), + TETHERNET_INFO_TR(Msg::kCoreRndisReady, rndis::FormatMac(info.permanent_address).data(), parameters.mtu, info.LinkSpeedMbps()); Emit(RuntimeEvent{.kind = RuntimeEvent::Kind::kNegotiated, @@ -485,7 +485,7 @@ void Runtime::OnNegotiated(const rndis::NegotiatedParameters& parameters, } void Runtime::OnLinkStateChanged(bool connected) { - TETHERKIT_INFO_TR(Msg::kCoreLinkState, + TETHERNET_INFO_TR(Msg::kCoreLinkState, Text(connected ? Msg::kCoreLinkConnected : Msg::kCoreLinkDisconnected)); { @@ -504,7 +504,7 @@ void Runtime::OnLinkStateChanged(bool connected) { } void Runtime::OnDeviceReset(bool addressing_lost) { - TETHERKIT_WARN_TR(Msg::kCoreDeviceReset, + TETHERNET_WARN_TR(Msg::kCoreDeviceReset, Text(addressing_lost ? Msg::kCoreAddressingLost : Msg::kCoreAddressingKept)); // The device discarded all outstanding packets during the reset. Pause @@ -526,4 +526,4 @@ void Runtime::OnFatalError(const Error& error) { RecordFatal(std::move(annotated).WithContext(Tr(Msg::kCoreLinkUnrecoverable))); } -} // namespace tetherkit::core +} // namespace tethernet::core diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt index e76b692..c1f4b48 100644 --- a/src/net/CMakeLists.txt +++ b/src/net/CMakeLists.txt @@ -1,4 +1,4 @@ -# tk_net -- virtual interface and link layer +# tn_net -- virtual interface and link layer # # Only depends on macOS syscalls (ioctl / BPF); neither libusb nor the RNDIS # protocol layer. LinkBackend is the module's public abstraction; BpfLink is the @@ -7,9 +7,9 @@ # run on machines without root and without a real interface. add_library( - tk_net STATIC + tn_net STATIC feth_device.cc bpf_link.cc loopback_link.cc) -target_link_libraries(tk_net PUBLIC tetherkit_base tk_common) +target_link_libraries(tn_net PUBLIC tethernet_base tn_common) diff --git a/src/net/bpf_link.cc b/src/net/bpf_link.cc index cd30eb9..177bc9e 100644 --- a/src/net/bpf_link.cc +++ b/src/net/bpf_link.cc @@ -1,4 +1,10 @@ -#include "tetherkit/net/bpf_link.h" +#include "tethernet/net/bpf_link.h" + +#include "tethernet/common/byte_order.h" +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" +#include "tethernet/net/darwin_abi.h" +#include "tethernet/net/feth_device.h" #include #include @@ -11,13 +17,7 @@ #include #include -#include "tetherkit/common/byte_order.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" -#include "tetherkit/net/darwin_abi.h" -#include "tetherkit/net/feth_device.h" - -namespace tetherkit::net { +namespace tethernet::net { namespace { /// The upper limit for scanning /dev/bpf%d. @@ -52,7 +52,7 @@ constexpr std::size_t kMaxBatchWriteBytes = std::size_t{256} * 1024; if (::ioctl(fd, request, argument) < 0) { // ENOTTY / EINVAL means this macOS version does not know the private ioctl, // an expected case. - TETHERKIT_DEBUG_TR(Msg::kNetOptionalIoctlUnavailable, what, errno); + TETHERNET_DEBUG_TR(Msg::kNetOptionalIoctlUnavailable, what, errno); return false; } return true; @@ -130,11 +130,10 @@ Result> BpfLink::Open(std::string_view interface_name, // so every read() below must use this written-back value as the length. // --------------------------------------------------------------------------- auto buffer_bytes = static_cast(config.kernel_buffer_bytes); - TETHERKIT_RETURN_IF_ERROR( - CallIoctl(link->fd_, BIOCSBLEN, &buffer_bytes, "ioctl(BIOCSBLEN)")); + TETHERNET_RETURN_IF_ERROR(CallIoctl(link->fd_, BIOCSBLEN, &buffer_bytes, "ioctl(BIOCSBLEN)")); link->kernel_buffer_bytes_ = buffer_bytes; if (buffer_bytes != config.kernel_buffer_bytes) { - TETHERKIT_INFO_TR(Msg::kNetBpfBufferClamped, config.kernel_buffer_bytes, buffer_bytes); + TETHERNET_INFO_TR(Msg::kNetBpfBufferClamped, config.kernel_buffer_bytes, buffer_bytes); } // --------------------------------------------------------------------------- @@ -152,7 +151,7 @@ Result> BpfLink::Open(std::string_view interface_name, // ENXIO, do not suspect the interface name. // --------------------------------------------------------------------------- int header_complete = 1; - TETHERKIT_RETURN_IF_ERROR( + TETHERNET_RETURN_IF_ERROR( CallIoctl(link->fd_, BIOCSHDRCMPLT, &header_complete, "ioctl(BIOCSHDRCMPLT)")); // --------------------------------------------------------------------------- @@ -180,8 +179,7 @@ Result> BpfLink::Open(std::string_view interface_name, // where every frame-format assumption would be wrong. // --------------------------------------------------------------------------- unsigned int data_link_type = 0; - TETHERKIT_RETURN_IF_ERROR( - CallIoctl(link->fd_, BIOCGDLT, &data_link_type, "ioctl(BIOCGDLT)")); + TETHERNET_RETURN_IF_ERROR(CallIoctl(link->fd_, BIOCGDLT, &data_link_type, "ioctl(BIOCGDLT)")); if (data_link_type != DLT_EN10MB) { return std::unexpected(Error::Generic( Tr(Msg::kNetBpfWrongDataLinkType, interface_name, data_link_type, DLT_EN10MB))); @@ -197,7 +195,7 @@ Result> BpfLink::Open(std::string_view interface_name, // batches automatically, NAPI-like behavior. // --------------------------------------------------------------------------- unsigned int immediate = 1; - TETHERKIT_RETURN_IF_ERROR( + TETHERNET_RETURN_IF_ERROR( CallIoctl(link->fd_, BIOCIMMEDIATE, &immediate, "ioctl(BIOCIMMEDIATE)")); // --------------------------------------------------------------------------- @@ -208,8 +206,7 @@ Result> BpfLink::Open(std::string_view interface_name, // only the input frames the host sends from the peer side. // --------------------------------------------------------------------------- unsigned int see_sent = 0; - TETHERKIT_RETURN_IF_ERROR( - CallIoctl(link->fd_, BIOCSSEESENT, &see_sent, "ioctl(BIOCSSEESENT)")); + TETHERNET_RETURN_IF_ERROR(CallIoctl(link->fd_, BIOCSSEESENT, &see_sent, "ioctl(BIOCSSEESENT)")); // --------------------------------------------------------------------------- // 8. BIOCSRTIMEOUT -- read timeout, used to answer shutdown @@ -222,7 +219,7 @@ Result> BpfLink::Open(std::string_view interface_name, read_timeout.tv_sec = config.read_timeout_millis / 1000; read_timeout.tv_usec = static_cast<__darwin_suseconds_t>( (config.read_timeout_millis % 1000) * 1000); - TETHERKIT_RETURN_IF_ERROR( + TETHERNET_RETURN_IF_ERROR( CallIoctl(link->fd_, BIOCSRTIMEOUT, &read_timeout, "ioctl(BIOCSRTIMEOUT)")); // BIOCPROMISC is deliberately **not** set: feth's feth_output_common hands @@ -255,7 +252,7 @@ Result> BpfLink::Open(std::string_view interface_name, link->write_buffer_.resize(kMaxBatchWriteBytes); } - TETHERKIT_INFO_TR(Msg::kNetBpfReady, link->device_path_, link->interface_name_, + TETHERNET_INFO_TR(Msg::kNetBpfReady, link->device_path_, link->interface_name_, link->kernel_buffer_bytes_ / 1024, link->max_frame_bytes_, Text(link->batch_write_enabled_ ? Msg::kNetBpfBatchWriteEnabled : Msg::kNetBpfBatchWriteUnavailable), @@ -328,7 +325,7 @@ Result BpfLink::ReadFrames() { const std::uint32_t header_length = LoadLe16(record + offsetof(::bpf_hdr, bh_hdrlen)); if (header_length < kBpfHeaderMinBytes) [[unlikely]] { - TETHERKIT_WARN_TR(Msg::kNetBpfHeaderTooShort, header_length, kBpfHeaderMinBytes); + TETHERNET_WARN_TR(Msg::kNetBpfHeaderTooShort, header_length, kBpfHeaderMinBytes); break; } @@ -336,7 +333,7 @@ Result BpfLink::ReadFrames() { if (offset + record_bytes > total) [[unlikely]] { // Record truncated: should never happen in normal operation (the kernel // does not write records that straddle the buffer end). - TETHERKIT_WARN_TR(Msg::kNetBpfRecordOutOfBounds, offset, record_bytes, total); + TETHERNET_WARN_TR(Msg::kNetBpfRecordOutOfBounds, offset, record_bytes, total); break; } @@ -348,7 +345,7 @@ Result BpfLink::ReadFrames() { capture_length <= max_frame_bytes_) [[likely]] { read_frames_.push_back(FrameView{.data = record + header_length, .length = capture_length}); } else [[unlikely]] { - TETHERKIT_TRACE_TR(Msg::kNetBpfSkipOversizedRecord, capture_length, original_length, + TETHERNET_TRACE_TR(Msg::kNetBpfSkipOversizedRecord, capture_length, original_length, max_frame_bytes_); } @@ -359,7 +356,7 @@ Result BpfLink::ReadFrames() { // -- is the data still in the kernel buffer? No: it has already been // copyout-ed and would be lost. max_frames_per_batch must therefore be // sized generously enough. Warn here rather than fail silently. - TETHERKIT_WARN_TR(Msg::kNetBpfBatchFrameLimit, read_frames_.capacity(), total - offset); + TETHERNET_WARN_TR(Msg::kNetBpfBatchFrameLimit, read_frames_.capacity(), total - offset); break; } } @@ -463,7 +460,7 @@ Result BpfLink::WriteFramesBatched(FrameBatch frames) { const std::size_t record_bytes = kBpfHeaderMinBytes + frame.length; const std::size_t aligned_bytes = BPF_WORDALIGN(record_bytes); if (cursor + aligned_bytes > write_buffer_.size()) { - TETHERKIT_RETURN_IF_ERROR(flush()); + TETHERNET_RETURN_IF_ERROR(flush()); if (aligned_bytes > write_buffer_.size()) [[unlikely]] { // A single frame exceeds the whole assembly buffer: a configuration // error; skip and count. @@ -492,8 +489,8 @@ Result BpfLink::WriteFramesBatched(FrameBatch frames) { pending_bytes += frame.length; } - TETHERKIT_RETURN_IF_ERROR(flush()); + TETHERNET_RETURN_IF_ERROR(flush()); return result; } -} // namespace tetherkit::net +} // namespace tethernet::net diff --git a/src/net/feth_device.cc b/src/net/feth_device.cc index ca8f7c2..4e2e65d 100644 --- a/src/net/feth_device.cc +++ b/src/net/feth_device.cc @@ -1,4 +1,8 @@ -#include "tetherkit/net/feth_device.h" +#include "tethernet/net/feth_device.h" + +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" +#include "tethernet/net/darwin_abi.h" #include #include @@ -15,11 +19,7 @@ #include #include -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" -#include "tetherkit/net/darwin_abi.h" - -namespace tetherkit::net { +namespace tethernet::net { namespace { /// The host-installed interface registry callback. An atomic rather than a @@ -144,7 +144,7 @@ bool IsRunningAsRoot() noexcept { } Result QueryFethMaxMtu() { - TETHERKIT_ASSIGN_OR_RETURN(const std::int32_t value, ReadInt32Sysctl("net.link.fake.max_mtu")); + TETHERNET_ASSIGN_OR_RETURN(const std::int32_t value, ReadInt32Sysctl("net.link.fake.max_mtu")); if (value <= 0) { return std::unexpected(Error::Generic(Tr(Msg::kNetFethMaxMtuInvalid, value))); } @@ -157,8 +157,7 @@ Status VerifyFethSysctls() { if (!value) { // Some sysctls may be missing on particular macOS versions; a missing // one is not an error, so log it and move on. - TETHERKIT_DEBUG_TR(Msg::kNetSysctlUnreadableSkipped, entry.name, - value.error().ToString()); + TETHERNET_DEBUG_TR(Msg::kNetSysctlUnreadableSkipped, entry.name, value.error().ToString()); continue; } if (*value != entry.required_value) { @@ -174,8 +173,8 @@ void SetInterfaceRegistry(InterfaceRegistry registry) noexcept { } Status DestroyInterfaceByName(std::string_view name) { - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); - TETHERKIT_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name)); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name)); if (const auto status = socket.Call(SIOCIFDESTROY, &request, "ioctl(SIOCIFDESTROY)"); !status) { Error error = status.error(); @@ -183,7 +182,7 @@ Status DestroyInterfaceByName(std::string_view name) { std::move(error).WithContext(Tr(Msg::kNetDestroyOrphanFailed, name))); } NotifyRegistry(name, false); - TETHERKIT_INFO_TR(Msg::kNetDestroyedOrphan, name); + TETHERNET_INFO_TR(Msg::kNetDestroyedOrphan, name); return Ok(); } @@ -214,32 +213,32 @@ void FethInterface::Destroy() noexcept { const auto socket = IoctlSocket::Open(); if (!socket) { - TETHERKIT_ERROR_TR(Msg::kNetDestroyFailed, name, socket.error().ToString()); + TETHERNET_ERROR_TR(Msg::kNetDestroyFailed, name, socket.error().ToString()); return; } auto request = MakeIfreq(name); if (!request) { - TETHERKIT_ERROR_TR(Msg::kNetDestroyFailed, name, request.error().ToString()); + TETHERNET_ERROR_TR(Msg::kNetDestroyFailed, name, request.error().ToString()); return; } // feth_clone_destroy unbinds the peer by itself, so there is no need for us // to Unpeer first. if (const auto status = socket->Call(SIOCIFDESTROY, &*request, "ioctl(SIOCIFDESTROY)"); !status) { - TETHERKIT_ERROR_TR(Msg::kNetDestroyFailed, name, status.error().ToString()); + TETHERNET_ERROR_TR(Msg::kNetDestroyFailed, name, status.error().ToString()); return; } NotifyRegistry(name, false); - TETHERKIT_INFO_TR(Msg::kNetDestroyed, name); + TETHERNET_INFO_TR(Msg::kNetDestroyed, name); } Result FethInterface::Create(std::string_view requested_name) { - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); // An empty name -> fill in the driver name "feth" as the wildcard; the // kernel picks the smallest free unit number and writes the full name back. const std::string_view name_to_request = requested_name.empty() ? std::string_view{kFethCloneName} : requested_name; - TETHERKIT_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_to_request)); + TETHERNET_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_to_request)); // SIOCIFCREATE and SIOCIFCREATE2 are exactly equivalent for feth -- // feth_clone_create ignores params, and the only difference of SIOCIFCREATE2 @@ -266,7 +265,7 @@ Result FethInterface::Create(std::string_view requested_name) { // process is killed from this moment on, the next start can still clean it // up", so no window may be left in between. NotifyRegistry(created_name, true); - TETHERKIT_INFO_TR(Msg::kNetCreated, created_name); + TETHERNET_INFO_TR(Msg::kNetCreated, created_name); return FethInterface{std::move(created_name)}; } @@ -278,7 +277,7 @@ Status FethInterface::PeerWith(const FethInterface& peer) { return std::unexpected(Error::Generic(Tr(Msg::kNetPeerNameTooLong, peer.Name()))); } - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); // The reserved field stays all zeros -- the kernel validates it and returns // EINVAL when it is nonzero. @@ -302,7 +301,7 @@ Status FethInterface::PeerWith(const FethInterface& peer) { std::move(error).WithContext(Tr(Msg::kNetPeerFailed, name_, peer.Name()))); } - TETHERKIT_INFO_TR(Msg::kNetPeered, name_, peer.Name()); + TETHERNET_INFO_TR(Msg::kNetPeered, name_, peer.Name()); return Ok(); } @@ -310,11 +309,11 @@ Status FethInterface::Unpeer() { if (!Valid()) { return std::unexpected(Error::Generic(Tr(Msg::kNetUnpeerInvalidObject))); } - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); // An empty peer name (first byte '\0') means unbind. FethRequest payload{}; - TETHERKIT_RETURN_IF_ERROR(CallFethDriverIoctl( + TETHERNET_RETURN_IF_ERROR(CallFethDriverIoctl( socket, name_, kSetDriverSpec, static_cast(FethSetCommand::kSetPeer), payload, Tr(Msg::kNetUnpeerIoctlWhat))); return Ok(); @@ -324,10 +323,10 @@ Result FethInterface::QueryPeer() const { if (!Valid()) { return std::unexpected(Error::Generic(Tr(Msg::kNetQueryPeerInvalidObject))); } - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); FethRequest payload{}; - TETHERKIT_RETURN_IF_ERROR(CallFethDriverIoctl( + TETHERNET_RETURN_IF_ERROR(CallFethDriverIoctl( socket, name_, kGetDriverSpec, static_cast(FethGetCommand::kGetPeer), payload, "ioctl(SIOCGDRVSPEC, IF_FAKE_G_CMD_GET_PEER)")); @@ -339,8 +338,8 @@ Status FethInterface::SetMtu(std::uint32_t mtu) { if (!Valid()) { return std::unexpected(Error::Generic(Tr(Msg::kNetSetMtuInvalidObject))); } - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); - TETHERKIT_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); request.ifr_mtu = static_cast(mtu); if (const auto status = socket.Call(SIOCSIFMTU, &request, "ioctl(SIOCSIFMTU)"); !status) { @@ -358,9 +357,9 @@ Status FethInterface::SetMtu(std::uint32_t mtu) { } Result FethInterface::QueryMtu() const { - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); - TETHERKIT_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); - TETHERKIT_RETURN_IF_ERROR(socket.Call(SIOCGIFMTU, &request, "ioctl(SIOCGIFMTU)")); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); + TETHERNET_RETURN_IF_ERROR(socket.Call(SIOCGIFMTU, &request, "ioctl(SIOCGIFMTU)")); return static_cast(request.ifr_mtu); } @@ -368,8 +367,8 @@ Status FethInterface::SetMacAddress(const MacAddress& mac) { if (!Valid()) { return std::unexpected(Error::Generic(Tr(Msg::kNetSetMacInvalidObject))); } - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); - TETHERKIT_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); // feth_ioctl hands ifr_addr straight to ifnet_set_lladdr(ifp, sa_data, // sa_len); it looks only at sa_len and sa_data and does not validate @@ -386,7 +385,7 @@ Status FethInterface::SetMacAddress(const MacAddress& mac) { return std::unexpected(std::move(error).WithContext( Tr(Msg::kNetSetMacFailed, name_, FormatMac(mac).data()))); } - TETHERKIT_INFO_TR(Msg::kNetMacSet, name_, FormatMac(mac).data()); + TETHERNET_INFO_TR(Msg::kNetMacSet, name_, FormatMac(mac).data()); return Ok(); } @@ -425,12 +424,12 @@ Status FethInterface::SetUp(bool up) { if (!Valid()) { return std::unexpected(Error::Generic(Tr(Msg::kNetSetUpInvalidObject))); } - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); - TETHERKIT_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); // Read the current flags back before changing them, so no other flag bits // get cleared. - TETHERKIT_RETURN_IF_ERROR(socket.Call(SIOCGIFFLAGS, &request, "ioctl(SIOCGIFFLAGS)")); + TETHERNET_RETURN_IF_ERROR(socket.Call(SIOCGIFFLAGS, &request, "ioctl(SIOCGIFFLAGS)")); // ifr_flags is a short, while the IFF_* values can exceed the short range // on 64-bit, so the mask arithmetic uses uint16 as is customary on macOS. @@ -454,9 +453,9 @@ Status FethInterface::SetUp(bool up) { } Result FethInterface::IsUp() const { - TETHERKIT_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); - TETHERKIT_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); - TETHERKIT_RETURN_IF_ERROR(socket.Call(SIOCGIFFLAGS, &request, "ioctl(SIOCGIFFLAGS)")); + TETHERNET_ASSIGN_OR_RETURN(const auto socket, IoctlSocket::Open()); + TETHERNET_ASSIGN_OR_RETURN(::ifreq request, MakeIfreq(name_)); + TETHERNET_RETURN_IF_ERROR(socket.Call(SIOCGIFFLAGS, &request, "ioctl(SIOCGIFFLAGS)")); return (static_cast(request.ifr_flags) & static_cast(IFF_UP)) != 0; } @@ -471,31 +470,31 @@ Result FethPair::Create(std::uint32_t mtu, const MacAddress* system_ma // 1. Verify the sysctls that get snapshotted at creation time -- **must // happen before creating anything**. - TETHERKIT_RETURN_IF_ERROR(VerifyFethSysctls()); + TETHERNET_RETURN_IF_ERROR(VerifyFethSysctls()); - TETHERKIT_ASSIGN_OR_RETURN(const std::uint32_t max_mtu, QueryFethMaxMtu()); + TETHERNET_ASSIGN_OR_RETURN(const std::uint32_t max_mtu, QueryFethMaxMtu()); if (mtu > max_mtu) { return std::unexpected(Error::Generic(Tr(Msg::kNetMtuExceedsFethLimit, mtu, max_mtu))); } // 2. Create the two interfaces. - TETHERKIT_ASSIGN_OR_RETURN(FethInterface system_side, FethInterface::Create()); - TETHERKIT_ASSIGN_OR_RETURN(FethInterface driver_side, FethInterface::Create()); + TETHERNET_ASSIGN_OR_RETURN(FethInterface system_side, FethInterface::Create()); + TETHERNET_ASSIGN_OR_RETURN(FethInterface driver_side, FethInterface::Create()); // 3. Pair -- before UP, so the link is up from the very start. - TETHERKIT_RETURN_IF_ERROR(driver_side.PeerWith(system_side)); + TETHERNET_RETURN_IF_ERROR(driver_side.PeerWith(system_side)); // 4. Both sides need the same MTU, otherwise a frame one side can send is // a frame the other side cannot receive. - TETHERKIT_RETURN_IF_ERROR(system_side.SetMtu(mtu)); - TETHERKIT_RETURN_IF_ERROR(driver_side.SetMtu(mtu)); + TETHERNET_RETURN_IF_ERROR(system_side.SetMtu(mtu)); + TETHERNET_RETURN_IF_ERROR(driver_side.SetMtu(mtu)); // 5. Set the system-side MAC -- **must happen before UP**. // The driver side deliberately keeps the kernel-assigned address: the // two MACs must differ, otherwise both sides end up with the same IPv6 // link-local address and DAD reports an address conflict. if (system_mac != nullptr) { - TETHERKIT_RETURN_IF_ERROR(system_side.SetMacAddress(*system_mac)); + TETHERNET_RETURN_IF_ERROR(system_side.SetMacAddress(*system_mac)); const auto driver_mac = driver_side.QueryMacAddress(); if (driver_mac && *driver_mac == *system_mac) { @@ -507,11 +506,11 @@ Result FethPair::Create(std::uint32_t mtu, const MacAddress* system_ma // 6. Bring both sides UP. // bpfwrite has a hard check: it returns ENETDOWN when the interface is // not IFF_UP. - TETHERKIT_RETURN_IF_ERROR(driver_side.SetUp(true)); - TETHERKIT_RETURN_IF_ERROR(system_side.SetUp(true)); + TETHERNET_RETURN_IF_ERROR(driver_side.SetUp(true)); + TETHERNET_RETURN_IF_ERROR(system_side.SetUp(true)); - TETHERKIT_INFO_TR(Msg::kNetFethPairReady, system_side.Name(), driver_side.Name(), mtu); + TETHERNET_INFO_TR(Msg::kNetFethPairReady, system_side.Name(), driver_side.Name(), mtu); return FethPair{std::move(system_side), std::move(driver_side)}; } -} // namespace tetherkit::net +} // namespace tethernet::net diff --git a/src/net/loopback_link.cc b/src/net/loopback_link.cc index 39392ff..c5d4df1 100644 --- a/src/net/loopback_link.cc +++ b/src/net/loopback_link.cc @@ -1,12 +1,12 @@ -#include "tetherkit/net/loopback_link.h" +#include "tethernet/net/loopback_link.h" -#include "tetherkit/common/i18n.h" +#include "tethernet/common/i18n.h" #include #include #include -namespace tetherkit::net { +namespace tethernet::net { LoopbackLink::LoopbackLink(const LoopbackConfig& config) : config_(config) { inbound_.reserve(config_.inbound_capacity); @@ -97,4 +97,4 @@ std::vector> LoopbackLink::DrainSent() { return drained; } -} // namespace tetherkit::net +} // namespace tethernet::net diff --git a/src/rndis/CMakeLists.txt b/src/rndis/CMakeLists.txt index 499831a..f9256a9 100644 --- a/src/rndis/CMakeLists.txt +++ b/src/rndis/CMakeLists.txt @@ -1,4 +1,4 @@ -# tk_rndis -- the RNDIS protocol layer +# tn_rndis -- the RNDIS protocol layer # # Pure-logic module: only wire-format codec work and state-machine stepping, # **touches no I/O at all**. This constraint is deliberate -- it lets the entire @@ -6,9 +6,9 @@ # device (this project's dev machine has neither a USB device nor root). add_library( - tk_rndis STATIC + tn_rndis STATIC protocol.cc messages.cc state_machine.cc) -target_link_libraries(tk_rndis PUBLIC tetherkit_base tk_common) +target_link_libraries(tn_rndis PUBLIC tethernet_base tn_common) diff --git a/src/rndis/messages.cc b/src/rndis/messages.cc index 802a5b7..719aafb 100644 --- a/src/rndis/messages.cc +++ b/src/rndis/messages.cc @@ -1,20 +1,20 @@ -#include "tetherkit/rndis/messages.h" +#include "tethernet/rndis/messages.h" + +#include "tethernet/common/byte_order.h" +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" #include #include #include #include -#include "tetherkit/common/byte_order.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" - -namespace tetherkit::rndis { +namespace tethernet::rndis { namespace { /// Build an error carrying the RNDIS status code's name. /// -/// The status-code-to-name map lives only in protocol.cc (tk_common knows +/// The status-code-to-name map lives only in protocol.cc (tn_common knows /// nothing about RNDIS), so the name is concatenated straight into the context /// string here and the Error itself keeps only the raw value. Error MakeStatusError(std::uint32_t status, std::string_view what) { @@ -47,7 +47,7 @@ Status RequireCapacity(std::span buffer, std::size_t needed, std::str /// successful status. Status ValidateCompletion(std::span buffer, MessageType expected_type, std::uint32_t minimum_bytes, std::string_view what) { - TETHERKIT_RETURN_IF_ERROR(RequireBytes(buffer, minimum_bytes, what)); + TETHERNET_RETURN_IF_ERROR(RequireBytes(buffer, minimum_bytes, what)); const std::uint32_t message_type = LoadLe32(buffer.data() + kMessageTypeOffset); if (message_type != ToRaw(expected_type)) { @@ -111,7 +111,7 @@ std::array FormatMac(const MacAddress& mac) noexcept { // ============================================================================= Result DecodeMessageHeader(std::span buffer) { - TETHERKIT_RETURN_IF_ERROR( + TETHERNET_RETURN_IF_ERROR( RequireBytes(buffer, kMessageHeaderBytes, Text(Msg::kRndisWhatMessageHeader))); return MessageHeader{ .message_type = LoadLe32(buffer.data() + kMessageTypeOffset), @@ -124,7 +124,7 @@ Result DecodeMessageHeader(std::span buffer) { // ============================================================================= Result Encode(const InitializeRequest& request, std::span buffer) { - TETHERKIT_RETURN_IF_ERROR( + TETHERNET_RETURN_IF_ERROR( RequireCapacity(buffer, kInitializeMsgBytes, "REMOTE_NDIS_INITIALIZE_MSG")); WriteHeader(buffer, MessageType::kInitialize, kInitializeMsgBytes); @@ -136,7 +136,7 @@ Result Encode(const InitializeRequest& request, std::span DecodeInitializeComplete(std::span buffer) { - TETHERKIT_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kInitializeComplete, + TETHERNET_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kInitializeComplete, kInitializeCmpltBytes, "REMOTE_NDIS_INITIALIZE_CMPLT")); @@ -180,7 +180,7 @@ Result Negotiate(const InitializeComplete& complete, if (!connectionless) { // Some devices set neither bit. Be lenient: continue as connectionless // and only warn. - TETHERKIT_WARN_TR(Msg::kRndisNoConnectionlessFlag, complete.device_flags); + TETHERNET_WARN_TR(Msg::kRndisNoConnectionlessFlag, complete.device_flags); } // ---- Version ---- @@ -202,7 +202,7 @@ Result Negotiate(const InitializeComplete& complete, // overflows or produces an absurd padding length. std::uint32_t alignment_factor = complete.packet_alignment_factor; if (alignment_factor > kMaxPacketAlignmentFactor) { - TETHERKIT_WARN_TR(Msg::kRndisAlignmentFactorClamped, alignment_factor, + TETHERNET_WARN_TR(Msg::kRndisAlignmentFactorClamped, alignment_factor, kMaxPacketAlignmentFactor); alignment_factor = kMaxPacketAlignmentFactor; } @@ -219,8 +219,7 @@ Result Negotiate(const InitializeComplete& complete, // 16KB frame caps that are meaningless at this link rate and would only make // us allocate huge transfer buffers. Do not follow the device blindly. if (device_limit > host_transfer_size_limit) { - TETHERKIT_INFO_TR(Msg::kRndisMaxTransferClampedToHost, device_limit, - host_transfer_size_limit); + TETHERNET_INFO_TR(Msg::kRndisMaxTransferClampedToHost, device_limit, host_transfer_size_limit); device_limit = host_transfer_size_limit; } params.device_max_transfer_size = device_limit; @@ -233,7 +232,7 @@ Result Negotiate(const InitializeComplete& complete, const std::uint32_t hard_mtu = HardMtuFor(requested_mtu); if (device_limit < hard_mtu) { params.mtu = device_limit - kHardHeaderBytes; - TETHERKIT_WARN_TR(Msg::kRndisMtuLoweredForTransferSize, device_limit, hard_mtu, requested_mtu, + TETHERNET_WARN_TR(Msg::kRndisMtuLoweredForTransferSize, device_limit, hard_mtu, requested_mtu, params.mtu); } @@ -245,7 +244,7 @@ Result Negotiate(const InitializeComplete& complete, // ============================================================================= Result EncodeHalt(std::uint32_t request_id, std::span buffer) { - TETHERKIT_RETURN_IF_ERROR(RequireCapacity(buffer, kHaltMsgBytes, "REMOTE_NDIS_HALT_MSG")); + TETHERNET_RETURN_IF_ERROR(RequireCapacity(buffer, kHaltMsgBytes, "REMOTE_NDIS_HALT_MSG")); WriteHeader(buffer, MessageType::kHalt, kHaltMsgBytes); StoreLe32(buffer.data() + kHaltRequestIdOffset, request_id); return kHaltMsgBytes; @@ -265,7 +264,7 @@ Result Encode(const QueryRequest& request, std::span b const std::uint32_t info_length = variable_length ? 0 : request.expected_response_bytes; const std::uint32_t message_length = kQuerySetHeaderBytes + info_length; - TETHERKIT_RETURN_IF_ERROR(RequireCapacity(buffer, message_length, "REMOTE_NDIS_QUERY_MSG")); + TETHERNET_RETURN_IF_ERROR(RequireCapacity(buffer, message_length, "REMOTE_NDIS_QUERY_MSG")); WriteHeader(buffer, MessageType::kQuery, message_length); StoreLe32(buffer.data() + kQuerySetRequestIdOffset, request.request_id); @@ -286,9 +285,8 @@ Result Encode(const QueryRequest& request, std::span b } Result DecodeQueryComplete(std::span buffer) { - TETHERKIT_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kQueryComplete, - kQueryCmpltHeaderBytes, - "REMOTE_NDIS_QUERY_CMPLT")); + TETHERNET_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kQueryComplete, + kQueryCmpltHeaderBytes, "REMOTE_NDIS_QUERY_CMPLT")); const std::byte* base = buffer.data(); const std::uint32_t message_length = LoadLe32(base + kMessageLengthOffset); @@ -310,17 +308,16 @@ Result DecodeQueryComplete(std::span buffer) { return complete; } - TETHERKIT_ASSIGN_OR_RETURN( - complete.information, - ResolveInlineBuffer(buffer, message_length, info_offset, info_length, - Text(Msg::kRndisWhatQueryCmpltInfoBuffer))); + TETHERNET_ASSIGN_OR_RETURN(complete.information, + ResolveInlineBuffer(buffer, message_length, info_offset, info_length, + Text(Msg::kRndisWhatQueryCmpltInfoBuffer))); return complete; } Result Encode(const SetRequest& request, std::span buffer) { const auto info_length = static_cast(request.information.size()); const std::uint32_t message_length = kQuerySetHeaderBytes + info_length; - TETHERKIT_RETURN_IF_ERROR(RequireCapacity(buffer, message_length, "REMOTE_NDIS_SET_MSG")); + TETHERNET_RETURN_IF_ERROR(RequireCapacity(buffer, message_length, "REMOTE_NDIS_SET_MSG")); WriteHeader(buffer, MessageType::kSet, message_length); StoreLe32(buffer.data() + kQuerySetRequestIdOffset, request.request_id); @@ -344,9 +341,8 @@ Result EncodeSetUint32(std::uint32_t request_id, Oid oid, std::ui } Result DecodeSetComplete(std::span buffer) { - TETHERKIT_RETURN_IF_ERROR( - ValidateCompletion(buffer, MessageType::kSetComplete, kSetCmpltBytes, - "REMOTE_NDIS_SET_CMPLT")); + TETHERNET_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kSetComplete, kSetCmpltBytes, + "REMOTE_NDIS_SET_CMPLT")); return SetComplete{ .request_id = LoadLe32(buffer.data() + kSetCmpltRequestIdOffset), .status = LoadLe32(buffer.data() + kSetCmpltStatusOffset), @@ -358,7 +354,7 @@ Result DecodeSetComplete(std::span buffer) { // ============================================================================= Result EncodeReset(std::span buffer) { - TETHERKIT_RETURN_IF_ERROR(RequireCapacity(buffer, kResetMsgBytes, "REMOTE_NDIS_RESET_MSG")); + TETHERNET_RETURN_IF_ERROR(RequireCapacity(buffer, kResetMsgBytes, "REMOTE_NDIS_RESET_MSG")); WriteHeader(buffer, MessageType::kReset, kResetMsgBytes); // Offset 8 is Reserved, not RequestId -- it must be written as 0. StoreLe32(buffer.data() + kResetReservedOffset, 0); @@ -366,7 +362,7 @@ Result EncodeReset(std::span buffer) { } Result DecodeResetComplete(std::span buffer) { - TETHERKIT_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kResetComplete, + TETHERNET_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kResetComplete, kResetCmpltBytes, "REMOTE_NDIS_RESET_CMPLT")); // Note the offsets: RESET_CMPLT has no RequestId, so Status sits at // offset 8 rather than 12. @@ -381,7 +377,7 @@ Result DecodeResetComplete(std::span buffer) { // ============================================================================= Result EncodeKeepAlive(std::uint32_t request_id, std::span buffer) { - TETHERKIT_RETURN_IF_ERROR( + TETHERNET_RETURN_IF_ERROR( RequireCapacity(buffer, kKeepAliveMsgBytes, "REMOTE_NDIS_KEEPALIVE_MSG")); WriteHeader(buffer, MessageType::kKeepAlive, kKeepAliveMsgBytes); StoreLe32(buffer.data() + kKeepAliveRequestIdOffset, request_id); @@ -389,7 +385,7 @@ Result EncodeKeepAlive(std::uint32_t request_id, std::span DecodeKeepAliveComplete(std::span buffer) { - TETHERKIT_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kKeepAliveComplete, + TETHERNET_RETURN_IF_ERROR(ValidateCompletion(buffer, MessageType::kKeepAliveComplete, kKeepAliveCmpltBytes, "REMOTE_NDIS_KEEPALIVE_CMPLT")); return KeepAliveComplete{ @@ -400,7 +396,7 @@ Result DecodeKeepAliveComplete(std::span buf Result EncodeKeepAliveComplete(std::uint32_t request_id, std::uint32_t status, std::span buffer) { - TETHERKIT_RETURN_IF_ERROR( + TETHERNET_RETURN_IF_ERROR( RequireCapacity(buffer, kKeepAliveCmpltBytes, "REMOTE_NDIS_KEEPALIVE_CMPLT")); WriteHeader(buffer, MessageType::kKeepAliveComplete, kKeepAliveCmpltBytes); StoreLe32(buffer.data() + kKeepAliveCmpltRequestIdOffset, request_id); @@ -413,8 +409,8 @@ Result EncodeKeepAliveComplete(std::uint32_t request_id, std::uin // ============================================================================= Result DecodeIndicateStatus(std::span buffer) { - TETHERKIT_RETURN_IF_ERROR(RequireBytes(buffer, kIndicateStatusHeaderBytes, - "REMOTE_NDIS_INDICATE_STATUS_MSG")); + TETHERNET_RETURN_IF_ERROR( + RequireBytes(buffer, kIndicateStatusHeaderBytes, "REMOTE_NDIS_INDICATE_STATUS_MSG")); const std::byte* base = buffer.data(); const std::uint32_t message_type = LoadLe32(base + kMessageTypeOffset); @@ -459,7 +455,7 @@ Result DecodeIndicateStatus(std::span buffer) { // **still return success** -- the status itself is useful (it may be // MEDIA_DISCONNECT), and an unparseable optional field must not condemn // the link. - TETHERKIT_WARN_TR(Msg::kRndisIndicateStatusBufferOutOfBounds, relative_offset, buffer_length, + TETHERNET_WARN_TR(Msg::kRndisIndicateStatusBufferOutOfBounds, relative_offset, buffer_length, message_length); return indication; } @@ -482,7 +478,7 @@ Result DecodeIndicateStatus(std::span buffer) { // ============================================================================= Result ParseUint32(std::span information) { - TETHERKIT_RETURN_IF_ERROR(RequireBytes(information, 4, Text(Msg::kRndisWhatOidUint32))); + TETHERNET_RETURN_IF_ERROR(RequireBytes(information, 4, Text(Msg::kRndisWhatOidUint32))); return LoadLe32(information.data()); } @@ -498,11 +494,11 @@ Result ParseCounter(std::span information) { } Result ParseMac(std::span information) { - TETHERKIT_RETURN_IF_ERROR( + TETHERNET_RETURN_IF_ERROR( RequireBytes(information, sizeof(MacAddress), Text(Msg::kRndisWhatOidMac))); MacAddress mac{}; std::memcpy(mac.data(), information.data(), mac.size()); return mac; } -} // namespace tetherkit::rndis +} // namespace tethernet::rndis diff --git a/src/rndis/protocol.cc b/src/rndis/protocol.cc index 523ef40..20456b7 100644 --- a/src/rndis/protocol.cc +++ b/src/rndis/protocol.cc @@ -1,6 +1,6 @@ -#include "tetherkit/rndis/protocol.h" +#include "tethernet/rndis/protocol.h" -namespace tetherkit::rndis { +namespace tethernet::rndis { std::string_view MessageTypeName(std::uint32_t message_type) noexcept { switch (static_cast(message_type)) { @@ -227,4 +227,4 @@ std::string_view OidName(std::uint32_t oid) noexcept { return {}; } -} // namespace tetherkit::rndis +} // namespace tethernet::rndis diff --git a/src/rndis/state_machine.cc b/src/rndis/state_machine.cc index ea33a6d..803238b 100644 --- a/src/rndis/state_machine.cc +++ b/src/rndis/state_machine.cc @@ -1,15 +1,15 @@ -#include "tetherkit/rndis/state_machine.h" +#include "tethernet/rndis/state_machine.h" + +#include "tethernet/common/byte_order.h" +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" #include #include #include #include -#include "tetherkit/common/byte_order.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" - -namespace tetherkit::rndis { +namespace tethernet::rndis { namespace { /// Placeholder buffer size used when QUERYing a fixed-length OID. @@ -81,7 +81,7 @@ void StateMachine::TransitionTo(State next) { } const State previous = state_; state_ = next; - TETHERKIT_INFO_TR(Msg::kRndisStateTransition, StateName(previous), StateName(next)); + TETHERNET_INFO_TR(Msg::kRndisStateTransition, StateName(previous), StateName(next)); observer_->OnStateChanged(previous, next); } @@ -91,8 +91,7 @@ void StateMachine::TransitionTo(State next) { Result StateMachine::PumpOnce(std::optional expected_reply, std::span& out_response) { - TETHERKIT_ASSIGN_OR_RETURN(const std::span message, - channel_->ReceiveMessage()); + TETHERNET_ASSIGN_OR_RETURN(const std::span message, channel_->ReceiveMessage()); if (message.empty()) { // Spec: the device returns a single 0x00 byte instead of STALL while it @@ -101,9 +100,9 @@ Result StateMachine::PumpOnce(std::optional expected_reply, } MarkDeviceActivity(); - TETHERKIT_ASSIGN_OR_RETURN(const MessageHeader header, DecodeMessageHeader(message)); + TETHERNET_ASSIGN_OR_RETURN(const MessageHeader header, DecodeMessageHeader(message)); const std::string_view name = MessageTypeName(header.message_type); - TETHERKIT_DEBUG_TR(Msg::kRndisControlMessageReceived, + TETHERNET_DEBUG_TR(Msg::kRndisControlMessageReceived, name.empty() ? Text(Msg::kRndisUnknownMessage) : name, header.message_type, header.message_length); @@ -115,7 +114,7 @@ Result StateMachine::PumpOnce(std::optional expected_reply, if (header.message_type == ToRaw(MessageType::kKeepAlive)) { // Device-initiated keep-alive. We **must reply**, otherwise the device may // decide the host is dead and disconnect. - TETHERKIT_RETURN_IF_ERROR(HandleDeviceKeepAlive(message)); + TETHERNET_RETURN_IF_ERROR(HandleDeviceKeepAlive(message)); return false; } @@ -135,14 +134,14 @@ Result StateMachine::PumpOnce(std::optional expected_reply, // Not the one we awaited, and not a known push. It may be a late response to // a previously timed-out request -- drop it and continue (treating it as // fatal would let a single timeout kill the link). - TETHERKIT_WARN_TR(Msg::kRndisDiscardUnexpectedMessage, + TETHERNET_WARN_TR(Msg::kRndisDiscardUnexpectedMessage, name.empty() ? Text(Msg::kRndisUnknown) : name, header.message_type); return false; } Result StateMachine::Transact(std::span request, MessageType expected_reply) { - TETHERKIT_RETURN_IF_ERROR(channel_->SendMessage(request)); + TETHERNET_RETURN_IF_ERROR(channel_->SendMessage(request)); // Wait for the interrupt endpoint's RESPONSE_AVAILABLE notification first, // then fetch the response. @@ -164,12 +163,12 @@ Result StateMachine::Transact(std::span const NotificationResult notification = channel_->WaitForNotification(notification_timeout); if (notification == NotificationResult::kResponseAvailable) { - TETHERKIT_TRACE_TR(Msg::kRndisNotificationReceived); + TETHERNET_TRACE_TR(Msg::kRndisNotificationReceived); } std::span response; for (std::uint32_t attempt = 0; attempt < config_.response_poll_attempts; ++attempt) { - TETHERKIT_ASSIGN_OR_RETURN(const bool matched, PumpOnce(expected_reply, response)); + TETHERNET_ASSIGN_OR_RETURN(const bool matched, PumpOnce(expected_reply, response)); if (matched) { return Exchange{.response = response}; } @@ -194,7 +193,7 @@ void StateMachine::HandleIndicateStatus(std::span message) { const auto indication = DecodeIndicateStatus(message); if (!indication) { // A parse failure must not kill the link -- this is a pure notification. - TETHERKIT_WARN_TR(Msg::kRndisIndicateStatusParseFailed, indication.error().ToString()); + TETHERNET_WARN_TR(Msg::kRndisIndicateStatusParseFailed, indication.error().ToString()); return; } @@ -203,12 +202,12 @@ void StateMachine::HandleIndicateStatus(std::span message) { // The device uses Rndis_Diagnostic_Info to tell us "byte N of the message // you sent is invalid", which is invaluable for tracking down our own // encoding bugs, so we log it separately. - TETHERKIT_WARN_TR(Msg::kRndisDeviceIndicatedWithDiagnostics, + TETHERNET_WARN_TR(Msg::kRndisDeviceIndicatedWithDiagnostics, status_name.empty() ? Text(Msg::kRndisUnknownStatus) : status_name, indication->status, indication->diagnostic_status, indication->diagnostic_error_offset); } else { - TETHERKIT_INFO_TR(Msg::kRndisDeviceIndicated, + TETHERNET_INFO_TR(Msg::kRndisDeviceIndicated, status_name.empty() ? Text(Msg::kRndisUnknownStatus) : status_name, indication->status); } @@ -245,9 +244,9 @@ Status StateMachine::HandleDeviceKeepAlive(std::span message) { return std::unexpected(Error::Generic(Tr(Msg::kRndisKeepAliveMsgTooShort))); } const std::uint32_t request_id = LoadLe32(message.data() + kKeepAliveRequestIdOffset); - TETHERKIT_DEBUG_TR(Msg::kRndisDeviceKeepAlive, request_id); + TETHERNET_DEBUG_TR(Msg::kRndisDeviceKeepAlive, request_id); - TETHERKIT_ASSIGN_OR_RETURN( + TETHERNET_ASSIGN_OR_RETURN( const std::uint32_t written, EncodeKeepAliveComplete(request_id, ToRaw(StatusCode::kSuccess), request_buffer_)); return channel_->SendMessage(std::span{request_buffer_.data(), written}); @@ -260,33 +259,31 @@ Status StateMachine::HandleDeviceKeepAlive(std::span message) { Result> StateMachine::QueryOid(Oid oid, std::uint32_t expected_bytes, bool fatal) { const std::string_view name = OidName(ToRaw(oid)); - TETHERKIT_ASSIGN_OR_RETURN( - const std::uint32_t written, - Encode(QueryRequest{.request_id = NextRequestId(), - .oid = oid, - .expected_response_bytes = expected_bytes}, - request_buffer_)); + TETHERNET_ASSIGN_OR_RETURN(const std::uint32_t written, + Encode(QueryRequest{.request_id = NextRequestId(), + .oid = oid, + .expected_response_bytes = expected_bytes}, + request_buffer_)); auto exchange = Transact(std::span{request_buffer_.data(), written}, MessageType::kQueryComplete); if (!exchange) { if (!fatal) { - TETHERKIT_DEBUG_TR(Msg::kRndisOptionalOidQueryFailed, name, exchange.error().ToString()); + TETHERNET_DEBUG_TR(Msg::kRndisOptionalOidQueryFailed, name, exchange.error().ToString()); return std::span{}; } return std::unexpected( std::move(exchange).error().WithContext(Tr(Msg::kRndisOidQueryFailed, name))); } - TETHERKIT_ASSIGN_OR_RETURN(const QueryComplete complete, - DecodeQueryComplete(exchange->response)); + TETHERNET_ASSIGN_OR_RETURN(const QueryComplete complete, DecodeQueryComplete(exchange->response)); if (complete.status != ToRaw(StatusCode::kSuccess)) { const std::string_view status_name = StatusName(complete.status); if (!fatal) { // An optional OID returning NOT_SUPPORTED is perfectly normal (e.g. // OID_GEN_PHYSICAL_MEDIUM). - TETHERKIT_DEBUG_TR(Msg::kRndisOptionalOidUnsupported, name, + TETHERNET_DEBUG_TR(Msg::kRndisOptionalOidUnsupported, name, status_name.empty() ? Text(Msg::kRndisUnknownStatus) : status_name); return std::span{}; } @@ -300,16 +297,14 @@ Result> StateMachine::QueryOid(Oid oid, std::uint32_t Status StateMachine::SetOidUint32(Oid oid, std::uint32_t value) { const std::string_view name = OidName(ToRaw(oid)); - TETHERKIT_ASSIGN_OR_RETURN( - const std::uint32_t written, - EncodeSetUint32(NextRequestId(), oid, value, request_buffer_)); + TETHERNET_ASSIGN_OR_RETURN(const std::uint32_t written, + EncodeSetUint32(NextRequestId(), oid, value, request_buffer_)); - TETHERKIT_ASSIGN_OR_RETURN( - const Exchange exchange, - Transact(std::span{request_buffer_.data(), written}, - MessageType::kSetComplete)); + TETHERNET_ASSIGN_OR_RETURN(const Exchange exchange, + Transact(std::span{request_buffer_.data(), written}, + MessageType::kSetComplete)); - TETHERKIT_ASSIGN_OR_RETURN(const SetComplete complete, DecodeSetComplete(exchange.response)); + TETHERNET_ASSIGN_OR_RETURN(const SetComplete complete, DecodeSetComplete(exchange.response)); if (complete.status != ToRaw(StatusCode::kSuccess)) { const std::string_view status_name = StatusName(complete.status); return std::unexpected(Error::FromRndisStatus( @@ -317,7 +312,7 @@ Status StateMachine::SetOidUint32(Oid oid, std::uint32_t value) { Tr(Msg::kRndisOidSetRejected, name, value, status_name.empty() ? Text(Msg::kRndisUnknownStatus) : status_name))); } - TETHERKIT_DEBUG_TR(Msg::kRndisOidSet, name, value); + TETHERNET_DEBUG_TR(Msg::kRndisOidSet, name, value); return Ok(); } @@ -340,12 +335,12 @@ Status StateMachine::CollectDeviceInfo() { // This is the address the host-side feth will adopt; without it we cannot // build the interface correctly. { - TETHERKIT_ASSIGN_OR_RETURN( + TETHERNET_ASSIGN_OR_RETURN( const std::span payload, QueryOid(Oid::kEthernetPermanentAddress, kFixedOidPlaceholderBytes, /*fatal=*/true)); - TETHERKIT_ASSIGN_OR_RETURN(info_.permanent_address, ParseMac(payload)); + TETHERNET_ASSIGN_OR_RETURN(info_.permanent_address, ParseMac(payload)); info_.has_permanent_address = true; - TETHERKIT_INFO_TR(Msg::kRndisPermanentMac, FormatMac(info_.permanent_address).data()); + TETHERNET_INFO_TR(Msg::kRndisPermanentMac, FormatMac(info_.permanent_address).data()); } // ---- Current MAC: not fatal (most devices report the same as permanent) ---- @@ -371,7 +366,7 @@ Status StateMachine::CollectDeviceInfo() { // The device says it accepts at most N bytes of payload while we // negotiated a larger MTU -- the device wins. if (*value != 0 && *value < parameters_.mtu) { - TETHERKIT_WARN_TR(Msg::kRndisMtuLoweredToDeviceFrameSize, *value, parameters_.mtu); + TETHERNET_WARN_TR(Msg::kRndisMtuLoweredToDeviceFrameSize, *value, parameters_.mtu); parameters_.mtu = *value; } } @@ -412,7 +407,7 @@ Status StateMachine::CollectDeviceInfo() { info_.vendor_description = ToPrintableString(*payload); } - TETHERKIT_INFO_TR(Msg::kRndisDeviceInfo, FormatMac(info_.permanent_address).data(), + TETHERNET_INFO_TR(Msg::kRndisDeviceInfo, FormatMac(info_.permanent_address).data(), info_.LinkSpeedMbps(), info_.maximum_frame_size, static_cast(info_.physical_medium), info_.vendor_id, info_.vendor_description); @@ -429,7 +424,7 @@ Status StateMachine::Start() { // ---- Step 1: INITIALIZE ---- { - TETHERKIT_ASSIGN_OR_RETURN( + TETHERNET_ASSIGN_OR_RETURN( const std::uint32_t written, Encode(InitializeRequest{.request_id = NextRequestId(), .max_transfer_size = config_.host_max_transfer_size}, @@ -461,12 +456,12 @@ Status StateMachine::Start() { // distrust it (see the config field's comment). if (config_.max_tx_packets_per_message != 0 && parameters_.max_packets_per_message > config_.max_tx_packets_per_message) { - TETHERKIT_INFO_TR(Msg::kRndisMaxPacketsClamped, parameters_.max_packets_per_message, + TETHERNET_INFO_TR(Msg::kRndisMaxPacketsClamped, parameters_.max_packets_per_message, config_.max_tx_packets_per_message); parameters_.max_packets_per_message = config_.max_tx_packets_per_message; } - TETHERKIT_INFO_TR(Msg::kRndisNegotiated, complete->major_version, complete->minor_version, + TETHERNET_INFO_TR(Msg::kRndisNegotiated, complete->major_version, complete->minor_version, parameters_.mtu, parameters_.device_max_transfer_size, parameters_.max_packets_per_message, parameters_.tx_alignment_bytes); } @@ -574,13 +569,13 @@ Status StateMachine::Poll() { return Ok(); } - TETHERKIT_ASSIGN_OR_RETURN(const std::uint32_t written, + TETHERNET_ASSIGN_OR_RETURN(const std::uint32_t written, EncodeKeepAlive(NextRequestId(), request_buffer_)); auto exchange = Transact(std::span{request_buffer_.data(), written}, MessageType::kKeepAliveComplete); if (!exchange) { ++consecutive_keepalive_failures_; - TETHERKIT_WARN_TR(Msg::kRndisKeepAliveFailed, consecutive_keepalive_failures_, + TETHERNET_WARN_TR(Msg::kRndisKeepAliveFailed, consecutive_keepalive_failures_, config_.keepalive_failure_threshold, exchange.error().ToString()); if (consecutive_keepalive_failures_ >= config_.keepalive_failure_threshold) { Error fatal = std::move(exchange).error().WithContext( @@ -601,13 +596,13 @@ Status StateMachine::Poll() { // The device explicitly returned a failure status. That usually means it // wants us to reset. const std::string_view name = StatusName(complete->status); - TETHERKIT_WARN_TR(Msg::kRndisKeepAliveRejected, + TETHERNET_WARN_TR(Msg::kRndisKeepAliveRejected, name.empty() ? Text(Msg::kRndisUnknownStatus) : name); return Reset(); } consecutive_keepalive_failures_ = 0; - TETHERKIT_TRACE_TR(Msg::kRndisKeepAliveOk); + TETHERNET_TRACE_TR(Msg::kRndisKeepAliveOk); return Ok(); } @@ -616,18 +611,16 @@ Status StateMachine::Poll() { // ============================================================================= Status StateMachine::Reset() { - TETHERKIT_INFO_TR(Msg::kRndisResetStarted); + TETHERNET_INFO_TR(Msg::kRndisResetStarted); // RESET_MSG has **no RequestId** (offset 8 is Reserved), so it cannot be // paired by ID; only one RESET may be in flight at a time. - TETHERKIT_ASSIGN_OR_RETURN(const std::uint32_t written, EncodeReset(request_buffer_)); - TETHERKIT_ASSIGN_OR_RETURN( - const Exchange exchange, - Transact(std::span{request_buffer_.data(), written}, - MessageType::kResetComplete)); + TETHERNET_ASSIGN_OR_RETURN(const std::uint32_t written, EncodeReset(request_buffer_)); + TETHERNET_ASSIGN_OR_RETURN(const Exchange exchange, + Transact(std::span{request_buffer_.data(), written}, + MessageType::kResetComplete)); - TETHERKIT_ASSIGN_OR_RETURN(const ResetComplete complete, - DecodeResetComplete(exchange.response)); + TETHERNET_ASSIGN_OR_RETURN(const ResetComplete complete, DecodeResetComplete(exchange.response)); if (complete.status != ToRaw(StatusCode::kSuccess)) { const std::string_view name = StatusName(complete.status); @@ -636,9 +629,9 @@ Status StateMachine::Reset() { name.empty() ? Text(Msg::kRndisUnknownStatus) : name))); } - TETHERKIT_INFO_TR(Msg::kRndisResetDone, Text(complete.addressing_reset - ? Msg::kRndisAddressingLost - : Msg::kRndisAddressingKept)); + TETHERNET_INFO_TR( + Msg::kRndisResetDone, + Text(complete.addressing_reset ? Msg::kRndisAddressingLost : Msg::kRndisAddressingKept)); observer_->OnDeviceReset(complete.addressing_reset); if (complete.addressing_reset) { @@ -651,9 +644,9 @@ Status StateMachine::Reset() { // SET OID_802_3_MULTICAST_LIST is ever added, replay it here too.) const std::uint32_t filter = active_packet_filter_ != 0 ? active_packet_filter_ : config_.packet_filter; - TETHERKIT_RETURN_IF_ERROR(SetOidUint32(Oid::kGenCurrentPacketFilter, filter)); + TETHERNET_RETURN_IF_ERROR(SetOidUint32(Oid::kGenCurrentPacketFilter, filter)); active_packet_filter_ = filter; - TETHERKIT_INFO_TR(Msg::kRndisPacketFilterReplayed, filter); + TETHERNET_INFO_TR(Msg::kRndisPacketFilterReplayed, filter); } consecutive_keepalive_failures_ = 0; @@ -670,7 +663,7 @@ void StateMachine::SendHalt() noexcept { // state uninitialized. const auto written = EncodeHalt(NextRequestId(), request_buffer_); if (!written) { - TETHERKIT_WARN_TR(Msg::kRndisEncodeHaltFailed, written.error().ToString()); + TETHERNET_WARN_TR(Msg::kRndisEncodeHaltFailed, written.error().ToString()); return; } if (const auto status = @@ -678,10 +671,10 @@ void StateMachine::SendHalt() noexcept { !status) { // Failures on the shutdown path are only logged -- the device may already // have been unplugged, which is perfectly normal. - TETHERKIT_DEBUG_TR(Msg::kRndisSendHaltFailed, status.error().ToString()); + TETHERNET_DEBUG_TR(Msg::kRndisSendHaltFailed, status.error().ToString()); return; } - TETHERKIT_DEBUG_TR(Msg::kRndisHaltSent); + TETHERNET_DEBUG_TR(Msg::kRndisHaltSent); } void StateMachine::Stop() { @@ -696,7 +689,7 @@ void StateMachine::Stop() { // believing the host is still there, and the next plug-in starts unclean. if (active_packet_filter_ != 0) { if (const auto status = SetOidUint32(Oid::kGenCurrentPacketFilter, 0); !status) { - TETHERKIT_DEBUG_TR(Msg::kRndisClearPacketFilterFailed, status.error().ToString()); + TETHERNET_DEBUG_TR(Msg::kRndisClearPacketFilterFailed, status.error().ToString()); } else { active_packet_filter_ = 0; } @@ -706,4 +699,4 @@ void StateMachine::Stop() { TransitionTo(State::kUninitialized); } -} // namespace tetherkit::rndis +} // namespace tethernet::rndis diff --git a/src/usb/CMakeLists.txt b/src/usb/CMakeLists.txt index 6687689..062f9cc 100644 --- a/src/usb/CMakeLists.txt +++ b/src/usb/CMakeLists.txt @@ -1,16 +1,16 @@ -# tk_usb -- USB transport layer +# tn_usb -- USB transport layer # # Wraps libusb: device discovery and identification, interface claiming, # control channel, interrupt notifications, and the asynchronous bulk transfer -# pool. It depends on tk_rndis only for protocol constants (interface +# pool. It depends on tn_rndis only for protocol constants (interface # signatures, control request codes, notification format) -- not the state # machine. add_library( - tk_usb STATIC + tn_usb STATIC context.cc device.cc data_channel.cc) -target_link_libraries(tk_usb PUBLIC tetherkit_base tk_common tk_rndis) -target_link_libraries(tk_usb PUBLIC LibUSB::LibUSB) +target_link_libraries(tn_usb PUBLIC tethernet_base tn_common tn_rndis) +target_link_libraries(tn_usb PUBLIC LibUSB::LibUSB) diff --git a/src/usb/context.cc b/src/usb/context.cc index 696d141..56ce7f8 100644 --- a/src/usb/context.cc +++ b/src/usb/context.cc @@ -1,12 +1,12 @@ -#include "tetherkit/usb/context.h" +#include "tethernet/usb/context.h" -#include +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" +#include "tethernet/common/scheduling.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" -#include "tetherkit/common/scheduling.h" +#include -namespace tetherkit::usb { +namespace tethernet::usb { namespace { /// Upper bound on how long a single handle_events call may block in the event @@ -32,9 +32,9 @@ Result> Context::Create() { return std::unexpected(Error::FromLibUsb(rc, Tr(Msg::kUsbInitFailed))); } - TETHERKIT_INFO_TR(Msg::kUsbInitialized, VersionString(), - Text(SupportsHotplug() ? Msg::kUsbHotplugSupported - : Msg::kUsbHotplugUnsupported)); + TETHERNET_INFO_TR( + Msg::kUsbInitialized, VersionString(), + Text(SupportsHotplug() ? Msg::kUsbHotplugSupported : Msg::kUsbHotplugUnsupported)); context->running_.store(true, std::memory_order_release); context->event_thread_ = std::thread([raw = context.get()] { raw->RunEventLoop(); }); @@ -58,7 +58,7 @@ Context::~Context() { ::libusb_exit(context_); context_ = nullptr; } - TETHERKIT_DEBUG_TR(Msg::kUsbContextReleased); + TETHERNET_DEBUG_TR(Msg::kUsbContextReleased); } std::string Context::VersionString() { @@ -83,7 +83,7 @@ void Context::RequestStop() noexcept { void Context::RunEventLoop() noexcept { ConfigureCurrentThread("usb-event", ThreadRole::kDataPath); - TETHERKIT_DEBUG_TR(Msg::kUsbEventThreadStarted); + TETHERNET_DEBUG_TR(Msg::kUsbEventThreadStarted); ::timeval timeout{}; timeout.tv_sec = kEventLoopTimeoutSeconds; @@ -102,15 +102,15 @@ void Context::RunEventLoop() noexcept { if (rc == LIBUSB_ERROR_NO_DEVICE) { // The device was unplugged. Not the event loop's fault; upper-level // reconnect logic handles it, so keep running here. - TETHERKIT_DEBUG_TR(Msg::kUsbEventLoopNoDevice); + TETHERNET_DEBUG_TR(Msg::kUsbEventLoopNoDevice); continue; } - TETHERKIT_ERROR_TR(Msg::kUsbHandleEventsFailed, ::libusb_error_name(rc), rc); + TETHERNET_ERROR_TR(Msg::kUsbHandleEventsFailed, ::libusb_error_name(rc), rc); break; } running_.store(false, std::memory_order_release); - TETHERKIT_DEBUG_TR(Msg::kUsbEventThreadExited); + TETHERNET_DEBUG_TR(Msg::kUsbEventThreadExited); } -} // namespace tetherkit::usb +} // namespace tethernet::usb diff --git a/src/usb/data_channel.cc b/src/usb/data_channel.cc index b0a5bad..c6db616 100644 --- a/src/usb/data_channel.cc +++ b/src/usb/data_channel.cc @@ -1,15 +1,15 @@ -#include "tetherkit/usb/data_channel.h" +#include "tethernet/usb/data_channel.h" + +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" +#include "tethernet/rndis/packet_codec.h" #include #include #include -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" -#include "tetherkit/rndis/packet_codec.h" - -namespace tetherkit::usb { +namespace tethernet::usb { namespace { /// Alignment in bytes for the transfer buffers. @@ -53,9 +53,9 @@ Result> UsbDataChannel::Create( rndis::kPacketMsgHeaderBytes + parameters.mtu + rndis::kEthernetHeaderBytes))); } - TETHERKIT_RETURN_IF_ERROR(channel->AllocatePool(channel->rx_pool_, config.rx_transfer_count, - config.rx_transfer_bytes)); - TETHERKIT_RETURN_IF_ERROR(channel->AllocatePool(channel->tx_pool_, config.tx_transfer_count, + TETHERNET_RETURN_IF_ERROR( + channel->AllocatePool(channel->rx_pool_, config.rx_transfer_count, config.rx_transfer_bytes)); + TETHERNET_RETURN_IF_ERROR(channel->AllocatePool(channel->tx_pool_, config.tx_transfer_count, channel->tx_transfer_bytes_)); // TX free-slot queue: everything starts out free. @@ -66,9 +66,8 @@ Result> UsbDataChannel::Create( } } - TETHERKIT_INFO_TR( - Msg::kUsbDataChannelReady, - config.rx_transfer_count, config.rx_transfer_bytes / 1024, + TETHERNET_INFO_TR( + Msg::kUsbDataChannelReady, config.rx_transfer_count, config.rx_transfer_bytes / 1024, config.rx_transfer_count * config.rx_transfer_bytes / 1024, config.tx_transfer_count, channel->tx_transfer_bytes_ / 1024, parameters.device_max_transfer_size, parameters.max_packets_per_message, parameters.tx_alignment_bytes); @@ -137,9 +136,9 @@ Status UsbDataChannel::StartReceiving(FrameRing& rx_ring, DirectionCounters& rx_ rx_counters_ = &rx_counters; for (Slot& slot : rx_pool_) { - TETHERKIT_RETURN_IF_ERROR(SubmitReceive(slot)); + TETHERNET_RETURN_IF_ERROR(SubmitReceive(slot)); } - TETHERKIT_DEBUG_TR(Msg::kUsbBulkInSubmitted, rx_pool_.size()); + TETHERNET_DEBUG_TR(Msg::kUsbBulkInSubmitted, rx_pool_.size()); return Ok(); } @@ -213,9 +212,8 @@ void UsbDataChannel::OnReceiveComplete(Slot& slot) noexcept { if (outcome == rndis::ReadOutcome::kMalformed) { malformed_transfers_.fetch_add(1, std::memory_order_relaxed); rx_counters_->AddDroppedMalformed(); - TETHERKIT_TRACE_TR(Msg::kUsbMalformedRndisMessage, - rndis::MalformedReasonName(reader.Reason()), - reader.FramesDecoded()); + TETHERNET_TRACE_TR(Msg::kUsbMalformedRndisMessage, + rndis::MalformedReasonName(reader.Reason()), reader.FramesDecoded()); } break; } @@ -236,9 +234,9 @@ void UsbDataChannel::OnReceiveComplete(Slot& slot) noexcept { // thread for tens of microseconds to milliseconds, hence only on a real // STALL). rx_counters_->AddIoError(); - TETHERKIT_WARN_TR(Msg::kUsbBulkInStall); + TETHERNET_WARN_TR(Msg::kUsbBulkInStall); if (const auto status = device_->ClearHalt(device_->BulkInEndpoint()); !status) { - TETHERKIT_ERROR_TR(Msg::kUsbBulkInClearHaltFailed, status.error().ToString()); + TETHERNET_ERROR_TR(Msg::kUsbBulkInClearHaltFailed, status.error().ToString()); should_resubmit = false; } break; @@ -252,7 +250,7 @@ void UsbDataChannel::OnReceiveComplete(Slot& slot) noexcept { // Device unplugged. Stop resubmitting so the in-flight count // converges; upper-layer reconnection logic takes it from here. should_resubmit = false; - TETHERKIT_INFO_TR(Msg::kUsbBulkInDeviceGone); + TETHERNET_INFO_TR(Msg::kUsbBulkInDeviceGone); break; case LIBUSB_TRANSFER_TIMED_OUT: @@ -265,7 +263,7 @@ void UsbDataChannel::OnReceiveComplete(Slot& slot) noexcept { case LIBUSB_TRANSFER_OVERFLOW: default: rx_counters_->AddIoError(); - TETHERKIT_WARN_TR(Msg::kUsbBulkInFailed, static_cast(transfer->status)); + TETHERNET_WARN_TR(Msg::kUsbBulkInFailed, static_cast(transfer->status)); break; } @@ -282,7 +280,7 @@ void UsbDataChannel::OnReceiveComplete(Slot& slot) noexcept { if (rc == LIBUSB_SUCCESS) { return; // in-flight count unchanged: one out, one in } - TETHERKIT_WARN_TR(Msg::kUsbBulkInResubmitFailed, ::libusb_error_name(rc)); + TETHERNET_WARN_TR(Msg::kUsbBulkInResubmitFailed, ::libusb_error_name(rc)); } // The transfer has left flight. Wake any Shutdown() waiting for zero. @@ -360,7 +358,7 @@ Result UsbDataChannel::SendFrames(std::span frames ++outcome.skipped; } if (!tx_free_slots_->TryPush(slot_index)) { - TETHERKIT_ERROR_TR(Msg::kUsbReturnTxSlotFailed, slot_index); + TETHERNET_ERROR_TR(Msg::kUsbReturnTxSlotFailed, slot_index); } continue; } @@ -381,7 +379,7 @@ Result UsbDataChannel::SendFrames(std::span frames if (rc != LIBUSB_SUCCESS) { outstanding_.fetch_sub(1, std::memory_order_acq_rel); if (!tx_free_slots_->TryPush(slot_index)) { - TETHERKIT_ERROR_TR(Msg::kUsbReturnTxSlotFailed, slot_index); + TETHERNET_ERROR_TR(Msg::kUsbReturnTxSlotFailed, slot_index); } async_send_errors_.fetch_add(1, std::memory_order_relaxed); return std::unexpected(Error::FromLibUsb(rc, Tr(Msg::kUsbSubmitBulkOutFailed))); @@ -390,7 +388,7 @@ Result UsbDataChannel::SendFrames(std::span frames // Frames and bytes are counted by the **bridge** (it is the only writer // of the TX counters). Here we only log the aggregation effect for // diagnostics. - TETHERKIT_TRACE_TR(Msg::kUsbBulkOutSubmitted, message_count, payload_bytes, transfer_bytes); + TETHERNET_TRACE_TR(Msg::kUsbBulkOutSubmitted, message_count, payload_bytes, transfer_bytes); outcome.consumed += scanned; outcome.sent_frames += appended; outcome.sent_bytes += payload_bytes; @@ -420,9 +418,9 @@ void UsbDataChannel::OnSendComplete(Slot& slot) noexcept { case LIBUSB_TRANSFER_STALL: async_send_errors_.fetch_add(1, std::memory_order_relaxed); - TETHERKIT_WARN_TR(Msg::kUsbBulkOutStall); + TETHERNET_WARN_TR(Msg::kUsbBulkOutStall); if (const auto status = device_->ClearHalt(device_->BulkOutEndpoint()); !status) { - TETHERKIT_ERROR_TR(Msg::kUsbBulkOutClearHaltFailed, status.error().ToString()); + TETHERNET_ERROR_TR(Msg::kUsbBulkOutClearHaltFailed, status.error().ToString()); } break; @@ -432,7 +430,7 @@ void UsbDataChannel::OnSendComplete(Slot& slot) noexcept { default: async_send_errors_.fetch_add(1, std::memory_order_relaxed); - TETHERKIT_WARN_TR(Msg::kUsbBulkOutFailed, static_cast(slot.transfer->status)); + TETHERNET_WARN_TR(Msg::kUsbBulkOutFailed, static_cast(slot.transfer->status)); break; } @@ -440,7 +438,7 @@ void UsbDataChannel::OnSendComplete(Slot& slot) noexcept { // nobody will take one anyway. if (!shutting_down_.load(std::memory_order_acquire)) { if (!tx_free_slots_->TryPush(slot.index)) { - TETHERKIT_ERROR_TR(Msg::kUsbReturnTxSlotFailed, slot.index); + TETHERNET_ERROR_TR(Msg::kUsbReturnTxSlotFailed, slot.index); } // Wake a TX thread that may be waiting for a slot in // WaitForSendCapacity(). Empty-critical-section idiom: take the lock @@ -490,7 +488,7 @@ void UsbDataChannel::Shutdown() { if (slot.transfer != nullptr) { const int rc = ::libusb_cancel_transfer(slot.transfer); if (rc != LIBUSB_SUCCESS && rc != LIBUSB_ERROR_NOT_FOUND) { - TETHERKIT_DEBUG_TR(Msg::kUsbCancelBulkInReturned, ::libusb_error_name(rc)); + TETHERNET_DEBUG_TR(Msg::kUsbCancelBulkInReturned, ::libusb_error_name(rc)); } } } @@ -498,7 +496,7 @@ void UsbDataChannel::Shutdown() { if (slot.transfer != nullptr) { const int rc = ::libusb_cancel_transfer(slot.transfer); if (rc != LIBUSB_SUCCESS && rc != LIBUSB_ERROR_NOT_FOUND) { - TETHERKIT_DEBUG_TR(Msg::kUsbCancelBulkOutReturned, ::libusb_error_name(rc)); + TETHERNET_DEBUG_TR(Msg::kUsbCancelBulkOutReturned, ::libusb_error_name(rc)); } } } @@ -517,7 +515,7 @@ void UsbDataChannel::Shutdown() { // still touch them), so **deliberately leak** -- leaking a few hundred // KB beats a use-after-free crash. const std::uint32_t stuck = outstanding_.load(std::memory_order_acquire); - TETHERKIT_ERROR_TR(Msg::kUsbTransferReclaimTimeout, stuck, + TETHERNET_ERROR_TR(Msg::kUsbTransferReclaimTimeout, stuck, (stuck * config_.rx_transfer_bytes) / 1024); // Null the transfer pointers so FreePool skips them. for (Slot& slot : rx_pool_) { @@ -532,7 +530,7 @@ void UsbDataChannel::Shutdown() { } shutdown_complete_ = true; - TETHERKIT_DEBUG_TR(Msg::kUsbDataChannelStopped); + TETHERNET_DEBUG_TR(Msg::kUsbDataChannelStopped); } -} // namespace tetherkit::usb +} // namespace tethernet::usb diff --git a/src/usb/device.cc b/src/usb/device.cc index 25061d3..3657030 100644 --- a/src/usb/device.cc +++ b/src/usb/device.cc @@ -1,4 +1,8 @@ -#include "tetherkit/usb/device.h" +#include "tethernet/usb/device.h" + +#include "tethernet/common/byte_order.h" +#include "tethernet/common/i18n.h" +#include "tethernet/common/logging.h" #include #include @@ -6,11 +10,7 @@ #include #include -#include "tetherkit/common/byte_order.h" -#include "tetherkit/common/i18n.h" -#include "tetherkit/common/logging.h" - -namespace tetherkit::usb { +namespace tethernet::usb { namespace { /// CDC functional descriptor type (bDescriptorType = CS_INTERFACE). @@ -182,7 +182,7 @@ Result> FindRndisDevices(const Context& context, continue; } if (LooksLikeRealAcmModem(descriptor)) { - TETHERKIT_DEBUG_TR(Msg::kUsbSkippedAcmModem, device_descriptor.idVendor, + TETHERNET_DEBUG_TR(Msg::kUsbSkippedAcmModem, device_descriptor.idVendor, device_descriptor.idProduct, descriptor.bInterfaceNumber); continue; } @@ -218,7 +218,7 @@ Result> FindRndisDevices(const Context& context, } if (!resolved) { - TETHERKIT_DEBUG_TR(Msg::kUsbNoPairedDataInterface, candidate.Describe(), + TETHERNET_DEBUG_TR(Msg::kUsbNoPairedDataInterface, candidate.Describe(), candidate.control_interface); continue; } @@ -228,7 +228,7 @@ Result> FindRndisDevices(const Context& context, // log with duplicates. What was "discovered" is the caller's call to // present -- the CLI prints its own list, the GUI shows it on the // device card. - TETHERKIT_DEBUG_TR( + TETHERNET_DEBUG_TR( Msg::kUsbDeviceFound, candidate.Describe(), candidate.control_interface, candidate.data_interface, signature.interface_class, signature.interface_subclass, signature.interface_protocol, @@ -309,12 +309,10 @@ Result> Device::Open(const Context& context, return std::unexpected(std::move(error)); }; - TETHERKIT_RETURN_IF_ERROR( - claim(candidate.control_interface, Text(Msg::kUsbControlInterface), - device->control_interface_claimed_)); - TETHERKIT_RETURN_IF_ERROR( - claim(candidate.data_interface, Text(Msg::kUsbDataInterface), - device->data_interface_claimed_)); + TETHERNET_RETURN_IF_ERROR(claim(candidate.control_interface, Text(Msg::kUsbControlInterface), + device->control_interface_claimed_)); + TETHERNET_RETURN_IF_ERROR(claim(candidate.data_interface, Text(Msg::kUsbDataInterface), + device->data_interface_claimed_)); // Resolve the endpoints. ::libusb_config_descriptor* config = nullptr; @@ -325,9 +323,9 @@ Result> Device::Open(const Context& context, const std::unique_ptr<::libusb_config_descriptor, void (*)(::libusb_config_descriptor*)> config_guard(config, &::libusb_free_config_descriptor); - TETHERKIT_RETURN_IF_ERROR(device->ResolveEndpoints(*config)); + TETHERNET_RETURN_IF_ERROR(device->ResolveEndpoints(*config)); - TETHERKIT_INFO_TR(Msg::kUsbClaimed, device->description_, device->SpeedName(), + TETHERNET_INFO_TR(Msg::kUsbClaimed, device->description_, device->SpeedName(), device->bulk_in_endpoint_, device->bulk_out_endpoint_, device->bulk_max_packet_size_, device->interrupt_in_endpoint_ == 0 @@ -357,7 +355,7 @@ Device::~Device() { } ::libusb_close(handle_); handle_ = nullptr; - TETHERKIT_DEBUG_TR(Msg::kUsbClosed, description_); + TETHERNET_DEBUG_TR(Msg::kUsbClosed, description_); } Status Device::ResolveEndpoints(const ::libusb_config_descriptor& config) { @@ -467,7 +465,7 @@ UsbControlChannel::~UsbControlChannel() { Status UsbControlChannel::StartNotificationListener() { if (device_->InterruptInEndpoint() == 0) { - TETHERKIT_DEBUG_TR(Msg::kUsbNoInterruptEndpoint); + TETHERNET_DEBUG_TR(Msg::kUsbNoInterruptEndpoint); return Ok(); } if (notification_transfer_ != nullptr) { @@ -495,7 +493,7 @@ Status UsbControlChannel::StartNotificationListener() { notification_in_flight_.store(false, std::memory_order_release); return std::unexpected(Error::FromLibUsb(rc, Tr(Msg::kUsbSubmitInterruptFailed))); } - TETHERKIT_DEBUG_TR(Msg::kUsbNotificationStarted, device_->InterruptInEndpoint()); + TETHERNET_DEBUG_TR(Msg::kUsbNotificationStarted, device_->InterruptInEndpoint()); return Ok(); } @@ -508,7 +506,7 @@ void UsbControlChannel::StopNotificationListener() { if (notification_in_flight_.load(std::memory_order_acquire)) { const int rc = ::libusb_cancel_transfer(notification_transfer_); if (rc != LIBUSB_SUCCESS && rc != LIBUSB_ERROR_NOT_FOUND) { - TETHERKIT_DEBUG_TR(Msg::kUsbCancelInterruptReturned, ::libusb_error_name(rc)); + TETHERNET_DEBUG_TR(Msg::kUsbCancelInterruptReturned, ::libusb_error_name(rc)); } } @@ -523,7 +521,7 @@ void UsbControlChannel::StopNotificationListener() { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (notification_in_flight_.load(std::memory_order_acquire)) { - TETHERKIT_ERROR_TR(Msg::kUsbInterruptReclaimTimeout); + TETHERNET_ERROR_TR(Msg::kUsbInterruptReclaimTimeout); notification_transfer_ = nullptr; // deliberate leak } } @@ -545,7 +543,7 @@ void UsbControlChannel::OnNotificationComplete() noexcept { if (notification == rndis::kNotificationResponseAvailable) { notification_pending_.store(true, std::memory_order_release); } else { - TETHERKIT_TRACE_TR(Msg::kUsbUnknownNotification, notification); + TETHERNET_TRACE_TR(Msg::kUsbUnknownNotification, notification); } } break; @@ -561,14 +559,13 @@ void UsbControlChannel::OnNotificationComplete() noexcept { // endpoint (Linux's host driver does not use the interrupt endpoint at // all, so this is not fatal). if (const auto status = device_->ClearHalt(device_->InterruptInEndpoint()); !status) { - TETHERKIT_WARN_TR(Msg::kUsbInterruptHaltClearFailed, status.error().ToString()); + TETHERNET_WARN_TR(Msg::kUsbInterruptHaltClearFailed, status.error().ToString()); resubmit = false; } break; default: - TETHERKIT_TRACE_TR(Msg::kUsbInterruptTransferStatus, - static_cast(transfer->status)); + TETHERNET_TRACE_TR(Msg::kUsbInterruptTransferStatus, static_cast(transfer->status)); break; } @@ -664,4 +661,4 @@ rndis::NotificationResult UsbControlChannel::WaitForNotification(std::uint32_t t return rndis::NotificationResult::kTimeout; } -} // namespace tetherkit::usb +} // namespace tethernet::usb diff --git a/src/version.cc.in b/src/version.cc.in index 8b70a99..2768508 100644 --- a/src/version.cc.in +++ b/src/version.cc.in @@ -1,20 +1,20 @@ // Generated by CMake from src/version.cc.in; do not edit by hand. -#include "tetherkit/version.h" +#include "tethernet/version.h" -namespace tetherkit { +namespace tethernet { Version GetVersion() noexcept { - return Version{@TetherKit_VERSION_MAJOR@, @TetherKit_VERSION_MINOR@, @TetherKit_VERSION_PATCH@}; + return Version{@Tethernet_VERSION_MAJOR@, @Tethernet_VERSION_MINOR@, @Tethernet_VERSION_PATCH@}; } std::string_view GetVersionString() noexcept { - return "TetherKit @TetherKit_VERSION@ (C++@CMAKE_CXX_STANDARD@, macOS @TETHERKIT_MIN_MACOS_VERSION@+)"; + return "Tethernet @Tethernet_VERSION@ (C++@CMAKE_CXX_STANDARD@, macOS @TETHERNET_MIN_MACOS_VERSION@+)"; } std::string_view GetBuildDescription() noexcept { return "@CMAKE_BUILD_TYPE@, @CMAKE_CXX_COMPILER_ID@ @CMAKE_CXX_COMPILER_VERSION@, " - "C++@CMAKE_CXX_STANDARD@, native-arch=@TETHERKIT_NATIVE_ARCH@, " - "LTO=@TETHERKIT_ENABLE_LTO@, sanitizers=@TETHERKIT_SANITIZER_SUMMARY@"; + "C++@CMAKE_CXX_STANDARD@, native-arch=@TETHERNET_NATIVE_ARCH@, " + "LTO=@TETHERNET_ENABLE_LTO@, sanitizers=@TETHERNET_SANITIZER_SUMMARY@"; } -} // namespace tetherkit +} // namespace tethernet diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 06a1d52..02f7729 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,6 @@ # tests -- unit tests # -# Every test is linked into a single executable, tetherkit_tests, because: +# Every test is linked into a single executable, tethernet_tests, because: # * doctest registration is global, and one binary has the lowest startup # overhead; # * the module can be run via doctest's --test-suite filter alone; @@ -8,7 +8,7 @@ # pinpoint exactly which module broke. add_executable( - tetherkit_tests + tethernet_tests test_main.cc test_version.cc test_common_byte_order.cc @@ -24,15 +24,15 @@ add_executable( test_core_bridge.cc test_capi.cc) -# tk_rndis / tk_net both PUBLIC-link tk_common, so it does not need repeating +# tn_rndis / tn_net both PUBLIC-link tn_common, so it does not need repeating # (otherwise the linker prints "ignoring duplicate libraries"). -# tk_core PUBLIC-links tk_common/tk_rndis/tk_net/tk_usb, so listing it alone is +# tn_core PUBLIC-links tn_common/tn_rndis/tn_net/tn_usb, so listing it alone is # enough. -# tk_capi PUBLIC-links tk_version and tk_core, so neither needs repeating. -target_link_libraries(tetherkit_tests PRIVATE tetherkit_base doctest tk_capi) +# tn_capi PUBLIC-links tn_version and tn_core, so neither needs repeating. +target_link_libraries(tethernet_tests PRIVATE tethernet_base doctest tn_capi) # The src/capi directory: capi_support.h is an implementation-internal header, # not installed publicly, but it must be includable directly by the tests. -target_include_directories(tetherkit_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" +target_include_directories(tethernet_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" "${PROJECT_SOURCE_DIR}/src/capi") # ------------------------------------------------------------------------------ @@ -60,9 +60,9 @@ target_include_directories(tetherkit_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" # **authoritative result** -- even if a suite gets forgotten in the list below, # it will expose the failure. # ------------------------------------------------------------------------------ -add_test(NAME all COMMAND tetherkit_tests) +add_test(NAME all COMMAND tethernet_tests) -set(TETHERKIT_TEST_SUITES +set(TETHERNET_TEST_SUITES version common.byte_order common.error @@ -86,8 +86,8 @@ set(TETHERKIT_TEST_SUITES capi.process capi.net_config) -foreach(_suite IN LISTS TETHERKIT_TEST_SUITES) +foreach(_suite IN LISTS TETHERNET_TEST_SUITES) # Note: do **not** put quotes after --test-suite=, see the explanation above. - add_test(NAME "${_suite}" COMMAND tetherkit_tests --test-suite=${_suite}) + add_test(NAME "${_suite}" COMMAND tethernet_tests --test-suite=${_suite}) set_tests_properties("${_suite}" PROPERTIES FAIL_REGULAR_EXPRESSION "assertions: +0 \\|") endforeach() diff --git a/tests/language_guard.h b/tests/language_guard.h index 15c1c3e..5e922cc 100644 --- a/tests/language_guard.h +++ b/tests/language_guard.h @@ -12,9 +12,9 @@ // `const ScopedLanguage guard{Language::kEnglish};`. #pragma once -#include "tetherkit/common/i18n.h" +#include "tethernet/common/i18n.h" -namespace tetherkit::testing { +namespace tethernet::testing { /// Switches the language within the scope; restores the entry value in the /// destructor. @@ -35,4 +35,4 @@ class ScopedLanguage { Language previous_; }; -} // namespace tetherkit::testing +} // namespace tethernet::testing diff --git a/tests/mock_control_channel.h b/tests/mock_control_channel.h index b8499e6..d0b30d0 100644 --- a/tests/mock_control_channel.h +++ b/tests/mock_control_channel.h @@ -6,9 +6,15 @@ // real device yet the most error-prone ones. #pragma once +#include "tethernet/common/byte_order.h" +#include "tethernet/rndis/control_channel.h" +#include "tethernet/rndis/messages.h" +#include "tethernet/rndis/protocol.h" +#include "tethernet/rndis/state_machine.h" + +#include #include #include -#include #include #include #include @@ -16,13 +22,7 @@ #include #include -#include "tetherkit/common/byte_order.h" -#include "tetherkit/rndis/control_channel.h" -#include "tetherkit/rndis/messages.h" -#include "tetherkit/rndis/protocol.h" -#include "tetherkit/rndis/state_machine.h" - -namespace tetherkit::testing { +namespace tethernet::testing { /// Control channel that emulates device behavior. class MockControlChannel final : public rndis::ControlChannel { @@ -432,4 +432,4 @@ class RecordingObserver final : public rndis::StateMachineObserver { rndis::DeviceInfo info_snapshot{}; }; -} // namespace tetherkit::testing +} // namespace tethernet::testing diff --git a/tests/mock_data_channel.h b/tests/mock_data_channel.h index 34af2bc..f15a91c 100644 --- a/tests/mock_data_channel.h +++ b/tests/mock_data_channel.h @@ -12,6 +12,9 @@ // correctness under TSan. #pragma once +#include "tethernet/common/frame_ring.h" +#include "tethernet/usb/data_channel.h" + #include #include #include @@ -21,10 +24,7 @@ #include #include -#include "tetherkit/common/frame_ring.h" -#include "tetherkit/usb/data_channel.h" - -namespace tetherkit::testing { +namespace tethernet::testing { /// Data channel that emulates device behavior. class MockDataChannel final : public usb::DataChannel { @@ -218,4 +218,4 @@ class MockDataChannel final : public usb::DataChannel { std::atomic async_send_errors_{0}; }; -} // namespace tetherkit::testing +} // namespace tethernet::testing diff --git a/tests/test_capi.cc b/tests/test_capi.cc index 023b2c6..d8eafc1 100644 --- a/tests/test_capi.cc +++ b/tests/test_capi.cc @@ -6,19 +6,20 @@ // // Session and network configuration require root and a real device, so they are not tested // here -- only their argument-validation branches. +#include "tethernet/capi/tethernet_c.h" +#include "tethernet/common/logging.h" + +#include + #include #include #include -#include - #include "capi_support.h" #include "process_runner.h" -#include "tetherkit/capi/tetherkit_c.h" -#include "tetherkit/common/logging.h" -using tetherkit::capi::CopyText; -using tetherkit::capi::IsValidFethName; +using tethernet::capi::CopyText; +using tethernet::capi::IsValidFethName; TEST_SUITE("capi.support") { TEST_CASE("CopyText copies normally and adds a terminator") { @@ -82,15 +83,15 @@ TEST_SUITE("capi.support") { namespace { -using tetherkit::capi::DeviceIdentity; -using tetherkit::capi::ReconcileDeviceStrings; -using tetherkit::capi::RememberedDeviceStrings; +using tethernet::capi::DeviceIdentity; +using tethernet::capi::ReconcileDeviceStrings; +using tethernet::capi::RememberedDeviceStrings; /// Build an enumeration result with identity and strings filled in. Pass an empty string /// to mean "not read this time". -tk_device_info_t DeviceInfo(const DeviceIdentity& identity, const char* manufacturer, +tn_device_info_t DeviceInfo(const DeviceIdentity& identity, const char* manufacturer, const char* product, const char* serial) { - tk_device_info_t info{}; + tn_device_info_t info{}; info.vendor_id = identity.vendor_id; info.product_id = identity.product_id; info.bus_number = identity.bus_number; @@ -112,7 +113,7 @@ TEST_CASE("ReconcileDeviceStrings backfills the last successfully read name when const std::vector present{phone}; // First time: read while idle -> remember it. - std::vector infos{DeviceInfo(phone, "vivo", "iQOO Z10x", "10AFAC2X72005KT")}; + std::vector infos{DeviceInfo(phone, "vivo", "iQOO Z10x", "10AFAC2X72005KT")}; ReconcileDeviceStrings(memory, present, infos); REQUIRE(memory.size() == 1); @@ -137,7 +138,7 @@ TEST_CASE( const DeviceIdentity old_phone{.bus_number = 0, .device_address = 1, .vendor_id = 0x2d95, .product_id = 0x600b}; std::vector memory; - std::vector infos{DeviceInfo(old_phone, "vivo", "iQOO Z10x", "SN1")}; + std::vector infos{DeviceInfo(old_phone, "vivo", "iQOO Z10x", "SN1")}; ReconcileDeviceStrings(memory, {std::vector{old_phone}}, infos); REQUIRE(memory.size() == 1); @@ -168,7 +169,7 @@ TEST_CASE("ReconcileDeviceStrings keeps an honest empty string for a device with const DeviceIdentity mute{.bus_number = 2, .device_address = 7, .vendor_id = 0x1234, .product_id = 0x5678}; std::vector memory; - std::vector infos{DeviceInfo(mute, "", "", "")}; + std::vector infos{DeviceInfo(mute, "", "", "")}; ReconcileDeviceStrings(memory, {std::vector{mute}}, infos); // Do not remember (nothing was read) and do not backfill (nothing from nowhere). CHECK(memory.empty()); @@ -186,7 +187,7 @@ TEST_CASE( std::vector memory; const std::vector both{first, second}; - std::vector infos{DeviceInfo(first, "vivo", "iQOO Z10x", "SN1"), + std::vector infos{DeviceInfo(first, "vivo", "iQOO Z10x", "SN1"), DeviceInfo(second, "Google", "Pixel", "SN2")}; ReconcileDeviceStrings(memory, both, infos); REQUIRE(memory.size() == 2); @@ -206,22 +207,22 @@ TEST_CASE( } // TEST_SUITE("capi.support") TEST_SUITE("capi.basics") { - TEST_CASE("tk_version fills non-empty version strings") { - tk_version_info_t version{}; - tk_version(&version); + TEST_CASE("tn_version fills non-empty version strings") { + tn_version_info_t version{}; + tn_version(&version); CHECK(std::strlen(version.text) > 0); CHECK(std::strlen(version.build) > 0); CHECK(std::strlen(version.libusb) > 0); CHECK(version.major + version.minor + version.patch > 0); SUBCASE("passing nullptr is a no-op and does not crash") { - tk_version(nullptr); + tn_version(nullptr); } } - TEST_CASE("tk_check_environment always succeeds and expresses conclusions in fields only") { - tk_environment_t environment{}; - CHECK(tk_check_environment(&environment) == TK_OK); + TEST_CASE("tn_check_environment always succeeds and expresses conclusions in fields only") { + tn_environment_t environment{}; + CHECK(tn_check_environment(&environment) == TN_OK); // The test process is not root; this is certain. CHECK_FALSE(environment.is_root); // When sysctls are OK, there should be no explanatory text. @@ -230,21 +231,21 @@ TEST_SUITE("capi.basics") { } SUBCASE("passing nullptr returns invalid argument") { - CHECK(tk_check_environment(nullptr) == TK_ERR_INVALID_ARGUMENT); + CHECK(tn_check_environment(nullptr) == TN_ERR_INVALID_ARGUMENT); } } - TEST_CASE("tk_list_devices can count only") { + TEST_CASE("tn_list_devices can count only") { std::size_t count = 0; - tk_error_t error{}; + tn_error_t error{}; // The development machine usually has no RNDIS device inserted; only verify that the call // succeeds and does not write out of bounds, not the specific count. - const tk_result_t result = tk_list_devices(nullptr, 0, &count, false, &error); - CHECK(result == TK_OK); + const tn_result_t result = tn_list_devices(nullptr, 0, &count, false, &error); + CHECK(result == TN_OK); CHECK(std::strlen(error.message) == 0); SUBCASE("missing out_count is rejected") { - CHECK(tk_list_devices(nullptr, 0, nullptr, false, nullptr) == TK_ERR_INVALID_ARGUMENT); + CHECK(tn_list_devices(nullptr, 0, nullptr, false, nullptr) == TN_ERR_INVALID_ARGUMENT); } } @@ -253,28 +254,28 @@ TEST_SUITE("capi.basics") { // context, it would start and stop an event thread and an IOKit runloop thread each time, // and logs would be flooded with "libusb initialized". This happened in reality; this test // pins down the fix. - tk_enable_log_capture(true); - const tetherkit::LogLevel saved_level = tetherkit::GetLogLevel(); - tetherkit::SetLogLevel(tetherkit::LogLevel::kInfo); + tn_enable_log_capture(true); + const tethernet::LogLevel saved_level = tethernet::GetLogLevel(); + tethernet::SetLogLevel(tethernet::LogLevel::kInfo); std::size_t count = 0; - std::array records{}; + std::array records{}; std::uint64_t dropped = 0; // Enumerate once and drain logs to ensure the shared context is already established (the // first initialization is expected). - tk_list_devices(nullptr, 0, &count, false, nullptr); - while (tk_drain_logs(records.data(), records.size(), &dropped) > 0) { + tn_list_devices(nullptr, 0, &count, false, nullptr); + while (tn_drain_logs(records.data(), records.size(), &dropped) > 0) { } constexpr int kRepeats = 3; for (int i = 0; i < kRepeats; ++i) { - CHECK(tk_list_devices(nullptr, 0, &count, false, nullptr) == TK_OK); + CHECK(tn_list_devices(nullptr, 0, &count, false, nullptr) == TN_OK); } std::size_t initialization_lines = 0; std::size_t taken = 0; - while ((taken = tk_drain_logs(records.data(), records.size(), &dropped)) > 0) { + while ((taken = tn_drain_logs(records.data(), records.size(), &dropped)) > 0) { for (std::size_t i = 0; i < taken; ++i) { if (std::string{records[i].message}.find("libusb initialized") != std::string::npos) { ++initialization_lines; @@ -283,65 +284,65 @@ TEST_SUITE("capi.basics") { } CHECK(initialization_lines == 0); - tetherkit::SetLogLevel(saved_level); - tk_enable_log_capture(false); + tethernet::SetLogLevel(saved_level); + tn_enable_log_capture(false); } } // TEST_SUITE("capi.basics") TEST_SUITE("capi.log_ring") { TEST_CASE("no records are produced when log capture is off") { - tk_enable_log_capture(false); - TETHERKIT_ERROR("this must not be captured"); + tn_enable_log_capture(false); + TETHERNET_ERROR("this must not be captured"); - std::array records{}; + std::array records{}; std::uint64_t dropped = 0; - CHECK(tk_drain_logs(records.data(), records.size(), &dropped) == 0); + CHECK(tn_drain_logs(records.data(), records.size(), &dropped) == 0); } TEST_CASE("logs can be retrieved in order after capture is enabled") { - tk_enable_log_capture(true); - const tetherkit::LogLevel saved_level = tetherkit::GetLogLevel(); - tetherkit::SetLogLevel(tetherkit::LogLevel::kInfo); + tn_enable_log_capture(true); + const tethernet::LogLevel saved_level = tethernet::GetLogLevel(); + tethernet::SetLogLevel(tethernet::LogLevel::kInfo); - TETHERKIT_INFO("first"); - TETHERKIT_WARN("second"); + TETHERNET_INFO("first"); + TETHERNET_WARN("second"); - std::array records{}; + std::array records{}; std::uint64_t dropped = 0; - const std::size_t taken = tk_drain_logs(records.data(), records.size(), &dropped); + const std::size_t taken = tn_drain_logs(records.data(), records.size(), &dropped); CHECK(taken == 2); CHECK(dropped == 0); CHECK(std::string{records[0].message} == "first"); - CHECK(records[0].level == TK_LOG_INFO); + CHECK(records[0].level == TN_LOG_INFO); CHECK(std::string{records[1].message} == "second"); - CHECK(records[1].level == TK_LOG_WARN); + CHECK(records[1].level == TN_LOG_WARN); CHECK(records[0].wall_nanos > 0); SUBCASE("draining again after empty returns 0") { - CHECK(tk_drain_logs(records.data(), records.size(), &dropped) == 0); + CHECK(tn_drain_logs(records.data(), records.size(), &dropped) == 0); } - tetherkit::SetLogLevel(saved_level); - tk_enable_log_capture(false); + tethernet::SetLogLevel(saved_level); + tn_enable_log_capture(false); } TEST_CASE("drops the oldest entries and reports the drop count when the buffer is full") { - tk_enable_log_capture(true); - const tetherkit::LogLevel saved_level = tetherkit::GetLogLevel(); - tetherkit::SetLogLevel(tetherkit::LogLevel::kInfo); + tn_enable_log_capture(true); + const tethernet::LogLevel saved_level = tethernet::GetLogLevel(); + tethernet::SetLogLevel(tethernet::LogLevel::kInfo); // Capacity is 256 (see log_ring.cc); log 10 extra entries to push the oldest out. constexpr int kOverflow = 10; constexpr int kTotal = 256 + kOverflow; for (int i = 0; i < kTotal; ++i) { - TETHERKIT_INFO("entry {}", i); + TETHERNET_INFO("entry {}", i); } - std::array records{}; + std::array records{}; std::uint64_t dropped = 0; - const std::size_t taken = tk_drain_logs(records.data(), records.size(), &dropped); + const std::size_t taken = tn_drain_logs(records.data(), records.size(), &dropped); CHECK(taken == 256); CHECK(dropped == kOverflow); @@ -351,20 +352,20 @@ TEST_SUITE("capi.log_ring") { SUBCASE("the drop count resets to zero after being drained") { std::uint64_t again = 1; - tk_drain_logs(records.data(), records.size(), &again); + tn_drain_logs(records.data(), records.size(), &again); CHECK(again == 0); } - tetherkit::SetLogLevel(saved_level); - tk_enable_log_capture(false); + tethernet::SetLogLevel(saved_level); + tn_enable_log_capture(false); } } // TEST_SUITE("capi.log_ring") TEST_SUITE("capi.session") { - TEST_CASE("tk_session_config_init fills usable defaults") { - tk_session_config_t config{}; - tk_session_config_init(&config); + TEST_CASE("tn_session_config_init fills usable defaults") { + tn_session_config_t config{}; + tn_session_config_init(&config); CHECK(config.mtu == 1500); CHECK(config.adopt_device_mac); @@ -380,19 +381,19 @@ TEST_SUITE("capi.session") { } TEST_CASE("session can be created, queried, and destroyed (no root required)") { - tk_session_config_t config{}; - tk_session_config_init(&config); + tn_session_config_t config{}; + tn_session_config_init(&config); - tk_error_t error{}; - tk_session_t* session = tk_session_create(&config, &error); + tn_error_t error{}; + tn_session_t* session = tn_session_create(&config, &error); REQUIRE(session != nullptr); CHECK(std::strlen(error.message) == 0); SUBCASE("IDLE before start, with all fields cleanly zeroed") { - tk_session_status_t status{}; - REQUIRE(tk_session_status_get(session, &status) == TK_OK); - CHECK(status.run_state == TK_RUN_IDLE); - CHECK(status.rndis_state == TK_RNDIS_UNINITIALIZED); + tn_session_status_t status{}; + REQUIRE(tn_session_status_get(session, &status) == TN_OK); + CHECK(status.run_state == TN_RUN_IDLE); + CHECK(status.rndis_state == TN_RNDIS_UNINITIALIZED); CHECK_FALSE(status.link_up); CHECK(std::strlen(status.system_interface) == 0); CHECK(std::strlen(status.fatal) == 0); @@ -401,43 +402,43 @@ TEST_SUITE("capi.session") { } SUBCASE("event queue is empty before start") { - std::array events{}; - CHECK(tk_session_poll_events(session, events.data(), events.size()) == 0); + std::array events{}; + CHECK(tn_session_poll_events(session, events.data(), events.size()) == 0); } - SUBCASE("starting as non-root returns TK_ERR_PERMISSION rather than a generic failure") { + SUBCASE("starting as non-root returns TN_ERR_PERMISSION rather than a generic failure") { // The GUI relies on this code to distinguish "prompt for authorization" from "a real // error occurred". - CHECK(tk_session_start(session, &error) == TK_ERR_PERMISSION); + CHECK(tn_session_start(session, &error) == TN_ERR_PERMISSION); CHECK(std::strlen(error.message) > 0); } SUBCASE("repeated stop is idempotent") { - CHECK(tk_session_stop(session) == TK_OK); - CHECK(tk_session_stop(session) == TK_OK); + CHECK(tn_session_stop(session) == TN_OK); + CHECK(tn_session_stop(session) == TN_OK); } - tk_session_destroy(session); + tn_session_destroy(session); } TEST_CASE("session API is safe for null pointers") { - tk_error_t error{}; - CHECK(tk_session_create(nullptr, &error) == nullptr); + tn_error_t error{}; + CHECK(tn_session_create(nullptr, &error) == nullptr); CHECK(std::strlen(error.message) > 0); - CHECK(tk_session_start(nullptr, nullptr) == TK_ERR_INVALID_ARGUMENT); - CHECK(tk_session_stop(nullptr) == TK_ERR_INVALID_ARGUMENT); - CHECK(tk_session_status_get(nullptr, nullptr) == TK_ERR_INVALID_ARGUMENT); - CHECK(tk_session_poll_events(nullptr, nullptr, 4) == 0); - tk_session_destroy(nullptr); // No crash means it passed. - tk_session_config_init(nullptr); + CHECK(tn_session_start(nullptr, nullptr) == TN_ERR_INVALID_ARGUMENT); + CHECK(tn_session_stop(nullptr) == TN_ERR_INVALID_ARGUMENT); + CHECK(tn_session_status_get(nullptr, nullptr) == TN_ERR_INVALID_ARGUMENT); + CHECK(tn_session_poll_events(nullptr, nullptr, 4) == 0); + tn_session_destroy(nullptr); // No crash means it passed. + tn_session_config_init(nullptr); } } // TEST_SUITE("capi.session") TEST_SUITE("capi.process") { TEST_CASE("RunTool collects child output and carries the exit code") { - const auto result = tetherkit::capi::RunTool("/bin/echo", {"hello", "world"}); + const auto result = tethernet::capi::RunTool("/bin/echo", {"hello", "world"}); REQUIRE(result.has_value()); CHECK(result->exit_code == 0); CHECK(result->Succeeded()); @@ -447,7 +448,7 @@ TEST_SUITE("capi.process") { TEST_CASE("RunTool merges stderr, and a non-zero exit is not treated as a call failure") { // sh -c 'echo boom >&2; exit 3': verify stderr is also collected and the exit code is // returned as-is. - const auto result = tetherkit::capi::RunTool("/bin/sh", {"-c", "echo boom >&2; exit 3"}); + const auto result = tethernet::capi::RunTool("/bin/sh", {"-c", "echo boom >&2; exit 3"}); REQUIRE(result.has_value()); CHECK_FALSE(result->Succeeded()); CHECK(result->exit_code == 3); @@ -458,7 +459,7 @@ TEST_SUITE("capi.process") { // The pipe buffer is 64 KiB. We must drain it before waitpid, otherwise the child blocks // once the pipe is full while we wait in waitpid and both sides deadlock. This test pins // down that order. - const auto result = tetherkit::capi::RunTool( + const auto result = tethernet::capi::RunTool( "/bin/sh", {"-c", "for i in $(seq 1 20000); do echo 0123456789; done"}); REQUIRE(result.has_value()); CHECK(result->exit_code == 0); @@ -466,52 +467,52 @@ TEST_SUITE("capi.process") { } TEST_CASE("RunTool returns an error rather than crashing for a nonexistent executable") { - const auto result = tetherkit::capi::RunTool("/nonexistent/tetherkit-test", {}); + const auto result = tethernet::capi::RunTool("/nonexistent/tethernet-test", {}); CHECK_FALSE(result.has_value()); } } // TEST_SUITE("capi.process") TEST_SUITE("capi.net_config") { - TEST_CASE("tk_ip_config_init defaults to DHCP and does not take over the global default route") { - tk_ip_config_t config{}; - tk_ip_config_init(&config); - CHECK(config.mode == TK_IP_MODE_DHCP); + TEST_CASE("tn_ip_config_init defaults to DHCP and does not take over the global default route") { + tn_ip_config_t config{}; + tn_ip_config_init(&config); + CHECK(config.mode == TN_IP_MODE_DHCP); CHECK_FALSE(config.set_default_route); CHECK(config.dns_count == 0); CHECK(std::strlen(config.address) == 0); } TEST_CASE("rejects operating on non-feth interfaces") { - tk_ip_config_t config{}; - tk_ip_config_init(&config); - tk_error_t error{}; + tn_ip_config_t config{}; + tn_ip_config_init(&config); + tn_error_t error{}; // This is the most important guardrail in this module: passing en0 by mistake would // overwrite the user's Wi-Fi configuration. - CHECK(tk_net_apply("en0", &config, &error) == TK_ERR_INVALID_ARGUMENT); + CHECK(tn_net_apply("en0", &config, &error) == TN_ERR_INVALID_ARGUMENT); CHECK(std::string{error.message}.find("en0") != std::string::npos); - CHECK(tk_net_clear("en0", &error) == TK_ERR_INVALID_ARGUMENT); - CHECK(tk_net_query("en0", nullptr, &error) == TK_ERR_INVALID_ARGUMENT); + CHECK(tn_net_clear("en0", &error) == TN_ERR_INVALID_ARGUMENT); + CHECK(tn_net_query("en0", nullptr, &error) == TN_ERR_INVALID_ARGUMENT); - tk_net_state_t state{}; - CHECK(tk_net_query("bridge0", &state, &error) == TK_ERR_INVALID_ARGUMENT); - CHECK(tk_net_apply(nullptr, &config, &error) == TK_ERR_INVALID_ARGUMENT); - CHECK(tk_net_apply("feth0", nullptr, &error) == TK_ERR_INVALID_ARGUMENT); + tn_net_state_t state{}; + CHECK(tn_net_query("bridge0", &state, &error) == TN_ERR_INVALID_ARGUMENT); + CHECK(tn_net_apply(nullptr, &config, &error) == TN_ERR_INVALID_ARGUMENT); + CHECK(tn_net_apply("feth0", nullptr, &error) == TN_ERR_INVALID_ARGUMENT); } - TEST_CASE("write operations return TK_ERR_PERMISSION when not root") { - tk_ip_config_t config{}; - tk_ip_config_init(&config); - tk_error_t error{}; + TEST_CASE("write operations return TN_ERR_PERMISSION when not root") { + tn_ip_config_t config{}; + tn_ip_config_init(&config); + tn_error_t error{}; - CHECK(tk_net_apply("feth9", &config, &error) == TK_ERR_PERMISSION); + CHECK(tn_net_apply("feth9", &config, &error) == TN_ERR_PERMISSION); CHECK(std::strlen(error.message) > 0); - CHECK(tk_net_clear("feth9", &error) == TK_ERR_PERMISSION); + CHECK(tn_net_clear("feth9", &error) == TN_ERR_PERMISSION); std::size_t removed = 1; - CHECK(tk_cleanup_orphan_interfaces(&removed, &error) == TK_ERR_PERMISSION); + CHECK(tn_cleanup_orphan_interfaces(&removed, &error) == TN_ERR_PERMISSION); CHECK(removed == 0); } @@ -519,9 +520,9 @@ TEST_SUITE("capi.net_config") { // The GUI also refreshes network status before the session is up, when the interface does // not exist yet. That case must be "no address" rather than "query failed", otherwise the // UI would show a spurious error. - tk_net_state_t state{}; - tk_error_t error{}; - CHECK(tk_net_query("feth99", &state, &error) == TK_OK); + tn_net_state_t state{}; + tn_error_t error{}; + CHECK(tn_net_query("feth99", &state, &error) == TN_OK); CHECK_FALSE(state.has_address); CHECK(state.dns_count == 0); CHECK_FALSE(state.is_primary_default_route); diff --git a/tests/test_common_byte_order.cc b/tests/test_common_byte_order.cc index c30d051..f42f619 100644 --- a/tests/test_common_byte_order.cc +++ b/tests/test_common_byte_order.cc @@ -2,23 +2,23 @@ // // Key verification: unaligned access must be correct (offsets of RNDIS messages in USB // buffers are not guaranteed to be aligned). -#include -#include -#include +#include "tethernet/common/byte_order.h" #include -#include "tetherkit/common/byte_order.h" +#include +#include +#include -using tetherkit::AlignUp; -using tetherkit::IsPowerOfTwo; -using tetherkit::LoadBe16; -using tetherkit::LoadLe16; -using tetherkit::LoadLe32; -using tetherkit::LoadLe64; -using tetherkit::StoreBe16; -using tetherkit::StoreLe32; -using tetherkit::StoreLe64; +using tethernet::AlignUp; +using tethernet::IsPowerOfTwo; +using tethernet::LoadBe16; +using tethernet::LoadLe16; +using tethernet::LoadLe32; +using tethernet::LoadLe64; +using tethernet::StoreBe16; +using tethernet::StoreLe32; +using tethernet::StoreLe64; TEST_SUITE("common.byte_order") { TEST_CASE("little-endian reads interpret byte order as specified") { @@ -68,8 +68,8 @@ TEST_SUITE("common.byte_order") { TEST_CASE("single-byte reads and writes perform no conversion") { std::array buffer{}; - tetherkit::StoreLe(buffer.data(), 0xA5U); - CHECK(tetherkit::LoadLe(buffer.data()) == 0xA5U); + tethernet::StoreLe(buffer.data(), 0xA5U); + CHECK(tethernet::LoadLe(buffer.data()) == 0xA5U); } TEST_CASE("AlignUp rounds up to a power of two") { diff --git a/tests/test_common_error.cc b/tests/test_common_error.cc index 57da352..201d897 100644 --- a/tests/test_common_error.cc +++ b/tests/test_common_error.cc @@ -1,18 +1,18 @@ // Unit tests for error.h / logging.h. -#include -#include +#include "tethernet/common/error.h" +#include "tethernet/common/logging.h" #include -#include "tetherkit/common/error.h" -#include "tetherkit/common/logging.h" +#include +#include -using tetherkit::Error; -using tetherkit::ErrorDomain; -using tetherkit::LogLevel; -using tetherkit::Ok; -using tetherkit::Result; -using tetherkit::Status; +using tethernet::Error; +using tethernet::ErrorDomain; +using tethernet::LogLevel; +using tethernet::Ok; +using tethernet::Result; +using tethernet::Status; namespace { @@ -24,10 +24,10 @@ Status SucceedingStep() { return Ok(); } -/// Demonstrates the TETHERKIT_RETURN_IF_ERROR propagation chain. +/// Demonstrates the TETHERNET_RETURN_IF_ERROR propagation chain. Status PipelineWithFailure() { - TETHERKIT_RETURN_IF_ERROR(SucceedingStep()); - TETHERKIT_RETURN_IF_ERROR(FailingStep()); + TETHERNET_RETURN_IF_ERROR(SucceedingStep()); + TETHERNET_RETURN_IF_ERROR(FailingStep()); return Ok(); } @@ -39,7 +39,7 @@ Result ProducingStep(bool succeed) { } Result ConsumingPipeline(bool succeed) { - TETHERKIT_ASSIGN_OR_RETURN(const int value, ProducingStep(succeed)); + TETHERNET_ASSIGN_OR_RETURN(const int value, ProducingStep(succeed)); return value * 2; } @@ -71,7 +71,7 @@ TEST_SUITE("common.error") { } TEST_CASE("RNDIS domain outputs the raw status code value") { - // tk_common deliberately does not know the symbolic names of RNDIS status codes -- that + // tn_common deliberately does not know the symbolic names of RNDIS status codes -- that // table lives in only one place, rndis/protocol.cc (single source, avoids inconsistencies). // Therefore Error::ToString() here only outputs the hexadecimal value; the symbolic name is // appended to the context string by the rndis layer when constructing the Error (see @@ -112,24 +112,24 @@ TEST_SUITE("common.error") { } TEST_CASE("log level setting and checking") { - const LogLevel original = tetherkit::GetLogLevel(); + const LogLevel original = tethernet::GetLogLevel(); - tetherkit::SetLogLevel(LogLevel::kWarn); - CHECK(tetherkit::GetLogLevel() == LogLevel::kWarn); - CHECK_FALSE(tetherkit::detail::IsLogLevelEnabled(LogLevel::kInfo)); - CHECK(tetherkit::detail::IsLogLevelEnabled(LogLevel::kWarn)); - CHECK(tetherkit::detail::IsLogLevelEnabled(LogLevel::kError)); + tethernet::SetLogLevel(LogLevel::kWarn); + CHECK(tethernet::GetLogLevel() == LogLevel::kWarn); + CHECK_FALSE(tethernet::detail::IsLogLevelEnabled(LogLevel::kInfo)); + CHECK(tethernet::detail::IsLogLevelEnabled(LogLevel::kWarn)); + CHECK(tethernet::detail::IsLogLevelEnabled(LogLevel::kError)); - tetherkit::SetLogLevel(LogLevel::kOff); - CHECK_FALSE(tetherkit::detail::IsLogLevelEnabled(LogLevel::kError)); + tethernet::SetLogLevel(LogLevel::kOff); + CHECK_FALSE(tethernet::detail::IsLogLevelEnabled(LogLevel::kError)); - tetherkit::SetLogLevel(original); + tethernet::SetLogLevel(original); } TEST_CASE("extract the file name from __FILE__") { - CHECK(tetherkit::detail::BaseName("/a/b/c/bpf_link.cc") == "bpf_link.cc"); - CHECK(tetherkit::detail::BaseName("bpf_link.cc") == "bpf_link.cc"); - CHECK(tetherkit::detail::BaseName("") == ""); + CHECK(tethernet::detail::BaseName("/a/b/c/bpf_link.cc") == "bpf_link.cc"); + CHECK(tethernet::detail::BaseName("bpf_link.cc") == "bpf_link.cc"); + CHECK(tethernet::detail::BaseName("") == ""); } } // TEST_SUITE("common.error") diff --git a/tests/test_common_frame_ring.cc b/tests/test_common_frame_ring.cc index 86dffe0..d674935 100644 --- a/tests/test_common_frame_ring.cc +++ b/tests/test_common_frame_ring.cc @@ -3,6 +3,11 @@ // FrameRing is the core of the RX path: the libusb callback thread writes extracted Ethernet // frames into it, and the BPF write thread takes them out and calls write(). Therefore this // tests both semantics and concurrency. +#include "tethernet/common/cache.h" +#include "tethernet/common/frame_ring.h" + +#include + #include #include #include @@ -10,14 +15,9 @@ #include #include -#include - -#include "tetherkit/common/cache.h" -#include "tetherkit/common/frame_ring.h" - -using tetherkit::FrameRing; -using tetherkit::FrameView; -using tetherkit::kCacheLineSize; +using tethernet::FrameRing; +using tethernet::FrameView; +using tethernet::kCacheLineSize; namespace { diff --git a/tests/test_common_i18n.cc b/tests/test_common_i18n.cc index 820b1e9..4a9f5d6 100644 --- a/tests/test_common_i18n.cc +++ b/tests/test_common_i18n.cc @@ -17,6 +17,9 @@ // When a language is added, the cross-language parity checks return: each // table must expose the same placeholder index set and presentation types as // the English one (see the checklist in messages.def). +#include "tethernet/common/error.h" +#include "tethernet/common/i18n.h" + #include #include #include @@ -26,13 +29,11 @@ #include #include "doctest.h" -#include "tetherkit/common/error.h" -#include "tetherkit/common/i18n.h" namespace { -using tetherkit::Language; -using tetherkit::Msg; +using tethernet::Language; +using tethernet::Msg; constexpr std::size_t kMessageCount = static_cast(Msg::kMessageCount); @@ -132,7 +133,7 @@ struct ParsedFormat { /// names (the X-macro expands to text only), so an index plus the start of the /// English text is enough to locate the offending messages.def row. [[nodiscard]] std::string Label(std::size_t index) { - const std::string_view english = tetherkit::TextIn(Language::kEnglish, static_cast(index)); + const std::string_view english = tethernet::TextIn(Language::kEnglish, static_cast(index)); return std::format("message #{} (\"{}\")", index, english.substr(0, 48)); } @@ -140,42 +141,42 @@ struct ParsedFormat { TEST_SUITE("common.i18n") { TEST_CASE("language switch and table lookup") { - const Language original = tetherkit::GetLanguage(); + const Language original = tethernet::GetLanguage(); // The selection machinery is process-wide and round-trips; today every // value resolves to the English table, but these calls exercise the same // path a second language will ride. - tetherkit::SetLanguage(Language::kEnglish); - CHECK(tetherkit::GetLanguage() == Language::kEnglish); - CHECK(tetherkit::Text(Msg::kCoreRunStateRunning) == "running"); + tethernet::SetLanguage(Language::kEnglish); + CHECK(tethernet::GetLanguage() == Language::kEnglish); + CHECK(tethernet::Text(Msg::kCoreRunStateRunning) == "running"); - tetherkit::SetLanguage(original); + tethernet::SetLanguage(original); } TEST_CASE("Tr renders the current language and substitutes arguments") { - const Language original = tetherkit::GetLanguage(); - tetherkit::SetLanguage(Language::kEnglish); + const Language original = tethernet::GetLanguage(); + tethernet::SetLanguage(Language::kEnglish); - const std::string text = tetherkit::Tr(Msg::kNetBpfBindFailed, "feth1"); + const std::string text = tethernet::Tr(Msg::kNetBpfBindFailed, "feth1"); // The argument really was substituted, not left as an unreplaced field. CHECK(text.find("feth1") != std::string::npos); CHECK(text.find("{}") == std::string::npos); - tetherkit::SetLanguage(original); + tethernet::SetLanguage(original); } TEST_CASE("every message is non-empty") { for (std::size_t i = 0; i < kMessageCount; ++i) { const auto id = static_cast(i); CAPTURE(i); - CHECK_FALSE(tetherkit::TextIn(Language::kEnglish, id).empty()); + CHECK_FALSE(tethernet::TextIn(Language::kEnglish, id).empty()); } } TEST_CASE("out-of-range Msg returns an empty string instead of crashing") { const auto out_of_range = static_cast(kMessageCount); - CHECK(tetherkit::Text(out_of_range).empty()); - CHECK(tetherkit::TextIn(Language::kEnglish, out_of_range).empty()); + CHECK(tethernet::Text(out_of_range).empty()); + CHECK(tethernet::TextIn(Language::kEnglish, out_of_range).empty()); } // This suite's core: the one check the compiler stopped doing, done entry by @@ -183,7 +184,7 @@ TEST_SUITE("common.i18n") { TEST_CASE("every message's placeholders are well-formed and contiguous") { for (std::size_t i = 0; i < kMessageCount; ++i) { const ParsedFormat parsed = - ParseFormat(tetherkit::TextIn(Language::kEnglish, static_cast(i))); + ParseFormat(tethernet::TextIn(Language::kEnglish, static_cast(i))); CAPTURE(Label(i)); CHECK_FALSE(parsed.malformed); @@ -204,24 +205,24 @@ TEST_SUITE("common.i18n") { TEST_CASE("ParseLanguage understands the established spellings") { Language language = Language::kEnglish; - CHECK(tetherkit::ParseLanguage("en", &language)); + CHECK(tethernet::ParseLanguage("en", &language)); CHECK(language == Language::kEnglish); - CHECK(tetherkit::ParseLanguage("EN_US", &language)); + CHECK(tethernet::ParseLanguage("EN_US", &language)); CHECK(language == Language::kEnglish); - CHECK(tetherkit::ParseLanguage("english", &language)); + CHECK(tethernet::ParseLanguage("english", &language)); CHECK(language == Language::kEnglish); // auto defers to the environment; only require it to succeed and yield a // valid language. - CHECK(tetherkit::ParseLanguage("auto", &language)); + CHECK(tethernet::ParseLanguage("auto", &language)); - CHECK_FALSE(tetherkit::ParseLanguage("klingon", &language)); - CHECK_FALSE(tetherkit::ParseLanguage("", &language)); - CHECK_FALSE(tetherkit::ParseLanguage("zh", nullptr)); + CHECK_FALSE(tethernet::ParseLanguage("klingon", &language)); + CHECK_FALSE(tethernet::ParseLanguage("", &language)); + CHECK_FALSE(tethernet::ParseLanguage("zh", nullptr)); // Chinese is fully out: no table, no spelling, not even as a parse. - CHECK_FALSE(tetherkit::ParseLanguage("zh", &language)); - CHECK_FALSE(tetherkit::ParseLanguage("Chinese", &language)); + CHECK_FALSE(tethernet::ParseLanguage("zh", &language)); + CHECK_FALSE(tethernet::ParseLanguage("Chinese", &language)); } TEST_CASE("locale strings resolve through the current language set") { @@ -229,29 +230,29 @@ TEST_SUITE("common.i18n") { // locale string resolves to English. When a language is added, the prefix // table goes into LanguageFromLocaleString() and these checks become // per-prefix. - CHECK(tetherkit::LanguageFromLocaleString("zh_CN.UTF-8") == Language::kEnglish); - CHECK(tetherkit::LanguageFromLocaleString("ZH-hant") == Language::kEnglish); - CHECK(tetherkit::LanguageFromLocaleString("en_US.UTF-8") == Language::kEnglish); - CHECK(tetherkit::LanguageFromLocaleString("C") == Language::kEnglish); - CHECK(tetherkit::LanguageFromLocaleString("") == Language::kEnglish); - CHECK(tetherkit::LanguageFromLocaleString("en_zh") == Language::kEnglish); + CHECK(tethernet::LanguageFromLocaleString("zh_CN.UTF-8") == Language::kEnglish); + CHECK(tethernet::LanguageFromLocaleString("ZH-hant") == Language::kEnglish); + CHECK(tethernet::LanguageFromLocaleString("en_US.UTF-8") == Language::kEnglish); + CHECK(tethernet::LanguageFromLocaleString("C") == Language::kEnglish); + CHECK(tethernet::LanguageFromLocaleString("") == Language::kEnglish); + CHECK(tethernet::LanguageFromLocaleString("en_zh") == Language::kEnglish); } TEST_CASE("language tag") { - CHECK(tetherkit::LanguageTag(Language::kEnglish) == "en"); + CHECK(tethernet::LanguageTag(Language::kEnglish) == "en"); } TEST_CASE("error context separator is deterministic") { - const Language original = tetherkit::GetLanguage(); - tetherkit::SetLanguage(Language::kEnglish); + const Language original = tethernet::GetLanguage(); + tethernet::SetLanguage(Language::kEnglish); // Context() returns a view, so the Error must land in a named variable // before it is read; a temporary dies at the end of the statement and the // view dangles on the spot. - const tetherkit::Error error = tetherkit::Error::Generic("inner").WithContext("outer"); + const tethernet::Error error = tethernet::Error::Generic("inner").WithContext("outer"); CHECK(error.Context().starts_with("outer: ")); - tetherkit::SetLanguage(original); + tethernet::SetLanguage(original); } } // TEST_SUITE diff --git a/tests/test_common_spsc_ring.cc b/tests/test_common_spsc_ring.cc index 886d881..b4cb140 100644 --- a/tests/test_common_spsc_ring.cc +++ b/tests/test_common_spsc_ring.cc @@ -1,18 +1,18 @@ // Unit tests for SpscRing / SpscCursor. // // Multi-threaded cases only have full meaning under a ThreadSanitizer build: -// cmake -B build-tsan -DTETHERKIT_ENABLE_TSAN=ON && ctest --test-dir build-tsan -R spsc -#include -#include -#include +// cmake -B build-tsan -DTETHERNET_ENABLE_TSAN=ON && ctest --test-dir build-tsan -R spsc +#include "tethernet/common/cache.h" +#include "tethernet/common/spsc_ring.h" #include -#include "tetherkit/common/cache.h" -#include "tetherkit/common/spsc_ring.h" +#include +#include +#include -using tetherkit::kCacheLineSize; -using tetherkit::SpscRing; +using tethernet::kCacheLineSize; +using tethernet::SpscRing; namespace { @@ -88,7 +88,7 @@ TEST_SUITE("common.spsc_ring") { const auto base = reinterpret_cast(&cursor); // The four indices in SpscCursor are each alignas(kCacheLineSize), so the whole object // must occupy at least 4 cache lines (plus the one for read-only fields). - CHECK(sizeof(tetherkit::SpscCursor) >= 4 * kCacheLineSize); + CHECK(sizeof(tethernet::SpscCursor) >= 4 * kCacheLineSize); CHECK(base % kCacheLineSize == 0); } diff --git a/tests/test_common_stats.cc b/tests/test_common_stats.cc index 58ca0fa..a0bd48d 100644 --- a/tests/test_common_stats.cc +++ b/tests/test_common_stats.cc @@ -1,20 +1,20 @@ // Unit tests for stats.h and time.h. -#include -#include +#include "tethernet/common/stats.h" +#include "tethernet/common/time.h" #include -#include "tetherkit/common/stats.h" -#include "tetherkit/common/time.h" +#include +#include -using tetherkit::DirectionCounters; -using tetherkit::kNanosPerMilli; -using tetherkit::MonotonicNanos; -using tetherkit::PathCounters; -using tetherkit::PeriodicTimer; -using tetherkit::RateSampler; -using tetherkit::Snapshot; -using tetherkit::Stopwatch; +using tethernet::DirectionCounters; +using tethernet::kNanosPerMilli; +using tethernet::MonotonicNanos; +using tethernet::PathCounters; +using tethernet::PeriodicTimer; +using tethernet::RateSampler; +using tethernet::Snapshot; +using tethernet::Stopwatch; TEST_SUITE("common.stats") { TEST_CASE("counter accumulation and snapshot") { @@ -98,7 +98,7 @@ TEST_SUITE("common.stats") { TEST_CASE( "PeriodicTimer advances by accumulated deadlines without drifting on scheduling delay") { - const tetherkit::Nanos now = MonotonicNanos(); + const tethernet::Nanos now = MonotonicNanos(); // `now` must be passed explicitly as the timing origin. If the constructor reads the clock // itself, the time it reads will be slightly later than `now` here, so "now + period" would // not yet be due and the assertion would fail randomly. @@ -116,12 +116,12 @@ TEST_SUITE("common.stats") { } TEST_CASE("PeriodicTimer does not emit a burst of triggers when more than one period behind") { - const tetherkit::Nanos now = MonotonicNanos(); + const tethernet::Nanos now = MonotonicNanos(); PeriodicTimer timer(10 * kNanosPerMilli, now); // The thread wakes up 1 second later after being suspended: it should fire only once, then // align to after the current time. - const tetherkit::Nanos late = now + 1000 * kNanosPerMilli; + const tethernet::Nanos late = now + 1000 * kNanosPerMilli; CHECK(timer.Expired(late)); CHECK_FALSE(timer.Expired(late)); CHECK_FALSE(timer.Expired(late + 9 * kNanosPerMilli)); diff --git a/tests/test_core_bridge.cc b/tests/test_core_bridge.cc index af2c733..a51c057 100644 --- a/tests/test_core_bridge.cc +++ b/tests/test_core_bridge.cc @@ -5,7 +5,12 @@ // backpressure, pausing and graceful shutdown. // // These cases only make full sense under ThreadSanitizer: -// cmake -B build-tsan -DTETHERKIT_ENABLE_TSAN=ON && ctest --test-dir build-tsan -R core +// cmake -B build-tsan -DTETHERNET_ENABLE_TSAN=ON && ctest --test-dir build-tsan -R core +#include "tethernet/core/bridge.h" +#include "tethernet/net/loopback_link.h" + +#include + #include #include #include @@ -13,19 +18,14 @@ #include #include -#include - #include "language_guard.h" - #include "mock_data_channel.h" -#include "tetherkit/core/bridge.h" -#include "tetherkit/net/loopback_link.h" -using namespace tetherkit; // NOLINT(google-build-using-namespace) -using namespace tetherkit::core; // NOLINT(google-build-using-namespace) -using tetherkit::net::LoopbackConfig; -using tetherkit::net::LoopbackLink; -using tetherkit::testing::MockDataChannel; +using namespace tethernet; // NOLINT(google-build-using-namespace) +using namespace tethernet::core; // NOLINT(google-build-using-namespace) +using tethernet::net::LoopbackConfig; +using tethernet::net::LoopbackLink; +using tethernet::testing::MockDataChannel; namespace { @@ -509,7 +509,7 @@ TEST_SUITE("core.bridge") { TEST_CASE("stats line renders rates and drops") { // The stats line is user-facing text; pin the language before asserting. - const tetherkit::testing::ScopedLanguage guard{tetherkit::Language::kEnglish}; + const tethernet::testing::ScopedLanguage guard{tethernet::Language::kEnglish}; MockDataChannel channel; LoopbackLink link(LoopbackConfig{.sent_capacity = 4096}); Bridge bridge(channel, link, TestConfig()); diff --git a/tests/test_net_link.cc b/tests/test_net_link.cc index 1c8f0f8..bc2d5c4 100644 --- a/tests/test_net_link.cc +++ b/tests/test_net_link.cc @@ -4,8 +4,24 @@ // * loopback backend -- runnable in any environment; it is the infrastructure for bridge // layer tests and must itself be reliable; // * feth + BPF -- requires root and is skipped rather than failed in non-root environments. -// Enabled explicitly with TETHERKIT_ROOT_TESTS=1 (to avoid accidentally running real +// Enabled explicitly with TETHERNET_ROOT_TESTS=1 (to avoid accidentally running real // interface operations in CI). +#include "tethernet/common/i18n.h" +#include "tethernet/net/bpf_link.h" +#include "tethernet/net/darwin_abi.h" +#include "tethernet/net/feth_device.h" +#include "tethernet/net/loopback_link.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include #include #include #include @@ -17,24 +33,8 @@ #include #include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "tetherkit/common/i18n.h" -#include "tetherkit/net/bpf_link.h" -#include "tetherkit/net/darwin_abi.h" -#include "tetherkit/net/feth_device.h" -#include "tetherkit/net/loopback_link.h" - -using namespace tetherkit; // NOLINT(google-build-using-namespace) -using namespace tetherkit::net; // NOLINT(google-build-using-namespace) +using namespace tethernet; // NOLINT(google-build-using-namespace) +using namespace tethernet::net; // NOLINT(google-build-using-namespace) namespace { @@ -62,7 +62,7 @@ std::vector MakeEthernetFrame(std::uint32_t total_length, std::uint8_ /// test case and there is no concurrent setenv in the process, so the check is suppressed. // NOLINTNEXTLINE(concurrency-mt-unsafe) bool RootTestsEnabled() { - const char* flag = std::getenv("TETHERKIT_ROOT_TESTS"); + const char* flag = std::getenv("TETHERNET_ROOT_TESTS"); return flag != nullptr && flag[0] == '1' && IsRunningAsRoot(); } @@ -84,7 +84,7 @@ std::string Why(const T& result) { /// it directly would print an address. void ReportSkip(std::string_view what) { const std::string message = - std::format("Skipped {}: requires root and TETHERKIT_ROOT_TESTS=1 (current euid={})", what, + std::format("Skipped {}: requires root and TETHERNET_ROOT_TESTS=1 (current euid={})", what, IsRunningAsRoot() ? "0" : "not 0"); MESSAGE(message); } @@ -235,7 +235,7 @@ TEST_SUITE("net.abi") { // why holds a message identifier; the shipped language must really have // text for it (a missing row would render as an empty string and leave // "reason:" empty in sysctl errors). - const std::string_view why = tetherkit::TextIn(tetherkit::Language::kEnglish, entry.why); + const std::string_view why = tethernet::TextIn(tethernet::Language::kEnglish, entry.why); // The reason must be a real sentence, not one word. CHECK(why.size() > 10); } diff --git a/tests/test_rndis_messages.cc b/tests/test_rndis_messages.cc index cb045b7..7914bfe 100644 --- a/tests/test_rndis_messages.cc +++ b/tests/test_rndis_messages.cc @@ -9,20 +9,20 @@ // 5. Normalization of device quirks (MaxPacketsPerMessage=0, alignment factor out of range, // MaxTransferSize too large or too small); // 6. All malformed inputs must return an error rather than crashing or reading out of bounds. +#include "tethernet/common/byte_order.h" +#include "tethernet/rndis/messages.h" +#include "tethernet/rndis/protocol.h" + +#include + #include #include #include #include #include -#include - -#include "tetherkit/common/byte_order.h" -#include "tetherkit/rndis/messages.h" -#include "tetherkit/rndis/protocol.h" - -using namespace tetherkit; // NOLINT(google-build-using-namespace) -using namespace tetherkit::rndis; // NOLINT(google-build-using-namespace) +using namespace tethernet; // NOLINT(google-build-using-namespace) +using namespace tethernet::rndis; // NOLINT(google-build-using-namespace) namespace { diff --git a/tests/test_rndis_packet_codec.cc b/tests/test_rndis_packet_codec.cc index 01e925f..40d0d7d 100644 --- a/tests/test_rndis_packet_codec.cc +++ b/tests/test_rndis_packet_codec.cc @@ -13,22 +13,22 @@ // transfer reports a spurious error); // 5. All malformed inputs are recognized as kMalformed without reading out // of bounds. +#include "tethernet/common/byte_order.h" +#include "tethernet/rndis/packet_codec.h" +#include "tethernet/rndis/protocol.h" + +#include + #include #include #include #include #include -#include - #include "language_guard.h" -#include "tetherkit/common/byte_order.h" -#include "tetherkit/rndis/packet_codec.h" -#include "tetherkit/rndis/protocol.h" - -using namespace tetherkit; // NOLINT(google-build-using-namespace) -using namespace tetherkit::rndis; // NOLINT(google-build-using-namespace) +using namespace tethernet; // NOLINT(google-build-using-namespace) +using namespace tethernet::rndis; // NOLINT(google-build-using-namespace) namespace { @@ -441,7 +441,7 @@ TEST_SUITE("rndis.packet_codec") { TEST_CASE("MalformedReason names are complete") { // The names are user-facing text and follow the language, so pin it first. - const tetherkit::testing::ScopedLanguage guard{tetherkit::Language::kEnglish}; + const tethernet::testing::ScopedLanguage guard{tethernet::Language::kEnglish}; CHECK(MalformedReasonName(MalformedReason::kNone) == "none"); CHECK_FALSE(MalformedReasonName(MalformedReason::kNotPacketMessage).empty()); CHECK_FALSE(MalformedReasonName(MalformedReason::kDataOutOfBounds).empty()); diff --git a/tests/test_rndis_state_machine.cc b/tests/test_rndis_state_machine.cc index ba61c9a..d81de14 100644 --- a/tests/test_rndis_state_machine.cc +++ b/tests/test_rndis_state_machine.cc @@ -5,18 +5,18 @@ // (the device pushing an out-of-band message, a keep-alive failure, a replay // demand after a reset). MockControlChannel plays the device side here, so // all of those paths get covered. +#include "tethernet/rndis/state_machine.h" + +#include + #include #include #include #include #include -#include - #include "language_guard.h" - #include "mock_control_channel.h" -#include "tetherkit/rndis/state_machine.h" // Same reasoning as the identically named helper in test_net_link.cc: writing // `r ? "" : r.error().ToString()` directly inside REQUIRE_MESSAGE does not @@ -29,22 +29,22 @@ std::string Why(const T& result) { } } // namespace -using namespace tetherkit; // NOLINT(google-build-using-namespace) -using namespace tetherkit::rndis; // NOLINT(google-build-using-namespace) -using tetherkit::testing::MakeDeviceKeepAlive; -using tetherkit::testing::MakeIndicateStatus; -using tetherkit::testing::MakeInitializeComplete; -using tetherkit::testing::MakeKeepAliveComplete; -using tetherkit::testing::MakeQueryComplete; -using tetherkit::testing::MakeQueryCompleteMac; -using tetherkit::testing::MakeQueryCompleteUint32; -using tetherkit::testing::MakeResetComplete; -using tetherkit::testing::MakeSetComplete; -using tetherkit::testing::MakeWellBehavedDevice; -using tetherkit::testing::MockControlChannel; -using tetherkit::testing::OidOf; -using tetherkit::testing::RecordingObserver; -using tetherkit::testing::RequestIdOf; +using namespace tethernet; // NOLINT(google-build-using-namespace) +using namespace tethernet::rndis; // NOLINT(google-build-using-namespace) +using tethernet::testing::MakeDeviceKeepAlive; +using tethernet::testing::MakeIndicateStatus; +using tethernet::testing::MakeInitializeComplete; +using tethernet::testing::MakeKeepAliveComplete; +using tethernet::testing::MakeQueryComplete; +using tethernet::testing::MakeQueryCompleteMac; +using tethernet::testing::MakeQueryCompleteUint32; +using tethernet::testing::MakeResetComplete; +using tethernet::testing::MakeSetComplete; +using tethernet::testing::MakeWellBehavedDevice; +using tethernet::testing::MockControlChannel; +using tethernet::testing::OidOf; +using tethernet::testing::RecordingObserver; +using tethernet::testing::RequestIdOf; namespace { @@ -343,7 +343,7 @@ TEST_SUITE("rndis.state_machine") { TEST_CASE("messages aimed at connection-oriented devices are explicitly rejected") { // The error message wording is asserted below; pin the language. - const tetherkit::testing::ScopedLanguage guard{tetherkit::Language::kEnglish}; + const tethernet::testing::ScopedLanguage guard{tethernet::Language::kEnglish}; MockControlChannel channel; channel.SetRequestHandler([](std::span request, MockControlChannel& mock) { if (LoadLe32(request.data() + kMessageTypeOffset) == ToRaw(MessageType::kInitialize)) { @@ -397,7 +397,7 @@ TEST_SUITE("rndis.state_machine") { TEST_CASE("reaching the keepalive failure threshold reports a fatal error") { // The fatal error wording is asserted below; pin the language. - const tetherkit::testing::ScopedLanguage guard{tetherkit::Language::kEnglish}; + const tethernet::testing::ScopedLanguage guard{tethernet::Language::kEnglish}; MockControlChannel channel; // The device answers only INITIALIZE / QUERY / SET and never KEEPALIVE -> // every poll times out. diff --git a/tests/test_version.cc b/tests/test_version.cc index edea9a3..2210bd9 100644 --- a/tests/test_version.cc +++ b/tests/test_version.cc @@ -1,18 +1,18 @@ // Smoke tests for the version info: confirms the CMake version-injection // pipeline is wired up. -#include +#include "tethernet/version.h" -#include "tetherkit/version.h" +#include TEST_SUITE("version") { TEST_CASE("Version number is not all zeros") { - const auto v = tetherkit::GetVersion(); + const auto v = tethernet::GetVersion(); CHECK((v.major != 0 || v.minor != 0 || v.patch != 0)); } TEST_CASE("Version string contains the project name") { - const std::string_view s = tetherkit::GetVersionString(); - CHECK(s.find("TetherKit") != std::string_view::npos); + const std::string_view s = tethernet::GetVersionString(); + CHECK(s.find("Tethernet") != std::string_view::npos); } } // TEST_SUITE("version")