Обзор подсистемы DNS
Исходный код: dns/, dns/transport/, dns/transport/fakeip/, dns/transport/hosts/, dns/transport/local/, dns/transport/dhcp/
Архитектура
Подсистема DNS sing-box состоит из трёх основных компонентов:
+------------------+
| DNS Router | Сопоставление правил, выбор транспорта
+------------------+
|
+------------------+
| DNS Client | Кэширование, EDNS0, RDRC, управление TTL
+------------------+
|
+-------------+-------------+
| | |
+---------+ +---------+ +---------+
| UDP | | HTTPS | | FakeIP | ... другие транспорты
+---------+ +---------+ +---------+- Маршрутизатор DNS (
dns/router.go): Сопоставляет DNS-запросы с правилами, выбирает соответствующий транспорт, обрабатывает стратегию домена и обратное отображение - Клиент DNS (
dns/client.go): Выполняет фактический обмен DNS с кэшированием (freelru), внедрением подсети клиента EDNS0, кэшем отклонённых доменов ответов (RDRC) и корректировкой TTL - Транспорты DNS (
dns/transport/): Выполнение запросов по конкретным протоколам (UDP, TCP, TLS, HTTPS, QUIC/HTTP3, FakeIP, Hosts, Local, DHCP)
Вспомогательные компоненты
- Реестр транспортов (
dns/transport_registry.go): Типобезопасная регистрация типов транспортов на основе обобщений (generics) - Адаптер транспорта (
dns/transport_adapter.go): Базовая структура с типом/тегом/зависимостями/стратегией/подсетью клиента - Базовый транспорт (
dns/transport/base.go): Конечный автомат (New/Started/Closing/Closed) с отслеживанием активных запросов - Коннектор (
dns/transport/connector.go): Обобщённое управление соединениями с защитой от дублирования (singleflight)
Поток обработки запросов
Exchange (необработанное DNS-сообщение)
- Router.Exchange получает
*dns.Msg - Извлечение метаданных: тип запроса, домен, версия IP
- Если транспорт не указан явно, выполняется сопоставление с правилами DNS:
RuleActionDNSRoute-- выбор транспорта с параметрами (стратегия, кэш, TTL, подсеть клиента)RuleActionDNSRouteOptions-- изменение параметров без выбора транспортаRuleActionReject-- возврат REFUSED или сбросRuleActionPredefined-- возврат предварительно настроенного ответа
- Client.Exchange выполняет фактический запрос:
- Проверка кэша (с дедупликацией через блокировку на основе каналов)
- Проверка RDRC на ранее отклонённые ответы
- Применение подсети клиента EDNS0
- Выполнение transport.Exchange с таймаутом
- Валидация ответа (проверка ограничения адресов)
- Нормализация TTL
- Сохранение в кэш
- Сохранение обратного отображения (IP -> домен), если включено
Lookup (домен в адреса)
- Router.Lookup получает строку домена
- Определяет стратегию (IPv4Only, IPv6Only, PreferIPv4, PreferIPv6, AsIS)
- Client.Lookup распределяет:
- IPv4Only: один запрос A
- IPv6Only: один запрос AAAA
- В остальных случаях: параллельные запросы A + AAAA через
task.Group
- Результаты сортируются в соответствии с предпочтением стратегии
Цикл повторных попыток по правилам
Когда правило имеет ограничения адресов (например, ограничения geoip для адресов ответа), маршрутизатор повторяет попытку с последующими подходящими правилами, если ответ отклонён:
for {
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery, &dnsOptions)
responseCheck := addressLimitResponseCheck(rule, metadata)
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
if responseCheck != nil && rejected {
continue // Try next matching rule
}
break
}Ключевые проектные решения
Дедупликация
Кэш использует дедупликацию на основе каналов для предотвращения эффекта "громового стада" (thundering herd):
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
<-cond // Wait for the in-flight query to complete
} else {
defer func() {
c.cacheLock.Delete(question)
close(cond) // Signal waiters
}()
}Обнаружение петель
Петли DNS-запросов (например, транспорт A должен разрешить адрес своего сервера через транспорт A) обнаруживаются через контекст:
contextTransport, loaded := transportTagFromContext(ctx)
if loaded && transport.Tag() == contextTransport {
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
}
ctx = contextWithTransportTag(ctx, transport.Tag())RDRC (кэш отклонённых доменов ответов)
Когда ответ отклоняется проверкой ограничения адресов, комбинация домен/тип запроса/транспорт кэшируется в RDRC, чтобы пропустить будущие запросы к тому же транспорту:
if rejected {
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
}
// On subsequent queries:
if c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype) {
return nil, ErrResponseRejectedCached
}Подсеть клиента EDNS0
Применяется перед обменом, если настроено:
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}