-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(backends): sing-box * refactor(sing-box): rename some env variables * ci(sing-box): add sing-box to the docker image, include the path in compose
- Loading branch information
1 parent
d7f657f
commit 3a43957
Showing
17 changed files
with
990 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
"""Implements accounts for different protocols in sing-box""" | ||
|
||
# pylint: disable=E0611,C0115 | ||
from abc import ABC | ||
from enum import Enum | ||
from typing import Optional | ||
|
||
from pydantic import ( | ||
BaseModel, | ||
field_validator, | ||
ValidationInfo, | ||
ValidationError, | ||
computed_field, | ||
Field, | ||
) | ||
|
||
from marznode.utils.key_gen import generate_password, generate_uuid | ||
|
||
|
||
class SingBoxAccount(BaseModel, ABC): | ||
identifier: str | ||
seed: str | ||
|
||
@field_validator("uuid", "password", check_fields=False) | ||
@classmethod | ||
def generate_creds(cls, v: str, info: ValidationInfo): | ||
if v: | ||
return v | ||
if "seed" in info.data: | ||
seed = info.data["seed"] | ||
if info.field_name == "uuid": | ||
return str(generate_uuid(seed)) | ||
elif info.field_name == "password": | ||
return generate_password(seed) | ||
raise ValidationError("Both password/id and seed are empty") | ||
|
||
def to_dict(self): | ||
return self.model_dump(exclude={"seed", "identifier"}) | ||
|
||
def __repr__(self) -> str: | ||
return f"<{self.__class__.__name__} {self.email}>" | ||
|
||
|
||
class NamedAccount(SingBoxAccount): | ||
@computed_field | ||
@property | ||
def name(self) -> str: | ||
return self.identifier | ||
|
||
|
||
class UserNamedAccount(SingBoxAccount): | ||
@computed_field | ||
@property | ||
def username(self) -> str: | ||
return self.identifier | ||
|
||
|
||
class VMessAccount(NamedAccount, SingBoxAccount): | ||
uuid: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class XTLSFlows(str, Enum): | ||
NONE = "" | ||
VISION = "xtls-rprx-vision" | ||
|
||
|
||
class VLESSAccount(NamedAccount, SingBoxAccount): | ||
uuid: Optional[str] = Field(None, validate_default=True) | ||
flow: XTLSFlows = XTLSFlows.NONE | ||
|
||
|
||
class TrojanAccount(NamedAccount, SingBoxAccount): | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class ShadowsocksAccount(NamedAccount, SingBoxAccount): | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class TUICAccount(NamedAccount, SingBoxAccount): | ||
uuid: Optional[str] = Field(None, validate_default=True) | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class Hysteria2Account(NamedAccount, SingBoxAccount): | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class NaiveAccount(UserNamedAccount, SingBoxAccount): | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class ShadowTLSAccount(NamedAccount, SingBoxAccount): | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class SocksAccount(UserNamedAccount, SingBoxAccount): | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class HTTPAccount(UserNamedAccount, SingBoxAccount): | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
class MixedAccount(UserNamedAccount, SingBoxAccount): | ||
password: Optional[str] = Field(None, validate_default=True) | ||
|
||
|
||
accounts_map = { | ||
"shadowsocks": ShadowsocksAccount, | ||
"trojan": TrojanAccount, | ||
"vmess": VMessAccount, | ||
"vless": VLESSAccount, | ||
"shadowtls": ShadowTLSAccount, | ||
"tuic": TUICAccount, | ||
"hysteria2": Hysteria2Account, | ||
"naive": NaiveAccount, | ||
"socks": SocksAccount, | ||
"mixed": MixedAccount, | ||
"http": HTTPAccount, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import json | ||
|
||
import commentjson | ||
|
||
from marznode.backends.singbox._accounts import accounts_map | ||
from marznode.backends.xray._utils import get_x25519 | ||
from marznode.config import XRAY_EXECUTABLE_PATH | ||
from marznode.models import User, Inbound | ||
from marznode.storage import BaseStorage | ||
|
||
|
||
class SingBoxConfig(dict): | ||
def __init__( | ||
self, | ||
config: str, | ||
api_host: str = "127.0.0.1", | ||
api_port: int = 8080, | ||
): | ||
try: | ||
# considering string as json | ||
config = commentjson.loads(config) | ||
except (json.JSONDecodeError, ValueError): | ||
# considering string as file path | ||
with open(config) as file: | ||
config = commentjson.loads(file.read()) | ||
|
||
self.api_host = api_host | ||
self.api_port = api_port | ||
|
||
super().__init__(config) | ||
|
||
self.inbounds = [] | ||
self.inbounds_by_tag = {} | ||
self._resolve_inbounds() | ||
|
||
self._apply_api() | ||
|
||
def _apply_api(self): | ||
if not self.get("experimental"): | ||
self["experimental"] = {} | ||
self["experimental"]["v2ray_api"] = { | ||
"listen": self.api_host + ":" + str(self.api_port), | ||
"stats": {"enabled": True, "users": []}, | ||
} | ||
|
||
def _resolve_inbounds(self): | ||
for inbound in self.get("inbounds", []): | ||
if inbound.get("type") not in { | ||
"shadowsocks", | ||
"vmess", | ||
"trojan", | ||
"vless", | ||
"hysteria2", | ||
"tuic", | ||
} or not inbound.get("tag"): | ||
continue | ||
|
||
settings = { | ||
"tag": inbound["tag"], | ||
"protocol": inbound["type"], | ||
"port": inbound.get("listen_port"), | ||
"network": "tcp", | ||
"tls": "none", | ||
"sni": [], | ||
"host": [], | ||
"path": None, | ||
"header_type": None, | ||
"flow": None, | ||
} | ||
|
||
if "tls" in inbound and inbound["tls"].get("enabled") == True: | ||
settings["tls"] = "tls" | ||
if sni := inbound["tls"].get("server_name"): | ||
settings["sni"].append(sni) | ||
if inbound["tls"].get("reality", {}).get("enabled"): | ||
settings["tls"] = "reality" | ||
pvk = inbound["tls"]["reality"].get("private_key") | ||
|
||
x25519 = get_x25519(XRAY_EXECUTABLE_PATH, pvk) | ||
settings["pbk"] = x25519["public_key"] | ||
|
||
settings["sid"] = inbound["tls"]["reality"].get("short_id", [""])[0] | ||
|
||
if "transport" in inbound: | ||
settings["network"] = inbound["transport"].get("type") | ||
if settings["network"] == "ws": | ||
settings["path"] = inbound["transport"].get("path") | ||
elif settings["network"] == "http": | ||
settings["path"] = inbound["transport"].get("path") | ||
settings["network"] = "tcp" | ||
settings["header_type"] = "http" | ||
settings["host"] = inbound["transport"].get("host", []) | ||
elif settings["network"] == "grpc": | ||
settings["path"] = inbound["transport"].get("service_name") | ||
elif settings["network"] == "httpupgrade": | ||
settings["path"] = inbound["transport"].get("path") | ||
|
||
self.inbounds.append(settings) | ||
self.inbounds_by_tag[inbound["tag"]] = settings | ||
|
||
def append_user(self, user: User, inbound: Inbound): | ||
identifier = str(user.id) + "." + user.username | ||
account = accounts_map[inbound.protocol](identifier=identifier, seed=user.key) | ||
for i in self.get("inbounds", []): | ||
if i.get("tag") == inbound.tag: | ||
if not i.get("users"): | ||
i["users"] = [] | ||
i["users"].append(account.to_dict()) | ||
if ( | ||
identifier | ||
not in self["experimental"]["v2ray_api"]["stats"]["users"] | ||
): | ||
self["experimental"]["v2ray_api"]["stats"]["users"].append( | ||
identifier | ||
) | ||
break | ||
|
||
def pop_user(self, user: User, inbound: Inbound): | ||
identifier = str(user.id) + "." + user.username | ||
for i in self.get("inbounds", []): | ||
if i.get("tag") != inbound.tag or not i.get("users"): | ||
continue | ||
i["users"] = [ | ||
user | ||
for user in i["users"] | ||
if user.get("name") != identifier and user.get("username") != identifier | ||
] | ||
break | ||
|
||
def register_inbounds(self, storage: BaseStorage): | ||
for inbound in self.list_inbounds(): | ||
storage.register_inbound(inbound) | ||
|
||
def list_inbounds(self) -> list[Inbound]: | ||
return [ | ||
Inbound(tag=i["tag"], protocol=i["protocol"], config=i) | ||
for i in self.inbounds_by_tag.values() | ||
] | ||
|
||
def to_json(self, **json_kwargs): | ||
return json.dumps(self, **json_kwargs) |
Oops, something went wrong.