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.
- tethered-0.1.0/.github/workflows/ci.yml +73 -0
- tethered-0.1.0/.github/workflows/codeql.yml +30 -0
- tethered-0.1.0/.github/workflows/publish.yml +29 -0
- tethered-0.1.0/.gitignore +40 -0
- tethered-0.1.0/.markdownlint-cli2.yaml +4 -0
- tethered-0.1.0/.pre-commit-config.yaml +42 -0
- tethered-0.1.0/AGENTS.md +96 -0
- tethered-0.1.0/CITATION.cff +20 -0
- tethered-0.1.0/CLAUDE.md +1 -0
- tethered-0.1.0/CONTRIBUTING.md +92 -0
- tethered-0.1.0/LICENSE +21 -0
- tethered-0.1.0/PKG-INFO +24 -0
- tethered-0.1.0/README.md +215 -0
- tethered-0.1.0/pyproject.toml +96 -0
- tethered-0.1.0/src/tethered/__init__.py +9 -0
- tethered-0.1.0/src/tethered/_core.py +468 -0
- tethered-0.1.0/src/tethered/_policy.py +275 -0
- tethered-0.1.0/src/tethered/py.typed +0 -0
- tethered-0.1.0/tests/conftest.py +55 -0
- tethered-0.1.0/tests/test_core.py +1395 -0
- tethered-0.1.0/tests/test_policy.py +482 -0
- tethered-0.1.0/uv.lock +920 -0
|
@@ -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,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
|
tethered-0.1.0/AGENTS.md
ADDED
|
@@ -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
|
tethered-0.1.0/CLAUDE.md
ADDED
|
@@ -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.
|
tethered-0.1.0/PKG-INFO
ADDED
|
@@ -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
|