238 lines
9.1 KiB
Go
238 lines
9.1 KiB
Go
package checker
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// socks5ReplyHints maps SOCKS5 REP codes to short Russian explanations
|
|
// used by hintFor for the "connect" and "udp" steps. Codes outside this
|
|
// table fall back to a generic "unknown REP" message.
|
|
var socks5ReplyHints = map[byte]string{
|
|
0x01: "общий сбой SOCKS5-сервера",
|
|
0x02: "правила прокси запрещают это соединение",
|
|
0x03: "сеть назначения недоступна",
|
|
0x04: "хост назначения недоступен",
|
|
0x05: "connection refused",
|
|
0x06: "истёк TTL",
|
|
0x07: "команда не поддерживается",
|
|
0x08: "тип адреса не поддерживается",
|
|
}
|
|
|
|
// tcpFriendlyName turns a testID into a Russian-friendly label for the
|
|
// generic fallback hint.
|
|
func tcpFriendlyName(testID string) string {
|
|
switch testID {
|
|
case "tcp":
|
|
return "TCP"
|
|
case "greet":
|
|
return "приветствие SOCKS5"
|
|
case "auth":
|
|
return "авторизация SOCKS5"
|
|
case "connect":
|
|
return "TCP-туннель к Discord"
|
|
case "udp":
|
|
return "UDP ASSOCIATE"
|
|
case "voice-quality":
|
|
return "качество UDP-канала"
|
|
case "api":
|
|
return "Discord API"
|
|
default:
|
|
return testID
|
|
}
|
|
}
|
|
|
|
// hintFor returns a short Russian-language explanation of why a test
|
|
// failed. Returns "" when err is nil.
|
|
func hintFor(testID string, err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
if isContextErr(err) {
|
|
return "Проверка отменена."
|
|
}
|
|
|
|
// Common error shapes we recognise across all testIDs.
|
|
var ne net.Error
|
|
isTimeout := errors.As(err, &ne) && ne.Timeout()
|
|
|
|
var rep ErrSocks5Reply
|
|
hasReply := errors.As(err, &rep)
|
|
|
|
switch testID {
|
|
case "tcp":
|
|
switch {
|
|
case isTimeout:
|
|
return "Превышен таймаут подключения — прокси может быть выключен или брандмауэр режет порт."
|
|
case errors.Is(err, syscall.ECONNREFUSED):
|
|
return "Прокси отклонил TCP-соединение — порт закрыт или сервис не запущен."
|
|
}
|
|
return fmt.Sprintf("Прокси не отвечает по TCP — проверь host и port (%s).", err.Error())
|
|
|
|
case "greet":
|
|
switch {
|
|
case errors.Is(err, ErrSocks5BadVersion):
|
|
return "Сервер вернул не SOCKS5 — возможно, это HTTP-прокси."
|
|
case errors.Is(err, ErrSocks5RejectedAllAuth):
|
|
return "Прокси требует авторизацию, но мы её не предложили (или прокси не принимает наши методы)."
|
|
case errors.Is(err, ErrShortReply):
|
|
return "SOCKS5-сервер прислал укороченный ответ на приветствие."
|
|
case isTimeout:
|
|
return "SOCKS5-сервер не ответил на приветствие вовремя."
|
|
}
|
|
return genericFallback(testID, err)
|
|
|
|
case "auth":
|
|
switch {
|
|
case errors.Is(err, ErrAuthRejected):
|
|
return "Логин или пароль неверны."
|
|
case errors.Is(err, ErrCredentialTooLong):
|
|
return "Логин или пароль длиннее 255 байт — SOCKS5 такого не позволяет."
|
|
case errors.Is(err, ErrShortReply):
|
|
return "SOCKS5-сервер прислал укороченный ответ на авторизацию."
|
|
case isTimeout:
|
|
return "SOCKS5-сервер не ответил на авторизацию вовремя."
|
|
}
|
|
return genericFallback(testID, err)
|
|
|
|
case "connect":
|
|
if hasReply {
|
|
return socks5ReplyHint("connect", rep.Code)
|
|
}
|
|
switch {
|
|
case errors.Is(err, ErrHostTooLong):
|
|
return "Имя хоста длиннее 255 байт — SOCKS5 такого не позволяет."
|
|
case errors.Is(err, ErrShortReply):
|
|
return "SOCKS5-сервер прислал укороченный ответ на CONNECT."
|
|
case isTimeout:
|
|
return "SOCKS5-сервер не ответил на CONNECT вовремя."
|
|
}
|
|
return genericFallback(testID, err)
|
|
|
|
case "udp":
|
|
if hasReply {
|
|
return socks5ReplyHint("udp", rep.Code)
|
|
}
|
|
switch {
|
|
case errors.Is(err, ErrUnsupportedRelayATYP):
|
|
return "Прокси выдал IPv6 relay для UDP — пока не поддерживается, голос работать не будет."
|
|
case errors.Is(err, ErrShortReply):
|
|
return "SOCKS5-сервер прислал укороченный ответ на UDP ASSOCIATE."
|
|
case isTimeout:
|
|
return "SOCKS5-сервер не ответил на UDP ASSOCIATE вовремя."
|
|
}
|
|
return genericFallback(testID, err)
|
|
|
|
case "voice-quality":
|
|
switch {
|
|
case errors.Is(err, ErrSTUNNoMappedAddress):
|
|
return "STUN-ответ без XOR-MAPPED-ADDRESS — UDP-релей не пропускает обратный трафик."
|
|
case errors.Is(err, ErrSTUNTooShort),
|
|
errors.Is(err, ErrSTUNBadMagicCookie),
|
|
errors.Is(err, ErrSTUNNotSuccess),
|
|
errors.Is(err, ErrSTUNTxIDMismatch),
|
|
errors.Is(err, ErrSTUNUnsupportedFamily):
|
|
return "STUN-релей возвращает мусор — голос работать не будет."
|
|
case isTimeout:
|
|
return "STUN-релей не отвечает — UDP через прокси сильно теряет пакеты."
|
|
}
|
|
var dnsErr *net.DNSError
|
|
if errors.As(err, &dnsErr) {
|
|
return "Не удалось разрезолвить STUN-сервер — проверь системный DNS."
|
|
}
|
|
return genericFallback(testID, err)
|
|
|
|
case "api":
|
|
switch {
|
|
case isTimeout:
|
|
return "Discord API не ответил вовремя через прокси — таймаут."
|
|
}
|
|
return fmt.Sprintf("Discord API недоступен через прокси — TLS handshake упал (%s).", err.Error())
|
|
}
|
|
|
|
return genericFallback(testID, err)
|
|
}
|
|
|
|
// socks5ReplyHint formats a SOCKS5 REP-code hint specialised by step.
|
|
// "connect" wording references Discord; "udp" wording references voice.
|
|
func socks5ReplyHint(step string, code byte) string {
|
|
desc, ok := socks5ReplyHints[code]
|
|
if !ok {
|
|
desc = "неизвестная REP"
|
|
}
|
|
switch step {
|
|
case "udp":
|
|
// 0x07 (cmd not supported) is the headline UDP failure mode.
|
|
if code == 0x07 {
|
|
return "Прокси не поддерживает UDP ASSOCIATE — голос Discord работать не будет."
|
|
}
|
|
return fmt.Sprintf("Прокси отклонил UDP ASSOCIATE (REP=%02X, %s).", code, desc)
|
|
case "connect":
|
|
if code == 0x05 {
|
|
return "Прокси не смог подключиться к Discord (REP=05, connection refused)."
|
|
}
|
|
if code == 0x07 {
|
|
return "Прокси не поддерживает CONNECT (REP=07)."
|
|
}
|
|
return fmt.Sprintf("Прокси отклонил CONNECT к Discord (REP=%02X, %s).", code, desc)
|
|
}
|
|
return fmt.Sprintf("Прокси отклонил запрос (REP=%02X, %s).", code, desc)
|
|
}
|
|
|
|
// genericFallback is the catch-all used when we don't recognise the
|
|
// (testID, err) shape. Keeps the user informed without exposing raw Go
|
|
// error wrapping.
|
|
func genericFallback(testID string, err error) string {
|
|
return fmt.Sprintf("Не удалось выполнить шаг «%s»: %s", tcpFriendlyName(testID), err.Error())
|
|
}
|
|
|
|
// voiceQualityWarnHint composes a warn-tier hint based on which threshold
|
|
// was violated. Thresholds match runVoiceQuality's warn band: loss>5,
|
|
// jitter>30, p50>250. Always returns non-empty.
|
|
func voiceQualityWarnHint(loss, jitter, p50 float64) string {
|
|
parts := make([]string, 0, 3)
|
|
if loss > 5.0 {
|
|
parts = append(parts, fmt.Sprintf("Потери UDP %.0f%% — голос будет с заиканиями", loss))
|
|
}
|
|
if jitter > 30.0 {
|
|
parts = append(parts, fmt.Sprintf("большой джиттер %.1fms — звук будет дёргаться", jitter))
|
|
}
|
|
if p50 > 250.0 {
|
|
parts = append(parts, fmt.Sprintf("высокая задержка %.0fms — заметная рассинхронизация при разговоре", p50))
|
|
}
|
|
if len(parts) == 0 {
|
|
// Shouldn't happen — caller only invokes us in the warn band.
|
|
return "UDP-канал на грани приемлемого — возможны помехи в голосе."
|
|
}
|
|
return strings.Join(parts, "; ") + "."
|
|
}
|
|
|
|
// voiceQualityFailHint composes a fail-tier hint. p95 is informational —
|
|
// included only when notably worse than p50.
|
|
func voiceQualityFailHint(loss, jitter, p50, p95 float64) string {
|
|
_ = p95
|
|
parts := make([]string, 0, 3)
|
|
if loss > 15.0 {
|
|
parts = append(parts, fmt.Sprintf("Потери UDP %.0f%% — голос работать не будет", loss))
|
|
} else if loss > 5.0 {
|
|
parts = append(parts, fmt.Sprintf("Потери UDP %.0f%%", loss))
|
|
}
|
|
if jitter > 60.0 {
|
|
parts = append(parts, fmt.Sprintf("джиттер %.1fms — звук развалится", jitter))
|
|
} else if jitter > 30.0 {
|
|
parts = append(parts, fmt.Sprintf("джиттер %.1fms", jitter))
|
|
}
|
|
if p50 > 400.0 {
|
|
parts = append(parts, fmt.Sprintf("задержка %.0fms — голос идёт со значительной паузой", p50))
|
|
} else if p50 > 250.0 {
|
|
parts = append(parts, fmt.Sprintf("задержка %.0fms", p50))
|
|
}
|
|
if len(parts) == 0 {
|
|
return "UDP-канал не пригоден для голоса."
|
|
}
|
|
return strings.Join(parts, "; ") + "."
|
|
}
|