Use Prefixable for ip_cidr rule options

This commit is contained in:
世界 2026-09-24 16:25:31 +08:00
parent 2f10bfc8ea
commit c8d9ae4acf
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
10 changed files with 61 additions and 343 deletions

View file

@ -1,284 +0,0 @@
package main
import (
"context"
"fmt"
"io"
"net/netip"
"os"
"os/exec"
"strings"
"syscall"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/shell"
)
var iperf3Path string
func main() {
err := main0()
if err != nil {
log.Fatal(err)
}
}
func main0() error {
err := shell.Exec("sudo", "ls").Run()
if err != nil {
return err
}
results, err := runTests()
if err != nil {
return err
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(results)
}
func runTests() ([]TestResult, error) {
boxPaths := []string{
os.ExpandEnv("$HOME/Downloads/sing-box-1.11.15-darwin-arm64/sing-box"),
//"/Users/sekai/Downloads/sing-box-1.11.15-linux-arm64/sing-box",
"./sing-box",
}
stacks := []string{
"gvisor",
"system",
}
mtus := []int{
1500,
4064,
// 16384,
// 32768,
// 49152,
65535,
}
flagList := [][]string{
{},
}
var results []TestResult
for _, boxPath := range boxPaths {
for _, stack := range stacks {
for _, mtu := range mtus {
if strings.HasPrefix(boxPath, ".") {
for _, flags := range flagList {
result, err := testOnce(boxPath, stack, mtu, false, flags)
if err != nil {
return nil, err
}
results = append(results, *result)
}
} else {
result, err := testOnce(boxPath, stack, mtu, false, nil)
if err != nil {
return nil, err
}
results = append(results, *result)
}
}
}
}
return results, nil
}
type TestResult struct {
BoxPath string `json:"box_path"`
Stack string `json:"stack"`
MTU int `json:"mtu"`
Flags []string `json:"flags"`
MultiThread bool `json:"multi_thread"`
UploadSpeed string `json:"upload_speed"`
DownloadSpeed string `json:"download_speed"`
}
func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags []string) (result *TestResult, err error) {
testAddress := netip.MustParseAddr("1.1.1.1")
testConfig := option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeTun,
Options: &option.TunInboundOptions{
Address: []netip.Prefix{netip.MustParsePrefix("172.18.0.1/30")},
AutoRoute: true,
MTU: uint32(mtu),
Stack: stackName,
RouteAddress: []netip.Prefix{netip.PrefixFrom(testAddress, testAddress.BitLen())},
},
},
},
Route: &option.RouteOptions{
Rules: []option.Rule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
RawDefaultRule: option.RawDefaultRule{
IPCIDR: []string{testAddress.String()},
},
RuleAction: option.RuleAction{
Action: C.RuleActionTypeRouteOptions,
RouteOptionsOptions: option.RouteOptionsActionOptions{
OverrideAddress: "127.0.0.1",
},
},
},
},
},
AutoDetectInterface: true,
},
}
ctx := include.Context(context.Background())
tempConfig, err := os.CreateTemp("", "tun-bench-*.json")
if err != nil {
return
}
defer os.Remove(tempConfig.Name())
encoder := json.NewEncoderContext(ctx, tempConfig)
encoder.SetIndent("", " ")
err = encoder.Encode(testConfig)
if err != nil {
return nil, E.Cause(err, "encode test config")
}
tempConfig.Close()
var sudoArgs []string
if len(flags) > 0 {
sudoArgs = append(sudoArgs, "env")
sudoArgs = append(sudoArgs, flags...)
}
sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name())
boxProcess := shell.Exec("sudo", sudoArgs...)
boxProcess.Stdout = &stderrWriter{}
boxProcess.Stderr = io.Discard
err = boxProcess.Start()
if err != nil {
return
}
if C.IsDarwin {
iperf3Path, err = exec.LookPath("iperf3-darwin")
} else {
iperf3Path, err = exec.LookPath("iperf3")
}
if err != nil {
return
}
serverProcess := shell.Exec(iperf3Path, "-s")
serverProcess.Stdout = io.Discard
serverProcess.Stderr = io.Discard
err = serverProcess.Start()
if err != nil {
return nil, E.Cause(err, "start iperf3 server")
}
time.Sleep(time.Second)
args := []string{"-c", testAddress.String()}
if multiThread {
args = append(args, "-P", "10")
}
uploadProcess := shell.Exec(iperf3Path, args...)
output, err := uploadProcess.Read()
if err != nil {
boxProcess.Process.Signal(syscall.SIGKILL)
serverProcess.Process.Signal(syscall.SIGKILL)
println(output)
return
}
uploadResult := common.SubstringBeforeLast(output, "iperf Done.")
uploadResult = common.SubstringBeforeLast(uploadResult, "sender")
uploadResult = common.SubstringBeforeLast(uploadResult, "bits/sec")
uploadResult = common.SubstringAfterLast(uploadResult, "Bytes")
uploadResult = strings.ReplaceAll(uploadResult, " ", "")
result = &TestResult{
BoxPath: boxPath,
Stack: stackName,
MTU: mtu,
Flags: flags,
MultiThread: multiThread,
UploadSpeed: uploadResult,
}
downloadProcess := shell.Exec(iperf3Path, append(args, "-R")...)
output, err = downloadProcess.Read()
if err != nil {
boxProcess.Process.Signal(syscall.SIGKILL)
serverProcess.Process.Signal(syscall.SIGKILL)
println(output)
return
}
downloadResult := common.SubstringBeforeLast(output, "iperf Done.")
downloadResult = common.SubstringBeforeLast(downloadResult, "receiver")
downloadResult = common.SubstringBeforeLast(downloadResult, "bits/sec")
downloadResult = common.SubstringAfterLast(downloadResult, "Bytes")
downloadResult = strings.ReplaceAll(downloadResult, " ", "")
result.DownloadSpeed = downloadResult
printArgs := []any{boxPath, stackName, mtu, "upload", uploadResult, "download", downloadResult}
if len(flags) > 0 {
printArgs = append(printArgs, "flags", strings.Join(flags, " "))
}
if multiThread {
printArgs = append(printArgs, "(-P 10)")
}
fmt.Println(printArgs...)
err = boxProcess.Process.Signal(syscall.SIGTERM)
if err != nil {
return
}
err = serverProcess.Process.Signal(syscall.SIGTERM)
if err != nil {
return
}
boxDone := make(chan struct{})
go func() {
boxProcess.Cmd.Wait()
close(boxDone)
}()
serverDone := make(chan struct{})
go func() {
serverProcess.Process.Wait()
close(serverDone)
}()
select {
case <-boxDone:
case <-time.After(2 * time.Second):
boxProcess.Process.Kill()
case <-time.After(4 * time.Second):
println("box process did not close!")
os.Exit(1)
}
select {
case <-serverDone:
case <-time.After(2 * time.Second):
serverProcess.Process.Kill()
case <-time.After(4 * time.Second):
println("server process did not close!")
os.Exit(1)
}
return
}
type stderrWriter struct{}
func (w *stderrWriter) Write(p []byte) (n int, err error) {
return os.Stderr.Write(p)
}

View file

@ -9,11 +9,14 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
"github.com/oschwald/maxminddb-golang"
"github.com/spf13/cobra"
"go4.org/netipx"
)
var flagGeoipExportOutput string
@ -83,9 +86,13 @@ func geoipExport(countryCode string) error {
encoder := json.NewEncoder(outputWriter)
encoder.SetIndent("", " ")
var headlessRule option.DefaultHeadlessRule
headlessRule.IPCIDR = make([]string, 0, len(ipNets))
headlessRule.IPCIDR = make([]*badoption.Prefixable, 0, len(ipNets))
for _, cidr := range ipNets {
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
prefix, loaded := netipx.FromStdIPNet(cidr)
if !loaded {
return E.New("invalid network: ", cidr)
}
headlessRule.IPCIDR = append(headlessRule.IPCIDR, common.Ptr(badoption.Prefixable(prefix)))
}
var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion2

View file

@ -206,7 +206,9 @@ func readDefaultRule(reader varbin.Reader, recover bool, mmap *mmapReader) (rule
return
}
if recover {
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), func(it netip.Prefix) *badoption.Prefixable {
return common.Ptr(badoption.Prefixable(it))
})
}
case ruleItemIPCIDR:
if mmap != nil {
@ -218,7 +220,9 @@ func readDefaultRule(reader varbin.Reader, recover bool, mmap *mmapReader) (rule
return
}
if recover {
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), func(it netip.Prefix) *badoption.Prefixable {
return common.Ptr(badoption.Prefixable(it))
})
}
case ruleItemSourcePort:
rule.SourcePort, err = varbin.ReadSlice[uint16](reader, binary.BigEndian)
@ -621,22 +625,12 @@ func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) e
return binary.Write(writer, binary.BigEndian, value)
}
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string, rawSet *ipset.Set, mmap *mmapWriter) error {
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []*badoption.Prefixable, rawSet *ipset.Set, mmap *mmapWriter) error {
set := rawSet
if len(value) > 0 {
var builder netipx.IPSetBuilder
for i, prefixString := range value {
prefix, err := netip.ParsePrefix(prefixString)
if err == nil {
builder.AddPrefix(prefix)
continue
}
addr, addrErr := netip.ParseAddr(prefixString)
if addrErr == nil {
builder.Add(addr)
continue
}
return E.Cause(err, "parse [", i, "]")
for _, prefixable := range value {
builder.AddPrefix(prefixable.Build(netip.Prefix{}))
}
ipSet, err := builder.IPSet()
if err != nil {

View file

@ -12,6 +12,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json/badoption"
"github.com/stretchr/testify/require"
@ -25,7 +26,11 @@ func TestMmapMatchesSource(t *testing.T) {
DefaultOptions: option.DefaultHeadlessRule{
Domain: badoption.Listable[string]{"example.com"},
DomainSuffix: badoption.Listable[string]{".example.org"},
IPCIDR: badoption.Listable[string]{"10.0.0.0/8", "2001:db8::/32", "192.0.2.1"},
IPCIDR: badoption.Listable[*badoption.Prefixable]{
common.Ptr(badoption.Prefixable(netip.MustParsePrefix("10.0.0.0/8"))),
common.Ptr(badoption.Prefixable(netip.MustParsePrefix("2001:db8::/32"))),
common.Ptr(badoption.Prefixable(netip.MustParsePrefix("192.0.2.1/32"))),
},
},
},
{
@ -36,7 +41,7 @@ func TestMmapMatchesSource(t *testing.T) {
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: badoption.Listable[string]{"||ads.example.net^"},
SourceIPCIDR: badoption.Listable[string]{"172.16.0.0/12"},
SourceIPCIDR: badoption.Listable[*badoption.Prefixable]{common.Ptr(badoption.Prefixable(netip.MustParsePrefix("172.16.0.0/12")))},
},
}},
},

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" schema:"prefixable"`
SourceIPCIDR badoption.Listable[*badoption.Prefixable] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty" schema:"prefixable"`
IPCIDR badoption.Listable[*badoption.Prefixable] `json:"ip_cidr,omitempty"`
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" schema:"prefixable"`
SourceIPCIDR badoption.Listable[*badoption.Prefixable] `json:"source_ip_cidr,omitempty"`
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" schema:"prefixable"`
IPCIDR badoption.Listable[*badoption.Prefixable] `json:"ip_cidr,omitempty"`
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" schema:"prefixable"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty" schema:"prefixable"`
SourceIPCIDR badoption.Listable[*badoption.Prefixable] `json:"source_ip_cidr,omitempty"`
IPCIDR badoption.Listable[*badoption.Prefixable] `json:"ip_cidr,omitempty"`
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

@ -8,7 +8,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/ipset"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
"go4.org/netipx"
)
@ -21,20 +21,10 @@ type IPCIDRItem struct {
description string
}
func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
func NewIPCIDRItem(isSource bool, prefixables []*badoption.Prefixable) (*IPCIDRItem, error) {
var builder netipx.IPSetBuilder
for i, prefixString := range prefixStrings {
prefix, err := netip.ParsePrefix(prefixString)
if err == nil {
builder.AddPrefix(prefix)
continue
}
addr, addrErr := netip.ParseAddr(prefixString)
if addrErr == nil {
builder.Add(addr)
continue
}
return nil, E.Cause(err, "parse [", i, "]")
for _, prefixable := range prefixables {
builder.AddPrefix(prefixable.Build(netip.Prefix{}))
}
var description string
if isSource {
@ -42,12 +32,15 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
} else {
description = "ip_cidr="
}
if dLen := len(prefixStrings); dLen == 1 {
description += prefixStrings[0]
} else if dLen > 3 {
description += "[" + strings.Join(prefixStrings[:3], " ") + "...]"
prefixString := func(it *badoption.Prefixable) string {
return it.Build(netip.Prefix{}).String()
}
if prefixCount := len(prefixables); prefixCount == 1 {
description += prefixString(prefixables[0])
} else if prefixCount > 3 {
description += "[" + strings.Join(common.Map(prefixables[:3], prefixString), " ") + "...]"
} else {
description += "[" + strings.Join(prefixStrings, " ") + "]"
description += "[" + strings.Join(common.Map(prefixables, prefixString), " ") + "]"
}
ipSet, err := builder.IPSet()
if err != nil {

View file

@ -10,6 +10,8 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/convertor/adguard"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json/badoption"
slogger "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@ -1432,7 +1434,7 @@ func addOtherItem(rule *abstractDefaultRule, item RuleItem) {
func addSourceAddressItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) {
t.Helper()
item, err := NewIPCIDRItem(true, cidrs)
item, err := NewIPCIDRItem(true, parsePrefixables(cidrs))
require.NoError(t, err)
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
@ -1462,7 +1464,7 @@ func addDestinationRegexItem(t *testing.T, rule *abstractDefaultRule, regexes []
func addDestinationIPCIDRItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) {
t.Helper()
item, err := NewIPCIDRItem(false, cidrs)
item, err := NewIPCIDRItem(false, parsePrefixables(cidrs))
require.NoError(t, err)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
@ -1507,3 +1509,9 @@ func addDestinationPortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges
rule.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, item)
}
func parsePrefixables(cidrs []string) []*badoption.Prefixable {
return common.Map(cidrs, func(it string) *badoption.Prefixable {
return common.Ptr(badoption.Prefixable(netip.MustParsePrefix(it)))
})
}

View file

@ -225,6 +225,9 @@ func (g *generator) FlattenStruct(node *Node, structType reflect.Type) error {
if schemaTag == "omit" {
continue
}
if schemaTag != "" {
return E.New("unknown schema tag ", schemaTag, " on ", structType.Name(), ".", field.Name)
}
if field.Anonymous && tagName == "" {
err := g.FlattenStruct(node, fieldType)
if err != nil {
@ -241,8 +244,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 != "" || schemaTag != "" {
fieldNode, err = g.taggedFieldNode(fieldType, enumTag, examplesTag, referenceTag, schemaTag)
if enumTag != "" || examplesTag != "" || referenceTag != "" {
fieldNode, err = g.taggedFieldNode(fieldType, enumTag, examplesTag, referenceTag)
} else {
fieldNode, err = g.Describe(fieldType)
}
@ -255,7 +258,7 @@ func (g *generator) FlattenStruct(node *Node, structType reflect.Type) error {
return nil
}
func (g *generator) taggedFieldNode(fieldType reflect.Type, enumTag string, examplesTag string, referenceTag string, schemaTag string) (*Node, error) {
func (g *generator) taggedFieldNode(fieldType reflect.Type, enumTag string, examplesTag string, referenceTag string) (*Node, error) {
elementType := fieldType
for elementType.Kind() == reflect.Pointer {
elementType = elementType.Elem()
@ -269,17 +272,9 @@ func (g *generator) taggedFieldNode(fieldType reflect.Type, enumTag string, exam
}
var element *Node
var err error
switch {
case enumTag != "":
if enumTag != "" {
element, err = taggedValueNode(elementType, strings.Split(enumTag, ","))
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:
} else {
element, err = g.Describe(elementType)
}
if err != nil {