tethered 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+ pull_request:
7
+ branches: [main, dev]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ lint:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@e4db8464a088ece1b920f60402e813ea4de65b8f # v4
21
+
22
+ - name: Set up Python
23
+ run: uv python install 3.13
24
+
25
+ - name: Install dependencies
26
+ run: uv sync
27
+
28
+ - name: Run pre-commit checks
29
+ run: uv run pre-commit run --all-files
30
+
31
+ test:
32
+ runs-on: ${{ matrix.os }}
33
+ strategy:
34
+ fail-fast: false
35
+ matrix:
36
+ os: [ubuntu-latest, macos-latest, windows-latest]
37
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
38
+
39
+ steps:
40
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
41
+
42
+ - name: Install uv
43
+ uses: astral-sh/setup-uv@e4db8464a088ece1b920f60402e813ea4de65b8f # v4
44
+
45
+ - name: Set up Python ${{ matrix.python-version }}
46
+ run: uv python install ${{ matrix.python-version }}
47
+
48
+ - name: Install dependencies
49
+ run: uv sync
50
+
51
+ - name: Run tests with coverage
52
+ run: uv run pytest tests/ -v --cov=src/tethered --cov-report=xml --cov-report=term-missing
53
+
54
+ - name: Extract coverage percentage
55
+ if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
56
+ id: coverage
57
+ run: |
58
+ RATE=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.parse('coverage.xml').getroot().get('line-rate'))")
59
+ PCT=$(python3 -c "print(round(float('$RATE') * 100))")
60
+ echo "pct=$PCT" >> "$GITHUB_OUTPUT"
61
+
62
+ - name: Update coverage badge
63
+ if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && github.ref == 'refs/heads/main'
64
+ uses: schneegans/dynamic-badges-action@e9a478b16159b4d31420099ba146cdc50f134483 # v1.7.0
65
+ with:
66
+ auth: ${{ secrets.GIST_TOKEN }}
67
+ gistID: ${{ vars.COVERAGE_GIST_ID }}
68
+ filename: tethered-coverage.json
69
+ label: coverage
70
+ message: ${{ steps.coverage.outputs.pct }}%
71
+ valColorRange: ${{ steps.coverage.outputs.pct }}
72
+ minColorRange: 50
73
+ maxColorRange: 100
@@ -0,0 +1,30 @@
1
+ name: CodeQL
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+ pull_request:
7
+ branches: [main, dev]
8
+ schedule:
9
+ - cron: "0 6 * * 1"
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+ security-events: write
15
+ actions: read
16
+
17
+ jobs:
18
+ analyze:
19
+ runs-on: ubuntu-latest
20
+
21
+ steps:
22
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
23
+
24
+ - name: Initialize CodeQL
25
+ uses: github/codeql-action/init@c4a7bc332abaec03596ff2803dd7f3ca3a238975 # v3
26
+ with:
27
+ languages: python
28
+
29
+ - name: Perform CodeQL analysis
30
+ uses: github/codeql-action/analyze@c4a7bc332abaec03596ff2803dd7f3ca3a238975 # v3
@@ -0,0 +1,29 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+
16
+ steps:
17
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@e4db8464a088ece1b920f60402e813ea4de65b8f # v4
21
+
22
+ - name: Set up Python
23
+ run: uv python install 3.13
24
+
25
+ - name: Build package
26
+ run: uv build
27
+
28
+ - name: Publish to PyPI
29
+ uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
@@ -0,0 +1,40 @@
1
+ # Virtual environments
2
+ .venv/
3
+ venv/
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+
12
+ # Testing
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+ coverage.xml
17
+
18
+ # Linting
19
+ .ruff_cache/
20
+
21
+ # IDE
22
+ .idea/
23
+ .vscode/
24
+ *.swp
25
+ *.swo
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Secrets
32
+ .env
33
+ .env.*
34
+ *.pem
35
+ *.key
36
+ credentials.json
37
+ token.json
38
+
39
+ # Claude Code
40
+ .claude/
@@ -0,0 +1,4 @@
1
+ config:
2
+ line-length: false
3
+ first-line-heading: false
4
+ table-column-style: false
@@ -0,0 +1,42 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-toml
9
+
10
+ - repo: https://github.com/astral-sh/ruff-pre-commit
11
+ rev: 0839f92796ae388643a08a21640a029b322be5c2 # v0.15.2
12
+ hooks:
13
+ - id: ruff-check
14
+ args: [--fix]
15
+ - id: ruff-format
16
+
17
+ - repo: https://github.com/DavidAnson/markdownlint-cli2
18
+ rev: 5387279b3b4c24822c0f86d4df4f28b37e3e8992 # v0.21.0
19
+ hooks:
20
+ - id: markdownlint-cli2
21
+
22
+ - repo: https://github.com/PyCQA/bandit
23
+ rev: ea0d187d78b2e6365e35f676d2eb9b1be264c091 # 1.9.2
24
+ hooks:
25
+ - id: bandit
26
+ args: ["-c", "pyproject.toml"]
27
+ additional_dependencies: ["bandit[toml]"]
28
+
29
+ - repo: https://github.com/commitizen-tools/commitizen
30
+ rev: 7179a42b3544e953e3683a670056ad385892365d # v4.13.8
31
+ hooks:
32
+ - id: commitizen
33
+ stages: [commit-msg]
34
+
35
+ - repo: local
36
+ hooks:
37
+ - id: pytest
38
+ name: pytest
39
+ entry: uv run pytest tests/ -x -q
40
+ language: system
41
+ types: [python]
42
+ pass_filenames: false
@@ -0,0 +1,96 @@
1
+ # AGENTS.md — Tethered
2
+
3
+ ## What is Tethered
4
+
5
+ Tethered is a zero-dependency Python library for runtime network egress control. It uses `sys.addaudithook` (PEP 578) to intercept outbound socket connections and enforce an allow list of permitted destinations. One function call, no sidecar containers, no infrastructure changes.
6
+
7
+ ```python
8
+ import tethered
9
+
10
+ tethered.activate(allow=["*.stripe.com:443", "db.internal:5432"])
11
+ ```
12
+
13
+ ## Architecture
14
+
15
+ ```text
16
+ src/tethered/
17
+ __init__.py # Public API surface: activate(), deactivate(), EgressBlocked, TetheredLocked
18
+ _policy.py # AllowPolicy — pattern parsing and matching (pure logic, no side effects)
19
+ _core.py # Audit hook, _Config bundle, state management, IP-to-hostname resolution
20
+ tests/
21
+ conftest.py # Test-suite egress guard — uses AllowPolicy
22
+ test_policy.py # Unit tests for AllowPolicy (no hooks, no network)
23
+ test_core.py # Integration tests with real sockets (sync and async)
24
+ ```
25
+
26
+ ### Module responsibilities
27
+
28
+ - **`_policy.py`** is pure logic. `AllowPolicy` is immutable after construction and thread-safe to read. It handles hostname wildcards (`*.stripe.com`), CIDR ranges (`10.0.0.0/8`), port filtering (`host:443`), and localhost detection. It has zero side effects and no imports beyond stdlib (`fnmatch`, `ipaddress`, `logging`, `dataclasses`).
29
+
30
+ - **`_core.py`** owns the audit hook lifecycle. It installs a single `sys.addaudithook` that intercepts `socket.getaddrinfo` and `socket.gethostbyname`/`gethostbyaddr` (to enforce DNS-level policy and map IPs back to hostnames) and `socket.connect`/`sendto`/`sendmsg` (to enforce the connection policy). All per-activation state (`policy`, `log_only`, `fail_closed`, `on_blocked`, `locked`, `lock_token`) is bundled into a frozen `_Config` dataclass that is swapped atomically — this eliminates TOCTOU bugs between separate state reads and is safe on free-threaded Python (PEP 703). The IP-to-hostname map is an `OrderedDict` with LRU eviction. The hook is installed once and can never be removed — `deactivate()` sets `_config` to `None`, making the hook a no-op.
31
+
32
+ - **`__init__.py`** re-exports the public API. Nothing else lives here.
33
+
34
+ ### Key design decisions
35
+
36
+ 1. **Fail-open by default, fail-closed optional.** If the matching logic itself raises an unexpected exception, the connection is allowed and a warning is logged (fail-open). Users can set `fail_closed=True` for stricter environments where errors should block rather than allow.
37
+
38
+ 2. **Audit hooks are irremovable.** `sys.addaudithook` has no corresponding remove function. This is a feature for security (malicious code can't unhook it) but means tests must use `deactivate()` + `_reset_state()` for cleanup rather than removing the hook.
39
+
40
+ 3. **IP-to-hostname mapping via getaddrinfo interception.** When `socket.getaddrinfo("api.stripe.com", 443)` fires, we resolve it ourselves (with a reentrancy guard) and store `{resolved_ip: "api.stripe.com"}`. When `socket.connect(sock, (resolved_ip, 443))` fires later, we look up the hostname and check it against the policy.
41
+
42
+ 4. **Thread safety.** `AllowPolicy` is immutable. The `_Config` bundle is a frozen dataclass swapped atomically (single reference assignment). `_ip_to_hostname` is an `OrderedDict` guarded by `_ip_map_lock` with LRU eviction. Reentrancy guard uses `threading.local()`.
43
+
44
+ 5. **Zero dependencies.** Everything uses stdlib only: `sys`, `socket`, `threading`, `collections`, `ipaddress`, `fnmatch`, `logging`, `dataclasses`.
45
+
46
+ ## Conventions
47
+
48
+ ### Code style
49
+
50
+ - Use `from __future__ import annotations` in all modules.
51
+ - Private modules are prefixed with `_` (e.g., `_core.py`, `_policy.py`).
52
+ - Type hints on all public function signatures.
53
+ - No docstrings on private helpers unless the logic is non-obvious.
54
+ - Keep modules small and focused. If a module exceeds ~200 lines, consider splitting.
55
+
56
+ ### Testing
57
+
58
+ - **Egress guard** (`conftest.py`): An independent audit hook that uses `AllowPolicy` to block unexpected network access between tests (when tethered is deactivated). Only `dns.google` and localhost are allowed. When tethered IS active, its own hook handles enforcement and the guard is a no-op.
59
+ - **Unit tests** (`test_policy.py`): Test `AllowPolicy` in isolation. No audit hooks, no network calls. This is where the bulk of pattern-matching coverage lives.
60
+ - **Integration tests** (`test_core.py`): Test `activate()`/`deactivate()` with real sockets (sync and async). Use the `_cleanup` autouse fixture that calls `_reset_state()` after each test. Async tests use `pytest-asyncio` with `asyncio_mode = "auto"`.
61
+ - Run tests with: `uv run pytest tests/ -v`
62
+ - Run with coverage: `uv run pytest tests/ -v --cov`
63
+ - Tests must not depend on external network availability for correctness. Use known IPs, localhost, and `settimeout()` to handle network-level failures gracefully.
64
+
65
+ ### Linting and formatting
66
+
67
+ - Ruff handles linting and formatting. Configuration is in `pyproject.toml` under `[tool.ruff]`.
68
+ - Bandit handles security scanning. Configuration is in `pyproject.toml` under `[tool.bandit]`. Tests are excluded. Intentional suppressions use inline `# nosec BXXX` comments.
69
+ - Lint: `uv run ruff check .` (auto-fix with `--fix`)
70
+ - Format: `uv run ruff format .`
71
+ - Security: `uv run bandit -c pyproject.toml -r src/`
72
+ - Pre-commit hooks run ruff, bandit, markdownlint, and pytest on every commit, and commitizen on commit messages. All hooks are pinned by commit SHA for supply-chain integrity. Install with `uv run pre-commit install --hook-type pre-commit --hook-type commit-msg`.
73
+ - CI runs `pre-commit run --all-files` (lint job) and the full test matrix. GitHub Actions are pinned by commit SHA. The workflow uses `permissions: { contents: read }` for least privilege. A separate CodeQL workflow runs on push to main, on PRs, and weekly.
74
+
75
+ ### What NOT to do
76
+
77
+ - Do not add runtime dependencies. This library must remain zero-dep.
78
+ - Do not monkey-patch `socket.socket` or any other stdlib class. The audit hook API is the only interception mechanism.
79
+ - Do not catch `EgressBlocked` inside tethered itself (except in tests). It must propagate to the caller.
80
+ - Do not add framework-specific code (Django, Flask, etc.) to the core package. Framework integrations belong in documentation, not code.
81
+ - Do not add features speculatively. Every addition must serve a concrete, current use case.
82
+ - Do not stage or commit changes. The developer reviews and commits manually.
83
+
84
+ ### Adding new allow rule types
85
+
86
+ If adding a new rule type (e.g., regex patterns, deny lists):
87
+
88
+ 1. Add a new `_XxxRule` dataclass in `_policy.py`.
89
+ 2. Parse it in `AllowPolicy.__init__`.
90
+ 3. Check it in `is_allowed()` or `_check_hostname()`/`_check_ip()`.
91
+ 4. Add unit tests in `test_policy.py` first, then integration tests if needed.
92
+
93
+ ### Python version support
94
+
95
+ - Python 3.10–3.14. `sys.addaudithook` was added in 3.8, so this is well within range.
96
+ - Do not use syntax or features unavailable in 3.10 (e.g., no `type` statement from 3.12, no `except*` from 3.11).
@@ -0,0 +1,20 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ type: software
4
+ title: "Tethered"
5
+ abstract: "Runtime network egress control for Python"
6
+ version: 0.1.0
7
+ date-released: 2026-02-24
8
+ license: MIT
9
+ repository-code: "https://github.com/shcherbak-ai/tethered"
10
+ url: "https://github.com/shcherbak-ai/tethered"
11
+ authors:
12
+ - family-names: "Shcherbak"
13
+ given-names: "Sergii"
14
+ keywords:
15
+ - security
16
+ - egress
17
+ - network
18
+ - audit
19
+ - supply-chain
20
+ - python
@@ -0,0 +1 @@
1
+ See @AGENTS.md for project context, architecture, and conventions.
@@ -0,0 +1,92 @@
1
+ # Contributing to Tethered
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ git clone https://github.com/shcherbak-ai/tethered.git
7
+ cd tethered
8
+ uv sync
9
+ uv run pre-commit install --hook-type pre-commit --hook-type commit-msg
10
+ ```
11
+
12
+ ## Running tests
13
+
14
+ ```bash
15
+ uv run pytest tests/ -v
16
+ ```
17
+
18
+ With coverage:
19
+
20
+ ```bash
21
+ uv run pytest tests/ -v --cov
22
+ ```
23
+
24
+ ## Linting and formatting
25
+
26
+ Pre-commit hooks run all checks automatically on every commit, but you can also run them manually:
27
+
28
+ ```bash
29
+ uv run pre-commit run --all-files
30
+ ```
31
+
32
+ Individual tools:
33
+
34
+ ```bash
35
+ uv run ruff check . # lint
36
+ uv run ruff check --fix . # lint with auto-fix
37
+ uv run ruff format . # format
38
+ uv run bandit -c pyproject.toml -r src/ # security scan
39
+ ```
40
+
41
+ ## Commit messages
42
+
43
+ This project uses [Conventional Commits](https://www.conventionalcommits.org/) enforced by [commitizen](https://commitizen-tools.github.io/commitizen/). The commit-msg hook validates every commit message automatically. Run `uv run cz commit` for an interactive session that guides you through the format.
44
+
45
+ ## Project structure
46
+
47
+ ```text
48
+ src/tethered/
49
+ __init__.py # Public API: activate(), deactivate(), EgressBlocked
50
+ _policy.py # AllowPolicy — pattern parsing and matching (pure logic)
51
+ _core.py # Audit hook, state management, IP-to-hostname resolution
52
+ tests/
53
+ test_policy.py # Unit tests for AllowPolicy (no network)
54
+ test_core.py # Integration tests with real sockets
55
+ ```
56
+
57
+ ## Network safety in tests
58
+
59
+ ### IP addresses
60
+
61
+ Use only reserved, unroutable ranges for IP addresses in tests:
62
+
63
+ | Range | Name | RFC |
64
+ |---|---|---|
65
+ | `192.0.2.0/24` | TEST-NET-1 | RFC 5737 |
66
+ | `198.51.100.0/24` | TEST-NET-2 | RFC 5737 |
67
+ | `203.0.113.0/24` | TEST-NET-3 | RFC 5737 |
68
+ | `2001:db8::/32` | Documentation IPv6 | RFC 3849 |
69
+ | `127.0.0.0/8`, `::1` | Loopback | — |
70
+
71
+ Never use real, routable IPs (e.g., `8.8.8.8`, `93.184.215.14`). Even in "allow" tests, TEST-NET addresses ensure no packets reach real servers.
72
+
73
+ ### Hostnames
74
+
75
+ Some integration tests need real DNS resolution to verify that allowed hostnames pass through correctly. Use `dns.google` or `localhost` for this. These are the only real hostnames permitted in tests — they perform only a harmless DNS lookup, no application-level connection is made.
76
+
77
+ Never use hostnames that could trigger unexpected connections to services with side effects (e.g., API endpoints, webhook URLs).
78
+
79
+ ## Code conventions
80
+
81
+ - Zero runtime dependencies — stdlib only.
82
+ - `from __future__ import annotations` in all modules.
83
+ - Private modules prefixed with `_`.
84
+ - Type hints on all public functions.
85
+ - Python 3.10+ only — no syntax from 3.12+ (`type` statement) or 3.11+ (`except*`).
86
+
87
+ ## Testing conventions
88
+
89
+ - **Policy tests** (`test_policy.py`): Pure logic, no audit hooks, no network. Bulk of coverage lives here.
90
+ - **Integration tests** (`test_core.py`): Use real sockets. The `_cleanup` autouse fixture calls `_reset_state()` after each test.
91
+ - Tests must not depend on external network availability for correctness.
92
+ - Blocked connection tests verify `EgressBlocked` is raised before any packet leaves the machine.
tethered-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergii Shcherbak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: tethered
3
+ Version: 0.1.0
4
+ Summary: Runtime network egress control for Python
5
+ Project-URL: Homepage, https://github.com/shcherbak-ai/tethered
6
+ Project-URL: Repository, https://github.com/shcherbak-ai/tethered
7
+ Project-URL: Issues, https://github.com/shcherbak-ai/tethered/issues
8
+ Author: Sergii Shcherbak
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: audit,egress,network,security,supply-chain
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: System :: Networking
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10