byway 0.6.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.
- byway-0.6.0/.editorconfig +14 -0
- byway-0.6.0/.gitattributes +5 -0
- byway-0.6.0/.github/workflows/build.yml +195 -0
- byway-0.6.0/.gitignore +20 -0
- byway-0.6.0/.python-version +1 -0
- byway-0.6.0/CLAUDE.md +23 -0
- byway-0.6.0/CONTRIBUTING.md +3 -0
- byway-0.6.0/LICENSE +5 -0
- byway-0.6.0/LICENSES/0BSD.txt +5 -0
- byway-0.6.0/PKG-INFO +111 -0
- byway-0.6.0/README.md +80 -0
- byway-0.6.0/REUSE.toml +14 -0
- byway-0.6.0/docs/DESIGN.md +93 -0
- byway-0.6.0/docs/DEVELOPMENT.md +65 -0
- byway-0.6.0/docs/MANUAL.md +253 -0
- byway-0.6.0/docs/OPERATIONS.md +50 -0
- byway-0.6.0/docs/http-async.md +59 -0
- byway-0.6.0/docs/http-sync.md +83 -0
- byway-0.6.0/docs/protocol.md +110 -0
- byway-0.6.0/docs/throughput.md +28 -0
- byway-0.6.0/pyproject.toml +119 -0
- byway-0.6.0/src/byway/__init__.py +69 -0
- byway-0.6.0/src/byway/__main__.py +12 -0
- byway-0.6.0/src/byway/_logging.py +61 -0
- byway-0.6.0/src/byway/api.py +324 -0
- byway-0.6.0/src/byway/auth/__init__.py +10 -0
- byway-0.6.0/src/byway/auth/client.py +326 -0
- byway-0.6.0/src/byway/auth/models.py +108 -0
- byway-0.6.0/src/byway/auth/xml_forms.py +305 -0
- byway-0.6.0/src/byway/cli/__init__.py +126 -0
- byway-0.6.0/src/byway/cli/cmd_http.py +449 -0
- byway-0.6.0/src/byway/cli/cmd_nc.py +340 -0
- byway-0.6.0/src/byway/cli/cmd_password.py +99 -0
- byway-0.6.0/src/byway/cli/cmd_probe.py +181 -0
- byway-0.6.0/src/byway/cli/cmd_up.py +603 -0
- byway-0.6.0/src/byway/cli/dashboard.py +485 -0
- byway-0.6.0/src/byway/cli/main.py +96 -0
- byway-0.6.0/src/byway/cli/terminal.py +245 -0
- byway-0.6.0/src/byway/config.py +62 -0
- byway-0.6.0/src/byway/cstp/__init__.py +9 -0
- byway-0.6.0/src/byway/cstp/config.py +254 -0
- byway-0.6.0/src/byway/cstp/framing.py +137 -0
- byway-0.6.0/src/byway/cstp/handshake.py +124 -0
- byway-0.6.0/src/byway/cstp/link.py +390 -0
- byway-0.6.0/src/byway/cstp/session.py +426 -0
- byway-0.6.0/src/byway/errors.py +127 -0
- byway-0.6.0/src/byway/profile.py +189 -0
- byway-0.6.0/src/byway/sync_api.py +221 -0
- byway-0.6.0/tests/__init__.py +0 -0
- byway-0.6.0/tests/corpus/asa_challenge.xml +14 -0
- byway-0.6.0/tests/corpus/asa_group_list.xml +20 -0
- byway-0.6.0/tests/corpus/asa_login_failed.xml +16 -0
- byway-0.6.0/tests/corpus/asa_main_form.xml +18 -0
- byway-0.6.0/tests/corpus/asa_success.xml +8 -0
- byway-0.6.0/tests/fakegw/__init__.py +15 -0
- byway-0.6.0/tests/fakegw/server.py +284 -0
- byway-0.6.0/tests/integration/__init__.py +0 -0
- byway-0.6.0/tests/integration/test_api.py +302 -0
- byway-0.6.0/tests/integration/test_auth_flow.py +196 -0
- byway-0.6.0/tests/integration/test_http.py +313 -0
- byway-0.6.0/tests/integration/test_https.py +228 -0
- byway-0.6.0/tests/integration/test_nc.py +258 -0
- byway-0.6.0/tests/integration/test_reconnect.py +439 -0
- byway-0.6.0/tests/integration/test_socks.py +386 -0
- byway-0.6.0/tests/integration/test_sync_api.py +122 -0
- byway-0.6.0/tests/integration/test_tunnel.py +370 -0
- byway-0.6.0/tests/unit/__init__.py +0 -0
- byway-0.6.0/tests/unit/test_cli_connect_validation.py +64 -0
- byway-0.6.0/tests/unit/test_cli_global_flags.py +83 -0
- byway-0.6.0/tests/unit/test_cli_http.py +182 -0
- byway-0.6.0/tests/unit/test_cli_liveness.py +140 -0
- byway-0.6.0/tests/unit/test_cli_password.py +281 -0
- byway-0.6.0/tests/unit/test_cli_probe.py +138 -0
- byway-0.6.0/tests/unit/test_cli_profile_merge.py +224 -0
- byway-0.6.0/tests/unit/test_cli_socks_flags.py +94 -0
- byway-0.6.0/tests/unit/test_dashboard.py +355 -0
- byway-0.6.0/tests/unit/test_framing.py +137 -0
- byway-0.6.0/tests/unit/test_handshake.py +143 -0
- byway-0.6.0/tests/unit/test_logging_redaction.py +58 -0
- byway-0.6.0/tests/unit/test_profile.py +297 -0
- byway-0.6.0/tests/unit/test_terminal.py +167 -0
- byway-0.6.0/tests/unit/test_tunnel_config.py +163 -0
- byway-0.6.0/tests/unit/test_write_strategy.py +133 -0
- byway-0.6.0/tests/unit/test_xml_forms.py +296 -0
- byway-0.6.0/tools/tcp-retransmits.ps1 +47 -0
- byway-0.6.0/uv.lock +934 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 RenΓ© de Hesselle <dehesselle@web.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: 0BSD
|
|
4
|
+
|
|
5
|
+
root = true
|
|
6
|
+
|
|
7
|
+
[*]
|
|
8
|
+
end_of_line = lf
|
|
9
|
+
insert_final_newline = true
|
|
10
|
+
charset = utf-8
|
|
11
|
+
trim_trailing_whitespace = true
|
|
12
|
+
|
|
13
|
+
[*.md]
|
|
14
|
+
trim_trailing_whitespace = false
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 RenΓ© de Hesselle <dehesselle@web.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: 0BSD
|
|
4
|
+
|
|
5
|
+
name: Build
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: ["**"]
|
|
9
|
+
tags: ["v[0-9]*"]
|
|
10
|
+
pull_request:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
#-----------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
Lint:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout repository
|
|
22
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
23
|
+
with:
|
|
24
|
+
persist-credentials: false
|
|
25
|
+
|
|
26
|
+
- name: Install uv
|
|
27
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
28
|
+
|
|
29
|
+
# keep aligned with [tool.ruff] in pyproject.toml
|
|
30
|
+
- name: Check formatting
|
|
31
|
+
run: uvx --from 'ruff~=0.16.0' ruff format --check .
|
|
32
|
+
|
|
33
|
+
- name: Lint
|
|
34
|
+
run: uvx --from 'ruff~=0.16.0' ruff check .
|
|
35
|
+
|
|
36
|
+
- name: Check REUSE compliance
|
|
37
|
+
run: uvx reuse lint
|
|
38
|
+
|
|
39
|
+
#-----------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
Typecheck:
|
|
42
|
+
strategy:
|
|
43
|
+
fail-fast: false
|
|
44
|
+
matrix:
|
|
45
|
+
os: [ubuntu-latest, windows-latest]
|
|
46
|
+
runs-on: ${{ matrix.os }}
|
|
47
|
+
steps:
|
|
48
|
+
# full history for hatch-vcs
|
|
49
|
+
- name: Checkout repository
|
|
50
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
51
|
+
with:
|
|
52
|
+
fetch-depth: 0
|
|
53
|
+
persist-credentials: false
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
57
|
+
|
|
58
|
+
# per OS: mypy narrows sys.platform checks to the host
|
|
59
|
+
- name: Type-check
|
|
60
|
+
run: uv run --locked mypy src
|
|
61
|
+
|
|
62
|
+
#-----------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
Build:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
# full history for hatch-vcs
|
|
68
|
+
- name: Checkout repository
|
|
69
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
70
|
+
with:
|
|
71
|
+
fetch-depth: 0
|
|
72
|
+
persist-credentials: false
|
|
73
|
+
|
|
74
|
+
- name: Install uv
|
|
75
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
76
|
+
|
|
77
|
+
- name: Build project
|
|
78
|
+
run: uv build --no-sources
|
|
79
|
+
|
|
80
|
+
- name: Smoke-test wheel
|
|
81
|
+
run: |
|
|
82
|
+
uv run --isolated --no-project --with dist/*.whl python - <<'EOF'
|
|
83
|
+
import os
|
|
84
|
+
import byway
|
|
85
|
+
print(byway.__version__)
|
|
86
|
+
if os.environ["GITHUB_REF_TYPE"] == "tag":
|
|
87
|
+
expected = os.environ["GITHUB_REF_NAME"].removeprefix("v")
|
|
88
|
+
assert byway.__version__ == expected, (byway.__version__, expected)
|
|
89
|
+
EOF
|
|
90
|
+
|
|
91
|
+
- name: Upload artifacts
|
|
92
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
93
|
+
with:
|
|
94
|
+
name: dist
|
|
95
|
+
path: dist/
|
|
96
|
+
|
|
97
|
+
#-----------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
Test:
|
|
100
|
+
needs:
|
|
101
|
+
- Build
|
|
102
|
+
strategy:
|
|
103
|
+
fail-fast: false
|
|
104
|
+
matrix:
|
|
105
|
+
os: [ubuntu-latest, windows-latest]
|
|
106
|
+
python: ["3.12", "3.13", "3.14"]
|
|
107
|
+
runs-on: ${{ matrix.os }}
|
|
108
|
+
defaults:
|
|
109
|
+
run:
|
|
110
|
+
shell: bash
|
|
111
|
+
steps:
|
|
112
|
+
# Only the tests; byway comes from the wheel the Build job made.
|
|
113
|
+
# .gitattributes keeps the corpus and the tests at LF on Windows.
|
|
114
|
+
- name: Checkout repository
|
|
115
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
116
|
+
with:
|
|
117
|
+
persist-credentials: false
|
|
118
|
+
sparse-checkout: |
|
|
119
|
+
tests
|
|
120
|
+
pyproject.toml
|
|
121
|
+
.gitattributes
|
|
122
|
+
sparse-checkout-cone-mode: false
|
|
123
|
+
|
|
124
|
+
- name: Download artifacts
|
|
125
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
126
|
+
with:
|
|
127
|
+
name: dist
|
|
128
|
+
path: dist/
|
|
129
|
+
|
|
130
|
+
- name: Install uv
|
|
131
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
132
|
+
with:
|
|
133
|
+
python-version: ${{ matrix.python }}
|
|
134
|
+
|
|
135
|
+
# The versions in uv.lock. byway's own dependencies, usernet among them,
|
|
136
|
+
# resolve from the wheel's metadata, as they do for users.
|
|
137
|
+
- name: Install wheel and test dependencies
|
|
138
|
+
run: |
|
|
139
|
+
uv venv
|
|
140
|
+
uv pip install "$(ls dist/*.whl)[keyring]" "usernet[testing]" pytest==9.1.1 pytest-asyncio==1.4.0 pytest-timeout==2.4.0 hypothesis==6.165.9 trustme==1.2.1
|
|
141
|
+
|
|
142
|
+
- name: Run tests
|
|
143
|
+
run: uv run --no-project pytest -q -m "not needs_gateway"
|
|
144
|
+
|
|
145
|
+
#-----------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
Release:
|
|
148
|
+
runs-on: ubuntu-latest
|
|
149
|
+
permissions:
|
|
150
|
+
contents: write
|
|
151
|
+
needs:
|
|
152
|
+
- Lint
|
|
153
|
+
- Typecheck
|
|
154
|
+
- Build
|
|
155
|
+
- Test
|
|
156
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
157
|
+
steps:
|
|
158
|
+
- name: Download artifacts
|
|
159
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
160
|
+
with:
|
|
161
|
+
name: dist
|
|
162
|
+
path: dist/
|
|
163
|
+
|
|
164
|
+
- name: Create draft release
|
|
165
|
+
env:
|
|
166
|
+
GH_TOKEN: ${{ github.token }}
|
|
167
|
+
run: >-
|
|
168
|
+
gh release create "$GITHUB_REF_NAME" dist/*
|
|
169
|
+
--repo "$GITHUB_REPOSITORY"
|
|
170
|
+
--draft
|
|
171
|
+
--verify-tag
|
|
172
|
+
--generate-notes
|
|
173
|
+
|
|
174
|
+
#-----------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
PyPI:
|
|
177
|
+
runs-on: ubuntu-latest
|
|
178
|
+
permissions:
|
|
179
|
+
id-token: write
|
|
180
|
+
needs:
|
|
181
|
+
- Build
|
|
182
|
+
- Release
|
|
183
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
184
|
+
environment:
|
|
185
|
+
name: pypi
|
|
186
|
+
url: https://pypi.org/p/byway
|
|
187
|
+
steps:
|
|
188
|
+
- name: Download artifacts
|
|
189
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
190
|
+
with:
|
|
191
|
+
name: dist
|
|
192
|
+
path: dist/
|
|
193
|
+
|
|
194
|
+
- name: Publish to PyPI
|
|
195
|
+
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
byway-0.6.0/.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 RenΓ© de Hesselle <dehesselle@web.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: 0BSD
|
|
4
|
+
|
|
5
|
+
# Python-generated files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[oc]
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
wheels/
|
|
11
|
+
*.egg-info
|
|
12
|
+
.mypy_cache
|
|
13
|
+
.pytest_cache
|
|
14
|
+
.ruff_cache
|
|
15
|
+
|
|
16
|
+
# Virtual environments
|
|
17
|
+
.venv
|
|
18
|
+
|
|
19
|
+
# other
|
|
20
|
+
.hypothesis
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
byway-0.6.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
byway is a userspace client that speaks the Cisco AnyConnect VPN protocol: the tunnel
|
|
4
|
+
terminates in an in-process TCP/IP stack (from `usernet`), exposed through SOCKS5 and a
|
|
5
|
+
Python API.
|
|
6
|
+
|
|
7
|
+
- Read [docs/DESIGN.md](docs/DESIGN.md) before architectural changes and respect its core
|
|
8
|
+
principles.
|
|
9
|
+
- CLI options, profiles and the library API: [docs/MANUAL.md](docs/MANUAL.md).
|
|
10
|
+
- Runtime behavior (idle timeout, reconnects, throughput): [docs/OPERATIONS.md](docs/OPERATIONS.md).
|
|
11
|
+
- Gateway quirks and protocol facts: [docs/protocol.md](docs/protocol.md). Add anything
|
|
12
|
+
learned the hard way there.
|
|
13
|
+
- Anything not specific to this VPN protocol belongs in `../usernet`, not here.
|
|
14
|
+
|
|
15
|
+
@docs/DEVELOPMENT.md
|
|
16
|
+
|
|
17
|
+
## Documentation rules
|
|
18
|
+
|
|
19
|
+
- Keep the README short: what byway is and a working usage example. Everything else goes
|
|
20
|
+
into `docs/`.
|
|
21
|
+
- A change that affects design, usage or behavior updates the matching document in the same
|
|
22
|
+
change.
|
|
23
|
+
- DESIGN.md is kept current and carries no project history.
|
byway-0.6.0/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (C) 2026 by RenΓ© de Hesselle
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
|
4
|
+
|
|
5
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (C) 2026 by RenΓ© de Hesselle
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
|
4
|
+
|
|
5
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
byway-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: byway
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Userspace client that speaks the Cisco AnyConnect VPN protocol, with an in-process TCP/IP stack
|
|
5
|
+
Project-URL: Homepage, https://github.com/dehesselle/byway
|
|
6
|
+
Project-URL: Repository, https://github.com/dehesselle/byway
|
|
7
|
+
Author-email: RenΓ© de Hesselle <dehesselle@web.de>
|
|
8
|
+
License-Expression: 0BSD
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: LICENSES/0BSD.txt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
22
|
+
Classifier: Topic :: System :: Networking
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: defusedxml>=0.7.1
|
|
25
|
+
Requires-Dist: httpx<0.29,>=0.27
|
|
26
|
+
Requires-Dist: platformdirs>=4
|
|
27
|
+
Requires-Dist: usernet[httpx]>=0.7
|
|
28
|
+
Provides-Extra: keyring
|
|
29
|
+
Requires-Dist: keyring>=25; extra == 'keyring'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# byway
|
|
33
|
+
|
|
34
|
+
> __Byway__ Β· _English noun_
|
|
35
|
+
>
|
|
36
|
+
> A secondary road or path, away from the main route.
|
|
37
|
+
|
|
38
|
+
byway is a userspace client that speaks the Cisco AnyConnect VPN protocol. No tun device, no
|
|
39
|
+
virtual adapter, no routing changes, no administrator rights: the tunnel and everything carried
|
|
40
|
+
over it live inside the Python process. Access is through a local SOCKS5 proxy, so `ssh` and
|
|
41
|
+
`curl` work unchanged, and an in-process API.
|
|
42
|
+
|
|
43
|
+
Use it when you need to reach a REST API or SSH into a host inside a corporate VPN, but
|
|
44
|
+
cannot, or would rather not, install a VPN client, change routing, or use admin rights. VPN
|
|
45
|
+
access is scoped to one process rather than to the whole machine.
|
|
46
|
+
|
|
47
|
+
## Install and run
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ uv tool install byway
|
|
51
|
+
$ byway probe vpn.corp.example # what does this gateway ask for?
|
|
52
|
+
$ byway up vpn.corp.example -u user --socks
|
|
53
|
+
address 172.29.29.3/255.255.0.0
|
|
54
|
+
mtu 1383
|
|
55
|
+
routes 10.0.0.0/23
|
|
56
|
+
socks5 127.0.0.1:1080
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`byway up` prompts for the password and holds the tunnel open. From another terminal:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
curl --socks5 127.0.0.1:1080 http://10.0.0.20/
|
|
63
|
+
ssh -o ProxyCommand="byway nc --via 127.0.0.1:1080 %h %p" user@10.0.0.32
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Saved profiles, other ways to pass the password, and certificate pinning are in the
|
|
67
|
+
[manual](https://github.com/dehesselle/byway/blob/master/docs/MANUAL.md).
|
|
68
|
+
|
|
69
|
+
## Use it from your own program
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
uv add byway
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import byway
|
|
77
|
+
|
|
78
|
+
async with byway.connect("vpn.corp.example", user, password) as vpn:
|
|
79
|
+
reader, writer = await vpn.open_connection("10.0.0.32", 22)
|
|
80
|
+
|
|
81
|
+
async with vpn.httpx_client() as http:
|
|
82
|
+
response = await http.get("http://10.0.0.20/v1/things")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Caveats
|
|
86
|
+
|
|
87
|
+
- Hosts are IP addresses, or names from a fixed table in the profile: there is no in-tunnel
|
|
88
|
+
DNS.
|
|
89
|
+
- byway supports IPv4, TLS, outbound TCP and username/password authentication only.
|
|
90
|
+
- Many organisations require a sanctioned VPN client, and byway bypasses posture assessment
|
|
91
|
+
(CSD/HostScan) entirely. Check that you are allowed to use it before you point it at a
|
|
92
|
+
corporate gateway.
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
- [docs/MANUAL.md](https://github.com/dehesselle/byway/blob/master/docs/MANUAL.md) β all commands, profiles, credentials, certificate pinning and the library API.
|
|
97
|
+
- [docs/http-async.md](https://github.com/dehesselle/byway/blob/master/docs/http-async.md) β calling REST APIs from async code.
|
|
98
|
+
- [docs/http-sync.md](https://github.com/dehesselle/byway/blob/master/docs/http-sync.md) β calling REST APIs from sync code.
|
|
99
|
+
- [docs/OPERATIONS.md](https://github.com/dehesselle/byway/blob/master/docs/OPERATIONS.md) β runtime behavior: idle timeouts, reconnects, sessions, throughput, diagnostics.
|
|
100
|
+
- [docs/DESIGN.md](https://github.com/dehesselle/byway/blob/master/docs/DESIGN.md) β architecture, core principles and deliberate omissions.
|
|
101
|
+
- [docs/protocol.md](https://github.com/dehesselle/byway/blob/master/docs/protocol.md) β protocol notes and gateway observations.
|
|
102
|
+
- [docs/throughput.md](https://github.com/dehesselle/byway/blob/master/docs/throughput.md) β throughput measurements.
|
|
103
|
+
- [docs/DEVELOPMENT.md](https://github.com/dehesselle/byway/blob/master/docs/DEVELOPMENT.md) β building and testing.
|
|
104
|
+
|
|
105
|
+
## AI disclosure
|
|
106
|
+
|
|
107
|
+
Substantial parts of this code were generated with Sonnet 5.x/Opus 5.x. Generated code was tested, but not every line was reviewed in detail.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
[0BSD](https://github.com/dehesselle/byway/blob/master/LICENSES/0BSD.txt)
|
byway-0.6.0/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# byway
|
|
2
|
+
|
|
3
|
+
> __Byway__ Β· _English noun_
|
|
4
|
+
>
|
|
5
|
+
> A secondary road or path, away from the main route.
|
|
6
|
+
|
|
7
|
+
byway is a userspace client that speaks the Cisco AnyConnect VPN protocol. No tun device, no
|
|
8
|
+
virtual adapter, no routing changes, no administrator rights: the tunnel and everything carried
|
|
9
|
+
over it live inside the Python process. Access is through a local SOCKS5 proxy, so `ssh` and
|
|
10
|
+
`curl` work unchanged, and an in-process API.
|
|
11
|
+
|
|
12
|
+
Use it when you need to reach a REST API or SSH into a host inside a corporate VPN, but
|
|
13
|
+
cannot, or would rather not, install a VPN client, change routing, or use admin rights. VPN
|
|
14
|
+
access is scoped to one process rather than to the whole machine.
|
|
15
|
+
|
|
16
|
+
## Install and run
|
|
17
|
+
|
|
18
|
+
```console
|
|
19
|
+
$ uv tool install byway
|
|
20
|
+
$ byway probe vpn.corp.example # what does this gateway ask for?
|
|
21
|
+
$ byway up vpn.corp.example -u user --socks
|
|
22
|
+
address 172.29.29.3/255.255.0.0
|
|
23
|
+
mtu 1383
|
|
24
|
+
routes 10.0.0.0/23
|
|
25
|
+
socks5 127.0.0.1:1080
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`byway up` prompts for the password and holds the tunnel open. From another terminal:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
curl --socks5 127.0.0.1:1080 http://10.0.0.20/
|
|
32
|
+
ssh -o ProxyCommand="byway nc --via 127.0.0.1:1080 %h %p" user@10.0.0.32
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Saved profiles, other ways to pass the password, and certificate pinning are in the
|
|
36
|
+
[manual](https://github.com/dehesselle/byway/blob/master/docs/MANUAL.md).
|
|
37
|
+
|
|
38
|
+
## Use it from your own program
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
uv add byway
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import byway
|
|
46
|
+
|
|
47
|
+
async with byway.connect("vpn.corp.example", user, password) as vpn:
|
|
48
|
+
reader, writer = await vpn.open_connection("10.0.0.32", 22)
|
|
49
|
+
|
|
50
|
+
async with vpn.httpx_client() as http:
|
|
51
|
+
response = await http.get("http://10.0.0.20/v1/things")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Caveats
|
|
55
|
+
|
|
56
|
+
- Hosts are IP addresses, or names from a fixed table in the profile: there is no in-tunnel
|
|
57
|
+
DNS.
|
|
58
|
+
- byway supports IPv4, TLS, outbound TCP and username/password authentication only.
|
|
59
|
+
- Many organisations require a sanctioned VPN client, and byway bypasses posture assessment
|
|
60
|
+
(CSD/HostScan) entirely. Check that you are allowed to use it before you point it at a
|
|
61
|
+
corporate gateway.
|
|
62
|
+
|
|
63
|
+
## Documentation
|
|
64
|
+
|
|
65
|
+
- [docs/MANUAL.md](https://github.com/dehesselle/byway/blob/master/docs/MANUAL.md) β all commands, profiles, credentials, certificate pinning and the library API.
|
|
66
|
+
- [docs/http-async.md](https://github.com/dehesselle/byway/blob/master/docs/http-async.md) β calling REST APIs from async code.
|
|
67
|
+
- [docs/http-sync.md](https://github.com/dehesselle/byway/blob/master/docs/http-sync.md) β calling REST APIs from sync code.
|
|
68
|
+
- [docs/OPERATIONS.md](https://github.com/dehesselle/byway/blob/master/docs/OPERATIONS.md) β runtime behavior: idle timeouts, reconnects, sessions, throughput, diagnostics.
|
|
69
|
+
- [docs/DESIGN.md](https://github.com/dehesselle/byway/blob/master/docs/DESIGN.md) β architecture, core principles and deliberate omissions.
|
|
70
|
+
- [docs/protocol.md](https://github.com/dehesselle/byway/blob/master/docs/protocol.md) β protocol notes and gateway observations.
|
|
71
|
+
- [docs/throughput.md](https://github.com/dehesselle/byway/blob/master/docs/throughput.md) β throughput measurements.
|
|
72
|
+
- [docs/DEVELOPMENT.md](https://github.com/dehesselle/byway/blob/master/docs/DEVELOPMENT.md) β building and testing.
|
|
73
|
+
|
|
74
|
+
## AI disclosure
|
|
75
|
+
|
|
76
|
+
Substantial parts of this code were generated with Sonnet 5.x/Opus 5.x. Generated code was tested, but not every line was reviewed in detail.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
[0BSD](https://github.com/dehesselle/byway/blob/master/LICENSES/0BSD.txt)
|
byway-0.6.0/REUSE.toml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
|
|
3
|
+
# Files that can't carry an SPDX comment, or shouldn't: docs stay clean when
|
|
4
|
+
# rendered, the corpus is test input for the parsers, and uv.lock is
|
|
5
|
+
# regenerated.
|
|
6
|
+
[[annotations]]
|
|
7
|
+
path = [
|
|
8
|
+
"**.md",
|
|
9
|
+
"tests/corpus/*.xml",
|
|
10
|
+
"uv.lock",
|
|
11
|
+
".python-version",
|
|
12
|
+
]
|
|
13
|
+
SPDX-FileCopyrightText = "2026 RenΓ© de Hesselle <dehesselle@web.de>"
|
|
14
|
+
SPDX-License-Identifier = "0BSD"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# byway design
|
|
2
|
+
|
|
3
|
+
How byway is built and why. This is the standing design reference: keep it current when the
|
|
4
|
+
design changes.
|
|
5
|
+
|
|
6
|
+
## Architecture
|
|
7
|
+
|
|
8
|
+
openconnect solves the same problem as byway by handing decapsulated IP packets to a tun
|
|
9
|
+
device and letting the kernel do TCP. byway refuses that, so it has to terminate IP itself:
|
|
10
|
+
the gateway hands us raw IP packets, and an in-process TCP/IP stack turns them back into
|
|
11
|
+
streams.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
curl, ssh ββSOCKS5βββ
|
|
15
|
+
βββΆ usernet: TCP/IP stack, driver ββIpLinkβββΆ ReconnectingLink ββCSTP/TLSβββΆ gateway
|
|
16
|
+
your code ββAPIββββββ (SOCKS5 server, stream bridge) (this repository)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The TCP/IP stack, the driver, the SOCKS5 server and the transport bridge are not in this
|
|
20
|
+
repository. They are [`usernet`](https://github.com/dehesselle/usernet), because none of
|
|
21
|
+
that code knows what AnyConnect is and a second client β a Tailscale one β needed exactly the
|
|
22
|
+
same half. What is left here is CSTP: authentication, the tunnel handshake, frame transport,
|
|
23
|
+
and reconnection.
|
|
24
|
+
|
|
25
|
+
The seam is `usernet.link.IpLink`: anything that can carry raw IPv4 packets and say what
|
|
26
|
+
address it was assigned. `ReconnectingLink` is this project's implementation of it.
|
|
27
|
+
|
|
28
|
+
## Core principles
|
|
29
|
+
|
|
30
|
+
1. **No tun device, no routing changes, no administrator rights.** Everything carried over
|
|
31
|
+
the tunnel lives inside the Python process, so VPN access is scoped to that process.
|
|
32
|
+
2. **Only the VPN protocol lives here.** Anything not specific to it belongs in `usernet`,
|
|
33
|
+
behind `IpLink`.
|
|
34
|
+
3. **Every session ends with a logout.** Leaving `byway.connect()` closes the tunnel *and*
|
|
35
|
+
posts the logout, whatever went wrong. That is not housekeeping: gateways enforce
|
|
36
|
+
concurrent-session limits, and leaked sessions eventually lock you out of your own VPN
|
|
37
|
+
with nothing to point at.
|
|
38
|
+
4. **Credentials never go in `argv` or in the config file.** Passwords come from stdin, the
|
|
39
|
+
environment, the keyring or a prompt ([MANUAL.md](MANUAL.md#credentials)).
|
|
40
|
+
5. **One CSTP frame per TLS record.** A Cisco ASA hangs up on anything else; see
|
|
41
|
+
[protocol.md](protocol.md#one-frame-per-tls-record--mandatory). `--coalesce` exists for
|
|
42
|
+
experiments only and must never be the default.
|
|
43
|
+
6. **Names come from the tunnel's host table or nowhere.** Falling back to the host's
|
|
44
|
+
resolver would send internal hostnames to whatever DNS the machine uses. A name is
|
|
45
|
+
resolved from a static table (the profile's `hosts`, `--add-host`, or `hosts=` in the
|
|
46
|
+
API) and refused otherwise, everywhere: SOCKS5, `http`, `nc` and the library. The table
|
|
47
|
+
is a `usernet.resolve.StaticResolver` on the driver, so every consumer gets the same
|
|
48
|
+
answer, and routing is checked against the address, not the name.
|
|
49
|
+
7. **Streams are real stdlib streams, and backpressure reaches the peer.**
|
|
50
|
+
`StreamReader`'s high-water mark calls `pause_reading()`, which stops us draining the
|
|
51
|
+
stack, which shrinks the advertised window, which stops the far end sending β without a
|
|
52
|
+
queue anywhere in between.
|
|
53
|
+
8. **SOCKS5 binds loopback unless told otherwise.** An unauthenticated proxy on a routable
|
|
54
|
+
address is an open door into the corporate network for anything that can reach the
|
|
55
|
+
machine.
|
|
56
|
+
|
|
57
|
+
## Deliberate omissions
|
|
58
|
+
|
|
59
|
+
- **No DNS client, a static host table only.** The gateway this was developed against
|
|
60
|
+
advertises no DNS servers at all, so there is nothing to ask, and the machines behind it
|
|
61
|
+
have fixed addresses. The table matches names exactly: no wildcards, no search domain, no
|
|
62
|
+
reverse lookups. Querying a DNS server would also need UDP, which byway does not carry. On
|
|
63
|
+
a gateway that does advertise DNS servers, a resolver using them could take the table's
|
|
64
|
+
place on the driver.
|
|
65
|
+
- **No compression.** byway does not advertise `X-CSTP-Accept-Encoding`, so LZS, LZ4 and
|
|
66
|
+
deflate never need implementing. A compressed frame arriving anyway is a loud error.
|
|
67
|
+
- **TLS only, no DTLS.** Everything rides the TLS connection.
|
|
68
|
+
- **IPv4 only, outbound TCP only.**
|
|
69
|
+
- **Username and password only.** SAML/SSO, client certificates and MFA challenges are
|
|
70
|
+
detected and reported clearly, but not supported.
|
|
71
|
+
- **No posture assessment.** CSD/HostScan is bypassed entirely.
|
|
72
|
+
|
|
73
|
+
## Reconnection
|
|
74
|
+
|
|
75
|
+
`ReconnectingLink` rebuilds the tunnel when it ends, unless `reconnect=False`
|
|
76
|
+
(`--no-reconnect`). Some gateways assign a fresh address on every connection. Sockets bound
|
|
77
|
+
to the old address cannot survive that, so they are failed with an error saying exactly
|
|
78
|
+
that, rather than left to hang. The tunnel and the SOCKS listener stay up.
|
|
79
|
+
|
|
80
|
+
A gateway's `X-CSTP-Rekey-Method: ssl` is treated as `new-tunnel`, because Python's `ssl`
|
|
81
|
+
cannot renegotiate ([protocol.md](protocol.md#cisco-asa--secure-client-515)).
|
|
82
|
+
|
|
83
|
+
## Buffer size
|
|
84
|
+
|
|
85
|
+
One connection cannot exceed `buffer Γ· RTT`, whatever the code does, so the default
|
|
86
|
+
per-socket buffer is 1 MiB, raised with `--buffer KIB`. Bigger is not automatically better:
|
|
87
|
+
past about 1 MiB the
|
|
88
|
+
sender's bursts start overrunning queues below us and the returns go negative. The
|
|
89
|
+
measurements are in [throughput.md](throughput.md).
|
|
90
|
+
|
|
91
|
+
If a link ever needs more than this can give, `usernet`'s `NetworkBackend` protocol exists
|
|
92
|
+
so a native stack could be dropped in. On a high-latency or policed link that would change
|
|
93
|
+
nothing, because the limit is not the CPU.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Building and testing byway. The design is described in [DESIGN.md](DESIGN.md).
|
|
4
|
+
|
|
5
|
+
byway uses Python 3.12 and [uv](https://docs.astral.sh/uv/):
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uv sync --dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The version comes from git tags via hatch-vcs, so clone with full history.
|
|
12
|
+
|
|
13
|
+
## Checks
|
|
14
|
+
|
|
15
|
+
CI runs ruff and the REUSE check once on Ubuntu, mypy on Ubuntu and Windows, and pytest on
|
|
16
|
+
Ubuntu and Windows with Python 3.12, 3.13 and 3.14. Run these before committing:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
uvx --from 'ruff~=0.16.0' ruff check .
|
|
20
|
+
uvx --from 'ruff~=0.16.0' ruff format --check .
|
|
21
|
+
uvx reuse lint
|
|
22
|
+
uv run mypy src
|
|
23
|
+
uv run pytest -q -m "not needs_gateway"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Ruff isn't a project dependency. `required-version` in `pyproject.toml` and the pin in
|
|
27
|
+
`.github/workflows/build.yml` are bumped together, once a new minor release's findings have
|
|
28
|
+
been reviewed.
|
|
29
|
+
- CI tests the built wheel rather than the checkout, against the test dependency versions
|
|
30
|
+
pinned in `.github/workflows/build.yml`. Update those pins when `uv.lock` moves them.
|
|
31
|
+
- The project is REUSE-compliant. New files start with the SPDX header that `reuse annotate`
|
|
32
|
+
writes; files that can't carry a comment get covered in `REUSE.toml`.
|
|
33
|
+
|
|
34
|
+
## Releasing
|
|
35
|
+
|
|
36
|
+
Pushing a tag `vX.Y.Z` runs the full workflow and, if every job passes, attaches the built
|
|
37
|
+
sdist and wheel to a draft GitHub release and publishes them to PyPI. The build job checks that
|
|
38
|
+
the wheel's `__version__` matches the tag. PyPI accepts the upload through trusted publishing,
|
|
39
|
+
which requires a publisher on pypi.org for this repository, the workflow `build.yml` and the
|
|
40
|
+
GitHub environment `pypi`. The draft release is published by hand.
|
|
41
|
+
|
|
42
|
+
## usernet
|
|
43
|
+
|
|
44
|
+
[`usernet`](https://github.com/dehesselle/usernet) is an ordinary dependency from PyPI. To
|
|
45
|
+
try byway against a local checkout of it, install that into the venv with
|
|
46
|
+
`uv pip install -e ../usernet`; the next `uv sync` puts the released version back.
|
|
47
|
+
|
|
48
|
+
## How it is tested
|
|
49
|
+
|
|
50
|
+
CSTP is what this repository is responsible for, and the ways a gateway can surprise you are
|
|
51
|
+
what it tests:
|
|
52
|
+
|
|
53
|
+
- **Codecs** (`tests/unit`): CSTP framing and the tunnel headers, auth XML against a corpus
|
|
54
|
+
of real gateway responses (`tests/corpus`), plus property-based tests feeding arbitrary
|
|
55
|
+
bytes into every parser and asserting that only declared parse errors escape.
|
|
56
|
+
- **A fake CSTP gateway** (`tests/fakegw`) that serves the auth exchange, speaks framing,
|
|
57
|
+
injects faults, and terminates IP into a test peer β so `byway up` β SOCKS5 β a real HTTP
|
|
58
|
+
GET runs in CI with no VPN anywhere (`tests/integration`).
|
|
59
|
+
- **Reconnection and rekey**, including the case that matters most: a gateway that assigns a
|
|
60
|
+
different address on reconnect, where every open socket has to fail with a reason rather
|
|
61
|
+
than hang.
|
|
62
|
+
|
|
63
|
+
The stack's own test layers β the frozen-clock state machine, the virtual network, and
|
|
64
|
+
conformance against a real Linux kernel over a tun device degraded with `netem` β live in
|
|
65
|
+
`usernet` along with the code they cover.
|