Validate IP addresses and prefixes in JSON schema

This commit is contained in:
世界 2026-09-18 20:27:14 +08:00
parent 6cea6c95a1
commit 2c28efdcad
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
6 changed files with 288 additions and 224 deletions

File diff suppressed because it is too large Load diff

View file

@ -151,9 +151,9 @@ type RawDefaultRule struct {
Geosite badoption.Listable[string] `json:"geosite,omitempty" schema:"omit"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty" schema:"omit"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty" schema:"omit"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty" schema:"prefixable"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty" schema:"prefixable"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`

View file

@ -147,7 +147,7 @@ type RawDefaultDNSRule struct {
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty" schema:"prefixable"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
@ -176,7 +176,7 @@ type RawDefaultDNSRule struct {
RuleSet badoption.Listable[string] `json:"rule_set,omitempty" reference:"rule_set"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
MatchResponse *DNSRuleMatchResponse `json:"match_response,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty" schema:"prefixable"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
ResponseRcode *DNSRCode `json:"response_rcode,omitempty"`

View file

@ -254,8 +254,8 @@ type DefaultHeadlessRule struct {
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty" schema:"prefixable"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty" schema:"prefixable"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"`

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding"
stdjson "encoding/json"
"net/netip"
"reflect"
"slices"
"strconv"
@ -25,6 +26,8 @@ var (
addrType = reflect.TypeFor[badoption.Addr]()
prefixType = reflect.TypeFor[badoption.Prefix]()
prefixableType = reflect.TypeFor[badoption.Prefixable]()
netipAddrType = reflect.TypeFor[netip.Addr]()
netipPrefixType = reflect.TypeFor[netip.Prefix]()
httpHeaderType = reflect.TypeFor[badoption.HTTPHeader]()
memoryBytesType = reflect.TypeFor[byteformats.MemoryBytes]()
networkBytesCompatType = reflect.TypeFor[byteformats.NetworkBytesCompat]()
@ -77,8 +80,18 @@ func (g *generator) Describe(valueType reflect.Type) (*Node, error) {
return g.Define("Duration", func() (*Node, error) {
return DurationNode(), nil
})
case addrType, prefixType, prefixableType:
return StringNode(), nil
case addrType, netipAddrType:
return g.Define("IPAddress", func() (*Node, error) {
return IPAddressNode(), nil
})
case prefixType, netipPrefixType:
return g.Define("IPPrefix", func() (*Node, error) {
return IPPrefixNode(), nil
})
case prefixableType:
return g.Define("IPAddressOrPrefix", func() (*Node, error) {
return IPAddressOrPrefixNode(), nil
})
case httpHeaderType:
return g.Define("HTTPHeader", func() (*Node, error) {
return &Node{Type: "object", AdditionalProperties: ListableOf(StringNode())}, nil
@ -208,7 +221,8 @@ func (g *generator) FlattenStruct(node *Node, structType reflect.Type) error {
for fieldType.Kind() == reflect.Pointer {
fieldType = fieldType.Elem()
}
if field.Tag.Get("schema") == "omit" {
schemaTag := field.Tag.Get("schema")
if schemaTag == "omit" {
continue
}
if field.Anonymous && tagName == "" {
@ -227,8 +241,8 @@ func (g *generator) FlattenStruct(node *Node, structType reflect.Type) error {
g.path = append(g.path, structType.Name()+"."+tagName)
var fieldNode *Node
var err error
if enumTag != "" || examplesTag != "" || referenceTag != "" {
fieldNode, err = taggedFieldNode(fieldType, enumTag, examplesTag, referenceTag)
if enumTag != "" || examplesTag != "" || referenceTag != "" || schemaTag != "" {
fieldNode, err = g.taggedFieldNode(fieldType, enumTag, examplesTag, referenceTag, schemaTag)
} else {
fieldNode, err = g.Describe(fieldType)
}
@ -241,7 +255,7 @@ func (g *generator) FlattenStruct(node *Node, structType reflect.Type) error {
return nil
}
func taggedFieldNode(fieldType reflect.Type, enumTag string, examplesTag string, referenceTag string) (*Node, error) {
func (g *generator) taggedFieldNode(fieldType reflect.Type, enumTag string, examplesTag string, referenceTag string, schemaTag string) (*Node, error) {
elementType := fieldType
for elementType.Kind() == reflect.Pointer {
elementType = elementType.Elem()
@ -255,16 +269,21 @@ func taggedFieldNode(fieldType reflect.Type, enumTag string, examplesTag string,
}
var element *Node
var err error
if enumTag != "" {
switch {
case enumTag != "":
element, err = taggedValueNode(elementType, strings.Split(enumTag, ","))
if err != nil {
return nil, err
}
} else {
element, err = taggedValueNode(elementType, nil)
if err != nil {
return nil, err
case schemaTag == "prefixable":
if elementType.Kind() != reflect.String {
return nil, E.New("prefixable schema tags require a string field, got ", fieldType.String())
}
element, err = g.Describe(prefixableType)
case schemaTag != "":
return nil, E.New("unknown schema tag ", schemaTag, " on ", fieldType.String())
default:
element, err = g.Describe(elementType)
}
if err != nil {
return nil, err
}
if examplesTag != "" {
examples, parseErr := taggedValues(elementType, strings.Split(examplesTag, ","))

View file

@ -76,6 +76,39 @@ func DurationNode() *Node {
return &Node{Type: "string", Pattern: durationPattern}
}
const (
ipv4Octet = `(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])`
ipv4Address = ipv4Octet + `(?:\.` + ipv4Octet + `){3}`
ipv6Group = `[0-9A-Fa-f]{1,4}`
ipv6Low32Bits = `(?:` + ipv6Group + `:` + ipv6Group + `|` + ipv4Address + `)`
ipv6Address = `(?:` +
`(?:` + ipv6Group + `:){6}` + ipv6Low32Bits +
`|::(?:` + ipv6Group + `:){5}` + ipv6Low32Bits +
`|(?:` + ipv6Group + `)?::(?:` + ipv6Group + `:){4}` + ipv6Low32Bits +
`|(?:(?:` + ipv6Group + `:)?` + ipv6Group + `)?::(?:` + ipv6Group + `:){3}` + ipv6Low32Bits +
`|(?:(?:` + ipv6Group + `:){0,2}` + ipv6Group + `)?::(?:` + ipv6Group + `:){2}` + ipv6Low32Bits +
`|(?:(?:` + ipv6Group + `:){0,3}` + ipv6Group + `)?::` + ipv6Group + `:` + ipv6Low32Bits +
`|(?:(?:` + ipv6Group + `:){0,4}` + ipv6Group + `)?::` + ipv6Low32Bits +
`|(?:(?:` + ipv6Group + `:){0,5}` + ipv6Group + `)?::` + ipv6Group +
`|(?:(?:` + ipv6Group + `:){0,6}` + ipv6Group + `)?::` +
`)`
ipv6Zone = `%.+`
ipv4PrefixBits = `/(?:3[0-2]|[12]?[0-9])`
ipv6PrefixBits = `/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])`
)
func IPAddressNode() *Node {
return &Node{Type: "string", Pattern: `^(?:` + ipv4Address + `|` + ipv6Address + `(?:` + ipv6Zone + `)?)$`}
}
func IPPrefixNode() *Node {
return &Node{Type: "string", Pattern: `^(?:` + ipv4Address + ipv4PrefixBits + `|` + ipv6Address + ipv6PrefixBits + `)$`}
}
func IPAddressOrPrefixNode() *Node {
return &Node{Type: "string", Pattern: `^(?:` + ipv4Address + `(?:` + ipv4PrefixBits + `)?|` + ipv6Address + `(?:` + ipv6PrefixBits + `|` + ipv6Zone + `)?)$`}
}
func StringEnum(values ...string) *Node {
anyValues := make([]any, 0, len(values))
for _, value := range values {