Files
drover-go/internal/checker/hints.go
T
root acd5291604
Build / test (push) Failing after 38s
Build / build-windows (push) Has been skipped
internal/checker: error classification + RU hints + tests
Adds:
- retry.go: classifyError() splits errors into Permanent vs Transient
  (used to gate auto-retry); isContextErr() detects ctx cancellation
  through wrapping (OpError, errors.Join).
- hints.go: hintFor(testID, err) returns short Russian explanation per
  failure step, with dedicated branches for SOCKS5 sentinels, every
  documented REP code (0x01..0x08), STUN sentinels, timeouts, and a
  friendly-name fallback.

Coverage: retry.go 100%, hints.go 100%; package total 94.2%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:58:56 +03:00

190 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package checker
import (
"errors"
"fmt"
"net"
"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 "stun":
return "STUN round-trip"
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 "stun":
switch {
case errors.Is(err, ErrSTUNNoMappedAddress):
return "STUN-ответ без XOR-MAPPED-ADDRESS — UDP-релей не пропускает обратный трафик."
case errors.Is(err, ErrSTUNTooShort):
return "STUN-ответ короче 20-байтного заголовка — релей возвращает мусор."
case errors.Is(err, ErrSTUNBadMagicCookie):
return "STUN-ответ без правильного magic cookie — релей возвращает мусор."
case errors.Is(err, ErrSTUNNotSuccess):
return "STUN-сервер вернул не Binding Success — UDP-релей сломан."
case errors.Is(err, ErrSTUNTxIDMismatch):
return "STUN-ответ с чужим transaction ID — релей путает пакеты."
case errors.Is(err, ErrSTUNUnsupportedFamily):
return "STUN-ответ с неподдерживаемым семейством адресов."
case isTimeout:
return "STUN-сервер не ответил вовремя — UDP-релей не работает в обе стороны."
}
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())
}