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.
Files changed (86) hide show
  1. byway-0.6.0/.editorconfig +14 -0
  2. byway-0.6.0/.gitattributes +5 -0
  3. byway-0.6.0/.github/workflows/build.yml +195 -0
  4. byway-0.6.0/.gitignore +20 -0
  5. byway-0.6.0/.python-version +1 -0
  6. byway-0.6.0/CLAUDE.md +23 -0
  7. byway-0.6.0/CONTRIBUTING.md +3 -0
  8. byway-0.6.0/LICENSE +5 -0
  9. byway-0.6.0/LICENSES/0BSD.txt +5 -0
  10. byway-0.6.0/PKG-INFO +111 -0
  11. byway-0.6.0/README.md +80 -0
  12. byway-0.6.0/REUSE.toml +14 -0
  13. byway-0.6.0/docs/DESIGN.md +93 -0
  14. byway-0.6.0/docs/DEVELOPMENT.md +65 -0
  15. byway-0.6.0/docs/MANUAL.md +253 -0
  16. byway-0.6.0/docs/OPERATIONS.md +50 -0
  17. byway-0.6.0/docs/http-async.md +59 -0
  18. byway-0.6.0/docs/http-sync.md +83 -0
  19. byway-0.6.0/docs/protocol.md +110 -0
  20. byway-0.6.0/docs/throughput.md +28 -0
  21. byway-0.6.0/pyproject.toml +119 -0
  22. byway-0.6.0/src/byway/__init__.py +69 -0
  23. byway-0.6.0/src/byway/__main__.py +12 -0
  24. byway-0.6.0/src/byway/_logging.py +61 -0
  25. byway-0.6.0/src/byway/api.py +324 -0
  26. byway-0.6.0/src/byway/auth/__init__.py +10 -0
  27. byway-0.6.0/src/byway/auth/client.py +326 -0
  28. byway-0.6.0/src/byway/auth/models.py +108 -0
  29. byway-0.6.0/src/byway/auth/xml_forms.py +305 -0
  30. byway-0.6.0/src/byway/cli/__init__.py +126 -0
  31. byway-0.6.0/src/byway/cli/cmd_http.py +449 -0
  32. byway-0.6.0/src/byway/cli/cmd_nc.py +340 -0
  33. byway-0.6.0/src/byway/cli/cmd_password.py +99 -0
  34. byway-0.6.0/src/byway/cli/cmd_probe.py +181 -0
  35. byway-0.6.0/src/byway/cli/cmd_up.py +603 -0
  36. byway-0.6.0/src/byway/cli/dashboard.py +485 -0
  37. byway-0.6.0/src/byway/cli/main.py +96 -0
  38. byway-0.6.0/src/byway/cli/terminal.py +245 -0
  39. byway-0.6.0/src/byway/config.py +62 -0
  40. byway-0.6.0/src/byway/cstp/__init__.py +9 -0
  41. byway-0.6.0/src/byway/cstp/config.py +254 -0
  42. byway-0.6.0/src/byway/cstp/framing.py +137 -0
  43. byway-0.6.0/src/byway/cstp/handshake.py +124 -0
  44. byway-0.6.0/src/byway/cstp/link.py +390 -0
  45. byway-0.6.0/src/byway/cstp/session.py +426 -0
  46. byway-0.6.0/src/byway/errors.py +127 -0
  47. byway-0.6.0/src/byway/profile.py +189 -0
  48. byway-0.6.0/src/byway/sync_api.py +221 -0
  49. byway-0.6.0/tests/__init__.py +0 -0
  50. byway-0.6.0/tests/corpus/asa_challenge.xml +14 -0
  51. byway-0.6.0/tests/corpus/asa_group_list.xml +20 -0
  52. byway-0.6.0/tests/corpus/asa_login_failed.xml +16 -0
  53. byway-0.6.0/tests/corpus/asa_main_form.xml +18 -0
  54. byway-0.6.0/tests/corpus/asa_success.xml +8 -0
  55. byway-0.6.0/tests/fakegw/__init__.py +15 -0
  56. byway-0.6.0/tests/fakegw/server.py +284 -0
  57. byway-0.6.0/tests/integration/__init__.py +0 -0
  58. byway-0.6.0/tests/integration/test_api.py +302 -0
  59. byway-0.6.0/tests/integration/test_auth_flow.py +196 -0
  60. byway-0.6.0/tests/integration/test_http.py +313 -0
  61. byway-0.6.0/tests/integration/test_https.py +228 -0
  62. byway-0.6.0/tests/integration/test_nc.py +258 -0
  63. byway-0.6.0/tests/integration/test_reconnect.py +439 -0
  64. byway-0.6.0/tests/integration/test_socks.py +386 -0
  65. byway-0.6.0/tests/integration/test_sync_api.py +122 -0
  66. byway-0.6.0/tests/integration/test_tunnel.py +370 -0
  67. byway-0.6.0/tests/unit/__init__.py +0 -0
  68. byway-0.6.0/tests/unit/test_cli_connect_validation.py +64 -0
  69. byway-0.6.0/tests/unit/test_cli_global_flags.py +83 -0
  70. byway-0.6.0/tests/unit/test_cli_http.py +182 -0
  71. byway-0.6.0/tests/unit/test_cli_liveness.py +140 -0
  72. byway-0.6.0/tests/unit/test_cli_password.py +281 -0
  73. byway-0.6.0/tests/unit/test_cli_probe.py +138 -0
  74. byway-0.6.0/tests/unit/test_cli_profile_merge.py +224 -0
  75. byway-0.6.0/tests/unit/test_cli_socks_flags.py +94 -0
  76. byway-0.6.0/tests/unit/test_dashboard.py +355 -0
  77. byway-0.6.0/tests/unit/test_framing.py +137 -0
  78. byway-0.6.0/tests/unit/test_handshake.py +143 -0
  79. byway-0.6.0/tests/unit/test_logging_redaction.py +58 -0
  80. byway-0.6.0/tests/unit/test_profile.py +297 -0
  81. byway-0.6.0/tests/unit/test_terminal.py +167 -0
  82. byway-0.6.0/tests/unit/test_tunnel_config.py +163 -0
  83. byway-0.6.0/tests/unit/test_write_strategy.py +133 -0
  84. byway-0.6.0/tests/unit/test_xml_forms.py +296 -0
  85. byway-0.6.0/tools/tcp-retransmits.ps1 +47 -0
  86. 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,5 @@
1
+ # SPDX-FileCopyrightText: 2026 RenΓ© de Hesselle <dehesselle@web.de>
2
+ #
3
+ # SPDX-License-Identifier: 0BSD
4
+
5
+ * text=auto eol=lf
@@ -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.
@@ -0,0 +1,3 @@
1
+ # Contribution guidelines
2
+
3
+ I'm open to receiving bug reports, but I'm not really interested in anything else at this point. You can still bring forward whatever you want to say, just manage your expectations accordingly.
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.