Проверка IP-адресов iOS: семейства и нормализация
Вы настроили подключение в Windows и передали задачу на проверку в облачный Mac, но обнаружили, что IPv6-адрес одного устройства воспринимается как две разные цели: в одной записи все нулевые группы указаны полностью, а в другой использовано двойное двоеточие. Если список подключений, логика удаления дубликатов и правила доступа сравнивают исходные строки независимо друг от друга, результаты могут разойтись. Ниже мы создадим автономные проверки на Swift с помощью системных функций разбора. Реальный сервер и доступ к сети для этого не нужны.
Сначала определите допустимый ввод
В этом примере принимаются только IP-литералы — текстовые записи IP-адресов, без доменных имён, портов и полных URL. IPv4 должен состоять из четырёх десятичных компонентов; для IPv6 допускаются стандартные формы записи, поддерживаемые системой. Пробельные символы в начале и конце строки приводят к отказу, а не удаляются автоматически: так значение в интерфейсе не будет отличаться от того, которое используется для подключения.
Квадратные скобки вокруг IPv6-адреса относятся к представлению хоста в URL, а не к самому адресу. Идентификаторы зоны, например %en0, этот парсер тоже не принимает. Если требуется подключаться к канальным локальным адресам, на другом уровне нужно явно принимать и адрес, и сведения об интерфейсе. Нельзя просто отбросить знак процента со всем, что идёт после него, и продолжить подключение.
Успешный разбор означает лишь то, что текст представляет собой адрес. Он не подтверждает ни разрешение доступа к этому адресу, ни доступность устройства.
Адреса из диапазонов для документации в примерах используются только для локального разбора. Скрипт не устанавливает с ними соединений.
Храните семейство адреса и его байты
Создайте файл ip-contract.swift в каталоге проекта на облачном Mac. Сначала убедитесь, что xcrun swiftc --version выводит сведения о компиляторе, затем добавьте следующий код. Здесь не используются интерфейсы, способные разрешать имена хостов: поведение DNS не должно влиять на проверку IP-литералов.
import Darwin
import Foundation
struct IPAddress: Equatable {
let family: Int32
let bytes: [UInt8]
}
func parseIP(_ text: String) -> IPAddress? {
guard !text.isEmpty,
text.utf8.count <= 45,
!text.utf8.contains(0) else {
return nil
}
var v4 = in_addr()
let result4 = text.withCString {
inet_pton(AF_INET, $0, &v4)
}
if result4 == 1 {
return IPAddress(
family: AF_INET,
bytes: withUnsafeBytes(of: &v4) { Array($0) }
)
}
var v6 = in6_addr()
let result6 = text.withCString {
inet_pton(AF_INET6, $0, &v6)
}
if result6 == 1 {
return IPAddress(
family: AF_INET6,
bytes: withUnsafeBytes(of: &v6) { Array($0) }
)
}
return nil
}
func require(_ condition: Bool, _ message: String) {
if !condition {
fatalError(message)
}
}
let good = [
"192.0.2.1",
"2001:db8::1",
"2001:0DB8:0000:0000:0000:0000:0000:0001",
"::",
"::1",
"::ffff:192.0.2.1"
]
for text in good {
require(parseIP(text) != nil, "Rejected valid literal")
}
let bad = [
"",
" 192.0.2.1",
"192.0.2.1\n",
"192.0.2.256",
"192.0.2",
"192.0.02.1",
"0xC0000201",
"[2001:db8::1]",
"fe80::1%en0",
"2001:::1",
"192.0.2.1\u{0}ignored"
]
for text in bad {
require(parseIP(text) == nil, "Accepted invalid literal")
}
let short = parseIP("2001:db8::1")!
let expanded = parseIP(
"2001:0DB8:0000:0000:0000:0000:0000:0001"
)!
require(short == expanded, "Equivalent IPv6 mismatch")
require(short.bytes.count == 16, "Invalid IPv6 length")
let v4 = parseIP("192.0.2.1")!
let mapped = parseIP("::ffff:192.0.2.1")!
require(v4.bytes.count == 4, "Invalid IPv4 length")
require(v4 != mapped, "Address families collapsed")
Ограничение длины — это лимит в байтах для данного контракта разбора, а не универсальное ограничение для всех адресов с идентификаторами зоны. Байты адреса берутся непосредственно из системных структур и сохраняют сетевой порядок байтов. Не следует интерпретировать весь буфер как целое число в машинном порядке байтов для последующего сравнения.
Скомпилируйте, запустите и проверьте обработку ошибок
Скомпилируйте скрипт в отдельную программу: её можно включить в задание сборки, а различия в способах запуска не будут влиять на выполнение через интерпретатор:
set -eu
mkdir -p .ip-check
xcrun swiftc ip-contract.swift -o .ip-check/ip-contract
.ip-check/ip-contract
При успешном выполнении программа ничего не выводит и завершается с нулевым кодом. Нарушение любого условия контракта приводит к ненулевому коду завершения. Здесь используются явные проверки, а не assert, поскольку настройки оптимизации могут удалять такие утверждения.
Временно замените проверку равенства на проверку неравенства, убедитесь, что задание действительно завершается ошибкой, а затем верните исходное условие. Такой негативный тест помогает выявить ситуации, когда конвейер игнорирует код завершения или лишь сохраняет журнал, продолжая показывать успешный результат. Недостаточно убедиться, что скрипт «один раз запустился».
Отделите нормализацию от политики доступа
Не расширяйте эквивалентность неявно
Сокращённая и полная записи одного IPv6-адреса должны считаться равными. Однако IPv4 и IPv4-отображённый IPv6 в этой модели остаются адресами разных семейств. Если приложению нужно считать их одной логической целью, добавьте явное преобразование и проверьте правила доступа до и после него. Простое удаление строкового префикса для этого не подходит.
Для отображения можно использовать inet_ntop, чтобы получать единообразное текстовое представление. Удаление дубликатов и авторизация по-прежнему должны опираться на разобранную модель. Не считайте формат строки, выдаваемый одной версией системы, неизменным контрактом хранения данных для всех последующих версий.
Проверяйте доменные имена отдельно
Если доменное имя не проходит через эту функцию, это не означает, что оно некорректно: оно просто не относится к допустимому типу ввода. Если продукт поддерживает и доменные имена, предусмотрите отдельный путь их ввода, а после разрешения проверяйте набор адресов, к которым приложение фактически собирается подключиться. Проверка только введённого пользователем имени хоста не учитывает изменения результатов разрешения.
Допустимость адресов обратной петли, частных, канальных локальных и других подобных адресов зависит от назначения продукта. Инструменту отладки устройств и сервису загрузки общедоступных интернет-ресурсов нужны разные правила. Не стоит сводить их к одному булевому признаку «безопасный адрес» с нечётким смыслом.
Сохраните проблемные примеры для регрессионных тестов
После переноса парсера в общий исходный код запускайте один и тот же набор тестовых векторов и в проверках командной строки macOS, и в тестовой цели iOS. Первые удобны для быстрого поиска ошибок в удалённой среде, вторая подтверждает поведение на целевой системе. Успешная проверка на хосте не заменяет приёмочные испытания на целевой платформе.
Для каждого исправления добавляйте исходный ввод, ожидаемое семейство адреса и ожидаемый результат. Особенно важно сохранять исходные байты строк со встроенным нулевым байтом: в журнале он может быть не виден. Строки C заканчиваются на нулевом байте, поэтому без проверки на входе парсер может проверить лишь корректный префикс строки.
В завершение проверьте три вещи: распознаются ли разные записи одного адреса как эквивалентные, сохраняется ли граница между семействами адресов и отклоняется ли некорректный ввод до попытки подключения. Пока эти условия контракта стабильны, у вас есть воспроизводимый набор проверок для дальнейшего развития валидации в интерфейсе и политик подключения.
Часто задаваемые вопросы
Всегда ли разные строки IPv6 означают разные адреса?
Нет. Сжатие нулей, ведущие нули и регистр букв меняют запись. Сравнивайте семейство адреса и байты в сетевом порядке после разбора.
Можно ли разрешить соединение после успешного inet_pton?
Нет. Для loopback, частных, link-local и отображённых адресов нужны отдельные правила. Результаты DNS также требуют проверки.
Поручите задачи выделенному облачному Mac
Арендуйте выделенный физический Mac mini и выберите локацию и срок аренды под свой график. После активации данные для подключения появятся в консоли; если возникнут вопросы, отправьте заявку в поддержку через консоль.
Выбрать облачный Mac