feat: support vmess, trojan, and hysteria2

This commit is contained in:
zhom committed 2026-10-05 20:13:47 +04:00
1 parent c098f82806
commit 4ee54cf9f4
52 files changed
+4325 -1191

No files matched your search

+111 -75
View File
@@ -25,7 +25,11 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { translateBackendError } from "@/lib/backend-errors";
import { isFirstHopEncrypted, pickParsedProxy } from "@/lib/proxy-string";
import {
isFirstHopEncrypted,
isXrayProxyType,
pickParsedProxy,
} from "@/lib/proxy-string";
import { canonicalProxyType } from "@/lib/proxy-type";
import type { ProxyParseResult, StoredProxy } from "@/types";
import { RippleButton } from "./ui/ripple";
@@ -71,8 +75,19 @@ const DEFAULT_FORM: ProxyFormData = {
* note directly below the Select then denied, both readable in one glance. A
* heading that says the cipher decides is the true one, and it stays true for
* the `none` cipher that the encrypted heading never covered either.
*
* The Xray types get one for the same reason: their share link picks TLS,
* REALITY or nothing at all, and until the backend accepts a link the note
* below says the link decides. Hysteria2 always runs over QUIC, but under the
* encrypted heading it met that same note, so it sits here with the others.
*/
const ALWAYS_ENCRYPTED_FIRST_HOP_TYPES = ["httpstls", "vless"] as const;
const ALWAYS_ENCRYPTED_FIRST_HOP_TYPES = ["httpstls"] as const;
const LINK_DEPENDENT_FIRST_HOP_TYPES = [
"vless",
"vmess",
"trojan",
"hysteria2",
] as const;
const CIPHER_DEPENDENT_FIRST_HOP_TYPES = ["ss"] as const;
const PLAINTEXT_FIRST_HOP_TYPES = [
"http",
@@ -86,6 +101,10 @@ const TYPE_GROUPS = [
labelKey: "proxies.form.firstHopGroupEncrypted",
types: ALWAYS_ENCRYPTED_FIRST_HOP_TYPES,
},
{
labelKey: "proxies.form.firstHopGroupLink",
types: LINK_DEPENDENT_FIRST_HOP_TYPES,
},
{
labelKey: "proxies.form.firstHopGroupCipher",
types: CIPHER_DEPENDENT_FIRST_HOP_TYPES,
@@ -96,33 +115,18 @@ const TYPE_GROUPS = [
},
] as const;
interface VlessEndpoint {
/** What `validate_vless_uri` reads out of a share link Donut can use. */
interface ShareLinkSummary {
proxy_type: string;
host: string;
port: number;
}
function parseVlessEndpoint(uri: string): VlessEndpoint | null {
try {
const parsed = new URL(uri.trim());
const port = Number.parseInt(parsed.port, 10);
if (
parsed.protocol !== "vless:" ||
!parsed.hostname ||
!Number.isInteger(port) ||
port < 1 ||
port > 65535
) {
return null;
}
const host =
parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]")
? parsed.hostname.slice(1, -1)
: parsed.hostname;
return { host, port };
} catch {
return null;
}
/** The backend's answer for one exact link. */
interface ShareLinkValidation {
link: string;
summary: ShareLinkSummary | null;
error: string | null;
}
export function ProxyFormDialog({
@@ -133,12 +137,21 @@ export function ProxyFormDialog({
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
const [form, setForm] = useState<ProxyFormData>(DEFAULT_FORM);
// The local parse only covers scheme/host/port. Whether Donut can actually
// use the server — REALITY, XTLS Vision, plain TCP — is decided by the Rust
// parser, so ask it (below) and show the specific reason while the user is
// still editing rather than after they save. Declared here because
// `handleSubmit` guards on it.
const [vlessUnsupported, setVlessUnsupported] = useState<string | null>(null);
// A share link is read by the Rust parser alone: VMess hides its server in
// base64, and whether Donut can use a transport or a security layer is the
// parser's call. Ask it (below) and show the specific reason while the user
// is still editing rather than after they save. The answer is kept with the
// link it is for, so an answer for an older link never counts. Declared here
// because `handleSubmit` guards on it.
const [validation, setValidation] = useState<ShareLinkValidation | null>(
null,
);
const trimmedLink = form.vless_uri.trim();
// A link left in the form after switching to, say, HTTP is not this proxy.
const isCurrentValidation =
isXrayProxyType(form.proxy_type) && validation?.link === trimmedLink;
const linkSummary = isCurrentValidation ? validation.summary : null;
const linkError = isCurrentValidation ? validation.error : null;
const resetForm = useCallback(() => {
setForm(DEFAULT_FORM);
@@ -172,25 +185,24 @@ export function ProxyFormDialog({
}
const canonicalType = canonicalProxyType(form.proxy_type);
const isVless = canonicalType === "vless";
const vlessEndpoint = isVless ? parseVlessEndpoint(form.vless_uri) : null;
const isXray = isXrayProxyType(canonicalType);
if (isVless && !form.vless_uri.trim()) {
if (isXray && !form.vless_uri.trim()) {
toast.error(t("proxies.form.vlessUriRequired"));
return;
}
if (isVless && !vlessEndpoint) {
if (isXray && linkError) {
toast.error(linkError);
return;
}
if (isXray && !linkSummary) {
toast.error(t("proxies.form.vlessUriInvalid"));
return;
}
if (isVless && vlessUnsupported) {
toast.error(vlessUnsupported);
return;
}
if (!isVless && (!form.host.trim() || !form.port)) {
if (!isXray && (!form.host.trim() || !form.port)) {
toast.error(t("proxies.form.hostPortRequired"));
return;
}
@@ -208,12 +220,12 @@ export function ProxyFormDialog({
const payload = {
name: form.name.trim(),
proxySettings: {
proxy_type: form.proxy_type,
host: vlessEndpoint?.host ?? form.host.trim(),
port: vlessEndpoint?.port ?? form.port,
username: isVless ? undefined : form.username.trim() || undefined,
password: isVless ? undefined : form.password.trim() || undefined,
vless_uri: isVless ? form.vless_uri.trim() : undefined,
proxy_type: linkSummary?.proxy_type ?? form.proxy_type,
host: linkSummary?.host ?? form.host.trim(),
port: linkSummary?.port ?? form.port,
username: isXray ? undefined : form.username.trim() || undefined,
password: isXray ? undefined : form.password.trim() || undefined,
vless_uri: isXray ? form.vless_uri.trim() : undefined,
},
};
@@ -239,7 +251,7 @@ export function ProxyFormDialog({
} finally {
setIsSubmitting(false);
}
}, [editingProxy, form, onClose, t, vlessUnsupported]);
}, [editingProxy, form, onClose, t, linkError, linkSummary]);
const handleClose = useCallback(() => {
if (!isSubmitting) {
@@ -293,21 +305,29 @@ export function ProxyFormDialog({
// through the REST API arrives as `shadowsocks`, and every branch that asked
// `=== "ss"` skipped it. Derive the type once and compare against that.
const canonicalType = canonicalProxyType(form.proxy_type);
const isVless = canonicalType === "vless";
const isXray = isXrayProxyType(canonicalType);
const isShadowsocks = canonicalType === "ss";
const vlessEndpoint = isVless ? parseVlessEndpoint(form.vless_uri) : null;
// The cipher decides for Shadowsocks, and this form keeps it in `username`.
// Asked with `canonicalType`, not the raw stored spelling, so the answer
// cannot disagree with the field labels two lines below: a REST-stored
// `"ss "` was trimmed for the labels and not for this, and the form called a
// proxy with a real cipher unencrypted while calling its field "Cipher".
const firstHopEncrypted = isFirstHopEncrypted(canonicalType, form.username);
// A share link only counts once the backend has accepted it, so a link it
// refuses is never called encrypted.
const firstHopEncrypted = isFirstHopEncrypted(
canonicalType,
form.username,
linkSummary ? form.vless_uri : "",
);
// Shadowsocks is the one type whose hop is only as encrypted as its cipher,
// and the cipher is empty until the user fills it in. Saying "not encrypted"
// there states as settled something nobody has chosen yet, right under a
// heading about encryption. Nothing downstream reads this: `firstHopEncrypted`
// stays false, so every guard still fails closed on an empty cipher.
const cipherUndecided = isShadowsocks && form.username.trim().length === 0;
// The same holds for a share link that is not there yet, or that Donut
// cannot use: nothing has decided the hop, so the note says the link will.
const linkUndecided = isXray && !linkSummary;
// What a filled field on a plaintext hop actually exposes, which is not the
// same thing for every protocol: for the credentialed types it is the
// username and password (see the payload built in handleSubmit), and for
@@ -315,7 +335,7 @@ export function ProxyFormDialog({
// the cipher, it is the destination and the payload. Same condition, two
// different truths, so the panel below picks its sentence from the type.
const showPlaintextExposure =
!isVless && !firstHopEncrypted && form.username.trim().length > 0;
!isXray && !firstHopEncrypted && form.username.trim().length > 0;
// Radix matches an item by its value, so the item standing for this proxy
// carries the proxy's own spelling. Without it a stored `shadowsocks` left
// the trigger on its placeholder, and picking the visible Shadowsocks entry
@@ -323,36 +343,50 @@ export function ProxyFormDialog({
const typeItemValue = (type: string) =>
type === canonicalType ? form.proxy_type : type;
const trimmedVlessUri = form.vless_uri.trim();
// Nothing is set here synchronously. Clearing the answer on every keystroke
// made the textarea lose about one typed character in 50; the answer is
// matched to its link above instead.
useEffect(() => {
if (!isVless || trimmedVlessUri.length === 0) {
setVlessUnsupported(null);
if (!isXray || trimmedLink.length === 0) {
return;
}
let cancelled = false;
const timer = window.setTimeout(() => {
void invoke("validate_vless_uri", { uri: trimmedVlessUri })
.then(() => {
if (!cancelled) setVlessUnsupported(null);
void invoke<ShareLinkSummary>("validate_vless_uri", { uri: trimmedLink })
.then((summary) => {
if (cancelled) return;
setValidation({ link: trimmedLink, summary, error: null });
})
.catch((error: unknown) => {
if (!cancelled) setVlessUnsupported(translateBackendError(t, error));
if (cancelled) return;
setValidation({
link: trimmedLink,
summary: null,
error: translateBackendError(t, error),
});
});
}, 300);
return () => {
cancelled = true;
window.clearTimeout(timer);
};
}, [isVless, trimmedVlessUri, t]);
}, [isXray, trimmedLink, t]);
const hasInvalidVlessUri =
isVless &&
trimmedVlessUri.length > 0 &&
(!vlessEndpoint || vlessUnsupported !== null);
// The link names its protocol, and the backend stores the proxy under it, so
// the type shown follows an accepted link: a VMess link pasted under VLESS
// shows as VMess, and picking another Xray type keeps the link's own.
const linkProxyType = linkSummary?.proxy_type;
useEffect(() => {
if (linkProxyType && linkProxyType !== canonicalType) {
setForm((previous) => ({ ...previous, proxy_type: linkProxyType }));
}
}, [linkProxyType, canonicalType]);
const hasInvalidLink = isXray && linkError !== null;
const isFormValid =
form.name.trim() &&
(isVless
? vlessEndpoint !== null && vlessUnsupported === null
(isXray
? linkSummary !== null
: form.host.trim() &&
form.port > 0 &&
form.port <= 65535 &&
@@ -426,7 +460,9 @@ export function ProxyFormDialog({
? t("proxies.form.firstHopEncryptedNote")
: cipherUndecided
? t("proxies.form.firstHopCipherNote")
: t("proxies.form.firstHopPlaintextNote")}
: linkUndecided
? t("proxies.form.firstHopLinkNote")
: t("proxies.form.firstHopPlaintextNote")}
</p>
{canonicalType === "httpstls" && (
<p
@@ -438,7 +474,7 @@ export function ProxyFormDialog({
)}
</div>
{isVless ? (
{isXray ? (
<div className="grid gap-2">
<Label htmlFor="proxy-vless-uri">
{t("proxies.form.vlessUri")}
@@ -450,9 +486,11 @@ export function ProxyFormDialog({
setForm({ ...form, vless_uri: e.target.value });
}}
onPaste={handleProxyPaste}
placeholder={t("proxies.form.vlessUriPlaceholder")}
placeholder={t("proxies.form.vlessUriPlaceholder", {
scheme: canonicalType,
})}
disabled={isSubmitting}
aria-invalid={hasInvalidVlessUri}
aria-invalid={hasInvalidLink}
aria-describedby="proxy-vless-uri-help"
autoCapitalize="none"
autoComplete="off"
@@ -462,15 +500,13 @@ export function ProxyFormDialog({
<p
id="proxy-vless-uri-help"
className={
hasInvalidVlessUri
hasInvalidLink
? "text-xs text-destructive-text"
: "text-xs text-muted-foreground"
}
role={hasInvalidVlessUri ? "alert" : undefined}
role={hasInvalidLink ? "alert" : undefined}
>
{hasInvalidVlessUri
? (vlessUnsupported ?? t("proxies.form.vlessUriInvalid"))
: t("proxies.form.vlessUriHint")}
{linkError ?? t("proxies.form.vlessUriHint")}
</p>
</div>
) : (
+3 -1
View File
@@ -286,7 +286,8 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
// as a warning, and `isFirstHopEncrypted`, the fail-closed guard, is
// untouched.
const notEncrypted = parsedProxies.filter(
(proxy) => !isFirstHopEncrypted(proxy.proxy_type, proxy.username),
(proxy) =>
!isFirstHopEncrypted(proxy.proxy_type, proxy.username, proxy.vless_uri),
);
const cipherUndecidedCount = notEncrypted.filter(
isFirstHopCipherUndecided,
@@ -469,6 +470,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
isFirstHopEncrypted(
proxy.proxy_type,
proxy.username,
proxy.vless_uri,
)
? "text-primary-text"
: "text-warning-text"
+5 -1
View File
@@ -532,7 +532,11 @@ export function ProxyManagementDialog({
// Shadowsocks keeps its cipher in `username`, and the cipher is what
// decides this hop.
const cipher = row.original.proxy_settings.username;
const encrypted = isFirstHopEncrypted(proxyType, cipher);
const encrypted = isFirstHopEncrypted(
proxyType,
cipher,
row.original.proxy_settings.vless_uri,
);
// A stored Shadowsocks proxy can carry no cipher at all -
// `parse_txt_proxies` reads `ss://host:8388` as username None, and
// `import_proxies_json` stores proxy_type and username verbatim, and
+25 -19
View File
@@ -576,15 +576,17 @@
"ssCipherRequired": "Für Shadowsocks sind Verschlüsselungsmethode und Passwort erforderlich",
"selectType": "Proxytyp auswählen",
"saveFailed": "Der Proxy konnte nicht gespeichert werden: {{error}}",
"vlessUri": "VLESS-URI",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "Erfordert XTLS Vision und REALITY.",
"vlessUriRequired": "Geben Sie eine VLESS-URI ein.",
"vlessUriInvalid": "Geben Sie eine gültige VLESS-URI mit Host und Port ein.",
"vlessUri": "Freigabelink",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "Fügen Sie den vollständigen Link Ihres Anbieters ein. Er enthält den Server, die Zugangsdaten und den Transport.",
"vlessUriRequired": "Geben Sie einen Freigabelink ein.",
"vlessUriInvalid": "Geben Sie einen gültigen Freigabelink mit Host und Port ein.",
"firstHopGroupEncrypted": "Erster Hop verschlüsselt",
"firstHopGroupLink": "Erster Hop wird vom Link festgelegt",
"firstHopGroupPlaintext": "Erster Hop unverschlüsselt",
"firstHopGroupCipher": "Erster Hop hängt von der Verschlüsselungsmethode ab",
"firstHopCipherNote": "Ob die Verbindung von diesem Rechner zum Proxy verschlüsselt ist, hängt von der Verschlüsselungsmethode unten ab. Ohne Methode oder mit einer Null-Methode wie none bleibt der Datenverkehr für den Anbieter und für alle im Netzwerk dazwischen lesbar.",
"firstHopLinkNote": "Ob die Verbindung von diesem Rechner zum Proxy verschlüsselt ist, legt der Freigabelink fest: TLS, REALITY oder die eigene Verschlüsselung des Protokolls. Ohne diese bleibt der Datenverkehr für den Anbieter und für alle im Netzwerk dazwischen lesbar.",
"firstHopEncryptedNote": "Die Verbindung von diesem Rechner zum Proxy ist verschlüsselt. Der Anbieter sieht trotzdem jede Seite, die Sie aufrufen, und alles auf Seiten ohne HTTPS.",
"firstHopPlaintextNote": "Die Verbindung von diesem Rechner zum Proxy ist nicht verschlüsselt. Der Anbieter und alle im Netzwerk zwischen diesem Rechner und dem Proxy können den Datenverkehr mitlesen.",
"credentialsInClearHeading": "Diese Zugangsdaten werden lesbar über das Netzwerk übertragen",
@@ -600,7 +602,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP über TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "Normal",
@@ -2218,9 +2223,9 @@
"mcpAgentUnknown": "Dieser MCP-Client wird nicht unterstützt.",
"mcpAgentInstallFailed": "Donut Browser konnte dem MCP-Client nicht hinzugefügt werden: {{detail}}",
"mcpAgentRemoveFailed": "Donut Browser konnte nicht aus dem MCP-Client entfernt werden: {{detail}}",
"vlessConfigInvalid": "Die VLESS-URI ist ungültig.",
"vlessConfigInvalid": "Der Freigabelink ist ungültig.",
"xrayUnavailable": "Xray-core ist auf diesem System nicht verfügbar.",
"xrayUnsupportedOs": "VLESS erfordert macOS 12 oder neuer.",
"xrayUnsupportedOs": "VLESS-, VMess-, Trojan- und Hysteria2-Proxys erfordern macOS 12 oder neuer.",
"xrayStartFailed": "Xray-core konnte nicht gestartet werden.",
"cloudNotSignedIn": "Melden Sie sich bei Ihrem Donut Browser-Konto an, um dies zu nutzen.",
"cloudUnreachable": "Die Server von Donut Browser sind nicht erreichbar. Prüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
@@ -2265,17 +2270,18 @@
"vpnWorkerStartFailed": "Die VPN-Verbindung konnte nicht gestartet werden: {{detail}}",
"exitProbeFailed": "Der Exit-Node des Proxys war nicht erreichbar, um seinen Standort zu prüfen.",
"vlessUnsupported": {
"security": "Dieser VLESS-Server verwendet kein REALITY. Donut unterstützt VLESS nur mit REALITY.",
"flow": "Dieser VLESS-Server verwendet keinen XTLS-Vision-Flow, den Donut benötigt.",
"transport": "Donut unterstützt VLESS nur über reines TCP. Dieser Server verwendet einen anderen Transport (z. B. WebSocket oder gRPC).",
"encryption": "Dieser VLESS-Server verwendet eine Verschlüsselungseinstellung, die Donut nicht unterstützt.",
"headerType": "Dieser VLESS-Server verwendet eine Header-Verschleierung, die Donut nicht unterstützt.",
"fingerprint": "Diese VLESS-URI fordert einen TLS-Fingerprint an, den Donut nicht unterstützt.",
"sni": "Der VLESS-URI fehlt die für REALITY benötigte SNI (sni).",
"publicKey": "Der VLESS-URI fehlt der öffentliche REALITY-Schlüssel (pbk).",
"scheme": "Das ist kein VLESS-Link. Er muss mit vless:// beginnen.",
"parameter": "Die VLESS-URI enthält eine Option, die Donut nicht unterstützt.",
"malformed": "Die VLESS-URI ist ungültig."
"security": "Dieser Link verwendet eine Sicherheitseinstellung, die Donut nicht unterstützt. Donut unterstützt none, TLS und REALITY, und REALITY nur über RAW (TCP), gRPC oder XHTTP.",
"flow": "Dieser Link verwendet einen Flow, den Donut nicht unterstützt. XTLS Vision funktioniert nur mit VLESS über RAW (TCP) mit TLS oder REALITY.",
"transport": "Dieser Link verwendet einen Transport oder Transportmodus, den Donut nicht unterstützt. Donut unterstützt RAW (TCP), WebSocket, gRPC, HTTPUpgrade und XHTTP, aber nicht mKCP, HTTP/2 oder QUIC.",
"encryption": "Dieser Link verwendet eine Verschlüsselungseinstellung, die Donut nicht unterstützt.",
"headerType": "Dieser Link verwendet eine Header-Verschleierung, die Donut nicht unterstützt.",
"fingerprint": "Dieser Link fordert einen TLS-Fingerprint an, den Donut nicht unterstützt.",
"sni": "Dem Link fehlt ein gültiger Servername (sni).",
"publicKey": "Dem Link fehlt der öffentliche REALITY-Schlüssel (pbk).",
"obfs": "Dieser Hysteria2-Link verwendet eine Verschleierung, die Donut nicht unterstützt. Donut unterstützt nur Salamander.",
"scheme": "Das ist kein unterstützter Freigabelink. Er muss mit vless://, vmess://, trojan://, hysteria2:// oder hy2:// beginnen.",
"parameter": "Der Link enthält eine Option, die Donut nicht unterstützt.",
"malformed": "Der Freigabelink ist ungültig."
},
"camoufoxRemoved": "Camoufox wird nicht mehr unterstützt. Erstellen Sie dieses Profil mit Wayfern neu.",
"noE2ePasswordSet": "Es ist kein Passwort für die Ende-zu-Ende-Verschlüsselung festgelegt. Legen Sie eines fest, bevor Sie verschlüsselte Daten synchronisieren.",
+25 -19
View File
@@ -576,15 +576,17 @@
"ssCipherRequired": "Cipher and password are required for Shadowsocks",
"selectType": "Select proxy type",
"saveFailed": "Failed to save proxy: {{error}}",
"vlessUri": "VLESS URI",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "Requires XTLS Vision and REALITY.",
"vlessUriRequired": "Enter a VLESS URI.",
"vlessUriInvalid": "Enter a valid VLESS URI with a host and port.",
"vlessUri": "Share link",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "Paste the full link from your provider. It holds the server, the credentials and the transport.",
"vlessUriRequired": "Enter a share link.",
"vlessUriInvalid": "Enter a valid share link with a host and port.",
"firstHopGroupEncrypted": "First hop encrypted",
"firstHopGroupLink": "First hop set by the link",
"firstHopGroupPlaintext": "First hop in the clear",
"firstHopGroupCipher": "First hop depends on the cipher",
"firstHopCipherNote": "Whether the connection from this machine to the proxy is encrypted is decided by the cipher below. An empty cipher, or a null one such as none, leaves the traffic readable to the provider and to anyone on the network in between.",
"firstHopLinkNote": "Whether the connection from this machine to the proxy is encrypted is decided by the share link: TLS, REALITY or the protocol's own encryption. A link without them leaves the traffic readable to the provider and to anyone on the network in between.",
"firstHopEncryptedNote": "The connection from this machine to the proxy is encrypted. The provider still sees every site you reach, and everything on pages that are not HTTPS.",
"firstHopPlaintextNote": "The connection from this machine to the proxy is not encrypted. The provider, and anyone on the network between this machine and it, can read the traffic.",
"credentialsInClearHeading": "These credentials cross the network readable",
@@ -600,7 +602,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "Regular",
@@ -2218,9 +2223,9 @@
"mcpAgentUnknown": "This MCP client is not supported.",
"mcpAgentInstallFailed": "Could not add Donut Browser to the MCP client: {{detail}}",
"mcpAgentRemoveFailed": "Could not remove Donut Browser from the MCP client: {{detail}}",
"vlessConfigInvalid": "The VLESS URI is invalid.",
"vlessConfigInvalid": "The share link is invalid.",
"xrayUnavailable": "Xray-core is unavailable on this system.",
"xrayUnsupportedOs": "VLESS requires macOS 12 or newer.",
"xrayUnsupportedOs": "VLESS, VMess, Trojan and Hysteria2 proxies require macOS 12 or newer.",
"xrayStartFailed": "Xray-core could not start.",
"cloudNotSignedIn": "Sign in to your Donut Browser account to use this.",
"cloudUnreachable": "Could not reach Donut Browser's servers. Check your connection and try again.",
@@ -2265,17 +2270,18 @@
"vpnWorkerStartFailed": "Couldn't start the VPN connection: {{detail}}",
"exitProbeFailed": "Couldn't reach the proxy exit node to check its location.",
"vlessUnsupported": {
"security": "This VLESS server does not use REALITY. Donut supports VLESS with REALITY only.",
"flow": "This VLESS server does not use XTLS Vision flow, which Donut requires.",
"transport": "Donut supports VLESS over plain TCP only — this server uses a different transport (such as WebSocket or gRPC).",
"encryption": "This VLESS server uses an encryption setting Donut does not support.",
"headerType": "This VLESS server uses a header obfuscation Donut does not support.",
"fingerprint": "This VLESS URI requests a TLS fingerprint Donut does not support.",
"sni": "The VLESS URI is missing the SNI (sni) needed for REALITY.",
"publicKey": "The VLESS URI is missing the REALITY public key (pbk).",
"scheme": "That is not a VLESS link. It must start with vless://.",
"parameter": "The VLESS URI contains an option Donut does not support.",
"malformed": "The VLESS URI is invalid."
"security": "This link uses a security setting Donut does not support. Donut supports none, TLS and REALITY, and REALITY only over RAW (TCP), gRPC or XHTTP.",
"flow": "This link uses a flow Donut does not support. XTLS Vision works only with VLESS over RAW (TCP) with TLS or REALITY.",
"transport": "This link uses a transport, or a transport mode, that Donut does not support. Donut supports RAW (TCP), WebSocket, gRPC, HTTPUpgrade and XHTTP, but not mKCP, HTTP/2 or QUIC.",
"encryption": "This link uses an encryption setting Donut does not support.",
"headerType": "This link uses a header obfuscation Donut does not support.",
"fingerprint": "This link asks for a TLS fingerprint Donut does not support.",
"sni": "The link is missing a valid server name (sni).",
"publicKey": "The link is missing the REALITY public key (pbk).",
"obfs": "This Hysteria2 link uses an obfuscation Donut does not support. Donut supports Salamander only.",
"scheme": "That is not a supported share link. It must start with vless://, vmess://, trojan://, hysteria2:// or hy2://.",
"parameter": "The link contains an option Donut does not support.",
"malformed": "The share link is invalid."
},
"camoufoxRemoved": "Camoufox is no longer supported. Recreate this profile with Wayfern.",
"noE2ePasswordSet": "No end-to-end encryption password is set. Set one before syncing encrypted data.",
+25 -19
View File
@@ -578,15 +578,17 @@
"ssCipherRequired": "Para Shadowsocks se requieren cifrado y contraseña",
"selectType": "Selecciona el tipo de proxy",
"saveFailed": "Error al guardar el proxy: {{error}}",
"vlessUri": "URI de VLESS",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "Requiere XTLS Vision y REALITY.",
"vlessUriRequired": "Introduce un URI de VLESS.",
"vlessUriInvalid": "Introduce un URI de VLESS válido con host y puerto.",
"vlessUri": "Enlace para compartir",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "Pega el enlace completo de tu proveedor. Contiene el servidor, las credenciales y el transporte.",
"vlessUriRequired": "Introduce un enlace para compartir.",
"vlessUriInvalid": "Introduce un enlace para compartir válido con host y puerto.",
"firstHopGroupEncrypted": "Primer salto cifrado",
"firstHopGroupLink": "El enlace define el primer salto",
"firstHopGroupPlaintext": "Primer salto sin cifrar",
"firstHopGroupCipher": "El primer salto depende del cifrado",
"firstHopCipherNote": "Que la conexión de este equipo al proxy esté cifrada lo decide el cifrado de abajo. Un cifrado vacío, o uno nulo como none, deja el tráfico legible para el proveedor y para cualquiera en la red intermedia.",
"firstHopLinkNote": "Que la conexión de este equipo al proxy esté cifrada lo decide el enlace para compartir: TLS, REALITY o el cifrado propio del protocolo. Un enlace sin ellos deja el tráfico legible para el proveedor y para cualquiera en la red intermedia.",
"firstHopEncryptedNote": "La conexión de este equipo al proxy está cifrada. El proveedor sigue viendo todos los sitios que visitas y todo el contenido de las páginas que no usan HTTPS.",
"firstHopPlaintextNote": "La conexión de este equipo al proxy no está cifrada. El proveedor, y cualquiera en la red entre este equipo y él, puede leer el tráfico.",
"credentialsInClearHeading": "Estas credenciales viajan legibles por la red",
@@ -602,7 +604,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "Regular",
@@ -2228,9 +2233,9 @@
"mcpAgentUnknown": "Este cliente MCP no es compatible.",
"mcpAgentInstallFailed": "No se pudo añadir Donut Browser al cliente MCP: {{detail}}",
"mcpAgentRemoveFailed": "No se pudo eliminar Donut Browser del cliente MCP: {{detail}}",
"vlessConfigInvalid": "El URI de VLESS no es válido.",
"vlessConfigInvalid": "El enlace para compartir no es válido.",
"xrayUnavailable": "Xray-core no está disponible en este sistema.",
"xrayUnsupportedOs": "VLESS requiere macOS 12 o posterior.",
"xrayUnsupportedOs": "Los proxies VLESS, VMess, Trojan y Hysteria2 requieren macOS 12 o posterior.",
"xrayStartFailed": "No se pudo iniciar Xray-core.",
"cloudNotSignedIn": "Inicia sesión en tu cuenta de Donut Browser para usar esto.",
"cloudUnreachable": "No se pudo contactar con los servidores de Donut Browser. Comprueba tu conexión e inténtalo de nuevo.",
@@ -2275,17 +2280,18 @@
"vpnWorkerStartFailed": "No se pudo iniciar la conexión VPN: {{detail}}",
"exitProbeFailed": "No se pudo contactar con el nodo de salida del proxy para comprobar su ubicación.",
"vlessUnsupported": {
"security": "Este servidor VLESS no usa REALITY. Donut solo admite VLESS con REALITY.",
"flow": "Este servidor VLESS no usa el flujo XTLS Vision, que Donut requiere.",
"transport": "Donut solo admite VLESS sobre TCP simple: este servidor usa otro transporte (como WebSocket o gRPC).",
"encryption": "Este servidor VLESS usa un cifrado que Donut no admite.",
"headerType": "Este servidor VLESS usa una ofuscación de cabecera que Donut no admite.",
"fingerprint": "Esta URI VLESS solicita una huella TLS que Donut no admite.",
"sni": "A la URI VLESS le falta el SNI (sni) necesario para REALITY.",
"publicKey": "A la URI VLESS le falta la clave pública de REALITY (pbk).",
"scheme": "Eso no es un enlace VLESS. Debe empezar por vless://.",
"parameter": "La URI VLESS contiene una opción que Donut no admite.",
"malformed": "La URI VLESS no es válida."
"security": "Este enlace usa un ajuste de seguridad que Donut no admite. Donut admite none, TLS y REALITY, y REALITY solo sobre RAW (TCP), gRPC o XHTTP.",
"flow": "Este enlace usa un flow que Donut no admite. XTLS Vision solo funciona con VLESS sobre RAW (TCP) con TLS o REALITY.",
"transport": "Este enlace usa un transporte, o un modo de transporte, que Donut no admite. Donut admite RAW (TCP), WebSocket, gRPC, HTTPUpgrade y XHTTP, pero no mKCP, HTTP/2 ni QUIC.",
"encryption": "Este enlace usa un ajuste de cifrado que Donut no admite.",
"headerType": "Este enlace usa una ofuscación de cabecera que Donut no admite.",
"fingerprint": "Este enlace pide una huella TLS que Donut no admite.",
"sni": "Al enlace le falta un nombre de servidor (sni) válido.",
"publicKey": "Al enlace le falta la clave pública de REALITY (pbk).",
"obfs": "Este enlace de Hysteria2 usa una ofuscación que Donut no admite. Donut solo admite Salamander.",
"scheme": "Eso no es un enlace para compartir admitido. Debe empezar por vless://, vmess://, trojan://, hysteria2:// o hy2://.",
"parameter": "El enlace contiene una opción que Donut no admite.",
"malformed": "El enlace para compartir no es válido."
},
"camoufoxRemoved": "Camoufox ya no es compatible. Vuelve a crear este perfil con Wayfern.",
"noE2ePasswordSet": "No hay contraseña de cifrado de extremo a extremo. Establece una antes de sincronizar datos cifrados.",
+25 -19
View File
@@ -578,15 +578,17 @@
"ssCipherRequired": "Le chiffrement et le mot de passe sont requis pour Shadowsocks",
"selectType": "Sélectionnez le type de proxy",
"saveFailed": "Échec de la sauvegarde du proxy : {{error}}",
"vlessUri": "URI VLESS",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "Nécessite XTLS Vision et REALITY.",
"vlessUriRequired": "Saisissez une URI VLESS.",
"vlessUriInvalid": "Saisissez une URI VLESS valide avec un hôte et un port.",
"vlessUri": "Lien de partage",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "Collez le lien complet de votre fournisseur. Il contient le serveur, les identifiants et le transport.",
"vlessUriRequired": "Saisissez un lien de partage.",
"vlessUriInvalid": "Saisissez un lien de partage valide avec un hôte et un port.",
"firstHopGroupEncrypted": "Premier saut chiffré",
"firstHopGroupLink": "Le premier saut dépend du lien",
"firstHopGroupPlaintext": "Premier saut en clair",
"firstHopGroupCipher": "Le premier saut dépend du chiffrement",
"firstHopCipherNote": "C'est le chiffrement ci-dessous qui décide si la connexion de cette machine au proxy est chiffrée. Un chiffrement vide, ou un chiffrement nul comme none, laisse le trafic lisible par le fournisseur et par toute personne sur le réseau intermédiaire.",
"firstHopLinkNote": "C'est le lien de partage qui décide si la connexion de cette machine au proxy est chiffrée : TLS, REALITY ou le chiffrement propre au protocole. Un lien sans aucun d'eux laisse le trafic lisible par le fournisseur et par toute personne sur le réseau intermédiaire.",
"firstHopEncryptedNote": "La connexion de cette machine au proxy est chiffrée. Le fournisseur voit toujours chaque site que vous atteignez, ainsi que tout le contenu des pages qui ne sont pas en HTTPS.",
"firstHopPlaintextNote": "La connexion de cette machine au proxy n'est pas chiffrée. Le fournisseur, et toute personne sur le réseau entre cette machine et lui, peut lire le trafic.",
"credentialsInClearHeading": "Ces identifiants traversent le réseau en clair",
@@ -602,7 +604,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "Standard",
@@ -2228,9 +2233,9 @@
"mcpAgentUnknown": "Ce client MCP n’est pas pris en charge.",
"mcpAgentInstallFailed": "Impossible d’ajouter Donut Browser au client MCP : {{detail}}",
"mcpAgentRemoveFailed": "Impossible de retirer Donut Browser du client MCP : {{detail}}",
"vlessConfigInvalid": "L’URI VLESS n’est pas valide.",
"vlessConfigInvalid": "Le lien de partage n'est pas valide.",
"xrayUnavailable": "Xray-core n’est pas disponible sur ce système.",
"xrayUnsupportedOs": "VLESS nécessite macOS 12 ou une version ultérieure.",
"xrayUnsupportedOs": "Les proxys VLESS, VMess, Trojan et Hysteria2 nécessitent macOS 12 ou une version ultérieure.",
"xrayStartFailed": "Impossible de démarrer Xray-core.",
"cloudNotSignedIn": "Connectez-vous à votre compte Donut Browser pour utiliser cette fonction.",
"cloudUnreachable": "Impossible de joindre les serveurs de Donut Browser. Vérifiez votre connexion et réessayez.",
@@ -2275,17 +2280,18 @@
"vpnWorkerStartFailed": "Impossible de démarrer la connexion VPN : {{detail}}",
"exitProbeFailed": "Impossible de joindre le nœud de sortie du proxy pour vérifier sa localisation.",
"vlessUnsupported": {
"security": "Ce serveur VLESS n'utilise pas REALITY. Donut ne prend en charge que VLESS avec REALITY.",
"flow": "Ce serveur VLESS n'utilise pas le flux XTLS Vision, requis par Donut.",
"transport": "Donut ne prend en charge que VLESS sur TCP simple — ce serveur utilise un autre transport (WebSocket ou gRPC, par exemple).",
"encryption": "Ce serveur VLESS utilise un chiffrement non pris en charge par Donut.",
"headerType": "Ce serveur VLESS utilise une obfuscation d'en-tête non prise en charge par Donut.",
"fingerprint": "Cette URI VLESS demande une empreinte TLS non prise en charge par Donut.",
"sni": "Il manque le SNI (sni) nécessaire à REALITY dans l'URI VLESS.",
"publicKey": "Il manque la clé publique REALITY (pbk) dans l'URI VLESS.",
"scheme": "Ce n'est pas un lien VLESS. Il doit commencer par vless://.",
"parameter": "L'URI VLESS contient une option non prise en charge par Donut.",
"malformed": "L'URI VLESS n'est pas valide."
"security": "Ce lien utilise un réglage de sécurité que Donut ne prend pas en charge. Donut prend en charge none, TLS et REALITY, et REALITY uniquement sur RAW (TCP), gRPC ou XHTTP.",
"flow": "Ce lien utilise un flow que Donut ne prend pas en charge. XTLS Vision fonctionne uniquement avec VLESS sur RAW (TCP) avec TLS ou REALITY.",
"transport": "Ce lien utilise un transport, ou un mode de transport, que Donut ne prend pas en charge. Donut prend en charge RAW (TCP), WebSocket, gRPC, HTTPUpgrade et XHTTP, mais pas mKCP, HTTP/2 ni QUIC.",
"encryption": "Ce lien utilise un réglage de chiffrement que Donut ne prend pas en charge.",
"headerType": "Ce lien utilise une obfuscation d'en-tête que Donut ne prend pas en charge.",
"fingerprint": "Ce lien demande une empreinte TLS que Donut ne prend pas en charge.",
"sni": "Il manque au lien un nom de serveur (sni) valide.",
"publicKey": "Il manque au lien la clé publique REALITY (pbk).",
"obfs": "Ce lien Hysteria2 utilise une obfuscation que Donut ne prend pas en charge. Donut prend en charge Salamander uniquement.",
"scheme": "Ce n'est pas un lien de partage pris en charge. Il doit commencer par vless://, vmess://, trojan://, hysteria2:// ou hy2://.",
"parameter": "Le lien contient une option que Donut ne prend pas en charge.",
"malformed": "Le lien de partage n'est pas valide."
},
"camoufoxRemoved": "Camoufox n'est plus pris en charge. Recréez ce profil avec Wayfern.",
"noE2ePasswordSet": "Aucun mot de passe de chiffrement de bout en bout n'est défini. Définissez-en un avant de synchroniser des données chiffrées.",
+25 -19
View File
@@ -576,15 +576,17 @@
"ssCipherRequired": "Shadowsocks には暗号とパスワードが必要です",
"selectType": "プロキシの種類を選択",
"saveFailed": "プロキシの保存に失敗しました: {{error}}",
"vlessUri": "VLESS URI",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "XTLS Vision と REALITY が必要です。",
"vlessUriRequired": "VLESS URI を入力してください。",
"vlessUriInvalid": "ホストとポートを含む有効な VLESS URI を入力してください。",
"vlessUri": "共有リンク",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "プロバイダーのリンクをそのまま貼り付けてください。リンクにはサーバー、認証情報、トランスポートが含まれます。",
"vlessUriRequired": "共有リンクを入力してください。",
"vlessUriInvalid": "ホストとポートを含む有効な共有リンクを入力してください。",
"firstHopGroupEncrypted": "第一ホップは暗号化",
"firstHopGroupLink": "第一ホップはリンク次第",
"firstHopGroupPlaintext": "第一ホップは平文",
"firstHopGroupCipher": "第一ホップは暗号方式次第",
"firstHopCipherNote": "このマシンからプロキシまでの接続が暗号化されるかどうかは、下の暗号方式で決まります。暗号方式が空欄の場合や、none のように何も暗号化しない方式の場合、通信はプロバイダーや途中のネットワーク上の誰にでも読み取れるままです。",
"firstHopLinkNote": "このマシンからプロキシまでの接続が暗号化されるかどうかは、共有リンクで決まります(TLS、REALITY、またはプロトコル自身の暗号化)。どれも含まないリンクでは、通信はプロバイダーや途中のネットワーク上の誰にでも読み取れるままです。",
"firstHopEncryptedNote": "このマシンからプロキシまでの接続は暗号化されています。ただしプロバイダーは、アクセスするすべてのサイトと、HTTPS ではないページの内容をすべて見ることができます。",
"firstHopPlaintextNote": "このマシンからプロキシまでの接続は暗号化されていません。プロバイダー、およびこのマシンとプロキシの間のネットワーク上にいる誰もが通信を読み取れます。",
"credentialsInClearHeading": "この認証情報は読み取り可能な状態でネットワークを通過します",
@@ -600,7 +602,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "通常",
@@ -2218,9 +2223,9 @@
"mcpAgentUnknown": "このMCPクライアントはサポートされていません。",
"mcpAgentInstallFailed": "MCPクライアントにDonut Browserを追加できませんでした:{{detail}}",
"mcpAgentRemoveFailed": "MCPクライアントからDonut Browserを削除できませんでした:{{detail}}",
"vlessConfigInvalid": "VLESS URI が無効です。",
"vlessConfigInvalid": "共有リンクが無効です。",
"xrayUnavailable": "このシステムでは Xray-core を利用できません。",
"xrayUnsupportedOs": "VLESS には macOS 12 以降が必要です。",
"xrayUnsupportedOs": "VLESS、VMess、Trojan、Hysteria2のプロキシにはmacOS 12以降が必要です。",
"xrayStartFailed": "Xray-core を起動できませんでした。",
"cloudNotSignedIn": "この機能を使うには Donut Browser アカウントにサインインしてください。",
"cloudUnreachable": "Donut Browser のサーバーに接続できませんでした。接続を確認してもう一度お試しください。",
@@ -2265,17 +2270,18 @@
"vpnWorkerStartFailed": "VPN接続を開始できませんでした: {{detail}}",
"exitProbeFailed": "プロキシの出口ノードに接続できず、所在地を確認できませんでした。",
"vlessUnsupported": {
"security": "このVLESSサーバーはREALITYを使用していません。DonutはREALITY付きのVLESSのみに対応しています。",
"flow": "このVLESSサーバーは、Donutが必要とするXTLS Visionフローを使用していません。",
"transport": "Donutは素のTCP上のVLESSのみに対応しています。このサーバーは別のトランスポート(WebSocketやgRPCなど)を使用しています。",
"encryption": "このVLESSサーバーは、Donutが対応していない暗号化設定を使用しています。",
"headerType": "このVLESSサーバーは、Donutが対応していないヘッダー難読化を使用しています。",
"fingerprint": "このVLESS URIは、Donutが対応していないTLSフィンガープリントを要求しています。",
"sni": "VLESS URIに、REALITYに必要なSNI(sni)がありません。",
"publicKey": "VLESS URIに、REALITYの公開鍵(pbk)がありません。",
"scheme": "これはVLESSリンクではありません。vless:// で始まる必要があります。",
"parameter": "VLESS URIに、Donutが対応していないオプションが含まれています。",
"malformed": "VLESS URIが無効です。"
"security": "このリンクはDonutが対応していないセキュリティ設定を使用しています。Donutはnone、TLS、REALITYに対応しており、REALITYはRAW (TCP)、gRPC、XHTTP上でのみ使用できます。",
"flow": "このリンクはDonutが対応していないflowを使用しています。XTLS Visionは、TLSまたはREALITYを使うRAW (TCP)上のVLESSでのみ動作します。",
"transport": "このリンクはDonutが対応していないトランスポートまたはトランスポートモードを使用しています。DonutはRAW (TCP)、WebSocket、gRPC、HTTPUpgrade、XHTTPに対応していますが、mKCP、HTTP/2、QUICには対応していません。",
"encryption": "このリンクはDonutが対応していない暗号化設定を使用しています。",
"headerType": "このリンクはDonutが対応していないヘッダー難読化を使用しています。",
"fingerprint": "このリンクはDonutが対応していないTLSフィンガープリントを要求しています。",
"sni": "リンクに有効なサーバー名 (sni) がありません。",
"publicKey": "リンクにREALITYの公開鍵 (pbk) がありません。",
"obfs": "このHysteria2リンクはDonutが対応していない難読化を使用しています。DonutはSalamanderのみに対応しています。",
"scheme": "対応している共有リンクではありません。vless://、vmess://、trojan://、hysteria2:// または hy2:// で始まる必要があります。",
"parameter": "リンクにDonutが対応していないオプションが含まれています。",
"malformed": "共有リンクが無効です。"
},
"camoufoxRemoved": "Camoufoxはサポートされなくなりました。Wayfernでこのプロファイルを作り直してください。",
"noE2ePasswordSet": "エンドツーエンド暗号化のパスワードが設定されていません。暗号化データを同期する前に設定してください。",
+25 -19
View File
@@ -576,15 +576,17 @@
"ssCipherRequired": "Shadowsocks에는 암호화와 비밀번호가 필요합니다",
"selectType": "프록시 유형 선택",
"saveFailed": "프록시 저장 실패: {{error}}",
"vlessUri": "VLESS URI",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "XTLS Vision 및 REALITY가 필요합니다.",
"vlessUriRequired": "VLESS URI를 입력하세요.",
"vlessUriInvalid": "호스트와 포트가 포함된 올바른 VLESS URI를 입력하세요.",
"vlessUri": "공유 링크",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "제공업체의 링크 전체를 붙여 넣으세요. 링크에는 서버, 인증 정보, 전송 방식이 들어 있습니다.",
"vlessUriRequired": "공유 링크를 입력하세요.",
"vlessUriInvalid": "호스트와 포트가 포함된 올바른 공유 링크를 입력하세요.",
"firstHopGroupEncrypted": "첫 구간 암호화됨",
"firstHopGroupLink": "첫 구간은 링크가 결정",
"firstHopGroupPlaintext": "첫 구간 평문",
"firstHopGroupCipher": "첫 구간은 암호화 방식에 달림",
"firstHopCipherNote": "이 컴퓨터에서 프록시까지의 연결이 암호화되는지는 아래 암호화 방식이 결정합니다. 암호화 방식이 비어 있거나 none처럼 아무것도 암호화하지 않는 방식이면, 트래픽은 제공업체와 중간 네트워크에 있는 누구에게나 읽히는 상태로 남습니다.",
"firstHopLinkNote": "이 컴퓨터에서 프록시까지의 연결이 암호화되는지는 공유 링크가 결정합니다: TLS, REALITY 또는 프로토콜 자체의 암호화. 이 중 어느 것도 없는 링크라면, 트래픽은 제공업체와 중간 네트워크에 있는 누구에게나 읽히는 상태로 남습니다.",
"firstHopEncryptedNote": "이 컴퓨터에서 프록시까지의 연결은 암호화됩니다. 다만 제공업체는 접속하는 모든 사이트와 HTTPS가 아닌 페이지의 모든 내용을 여전히 볼 수 있습니다.",
"firstHopPlaintextNote": "이 컴퓨터에서 프록시까지의 연결은 암호화되지 않습니다. 제공업체와 이 컴퓨터와 프록시 사이 네트워크에 있는 누구나 트래픽을 읽을 수 있습니다.",
"credentialsInClearHeading": "이 자격 증명은 읽을 수 있는 상태로 네트워크를 지나갑니다",
@@ -600,7 +602,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "일반",
@@ -2218,9 +2223,9 @@
"mcpAgentUnknown": "이 MCP 클라이언트는 지원되지 않습니다.",
"mcpAgentInstallFailed": "MCP 클라이언트에 Donut Browser를 추가하지 못했습니다: {{detail}}",
"mcpAgentRemoveFailed": "MCP 클라이언트에서 Donut Browser를 제거하지 못했습니다: {{detail}}",
"vlessConfigInvalid": "VLESS URI가 올바르지 않습니다.",
"vlessConfigInvalid": "공유 링크가 올바르지 않습니다.",
"xrayUnavailable": "이 시스템에서는 Xray-core를 사용할 수 없습니다.",
"xrayUnsupportedOs": "VLESS를 사용하려면 macOS 12 이상이 필요합니다.",
"xrayUnsupportedOs": "VLESS, VMess, Trojan, Hysteria2 프록시는 macOS 12 이상이 필요합니다.",
"xrayStartFailed": "Xray-core를 시작할 수 없습니다.",
"cloudNotSignedIn": "이 기능을 사용하려면 Donut Browser 계정에 로그인하세요.",
"cloudUnreachable": "Donut Browser 서버에 연결하지 못했습니다. 연결 상태를 확인하고 다시 시도하세요.",
@@ -2265,17 +2270,18 @@
"vpnWorkerStartFailed": "VPN 연결을 시작하지 못했습니다: {{detail}}",
"exitProbeFailed": "프록시 출구 노드에 연결할 수 없어 위치를 확인하지 못했습니다.",
"vlessUnsupported": {
"security": "이 VLESS 서버는 REALITY를 사용하지 않습니다. Donut은 REALITY를 사용하는 VLESS만 지원합니다.",
"flow": "이 VLESS 서버는 Donut이 요구하는 XTLS Vision 플로우를 사용하지 않습니다.",
"transport": "Donut은 일반 TCP 기반 VLESS만 지원합니다. 이 서버는 다른 전송 방식(WebSocket, gRPC 등)을 사용합니다.",
"encryption": "이 VLESS 서버는 Donut이 지원하지 않는 암호화 설정을 사용합니다.",
"headerType": "이 VLESS 서버는 Donut이 지원하지 않는 헤더 난독화를 사용합니다.",
"fingerprint": "이 VLESS URI는 Donut이 지원하지 않는 TLS 지문을 요청합니다.",
"sni": "VLESS URI에 REALITY에 필요한 SNI(sni)가 없습니다.",
"publicKey": "VLESS URI에 REALITY 공개 키(pbk)가 없습니다.",
"scheme": "VLESS 링크가 아닙니다. vless:// 로 시작해야 합니다.",
"parameter": "VLESS URI에 Donut이 지원하지 않는 옵션이 있습니다.",
"malformed": "VLESS URI가 올바르지 않습니다."
"security": "이 링크는 Donut이 지원하지 않는 보안 설정을 사용합니다. Donut은 none, TLS, REALITY를 지원하며, REALITY는 RAW (TCP), gRPC, XHTTP에서만 사용할 수 있습니다.",
"flow": "이 링크는 Donut이 지원하지 않는 flow를 사용합니다. XTLS Vision은 TLS 또는 REALITY를 사용하는 RAW (TCP) 위의 VLESS에서만 작동합니다.",
"transport": "이 링크는 Donut이 지원하지 않는 전송 방식 또는 전송 모드를 사용합니다. Donut은 RAW (TCP), WebSocket, gRPC, HTTPUpgrade, XHTTP를 지원하지만 mKCP, HTTP/2, QUIC은 지원하지 않습니다.",
"encryption": "이 링크는 Donut이 지원하지 않는 암호화 설정을 사용합니다.",
"headerType": "이 링크는 Donut이 지원하지 않는 헤더 난독화를 사용합니다.",
"fingerprint": "이 링크는 Donut이 지원하지 않는 TLS 지문을 요청합니다.",
"sni": "링크에 올바른 서버 이름(sni)이 없습니다.",
"publicKey": "링크에 REALITY 공개 키(pbk)가 없습니다.",
"obfs": "이 Hysteria2 링크는 Donut이 지원하지 않는 난독화를 사용합니다. Donut은 Salamander만 지원합니다.",
"scheme": "지원되는 공유 링크가 아닙니다. vless://, vmess://, trojan://, hysteria2:// 또는 hy2:// 로 시작해야 합니다.",
"parameter": "링크에 Donut이 지원하지 않는 옵션이 포함되어 있습니다.",
"malformed": "공유 링크가 올바르지 않습니다."
},
"camoufoxRemoved": "Camoufox는 더 이상 지원되지 않습니다. Wayfern으로 이 프로필을 다시 만드세요.",
"noE2ePasswordSet": "종단 간 암호화 비밀번호가 설정되지 않았습니다. 암호화된 데이터를 동기화하기 전에 설정하세요.",
+25 -19
View File
@@ -578,15 +578,17 @@
"ssCipherRequired": "Cifra e senha são obrigatórias para Shadowsocks",
"selectType": "Selecione o tipo de proxy",
"saveFailed": "Falha ao salvar o proxy: {{error}}",
"vlessUri": "URI VLESS",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "Requer XTLS Vision e REALITY.",
"vlessUriRequired": "Insira um URI VLESS.",
"vlessUriInvalid": "Insira um URI VLESS válido com host e porta.",
"vlessUri": "Link de compartilhamento",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "Cole o link completo do seu fornecedor. Ele contém o servidor, as credenciais e o transporte.",
"vlessUriRequired": "Insira um link de compartilhamento.",
"vlessUriInvalid": "Insira um link de compartilhamento válido com host e porta.",
"firstHopGroupEncrypted": "Primeiro salto cifrado",
"firstHopGroupLink": "O primeiro salto é definido pelo link",
"firstHopGroupPlaintext": "Primeiro salto em texto simples",
"firstHopGroupCipher": "O primeiro salto depende da cifra",
"firstHopCipherNote": "Se a ligação deste computador ao proxy é cifrada depende da cifra abaixo. Uma cifra vazia, ou uma cifra nula como none, deixa o tráfego legível para o fornecedor e para qualquer pessoa na rede pelo meio.",
"firstHopLinkNote": "Se a ligação deste computador ao proxy é cifrada depende do link de compartilhamento: TLS, REALITY ou a cifra própria do protocolo. Um link sem nenhum deles deixa o tráfego legível para o fornecedor e para qualquer pessoa na rede pelo meio.",
"firstHopEncryptedNote": "A ligação deste computador ao proxy é cifrada. O fornecedor continua a ver todos os sites que visita e todo o conteúdo das páginas que não usam HTTPS.",
"firstHopPlaintextNote": "A ligação deste computador ao proxy não é cifrada. O fornecedor, e qualquer pessoa na rede entre este computador e ele, consegue ler o tráfego.",
"credentialsInClearHeading": "Estas credenciais atravessam a rede legíveis",
@@ -602,7 +604,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "Regular",
@@ -2228,9 +2233,9 @@
"mcpAgentUnknown": "Este cliente MCP não é compatível.",
"mcpAgentInstallFailed": "Não foi possível adicionar o Donut Browser ao cliente MCP: {{detail}}",
"mcpAgentRemoveFailed": "Não foi possível remover o Donut Browser do cliente MCP: {{detail}}",
"vlessConfigInvalid": "O URI VLESS é inválido.",
"vlessConfigInvalid": "O link de compartilhamento é inválido.",
"xrayUnavailable": "O Xray-core não está disponível neste sistema.",
"xrayUnsupportedOs": "O VLESS requer macOS 12 ou posterior.",
"xrayUnsupportedOs": "Os proxies VLESS, VMess, Trojan e Hysteria2 requerem macOS 12 ou mais recente.",
"xrayStartFailed": "Não foi possível iniciar o Xray-core.",
"cloudNotSignedIn": "Entre na sua conta do Donut Browser para usar este recurso.",
"cloudUnreachable": "Não foi possível contatar os servidores do Donut Browser. Verifique sua conexão e tente novamente.",
@@ -2275,17 +2280,18 @@
"vpnWorkerStartFailed": "Não foi possível iniciar a conexão VPN: {{detail}}",
"exitProbeFailed": "Não foi possível alcançar o nó de saída do proxy para verificar sua localização.",
"vlessUnsupported": {
"security": "Este servidor VLESS não usa REALITY. O Donut só oferece suporte a VLESS com REALITY.",
"flow": "Este servidor VLESS não usa o fluxo XTLS Vision, exigido pelo Donut.",
"transport": "O Donut só oferece suporte a VLESS sobre TCP simples — este servidor usa outro transporte (como WebSocket ou gRPC).",
"encryption": "Este servidor VLESS usa uma criptografia sem suporte no Donut.",
"headerType": "Este servidor VLESS usa uma ofuscação de cabeçalho sem suporte no Donut.",
"fingerprint": "Esta URI VLESS solicita uma impressão digital TLS sem suporte no Donut.",
"sni": "Falta na URI VLESS o SNI (sni) necessário para o REALITY.",
"publicKey": "Falta na URI VLESS a chave pública do REALITY (pbk).",
"scheme": "Isso não é um link VLESS. Ele precisa começar com vless://.",
"parameter": "A URI VLESS contém uma opção sem suporte no Donut.",
"malformed": "A URI VLESS é inválida."
"security": "Este link usa uma definição de segurança que o Donut não suporta. O Donut suporta none, TLS e REALITY, e REALITY apenas sobre RAW (TCP), gRPC ou XHTTP.",
"flow": "Este link usa um flow que o Donut não suporta. O XTLS Vision só funciona com VLESS sobre RAW (TCP) com TLS ou REALITY.",
"transport": "Este link usa um transporte, ou um modo de transporte, que o Donut não suporta. O Donut suporta RAW (TCP), WebSocket, gRPC, HTTPUpgrade e XHTTP, mas não mKCP, HTTP/2 nem QUIC.",
"encryption": "Este link usa uma definição de cifra que o Donut não suporta.",
"headerType": "Este link usa uma ofuscação de cabeçalho que o Donut não suporta.",
"fingerprint": "Este link pede uma impressão digital TLS que o Donut não suporta.",
"sni": "Falta ao link um nome de servidor (sni) válido.",
"publicKey": "Falta ao link a chave pública do REALITY (pbk).",
"obfs": "Este link Hysteria2 usa uma ofuscação que o Donut não suporta. O Donut suporta apenas Salamander.",
"scheme": "Isso não é um link de compartilhamento suportado. Ele precisa começar com vless://, vmess://, trojan://, hysteria2:// ou hy2://.",
"parameter": "O link contém uma opção que o Donut não suporta.",
"malformed": "O link de compartilhamento é inválido."
},
"camoufoxRemoved": "O Camoufox não é mais compatível. Recrie este perfil com o Wayfern.",
"noE2ePasswordSet": "Nenhuma senha de criptografia de ponta a ponta foi definida. Defina uma antes de sincronizar dados criptografados.",
+25 -19
View File
@@ -580,15 +580,17 @@
"ssCipherRequired": "Для Shadowsocks требуется шифр и пароль",
"selectType": "Выберите тип прокси",
"saveFailed": "Не удалось сохранить прокси: {{error}}",
"vlessUri": "URI VLESS",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "Требуются XTLS Vision и REALITY.",
"vlessUriRequired": "Введите URI VLESS.",
"vlessUriInvalid": "Введите корректный URI VLESS с адресом и портом.",
"vlessUri": "Ссылка для подключения",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "Вставьте полную ссылку от провайдера. В ней есть сервер, учётные данные и транспорт.",
"vlessUriRequired": "Введите ссылку для подключения.",
"vlessUriInvalid": "Введите корректную ссылку для подключения с хостом и портом.",
"firstHopGroupEncrypted": "Первый узел зашифрован",
"firstHopGroupLink": "Первый узел задаёт ссылка",
"firstHopGroupPlaintext": "Первый узел без шифрования",
"firstHopGroupCipher": "Первый узел зависит от шифра",
"firstHopCipherNote": "Будет ли соединение с этого компьютера до прокси зашифровано, решает шифр ниже. Пустой шифр или пустышка вроде none оставляет трафик читаемым для провайдера и для любого в сети между ними.",
"firstHopLinkNote": "Будет ли соединение с этого компьютера до прокси зашифровано, решает ссылка для подключения: TLS, REALITY или собственное шифрование протокола. Ссылка без них оставляет трафик читаемым для провайдера и для любого в сети между ними.",
"firstHopEncryptedNote": "Соединение с этого компьютера до прокси зашифровано. Провайдер всё равно видит каждый сайт, к которому вы обращаетесь, и всё содержимое страниц без HTTPS.",
"firstHopPlaintextNote": "Соединение с этого компьютера до прокси не зашифровано. Провайдер и любой в сети между этим компьютером и прокси может прочитать трафик.",
"credentialsInClearHeading": "Эти учётные данные проходят по сети в читаемом виде",
@@ -604,7 +606,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "Обычный",
@@ -2238,9 +2243,9 @@
"mcpAgentUnknown": "Этот клиент MCP не поддерживается.",
"mcpAgentInstallFailed": "Не удалось добавить Donut Browser в клиент MCP: {{detail}}",
"mcpAgentRemoveFailed": "Не удалось удалить Donut Browser из клиента MCP: {{detail}}",
"vlessConfigInvalid": "URI VLESS недействителен.",
"vlessConfigInvalid": "Ссылка для подключения недействительна.",
"xrayUnavailable": "Xray-core недоступен в этой системе.",
"xrayUnsupportedOs": "Для VLESS требуется macOS 12 или новее.",
"xrayUnsupportedOs": "Для прокси VLESS, VMess, Trojan и Hysteria2 нужна macOS 12 или новее.",
"xrayStartFailed": "Не удалось запустить Xray-core.",
"cloudNotSignedIn": "Войдите в аккаунт Donut Browser, чтобы использовать эту функцию.",
"cloudUnreachable": "Не удалось связаться с серверами Donut Browser. Проверьте подключение и попробуйте снова.",
@@ -2285,17 +2290,18 @@
"vpnWorkerStartFailed": "Не удалось запустить VPN-подключение: {{detail}}",
"exitProbeFailed": "Не удалось связаться с выходным узлом прокси, чтобы определить его местоположение.",
"vlessUnsupported": {
"security": "Этот сервер VLESS не использует REALITY. Donut поддерживает только VLESS с REALITY.",
"flow": "Этот сервер VLESS не использует поток XTLS Vision, который требуется Donut.",
"transport": "Donut поддерживает VLESS только поверх обычного TCP — этот сервер использует другой транспорт (например, WebSocket или gRPC).",
"encryption": "Этот сервер VLESS использует шифрование, которое Donut не поддерживает.",
"headerType": "Этот сервер VLESS использует обфускацию заголовков, которую Donut не поддерживает.",
"fingerprint": "Этот VLESS URI запрашивает отпечаток TLS, который Donut не поддерживает.",
"sni": "В VLESS URI отсутствует SNI (sni), необходимый для REALITY.",
"publicKey": "В VLESS URI отсутствует открытый ключ REALITY (pbk).",
"scheme": "Это не ссылка VLESS. Она должна начинаться с vless://.",
"parameter": "VLESS URI содержит параметр, который Donut не поддерживает.",
"malformed": "VLESS URI недействителен."
"security": "Эта ссылка использует настройку безопасности, которую Donut не поддерживает. Donut поддерживает none, TLS и REALITY, причём REALITY только поверх RAW (TCP), gRPC или XHTTP.",
"flow": "Эта ссылка использует flow, который Donut не поддерживает. XTLS Vision работает только с VLESS поверх RAW (TCP) с TLS или REALITY.",
"transport": "Эта ссылка использует транспорт или режим транспорта, который Donut не поддерживает. Donut поддерживает RAW (TCP), WebSocket, gRPC, HTTPUpgrade и XHTTP, но не mKCP, HTTP/2 и QUIC.",
"encryption": "Эта ссылка использует настройку шифрования, которую Donut не поддерживает.",
"headerType": "Эта ссылка использует маскировку заголовков, которую Donut не поддерживает.",
"fingerprint": "Эта ссылка запрашивает отпечаток TLS, который Donut не поддерживает.",
"sni": "В ссылке нет корректного имени сервера (sni).",
"publicKey": "В ссылке нет открытого ключа REALITY (pbk).",
"obfs": "Эта ссылка Hysteria2 использует обфускацию, которую Donut не поддерживает. Donut поддерживает только Salamander.",
"scheme": "Это не поддерживаемая ссылка для подключения. Она должна начинаться с vless://, vmess://, trojan://, hysteria2:// или hy2://.",
"parameter": "Ссылка содержит параметр, который Donut не поддерживает.",
"malformed": "Ссылка для подключения недействительна."
},
"camoufoxRemoved": "Camoufox больше не поддерживается. Создайте этот профиль заново с Wayfern.",
"noE2ePasswordSet": "Пароль сквозного шифрования не задан. Задайте его перед синхронизацией зашифрованных данных.",
+25 -19
View File
@@ -576,15 +576,17 @@
"ssCipherRequired": "Shadowsocks için şifreleme algoritması ve parola zorunludur",
"selectType": "Proxy türünü seçin",
"saveFailed": "Proxy kaydedilemedi: {{error}}",
"vlessUri": "VLESS URI'si",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "XTLS Vision ve REALITY gerektirir.",
"vlessUriRequired": "Bir VLESS URI'si girin.",
"vlessUriInvalid": "Sunucu ve bağlantı noktası içeren geçerli bir VLESS URI'si girin.",
"vlessUri": "Paylaşım bağlantısı",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "Sağlayıcınızdan gelen bağlantının tamamını yapıştırın. Bağlantı sunucuyu, kimlik bilgilerini ve aktarımı içerir.",
"vlessUriRequired": "Bir paylaşım bağlantısı girin.",
"vlessUriInvalid": "Ana bilgisayar ve port içeren geçerli bir paylaşım bağlantısı girin.",
"firstHopGroupEncrypted": "İlk adım şifreli",
"firstHopGroupLink": "İlk adımı bağlantı belirler",
"firstHopGroupPlaintext": "İlk adım şifresiz",
"firstHopGroupCipher": "İlk adım şifreleme algoritmasına bağlı",
"firstHopCipherNote": "Bu makineden proxy'ye giden bağlantının şifreli olup olmadığına aşağıdaki şifreleme algoritması karar verir. Boş bir algoritma ya da none gibi hiçbir şey şifrelemeyen bir algoritma, trafiği sağlayıcı ve aradaki ağda bulunan herkes için okunabilir bırakır.",
"firstHopLinkNote": "Bu makineden proxy'ye giden bağlantının şifreli olup olmadığına paylaşım bağlantısı karar verir: TLS, REALITY ya da protokolün kendi şifrelemesi. Bunların hiçbirini içermeyen bir bağlantı, trafiği sağlayıcı ve aradaki ağda bulunan herkes için okunabilir bırakır.",
"firstHopEncryptedNote": "Bu makineden proxy'ye giden bağlantı şifrelidir. Sağlayıcı yine de eriştiğiniz her siteyi ve HTTPS olmayan sayfalardaki her şeyi görür.",
"firstHopPlaintextNote": "Bu makineden proxy'ye giden bağlantı şifreli değildir. Sağlayıcı ve bu makine ile proxy arasındaki ağdaki herkes trafiği okuyabilir.",
"credentialsInClearHeading": "Bu kimlik bilgileri ağ üzerinden okunabilir biçimde geçer",
@@ -600,7 +602,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "Normal",
@@ -2218,9 +2223,9 @@
"mcpAgentUnknown": "Bu MCP istemcisi desteklenmiyor.",
"mcpAgentInstallFailed": "Donut Browser MCP istemcisine eklenemedi: {{detail}}",
"mcpAgentRemoveFailed": "Donut Browser MCP istemcisinden kaldırılamadı: {{detail}}",
"vlessConfigInvalid": "VLESS URI'si geçersiz.",
"vlessConfigInvalid": "Paylaşım bağlantısı geçersiz.",
"xrayUnavailable": "Xray-core bu sistemde kullanılamıyor.",
"xrayUnsupportedOs": "VLESS için macOS 12 veya sonraki bir sürüm gerekir.",
"xrayUnsupportedOs": "VLESS, VMess, Trojan ve Hysteria2 proxy'leri macOS 12 veya daha yenisini gerektirir.",
"xrayStartFailed": "Xray-core başlatılamadı.",
"cloudNotSignedIn": "Bunu kullanmak için Donut Browser hesabınıza giriş yapın.",
"cloudUnreachable": "Donut Browser sunucularına ulaşılamadı. Bağlantınızı kontrol edip tekrar deneyin.",
@@ -2265,17 +2270,18 @@
"vpnWorkerStartFailed": "VPN bağlantısı başlatılamadı: {{detail}}",
"exitProbeFailed": "Konumunu denetlemek için proxy çıkış düğümüne ulaşılamadı.",
"vlessUnsupported": {
"security": "Bu VLESS sunucusu REALITY kullanmıyor. Donut yalnızca REALITY ile VLESS'i destekler.",
"flow": "Bu VLESS sunucusu, Donut'ın gerektirdiği XTLS Vision akışını kullanmıyor.",
"transport": "Donut yalnızca düz TCP üzerinden VLESS'i destekler — bu sunucu farklı bir taşıma (WebSocket veya gRPC gibi) kullanıyor.",
"encryption": "Bu VLESS sunucusu, Donut'ın desteklemediği bir şifreleme kullanıyor.",
"headerType": "Bu VLESS sunucusu, Donut'ın desteklemediği bir başlık gizlemesi kullanıyor.",
"fingerprint": "Bu VLESS URI'si, Donut'ın desteklemediği bir TLS parmak izi istiyor.",
"sni": "VLESS URI'sinde REALITY için gereken SNI (sni) eksik.",
"publicKey": "VLESS URI'sinde REALITY genel anahtarı (pbk) eksik.",
"scheme": "Bu bir VLESS bağlantısı değil. vless:// ile başlamalı.",
"parameter": "VLESS URI'si, Donut'ın desteklemediği bir seçenek içeriyor.",
"malformed": "VLESS URI'si geçersiz."
"security": "Bu bağlantı, Donut'un desteklemediği bir güvenlik ayarı kullanıyor. Donut none, TLS ve REALITY'yi destekler; REALITY yalnızca RAW (TCP), gRPC veya XHTTP üzerinde çalışır.",
"flow": "Bu bağlantı, Donut'un desteklemediği bir flow kullanıyor. XTLS Vision yalnızca TLS veya REALITY ile RAW (TCP) üzerinden VLESS ile çalışır.",
"transport": "Bu bağlantı, Donut'un desteklemediği bir aktarım veya aktarım modu kullanıyor. Donut RAW (TCP), WebSocket, gRPC, HTTPUpgrade ve XHTTP'yi destekler; mKCP, HTTP/2 ve QUIC'i desteklemez.",
"encryption": "Bu bağlantı, Donut'un desteklemediği bir şifreleme ayarı kullanıyor.",
"headerType": "Bu bağlantı, Donut'un desteklemediği bir başlık gizlemesi kullanıyor.",
"fingerprint": "Bu bağlantı, Donut'un desteklemediği bir TLS parmak izi istiyor.",
"sni": "Bağlantıda geçerli bir sunucu adı (sni) yok.",
"publicKey": "Bağlantıda REALITY açık anahtarı (pbk) yok.",
"obfs": "Bu Hysteria2 bağlantısı, Donut'un desteklemediği bir gizleme kullanıyor. Donut yalnızca Salamander'ı destekler.",
"scheme": "Bu desteklenen bir paylaşım bağlantısı değil. vless://, vmess://, trojan://, hysteria2:// veya hy2:// ile başlamalı.",
"parameter": "Bağlantı, Donut'un desteklemediği bir seçenek içeriyor.",
"malformed": "Paylaşım bağlantısı geçersiz."
},
"camoufoxRemoved": "Camoufox artık desteklenmiyor. Bu profili Wayfern ile yeniden oluşturun.",
"noE2ePasswordSet": "Uçtan uca şifreleme parolası ayarlanmamış. Şifreli veriyi eşitlemeden önce bir parola belirleyin.",
+25 -19
View File
@@ -576,15 +576,17 @@
"ssCipherRequired": "Cipher và mật khẩu là bắt buộc cho Shadowsocks",
"selectType": "Chọn loại proxy",
"saveFailed": "Lưu proxy thất bại: {{error}}",
"vlessUri": "URI VLESS",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "Yêu cầu XTLS Vision và REALITY.",
"vlessUriRequired": "Nhập URI VLESS.",
"vlessUriInvalid": "Nhập URI VLESS hợp lệ có máy chủ và cổng.",
"vlessUri": "Liên kết chia sẻ",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "Dán toàn bộ liên kết từ nhà cung cấp. Liên kết chứa máy chủ, thông tin xác thực và phương thức truyền tải.",
"vlessUriRequired": "Nhập một liên kết chia sẻ.",
"vlessUriInvalid": "Nhập một liên kết chia sẻ hợp lệ có host và cổng.",
"firstHopGroupEncrypted": "Chặng đầu được mã hóa",
"firstHopGroupLink": "Chặng đầu do liên kết quyết định",
"firstHopGroupPlaintext": "Chặng đầu không mã hóa",
"firstHopGroupCipher": "Chặng đầu tùy thuộc vào cipher",
"firstHopCipherNote": "Kết nối từ máy này đến proxy có được mã hóa hay không là do cipher bên dưới quyết định. Cipher để trống, hoặc một cipher rỗng như none, khiến lưu lượng vẫn đọc được với nhà cung cấp và với bất kỳ ai trên đường mạng ở giữa.",
"firstHopLinkNote": "Kết nối từ máy này đến proxy có được mã hóa hay không là do liên kết chia sẻ quyết định: TLS, REALITY hoặc mã hóa riêng của giao thức. Liên kết không có những thứ đó khiến lưu lượng vẫn đọc được với nhà cung cấp và với bất kỳ ai trên đường mạng ở giữa.",
"firstHopEncryptedNote": "Kết nối từ máy này đến proxy được mã hóa. Nhà cung cấp vẫn thấy mọi trang bạn truy cập và toàn bộ nội dung của các trang không dùng HTTPS.",
"firstHopPlaintextNote": "Kết nối từ máy này đến proxy không được mã hóa. Nhà cung cấp, và bất kỳ ai trên đường mạng giữa máy này và proxy, đều đọc được lưu lượng.",
"credentialsInClearHeading": "Thông tin đăng nhập này đi qua mạng ở dạng đọc được",
@@ -600,7 +602,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "Thông thường",
@@ -2218,9 +2223,9 @@
"mcpAgentUnknown": "Ứng dụng MCP này không được hỗ trợ.",
"mcpAgentInstallFailed": "Không thể thêm Donut Browser vào ứng dụng MCP: {{detail}}",
"mcpAgentRemoveFailed": "Không thể xóa Donut Browser khỏi ứng dụng MCP: {{detail}}",
"vlessConfigInvalid": "URI VLESS không hợp lệ.",
"vlessConfigInvalid": "Liên kết chia sẻ không hợp lệ.",
"xrayUnavailable": "Xray-core không khả dụng trên hệ thống này.",
"xrayUnsupportedOs": "VLESS yêu cầu macOS 12 trở lên.",
"xrayUnsupportedOs": "Proxy VLESS, VMess, Trojan và Hysteria2 yêu cầu macOS 12 trở lên.",
"xrayStartFailed": "Không thể khởi động Xray-core.",
"cloudNotSignedIn": "Hãy đăng nhập tài khoản Donut Browser để dùng tính năng này.",
"cloudUnreachable": "Không kết nối được tới máy chủ Donut Browser. Hãy kiểm tra kết nối và thử lại.",
@@ -2265,17 +2270,18 @@
"vpnWorkerStartFailed": "Không thể khởi động kết nối VPN: {{detail}}",
"exitProbeFailed": "Không thể kết nối tới nút thoát của proxy để kiểm tra vị trí.",
"vlessUnsupported": {
"security": "Máy chủ VLESS này không dùng REALITY. Donut chỉ hỗ trợ VLESS kèm REALITY.",
"flow": "Máy chủ VLESS này không dùng luồng XTLS Vision mà Donut yêu cầu.",
"transport": "Donut chỉ hỗ trợ VLESS trên TCP thuần — máy chủ này dùng phương thức truyền khác (như WebSocket hoặc gRPC).",
"encryption": "Máy chủ VLESS này dùng thiết lập mã hóa mà Donut không hỗ trợ.",
"headerType": "Máy chủ VLESS này dùng cách che giấu tiêu đề mà Donut không hỗ trợ.",
"fingerprint": "URI VLESS này yêu cầu một dấu vân tay TLS mà Donut không hỗ trợ.",
"sni": "URI VLESS thiếu SNI (sni) cần cho REALITY.",
"publicKey": "URI VLESS thiếu khóa công khai REALITY (pbk).",
"scheme": "Đây không phải liên kết VLESS. Nó phải bắt đầu bằng vless://.",
"parameter": "URI VLESS chứa một tùy chọn mà Donut không hỗ trợ.",
"malformed": "URI VLESS không hợp lệ."
"security": "Liên kết này dùng một thiết lập bảo mật mà Donut không hỗ trợ. Donut hỗ trợ none, TLS và REALITY, và REALITY chỉ qua RAW (TCP), gRPC hoặc XHTTP.",
"flow": "Liên kết này dùng một flow mà Donut không hỗ trợ. XTLS Vision chỉ hoạt động với VLESS qua RAW (TCP) kèm TLS hoặc REALITY.",
"transport": "Liên kết này dùng một phương thức truyền tải, hoặc một chế độ truyền tải, mà Donut không hỗ trợ. Donut hỗ trợ RAW (TCP), WebSocket, gRPC, HTTPUpgrade và XHTTP, nhưng không hỗ trợ mKCP, HTTP/2 hay QUIC.",
"encryption": "Liên kết này dùng một thiết lập mã hóa mà Donut không hỗ trợ.",
"headerType": "Liên kết này dùng một kiểu làm rối header mà Donut không hỗ trợ.",
"fingerprint": "Liên kết này yêu cầu một dấu vân tay TLS mà Donut không hỗ trợ.",
"sni": "Liên kết thiếu tên máy chủ (sni) hợp lệ.",
"publicKey": "Liên kết thiếu khóa công khai REALITY (pbk).",
"obfs": "Liên kết Hysteria2 này dùng một kiểu làm rối mà Donut không hỗ trợ. Donut chỉ hỗ trợ Salamander.",
"scheme": "Đây không phải liên kết chia sẻ được hỗ trợ. Nó phải bắt đầu bằng vless://, vmess://, trojan://, hysteria2:// hoặc hy2://.",
"parameter": "Liên kết chứa một tùy chọn mà Donut không hỗ trợ.",
"malformed": "Liên kết chia sẻ không hợp lệ."
},
"camoufoxRemoved": "Camoufox không còn được hỗ trợ. Hãy tạo lại hồ sơ này bằng Wayfern.",
"noE2ePasswordSet": "Chưa đặt mật khẩu mã hóa đầu cuối. Hãy đặt trước khi đồng bộ dữ liệu đã mã hóa.",
+25 -19
View File
@@ -576,15 +576,17 @@
"ssCipherRequired": "Shadowsocks 需要密码学和密码",
"selectType": "选择代理类型",
"saveFailed": "保存代理失败: {{error}}",
"vlessUri": "VLESS URI",
"vlessUriPlaceholder": "vless://…",
"vlessUriHint": "需要 XTLS Vision 和 REALITY。",
"vlessUriRequired": "请输入 VLESS URI。",
"vlessUriInvalid": "请输入包含主机和端口的有效 VLESS URI。",
"vlessUri": "分享链接",
"vlessUriPlaceholder": "{{scheme}}://…",
"vlessUriHint": "粘贴服务商提供的完整链接。链接中包含服务器、凭据和传输方式。",
"vlessUriRequired": "请输入分享链接。",
"vlessUriInvalid": "请输入包含主机和端口的有效分享链接。",
"firstHopGroupEncrypted": "第一跳已加密",
"firstHopGroupLink": "第一跳由链接决定",
"firstHopGroupPlaintext": "第一跳未加密",
"firstHopGroupCipher": "第一跳取决于加密方式",
"firstHopCipherNote": "本机到代理之间的连接是否加密,由下方的加密方式决定。加密方式留空,或选用 none 这类不加密的方式,流量对服务商以及中间网络上的任何人都是可读的。",
"firstHopLinkNote": "本机到代理之间的连接是否加密,由分享链接决定:TLS、REALITY 或协议自身的加密。链接中都没有时,流量对服务商以及中间网络上的任何人都是可读的。",
"firstHopEncryptedNote": "本机到代理之间的连接已加密。但服务商仍然能看到你访问的每一个网站,以及所有非 HTTPS 页面的全部内容。",
"firstHopPlaintextNote": "本机到代理之间的连接未加密。服务商,以及本机与代理之间网络上的任何人,都能读取流量。",
"credentialsInClearHeading": "这些凭据会以可读形式经过网络",
@@ -600,7 +602,10 @@
"socks5": "SOCKS5",
"ss": "Shadowsocks",
"httpstls": "HTTP over TLS",
"vless": "VLESS (REALITY)"
"vless": "VLESS",
"vmess": "VMess",
"trojan": "Trojan",
"hysteria2": "Hysteria2"
},
"tabs": {
"regular": "常规",
@@ -2218,9 +2223,9 @@
"mcpAgentUnknown": "不支持此 MCP 客户端。",
"mcpAgentInstallFailed": "无法将 Donut Browser 添加到 MCP 客户端:{{detail}}",
"mcpAgentRemoveFailed": "无法从 MCP 客户端移除 Donut Browser:{{detail}}",
"vlessConfigInvalid": "VLESS URI 无效。",
"vlessConfigInvalid": "分享链接无效。",
"xrayUnavailable": "此系统无法使用 Xray-core。",
"xrayUnsupportedOs": "VLESS 需要 macOS 12 或更高版本。",
"xrayUnsupportedOs": "VLESS、VMess、Trojan 和 Hysteria2 代理需要 macOS 12 或更高版本。",
"xrayStartFailed": "无法启动 Xray-core。",
"cloudNotSignedIn": "请先登录你的 Donut Browser 账号再使用此功能。",
"cloudUnreachable": "无法连接 Donut Browser 服务器。请检查网络后重试。",
@@ -2265,17 +2270,18 @@
"vpnWorkerStartFailed": "无法启动 VPN 连接:{{detail}}",
"exitProbeFailed": "无法连接代理出口节点以检查其位置。",
"vlessUnsupported": {
"security": "此 VLESS 服务器未使用 REALITY。Donut 仅支持搭配 REALITY 的 VLESS。",
"flow": "此 VLESS 服务器未使用 Donut 所需的 XTLS Vision 流控。",
"transport": "Donut 仅支持基于普通 TCP 的 VLESS —— 此服务器使用了其他传输方式(如 WebSocket 或 gRPC)。",
"encryption": "此 VLESS 服务器使用了 Donut 不支持的加密设置。",
"headerType": "此 VLESS 服务器使用了 Donut 不支持的头部混淆。",
"fingerprint": "此 VLESS URI 请求了 Donut 不支持的 TLS 指纹。",
"sni": "VLESS URI 缺少 REALITY 所需的 SNI(sni)。",
"publicKey": "VLESS URI 缺少 REALITY 公钥(pbk)。",
"scheme": "这不是 VLESS 链接,必须以 vless:// 开头。",
"parameter": "VLESS URI 含有 Donut 不支持的选项。",
"malformed": "VLESS URI 无效。"
"security": "此链接使用了 Donut 不支持的安全设置。Donut 支持 none、TLS 和 REALITY,且 REALITY 只能用于 RAW (TCP)、gRPC 或 XHTTP。",
"flow": "此链接使用了 Donut 不支持的 flow。XTLS Vision 只能用于搭配 TLS 或 REALITY 的 RAW (TCP) 上的 VLESS。",
"transport": "此链接使用了 Donut 不支持的传输方式或传输模式。Donut 支持 RAW (TCP)、WebSocket、gRPC、HTTPUpgrade 和 XHTTP,不支持 mKCP、HTTP/2 和 QUIC。",
"encryption": "此链接使用了 Donut 不支持的加密设置。",
"headerType": "此链接使用了 Donut 不支持的头部伪装。",
"fingerprint": "此链接要求的 TLS 指纹 Donut 不支持。",
"sni": "链接缺少有效的服务器名称 (sni)。",
"publicKey": "链接缺少 REALITY 公钥 (pbk)。",
"obfs": "此 Hysteria2 链接使用了 Donut 不支持的混淆方式。Donut 仅支持 Salamander。",
"scheme": "这不是受支持的分享链接,必须以 vless://、vmess://、trojan://、hysteria2:// 或 hy2:// 开头。",
"parameter": "链接中包含 Donut 不支持的选项。",
"malformed": "分享链接无效。"
},
"camoufoxRemoved": "Camoufox 已不再受支持。请使用 Wayfern 重新创建此配置文件。",
"noE2ePasswordSet": "尚未设置端到端加密密码。请先设置后再同步加密数据。",
+5 -3
View File
@@ -563,9 +563,10 @@ export function translateBackendError(t: TFunction, err: unknown): string {
return t("backendErrors.mcpAgentRemoveFailed", {
detail: parsed.params?.detail ?? "",
});
// Donut supports exactly one VLESS shape (REALITY + XTLS Vision over TCP),
// so most rejections mean "your server is a kind we do not support", not
// "you mistyped". Name the unsupported part instead of implying a typo.
// Any Xray share link (VLESS, VMess, Trojan, Hysteria2); the code keeps its
// old name because REST clients match on it. Most rejections mean "your
// server is a kind we do not support", not "you mistyped". Name the
// unsupported part instead of implying a typo.
case "VLESS_CONFIG_INVALID": {
const reason = parsed.params?.reason;
const known = [
@@ -577,6 +578,7 @@ export function translateBackendError(t: TFunction, err: unknown): string {
"fingerprint",
"sni",
"publicKey",
"obfs",
"scheme",
"parameter",
"malformed",
+75 -4
View File
@@ -4,6 +4,8 @@ import {
CREDENTIALS_FIRST_FORMAT,
HOST_FIRST_FORMAT,
isFirstHopEncrypted,
isShareLinkEncrypted,
isXrayProxyType,
pickParsedProxy,
proxyProtocolToken,
resolveAmbiguousProxyLine,
@@ -11,6 +13,11 @@ import {
} from "./proxy-string.ts";
import { canonicalProxyType } from "./proxy-type.ts";
const ID = "6d6e21a1-4829-4d2b-bc7f-1b25707b61e4";
const TLS_LINK = `vless://${ID}@a.example.com:443?security=tls&type=ws`;
const vmess = (fields) =>
`vmess://${Buffer.from(JSON.stringify(fields)).toString("base64")}`;
/**
* The formats themselves are exercised in Rust
* (`proxy_manager::tests::test_proxy_txt_parsing_various_formats`). What is
@@ -207,7 +214,7 @@ test("the TLS-wrapped scheme survives a paste", () => {
test("only the types that actually encrypt the first hop say they do", () => {
// `ss` is deliberately NOT in this list: its answer depends on the cipher,
// and a type-only query carries no cipher, so it cannot claim encryption.
for (const encrypted of ["httpstls", "vless", "HTTPSTLS", "VLESS"]) {
for (const encrypted of ["httpstls", "HTTPSTLS"]) {
assert.equal(isFirstHopEncrypted(encrypted), true, encrypted);
}
assert.equal(isFirstHopEncrypted("ss", "aes-256-gcm"), true);
@@ -241,8 +248,70 @@ test("only the types that actually encrypt the first hop say they do", () => {
}
// An unspecified cipher is not a claim of safety either.
assert.equal(isFirstHopEncrypted("ss", null), false);
// Types whose encryption does not depend on a cipher are unaffected.
assert.equal(isFirstHopEncrypted("vless", null), true);
// An Xray type answers from its share link, never from a cipher.
assert.equal(isFirstHopEncrypted("vless", null, TLS_LINK), true);
assert.equal(isFirstHopEncrypted("vless", "aes-256-gcm"), false);
});
test("an Xray share link decides whether its first hop is encrypted", () => {
const cases = [
// Plain VLESS is the "isolation only" setup, and it is in the clear.
[`vless://${ID}@a.example.com:80?security=none&type=tcp`, false],
[`vless://${ID}@a.example.com:80?type=tcp`, false],
[TLS_LINK, true],
[`vless://${ID}@a.example.com:443?security=REALITY&pbk=k`, true],
// Trojan is TLS unless the link says otherwise.
["trojan://pw@a.example.com:443?peer=a.example.com#x", true],
["trojan://pw@a.example.com:80?security=none", false],
// VMess encrypts with its own cipher even without TLS, unless the cipher
// is one of the two that do not.
[vmess({ add: "a.example.com", port: "443", id: ID, tls: "tls" }), true],
[vmess({ add: "a.example.com", port: "80", id: ID, scy: "auto" }), true],
[vmess({ add: "a.example.com", port: "80", id: ID, scy: "none" }), false],
[vmess({ add: "a.example.com", port: "80", id: ID, scy: "zero" }), false],
[`vmess://${ID}@a.example.com:80?encryption=zero`, false],
[`vmess://${ID}@a.example.com:80?encryption=aes-128-gcm`, true],
// Hysteria2 is QUIC, which is always TLS 1.3.
["hysteria2://pw@a.example.com:443/?sni=a.example.com", true],
["HY2://pw@a.example.com", true],
// A second `?` inside a WebSocket path must not hide a later option.
[
"trojan://pw@a.example.com:80?type=ws&path=/ws?ed=2048&security=none",
false,
],
[`vmess://${ID}@a.example.com:80?path=/v?ed=1&encryption=none`, false],
// VMess JSON values are trimmed, and `encryption` stands in for `scy`.
[vmess({ add: "a.example.com", port: "80", id: ID, scy: " none " }), false],
[
vmess({ add: "a.example.com", port: "80", id: ID, encryption: "none" }),
false,
],
[vmess({ add: "a.example.com", port: "443", id: ID, tls: " TLS " }), true],
// Unreadable is not a claim of safety.
["vmess://not-base64!", false],
["vmess://bnVsbA==", false],
["", false],
["a.example.com:443", false],
];
for (const [link, encrypted] of cases) {
assert.equal(isShareLinkEncrypted(link), encrypted, link);
}
assert.equal(isFirstHopEncrypted("trojan", null, cases[4][0]), true);
assert.equal(isFirstHopEncrypted("vless", null, ""), false);
assert.equal(isFirstHopEncrypted("hy2", null, cases[12][0]), true);
});
test("the Xray types are the ones configured by a share link", () => {
for (const type of ["vless", "VMess", " trojan ", "hysteria2", "hy2"]) {
assert.equal(isXrayProxyType(type), true, type);
}
for (const type of ["ss", "socks5", "http", "", "__proto__"]) {
assert.equal(isXrayProxyType(type), false, type);
}
assert.equal(
splitProxyScheme("hy2://pw@a.example.com").proxyType,
"hysteria2",
);
});
test("a padded stored type answers the same as an unpadded one", () => {
@@ -256,7 +325,7 @@ test("a padded stored type answers the same as an unpadded one", () => {
assert.equal(isFirstHopEncrypted("ss ", "aes-256-gcm"), true);
assert.equal(isFirstHopEncrypted(" shadowsocks", "aes-256-gcm"), true);
assert.equal(isFirstHopEncrypted("\thttpstls\n"), true);
assert.equal(isFirstHopEncrypted(" VLESS "), true);
assert.equal(isFirstHopEncrypted(" VLESS ", null, TLS_LINK), true);
// Padding does not buy encryption either: the cipher still decides, and the
// types that never encrypt still never do.
assert.equal(isFirstHopEncrypted(" ss ", ""), false);
@@ -282,6 +351,8 @@ test("the first-hop answer never disagrees with the canonical type", () => {
" HTTPSTLS ",
"vless",
" VLess ",
"hy2",
" Hysteria2 ",
"http",
" HTTPS ",
"socks5",
+93 -1
View File
@@ -34,8 +34,30 @@ const PROXY_SCHEMES = new Map<string, string>([
["ss", "ss"],
["shadowsocks", "ss"],
["vless", "vless"],
["vmess", "vmess"],
["trojan", "trojan"],
["hysteria2", "hysteria2"],
["hy2", "hysteria2"],
]);
/**
* Types an Xray-core sidecar carries. Their whole configuration is the share
* link stored in `vless_uri`, a name from before VMess, Trojan and Hysteria2
* joined VLESS.
*/
const XRAY_PROXY_TYPES = new Set([
"vless",
"vmess",
"trojan",
"hysteria2",
"hy2",
]);
/** Whether a stored `proxy_type` is configured by a share link. */
export function isXrayProxyType(proxyType: string): boolean {
return XRAY_PROXY_TYPES.has(normalizeProxyType(proxyType));
}
/**
* Protocol identifiers, shortened where the raw stored type would not fit the
* management table's column. Not translated: these are wire protocol names,
@@ -98,7 +120,6 @@ const ENCRYPTED_FIRST_HOP = new Set([
// clear, the same class of wrong claim this feature exists to remove, only
// inverted.
"shadowsocks",
"vless",
]);
/**
@@ -123,8 +144,10 @@ const NULL_CIPHERS = new Set(["none", "plain"]);
export function isFirstHopEncrypted(
proxyType: string,
cipher?: string | null,
shareLink?: string | null,
): boolean {
const type = normalizeProxyType(proxyType);
if (XRAY_PROXY_TYPES.has(type)) return isShareLinkEncrypted(shareLink ?? "");
if (!ENCRYPTED_FIRST_HOP.has(type)) return false;
if (type === "ss" || type === "shadowsocks") {
// Fail closed. An absent cipher is not evidence of encryption, and the
@@ -137,6 +160,75 @@ export function isFirstHopEncrypted(
return true;
}
const NULL_VMESS_CIPHERS = new Set(["none", "zero"]);
/**
* Whether an Xray share link encrypts the hop to its server.
*
* The link decides, not the protocol: plain VLESS (`security=none`) carries
* its payload in the clear, and so does VMess with the `none` or `zero`
* cipher. Hysteria2 always runs over QUIC, which is TLS 1.3. A link this cannot
* read is reported as not encrypted, for the same fail-closed reason as the
* Shadowsocks cipher above.
*/
export function isShareLinkEncrypted(shareLink: string): boolean {
const link = shareLink.trim();
const separator = link.indexOf("://");
if (separator === -1) return false;
const scheme = link.slice(0, separator).toLowerCase();
const body = link.slice(separator + 3).split("#")[0] ?? "";
if (scheme === "hysteria2" || scheme === "hy2") return true;
if (scheme === "vmess" && !body.includes("@")) {
const json = decodeVmessJson(body);
if (!json) return false;
// Read as `uri.rs` reads it: values trimmed, and `encryption` taken as
// the cipher when `scy` is absent.
const field = (key: string) =>
String(json[key] ?? "")
.trim()
.toLowerCase();
const security = field("tls");
if (security === "tls" || security === "reality") return true;
const cipher = field("scy") || field("encryption") || "auto";
return !NULL_VMESS_CIPHERS.has(cipher);
}
// Everything after the FIRST `?` is the query. A WebSocket path such as
// `/ws?ed=2048` may carry a second one, and cutting there hid a later
// `security=none` and called a plaintext link encrypted.
const queryStart = body.indexOf("?");
const query = new URLSearchParams(
queryStart === -1 ? "" : body.slice(queryStart + 1),
);
const defaultSecurity = scheme === "trojan" ? "tls" : "none";
const security = (query.get("security") || defaultSecurity).toLowerCase();
if (security === "tls" || security === "reality") return true;
if (scheme === "vmess") {
const cipher = (query.get("encryption") || "auto").toLowerCase();
return !NULL_VMESS_CIPHERS.has(cipher);
}
return false;
}
/** v2rayN's `vmess://<base64 JSON>` body, or null when it is not one. */
function decodeVmessJson(body: string): Record<string, unknown> | null {
try {
const base64 = decodeURIComponent(body)
.replace(/\s/g, "")
.replace(/-/g, "+")
.replace(/_/g, "/");
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
const json: unknown = JSON.parse(new TextDecoder().decode(bytes));
return json !== null && typeof json === "object" && !Array.isArray(json)
? (json as Record<string, unknown>)
: null;
} catch {
return null;
}
}
export const HOST_FIRST_FORMAT = "host:port:username:password";
export const CREDENTIALS_FIRST_FORMAT = "username:password:host:port";
+38 -3
View File
@@ -28,10 +28,22 @@ test("both Shadowsocks spellings answer as one type", () => {
});
test("every other type keeps its own identity", () => {
for (const type of ["http", "https", "httpstls", "socks4", "socks5", "vless"])
for (const type of [
"http",
"https",
"httpstls",
"socks4",
"socks5",
"vless",
"vmess",
"trojan",
"hysteria2",
])
assert.equal(canonicalProxyType(type), type);
assert.equal(canonicalProxyType("HTTPSTLS"), "httpstls");
assert.equal(canonicalProxyType("VLESS"), "vless");
// Hysteria clients export `hy2://`, and the backend stores it as hysteria2.
assert.equal(canonicalProxyType(" HY2 "), "hysteria2");
// `socks` is not `socks5` to `Url::scheme()`, so folding it here would have
// the UI describe a hop the Rust worker never dials.
assert.equal(canonicalProxyType("socks"), "socks");
@@ -145,6 +157,27 @@ test("Shadowsocks is not filed under a heading that promises encryption", () =>
);
});
test("no Xray type is filed under a heading that promises encryption", () => {
// Until the backend accepts a share link, the note under the Select says the
// link decides. Under "First hop encrypted" that note contradicts the heading,
// the same way an empty Shadowsocks cipher did.
const group = (name) =>
source.match(new RegExp(`const ${name} = \\[([^\\]]*)\\]`))?.[1];
const always = group("ALWAYS_ENCRYPTED_FIRST_HOP_TYPES");
const link = group("LINK_DEPENDENT_FIRST_HOP_TYPES");
assert.ok(always && link, "the link-dependent group must exist");
for (const type of ["vless", "vmess", "trojan", "hysteria2"]) {
assert.ok(
!always.includes(`"${type}"`),
`${type} is under the encrypted heading`,
);
assert.ok(
link.includes(`"${type}"`),
`${type} is missing from the link group`,
);
}
});
test("an empty cipher is described as undecided, and still fails closed", () => {
assert.match(
source,
@@ -177,8 +210,10 @@ test("an empty cipher is described as undecided, and still fails closed", () =>
2,
"cipherUndecided may only be declared and used to pick that one sentence",
);
assert.ok(
stripped.includes("isFirstHopEncrypted(canonicalType, form.username)"),
// The third argument is the share link, which decides for the Xray types.
assert.match(
stripped,
/isFirstHopEncrypted\(\s*canonicalType,\s*form\.username,/,
"the encryption answer must come from the same canonical type the field " +
"labels use, so a padded stored type cannot make them disagree",
);
+4 -1
View File
@@ -34,7 +34,10 @@
* `Url::scheme()`) must stay distinct, or the UI describes a wire that does not
* exist.
*/
const TYPE_ALIASES = new Map<string, string>([["shadowsocks", "ss"]]);
const TYPE_ALIASES = new Map<string, string>([
["shadowsocks", "ss"],
["hy2", "hysteria2"],
]);
/**
* The spelling the UI branches on, for a `proxy_type` that may carry any of