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()) }