xray-tools 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.
- xray_tools-0.1.0/.github/workflows/publish.yml +54 -0
- xray_tools-0.1.0/.github/workflows/test.yml +34 -0
- xray_tools-0.1.0/.gitignore +35 -0
- xray_tools-0.1.0/PKG-INFO +33 -0
- xray_tools-0.1.0/README.md +20 -0
- xray_tools-0.1.0/pyproject.toml +42 -0
- xray_tools-0.1.0/tests/__init__.py +0 -0
- xray_tools-0.1.0/tests/test_config.py +223 -0
- xray_tools-0.1.0/tests/test_protocol.py +30 -0
- xray_tools-0.1.0/xray_tools/__init__.py +1 -0
- xray_tools-0.1.0/xray_tools/base.py +29 -0
- xray_tools-0.1.0/xray_tools/config.py +741 -0
- xray_tools-0.1.0/xray_tools/protocols.py +127 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
uses: ./.github/workflows/test.yml
|
|
13
|
+
|
|
14
|
+
publish:
|
|
15
|
+
name: Publish
|
|
16
|
+
needs: test
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
environment:
|
|
19
|
+
name: pypi
|
|
20
|
+
url: https://pypi.org/p/xray-tools
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write
|
|
23
|
+
contents: read
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.12"
|
|
30
|
+
|
|
31
|
+
- name: Set version from the tag
|
|
32
|
+
run: |
|
|
33
|
+
version="${GITHUB_REF_NAME#v}"
|
|
34
|
+
sed -i "s/^__version__ = \".*\"/__version__ = \"${version}\"/" xray_tools/__init__.py
|
|
35
|
+
|
|
36
|
+
- name: Build
|
|
37
|
+
run: |
|
|
38
|
+
pip install build
|
|
39
|
+
python -m build
|
|
40
|
+
|
|
41
|
+
- name: Verify version matches the tag
|
|
42
|
+
run: |
|
|
43
|
+
version="${GITHUB_REF_NAME#v}"
|
|
44
|
+
shopt -s nullglob
|
|
45
|
+
wheels=(dist/*-"${version}"-*.whl)
|
|
46
|
+
sdists=(dist/*-"${version}".tar.gz)
|
|
47
|
+
if (( ${#wheels[@]} == 0 || ${#sdists[@]} == 0 )); then
|
|
48
|
+
echo "Expected version ${version} in dist/, got:"
|
|
49
|
+
ls -la dist
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
- name: Publish to PyPI
|
|
54
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Python ${{ matrix.python-version }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install Poetry
|
|
28
|
+
run: pipx install poetry
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: poetry install --with test --no-interaction --no-ansi
|
|
32
|
+
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: poetry run pytest
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Byte-compiled / optimized
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
*.egg
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
|
|
18
|
+
# Test and type-check caches
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
.coverage.*
|
|
24
|
+
htmlcov/
|
|
25
|
+
coverage.xml
|
|
26
|
+
|
|
27
|
+
# Secrets
|
|
28
|
+
.env
|
|
29
|
+
.env.*
|
|
30
|
+
|
|
31
|
+
# Editors and OS
|
|
32
|
+
.idea/
|
|
33
|
+
.vscode/
|
|
34
|
+
*.swp
|
|
35
|
+
.DS_Store
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xray-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure Python library for Xray-core configuration
|
|
5
|
+
Project-URL: Repository, https://github.com/shizacat/xray-tools
|
|
6
|
+
Author: Alexey Matveev
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: pydantic<3.0.0,>=2.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Xray tools — Python library
|
|
15
|
+
|
|
16
|
+
Create and manage [Xray-core](https://github.com/XTLS/Xray-core) configuration: inbounds, outbounds, routing and protocol settings. Pure Python implementation.
|
|
17
|
+
|
|
18
|
+
Links:
|
|
19
|
+
- https://github.com/XTLS/Xray-core
|
|
20
|
+
- https://xtls.github.io/config/
|
|
21
|
+
|
|
22
|
+
# Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install xray-tools
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
# Development
|
|
29
|
+
|
|
30
|
+
Run tests:
|
|
31
|
+
```bash
|
|
32
|
+
pytest
|
|
33
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Xray tools — Python library
|
|
2
|
+
|
|
3
|
+
Create and manage [Xray-core](https://github.com/XTLS/Xray-core) configuration: inbounds, outbounds, routing and protocol settings. Pure Python implementation.
|
|
4
|
+
|
|
5
|
+
Links:
|
|
6
|
+
- https://github.com/XTLS/Xray-core
|
|
7
|
+
- https://xtls.github.io/config/
|
|
8
|
+
|
|
9
|
+
# Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install xray-tools
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
# Development
|
|
16
|
+
|
|
17
|
+
Run tests:
|
|
18
|
+
```bash
|
|
19
|
+
pytest
|
|
20
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xray-tools"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Pure Python library for Xray-core configuration"
|
|
5
|
+
authors = [{ name = "Alexey Matveev" }]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"pydantic (>=2.0,<3.0.0)",
|
|
11
|
+
]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Repository = "https://github.com/shizacat/xray-tools"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.version]
|
|
21
|
+
path = "xray_tools/__init__.py"
|
|
22
|
+
|
|
23
|
+
[tool.poetry]
|
|
24
|
+
package-mode = false
|
|
25
|
+
|
|
26
|
+
[tool.poetry.group.dev.dependencies]
|
|
27
|
+
black = "^22.10.0"
|
|
28
|
+
|
|
29
|
+
[tool.poetry.group.test.dependencies]
|
|
30
|
+
pytest = "^6.2.5"
|
|
31
|
+
pytest-mock = "^3.6.1"
|
|
32
|
+
|
|
33
|
+
[tool.black]
|
|
34
|
+
target-version = ["py310"]
|
|
35
|
+
|
|
36
|
+
[tool.isort]
|
|
37
|
+
py_version = 310
|
|
38
|
+
profile = "black"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["hatchling"]
|
|
42
|
+
build-backend = "hatchling.build"
|
|
File without changes
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pytest import fixture
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
from xray_tools import config
|
|
8
|
+
from xray_tools import config as xray_config
|
|
9
|
+
from xray_tools import protocols
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# --- Fixture ---
|
|
13
|
+
|
|
14
|
+
@fixture
|
|
15
|
+
def xray_config_ex1() -> xray_config.XRayConfig:
|
|
16
|
+
return config.XRayConfig.from_config_file("""
|
|
17
|
+
{
|
|
18
|
+
"log": {"loglevel": "error"},
|
|
19
|
+
"inbounds": [
|
|
20
|
+
{
|
|
21
|
+
"port": 443,
|
|
22
|
+
"protocol": "vless",
|
|
23
|
+
"tag": "vless_tls",
|
|
24
|
+
"settings": {
|
|
25
|
+
"clients": [
|
|
26
|
+
{
|
|
27
|
+
"id": "{{ xray.uuid }}",
|
|
28
|
+
"email": "user@{{ ansible_fqdn }}",
|
|
29
|
+
"flow": "xtls-rprx-vision"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"decryption": "none"
|
|
33
|
+
},
|
|
34
|
+
"streamSettings": {
|
|
35
|
+
"network": "tcp",
|
|
36
|
+
"security": "reality",
|
|
37
|
+
"realitySettings": {
|
|
38
|
+
"show": false,
|
|
39
|
+
"dest": "www.microsoft.com:443",
|
|
40
|
+
"xver": 0,
|
|
41
|
+
"serverNames": [
|
|
42
|
+
"www.microsoft.com"
|
|
43
|
+
],
|
|
44
|
+
"privateKey": "{{ xray.x25519_private }}",
|
|
45
|
+
"minClientVer": "",
|
|
46
|
+
"maxClientVer": "",
|
|
47
|
+
"maxTimeDiff": 0,
|
|
48
|
+
"shortIds": [
|
|
49
|
+
"{{ xray.short_id }}"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"sniffing": {
|
|
54
|
+
"enabled": true,
|
|
55
|
+
"destOverride": [
|
|
56
|
+
"http",
|
|
57
|
+
"tls"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
""")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --- Tests ---
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_create_empty_config():
|
|
70
|
+
obj = config.XRayConfig()
|
|
71
|
+
|
|
72
|
+
# get config
|
|
73
|
+
c = obj.to_config_file()
|
|
74
|
+
assert isinstance(c, str)
|
|
75
|
+
|
|
76
|
+
# get config with indent
|
|
77
|
+
c = obj.to_config_file(indent=4)
|
|
78
|
+
assert isinstance(c, str)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_config_create_from_file(xray_config_ex1: xray_config.XRayConfig):
|
|
82
|
+
"""
|
|
83
|
+
Check type of config
|
|
84
|
+
"""
|
|
85
|
+
assert isinstance(xray_config_ex1, config.XRayConfig)
|
|
86
|
+
assert isinstance(
|
|
87
|
+
xray_config_ex1.inbounds[0].settings, protocols.ProtocolSettingsVless)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_config_to_config_file(xray_config_ex1: xray_config.XRayConfig):
|
|
91
|
+
"""
|
|
92
|
+
Check as create dump
|
|
93
|
+
"""
|
|
94
|
+
dump = xray_config_ex1.to_config_file()
|
|
95
|
+
assert isinstance(dump, str)
|
|
96
|
+
dump_dict = json.loads(dump)
|
|
97
|
+
assert len(dump_dict["inbounds"][0]["settings"]["clients"]) == 1
|
|
98
|
+
assert dump_dict["inbounds"][0]["settings"]["decryption"] == "none"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_config_create_fill_01():
|
|
102
|
+
"""
|
|
103
|
+
Создаем и заполняем конфиг
|
|
104
|
+
"""
|
|
105
|
+
config = xray_config.XRayConfig(
|
|
106
|
+
inbounds=[
|
|
107
|
+
xray_config.Inbound(
|
|
108
|
+
settings=protocols.ProtocolSettingsVless(),
|
|
109
|
+
)
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
config.inbounds[0].settings.client_add(email="test@test.ru")
|
|
113
|
+
assert isinstance(config.to_config_file(), str)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_log_crete_default():
|
|
117
|
+
r = config.Log()
|
|
118
|
+
assert isinstance(r, config.Log)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_stream_settings():
|
|
122
|
+
r = config.StreamSettings()
|
|
123
|
+
assert isinstance(r, config.StreamSettings)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_inbound():
|
|
127
|
+
r = config.Inbound(settings=protocols.ProtocolSettingsVless())
|
|
128
|
+
assert isinstance(r, config.Inbound)
|
|
129
|
+
assert r.protocol == protocols.InboundProtocol.vless
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_outbound():
|
|
133
|
+
r = config.Outbound()
|
|
134
|
+
assert isinstance(r, config.Outbound)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_stats():
|
|
138
|
+
r = xray_config.Stats()
|
|
139
|
+
assert isinstance(r, xray_config.Stats)
|
|
140
|
+
|
|
141
|
+
# Проверяем, что stats добавился как пустой словарь
|
|
142
|
+
obj = config.XRayConfig(stats=xray_config.Stats())
|
|
143
|
+
cfg = json.loads(obj.to_config_file())
|
|
144
|
+
assert cfg["stats"] == {}
|
|
145
|
+
|
|
146
|
+
# Check, that stats is not included in config
|
|
147
|
+
obj = config.XRayConfig()
|
|
148
|
+
cfg = json.loads(obj.to_config_file())
|
|
149
|
+
assert "stats" not in cfg
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_api():
|
|
153
|
+
r = xray_config.Api(tag="api")
|
|
154
|
+
assert isinstance(r, xray_config.Api)
|
|
155
|
+
|
|
156
|
+
# Проверяем, что api не включен в config
|
|
157
|
+
obj = config.XRayConfig()
|
|
158
|
+
cfg = json.loads(obj.to_config_file())
|
|
159
|
+
assert "api" not in cfg
|
|
160
|
+
|
|
161
|
+
# Проверяем, что api добавился в config
|
|
162
|
+
obj = config.XRayConfig(
|
|
163
|
+
api=xray_config.Api(
|
|
164
|
+
tag="api",
|
|
165
|
+
services=[xray_config.ApiService.StatsService]
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
cfg = json.loads(obj.to_config_file())
|
|
169
|
+
assert cfg["api"]["tag"] == "api"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_policy():
|
|
173
|
+
r = xray_config.Policy()
|
|
174
|
+
assert isinstance(r, xray_config.Policy)
|
|
175
|
+
|
|
176
|
+
# Check level key format, raise ValidationError
|
|
177
|
+
with pytest.raises(ValidationError):
|
|
178
|
+
xray_config.Policy(levels={"test": {}})
|
|
179
|
+
|
|
180
|
+
# Check level key correct format
|
|
181
|
+
r = xray_config.Policy(levels={"0": xray_config.PolicyLevel()})
|
|
182
|
+
assert isinstance(r, xray_config.Policy)
|
|
183
|
+
|
|
184
|
+
# Проверяем, что policy не включен в config
|
|
185
|
+
obj = config.XRayConfig()
|
|
186
|
+
cfg = json.loads(obj.to_config_file())
|
|
187
|
+
assert "policy" not in cfg
|
|
188
|
+
|
|
189
|
+
# Проверяем, что policy добавился в config
|
|
190
|
+
obj = config.XRayConfig(
|
|
191
|
+
policy=xray_config.Policy(
|
|
192
|
+
levels={
|
|
193
|
+
"0": xray_config.PolicyLevel(
|
|
194
|
+
statsUserUplink=True,
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
cfg = json.loads(obj.to_config_file())
|
|
200
|
+
assert cfg["policy"]["levels"]["0"]["statsUserUplink"] is True
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_ws_settings_roundtrip():
|
|
204
|
+
"""path/host у WebSocket не теряются при разборе и сборке конфига."""
|
|
205
|
+
import json as _json
|
|
206
|
+
from xray_tools.config import XRayConfig as _XRayConfig
|
|
207
|
+
|
|
208
|
+
raw = {
|
|
209
|
+
"inbounds": [{
|
|
210
|
+
"listen": "127.0.0.1", "port": 10443, "protocol": "vless",
|
|
211
|
+
"settings": {"clients": [], "decryption": "none"},
|
|
212
|
+
"streamSettings": {
|
|
213
|
+
"network": "ws", "security": "none",
|
|
214
|
+
"wsSettings": {"path": "/media/stream/abc",
|
|
215
|
+
"host": "cloud.example.org"},
|
|
216
|
+
},
|
|
217
|
+
}],
|
|
218
|
+
}
|
|
219
|
+
cfg = _XRayConfig.model_validate_json(_json.dumps(raw))
|
|
220
|
+
out = _json.loads(cfg.to_config_file())
|
|
221
|
+
ws = out["inbounds"][0]["streamSettings"]["wsSettings"]
|
|
222
|
+
assert ws["path"] == "/media/stream/abc"
|
|
223
|
+
assert ws["host"] == "cloud.example.org"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from xray_tools import protocols
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_protocol_vless_add_remove_client():
|
|
5
|
+
obj = protocols.ProtocolSettingsVless()
|
|
6
|
+
|
|
7
|
+
# Add
|
|
8
|
+
c = obj.client_add(email="test@example.com")
|
|
9
|
+
assert len(obj.clients) == 1
|
|
10
|
+
assert isinstance(c, protocols.ClientVless)
|
|
11
|
+
assert c == obj.clients[0]
|
|
12
|
+
|
|
13
|
+
# Remove
|
|
14
|
+
obj.client_remove(c.id)
|
|
15
|
+
assert len(obj.clients) == 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_protocol_vless_add_client_more_than_one():
|
|
19
|
+
"""При повторном добавлении клиента не добавлять его
|
|
20
|
+
при этом не должно быть исключения
|
|
21
|
+
"""
|
|
22
|
+
obj = protocols.ProtocolSettingsVless()
|
|
23
|
+
|
|
24
|
+
# Add
|
|
25
|
+
c = obj.client_add(client_id="test", email="test@example.com")
|
|
26
|
+
c = obj.client_add(client_id="test", email="test@example.com")
|
|
27
|
+
c = obj.client_add(client_id="test", email="test@example.com")
|
|
28
|
+
assert isinstance(c, protocols.ClientVless)
|
|
29
|
+
assert len(obj.clients) == 1
|
|
30
|
+
assert c == obj.clients[0]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseModelConfig(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Общая модель для всех конфигов
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def model_dump(self, *args, **kwargs) -> Dict:
|
|
14
|
+
"""Return dump config
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
- exclude none
|
|
18
|
+
- enum as value
|
|
19
|
+
"""
|
|
20
|
+
kwargs.setdefault("exclude_none", True)
|
|
21
|
+
# Ensures enums are dumped as values in python mode
|
|
22
|
+
kwargs.setdefault("mode", "python")
|
|
23
|
+
r = super().model_dump(*args, **kwargs)
|
|
24
|
+
return r
|
|
25
|
+
|
|
26
|
+
def model_dump_json(self, *args, **kwargs) -> str:
|
|
27
|
+
"""Return dump config as json"""
|
|
28
|
+
kwargs.setdefault("exclude_none", True)
|
|
29
|
+
return super().model_dump_json(*args, **kwargs)
|
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example:
|
|
3
|
+
|
|
4
|
+
{
|
|
5
|
+
"log": {
|
|
6
|
+
"loglevel": "info"
|
|
7
|
+
},
|
|
8
|
+
"routing": {
|
|
9
|
+
"rules": [],
|
|
10
|
+
"domainStrategy": "AsIs"
|
|
11
|
+
},
|
|
12
|
+
"inbounds": [
|
|
13
|
+
{
|
|
14
|
+
"port": 443,
|
|
15
|
+
"protocol": "vless",
|
|
16
|
+
"tag": "vless_tls",
|
|
17
|
+
"settings": {
|
|
18
|
+
"clients": [
|
|
19
|
+
{
|
|
20
|
+
"id": "{{ xray.uuid }}",
|
|
21
|
+
"email": "user@{{ ansible_fqdn }}",
|
|
22
|
+
"flow": "xtls-rprx-vision"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"decryption": "none"
|
|
26
|
+
},
|
|
27
|
+
"streamSettings": {
|
|
28
|
+
"network": "tcp",
|
|
29
|
+
"security": "reality",
|
|
30
|
+
"realitySettings": {
|
|
31
|
+
"show": false,
|
|
32
|
+
"dest": "www.microsoft.com:443",
|
|
33
|
+
"xver": 0,
|
|
34
|
+
"serverNames": [
|
|
35
|
+
"www.microsoft.com"
|
|
36
|
+
],
|
|
37
|
+
"privateKey": "{{ xray.x25519_private }}",
|
|
38
|
+
"minClientVer": "",
|
|
39
|
+
"maxClientVer": "",
|
|
40
|
+
"maxTimeDiff": 0,
|
|
41
|
+
"shortIds": [
|
|
42
|
+
"{{ xray.short_id }}"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"sniffing": {
|
|
47
|
+
"enabled": true,
|
|
48
|
+
"destOverride": [
|
|
49
|
+
"http",
|
|
50
|
+
"tls"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"outbounds": [
|
|
56
|
+
{
|
|
57
|
+
"protocol": "freedom",
|
|
58
|
+
"tag": "direct"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"protocol": "blackhole",
|
|
62
|
+
"tag": "block"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
from __future__ import annotations
|
|
69
|
+
|
|
70
|
+
import json
|
|
71
|
+
from enum import Enum
|
|
72
|
+
from typing import TextIO, Optional, List, Dict, Type, ClassVar, Union
|
|
73
|
+
|
|
74
|
+
# from pydantic import BaseModel, Field, root_validator
|
|
75
|
+
from pydantic import (
|
|
76
|
+
BaseModel,
|
|
77
|
+
Field,
|
|
78
|
+
ConfigDict,
|
|
79
|
+
RootModel,
|
|
80
|
+
field_validator,
|
|
81
|
+
model_validator,
|
|
82
|
+
constr
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
from .protocols import (
|
|
86
|
+
InboundProtocol,
|
|
87
|
+
ProtocolSettingsBase,
|
|
88
|
+
ProtocolSettingsVless,
|
|
89
|
+
ProtocolSettingsDokodemoDoor,
|
|
90
|
+
)
|
|
91
|
+
from .base import BaseModelConfig
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Enums
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LogLevel(str, Enum):
|
|
98
|
+
"""
|
|
99
|
+
Log level
|
|
100
|
+
"debug" | "info" | "warning" | "error" | "none"
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
debug = "debug"
|
|
104
|
+
info = "info"
|
|
105
|
+
warning = "warning"
|
|
106
|
+
error = "error"
|
|
107
|
+
none = "none"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Network(str, Enum):
|
|
111
|
+
tcp = "tcp"
|
|
112
|
+
kcp = "kcp"
|
|
113
|
+
ws = "ws"
|
|
114
|
+
http = "http"
|
|
115
|
+
domainsocket = "domainsocket"
|
|
116
|
+
quic = "quic"
|
|
117
|
+
grpc = "grpc"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class NetworkRoutingRule(str, Enum):
|
|
121
|
+
tcp = "tcp"
|
|
122
|
+
udp = "udp"
|
|
123
|
+
tcp_udp = "tcp,udp"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Security(str, Enum):
|
|
127
|
+
none = "none"
|
|
128
|
+
tls = "tls"
|
|
129
|
+
xtls = "xtls"
|
|
130
|
+
reality = "reality"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DomainStrategy(str, Enum):
|
|
134
|
+
"""
|
|
135
|
+
DomainStrategy
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
AsIs = "AsIs"
|
|
139
|
+
IPIfNonMatch = "IPIfNonMatch"
|
|
140
|
+
IPOnDemand = "IPOnDemand"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class DomainMatcher(str, Enum):
|
|
144
|
+
"""
|
|
145
|
+
DomainMatcher
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
hybrid = "hybrid"
|
|
149
|
+
linear = "linear"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SniffingDest(str, Enum):
|
|
153
|
+
"""
|
|
154
|
+
SniffingDest
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
http = "http"
|
|
158
|
+
tls = "tls"
|
|
159
|
+
quic = "quic"
|
|
160
|
+
fakedns = "fakedns"
|
|
161
|
+
fakedns_others = "fakedns+others"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ApiService(str, Enum):
|
|
165
|
+
"""
|
|
166
|
+
ApiService
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
HandlerService = "HandlerService"
|
|
170
|
+
LoggerService = "LoggerService"
|
|
171
|
+
StatsService = "StatsService"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Classes
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Stats(BaseModelConfig):
|
|
178
|
+
"""
|
|
179
|
+
Включает сбор статистики
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class Api(BaseModelConfig):
|
|
184
|
+
"""
|
|
185
|
+
API interface configuration provides a set
|
|
186
|
+
of APIs based on gRPC for remote invocation.
|
|
187
|
+
|
|
188
|
+
Example
|
|
189
|
+
{
|
|
190
|
+
"api": {
|
|
191
|
+
"tag": "api",
|
|
192
|
+
"listen": "127.0.0.1:8080",
|
|
193
|
+
"services": ["HandlerService", "LoggerService", "StatsService"]
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
"""
|
|
197
|
+
tag: str = Field(
|
|
198
|
+
..., description="The identifier of this inbound connection"
|
|
199
|
+
)
|
|
200
|
+
listen: Optional[str] = Field(
|
|
201
|
+
None,
|
|
202
|
+
description="The listening address. Example: 127.0.0.1:8080"
|
|
203
|
+
)
|
|
204
|
+
services: List[ApiService] = Field(
|
|
205
|
+
default_factory=list,
|
|
206
|
+
description=(
|
|
207
|
+
"The list of services to be enabled. "
|
|
208
|
+
"Available services: HandlerService, LoggerService, StatsService"
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Log(BaseModelConfig):
|
|
214
|
+
access: Optional[str] = Field(None, description="путь к файлу")
|
|
215
|
+
error: Optional[str] = Field(None, description="путь к файлу")
|
|
216
|
+
loglevel: LogLevel = Field(LogLevel.none)
|
|
217
|
+
dnsLog: bool = Field(False)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Ограничиваем ключи только строками, которые соответствуют числу
|
|
221
|
+
PolicyLevelKey = constr(pattern=r'^\d+$')
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class PolicyLevel(BaseModelConfig):
|
|
225
|
+
"""
|
|
226
|
+
Level Policy Object
|
|
227
|
+
"""
|
|
228
|
+
handshake: Optional[int] = Field(
|
|
229
|
+
None, description="Handshake timeout in seconds"
|
|
230
|
+
)
|
|
231
|
+
connIdle: Optional[int] = Field(
|
|
232
|
+
None, description="Idle timeout in seconds"
|
|
233
|
+
)
|
|
234
|
+
uplinkOnly: Optional[int] = Field(
|
|
235
|
+
None, description="Uplink only timeout in seconds"
|
|
236
|
+
)
|
|
237
|
+
downlinkOnly: Optional[int] = Field(
|
|
238
|
+
None, description="Downlink only timeout in seconds"
|
|
239
|
+
)
|
|
240
|
+
statsUserUplink: Optional[bool] = Field(
|
|
241
|
+
None, description="Enable statistics collection for user uplink"
|
|
242
|
+
)
|
|
243
|
+
statsUserDownlink: Optional[bool] = Field(
|
|
244
|
+
None, description="Enable statistics collection for user downlink"
|
|
245
|
+
)
|
|
246
|
+
statsUserOnline: Optional[bool] = Field(
|
|
247
|
+
None, description="Enable statistics collection for user online"
|
|
248
|
+
)
|
|
249
|
+
bufferSize: Optional[int] = Field(
|
|
250
|
+
None, description="Buffer size in bytes"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class PolicySystem(BaseModelConfig):
|
|
255
|
+
"""Policy System"""
|
|
256
|
+
statsInboundUplink: Optional[bool] = Field(
|
|
257
|
+
None, description="Enable statistics collection for inbound uplink"
|
|
258
|
+
)
|
|
259
|
+
statsInboundDownlink: Optional[bool] = Field(
|
|
260
|
+
None, description="Enable statistics collection for inbound downlink"
|
|
261
|
+
)
|
|
262
|
+
statsOutboundUplink: Optional[bool] = Field(
|
|
263
|
+
None, description="Enable statistics collection for outbound uplink"
|
|
264
|
+
)
|
|
265
|
+
statsOutboundDownlink: Optional[bool] = Field(
|
|
266
|
+
None, description="Enable statistics collection for outbound downlink"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class Policy(BaseModelConfig):
|
|
271
|
+
"""
|
|
272
|
+
Local Policy
|
|
273
|
+
Local policy can be used to set different policy settings
|
|
274
|
+
for different user levels
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
"policy": {
|
|
278
|
+
"levels": {
|
|
279
|
+
"0": {
|
|
280
|
+
"handshake": 4,
|
|
281
|
+
"connIdle": 300,
|
|
282
|
+
"uplinkOnly": 2,
|
|
283
|
+
"downlinkOnly": 5,
|
|
284
|
+
"statsUserUplink": false,
|
|
285
|
+
"statsUserDownlink": false,
|
|
286
|
+
"statsUserOnline": false,
|
|
287
|
+
"bufferSize": 4
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"system": {
|
|
291
|
+
"statsInboundUplink": false,
|
|
292
|
+
"statsInboundDownlink": false,
|
|
293
|
+
"statsOutboundUplink": false,
|
|
294
|
+
"statsOutboundDownlink": false
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
"""
|
|
299
|
+
levels: Dict[PolicyLevelKey, PolicyLevel] = Field(
|
|
300
|
+
default_factory=dict,
|
|
301
|
+
description=(
|
|
302
|
+
"Local policy can be used to set different policy settings "
|
|
303
|
+
"for different user levels"
|
|
304
|
+
"Key: '0', '1', ..."
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
system: PolicySystem = Field(
|
|
308
|
+
default_factory=PolicySystem,
|
|
309
|
+
description="System policy settings"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class Certificate(BaseModel):
|
|
314
|
+
"""
|
|
315
|
+
Certificate
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
ocspStapling: int = 3600
|
|
319
|
+
oneTimeLoading: bool = False
|
|
320
|
+
usage: str = "encipherment"
|
|
321
|
+
buildChain: bool = False
|
|
322
|
+
certificateFile: str = ""
|
|
323
|
+
keyFile: str = ""
|
|
324
|
+
certificate: List[str] = Field(default_factory=list)
|
|
325
|
+
key: List[str] = Field(default_factory=list)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class TLSSettings(BaseModel):
|
|
329
|
+
""" """
|
|
330
|
+
|
|
331
|
+
serverName: str = Field(
|
|
332
|
+
"", description="Specifies the domain of the server-side certificate"
|
|
333
|
+
)
|
|
334
|
+
rejectUnknownSni: bool = Field(
|
|
335
|
+
False,
|
|
336
|
+
description=(
|
|
337
|
+
"When true, the server rejects TLS handshakes if the SNI received "
|
|
338
|
+
"does not match domains specified in the certificate. "
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
allowInsecure: bool = Field(False)
|
|
342
|
+
alpn: List[str] = Field(
|
|
343
|
+
["h2", "http/1.1"],
|
|
344
|
+
description=(
|
|
345
|
+
"An array of strings specifying the ALPN values used "
|
|
346
|
+
"in TLS handshakes"
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
minVersion: str = Field("1.2")
|
|
350
|
+
maxVersion: str = Field("1.3")
|
|
351
|
+
cipherSuites: str = Field(
|
|
352
|
+
"",
|
|
353
|
+
description=(
|
|
354
|
+
"Specifies a list of supported cryptographic suites, "
|
|
355
|
+
"with names of each separated by a colon."
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
disableSystemRoot: bool = Field(
|
|
359
|
+
False,
|
|
360
|
+
description=(
|
|
361
|
+
"Whether to disable the CA certificates provided by "
|
|
362
|
+
"the operating system."
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
enableSessionResumption: bool = Field(
|
|
366
|
+
False,
|
|
367
|
+
description=(
|
|
368
|
+
"When false, the session_ticket extension will not be "
|
|
369
|
+
"included in ClientHello"
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
fingerprint: str = Field(
|
|
373
|
+
"",
|
|
374
|
+
description=(
|
|
375
|
+
"Specifies the fingerprint of the TLS Client Hello message"
|
|
376
|
+
),
|
|
377
|
+
)
|
|
378
|
+
pinnedPeerCertificateChainSha256: List[str] = Field(
|
|
379
|
+
default_factory=list,
|
|
380
|
+
description=(
|
|
381
|
+
"Specifies the SHA256 hash values of the certificate chain "
|
|
382
|
+
"of the remote server, using the standard encoding format."
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
masterKeyLog: str = Field(
|
|
386
|
+
"", description=("Path to the (Pre-)Master-Secret log file.")
|
|
387
|
+
)
|
|
388
|
+
certificates: List[Certificate] = Field(
|
|
389
|
+
default_factory=list,
|
|
390
|
+
description=(
|
|
391
|
+
"A list of certificates, each representing a single certificate "
|
|
392
|
+
"(fullchain recommended)."
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class TCPSettings(BaseModel):
|
|
398
|
+
acceptProxyProtocol: bool = False
|
|
399
|
+
header: Dict[str, str] = Field({"type": "none"})
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class KCPSettings(BaseModel):
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class WSSettings(BaseModel):
|
|
407
|
+
acceptProxyProtocol: bool = False
|
|
408
|
+
path: str = Field("/", description="HTTP path of the WebSocket")
|
|
409
|
+
host: str = Field("", description="HTTP Host header sent by the client")
|
|
410
|
+
headers: Dict[str, str] = Field(default_factory=dict)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class HttpSettings(BaseModel):
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class GrpcSettings(BaseModel):
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class HTTPUpgradeSettings(BaseModel):
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class SplitHTTPSettings(BaseModel):
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class ProxySettings(BaseModel):
|
|
430
|
+
tag: str
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class RealitySettings(BaseModel):
|
|
434
|
+
show: bool = Field(False)
|
|
435
|
+
dest: str | None = Field(None)
|
|
436
|
+
xver: int = Field(0)
|
|
437
|
+
serverName: str | None = Field(None)
|
|
438
|
+
serverNames: List[str] = Field(default_factory=list)
|
|
439
|
+
privateKey: str = Field("")
|
|
440
|
+
minClientVer: str = Field("")
|
|
441
|
+
maxClientVer: str = Field("")
|
|
442
|
+
maxTimeDiff: int = Field(0)
|
|
443
|
+
shortIds: List[str] = Field(default_factory=list)
|
|
444
|
+
shortId: str | None = Field(None)
|
|
445
|
+
fingerprint: str | None = Field(None)
|
|
446
|
+
publicKey: str | None = Field(None)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class SockOpt(BaseModel):
|
|
450
|
+
""" """
|
|
451
|
+
|
|
452
|
+
mark: int = 0
|
|
453
|
+
tcpFastOpen: bool = False
|
|
454
|
+
tproxy: str = "off"
|
|
455
|
+
domainStrategy: str = "AsIs"
|
|
456
|
+
dialerProxy: str = ""
|
|
457
|
+
acceptProxyProtocol: bool = False
|
|
458
|
+
tcpKeepAliveInterval: int = 0
|
|
459
|
+
tcpcongestion: str = "bbr"
|
|
460
|
+
interface: str = "wg0"
|
|
461
|
+
tcpMptcp: bool = False
|
|
462
|
+
tcpNoDelay: bool = False
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class StreamSettings(BaseModelConfig):
|
|
466
|
+
"""
|
|
467
|
+
StreamSettings corresponds to the streamSettings property
|
|
468
|
+
in the inbound or outbound config.
|
|
469
|
+
Each inbound or outbound can be configured with different
|
|
470
|
+
transports and can use streamSettings to specify local configs.
|
|
471
|
+
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
network: Network = Field(
|
|
475
|
+
Network.tcp,
|
|
476
|
+
description=(
|
|
477
|
+
"The underlying protocol of the transport "
|
|
478
|
+
"used by the data stream of the connection"
|
|
479
|
+
),
|
|
480
|
+
)
|
|
481
|
+
security: Security = Field(
|
|
482
|
+
Security.none,
|
|
483
|
+
description="Whether to enable transport layer encryption",
|
|
484
|
+
)
|
|
485
|
+
tlsSettings: TLSSettings | None = Field(
|
|
486
|
+
None, description="Configures vanilla TLS"
|
|
487
|
+
)
|
|
488
|
+
tcpSettings: TCPSettings | None = Field(None, description="Configures TCP")
|
|
489
|
+
kcpSettings: KCPSettings | None = Field(
|
|
490
|
+
default=None, description="Configures KCP"
|
|
491
|
+
)
|
|
492
|
+
wsSettings: WSSettings | None = Field(
|
|
493
|
+
None, description="Configures WebSocket"
|
|
494
|
+
)
|
|
495
|
+
httpSettings: HttpSettings | None = Field(
|
|
496
|
+
None, description="Configures HTTP"
|
|
497
|
+
)
|
|
498
|
+
grpcSettings: GrpcSettings | None = Field(
|
|
499
|
+
None, description="Configures gRPC"
|
|
500
|
+
)
|
|
501
|
+
httpupgradeSettings: HTTPUpgradeSettings | None = Field(
|
|
502
|
+
None, description="Configures HTTP"
|
|
503
|
+
)
|
|
504
|
+
splithttpSettings: SplitHTTPSettings | None = Field(
|
|
505
|
+
None, description="Configures HTTP"
|
|
506
|
+
)
|
|
507
|
+
realitySettings: RealitySettings | None = Field(
|
|
508
|
+
None, description="Configures Reality"
|
|
509
|
+
)
|
|
510
|
+
sockopt: SockOpt | None = Field(None)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class Sniffing(BaseModelConfig):
|
|
514
|
+
enabled: bool = True
|
|
515
|
+
destOverride: List[SniffingDest] = Field(
|
|
516
|
+
default_factory=list,
|
|
517
|
+
description=(
|
|
518
|
+
"When the traffic is of a specified type, reset the destination "
|
|
519
|
+
"of the current connection to the target address included "
|
|
520
|
+
"in the list."
|
|
521
|
+
),
|
|
522
|
+
)
|
|
523
|
+
metadataOnly: bool = False
|
|
524
|
+
domainsExcluded: List[str] = Field(
|
|
525
|
+
default_factory=list, description="A list of domain names."
|
|
526
|
+
)
|
|
527
|
+
routeOnly: bool = False
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class Allocate(BaseModel):
|
|
531
|
+
strategy: str = "always"
|
|
532
|
+
refresh: int = 5
|
|
533
|
+
concurrency: int = 3
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class Mux(BaseModel):
|
|
537
|
+
enabled: bool = False
|
|
538
|
+
concurrency: int = 8
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
class RoutingRule(BaseModelConfig):
|
|
542
|
+
domainMatcher: DomainMatcher = Field(DomainMatcher.hybrid)
|
|
543
|
+
type: str = "field"
|
|
544
|
+
domain: List[str] = Field(default_factory=list)
|
|
545
|
+
ip: List[str] = Field(default_factory=list)
|
|
546
|
+
port: Optional[str] = Field(None, description="The target port range")
|
|
547
|
+
sourcePort: Optional[str] = Field(None)
|
|
548
|
+
network: Optional[NetworkRoutingRule] = Field(None)
|
|
549
|
+
source: List[str] = Field(default_factory=list)
|
|
550
|
+
user: List[str] = Field(default_factory=list)
|
|
551
|
+
inboundTag: List[str] = Field(default_factory=list)
|
|
552
|
+
protocol: List[InboundProtocol] = Field(default_factory=list)
|
|
553
|
+
attrs: Dict[str, str] = Field(default_factory=dict)
|
|
554
|
+
outboundTag: Optional[str] = Field(None)
|
|
555
|
+
balancerTag: Optional[str] = Field(None)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class RoutingBalancer(BaseModelConfig):
|
|
559
|
+
tag: str = Field("")
|
|
560
|
+
selector: List[str] = Field(default_factory=list)
|
|
561
|
+
fallbackTag: str = Field("")
|
|
562
|
+
strategy: Dict[str, str] = Field(default_factory=dict)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class Routing(BaseModelConfig):
|
|
566
|
+
domainStrategy: DomainStrategy = Field(
|
|
567
|
+
DomainStrategy.AsIs, description="The domain name resolution strategy"
|
|
568
|
+
)
|
|
569
|
+
domainMatcher: DomainMatcher = Field(
|
|
570
|
+
DomainMatcher.hybrid, description="The domain name matching algorithm"
|
|
571
|
+
)
|
|
572
|
+
rules: List[RoutingRule] = Field(default_factory=list)
|
|
573
|
+
balancers: List[RoutingBalancer] = Field(default_factory=list)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class Inbound(BaseModelConfig):
|
|
577
|
+
"""
|
|
578
|
+
Inbound connections are used to receive incoming data
|
|
579
|
+
and the available protocols are listed in
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
_protocol_settings_map: ClassVar[
|
|
583
|
+
dict[InboundProtocol, Type[ProtocolSettingsBase]]
|
|
584
|
+
] = {
|
|
585
|
+
InboundProtocol.vless: ProtocolSettingsVless,
|
|
586
|
+
InboundProtocol.dokodemo_door: ProtocolSettingsDokodemoDoor,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
listen: str = Field(
|
|
590
|
+
"0.0.0.0",
|
|
591
|
+
description=(
|
|
592
|
+
"The listening address, "
|
|
593
|
+
"either an IP address or a Unix domain socket."
|
|
594
|
+
),
|
|
595
|
+
)
|
|
596
|
+
port: int | str = Field(1080, description="The listening port")
|
|
597
|
+
# Fill from settings model
|
|
598
|
+
protocol: InboundProtocol = Field(
|
|
599
|
+
..., description="The connection protocol name"
|
|
600
|
+
)
|
|
601
|
+
settings: Union[
|
|
602
|
+
ProtocolSettingsBase,
|
|
603
|
+
ProtocolSettingsVless,
|
|
604
|
+
ProtocolSettingsDokodemoDoor
|
|
605
|
+
] = Field(
|
|
606
|
+
default_factory=ProtocolSettingsBase,
|
|
607
|
+
description=(
|
|
608
|
+
"The specific configuration content depends on the protocol"
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
streamSettings: StreamSettings = Field(
|
|
612
|
+
default_factory=StreamSettings,
|
|
613
|
+
description=(
|
|
614
|
+
"The underlying transport method is how the current Xray node "
|
|
615
|
+
"interfaces with other nodes."
|
|
616
|
+
),
|
|
617
|
+
)
|
|
618
|
+
tag: str | None = Field(
|
|
619
|
+
None,
|
|
620
|
+
description=(
|
|
621
|
+
"The identifier of this inbound connection, used to locate "
|
|
622
|
+
"this connection in other configurations."
|
|
623
|
+
),
|
|
624
|
+
)
|
|
625
|
+
sniffing: Sniffing = Field(
|
|
626
|
+
default_factory=Sniffing,
|
|
627
|
+
description=(
|
|
628
|
+
"Configures sniffing, which is used to determine the protocol "
|
|
629
|
+
"of the current connection."
|
|
630
|
+
),
|
|
631
|
+
)
|
|
632
|
+
allocate: Allocate = Field(
|
|
633
|
+
default_factory=Allocate,
|
|
634
|
+
description=(
|
|
635
|
+
"Specifies the specific settings for port allocation when "
|
|
636
|
+
"multiple ports are set up"
|
|
637
|
+
),
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
@model_validator(mode="before")
|
|
641
|
+
@classmethod
|
|
642
|
+
def fill_protocol(cls, values: Dict[str, object]) -> Dict[str, object]:
|
|
643
|
+
settings = values.get("settings", {})
|
|
644
|
+
protocol = values.get("protocol")
|
|
645
|
+
|
|
646
|
+
# Apply settings class based on protocol
|
|
647
|
+
settings_class = cls._protocol_settings_map.get(protocol)
|
|
648
|
+
if settings_class and isinstance(settings, dict):
|
|
649
|
+
values["settings"] = settings_class(**settings)
|
|
650
|
+
|
|
651
|
+
# Ensure protocol is consistent
|
|
652
|
+
if isinstance(values.get("settings"), ProtocolSettingsBase):
|
|
653
|
+
values["protocol"] = values["settings"].protocol.value
|
|
654
|
+
elif isinstance(settings, dict):
|
|
655
|
+
settings["protocol"] = protocol
|
|
656
|
+
|
|
657
|
+
return values
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class Outbound(BaseModelConfig):
|
|
661
|
+
sendThrough: str = Field(
|
|
662
|
+
"0.0.0.0", description="The IP address used to send data."
|
|
663
|
+
)
|
|
664
|
+
protocol: InboundProtocol = Field(
|
|
665
|
+
InboundProtocol.freedom, description="The connection protocol name"
|
|
666
|
+
)
|
|
667
|
+
settings: Dict = Field(default_factory=dict)
|
|
668
|
+
tag: str | None = Field(
|
|
669
|
+
None,
|
|
670
|
+
description=(
|
|
671
|
+
"The identifier of this inbound connection, used to locate "
|
|
672
|
+
"this connection in other configurations."
|
|
673
|
+
),
|
|
674
|
+
)
|
|
675
|
+
streamSettings: StreamSettings | None = Field(
|
|
676
|
+
None,
|
|
677
|
+
description=(
|
|
678
|
+
"The underlying transport method is how the current Xray node "
|
|
679
|
+
"interfaces with other nodes."
|
|
680
|
+
),
|
|
681
|
+
)
|
|
682
|
+
proxySettings: ProxySettings | None = Field(
|
|
683
|
+
None, description="The outbound proxy configuration."
|
|
684
|
+
)
|
|
685
|
+
mux: Mux | None = Field(
|
|
686
|
+
None,
|
|
687
|
+
description=(
|
|
688
|
+
"The Mux feature distributes the data of multiple TCP connections "
|
|
689
|
+
"on a single TCP connection."
|
|
690
|
+
),
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class XRayConfig(BaseModelConfig):
|
|
695
|
+
"""
|
|
696
|
+
Содержит полную конфигурацию XRay сервера
|
|
697
|
+
"""
|
|
698
|
+
stats: Optional[Stats] = Field(
|
|
699
|
+
None, description="Включает сбор статистики")
|
|
700
|
+
api: Optional[Api] = Field(
|
|
701
|
+
None, description="API interface configuration provides a set of APIs")
|
|
702
|
+
policy: Optional[Policy] = Field(
|
|
703
|
+
None,
|
|
704
|
+
description="Local policy can be used to set different policy settings"
|
|
705
|
+
)
|
|
706
|
+
log: Log = Field(default_factory=Log)
|
|
707
|
+
routing: Routing = Field(
|
|
708
|
+
default_factory=Routing,
|
|
709
|
+
description=(
|
|
710
|
+
"Configures routing. Specify rules to route connections "
|
|
711
|
+
"through different outbounds."
|
|
712
|
+
),
|
|
713
|
+
)
|
|
714
|
+
inbounds: List[Inbound] = Field(
|
|
715
|
+
default_factory=list,
|
|
716
|
+
description=(
|
|
717
|
+
"Inbound connections are used to receive incoming data "
|
|
718
|
+
"and the available protocols are listed in"
|
|
719
|
+
),
|
|
720
|
+
)
|
|
721
|
+
outbounds: List[Outbound] = Field(
|
|
722
|
+
default_factory=list,
|
|
723
|
+
description=("An array of outbound connection configurations."),
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
@classmethod
|
|
727
|
+
def from_config_file(cls, content: TextIO) -> XRayConfig:
|
|
728
|
+
"""
|
|
729
|
+
Создает объект конфигурации из файла. Config format is json.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
content: Содержимое файла конфигурации
|
|
733
|
+
"""
|
|
734
|
+
return cls.model_validate_json(content)
|
|
735
|
+
|
|
736
|
+
def to_config_file(self, indent=None) -> str:
|
|
737
|
+
"""
|
|
738
|
+
Возвращает отрендеренный конфигурационный файл.
|
|
739
|
+
Json dump
|
|
740
|
+
"""
|
|
741
|
+
return self.model_dump_json(indent=indent, exclude_none=True)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Describe diffent protocols
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import TextIO, Optional, List, Dict, Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from .base import BaseModelConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def gen_uuid() -> str:
|
|
17
|
+
return str(uuid.uuid4())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InboundProtocol(str, Enum):
|
|
21
|
+
"""
|
|
22
|
+
Inbound connections are used to receive incoming data
|
|
23
|
+
and the available protocols are listed in
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
tcp = "tcp"
|
|
27
|
+
kcp = "kcp"
|
|
28
|
+
ws = "ws"
|
|
29
|
+
h2 = "h2"
|
|
30
|
+
quic = "quic"
|
|
31
|
+
grpc = "grpc"
|
|
32
|
+
dokodemo = "dokodemo"
|
|
33
|
+
dokodemo_door = "dokodemo-door"
|
|
34
|
+
freedom = "freedom"
|
|
35
|
+
blackhole = "blackhole"
|
|
36
|
+
socks = "socks"
|
|
37
|
+
http = "http"
|
|
38
|
+
mtproto = "mtproto"
|
|
39
|
+
tuic = "tuic"
|
|
40
|
+
vless = "vless"
|
|
41
|
+
vmess = "vmess"
|
|
42
|
+
trojan = "trojan"
|
|
43
|
+
shadowsocks = "shadowsocks"
|
|
44
|
+
reality = "reality"
|
|
45
|
+
wireguard = "wireguard"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ProtocolSettingsBase(BaseModelConfig, BaseModel):
|
|
49
|
+
"""
|
|
50
|
+
Base class for protocol settings
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
protocol: InboundProtocol = Field(..., exclude=True, description="Protocol")
|
|
54
|
+
|
|
55
|
+
def client_add(self, client_id: Optional[str] = None, **kwargs):
|
|
56
|
+
"""
|
|
57
|
+
Add a client to the protocol
|
|
58
|
+
"""
|
|
59
|
+
raise NotImplementedError()
|
|
60
|
+
|
|
61
|
+
def client_remove(self, client_id: str):
|
|
62
|
+
"""
|
|
63
|
+
Remove a client from the protocol
|
|
64
|
+
"""
|
|
65
|
+
raise NotImplementedError()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class VlessSettingFlow(str, Enum):
|
|
69
|
+
xtls_rprx_vision = "xtls-rprx-vision"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ClientVless(BaseModelConfig):
|
|
73
|
+
id: str = Field(
|
|
74
|
+
default_factory=gen_uuid, description="Unique ID of the client"
|
|
75
|
+
)
|
|
76
|
+
level: int = Field(0, description="Level of the client")
|
|
77
|
+
email: str = Field(..., description="Email of the client")
|
|
78
|
+
flow: Optional[VlessSettingFlow] = Field(
|
|
79
|
+
None, description="Flow control mode used to select the XTLS algorithm"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def __eq__(self, other: ClientVless):
|
|
83
|
+
if not isinstance(other, ClientVless):
|
|
84
|
+
return NotImplemented
|
|
85
|
+
return self.id == other.id and self.email == other.email
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ProtocolSettingsVless(ProtocolSettingsBase):
|
|
89
|
+
protocol: InboundProtocol = Field(InboundProtocol.vless, exclude=True)
|
|
90
|
+
|
|
91
|
+
clients: List[ClientVless] = Field(
|
|
92
|
+
default_factory=list, description="List of clients"
|
|
93
|
+
)
|
|
94
|
+
decryption: str = Field("none", description="Decryption method")
|
|
95
|
+
fallbacks: List[Dict] = Field(
|
|
96
|
+
default_factory=list,
|
|
97
|
+
description=(
|
|
98
|
+
"An array that contains a series of powerful fallback "
|
|
99
|
+
"configurations"
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def client_add(self, client_id: Optional[str] = None, **kwargs):
|
|
104
|
+
"""
|
|
105
|
+
Add a client to the protocol
|
|
106
|
+
"""
|
|
107
|
+
if client_id is not None:
|
|
108
|
+
kwargs["id"] = client_id
|
|
109
|
+
client = ClientVless(**kwargs)
|
|
110
|
+
if client not in self.clients:
|
|
111
|
+
self.clients.append(client)
|
|
112
|
+
return client
|
|
113
|
+
|
|
114
|
+
def client_remove(self, client_id: str):
|
|
115
|
+
"""
|
|
116
|
+
Remove a client from the protocol
|
|
117
|
+
"""
|
|
118
|
+
self.clients = [
|
|
119
|
+
client for client in self.clients if client.id != client_id
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ProtocolSettingsDokodemoDoor(ProtocolSettingsBase):
|
|
124
|
+
protocol: InboundProtocol = Field(
|
|
125
|
+
InboundProtocol.dokodemo_door, exclude=True)
|
|
126
|
+
|
|
127
|
+
address: str = Field(..., description="Address to bind to")
|