feat(reality): add live REALITY target scanner with IP/CIDR discovery

Replace the static reality-targets list with a server-side TLS 1.3 probe that checks TLS 1.3 + HTTP/2 + X25519 + a trusted certificate.

- Single-domain validate auto-fills target and serverNames from the cert SAN
- Discovery scans an IP/CIDR without SNI to find new targets from their certificates, deduped and ranked by feasibility then latency, private-IP guarded via netsafe
- New endpoints scanRealityTarget and scanRealityTargets with RealityScanResult, plus openapigen and api-docs entries
- Add scanner strings to all 13 locales
- Replace deprecated AntD Alert message prop with title across the panel
This commit is contained in:
MHSanaei
2026-06-26 22:18:47 +02:00
parent 451263f1db
commit 6964d84742
36 changed files with 1489 additions and 63 deletions
+25
View File
@@ -78,6 +78,8 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/getNewEchCert", a.getNewEchCert)
g.POST("/getCertHash", a.getCertHash)
g.POST("/getRemoteCertHash", a.getRemoteCertHash)
g.POST("/scanRealityTarget", a.scanRealityTarget)
g.POST("/scanRealityTargets", a.scanRealityTargets)
g.POST("/clientIps", a.setClientIps)
}
@@ -445,6 +447,29 @@ func (a *ServerController) getRemoteCertHash(c *gin.Context) {
jsonObj(c, hashes, nil)
}
// scanRealityTarget runs a live TLS 1.3 probe against the candidate REALITY
// target and returns a structured feasibility verdict plus the cert SAN names.
func (a *ServerController) scanRealityTarget(c *gin.Context) {
res, err := a.serverService.ScanRealityTarget(c.PostForm("target"))
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.scanRealityTargetError"), err)
return
}
jsonObj(c, res, nil)
}
// scanRealityTargets probes a batch of candidate REALITY targets (the supplied
// comma-separated list, or the built-in seed set when empty) and returns each
// verdict ranked by feasibility then latency.
func (a *ServerController) scanRealityTargets(c *gin.Context) {
res, err := a.serverService.ScanRealityTargets(c.PostForm("targets"))
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.scanRealityTargetError"), err)
return
}
jsonObj(c, res, nil)
}
// getNewVlessEnc generates a new VLESS encryption key.
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
out, err := a.serverService.GetNewVlessEnc()
+391
View File
@@ -0,0 +1,391 @@
package service
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
)
const (
realityScanTimeout = 10 * time.Second
realityDiscoverTimeout = 4 * time.Second
realityScanConcurrency = 32
realityDiscoverMaxIPs = 256
realityScanMaxTotal = 512
)
var defaultRealityScanCandidates = []string{
"www.cloudflare.com:443",
"www.microsoft.com:443",
"www.amazon.com:443",
"aws.amazon.com:443",
"www.samsung.com:443",
"www.nvidia.com:443",
"www.amd.com:443",
"www.intel.com:443",
"www.sony.com:443",
"dl.google.com:443",
}
type RealityScanResult struct {
Target string `json:"target" example:"www.cloudflare.com:443"`
Host string `json:"host" example:"www.cloudflare.com"`
IP string `json:"ip" example:"104.16.124.96"`
Port int `json:"port" example:"443"`
Feasible bool `json:"feasible" example:"true"`
TLS13 bool `json:"tls13" example:"true"`
TLSVersion string `json:"tlsVersion" example:"1.3"`
H2 bool `json:"h2" example:"true"`
ALPN string `json:"alpn" example:"h2"`
X25519 bool `json:"x25519" example:"true"`
CurveID string `json:"curveID" example:"X25519"`
CertValid bool `json:"certValid" example:"true"`
CertSubject string `json:"certSubject" example:"cloudflare.com"`
CertIssuer string `json:"certIssuer" example:"Google Trust Services"`
NotAfter string `json:"notAfter" example:"2026-08-01T00:00:00Z"`
ServerNames []string `json:"serverNames"`
LatencyMs int `json:"latencyMs" example:"180"`
Reason string `json:"reason" example:""`
}
type realityProbeTask struct {
dialHost string
port int
sni string
timeout time.Duration
bulk bool
}
func tlsVersionName(v uint16) string {
switch v {
case tls.VersionTLS13:
return "1.3"
case tls.VersionTLS12:
return "1.2"
case tls.VersionTLS11:
return "1.1"
case tls.VersionTLS10:
return "1.0"
default:
return "unknown"
}
}
func realityCurveName(id tls.CurveID) string {
switch id {
case tls.X25519:
return "X25519"
case tls.X25519MLKEM768:
return "X25519MLKEM768"
case tls.CurveP256:
return "P-256"
case tls.CurveP384:
return "P-384"
case tls.CurveP521:
return "P-521"
case 0:
return ""
default:
return fmt.Sprintf("0x%04x", uint16(id))
}
}
func filterUsableSANs(dnsNames []string) []string {
out := make([]string, 0, len(dnsNames))
for _, n := range dnsNames {
n = strings.TrimSpace(n)
if n == "" || strings.HasPrefix(n, "*.") {
continue
}
out = append(out, n)
}
return out
}
func firstUsableName(leaf *x509.Certificate) string {
cn := strings.TrimSpace(leaf.Subject.CommonName)
if cn != "" && !strings.HasPrefix(cn, "*.") {
return cn
}
for _, n := range leaf.DNSNames {
n = strings.TrimSpace(n)
if n != "" && !strings.HasPrefix(n, "*.") {
return n
}
}
return ""
}
func splitRealityTarget(target string) (string, int, error) {
target = strings.TrimSpace(target)
if target == "" {
return "", 0, common.NewError("target is required")
}
host, portStr := target, "443"
if h, p, err := net.SplitHostPort(target); err == nil {
host, portStr = h, p
}
host, err := netsafe.NormalizeHost(host)
if err != nil {
return "", 0, common.NewError("invalid target host: ", err)
}
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
return "", 0, common.NewError("invalid target port")
}
return host, port, nil
}
func incIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}
func enumerateCIDR(cidr string, max int) ([]string, error) {
_, ipnet, err := net.ParseCIDR(strings.TrimSpace(cidr))
if err != nil {
return nil, err
}
ips := make([]string, 0, max)
for ip := ipnet.IP.Mask(ipnet.Mask); ipnet.Contains(ip); incIP(ip) {
ips = append(ips, ip.String())
if len(ips) >= max {
break
}
}
return ips, nil
}
func (s *ServerService) probeRealityAddr(dialHost string, port int, sni string, timeout time.Duration) *RealityScanResult {
addr := net.JoinHostPort(dialHost, strconv.Itoa(port))
res := &RealityScanResult{Port: port}
if net.ParseIP(dialHost) != nil {
res.IP = dialHost
}
if sni != "" {
res.Host = sni
res.Target = net.JoinHostPort(sni, strconv.Itoa(port))
} else {
res.Host = dialHost
res.Target = addr
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
start := time.Now()
conn, err := netsafe.SSRFGuardedDialContext(ctx, "tcp", addr)
if err != nil {
res.Reason = "connection failed: " + err.Error()
return res
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(timeout))
cfg := &tls.Config{
ServerName: sni,
InsecureSkipVerify: true,
NextProtos: []string{"h2", "http/1.1"},
CurvePreferences: []tls.CurveID{tls.X25519, tls.X25519MLKEM768},
MinVersion: tls.VersionTLS12,
}
tlsConn := tls.Client(conn, cfg)
if err := tlsConn.HandshakeContext(ctx); err != nil {
res.Reason = "TLS handshake failed: " + err.Error()
return res
}
res.LatencyMs = int(time.Since(start).Milliseconds())
st := tlsConn.ConnectionState()
res.TLS13 = st.Version == tls.VersionTLS13
res.TLSVersion = tlsVersionName(st.Version)
res.ALPN = st.NegotiatedProtocol
res.H2 = st.NegotiatedProtocol == "h2"
res.CurveID = realityCurveName(st.CurveID)
res.X25519 = st.CurveID == tls.X25519 || st.CurveID == tls.X25519MLKEM768
verifyHost := sni
if len(st.PeerCertificates) > 0 {
leaf := st.PeerCertificates[0]
res.CertSubject = leaf.Subject.CommonName
if res.CertSubject == "" && len(leaf.DNSNames) > 0 {
res.CertSubject = leaf.DNSNames[0]
}
if len(leaf.Issuer.Organization) > 0 {
res.CertIssuer = leaf.Issuer.Organization[0]
} else {
res.CertIssuer = leaf.Issuer.CommonName
}
res.NotAfter = leaf.NotAfter.UTC().Format(time.RFC3339)
res.ServerNames = filterUsableSANs(leaf.DNSNames)
if sni == "" {
if discovered := firstUsableName(leaf); discovered != "" {
res.Host = discovered
res.Target = net.JoinHostPort(discovered, strconv.Itoa(port))
verifyHost = discovered
}
}
if verifyHost != "" {
opts := x509.VerifyOptions{DNSName: verifyHost, Intermediates: x509.NewCertPool()}
for _, c := range st.PeerCertificates[1:] {
opts.Intermediates.AddCert(c)
}
if _, verr := leaf.Verify(opts); verr == nil {
res.CertValid = true
} else {
res.Reason = "certificate not trusted: " + verr.Error()
}
} else {
res.Reason = "no usable domain in certificate"
}
} else {
res.Reason = "no certificate presented"
}
res.Feasible = res.TLS13 && res.H2 && res.X25519 && res.CertValid
if !res.Feasible && res.Reason == "" {
switch {
case !res.TLS13:
res.Reason = "server does not negotiate TLS 1.3"
case !res.H2:
res.Reason = "server does not negotiate HTTP/2 (h2)"
case !res.X25519:
res.Reason = "server did not use X25519 key exchange"
}
}
return res
}
func (s *ServerService) probeRealityTarget(host string, port int) *RealityScanResult {
return s.probeRealityAddr(host, port, host, realityScanTimeout)
}
func (s *ServerService) ScanRealityTarget(target string) (*RealityScanResult, error) {
host, port, err := splitRealityTarget(target)
if err != nil {
return nil, err
}
return s.probeRealityTarget(host, port), nil
}
func (s *ServerService) ScanRealityTargets(targetsCSV string) ([]*RealityScanResult, error) {
var tokens []string
for _, raw := range strings.Split(targetsCSV, ",") {
if t := strings.TrimSpace(raw); t != "" {
tokens = append(tokens, t)
}
}
if len(tokens) == 0 {
tokens = append(tokens, defaultRealityScanCandidates...)
}
var tasks []realityProbeTask
var invalid []*RealityScanResult
for _, token := range tokens {
if len(tasks) >= realityScanMaxTotal {
break
}
if strings.Contains(token, "/") {
ips, err := enumerateCIDR(token, realityDiscoverMaxIPs)
if err != nil {
invalid = append(invalid, &RealityScanResult{Target: token, Reason: "invalid CIDR: " + err.Error()})
continue
}
for _, ip := range ips {
if len(tasks) >= realityScanMaxTotal {
break
}
tasks = append(tasks, realityProbeTask{dialHost: ip, port: 443, timeout: realityDiscoverTimeout, bulk: true})
}
continue
}
host, port, err := splitRealityTarget(token)
if err != nil {
invalid = append(invalid, &RealityScanResult{Target: token, Reason: err.Error()})
continue
}
if net.ParseIP(host) != nil {
tasks = append(tasks, realityProbeTask{dialHost: host, port: port, timeout: realityDiscoverTimeout})
} else {
tasks = append(tasks, realityProbeTask{dialHost: host, port: port, sni: host, timeout: realityScanTimeout})
}
}
probed := make([]*RealityScanResult, len(tasks))
sem := make(chan struct{}, realityScanConcurrency)
var wg sync.WaitGroup
for i, task := range tasks {
wg.Add(1)
sem <- struct{}{}
go func(idx int, tk realityProbeTask) {
defer wg.Done()
defer func() { <-sem }()
r := s.probeRealityAddr(tk.dialHost, tk.port, tk.sni, tk.timeout)
if tk.bulk && r.TLSVersion == "" {
return
}
probed[idx] = r
}(i, task)
}
wg.Wait()
results := dedupRealityResults(append(probed, invalid...))
sortRealityResults(results)
return results, nil
}
func dedupRealityResults(results []*RealityScanResult) []*RealityScanResult {
best := make(map[string]*RealityScanResult)
order := make([]string, 0, len(results))
for _, r := range results {
if r == nil {
continue
}
if ex, ok := best[r.Target]; !ok {
best[r.Target] = r
order = append(order, r.Target)
} else if betterRealityResult(r, ex) {
best[r.Target] = r
}
}
out := make([]*RealityScanResult, 0, len(order))
for _, k := range order {
out = append(out, best[k])
}
return out
}
func betterRealityResult(a, b *RealityScanResult) bool {
if a.Feasible != b.Feasible {
return a.Feasible
}
return a.LatencyMs > 0 && (b.LatencyMs == 0 || a.LatencyMs < b.LatencyMs)
}
func sortRealityResults(results []*RealityScanResult) {
slices.SortStableFunc(results, func(a, b *RealityScanResult) int {
if a.Feasible != b.Feasible {
if a.Feasible {
return -1
}
return 1
}
return a.LatencyMs - b.LatencyMs
})
}
+111
View File
@@ -0,0 +1,111 @@
package service
import (
"crypto/tls"
"testing"
)
func TestTLSVersionName(t *testing.T) {
cases := map[uint16]string{
tls.VersionTLS13: "1.3",
tls.VersionTLS12: "1.2",
tls.VersionTLS11: "1.1",
tls.VersionTLS10: "1.0",
0: "unknown",
}
for in, want := range cases {
if got := tlsVersionName(in); got != want {
t.Errorf("tlsVersionName(%d) = %q, want %q", in, got, want)
}
}
}
func TestRealityCurveName(t *testing.T) {
cases := map[tls.CurveID]string{
tls.X25519: "X25519",
tls.X25519MLKEM768: "X25519MLKEM768",
tls.CurveP256: "P-256",
0: "",
}
for in, want := range cases {
if got := realityCurveName(in); got != want {
t.Errorf("realityCurveName(%d) = %q, want %q", in, got, want)
}
}
}
func TestFilterUsableSANs(t *testing.T) {
got := filterUsableSANs([]string{"example.com", "*.example.com", "", " a.com "})
want := []string{"example.com", "a.com"}
if len(got) != len(want) {
t.Fatalf("filterUsableSANs = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("filterUsableSANs[%d] = %q, want %q", i, got[i], want[i])
}
}
}
func TestSplitRealityTarget(t *testing.T) {
okCases := []struct {
in string
wantHost string
wantPort int
}{
{"example.com", "example.com", 443},
{"example.com:8443", "example.com", 8443},
{"1.1.1.1:443", "1.1.1.1", 443},
}
for _, c := range okCases {
host, port, err := splitRealityTarget(c.in)
if err != nil {
t.Errorf("splitRealityTarget(%q) unexpected error: %v", c.in, err)
continue
}
if host != c.wantHost || port != c.wantPort {
t.Errorf("splitRealityTarget(%q) = (%q, %d), want (%q, %d)", c.in, host, port, c.wantHost, c.wantPort)
}
}
badCases := []string{"", " ", "example.com:0", "example.com:70000", "bad host!"}
for _, in := range badCases {
if _, _, err := splitRealityTarget(in); err == nil {
t.Errorf("splitRealityTarget(%q) expected error, got nil", in)
}
}
}
func TestScanRealityTargetInputValidation(t *testing.T) {
if _, err := (&ServerService{}).ScanRealityTarget(""); err == nil {
t.Error("ScanRealityTarget(empty) expected error, got nil")
}
}
func TestScanRealityTargetBlocksPrivate(t *testing.T) {
res, err := (&ServerService{}).ScanRealityTarget("127.0.0.1:443")
if err != nil {
t.Fatalf("ScanRealityTarget(loopback) unexpected error: %v", err)
}
if res.Feasible {
t.Error("ScanRealityTarget(loopback) should not be feasible")
}
if res.Reason == "" {
t.Error("ScanRealityTarget(loopback) should set a reason")
}
}
func TestScanRealityTargetsHandlesPrivateAndBadInput(t *testing.T) {
results, err := (&ServerService{}).ScanRealityTargets("127.0.0.1:443,10.0.0.1:443,bad host!")
if err != nil {
t.Fatalf("ScanRealityTargets unexpected error: %v", err)
}
if len(results) != 3 {
t.Fatalf("ScanRealityTargets returned %d results, want 3", len(results))
}
for _, r := range results {
if r.Feasible {
t.Errorf("result %q unexpectedly feasible", r.Target)
}
}
}
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "حدث خطأ أثناء الحصول على شهادة X25519.",
"getNewmldsa65Error": "حدث خطاء في الحصول على mldsa65.",
"getNewVlessEncError": "حدث خطأ أثناء الحصول على VlessEnc.",
"scanRealityTargetError": "فشل فحص هدف REALITY.",
"scanRealityTargetFeasible": "الهدف مناسب — تم ملء الهدف وSNI.",
"scanRealityTargetNotFeasible": "الهدف قابل للوصول لكنه غير مناسب لـ REALITY.",
"invalidClientField": "العميل {client}: الحقل {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} أخرى)"
@@ -623,6 +626,20 @@
"realityTargetRequired": "هدف REALITY مطلوب",
"realityTargetNeedsPort": "يجب أن يتضمّن هدف REALITY منفذًا (مثل example.com:443)",
"realityTargetInvalidPort": "هدف REALITY يحتوي على منفذ غير صالح",
"scan": "فحص",
"findTargets": "البحث عن أهداف",
"scanModalTitle": "ماسح أهداف REALITY",
"scanModalDesc": "تحقق من نطاق، أو افحص نطاق IP / CIDR لاكتشاف أهداف REALITY جديدة من شهاداتها. اترك الحقل فارغًا لفحص المرشحين الشائعين.",
"scanDiscoverPlaceholder": "IP أو CIDR أو نطاق — اتركه فارغًا للمرشحين الشائعين",
"scanStatus": "الحالة",
"scanFeasible": "مناسب",
"scanNotFeasible": "غير مناسب",
"scanCurve": "تبادل المفاتيح",
"scanCert": "الشهادة",
"scanCertInvalid": "غير موثوق",
"scanLatency": "زمن الاستجابة",
"scanUse": "استخدام",
"scanRescan": "إعادة الفحص",
"spiderX": "SpiderX",
"getNewCert": "احصل على شهادة جديدة",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "Error while obtaining the X25519 certificate.",
"getNewmldsa65Error": "Error while obtaining mldsa65.",
"getNewVlessEncError": "Error while obtaining VlessEnc.",
"scanRealityTargetError": "Failed to scan REALITY target.",
"scanRealityTargetFeasible": "Target is feasible — filled target and SNI.",
"scanRealityTargetNotFeasible": "Target is reachable but not feasible for REALITY.",
"invalidClientField": "Client {client}: {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} more)"
@@ -635,6 +638,20 @@
"realityTargetRequired": "REALITY target is required",
"realityTargetNeedsPort": "REALITY target must include a port (e.g. example.com:443)",
"realityTargetInvalidPort": "REALITY target has an invalid port",
"scan": "Scan",
"findTargets": "Find Targets",
"scanModalTitle": "REALITY Target Scanner",
"scanModalDesc": "Validate a domain, or scan an IP / CIDR range to discover new REALITY targets from their certificates. Leave the box empty to probe common candidates.",
"scanDiscoverPlaceholder": "IP, CIDR, or domain — leave empty for common candidates",
"scanStatus": "Status",
"scanFeasible": "Feasible",
"scanNotFeasible": "Not feasible",
"scanCurve": "Key Exchange",
"scanCert": "Certificate",
"scanCertInvalid": "Not trusted",
"scanLatency": "Latency",
"scanUse": "Use",
"scanRescan": "Rescan",
"spiderX": "SpiderX",
"getNewCert": "Get New Cert",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "Error al obtener el certificado X25519.",
"getNewmldsa65Error": "Error al obtener el certificado mldsa65.",
"getNewVlessEncError": "Error al obtener el certificado VlessEnc.",
"scanRealityTargetError": "No se pudo escanear el objetivo REALITY.",
"scanRealityTargetFeasible": "El objetivo es apto: se rellenaron el objetivo y el SNI.",
"scanRealityTargetNotFeasible": "El objetivo es accesible pero no apto para REALITY.",
"invalidClientField": "Cliente {client}: campo {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} más)"
@@ -644,6 +647,20 @@
"realityTargetRequired": "El destino REALITY es obligatorio",
"realityTargetNeedsPort": "El destino REALITY debe incluir un puerto (p. ej. example.com:443)",
"realityTargetInvalidPort": "El destino REALITY tiene un puerto no válido",
"scan": "Escanear",
"findTargets": "Buscar objetivos",
"scanModalTitle": "Escáner de objetivos REALITY",
"scanModalDesc": "Valida un dominio o escanea un rango IP / CIDR para descubrir nuevos objetivos REALITY a partir de sus certificados. Deja el campo vacío para probar los candidatos comunes.",
"scanDiscoverPlaceholder": "IP, CIDR o dominio — déjalo vacío para candidatos comunes",
"scanStatus": "Estado",
"scanFeasible": "Apto",
"scanNotFeasible": "No apto",
"scanCurve": "Intercambio de claves",
"scanCert": "Certificado",
"scanCertInvalid": "No confiable",
"scanLatency": "Latencia",
"scanUse": "Usar",
"scanRescan": "Reescanear",
"spiderX": "SpiderX",
"getNewCert": "Obtener nuevo cert",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "خطا در دریافت گواهی X25519.",
"getNewmldsa65Error": "خطا در دریافت گواهی mldsa65.",
"getNewVlessEncError": "خطا در دریافت گواهی VlessEnc.",
"scanRealityTargetError": "اسکن هدف REALITY ناموفق بود.",
"scanRealityTargetFeasible": "هدف مناسب است — هدف و SNI پر شد.",
"scanRealityTargetNotFeasible": "هدف در دسترس است اما برای REALITY مناسب نیست.",
"invalidClientField": "کلاینت {client}: فیلد {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} مورد دیگر)"
@@ -635,6 +638,20 @@
"realityTargetRequired": "هدف REALITY الزامی است",
"realityTargetNeedsPort": "هدف REALITY باید شامل پورت باشد (مثلاً example.com:443)",
"realityTargetInvalidPort": "پورت هدف REALITY نامعتبر است",
"scan": "اسکن",
"findTargets": "یافتن هدف‌ها",
"scanModalTitle": "اسکنر هدف REALITY",
"scanModalDesc": "یک دامنه را اعتبارسنجی کنید، یا یک محدوده‌ی IP/CIDR را اسکن کنید تا هدف‌های جدید REALITY از روی گواهی‌هایشان کشف شوند. برای بررسی کاندیدهای پیش‌فرض، کادر را خالی بگذارید.",
"scanDiscoverPlaceholder": "آی‌پی، CIDR یا دامنه — برای کاندیدهای پیش‌فرض خالی بگذارید",
"scanStatus": "وضعیت",
"scanFeasible": "مناسب",
"scanNotFeasible": "نامناسب",
"scanCurve": "تبادل کلید",
"scanCert": "گواهی",
"scanCertInvalid": "نامعتبر",
"scanLatency": "تأخیر",
"scanUse": "استفاده",
"scanRescan": "اسکن مجدد",
"spiderX": "SpiderX",
"getNewCert": "دریافت گواهی جدید",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "Terjadi kesalahan saat mendapatkan sertifikat X25519.",
"getNewmldsa65Error": "Terjadi kesalahan saat mendapatkan sertifikat mldsa65.",
"getNewVlessEncError": "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc.",
"scanRealityTargetError": "Gagal memindai target REALITY.",
"scanRealityTargetFeasible": "Target layak — target dan SNI terisi.",
"scanRealityTargetNotFeasible": "Target dapat dijangkau tetapi tidak layak untuk REALITY.",
"invalidClientField": "Klien {client}: kolom {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} lainnya)"
@@ -623,6 +626,20 @@
"realityTargetRequired": "Target REALITY wajib diisi",
"realityTargetNeedsPort": "Target REALITY harus menyertakan port (mis. example.com:443)",
"realityTargetInvalidPort": "Target REALITY memiliki port yang tidak valid",
"scan": "Pindai",
"findTargets": "Cari Target",
"scanModalTitle": "Pemindai Target REALITY",
"scanModalDesc": "Validasi domain, atau pindai rentang IP / CIDR untuk menemukan target REALITY baru dari sertifikatnya. Biarkan kosong untuk memeriksa kandidat umum.",
"scanDiscoverPlaceholder": "IP, CIDR, atau domain — kosongkan untuk kandidat umum",
"scanStatus": "Status",
"scanFeasible": "Layak",
"scanNotFeasible": "Tidak layak",
"scanCurve": "Pertukaran Kunci",
"scanCert": "Sertifikat",
"scanCertInvalid": "Tidak tepercaya",
"scanLatency": "Latensi",
"scanUse": "Gunakan",
"scanRescan": "Pindai ulang",
"spiderX": "SpiderX",
"getNewCert": "Dapatkan sertifikat baru",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "X25519証明書の取得中にエラーが発生しました。",
"getNewmldsa65Error": "mldsa65証明書の取得中にエラーが発生しました。",
"getNewVlessEncError": "VlessEnc証明書の取得中にエラーが発生しました。",
"scanRealityTargetError": "REALITY ターゲットのスキャンに失敗しました。",
"scanRealityTargetFeasible": "ターゲットは利用可能です — ターゲットと SNI を入力しました。",
"scanRealityTargetNotFeasible": "ターゲットには到達できますが、REALITY には利用できません。",
"invalidClientField": "クライアント {client}: フィールド {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (他 {count} 件)"
@@ -644,6 +647,20 @@
"realityTargetRequired": "REALITY ターゲットは必須です",
"realityTargetNeedsPort": "REALITY ターゲットにはポートを含める必要があります(例: example.com:443",
"realityTargetInvalidPort": "REALITY ターゲットのポートが無効です",
"scan": "スキャン",
"findTargets": "ターゲットを検索",
"scanModalTitle": "REALITY ターゲットスキャナー",
"scanModalDesc": "ドメインを検証するか、IP / CIDR 範囲をスキャンして証明書から新しい REALITY ターゲットを発見します。空欄のままにすると一般的な候補を検査します。",
"scanDiscoverPlaceholder": "IP、CIDR、またはドメイン — 空欄で一般的な候補",
"scanStatus": "ステータス",
"scanFeasible": "利用可能",
"scanNotFeasible": "利用不可",
"scanCurve": "鍵交換",
"scanCert": "証明書",
"scanCertInvalid": "信頼できません",
"scanLatency": "レイテンシ",
"scanUse": "使用",
"scanRescan": "再スキャン",
"spiderX": "SpiderX",
"getNewCert": "新しい証明書を取得",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "Erro ao obter o certificado X25519.",
"getNewmldsa65Error": "Erro ao obter o certificado mldsa65.",
"getNewVlessEncError": "Erro ao obter o certificado VlessEnc.",
"scanRealityTargetError": "Falha ao escanear o alvo REALITY.",
"scanRealityTargetFeasible": "O alvo é viável — alvo e SNI preenchidos.",
"scanRealityTargetNotFeasible": "O alvo é acessível, mas não é viável para REALITY.",
"invalidClientField": "Cliente {client}: campo {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} mais)"
@@ -644,6 +647,20 @@
"realityTargetRequired": "O alvo REALITY é obrigatório",
"realityTargetNeedsPort": "O alvo REALITY deve incluir uma porta (ex.: example.com:443)",
"realityTargetInvalidPort": "O alvo REALITY tem uma porta inválida",
"scan": "Escanear",
"findTargets": "Buscar alvos",
"scanModalTitle": "Scanner de alvos REALITY",
"scanModalDesc": "Valide um domínio ou escaneie um intervalo IP / CIDR para descobrir novos alvos REALITY a partir dos certificados. Deixe vazio para testar os candidatos comuns.",
"scanDiscoverPlaceholder": "IP, CIDR ou domínio — deixe vazio para candidatos comuns",
"scanStatus": "Status",
"scanFeasible": "Viável",
"scanNotFeasible": "Inviável",
"scanCurve": "Troca de chaves",
"scanCert": "Certificado",
"scanCertInvalid": "Não confiável",
"scanLatency": "Latência",
"scanUse": "Usar",
"scanRescan": "Reescanear",
"spiderX": "SpiderX",
"getNewCert": "Obter novo certificado",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "Ошибка при получении сертификата X25519.",
"getNewmldsa65Error": "Ошибка при получении сертификата mldsa65.",
"getNewVlessEncError": "Ошибка при получении сертификата VlessEnc.",
"scanRealityTargetError": "Не удалось просканировать цель REALITY.",
"scanRealityTargetFeasible": "Цель подходит — поля target и SNI заполнены.",
"scanRealityTargetNotFeasible": "Цель доступна, но не подходит для REALITY.",
"invalidClientField": "Клиент {client}: поле {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} ещё)"
@@ -644,6 +647,20 @@
"realityTargetRequired": "Цель REALITY обязательна",
"realityTargetNeedsPort": "Цель REALITY должна содержать порт (например, example.com:443)",
"realityTargetInvalidPort": "У цели REALITY указан недопустимый порт",
"scan": "Сканировать",
"findTargets": "Найти цели",
"scanModalTitle": "Сканер целей REALITY",
"scanModalDesc": "Проверьте домен или просканируйте диапазон IP / CIDR, чтобы обнаружить новые цели REALITY по их сертификатам. Оставьте поле пустым для проверки обычных кандидатов.",
"scanDiscoverPlaceholder": "IP, CIDR или домен — пусто для обычных кандидатов",
"scanStatus": "Статус",
"scanFeasible": "Подходит",
"scanNotFeasible": "Не подходит",
"scanCurve": "Обмен ключами",
"scanCert": "Сертификат",
"scanCertInvalid": "Не доверенный",
"scanLatency": "Задержка",
"scanUse": "Выбрать",
"scanRescan": "Пересканировать",
"spiderX": "SpiderX",
"getNewCert": "Получить новый сертификат",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "X25519 sertifikası alınırken hata oluştu.",
"getNewmldsa65Error": "mldsa65 sertifikası alınırken hata oluştu.",
"getNewVlessEncError": "VlessEnc sertifikası alınırken hata oluştu.",
"scanRealityTargetError": "REALITY hedefi taranamadı.",
"scanRealityTargetFeasible": "Hedef uygun — hedef ve SNI dolduruldu.",
"scanRealityTargetNotFeasible": "Hedefe ulaşılabiliyor ancak REALITY için uygun değil.",
"invalidClientField": "Kullanıcı {client}: {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} tane daha)"
@@ -623,6 +626,20 @@
"realityTargetRequired": "REALITY hedefi zorunludur",
"realityTargetNeedsPort": "REALITY hedefi bir port içermelidir (ör. example.com:443)",
"realityTargetInvalidPort": "REALITY hedefinde geçersiz bir port var",
"scan": "Tara",
"findTargets": "Hedef bul",
"scanModalTitle": "REALITY Hedef Tarayıcı",
"scanModalDesc": "Bir alan adını doğrulayın veya sertifikalarından yeni REALITY hedefleri keşfetmek için bir IP / CIDR aralığını tarayın. Yaygın adayları taramak için kutuyu boş bırakın.",
"scanDiscoverPlaceholder": "IP, CIDR veya alan adı — yaygın adaylar için boş bırakın",
"scanStatus": "Durum",
"scanFeasible": "Uygun",
"scanNotFeasible": "Uygun değil",
"scanCurve": "Anahtar Değişimi",
"scanCert": "Sertifika",
"scanCertInvalid": "Güvenilmez",
"scanLatency": "Gecikme",
"scanUse": "Kullan",
"scanRescan": "Yeniden tara",
"spiderX": "SpiderX",
"getNewCert": "Yeni Sertifika Al",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "Помилка при отриманні сертифіката X25519.",
"getNewmldsa65Error": "Помилка при отриманні сертифіката mldsa65.",
"getNewVlessEncError": "Помилка при отриманні сертифіката VlessEnc.",
"scanRealityTargetError": "Не вдалося просканувати ціль REALITY.",
"scanRealityTargetFeasible": "Ціль підходить — поля target і SNI заповнено.",
"scanRealityTargetNotFeasible": "Ціль доступна, але не підходить для REALITY.",
"invalidClientField": "Клієнт {client}: поле {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} ще)"
@@ -623,6 +626,20 @@
"realityTargetRequired": "Ціль REALITY обов'язкова",
"realityTargetNeedsPort": "Ціль REALITY має містити порт (напр., example.com:443)",
"realityTargetInvalidPort": "Ціль REALITY має недійсний порт",
"scan": "Сканувати",
"findTargets": "Знайти цілі",
"scanModalTitle": "Сканер цілей REALITY",
"scanModalDesc": "Перевірте домен або проскануйте діапазон IP / CIDR, щоб виявити нові цілі REALITY за їхніми сертифікатами. Залиште поле порожнім для перевірки звичайних кандидатів.",
"scanDiscoverPlaceholder": "IP, CIDR або домен — порожнє для звичайних кандидатів",
"scanStatus": "Статус",
"scanFeasible": "Підходить",
"scanNotFeasible": "Не підходить",
"scanCurve": "Обмін ключами",
"scanCert": "Сертифікат",
"scanCertInvalid": "Ненадійний",
"scanLatency": "Затримка",
"scanUse": "Обрати",
"scanRescan": "Пересканувати",
"spiderX": "SpiderX",
"getNewCert": "Отримати новий сертифікат",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "Lỗi khi lấy chứng chỉ X25519.",
"getNewmldsa65Error": "Lỗi khi lấy chứng chỉ mldsa65.",
"getNewVlessEncError": "Lỗi khi lấy chứng chỉ VlessEnc.",
"scanRealityTargetError": "Quét mục tiêu REALITY thất bại.",
"scanRealityTargetFeasible": "Mục tiêu khả dụng — đã điền mục tiêu và SNI.",
"scanRealityTargetNotFeasible": "Mục tiêu có thể truy cập nhưng không khả dụng cho REALITY.",
"invalidClientField": "Khách hàng {client}: trường {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (+{count} lỗi khác)"
@@ -644,6 +647,20 @@
"realityTargetRequired": "Mục tiêu REALITY là bắt buộc",
"realityTargetNeedsPort": "Mục tiêu REALITY phải bao gồm cổng (ví dụ example.com:443)",
"realityTargetInvalidPort": "Mục tiêu REALITY có cổng không hợp lệ",
"scan": "Quét",
"findTargets": "Tìm mục tiêu",
"scanModalTitle": "Trình quét mục tiêu REALITY",
"scanModalDesc": "Xác thực một tên miền, hoặc quét một dải IP / CIDR để khám phá các mục tiêu REALITY mới từ chứng chỉ của chúng. Để trống để quét các ứng viên phổ biến.",
"scanDiscoverPlaceholder": "IP, CIDR hoặc tên miền — để trống cho ứng viên phổ biến",
"scanStatus": "Trạng thái",
"scanFeasible": "Khả dụng",
"scanNotFeasible": "Không khả dụng",
"scanCurve": "Trao đổi khóa",
"scanCert": "Chứng chỉ",
"scanCertInvalid": "Không tin cậy",
"scanLatency": "Độ trễ",
"scanUse": "Dùng",
"scanRescan": "Quét lại",
"spiderX": "SpiderX",
"getNewCert": "Lấy chứng chỉ mới",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "获取X25519证书时出错。",
"getNewmldsa65Error": "获取mldsa65证书时出错。",
"getNewVlessEncError": "获取VlessEnc证书时出错。",
"scanRealityTargetError": "扫描 REALITY 目标失败。",
"scanRealityTargetFeasible": "目标可用 — 已填入目标和 SNI。",
"scanRealityTargetNotFeasible": "目标可达,但不适用于 REALITY。",
"invalidClientField": "客户端 {client}:字段 {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (另有 {count} 项)"
@@ -643,6 +646,20 @@
"realityTargetRequired": "REALITY 目标为必填项",
"realityTargetNeedsPort": "REALITY 目标必须包含端口(例如 example.com:443",
"realityTargetInvalidPort": "REALITY 目标的端口无效",
"scan": "扫描",
"findTargets": "查找目标",
"scanModalTitle": "REALITY 目标扫描器",
"scanModalDesc": "验证某个域名,或扫描 IP / CIDR 范围,从证书中发现新的 REALITY 目标。留空则探测常用候选。",
"scanDiscoverPlaceholder": "IP、CIDR 或域名 — 留空使用常用候选",
"scanStatus": "状态",
"scanFeasible": "可用",
"scanNotFeasible": "不可用",
"scanCurve": "密钥交换",
"scanCert": "证书",
"scanCertInvalid": "不受信任",
"scanLatency": "延迟",
"scanUse": "使用",
"scanRescan": "重新扫描",
"spiderX": "SpiderX",
"getNewCert": "获取新证书",
"mldsa65Seed": "mldsa65 Seed",
+17
View File
@@ -474,6 +474,9 @@
"getNewX25519CertError": "取得X25519憑證時發生錯誤。",
"getNewmldsa65Error": "取得mldsa65憑證時發生錯誤。",
"getNewVlessEncError": "取得VlessEnc憑證時發生錯誤。",
"scanRealityTargetError": "掃描 REALITY 目標失敗。",
"scanRealityTargetFeasible": "目標可用 — 已填入目標與 SNI。",
"scanRealityTargetNotFeasible": "目標可達,但不適用於 REALITY。",
"invalidClientField": "用戶端 {client}:欄位 {field} — {reason}",
"invalidField": "{field} — {reason}",
"moreIssues": "{message} (另有 {count} 項)"
@@ -623,6 +626,20 @@
"realityTargetRequired": "REALITY 目標為必填項",
"realityTargetNeedsPort": "REALITY 目標必須包含連接埠(例如 example.com:443",
"realityTargetInvalidPort": "REALITY 目標的連接埠無效",
"scan": "掃描",
"findTargets": "尋找目標",
"scanModalTitle": "REALITY 目標掃描器",
"scanModalDesc": "驗證某個網域,或掃描 IP / CIDR 範圍,從憑證中探索新的 REALITY 目標。留空則探測常用候選。",
"scanDiscoverPlaceholder": "IP、CIDR 或網域 — 留空使用常用候選",
"scanStatus": "狀態",
"scanFeasible": "可用",
"scanNotFeasible": "不可用",
"scanCurve": "金鑰交換",
"scanCert": "憑證",
"scanCertInvalid": "不受信任",
"scanLatency": "延遲",
"scanUse": "使用",
"scanRescan": "重新掃描",
"spiderX": "SpiderX",
"getNewCert": "取得新憑證",
"mldsa65Seed": "mldsa65 Seed",