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, "; ") + "." }