xray-tools 0.1.0__py3-none-any.whl
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/__init__.py +1 -0
- xray_tools/base.py +29 -0
- xray_tools/config.py +741 -0
- xray_tools/protocols.py +127 -0
- xray_tools-0.1.0.dist-info/METADATA +33 -0
- xray_tools-0.1.0.dist-info/RECORD +7 -0
- xray_tools-0.1.0.dist-info/WHEEL +4 -0
xray_tools/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
xray_tools/base.py
ADDED
|
@@ -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)
|
xray_tools/config.py
ADDED
|
@@ -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)
|
xray_tools/protocols.py
ADDED
|
@@ -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")
|
|
@@ -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,7 @@
|
|
|
1
|
+
xray_tools/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
xray_tools/base.py,sha256=LakqC7ids7P9_icZ7_fcB_yRcOSyvXx1t6OTw8u6j1U,787
|
|
3
|
+
xray_tools/config.py,sha256=92EcFYcnGHx5T4PJ9YguTSv6GIqxjxhArpkgZCkCrcw,20177
|
|
4
|
+
xray_tools/protocols.py,sha256=zQYuzyN4no20ez__DPsIfihrDskL0Mg0lAh83iwf5yQ,3332
|
|
5
|
+
xray_tools-0.1.0.dist-info/METADATA,sha256=dQ75OBD5dMvuJkAInGvUZy-xTKU235ZVf-2P0g9Bp0I,803
|
|
6
|
+
xray_tools-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
xray_tools-0.1.0.dist-info/RECORD,,
|