Skip to content

Наборы правил

Наборы правил предоставляют переиспользуемые коллекции правил маршрутизации, которые могут загружаться из встроенных определений, локальных файлов или удалённых URL. Они являются современной заменой устаревших баз данных GeoIP/GeoSite.

Исходный код: common/srs/, route/rule/rule_set.go, route/rule/rule_set_local.go, route/rule/rule_set_remote.go, option/rule_set.go

Двоичный формат SRS

Формат SRS (Sing-box Rule Set) — это компактное двоичное представление наборов правил, разработанное для эффективной загрузки и уменьшения размера файла по сравнению с исходными JSON-файлами.

Структура файла

+--------+--------+------------------------------+
| Magic  | Version| zlib-compressed rule data    |
| 3 bytes| 1 byte |                              |
+--------+--------+------------------------------+
go
var MagicBytes = [3]byte{0x53, 0x52, 0x53} // ASCII "SRS"

История версий

ВерсияКонстантаНовые возможности
1RuleSetVersion1Начальный формат
2RuleSetVersion2Правила доменов AdGuard
3RuleSetVersion3network_type, network_is_expensive, network_is_constrained
4RuleSetVersion4network_interface_address, default_interface_address

Процесс чтения

go
func Read(reader io.Reader, recover bool) (PlainRuleSetCompat, error) {
    // 1. Чтение и проверка 3-байтового магического заголовка "SRS"
    // 2. Чтение 1-байтового номера версии (big-endian uint8)
    // 3. Открытие ридера декомпрессии zlib
    // 4. Чтение uvarint для количества правил
    // 5. Последовательное чтение каждого правила
}

Флаг recover определяет, будут ли оптимизированные для двоичного формата структуры (такие как матчеры доменов и IP-наборы) развёрнуты обратно в читаемую форму (списки строк). Это используется при декомпиляции .srs обратно в JSON.

Макет сжатых данных

После 4-байтового заголовка все последующие данные сжаты zlib (наилучший уровень сжатия). Внутри распакованного потока:

[uvarint: rule_count]
[rule_0]
[rule_1]
...
[rule_N]

Кодирование правил

Каждое правило начинается с байта типа uint8:

Байт типаЗначение
0Обычное правило (плоские условия)
1Логическое правило (AND/OR подправил)

Элементы обычного правила

Обычное правило — это последовательность типизированных элементов, завершающаяся 0xFF:

[uint8: 0x00 (default rule)]
[uint8: item_type] [item_data...]
[uint8: item_type] [item_data...]
...
[uint8: 0xFF (final)]
[bool: invert]

Константы типов элементов:

go
const (
    ruleItemQueryType              uint8 = 0   // []uint16 (big-endian)
    ruleItemNetwork                uint8 = 1   // []string
    ruleItemDomain                 uint8 = 2   // domain.Matcher binary
    ruleItemDomainKeyword          uint8 = 3   // []string
    ruleItemDomainRegex            uint8 = 4   // []string
    ruleItemSourceIPCIDR           uint8 = 5   // IPSet binary
    ruleItemIPCIDR                 uint8 = 6   // IPSet binary
    ruleItemSourcePort             uint8 = 7   // []uint16 (big-endian)
    ruleItemSourcePortRange        uint8 = 8   // []string
    ruleItemPort                   uint8 = 9   // []uint16 (big-endian)
    ruleItemPortRange              uint8 = 10  // []string
    ruleItemProcessName            uint8 = 11  // []string
    ruleItemProcessPath            uint8 = 12  // []string
    ruleItemPackageName            uint8 = 13  // []string
    ruleItemWIFISSID               uint8 = 14  // []string
    ruleItemWIFIBSSID              uint8 = 15  // []string
    ruleItemAdGuardDomain          uint8 = 16  // AdGuardMatcher binary (v2+)
    ruleItemProcessPathRegex       uint8 = 17  // []string
    ruleItemNetworkType            uint8 = 18  // []uint8 (v3+)
    ruleItemNetworkIsExpensive     uint8 = 19  // no data (v3+)
    ruleItemNetworkIsConstrained   uint8 = 20  // no data (v3+)
    ruleItemNetworkInterfaceAddress uint8 = 21 // TypedMap (v4+)
    ruleItemDefaultInterfaceAddress uint8 = 22 // []Prefix (v4+)
    ruleItemFinal                  uint8 = 0xFF
)

Кодирование массива строк

[uvarint: count]
  [uvarint: string_length] [bytes: string_data]
  ...

Кодирование массива uint16

[uvarint: count]
[uint16 big-endian] [uint16 big-endian] ...

Кодирование IP-набора

IP-наборы хранятся в виде диапазонов, а не CIDR-префиксов, для компактности:

[uint8: version (must be 1)]
[uint64 big-endian: range_count]
  [uvarint: from_addr_length] [bytes: from_addr]
  [uvarint: to_addr_length]   [bytes: to_addr]
  ...

Реализация использует unsafe.Pointer для прямого переинтерпретирования внутренней структуры netipx.IPSet (которая хранит IP-диапазоны как пары {from, to}). IPv4-адреса занимают 4 байта; IPv6-адреса — 16 байт.

Кодирование IP-префикса

Отдельные префиксы (используются в правилах адресов сетевого интерфейса v4+):

[uvarint: addr_byte_length]
[bytes: addr_bytes]
[uint8: prefix_bits]

Кодирование логического правила

[uint8: 0x01 (logical rule)]
[uint8: mode]  // 0 = AND, 1 = OR
[uvarint: sub_rule_count]
[sub_rule_0]
[sub_rule_1]
...
[bool: invert]

Типы наборов правил

Фабричная функция

go
func NewRuleSet(ctx, logger, options) (adapter.RuleSet, error) {
    switch options.Type {
    case "inline", "local", "":
        return NewLocalRuleSet(ctx, logger, options)
    case "remote":
        return NewRemoteRuleSet(ctx, logger, options), nil
    }
}

Локальный набор правил

LocalRuleSet обрабатывает как встроенные правила (встроенные в конфигурационный JSON), так и файловые наборы правил.

go
type LocalRuleSet struct {
    ctx        context.Context
    logger     logger.Logger
    tag        string
    access     sync.RWMutex
    rules      []adapter.HeadlessRule
    metadata   adapter.RuleSetMetadata
    fileFormat string              // "source" (JSON) или "binary" (SRS)
    watcher    *fswatch.Watcher    // наблюдатель изменений файла
    callbacks  list.List[adapter.RuleSetUpdateCallback]
    refs       atomic.Int32        // подсчёт ссылок
}

Ключевые особенности поведения:

  • Встроенный режим: Правила парсятся из options.InlineOptions.Rules во время создания
  • Файловый режим: Правила загружаются из options.LocalOptions.Path, и настраивается fswatch.Watcher для автоматической перезагрузки при изменении файла
  • Автоопределение формата: Расширение файла .json выбирает исходный формат; .srs выбирает двоичный формат
  • Горячая перезагрузка: Когда наблюдатель обнаруживает изменения, reloadFile() перечитывает и перепарсивает файл, а затем уведомляет все зарегистрированные обратные вызовы

Удалённый набор правил

RemoteRuleSet загружает наборы правил по URL с периодическим автообновлением.

go
type RemoteRuleSet struct {
    ctx            context.Context
    cancel         context.CancelFunc
    logger         logger.ContextLogger
    outbound       adapter.OutboundManager
    options        option.RuleSet
    updateInterval time.Duration    // по умолчанию: 24 часа
    dialer         N.Dialer
    access         sync.RWMutex
    rules          []adapter.HeadlessRule
    metadata       adapter.RuleSetMetadata
    lastUpdated    time.Time
    lastEtag       string           // HTTP ETag для условных запросов
    updateTicker   *time.Ticker
    cacheFile      adapter.CacheFile
    pauseManager   pause.Manager
    callbacks      list.List[adapter.RuleSetUpdateCallback]
    refs           atomic.Int32
}

Ключевые особенности поведения:

  • Постоянное кэширование: При запуске загружает кэшированное содержимое из adapter.CacheFile (база данных bbolt). Если кэшированные данные существуют, использует их немедленно вместо загрузки
  • Поддержка ETag: Использует HTTP If-None-Match / 304 Not Modified для предотвращения повторной загрузки неизменённых наборов правил
  • Маршрут загрузки: Может направлять трафик загрузки через указанный исходящий (например, для использования прокси при загрузке наборов правил)
  • Цикл обновления: После PostStart() запускает loopUpdate() в горутине, которая проверяет обновления через updateInterval
  • Управление памятью: После обновления, если refs == 0 (нет активных ссылок на правила), распарсенные правила устанавливаются в nil для освобождения памяти, с явным вызовом runtime.GC()

Подсчёт ссылок

И LocalRuleSet, и RemoteRuleSet реализуют подсчёт ссылок через atomic.Int32:

go
func (s *LocalRuleSet) IncRef()  { s.refs.Add(1) }
func (s *LocalRuleSet) DecRef()  {
    if s.refs.Add(-1) < 0 {
        panic("rule-set: negative refs")
    }
}
func (s *LocalRuleSet) Cleanup() {
    if s.refs.Load() == 0 {
        s.rules = nil  // освобождение памяти при отсутствии ссылок
    }
}

Это позволяет маршрутизатору отслеживать, какие наборы правил активно используются правилами маршрутизации, и освобождать память для неиспользуемых.

Метаданные набора правил

После загрузки правил вычисляются метаданные для определения необходимых типов поиска:

go
type RuleSetMetadata struct {
    ContainsProcessRule bool  // требуется поиск процессов
    ContainsWIFIRule    bool  // требуется состояние WIFI
    ContainsIPCIDRRule  bool  // требуются разрешённые IP-адреса
}

Эти флаги позволяют маршрутизатору пропускать затратные операции (такие как поиск имени процесса или DNS-разрешение), когда ни один набор правил не требует их.

Извлечение IP-набора

Наборы правил поддерживают извлечение всех элементов IP CIDR в значения netipx.IPSet через ExtractIPSet(). Это используется для системных оптимизаций, таких как настройка таблицы маршрутизации TUN, где IP-правила должны применяться на уровне сетевого стека, а не для каждого соединения.

Конфигурация

json
{
  "route": {
    "rule_set": [
      {
        "type": "local",
        "tag": "geoip-cn",
        "format": "binary",
        "path": "geoip-cn.srs"
      },
      {
        "type": "remote",
        "tag": "geosite-category-ads",
        "format": "binary",
        "url": "https://example.com/geosite-category-ads.srs",
        "download_detour": "proxy",
        "update_interval": "24h"
      },
      {
        "tag": "my-rules",
        "rules": [
          {
            "domain_suffix": [".example.com"]
          }
        ]
      }
    ]
  }
}

Замечания по реализации

  1. Двоичный формат SRS использует Go-пакет encoding/binary с порядком байтов big-endian и binary.ReadUvarint/varbin.WriteUvarint для целых чисел переменной длины
  2. Сопоставление доменов использует sing/common/domain.Matcher, который имеет собственный формат двоичной сериализации — это зависимость, которую необходимо реализовать или импортировать
  3. Двоичный формат IP-набора использует unsafe.Pointer для прямого манипулирования внутренними структурами netipx.IPSet — в реимплементации следует использовать корректную сериализацию IP-диапазонов
  4. Сжатие zlib использует уровень zlib.BestCompression при записи
  5. Автоопределение формата проверяет расширения файлов: .json = исходный, .srs = двоичный
  6. Кэширование на основе ETag для удалённых наборов правил должно обрабатывать как 200 OK (новое содержимое), так и 304 Not Modified (обновление только метки времени)