Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ db.sqlite3-journal
docker-compose*.yml
v2ray-core
venv
xray-core
xray-core
10 changes: 10 additions & 0 deletions app/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,13 @@ Copy `example.env` to `.env` then set the backend api address:
## Contribution

Feel free to contribute. Go on and fork the project. After commiting the changes, make a PR. It means a lot to us.
### Локальная сборка фронта с собственным API-префиксом

```bash
cd app/dashboard
VITE_BASE_API=/api/ npm ci
VITE_BASE_API=/api/ npm run build -- --outDir build --assetsDir statics
cp build/index.html build/404.html
```

Где `VITE_BASE_API` — базовый префикс для всех API‑запросов фронта (в продакшене `/api/`).
4 changes: 2 additions & 2 deletions app/dashboard/build/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
<meta name="msapplication-TileColor" content="#2b5797" />
<meta name="msapplication-config" content="/statics/favicon/browserconfig.xml" />
<meta name="theme-color" content="#3B81F6" />
<script type="module" crossorigin src="/statics/index.8d48a3d3.js"></script>
<link rel="modulepreload" crossorigin href="/statics/vendor.3f3f0bc0.js">
<script type="module" crossorigin src="/statics/index.4535663a.js"></script>
<link rel="modulepreload" crossorigin href="/statics/vendor.e36f197f.js">
<link rel="stylesheet" href="/statics/index.9b97897c.css">
</head>
<body>
Expand Down
4 changes: 2 additions & 2 deletions app/dashboard/build/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
<meta name="msapplication-TileColor" content="#2b5797" />
<meta name="msapplication-config" content="/statics/favicon/browserconfig.xml" />
<meta name="theme-color" content="#3B81F6" />
<script type="module" crossorigin src="/statics/index.8d48a3d3.js"></script>
<link rel="modulepreload" crossorigin href="/statics/vendor.3f3f0bc0.js">
<script type="module" crossorigin src="/statics/index.4535663a.js"></script>
<link rel="modulepreload" crossorigin href="/statics/vendor.e36f197f.js">
<link rel="stylesheet" href="/statics/index.9b97897c.css">
</head>
<body>
Expand Down
14 changes: 14 additions & 0 deletions app/dashboard/build/statics/index.4535663a.js

Large diffs are not rendered by default.

14 changes: 0 additions & 14 deletions app/dashboard/build/statics/index.8d48a3d3.js

This file was deleted.

1 change: 1 addition & 0 deletions app/dashboard/build/statics/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"hostsDialog.addHost": "Add host",
"hostsDialog.advancedOptions": "Advanced options",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.useSniAsHost": "Use sni as host",
"hostsDialog.alpn": "ALPN",
"hostsDialog.apply": "Apply",
"hostsDialog.currentServer": "IP Address of current server",
Expand Down
1 change: 1 addition & 0 deletions app/dashboard/build/statics/locales/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"hostsDialog.addHost": "افزودن هاست",
"hostsDialog.advancedOptions": "تنظیمات پیشرفته",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.useSniAsHost": "استفاده از sni به عنوان هاست",
"hostsDialog.alpn": "ALPN",
"hostsDialog.apply": "اعمال",
"hostsDialog.currentServer": "IP کنونی سرور",
Expand Down
3 changes: 2 additions & 1 deletion app/dashboard/build/statics/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"hostsDialog.addHost": "Добавить хост",
"hostsDialog.advancedOptions": "Дополнительные опции",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.useSniAsHost": "Use sni as host",
"hostsDialog.alpn": "ALPN",
"hostsDialog.apply": "Применить",
"hostsDialog.currentServer": "IP текущего сервера",
Expand Down Expand Up @@ -188,4 +189,4 @@
"usersTable.noUserMatched": "Похоже, нет пользователя, соответствующего вашему запросу",
"usersTable.status": "Статус",
"usersTable.total": "Всего"
}
}
5 changes: 3 additions & 2 deletions app/dashboard/build/statics/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"hostsDialog.addHost": "添加主机",
"hostsDialog.advancedOptions": "高级选项",
"hostsDialog.allowinsecure": "允许不安全连接",
"hostsDialog.useSniAsHost": "Use sni as host",
"hostsDialog.alpn": "ALPN",
"hostsDialog.apply": "保存",
"hostsDialog.currentServer": "当前服务器的 IP 地址",
Expand Down Expand Up @@ -188,5 +189,5 @@
"usersTable.noUserMatched": "没有找到您搜索的用户",
"usersTable.status": "状态",
"usersTable.sortByExpire": "按过期时间排序",
"usersTable.total": "总共",
}
"usersTable.total": "总共"
}
31 changes: 30 additions & 1 deletion app/dashboard/src/components/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ import {
useDashboard,
} from "contexts/DashboardContext";
import { t } from "i18next";
import { FC, forwardRef, PropsWithChildren, useState } from "react";
import {
FC,
forwardRef,
PropsWithChildren,
useEffect,
useMemo,
useState,
} from "react";
import {
ControllerRenderProps,
useFormContext,
Expand Down Expand Up @@ -136,6 +143,7 @@ const RadioCard: FC<
description: string;
toggleAccordion: () => void;
isSelected: boolean;
ss2022Method?: string | null;
}
>
> = ({
Expand All @@ -144,6 +152,7 @@ const RadioCard: FC<
description,
toggleAccordion,
isSelected,
ss2022Method,
...props
}) => {
const form = useFormContext();
Expand Down Expand Up @@ -437,11 +446,13 @@ const RadioCard: FC<
fontSize="xs"
size="sm"
borderRadius="6px"
isDisabled={Boolean(ss2022Method)}
{...form.register("proxies.shadowsocks.method")}
>
{shadowsocksMethods.map((method) => (
<option key={method} value={method}>
{method}
{ss2022Method && method === ss2022Method ? " (inbound)" : ""}
</option>
))}
</Select>
Expand All @@ -467,6 +478,23 @@ export type RadioGroupProps = ControllerRenderProps & {
export const RadioGroup = forwardRef<any, RadioGroupProps>(
({ name, list, onChange, disabled, ...props }, ref) => {
const form = useFormContext();
const ssInbounds =
useDashboard.getState().inbounds.get("shadowsocks") || [];
const ss2022Method = useMemo(() => {
const methods = ssInbounds
.map((i) => i.method)
.filter((m): m is string => Boolean(m));
if (methods.length && methods.every((m) => m.startsWith("2022-"))) {
return methods[0];
}
return null;
}, [ssInbounds]);

useEffect(() => {
if (ss2022Method) {
form.setValue("proxies.shadowsocks.method", ss2022Method);
}
}, [form, ss2022Method]);
const [expandedAccordions, setExpandedAccordions] = useState<number[]>([]);

const toggleAccordion = (i: number) => {
Expand Down Expand Up @@ -526,6 +554,7 @@ export const RadioGroup = forwardRef<any, RadioGroupProps>(
isSelected={
!!(props.value as string[]).find((v) => v === value.title)
}
ss2022Method={ss2022Method}
{...getCheckboxProps({ value: value.title })}
/>
);
Expand Down
3 changes: 3 additions & 0 deletions app/dashboard/src/constants/Proxies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const XTLSFlows = [
];

export const shadowsocksMethods = [
"2022-blake3-aes-256-gcm",
"2022-blake3-aes-128-gcm",
"2022-blake3-chacha20-poly1305",
"aes-128-gcm",
"aes-256-gcm",
"chacha20-ietf-poly1305",
Expand Down
4 changes: 3 additions & 1 deletion app/dashboard/src/contexts/DashboardContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export type InboundType = {
protocol: ProtocolType;
network: string;
tls: string;
port?: number;
port?: number | string | null;
method?: string | null;
server_psk?: string | null;
};
export type Inbounds = Map<ProtocolType, InboundType[]>;

Expand Down
33 changes: 30 additions & 3 deletions app/models/proxy.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import base64
import binascii
import json
import re
from enum import Enum
from typing import Optional, Union
from uuid import UUID, uuid4

from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator

from app.utils.system import random_password
from app.utils.system import generate_ss2022_key, random_password
from xray_api.types.account import (
ShadowsocksAccount,
ShadowsocksMethods,
Expand Down Expand Up @@ -94,6 +96,29 @@ class ShadowsocksSettings(ProxySettings):
def revoke(self):
self.password = random_password()

@field_validator("password", mode="after")
@classmethod
def ensure_ss2022_key(cls, v: str, info: ValidationInfo):
method = info.data.get("method")

if isinstance(method, ShadowsocksMethods):
method_value = method.value
else:
method_value = str(method) if method else ""

if not method_value.startswith("2022-"):
return v

expected_size = 16 if method_value.endswith("aes-128-gcm") else 32
try:
decoded = base64.b64decode(v or "", validate=True)
if len(decoded) == expected_size:
return v
except (binascii.Error, TypeError, ValueError):
pass

return generate_ss2022_key(method_value)


class ProxyHostSecurity(str, Enum):
inbound_default = "inbound_default"
Expand Down Expand Up @@ -204,4 +229,6 @@ class ProxyInbound(BaseModel):
protocol: ProxyTypes
network: str
tls: str
port: Union[int, str]
port: Optional[Union[int, str]] = None
method: Optional[str] = None
server_psk: Optional[str] = None
22 changes: 18 additions & 4 deletions app/subscription/clash.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):

proxy_remark = self._remark_validation(remark)

inbound_method = inbound.get("method") or inbound.get("settings", {}).get("method", "")
inbound_psk = inbound.get("server_psk") or inbound.get("settings", {}).get("password", "")

node = self.make_node(
name=remark,
remark=proxy_remark,
Expand Down Expand Up @@ -288,8 +291,12 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
node['password'] = settings['password']

elif inbound['protocol'] == 'shadowsocks':
node['password'] = settings['password']
node['cipher'] = settings['method']
if inbound_method.startswith("2022-"):
node['password'] = f"{inbound_psk}:{settings['password']}"
node['cipher'] = inbound_method
else:
node['password'] = settings['password']
node['cipher'] = settings['method']

else:
return
Expand Down Expand Up @@ -351,6 +358,9 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):

proxy_remark = self._remark_validation(remark)

inbound_method = inbound.get("method") or inbound.get("settings", {}).get("method", "")
inbound_psk = inbound.get("server_psk") or inbound.get("settings", {}).get("password", "")

node = self.make_node(
name=remark,
remark=proxy_remark,
Expand Down Expand Up @@ -388,8 +398,12 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
node['password'] = settings['password']

elif inbound['protocol'] == 'shadowsocks':
node['password'] = settings['password']
node['cipher'] = settings['method']
if inbound_method.startswith("2022-"):
node['password'] = f"{inbound_psk}:{settings['password']}"
node['cipher'] = inbound_method
else:
node['password'] = settings['password']
node['cipher'] = settings['method']

else:
return
Expand Down
10 changes: 8 additions & 2 deletions app/subscription/singbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):

net = inbound["network"]
path = inbound["path"]
inbound_method = inbound.get("method") or inbound.get("settings", {}).get("method", "")
inbound_psk = inbound.get("server_psk") or inbound.get("settings", {}).get("password", "")

# not supported by sing-box
if net in ("kcp", "splithttp", "xhttp") or (net == "quic" and inbound["header_type"] != "none"):
Expand Down Expand Up @@ -330,7 +332,11 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
outbound['password'] = settings['password']

elif inbound['protocol'] == 'shadowsocks':
outbound['password'] = settings['password']
outbound['method'] = settings['method']
if inbound_method.startswith("2022-"):
outbound['password'] = f"{inbound_psk}:{settings['password']}"
outbound['method'] = inbound_method
else:
outbound['password'] = settings['password']
outbound['method'] = settings['method']

self.add_outbound(outbound)
35 changes: 29 additions & 6 deletions app/subscription/v2ray.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,23 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
)

elif inbound["protocol"] == "shadowsocks":
inbound_method = inbound.get("method") or inbound.get("settings", {}).get("method", "")
inbound_psk = inbound.get("server_psk") or inbound.get("settings", {}).get("password", "")

if inbound_method.startswith("2022-"):
user_key = settings.get("password", "")
password = f"{inbound_psk}:{user_key}"
method = inbound_method
else:
password = settings["password"]
method = settings["method"]

link = self.shadowsocks(
remark=remark,
address=address,
port=inbound["port"],
password=settings["password"],
method=settings["method"],
password=password,
method=method,
)
else:
return
Expand Down Expand Up @@ -998,6 +1009,8 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
noise = inbound['noise_setting']
path = inbound["path"]
multi_mode = inbound.get("multiMode", False)
inbound_method = inbound.get("method") or inbound.get("settings", {}).get("method", "")
inbound_psk = inbound.get("server_psk") or inbound.get("settings", {}).get("password", "")

if net in ["grpc", "gun"]:
if multi_mode:
Expand Down Expand Up @@ -1032,10 +1045,20 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
password=settings['password'])

elif inbound['protocol'] == 'shadowsocks':
outbound["settings"] = self.shadowsocks_config(address=address,
port=port,
password=settings['password'],
method=settings['method'])
if inbound_method.startswith("2022-"):
outbound["settings"] = self.shadowsocks_config(
address=address,
port=port,
password=f"{inbound_psk}:{settings['password']}",
method=inbound_method,
)
else:
outbound["settings"] = self.shadowsocks_config(
address=address,
port=port,
password=settings['password'],
method=settings['method'],
)

outbounds = [outbound]
dialer_proxy = ''
Expand Down
8 changes: 8 additions & 0 deletions app/utils/system.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import ipaddress
import math
import secrets
Expand Down Expand Up @@ -97,6 +98,13 @@ def random_password() -> str:
return secrets.token_urlsafe(16)


def generate_ss2022_key(method: str = "2022-blake3-aes-256-gcm") -> str:
"""Generate a base64-encoded PSK for Shadowsocks 2022."""

size = 16 if method.endswith("aes-128-gcm") else 32
return base64.b64encode(secrets.token_bytes(size)).decode()


def check_port(port: int) -> bool:
s = socket.socket()
try:
Expand Down
Loading