devconfig-gen-singbox 2.0.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.
- devconfig_gen/__init__.py +67 -0
- devconfig_gen/cli.py +163 -0
- devconfig_gen/engine.py +217 -0
- devconfig_gen/formats.py +708 -0
- devconfig_gen/models.py +135 -0
- devconfig_gen/providers/__init__.py +8 -0
- devconfig_gen/providers/custom.py +92 -0
- devconfig_gen/providers/env_provider.py +184 -0
- devconfig_gen/providers/json_provider.py +84 -0
- devconfig_gen/providers/singbox/__init__.py +9 -0
- devconfig_gen/providers/singbox/data/rules.json +97 -0
- devconfig_gen/providers/singbox/links.py +24 -0
- devconfig_gen/providers/singbox/models.py +76 -0
- devconfig_gen/providers/singbox/plugins/__init__.py +33 -0
- devconfig_gen/providers/singbox/plugins/anytls.py +129 -0
- devconfig_gen/providers/singbox/plugins/base.py +117 -0
- devconfig_gen/providers/singbox/plugins/hysteria2.py +110 -0
- devconfig_gen/providers/singbox/plugins/tuic.py +90 -0
- devconfig_gen/providers/singbox/provider.py +362 -0
- devconfig_gen/providers/singbox/route.py +369 -0
- devconfig_gen/providers/singbox/schema.py +362 -0
- devconfig_gen/py.typed +0 -0
- devconfig_gen/registry.py +39 -0
- devconfig_gen/validation.py +148 -0
- devconfig_gen_singbox-2.0.0.dist-info/METADATA +343 -0
- devconfig_gen_singbox-2.0.0.dist-info/RECORD +30 -0
- devconfig_gen_singbox-2.0.0.dist-info/WHEEL +5 -0
- devconfig_gen_singbox-2.0.0.dist-info/entry_points.txt +3 -0
- devconfig_gen_singbox-2.0.0.dist-info/licenses/LICENSE +201 -0
- devconfig_gen_singbox-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Internal data structures for the sing-box provider.
|
|
2
|
+
|
|
3
|
+
These types are deliberately small and immutable. They carry the *normalized*
|
|
4
|
+
input (already validated by :mod:`schema`) so that variant plugins only deal
|
|
5
|
+
with their own domain fields and never with raw, untrusted mappings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ProtocolConfig:
|
|
16
|
+
"""One entry of ``network.protocols`` after normalization.
|
|
17
|
+
|
|
18
|
+
``raw`` is the whole protocol mapping (including ``auth`` / ``tls`` /
|
|
19
|
+
variant sections). ``path`` is the dotted location used for diagnostics,
|
|
20
|
+
for example ``network.protocols.0``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
type: str
|
|
24
|
+
enabled: bool
|
|
25
|
+
port: Optional[int]
|
|
26
|
+
raw: Mapping[str, Any] = field(default_factory=dict)
|
|
27
|
+
path: str = "network.protocols.0"
|
|
28
|
+
|
|
29
|
+
def section(self, name: str) -> Mapping[str, Any]:
|
|
30
|
+
"""Return a nested mapping section (``auth`` / ``tls`` / ...)."""
|
|
31
|
+
|
|
32
|
+
value = self.raw.get(name)
|
|
33
|
+
return value if isinstance(value, Mapping) else {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class NetworkConfig:
|
|
38
|
+
"""Normalized ``network`` section."""
|
|
39
|
+
|
|
40
|
+
domain_root: str = ""
|
|
41
|
+
subdomain_prefixes: Mapping[str, str] = field(default_factory=dict)
|
|
42
|
+
tunnel_mode: str = "proxy"
|
|
43
|
+
protocols: Sequence[ProtocolConfig] = ()
|
|
44
|
+
routing: Mapping[str, Any] = field(default_factory=dict)
|
|
45
|
+
dns: Mapping[str, Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ClientConfig:
|
|
50
|
+
"""Normalized ``client`` section."""
|
|
51
|
+
|
|
52
|
+
server_ip: Optional[str] = None
|
|
53
|
+
fingerprint: str = "chrome"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class BuildContext:
|
|
58
|
+
"""Everything a plugin needs to build its pieces.
|
|
59
|
+
|
|
60
|
+
Plugins receive this instead of reaching into the raw context, which keeps
|
|
61
|
+
them free of side effects and of knowledge about the surrounding schema.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
network: NetworkConfig
|
|
65
|
+
client: ClientConfig
|
|
66
|
+
hosts: Mapping[str, str]
|
|
67
|
+
options: Mapping[str, Any] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
def host(self, key: str) -> str:
|
|
70
|
+
"""Return the FQDN for a plugin's ``host_key``."""
|
|
71
|
+
|
|
72
|
+
return self.hosts.get(key, "")
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def fingerprint(self) -> str:
|
|
76
|
+
return self.client.fingerprint or "chrome"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Variant plugin registry.
|
|
2
|
+
|
|
3
|
+
Adding a protocol means adding one module here plus one line in
|
|
4
|
+
``_PLUGIN_CLASSES``; nothing in ``provider.py`` / ``schema.py`` / ``route.py``
|
|
5
|
+
changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .anytls import AnyTlsPlugin
|
|
13
|
+
from .hysteria2 import Hysteria2Plugin
|
|
14
|
+
from .tuic import TuicPlugin
|
|
15
|
+
|
|
16
|
+
_PLUGIN_CLASSES = (AnyTlsPlugin, TuicPlugin, Hysteria2Plugin)
|
|
17
|
+
|
|
18
|
+
PLUGINS = {cls.protocol_type: cls() for cls in _PLUGIN_CLASSES}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_plugin(protocol_type: str) -> Optional[object]:
|
|
22
|
+
"""Return the plugin for ``protocol_type`` or ``None`` when unknown."""
|
|
23
|
+
|
|
24
|
+
return PLUGINS.get(str(protocol_type).strip().lower())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def protocol_types() -> tuple:
|
|
28
|
+
"""Return the registered protocol type names in registry order."""
|
|
29
|
+
|
|
30
|
+
return tuple(PLUGINS)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["PLUGINS", "get_plugin", "protocol_types"]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""AnyTLS (REALITY) variant plugin.
|
|
2
|
+
|
|
3
|
+
All AnyTLS/REALITY field names live here. Upstream sing-box renames are
|
|
4
|
+
absorbed by editing this module only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
|
|
11
|
+
from ..models import BuildContext, ProtocolConfig
|
|
12
|
+
from .base import (
|
|
13
|
+
domain_resolver,
|
|
14
|
+
optional_integer,
|
|
15
|
+
optional_string,
|
|
16
|
+
require_string,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_PORT = 23244
|
|
20
|
+
DEFAULT_DECOY_SERVER = "www.cloudflare.com"
|
|
21
|
+
DEFAULT_DECOY_PORT = 443
|
|
22
|
+
|
|
23
|
+
PADDING_SCHEME = [
|
|
24
|
+
"stop=8",
|
|
25
|
+
"0=30-30",
|
|
26
|
+
"1=100-400",
|
|
27
|
+
"2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
|
|
28
|
+
"3=9-9,500-1000",
|
|
29
|
+
"4=500-1000",
|
|
30
|
+
"5=500-1000",
|
|
31
|
+
"6=500-1000",
|
|
32
|
+
"7=500-1000",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AnyTlsPlugin:
|
|
37
|
+
protocol_type = "anytls"
|
|
38
|
+
host_key = "reality"
|
|
39
|
+
inbound_tag = "anytls-in"
|
|
40
|
+
outbound_tag = "anytls-out"
|
|
41
|
+
default_port = DEFAULT_PORT
|
|
42
|
+
sniff_inbound = True
|
|
43
|
+
|
|
44
|
+
def validate(self, cfg: ProtocolConfig, errors: list) -> None:
|
|
45
|
+
require_string(cfg, "auth.password", errors)
|
|
46
|
+
require_string(cfg, "reality.private_key", errors)
|
|
47
|
+
require_string(cfg, "reality.public_key", errors)
|
|
48
|
+
require_string(cfg, "reality.short_id", errors)
|
|
49
|
+
optional_string(cfg, "reality.decoy_server", errors, default=DEFAULT_DECOY_SERVER)
|
|
50
|
+
optional_integer(
|
|
51
|
+
cfg, "reality.decoy_port", errors, default=DEFAULT_DECOY_PORT, minimum=1, maximum=65535
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _port(self, cfg: ProtocolConfig) -> int:
|
|
55
|
+
return cfg.port if cfg.port is not None else DEFAULT_PORT
|
|
56
|
+
|
|
57
|
+
def _decoy(self, cfg: ProtocolConfig) -> tuple:
|
|
58
|
+
server = optional_string(cfg, "reality.decoy_server", [], default=DEFAULT_DECOY_SERVER)
|
|
59
|
+
port = optional_integer(
|
|
60
|
+
cfg, "reality.decoy_port", [], default=DEFAULT_DECOY_PORT, minimum=1, maximum=65535
|
|
61
|
+
)
|
|
62
|
+
return server, port
|
|
63
|
+
|
|
64
|
+
def build_server_inbound(self, cfg: ProtocolConfig, ctx: BuildContext) -> dict:
|
|
65
|
+
password = require_string(cfg, "auth.password", [], default="")
|
|
66
|
+
private_key = require_string(cfg, "reality.private_key", [], default="")
|
|
67
|
+
short_id = require_string(cfg, "reality.short_id", [], default="")
|
|
68
|
+
decoy_server, decoy_port = self._decoy(cfg)
|
|
69
|
+
return {
|
|
70
|
+
"type": "anytls",
|
|
71
|
+
"tag": self.inbound_tag,
|
|
72
|
+
"listen": "::",
|
|
73
|
+
"listen_port": self._port(cfg),
|
|
74
|
+
"users": [{"name": "user", "password": password}],
|
|
75
|
+
"padding_scheme": list(PADDING_SCHEME),
|
|
76
|
+
"tls": {
|
|
77
|
+
"enabled": True,
|
|
78
|
+
"server_name": decoy_server,
|
|
79
|
+
"reality": {
|
|
80
|
+
"enabled": True,
|
|
81
|
+
"handshake": {
|
|
82
|
+
"server": decoy_server,
|
|
83
|
+
"server_port": decoy_port,
|
|
84
|
+
},
|
|
85
|
+
"private_key": private_key,
|
|
86
|
+
"short_id": short_id,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def build_client_outbound(self, cfg: ProtocolConfig, ctx: BuildContext) -> dict:
|
|
92
|
+
password = require_string(cfg, "auth.password", [], default="")
|
|
93
|
+
public_key = require_string(cfg, "reality.public_key", [], default="")
|
|
94
|
+
short_id = require_string(cfg, "reality.short_id", [], default="")
|
|
95
|
+
decoy_server, _ = self._decoy(cfg)
|
|
96
|
+
return {
|
|
97
|
+
"type": "anytls",
|
|
98
|
+
"tag": self.outbound_tag,
|
|
99
|
+
"server": ctx.host(self.host_key),
|
|
100
|
+
"domain_resolver": domain_resolver(),
|
|
101
|
+
"server_port": self._port(cfg),
|
|
102
|
+
"tls": {
|
|
103
|
+
"enabled": True,
|
|
104
|
+
"server_name": decoy_server,
|
|
105
|
+
"utls": {"enabled": True, "fingerprint": ctx.fingerprint},
|
|
106
|
+
"reality": {
|
|
107
|
+
"enabled": True,
|
|
108
|
+
"public_key": public_key,
|
|
109
|
+
"short_id": short_id,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
"password": password,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def build_share_link(self, cfg: ProtocolConfig, ctx: BuildContext) -> str:
|
|
116
|
+
password = require_string(cfg, "auth.password", [], default="")
|
|
117
|
+
public_key = require_string(cfg, "reality.public_key", [], default="")
|
|
118
|
+
short_id = require_string(cfg, "reality.short_id", [], default="")
|
|
119
|
+
decoy_server, _ = self._decoy(cfg)
|
|
120
|
+
params = urlencode(
|
|
121
|
+
{
|
|
122
|
+
"security": "reality",
|
|
123
|
+
"sni": decoy_server,
|
|
124
|
+
"fp": ctx.fingerprint,
|
|
125
|
+
"pbk": public_key,
|
|
126
|
+
"sid": short_id,
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
return f"anytls://{password}@{ctx.host(self.host_key)}:{self._port(cfg)}?{params}#AnyTLS"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Plugin contract and shared helpers for sing-box protocol variants.
|
|
2
|
+
|
|
3
|
+
A variant plugin owns *all* domain detail for one protocol: the exact inbound
|
|
4
|
+
and outbound field names, the share-link format, and the variant-specific
|
|
5
|
+
validation rules. The rest of the provider (``provider.py`` / ``schema.py`` /
|
|
6
|
+
``route.py``) never mentions a variant name, so an upstream sing-box change is
|
|
7
|
+
absorbed by editing a single file here plus the registry.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Mapping, Optional, Protocol
|
|
13
|
+
|
|
14
|
+
from ....validation import expect_integer, expect_string
|
|
15
|
+
from ..models import BuildContext, ProtocolConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProtocolPlugin(Protocol):
|
|
19
|
+
"""Structural contract every variant plugin implements."""
|
|
20
|
+
|
|
21
|
+
protocol_type: str
|
|
22
|
+
host_key: str
|
|
23
|
+
inbound_tag: str
|
|
24
|
+
outbound_tag: str
|
|
25
|
+
default_port: int
|
|
26
|
+
|
|
27
|
+
def validate(self, cfg: ProtocolConfig, errors: list) -> None:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def build_server_inbound(self, cfg: ProtocolConfig, ctx: BuildContext) -> dict:
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def build_client_outbound(self, cfg: ProtocolConfig, ctx: BuildContext) -> dict:
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def build_share_link(self, cfg: ProtocolConfig, ctx: BuildContext) -> str:
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def dig(source: Mapping[str, Any], path: str) -> Any:
|
|
41
|
+
"""Read a dotted path from a mapping, returning ``None`` when absent."""
|
|
42
|
+
|
|
43
|
+
current: Any = source
|
|
44
|
+
for part in path.split("."):
|
|
45
|
+
if not isinstance(current, Mapping):
|
|
46
|
+
return None
|
|
47
|
+
current = current.get(part)
|
|
48
|
+
return current
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def field_path(cfg: ProtocolConfig, relative: str) -> str:
|
|
52
|
+
return f"{cfg.path}.{relative}" if relative else cfg.path
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def require_string(
|
|
56
|
+
cfg: ProtocolConfig,
|
|
57
|
+
relative: str,
|
|
58
|
+
errors: list,
|
|
59
|
+
*,
|
|
60
|
+
default: Optional[str] = None,
|
|
61
|
+
) -> Optional[str]:
|
|
62
|
+
"""Validate a required string field, falling back to ``default``."""
|
|
63
|
+
|
|
64
|
+
value = dig(cfg.raw, relative)
|
|
65
|
+
if value is None and default is not None:
|
|
66
|
+
return default
|
|
67
|
+
result = expect_string(value, field_path(cfg, relative), errors)
|
|
68
|
+
return result if result is not None else default
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def optional_string(
|
|
72
|
+
cfg: ProtocolConfig,
|
|
73
|
+
relative: str,
|
|
74
|
+
errors: list,
|
|
75
|
+
*,
|
|
76
|
+
default: Optional[str] = None,
|
|
77
|
+
) -> Optional[str]:
|
|
78
|
+
"""Validate a string field only when it is present."""
|
|
79
|
+
|
|
80
|
+
value = dig(cfg.raw, relative)
|
|
81
|
+
if value is None:
|
|
82
|
+
return default
|
|
83
|
+
result = expect_string(value, field_path(cfg, relative), errors)
|
|
84
|
+
return result if result is not None else default
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def optional_integer(
|
|
88
|
+
cfg: ProtocolConfig,
|
|
89
|
+
relative: str,
|
|
90
|
+
errors: list,
|
|
91
|
+
*,
|
|
92
|
+
default: Optional[int] = None,
|
|
93
|
+
minimum: Optional[int] = None,
|
|
94
|
+
maximum: Optional[int] = None,
|
|
95
|
+
) -> Optional[int]:
|
|
96
|
+
"""Validate an integer field only when it is present."""
|
|
97
|
+
|
|
98
|
+
value = dig(cfg.raw, relative)
|
|
99
|
+
if value is None:
|
|
100
|
+
return default
|
|
101
|
+
result = expect_integer(
|
|
102
|
+
value,
|
|
103
|
+
field_path(cfg, relative),
|
|
104
|
+
errors,
|
|
105
|
+
minimum=minimum,
|
|
106
|
+
maximum=maximum,
|
|
107
|
+
)
|
|
108
|
+
return result if result is not None else default
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
DIRECT_DNS_TAG = "dns-direct"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def domain_resolver(server_tag: str = DIRECT_DNS_TAG) -> dict:
|
|
115
|
+
"""Domain resolver shared by every client outbound."""
|
|
116
|
+
|
|
117
|
+
return {"server": server_tag, "strategy": "prefer_ipv4"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Hysteria2 variant plugin.
|
|
2
|
+
|
|
3
|
+
All Hysteria2 field names live here. Upstream sing-box renames are absorbed by
|
|
4
|
+
editing this module only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
|
|
11
|
+
from ..models import BuildContext, ProtocolConfig
|
|
12
|
+
from .base import domain_resolver, optional_integer, optional_string, require_string
|
|
13
|
+
|
|
14
|
+
DEFAULT_PORT = 7443
|
|
15
|
+
DEFAULT_MASQUERADE = "https://www.cloudflare.com"
|
|
16
|
+
DEFAULT_SERVER_UP_MBPS = 500
|
|
17
|
+
DEFAULT_SERVER_DOWN_MBPS = 500
|
|
18
|
+
DEFAULT_CLIENT_UP_MBPS = 50
|
|
19
|
+
DEFAULT_CLIENT_DOWN_MBPS = 200
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Hysteria2Plugin:
|
|
23
|
+
protocol_type = "hysteria2"
|
|
24
|
+
host_key = "hy2"
|
|
25
|
+
inbound_tag = "hy2-in"
|
|
26
|
+
outbound_tag = "hy2-out"
|
|
27
|
+
default_port = DEFAULT_PORT
|
|
28
|
+
sniff_inbound = False
|
|
29
|
+
|
|
30
|
+
def validate(self, cfg: ProtocolConfig, errors: list) -> None:
|
|
31
|
+
require_string(cfg, "auth.password", errors)
|
|
32
|
+
require_string(cfg, "auth.obfs_password", errors)
|
|
33
|
+
require_string(cfg, "tls.cert_path", errors)
|
|
34
|
+
require_string(cfg, "tls.key_path", errors)
|
|
35
|
+
optional_integer(cfg, "bandwidth.up_mbps", errors, minimum=1)
|
|
36
|
+
optional_integer(cfg, "bandwidth.down_mbps", errors, minimum=1)
|
|
37
|
+
optional_string(cfg, "masquerade", errors, default=DEFAULT_MASQUERADE)
|
|
38
|
+
|
|
39
|
+
def _port(self, cfg: ProtocolConfig) -> int:
|
|
40
|
+
return cfg.port if cfg.port is not None else DEFAULT_PORT
|
|
41
|
+
|
|
42
|
+
def _obfs(self, cfg: ProtocolConfig) -> dict:
|
|
43
|
+
return {
|
|
44
|
+
"type": "salamander",
|
|
45
|
+
"password": require_string(cfg, "auth.obfs_password", [], default=""),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def build_server_inbound(self, cfg: ProtocolConfig, ctx: BuildContext) -> dict:
|
|
49
|
+
up = optional_integer(cfg, "bandwidth.up_mbps", [], default=DEFAULT_SERVER_UP_MBPS, minimum=1)
|
|
50
|
+
down = optional_integer(
|
|
51
|
+
cfg, "bandwidth.down_mbps", [], default=DEFAULT_SERVER_DOWN_MBPS, minimum=1
|
|
52
|
+
)
|
|
53
|
+
cert_path = require_string(cfg, "tls.cert_path", [], default="")
|
|
54
|
+
key_path = require_string(cfg, "tls.key_path", [], default="")
|
|
55
|
+
masquerade = optional_string(cfg, "masquerade", [], default=DEFAULT_MASQUERADE)
|
|
56
|
+
return {
|
|
57
|
+
"type": "hysteria2",
|
|
58
|
+
"tag": self.inbound_tag,
|
|
59
|
+
"listen": "::",
|
|
60
|
+
"listen_port": self._port(cfg),
|
|
61
|
+
"users": [{"password": require_string(cfg, "auth.password", [], default="")}],
|
|
62
|
+
"ignore_client_bandwidth": False,
|
|
63
|
+
"up_mbps": up,
|
|
64
|
+
"down_mbps": down,
|
|
65
|
+
"obfs": self._obfs(cfg),
|
|
66
|
+
"masquerade": masquerade,
|
|
67
|
+
"tls": {
|
|
68
|
+
"enabled": True,
|
|
69
|
+
"server_name": ctx.host(self.host_key),
|
|
70
|
+
"alpn": ["h3"],
|
|
71
|
+
"certificate_path": cert_path,
|
|
72
|
+
"key_path": key_path,
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def build_client_outbound(self, cfg: ProtocolConfig, ctx: BuildContext) -> dict:
|
|
77
|
+
up = optional_integer(cfg, "bandwidth.up_mbps", [], default=DEFAULT_CLIENT_UP_MBPS, minimum=1)
|
|
78
|
+
down = optional_integer(
|
|
79
|
+
cfg, "bandwidth.down_mbps", [], default=DEFAULT_CLIENT_DOWN_MBPS, minimum=1
|
|
80
|
+
)
|
|
81
|
+
return {
|
|
82
|
+
"type": "hysteria2",
|
|
83
|
+
"tag": self.outbound_tag,
|
|
84
|
+
"server": ctx.host(self.host_key),
|
|
85
|
+
"domain_resolver": domain_resolver(),
|
|
86
|
+
"server_port": self._port(cfg),
|
|
87
|
+
"up_mbps": up,
|
|
88
|
+
"down_mbps": down,
|
|
89
|
+
"obfs": self._obfs(cfg),
|
|
90
|
+
"password": require_string(cfg, "auth.password", [], default=""),
|
|
91
|
+
"tls": {
|
|
92
|
+
"enabled": True,
|
|
93
|
+
"server_name": ctx.host(self.host_key),
|
|
94
|
+
"alpn": ["h3"],
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def build_share_link(self, cfg: ProtocolConfig, ctx: BuildContext) -> str:
|
|
99
|
+
host = ctx.host(self.host_key)
|
|
100
|
+
params = urlencode(
|
|
101
|
+
{
|
|
102
|
+
"obfs": "salamander",
|
|
103
|
+
"obfs-password": require_string(cfg, "auth.obfs_password", [], default=""),
|
|
104
|
+
"sni": host,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return (
|
|
108
|
+
f"hy2://{require_string(cfg, 'auth.password', [], default='')}"
|
|
109
|
+
f"@{host}:{self._port(cfg)}?{params}#Hysteria2"
|
|
110
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""TUIC variant plugin.
|
|
2
|
+
|
|
3
|
+
All TUIC field names live here. Upstream sing-box renames are absorbed by
|
|
4
|
+
editing this module only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
|
|
11
|
+
from ..models import BuildContext, ProtocolConfig
|
|
12
|
+
from .base import domain_resolver, require_string
|
|
13
|
+
|
|
14
|
+
DEFAULT_PORT = 9443
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TuicPlugin:
|
|
18
|
+
protocol_type = "tuic"
|
|
19
|
+
host_key = "tuic"
|
|
20
|
+
inbound_tag = "tuic-in"
|
|
21
|
+
outbound_tag = "tuic-out"
|
|
22
|
+
default_port = DEFAULT_PORT
|
|
23
|
+
sniff_inbound = False
|
|
24
|
+
|
|
25
|
+
def validate(self, cfg: ProtocolConfig, errors: list) -> None:
|
|
26
|
+
require_string(cfg, "auth.uuid", errors)
|
|
27
|
+
require_string(cfg, "auth.password", errors)
|
|
28
|
+
require_string(cfg, "tls.cert_path", errors)
|
|
29
|
+
require_string(cfg, "tls.key_path", errors)
|
|
30
|
+
|
|
31
|
+
def _port(self, cfg: ProtocolConfig) -> int:
|
|
32
|
+
return cfg.port if cfg.port is not None else DEFAULT_PORT
|
|
33
|
+
|
|
34
|
+
def build_server_inbound(self, cfg: ProtocolConfig, ctx: BuildContext) -> dict:
|
|
35
|
+
uuid = require_string(cfg, "auth.uuid", [], default="")
|
|
36
|
+
password = require_string(cfg, "auth.password", [], default="")
|
|
37
|
+
cert_path = require_string(cfg, "tls.cert_path", [], default="")
|
|
38
|
+
key_path = require_string(cfg, "tls.key_path", [], default="")
|
|
39
|
+
return {
|
|
40
|
+
"type": "tuic",
|
|
41
|
+
"tag": self.inbound_tag,
|
|
42
|
+
"listen": "::",
|
|
43
|
+
"listen_port": self._port(cfg),
|
|
44
|
+
"users": [{"uuid": uuid, "password": password}],
|
|
45
|
+
"congestion_control": "bbr",
|
|
46
|
+
"zero_rtt_handshake": False,
|
|
47
|
+
"heartbeat": "10s",
|
|
48
|
+
"tls": {
|
|
49
|
+
"enabled": True,
|
|
50
|
+
"server_name": ctx.host(self.host_key),
|
|
51
|
+
"alpn": ["h3"],
|
|
52
|
+
"certificate_path": cert_path,
|
|
53
|
+
"key_path": key_path,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def build_client_outbound(self, cfg: ProtocolConfig, ctx: BuildContext) -> dict:
|
|
58
|
+
uuid = require_string(cfg, "auth.uuid", [], default="")
|
|
59
|
+
password = require_string(cfg, "auth.password", [], default="")
|
|
60
|
+
return {
|
|
61
|
+
"type": "tuic",
|
|
62
|
+
"tag": self.outbound_tag,
|
|
63
|
+
"server": ctx.host(self.host_key),
|
|
64
|
+
"domain_resolver": domain_resolver(),
|
|
65
|
+
"server_port": self._port(cfg),
|
|
66
|
+
"uuid": uuid,
|
|
67
|
+
"password": password,
|
|
68
|
+
"congestion_control": "bbr",
|
|
69
|
+
"udp_relay_mode": "native",
|
|
70
|
+
"zero_rtt_handshake": False,
|
|
71
|
+
"heartbeat": "10s",
|
|
72
|
+
"tls": {
|
|
73
|
+
"enabled": True,
|
|
74
|
+
"server_name": ctx.host(self.host_key),
|
|
75
|
+
"alpn": ["h3"],
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def build_share_link(self, cfg: ProtocolConfig, ctx: BuildContext) -> str:
|
|
80
|
+
uuid = require_string(cfg, "auth.uuid", [], default="")
|
|
81
|
+
password = require_string(cfg, "auth.password", [], default="")
|
|
82
|
+
host = ctx.host(self.host_key)
|
|
83
|
+
params = urlencode(
|
|
84
|
+
{
|
|
85
|
+
"congestion_control": "bbr",
|
|
86
|
+
"udp_relay_mode": "quic",
|
|
87
|
+
"sni": host,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
return f"tuic://{uuid}:{password}@{host}:{self._port(cfg)}?{params}#TUIC"
|