Add JSON schema support

This commit is contained in:
世界
2026-08-30 17:41:44 +08:00
parent e09e3b49f9
commit 6e09f2892a
87 changed files with 19320 additions and 305 deletions
+4 -1
View File
@@ -14,7 +14,7 @@ PREFIX ?= $(shell go env GOPATH)
SING_FFI ?= sing-ffi
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
.PHONY: test release docs build
.PHONY: test release docs build schema
build:
export GOTOOLCHAIN=local && \
@@ -32,6 +32,9 @@ ci_build:
generate_completions:
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
schema:
go run -ldflags "$(LDFLAGS_SHARED)" --tags "$(TAGS)" $(MAIN) schema -o docs/schema.json
install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
+8
View File
@@ -2,6 +2,8 @@ package certificate
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
}
}
func (m *Registry) OptionTypes() []string {
m.access.Lock()
defer m.access.Unlock()
return slices.Sorted(maps.Keys(m.optionsType))
}
func (m *Registry) CreateOptions(providerType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
+8
View File
@@ -2,6 +2,8 @@ package endpoint
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
}
}
func (m *Registry) OptionTypes() []string {
m.access.Lock()
defer m.access.Unlock()
return slices.Sorted(maps.Keys(m.optionsType))
}
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
+8
View File
@@ -2,6 +2,8 @@ package inbound
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
}
}
func (m *Registry) OptionTypes() []string {
m.access.Lock()
defer m.access.Unlock()
return slices.Sorted(maps.Keys(m.optionsType))
}
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
+8
View File
@@ -2,6 +2,8 @@ package outbound
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
}
}
func (r *Registry) OptionTypes() []string {
r.access.Lock()
defer r.access.Unlock()
return slices.Sorted(maps.Keys(r.optionsType))
}
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
+8
View File
@@ -2,6 +2,8 @@ package service
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
}
}
func (m *Registry) OptionTypes() []string {
m.access.Lock()
defer m.access.Unlock()
return slices.Sorted(maps.Keys(m.optionsType))
}
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
+45
View File
@@ -0,0 +1,45 @@
package main
import (
"context"
"os"
"reflect"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/schema"
"github.com/spf13/cobra"
)
var commandSchemaFlagOutput string
var commandSchema = &cobra.Command{
Use: "schema",
Short: "Generate configuration JSON schema",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := generateSchema()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandSchema.Flags().StringVarP(&commandSchemaFlagOutput, "output", "o", "", "write schema to file instead of stdout")
mainCommand.AddCommand(commandSchema)
}
func generateSchema() error {
content, err := schema.Generate(include.Context(context.Background()), reflect.TypeFor[option.Options]())
if err != nil {
return err
}
if commandSchemaFlagOutput != "" {
return os.WriteFile(commandSchemaFlagOutput, content, 0o644)
}
_, err = os.Stdout.Write(content)
return err
}
@@ -94,7 +94,9 @@ func TestNewAppleSessionConfig(t *testing.T) {
options: option.HTTPClientOptions{
Version: 2,
DialerOptions: option.DialerOptions{
ConnectTimeout: badoption.Duration(2 * time.Second),
AbstractDialerOptions: option.AbstractDialerOptions{
ConnectTimeout: badoption.Duration(2 * time.Second),
},
},
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
TLS: &option.OutboundTLSOptions{
+20 -4
View File
@@ -1623,8 +1623,10 @@ func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool {
return true
}
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
case "", C.RuleActionTypeRoute:
return action.RouteOptions.DisableOptimisticCache || action.RouteOptions.Speculative
case C.RuleActionTypeEvaluate:
return action.EvaluateOptions.DisableOptimisticCache || action.EvaluateOptions.Speculative
case C.RuleActionTypeRouteOptions:
return action.RouteOptionsOptions.DisableOptimisticCache
default:
@@ -1634,8 +1636,10 @@ func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool {
func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool {
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
case "", C.RuleActionTypeRoute:
return C.DomainStrategy(action.RouteOptions.Strategy) != C.DomainStrategyAsIS
case C.RuleActionTypeEvaluate:
return C.DomainStrategy(action.EvaluateOptions.Strategy) != C.DomainStrategyAsIS
case C.RuleActionTypeRouteOptions:
return C.DomainStrategy(action.RouteOptionsOptions.Strategy) != C.DomainStrategyAsIS
default:
@@ -1663,8 +1667,14 @@ func dnsRuleActionType(rule option.DNSRule) string {
func dnsRuleActionServer(rule option.DNSRule) string {
switch rule.Type {
case "", C.RuleTypeDefault:
if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate {
return rule.DefaultOptions.EvaluateOptions.Server
}
return rule.DefaultOptions.RouteOptions.Server
case C.RuleTypeLogical:
if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate {
return rule.LogicalOptions.EvaluateOptions.Server
}
return rule.LogicalOptions.RouteOptions.Server
default:
return ""
@@ -1674,9 +1684,9 @@ func dnsRuleActionServer(rule option.DNSRule) string {
func dnsRuleActionEvaluateTag(rule option.DNSRule) string {
switch rule.Type {
case "", C.RuleTypeDefault:
return rule.DefaultOptions.RouteOptions.Tag
return rule.DefaultOptions.EvaluateOptions.Tag
case C.RuleTypeLogical:
return rule.LogicalOptions.RouteOptions.Tag
return rule.LogicalOptions.EvaluateOptions.Tag
default:
return ""
}
@@ -1685,8 +1695,14 @@ func dnsRuleActionEvaluateTag(rule option.DNSRule) string {
func dnsRuleActionSpeculative(rule option.DNSRule) bool {
switch rule.Type {
case "", C.RuleTypeDefault:
if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate {
return rule.DefaultOptions.EvaluateOptions.Speculative
}
return rule.DefaultOptions.RouteOptions.Speculative
case C.RuleTypeLogical:
if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate {
return rule.LogicalOptions.EvaluateOptions.Speculative
}
return rule.LogicalOptions.RouteOptions.Speculative
default:
return false
+1 -1
View File
@@ -174,7 +174,7 @@ func evaluateRule(server string, tag string, speculative bool) option.DNSRule {
DefaultOptions: option.DefaultDNSRule{
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeEvaluate,
RouteOptions: option.DNSRouteActionOptions{
EvaluateOptions: option.DNSEvaluateActionOptions{
Server: server,
Tag: tag,
Speculative: speculative,
+4 -2
View File
@@ -328,8 +328,10 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
return nil, E.New("link has no DNS servers configured")
}
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: defaultInterface.Name,
UDPFragmentDefault: true,
AbstractDialerOptions: option.AbstractDialerOptions{
BindInterface: defaultInterface.Name,
UDPFragmentDefault: true,
},
})
if err != nil {
return nil, err
+8
View File
@@ -2,6 +2,8 @@ package dns
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -44,6 +46,12 @@ func NewTransportRegistry() *TransportRegistry {
}
}
func (r *TransportRegistry) OptionTypes() []string {
r.access.Lock()
defer r.access.Unlock()
return slices.Sorted(maps.Keys(r.optionsType))
}
func (r *TransportRegistry) CreateOptions(transportType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
+2
View File
@@ -5,6 +5,7 @@ sing-box uses JSON for configuration files.
```json
{
"$schema": "https://sing-box.sagernet.org/schema.json",
"log": {},
"dns": {},
"ntp": {},
@@ -25,6 +26,7 @@ sing-box uses JSON for configuration files.
| Key | Format |
|----------------|---------------------------------|
| `$schema` | [JSON Schema](./schema/) |
| `log` | [Log](./log/) |
| `dns` | [DNS](./dns/) |
| `ntp` | [NTP](./ntp/) |
+2
View File
@@ -5,6 +5,7 @@ sing-box 使用 JSON 作为配置文件格式。
```json
{
"$schema": "https://sing-box.sagernet.org/schema.json",
"log": {},
"dns": {},
"ntp": {},
@@ -25,6 +26,7 @@ sing-box 使用 JSON 作为配置文件格式。
| Key | Format |
|----------------|------------------------|
| `$schema` | [JSON Schema](./schema/) |
| `log` | [日志](./log/) |
| `dns` | [DNS](./dns/) |
| `ntp` | [NTP](./ntp/) |
+47
View File
@@ -0,0 +1,47 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# JSON Schema
sing-box provides a JSON Schema Draft 2020-12 for configuration files.
Compatible editors can use it for completion and validation.
### Structure
```json
{
"$schema": "https://sing-box.sagernet.org/schema.json"
}
```
### Fields
#### $schema
The schema URI used by compatible editors.
This field does not affect sing-box runtime behavior.
The schema published with this documentation is available at
[sing-box.sagernet.org/schema.json](https://sing-box.sagernet.org/schema.json).
### Generate
Use the following command to generate a schema matching the installed binary:
```bash
sing-box schema -o schema.json
```
Without `--output`, the schema is written to standard output.
The generated schema reflects the features included in the current build.
You can then reference the local schema from a configuration file:
```json
{
"$schema": "./schema.json"
}
```
+47
View File
@@ -0,0 +1,47 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# JSON Schema
sing-box 为配置文件提供 JSON Schema Draft 2020-12。
兼容的编辑器可使用它提供补全和校验。
### 结构
```json
{
"$schema": "https://sing-box.sagernet.org/schema.json"
}
```
### 字段
#### $schema
兼容编辑器使用的 Schema URI。
该字段不影响 sing-box 的运行行为。
随本文档发布的 Schema 位于
[sing-box.sagernet.org/schema.json](https://sing-box.sagernet.org/schema.json)。
### 生成
使用以下命令生成与已安装的二进制文件匹配的 Schema:
```bash
sing-box schema -o schema.json
```
未指定 `--output` 时,Schema 将写入标准输出。
生成的 Schema 会反映当前构建中包含的功能。
之后可从配置文件中引用本地 Schema:
```json
{
"$schema": "./schema.json"
}
```
+17317
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -2,12 +2,16 @@ package main
import (
"context"
"reflect"
"time"
"github.com/sagernet/sing-box/common/networkquality"
"github.com/sagernet/sing-box/common/stun"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/schema"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@@ -38,6 +42,14 @@ func (s *applicationService) FormatConfig(ctx context.Context, request *ConfigCo
return &ConfigContent{Content: content}, nil
}
func (s *applicationService) GenerateConfigSchema(ctx context.Context, request *emptypb.Empty) (*ConfigContent, error) {
content, err := schema.Generate(include.Context(context.Background()), reflect.TypeFor[option.Options]())
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &ConfigContent{Content: string(content)}, nil
}
func (s *applicationService) EncodeProfile(ctx context.Context, request *ProfileContent) (*ProfileData, error) {
content := libbox.ProfileContent{
Name: request.Name,
+40 -38
View File
@@ -6,7 +6,6 @@ import (
unsafe "unsafe"
daemon "github.com/sagernet/sing-box/daemon"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
@@ -1743,10 +1742,11 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" +
"\x13DeleteAllOOMReports\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12P\n" +
"\rInstallUpdate\x12\x1d.desktop.InstallUpdateRequest\x1a\x1e.desktop.InstallUpdateResponse\"\x00\x12J\n" +
"\x13GetSecuritySettings\x12\x16.google.protobuf.Empty\x1a\x19.desktop.SecuritySettings\"\x00\x12Z\n" +
"\x16SetInsecureModeEnabled\x12&.desktop.SetInsecureModeEnabledRequest\x1a\x16.google.protobuf.Empty\"\x002\xbd\x04\n" +
"\x16SetInsecureModeEnabled\x12&.desktop.SetInsecureModeEnabledRequest\x1a\x16.google.protobuf.Empty\"\x002\x87\x05\n" +
"\x12ApplicationService\x12?\n" +
"\vCheckConfig\x12\x16.desktop.ConfigContent\x1a\x16.google.protobuf.Empty\"\x00\x12@\n" +
"\fFormatConfig\x12\x16.desktop.ConfigContent\x1a\x16.desktop.ConfigContent\"\x00\x12@\n" +
"\fFormatConfig\x12\x16.desktop.ConfigContent\x1a\x16.desktop.ConfigContent\"\x00\x12H\n" +
"\x14GenerateConfigSchema\x12\x16.google.protobuf.Empty\x1a\x16.desktop.ConfigContent\"\x00\x12@\n" +
"\rEncodeProfile\x12\x17.desktop.ProfileContent\x1a\x14.desktop.ProfileData\"\x00\x12@\n" +
"\rDecodeProfile\x12\x14.desktop.ProfileData\x1a\x17.desktop.ProfileContent\"\x00\x12H\n" +
"\rArchiveReport\x12\x1d.desktop.ArchiveReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12y\n" +
@@ -1837,41 +1837,43 @@ var file_experimental_boxdd_desktop_service_proto_depIdxs = []int32{
27, // 28: desktop.DesktopService.SetInsecureModeEnabled:input_type -> desktop.SetInsecureModeEnabledRequest
9, // 29: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent
9, // 30: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent
10, // 31: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent
11, // 32: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData
3, // 33: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest
4, // 34: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest
5, // 35: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest
6, // 36: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo
30, // 37: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty
30, // 38: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty
30, // 39: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty
12, // 40: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo
30, // 41: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty
13, // 42: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList
17, // 43: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent
30, // 44: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty
19, // 45: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive
30, // 46: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty
30, // 47: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty
20, // 48: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList
24, // 49: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent
30, // 50: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty
19, // 51: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive
30, // 52: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty
30, // 53: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty
29, // 54: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse
26, // 55: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings
30, // 56: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty
30, // 57: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty
9, // 58: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent
11, // 59: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData
10, // 60: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent
30, // 61: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty
31, // 62: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
32, // 63: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress
36, // [36:64] is the sub-list for method output_type
8, // [8:36] is the sub-list for method input_type
30, // 31: desktop.ApplicationService.GenerateConfigSchema:input_type -> google.protobuf.Empty
10, // 32: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent
11, // 33: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData
3, // 34: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest
4, // 35: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest
5, // 36: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest
6, // 37: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo
30, // 38: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty
30, // 39: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty
30, // 40: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty
12, // 41: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo
30, // 42: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty
13, // 43: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList
17, // 44: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent
30, // 45: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty
19, // 46: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive
30, // 47: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty
30, // 48: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty
20, // 49: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList
24, // 50: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent
30, // 51: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty
19, // 52: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive
30, // 53: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty
30, // 54: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty
29, // 55: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse
26, // 56: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings
30, // 57: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty
30, // 58: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty
9, // 59: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent
9, // 60: desktop.ApplicationService.GenerateConfigSchema:output_type -> desktop.ConfigContent
11, // 61: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData
10, // 62: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent
30, // 63: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty
31, // 64: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
32, // 65: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress
37, // [37:66] is the sub-list for method output_type
8, // [8:37] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
+1
View File
@@ -33,6 +33,7 @@ service DesktopService {
service ApplicationService {
rpc CheckConfig(ConfigContent) returns (google.protobuf.Empty) {}
rpc FormatConfig(ConfigContent) returns (ConfigContent) {}
rpc GenerateConfigSchema(google.protobuf.Empty) returns (ConfigContent) {}
rpc EncodeProfile(ProfileContent) returns (ProfileData) {}
rpc DecodeProfile(ProfileData) returns (ProfileContent) {}
rpc ArchiveReport(ArchiveReportRequest) returns (google.protobuf.Empty) {}
+39 -1
View File
@@ -4,7 +4,6 @@ import (
context "context"
daemon "github.com/sagernet/sing-box/daemon"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
@@ -901,6 +900,7 @@ var DesktopService_ServiceDesc = grpc.ServiceDesc{
const (
ApplicationService_CheckConfig_FullMethodName = "/desktop.ApplicationService/CheckConfig"
ApplicationService_FormatConfig_FullMethodName = "/desktop.ApplicationService/FormatConfig"
ApplicationService_GenerateConfigSchema_FullMethodName = "/desktop.ApplicationService/GenerateConfigSchema"
ApplicationService_EncodeProfile_FullMethodName = "/desktop.ApplicationService/EncodeProfile"
ApplicationService_DecodeProfile_FullMethodName = "/desktop.ApplicationService/DecodeProfile"
ApplicationService_ArchiveReport_FullMethodName = "/desktop.ApplicationService/ArchiveReport"
@@ -914,6 +914,7 @@ const (
type ApplicationServiceClient interface {
CheckConfig(ctx context.Context, in *ConfigContent, opts ...grpc.CallOption) (*emptypb.Empty, error)
FormatConfig(ctx context.Context, in *ConfigContent, opts ...grpc.CallOption) (*ConfigContent, error)
GenerateConfigSchema(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ConfigContent, error)
EncodeProfile(ctx context.Context, in *ProfileContent, opts ...grpc.CallOption) (*ProfileData, error)
DecodeProfile(ctx context.Context, in *ProfileData, opts ...grpc.CallOption) (*ProfileContent, error)
ArchiveReport(ctx context.Context, in *ArchiveReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
@@ -949,6 +950,16 @@ func (c *applicationServiceClient) FormatConfig(ctx context.Context, in *ConfigC
return out, nil
}
func (c *applicationServiceClient) GenerateConfigSchema(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ConfigContent, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ConfigContent)
err := c.cc.Invoke(ctx, ApplicationService_GenerateConfigSchema_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *applicationServiceClient) EncodeProfile(ctx context.Context, in *ProfileContent, opts ...grpc.CallOption) (*ProfileData, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ProfileData)
@@ -1023,6 +1034,7 @@ type ApplicationService_StartStandaloneSTUNTestClient = grpc.ServerStreamingClie
type ApplicationServiceServer interface {
CheckConfig(context.Context, *ConfigContent) (*emptypb.Empty, error)
FormatConfig(context.Context, *ConfigContent) (*ConfigContent, error)
GenerateConfigSchema(context.Context, *emptypb.Empty) (*ConfigContent, error)
EncodeProfile(context.Context, *ProfileContent) (*ProfileData, error)
DecodeProfile(context.Context, *ProfileData) (*ProfileContent, error)
ArchiveReport(context.Context, *ArchiveReportRequest) (*emptypb.Empty, error)
@@ -1046,6 +1058,10 @@ func (UnimplementedApplicationServiceServer) FormatConfig(context.Context, *Conf
return nil, status.Error(codes.Unimplemented, "method FormatConfig not implemented")
}
func (UnimplementedApplicationServiceServer) GenerateConfigSchema(context.Context, *emptypb.Empty) (*ConfigContent, error) {
return nil, status.Error(codes.Unimplemented, "method GenerateConfigSchema not implemented")
}
func (UnimplementedApplicationServiceServer) EncodeProfile(context.Context, *ProfileContent) (*ProfileData, error) {
return nil, status.Error(codes.Unimplemented, "method EncodeProfile not implemented")
}
@@ -1122,6 +1138,24 @@ func _ApplicationService_FormatConfig_Handler(srv interface{}, ctx context.Conte
return interceptor(ctx, in, info, handler)
}
func _ApplicationService_GenerateConfigSchema_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApplicationServiceServer).GenerateConfigSchema(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ApplicationService_GenerateConfigSchema_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApplicationServiceServer).GenerateConfigSchema(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _ApplicationService_EncodeProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ProfileContent)
if err := dec(in); err != nil {
@@ -1213,6 +1247,10 @@ var ApplicationService_ServiceDesc = grpc.ServiceDesc{
MethodName: "FormatConfig",
Handler: _ApplicationService_FormatConfig_Handler,
},
{
MethodName: "GenerateConfigSchema",
Handler: _ApplicationService_GenerateConfigSchema_Handler,
},
{
MethodName: "EncodeProfile",
Handler: _ApplicationService_EncodeProfile_Handler,
+10
View File
@@ -5,6 +5,7 @@ import (
"context"
"net/netip"
"os"
"reflect"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
@@ -13,6 +14,7 @@ import (
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/schema"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
@@ -241,6 +243,14 @@ func (s *interfaceMonitorStub) MyInterfaces() []string {
return nil
}
func GenerateConfigSchema() (*StringBox, error) {
content, err := schema.Generate(baseContext(nil), reflect.TypeFor[option.Options]())
if err != nil {
return nil, err
}
return wrapString(string(content)), nil
}
func FormatConfig(configContent string) (*StringBox, error) {
options, err := parseConfig(baseContext(nil), configContent)
if err != nil {
+1
View File
@@ -81,6 +81,7 @@ nav:
- AnyTLS client metadata: manual/misc/anytls-client-metadata.md
- Configuration:
- configuration/index.md
- JSON Schema: configuration/schema.md
- Log:
- configuration/log/index.md
- DNS:
+23 -5
View File
@@ -1,9 +1,11 @@
package option
import (
"reflect"
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -23,21 +25,25 @@ type ACMECertificateProviderOptions struct {
AlternativeTLSPort uint16 `json:"alternative_tls_port,omitempty"`
ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"`
DNS01Challenge *ACMEProviderDNS01ChallengeOptions `json:"dns01_challenge,omitempty"`
KeyType ACMEKeyType `json:"key_type,omitempty"`
KeyType ACMEKeyType `json:"key_type,omitempty" enum:"ed25519,p256,p384,rsa2048,rsa4096"`
Profile string `json:"profile,omitempty"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
}
type _ACMEProviderDNS01ChallengeOptions struct {
AbstractACMEProviderDNS01ChallengeOptions
Provider string `json:"provider,omitempty" enum:"alidns,cloudflare,acmedns"`
AliDNSOptions ACMEDNS01AliDNSOptions `json:"-"`
CloudflareOptions ACMEDNS01CloudflareOptions `json:"-"`
ACMEDNSOptions ACMEDNS01ACMEDNSOptions `json:"-"`
}
type AbstractACMEProviderDNS01ChallengeOptions struct {
TTL badoption.Duration `json:"ttl,omitempty"`
PropagationDelay badoption.Duration `json:"propagation_delay,omitempty"`
PropagationTimeout badoption.Duration `json:"propagation_timeout,omitempty"`
Resolvers badoption.Listable[string] `json:"resolvers,omitempty"`
OverrideDomain string `json:"override_domain,omitempty"`
Provider string `json:"provider,omitempty"`
AliDNSOptions ACMEDNS01AliDNSOptions `json:"-"`
CloudflareOptions ACMEDNS01CloudflareOptions `json:"-"`
ACMEDNSOptions ACMEDNS01ACMEDNSOptions `json:"-"`
}
type ACMEProviderDNS01ChallengeOptions _ACMEProviderDNS01ChallengeOptions
@@ -80,6 +86,14 @@ func (o *ACMEProviderDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error {
return badjson.UnmarshallExcluded(bytes, (*_ACMEProviderDNS01ChallengeOptions)(o), v)
}
func (o ACMEProviderDNS01ChallengeOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("ACMEProviderDNS01Challenge", func() (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "provider", true, acmeDNS01Variants(), func(variant *schema.Node) error {
return builder.FlattenStruct(variant, reflect.TypeFor[AbstractACMEProviderDNS01ChallengeOptions]())
})
})
}
type ACMEKeyType string
const (
@@ -105,3 +119,7 @@ func (t *ACMEKeyType) UnmarshalJSON(data []byte) error {
}
return nil
}
func (t ACMEKeyType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("", "ed25519", "p256", "p384", "rsa2048", "rsa4096"), nil
}
+13 -1
View File
@@ -1,6 +1,9 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
)
@@ -8,7 +11,7 @@ import (
type APIServiceOptions struct {
ListenOptions
Secret string `json:"secret,omitempty"`
AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty" examples:"http://sing-box-dashboard.sagernet.org/,https://sing-box-dashboard.sagernet.org/"`
AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
Dashboard *APIDashboardOptions `json:"dashboard,omitempty"`
InboundTLSOptionsContainer
@@ -48,3 +51,12 @@ func (o *APIDashboardOptions) UnmarshalJSON(bytes []byte) error {
}
return json.UnmarshalDisallowUnknownFields(bytes, (*_APIDashboardOptions)(o))
}
func (o APIDashboardOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
objectForm := schema.StrictObject()
err := builder.FlattenStruct(objectForm, reflect.TypeFor[APIDashboardOptions]())
if err != nil {
return nil, err
}
return schema.AnyOf(schema.BooleanNode(), schema.StringNode(), objectForm), nil
}
+1 -1
View File
@@ -10,7 +10,7 @@ type CCMServiceOptions struct {
CredentialPath string `json:"credential_path,omitempty"`
Users []CCMUser `json:"users,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
Detour string `json:"detour,omitempty"`
Detour string `json:"detour,omitempty" reference:"outbound"`
UsagesPath string `json:"usages_path,omitempty"`
}
+13 -1
View File
@@ -1,13 +1,16 @@
package option
import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
)
type _CertificateOptions struct {
Store string `json:"store,omitempty"`
Store string `json:"store,omitempty" enum:"system,mozilla,chrome,none"`
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
@@ -34,3 +37,12 @@ func (o *CertificateOptions) UnmarshalJSON(data []byte) error {
}
return nil
}
func (o CertificateOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
node := schema.StrictObject()
err := builder.FlattenStruct(node, reflect.TypeFor[CertificateOptions]())
if err != nil {
return nil, err
}
return node, nil
}
+26
View File
@@ -3,6 +3,7 @@ package option
import (
"context"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -10,6 +11,7 @@ import (
)
type CertificateProviderOptionsRegistry interface {
OptionTypes() []string
CreateOptions(providerType string) (any, bool)
}
@@ -46,6 +48,16 @@ func (h *CertificateProvider) UnmarshalJSONContext(ctx context.Context, content
return nil
}
func (h CertificateProvider) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("CertificateProvider", func() (*schema.Node, error) {
registry := service.FromContext[CertificateProviderOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing certificate provider options registry in context")
}
return registryUnion(builder, registry, nil, true)
})
}
type CertificateProviderOptions struct {
Tag string `json:"-"`
Type string `json:"-"`
@@ -98,3 +110,17 @@ func (o *CertificateProviderOptions) UnmarshalJSONContext(ctx context.Context, c
func (o *CertificateProviderOptions) IsShared() bool {
return o.Tag != ""
}
func (o CertificateProviderOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("CertificateProviderReference", func() (*schema.Node, error) {
registry := service.FromContext[CertificateProviderOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing certificate provider options registry in context")
}
union, err := registryUnion(builder, registry, nil, false)
if err != nil {
return nil, err
}
return schema.AnyOf(schema.TagReferenceNode("certificate_provider"), union), nil
})
}
+3 -3
View File
@@ -5,10 +5,10 @@ import "github.com/sagernet/sing/common/json/badoption"
type CloudflaredInboundOptions struct {
Token string `json:"token,omitempty"`
HighAvailabilityConnections int `json:"ha_connections,omitempty"`
Protocol string `json:"protocol,omitempty"`
Protocol string `json:"protocol,omitempty" enum:"auto,quic,http2,h2mux"`
PostQuantum bool `json:"post_quantum,omitempty"`
EdgeIPVersion int `json:"edge_ip_version,omitempty"`
DatagramVersion string `json:"datagram_version,omitempty"`
EdgeIPVersion int `json:"edge_ip_version,omitempty" enum:"0,4,6"`
DatagramVersion string `json:"datagram_version,omitempty" enum:"v2,v3"`
GracePeriod badoption.Duration `json:"grace_period,omitempty"`
Region string `json:"region,omitempty"`
ControlDialer DialerOptions `json:"control_dialer,omitempty"`
+3 -3
View File
@@ -17,11 +17,11 @@ type DirectInboundOptions struct {
type _DirectOutboundOptions struct {
DialerOptions
// Deprecated: Use Route Action instead
OverrideAddress string `json:"override_address,omitempty"`
OverrideAddress string `json:"override_address,omitempty" schema:"omit"`
// Deprecated: Use Route Action instead
OverridePort uint16 `json:"override_port,omitempty"`
OverridePort uint16 `json:"override_port,omitempty" schema:"omit"`
// Deprecated: removed
ProxyProtocol uint8 `json:"proxy_protocol,omitempty"`
ProxyProtocol uint8 `json:"proxy_protocol,omitempty" schema:"omit"`
}
type DirectOutboundOptions _DirectOutboundOptions
+37 -4
View File
@@ -3,8 +3,10 @@ package option
import (
"context"
"net/netip"
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -16,7 +18,7 @@ import (
type RawDNSOptions struct {
Servers []DNSServerOptions `json:"servers,omitempty"`
Rules []DNSRule `json:"rules,omitempty"`
Final string `json:"final,omitempty"`
Final string `json:"final,omitempty" reference:"dns_server"`
ReverseMapping bool `json:"reverse_mapping,omitempty"`
DNSClientOptions
}
@@ -46,12 +48,23 @@ func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) e
return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions)
}
func (o DNSOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DNS", func() (*schema.Node, error) {
node := schema.StrictObject()
err := builder.FlattenStruct(node, reflect.TypeFor[RawDNSOptions]())
if err != nil {
return nil, err
}
return node, nil
})
}
type DNSClientOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableExpire bool `json:"disable_expire,omitempty"`
IndependentCache bool `json:"independent_cache,omitempty"`
IndependentCache bool `json:"independent_cache,omitempty" schema:"omit"`
CacheCapacity uint32 `json:"cache_capacity,omitempty"`
Optimistic *OptimisticDNSOptions `json:"optimistic,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
@@ -79,7 +92,17 @@ func (o *OptimisticDNSOptions) UnmarshalJSON(bytes []byte) error {
return json.UnmarshalDisallowUnknownFields(bytes, (*_OptimisticDNSOptions)(o))
}
func (o OptimisticDNSOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
objectForm := schema.StrictObject()
err := builder.FlattenStruct(objectForm, reflect.TypeFor[OptimisticDNSOptions]())
if err != nil {
return nil, err
}
return schema.AnyOf(schema.BooleanNode(), objectForm), nil
}
type DNSTransportOptionsRegistry interface {
OptionTypes() []string
CreateOptions(transportType string) (any, bool)
}
type _DNSServerOptions struct {
@@ -122,6 +145,16 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b
return nil
}
func (o DNSServerOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DNSServer", func() (*schema.Node, error) {
registry := service.FromContext[DNSTransportOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing DNS transport options registry in context")
}
return registryUnion(builder, registry, nil, true)
})
}
type DNSServerAddressOptions struct {
Server string `json:"server"`
ServerPort uint16 `json:"server_port,omitempty"`
@@ -176,8 +209,8 @@ type RemoteHTTPSDNSServerOptions struct {
}
type FakeIPDNSServerOptions struct {
Inet4Range *badoption.Prefix `json:"inet4_range,omitempty"`
Inet6Range *badoption.Prefix `json:"inet6_range,omitempty"`
Inet4Range *badoption.Prefix `json:"inet4_range,omitempty" examples:"198.18.0.0/15"`
Inet6Range *badoption.Prefix `json:"inet6_range,omitempty" examples:"fc00::/18"`
}
type DHCPDNSServerOptions struct {
+44
View File
@@ -1,9 +1,12 @@
package option
import (
"cmp"
"encoding/base64"
"slices"
"strings"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
@@ -51,6 +54,43 @@ func (r *DNSRCode) Build() int {
return int(*r)
}
func (r DNSRCode) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DNSRCode", func() (*schema.Node, error) {
type rCodeName struct {
name string
value int
canonical bool
}
rCodeNames := make([]rCodeName, 0, len(dns.StringToRcode))
for name, value := range dns.StringToRcode {
canonicalName, canonical := dns.RcodeToString[value]
rCodeNames = append(rCodeNames, rCodeName{
name: name,
value: value,
canonical: canonical && canonicalName == name,
})
}
slices.SortFunc(rCodeNames, func(left rCodeName, right rCodeName) int {
comparison := cmp.Compare(left.value, right.value)
if comparison != 0 {
return comparison
}
if left.canonical != right.canonical {
if left.canonical {
return -1
}
return 1
}
return cmp.Compare(left.name, right.name)
})
values := make([]string, 0, len(rCodeNames))
for _, entry := range rCodeNames {
values = append(values, entry.name)
}
return schema.AnyOf(schema.IntegerNode(), schema.StringEnum(values...)), nil
})
}
type DNSRecordOptions struct {
dns.RR
fromBase64 bool
@@ -123,3 +163,7 @@ func (o DNSRecordOptions) Match(record dns.RR) bool {
}
return dns.IsDuplicate(o.RR, record)
}
func (o DNSRecordOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringNode(), nil
}
+4
View File
@@ -13,6 +13,10 @@ import (
type stubDNSTransportOptionsRegistry struct{}
func (stubDNSTransportOptionsRegistry) OptionTypes() []string {
return []string{C.DNSTypeUDP, C.DNSTypeFakeIP}
}
func (stubDNSTransportOptionsRegistry) CreateOptions(transportType string) (any, bool) {
switch transportType {
case C.DNSTypeUDP:
+12
View File
@@ -3,6 +3,7 @@ package option
import (
"context"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -10,6 +11,7 @@ import (
)
type EndpointOptionsRegistry interface {
OptionTypes() []string
CreateOptions(endpointType string) (any, bool)
}
@@ -45,3 +47,13 @@ func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) err
h.Options = options
return nil
}
func (h Endpoint) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("Endpoint", func() (*schema.Node, error) {
registry := service.FromContext[EndpointOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing endpoint options registry in context")
}
return registryUnion(builder, registry, nil, true)
})
}
+7 -7
View File
@@ -14,7 +14,7 @@ type CacheFileOptions struct {
Path string `json:"path,omitempty"`
CacheID string `json:"cache_id,omitempty"`
StoreFakeIP bool `json:"store_fakeip,omitempty"`
StoreRDRC bool `json:"store_rdrc,omitempty"`
StoreRDRC bool `json:"store_rdrc,omitempty" schema:"omit"`
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
StoreDNS bool `json:"store_dns,omitempty"`
}
@@ -23,7 +23,7 @@ type ClashAPIOptions struct {
ExternalController string `json:"external_controller,omitempty"`
ExternalUI string `json:"external_ui,omitempty"`
ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"`
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty" reference:"outbound"`
Secret string `json:"secret,omitempty"`
DefaultMode string `json:"default_mode,omitempty"`
ModeList []string `json:"-"`
@@ -31,15 +31,15 @@ type ClashAPIOptions struct {
AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
// Deprecated: migrated to global cache file
CacheFile string `json:"cache_file,omitempty"`
CacheFile string `json:"cache_file,omitempty" schema:"omit"`
// Deprecated: migrated to global cache file
CacheID string `json:"cache_id,omitempty"`
CacheID string `json:"cache_id,omitempty" schema:"omit"`
// Deprecated: migrated to global cache file
StoreMode bool `json:"store_mode,omitempty"`
StoreMode bool `json:"store_mode,omitempty" schema:"omit"`
// Deprecated: migrated to global cache file
StoreSelected bool `json:"store_selected,omitempty"`
StoreSelected bool `json:"store_selected,omitempty" schema:"omit"`
// Deprecated: migrated to global cache file
StoreFakeIP bool `json:"store_fakeip,omitempty"`
StoreFakeIP bool `json:"store_fakeip,omitempty" schema:"omit"`
}
type V2RayAPIOptions struct {
+3 -3
View File
@@ -3,13 +3,13 @@ package option
import "github.com/sagernet/sing/common/json/badoption"
type SelectorOutboundOptions struct {
Outbounds []string `json:"outbounds"`
Default string `json:"default,omitempty"`
Outbounds []string `json:"outbounds" reference:"outbound"`
Default string `json:"default,omitempty" reference:"outbound"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
}
type URLTestOutboundOptions struct {
Outbounds []string `json:"outbounds"`
Outbounds []string `json:"outbounds" reference:"outbound"`
URL string `json:"url,omitempty"`
Interval badoption.Duration `json:"interval,omitempty"`
Tolerance uint16 `json:"tolerance,omitempty"`
+33 -2
View File
@@ -3,6 +3,7 @@ package option
import (
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
@@ -26,8 +27,8 @@ type QUICOptions struct {
type _HTTPClientOptions struct {
Tag string `json:"tag,omitempty"`
Engine string `json:"engine,omitempty"`
Version int `json:"version,omitempty"`
Engine string `json:"engine,omitempty" enum:"go,apple"`
Version int `json:"version,omitempty" enum:"0,1,2,3"`
DisableVersionFallback bool `json:"disable_version_fallback,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
HTTP2Options HTTP2Options `json:"-"`
@@ -125,3 +126,33 @@ func httpClientVariant(options _HTTPClientOptions) any {
return nil
}
}
func describeHTTPClientObject(builder schema.Builder) (*schema.Node, error) {
node := schema.StrictObject()
err := builder.FlattenStruct(node, reflect.TypeFor[HTTPClient]())
if err != nil {
return nil, err
}
err = builder.FlattenStruct(node, reflect.TypeFor[QUICOptions]())
if err != nil {
return nil, err
}
return node, nil
}
func (h HTTPClient) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("HTTPClient", func() (*schema.Node, error) {
return describeHTTPClientObject(builder)
})
}
func (o HTTPClientOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("HTTPClientReference", func() (*schema.Node, error) {
clientObject, err := describeHTTPClientObject(builder)
if err != nil {
return nil, err
}
clientObject.Properties.Remove("tag")
return schema.AnyOf(schema.TagReferenceNode("http_client"), clientObject), nil
})
}
+7 -7
View File
@@ -14,13 +14,13 @@ type HysteriaInboundOptions struct {
Obfs string `json:"obfs,omitempty"`
Users []HysteriaUser `json:"users,omitempty"`
// Deprecated: use QUIC fields instead
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"`
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
MaxConnClient int `json:"max_conn_client,omitempty"`
MaxConnClient int `json:"max_conn_client,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty" schema:"omit"`
InboundTLSOptionsContainer
QUICOptions
}
@@ -44,11 +44,11 @@ type HysteriaOutboundOptions struct {
Auth []byte `json:"auth,omitempty"`
AuthString string `json:"auth_str,omitempty"`
// Deprecated: use QUIC fields instead
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
ReceiveWindow uint64 `json:"recv_window,omitempty"`
ReceiveWindow uint64 `json:"recv_window,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty" schema:"omit"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
QUICOptions
+29 -5
View File
@@ -2,8 +2,10 @@ package option
import (
"net/url"
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -20,7 +22,7 @@ type Hysteria2InboundOptions struct {
InboundTLSOptionsContainer
QUICOptions
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
BBRProfile string `json:"bbr_profile,omitempty"`
BBRProfile string `json:"bbr_profile,omitempty" enum:"standard,conservative,aggressive"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
Realm *Hysteria2InboundRealm `json:"realm,omitempty"`
}
@@ -30,7 +32,7 @@ type Hysteria2Realm struct {
Token string `json:"token,omitempty"`
RealmID string `json:"realm_id"`
STUNServers badoption.Listable[string] `json:"stun_servers"`
IPVersion int `json:"ip_version,omitempty"`
IPVersion int `json:"ip_version,omitempty" enum:"0,4,6"`
PortMapping *Hysteria2RealmPortMapping `json:"port_mapping,omitempty"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
}
@@ -52,7 +54,7 @@ type Hysteria2ObfsGecko struct {
}
type _Hysteria2Obfs struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"salamander,gecko"`
Password string `json:"password,omitempty"`
GeckoOptions Hysteria2ObfsGecko `json:"-"`
}
@@ -93,13 +95,23 @@ func (o *Hysteria2Obfs) UnmarshalJSON(bytes []byte) error {
return badjson.UnmarshallExcluded(bytes, (*_Hysteria2Obfs)(o), v)
}
func (o Hysteria2Obfs) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "type", true, []schema.UnionVariant{
{Value: C.Hysteria2ObfsTypeSalamander},
{Value: C.Hysteria2ObfsTypeGecko, StructType: reflect.TypeFor[Hysteria2ObfsGecko]()},
}, func(variant *schema.Node) error {
variant.Properties.Put("password", schema.StringNode())
return nil
})
}
type Hysteria2User struct {
Name string `json:"name,omitempty"`
Password string `json:"password,omitempty"`
}
type _Hysteria2Masquerade struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"file,proxy,string"`
FileOptions Hysteria2MasqueradeFile `json:"-"`
ProxyOptions Hysteria2MasqueradeProxy `json:"-"`
StringOptions Hysteria2MasqueradeString `json:"-"`
@@ -160,6 +172,18 @@ func (m *Hysteria2Masquerade) UnmarshalJSON(bytes []byte) error {
return badjson.UnmarshallExcluded(bytes, (*_Hysteria2Masquerade)(m), v)
}
func (m Hysteria2Masquerade) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
union, err := schema.DiscriminatedUnion(builder, "type", true, []schema.UnionVariant{
{Value: C.Hysterai2MasqueradeTypeFile, StructType: reflect.TypeFor[Hysteria2MasqueradeFile]()},
{Value: C.Hysterai2MasqueradeTypeProxy, StructType: reflect.TypeFor[Hysteria2MasqueradeProxy]()},
{Value: C.Hysterai2MasqueradeTypeString, StructType: reflect.TypeFor[Hysteria2MasqueradeString]()},
}, nil)
if err != nil {
return nil, err
}
return schema.AnyOf(schema.StringNode(), union), nil
}
type Hysteria2MasqueradeFile struct {
Directory string `json:"directory"`
}
@@ -188,7 +212,7 @@ type Hysteria2OutboundOptions struct {
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
QUICOptions
BBRProfile string `json:"bbr_profile,omitempty"`
BBRProfile string `json:"bbr_profile,omitempty" enum:"standard,conservative,aggressive"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
Realm *Hysteria2Realm `json:"realm,omitempty"`
}
+35 -10
View File
@@ -4,6 +4,8 @@ import (
"context"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -12,6 +14,7 @@ import (
)
type InboundOptionsRegistry interface {
OptionTypes() []string
CreateOptions(outboundType string) (any, bool)
}
@@ -54,13 +57,23 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
return nil
}
func (h Inbound) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("Inbound", func() (*schema.Node, error) {
registry := service.FromContext[InboundOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing inbound options registry in context")
}
return registryUnion(builder, registry, []string{C.TypeShadowsocksR}, true)
})
}
// Deprecated: Use rule action instead
type InboundOptions struct {
SniffEnabled bool `json:"sniff,omitempty"`
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"`
SniffTimeout badoption.Duration `json:"sniff_timeout,omitempty"`
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"`
SniffEnabled bool `json:"sniff,omitempty" schema:"omit"`
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty" schema:"omit"`
SniffTimeout badoption.Duration `json:"sniff_timeout,omitempty" schema:"omit"`
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty" schema:"omit"`
UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty" schema:"omit"`
}
type ListenOptions struct {
@@ -69,7 +82,7 @@ type ListenOptions struct {
BindInterface string `json:"bind_interface,omitempty"`
RoutingMark FwMark `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,omitempty"`
NetNs string `json:"netns,omitempty"`
NetNs string `json:"netns,omitempty" reference:"network_namespace"`
DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"`
TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"`
TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"`
@@ -78,13 +91,15 @@ type ListenOptions struct {
UDPFragment *bool `json:"udp_fragment,omitempty"`
UDPFragmentDefault bool `json:"-"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
Detour string `json:"detour,omitempty"`
Detour string `json:"detour,omitempty" reference:"inbound"`
// Deprecated: removed
ProxyProtocol bool `json:"proxy_protocol,omitempty"`
ProxyProtocol bool `json:"proxy_protocol,omitempty" schema:"omit"`
// Deprecated: removed
ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty"`
InboundOptions
ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty" schema:"omit"`
// Legacy inbound fields are rejected since sing-box 1.13.0.
//nolint:staticcheck
InboundOptions `schema:"omit"`
}
type UDPNATBehavior uint8
@@ -129,6 +144,10 @@ func (b *UDPNATBehavior) UnmarshalJSON(data []byte) error {
return nil
}
func (b UDPNATBehavior) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("", "endpoint_independent", "address_dependent", "address_and_port_dependent"), nil
}
type UDPTimeoutCompat badoption.Duration
func (c UDPTimeoutCompat) MarshalJSON() ([]byte, error) {
@@ -145,6 +164,12 @@ func (c *UDPTimeoutCompat) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, (*badoption.Duration)(c))
}
func (c UDPTimeoutCompat) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("UDPTimeout", func() (*schema.Node, error) {
return schema.AnyOf(schema.UnsignedNode(32), schema.DurationNode()), nil
})
}
type ListenOptionsWrapper interface {
TakeListenOptions() ListenOptions
ReplaceListenOptions(options ListenOptions)
+1 -1
View File
@@ -8,7 +8,7 @@ type InboundMultiplexOptions struct {
type OutboundMultiplexOptions struct {
Enabled bool `json:"enabled,omitempty"`
Protocol string `json:"protocol,omitempty"`
Protocol string `json:"protocol,omitempty" enum:"h2mux,smux,yamux"`
MaxConnections int `json:"max_connections,omitempty"`
MinStreams int `json:"min_streams,omitempty"`
MaxStreams int `json:"max_streams,omitempty"`
+2 -2
View File
@@ -20,7 +20,7 @@ type NaiveInboundOptions struct {
ListenOptions
Users []auth.User `json:"users,omitempty"`
Network NetworkList `json:"network,omitempty"`
QUICCongestionControl string `json:"quic_congestion_control,omitempty"`
QUICCongestionControl string `json:"quic_congestion_control,omitempty" enum:"bbr,bbr_standard,bbr2,bbr2_variant,cubic,reno"`
InboundTLSOptionsContainer
}
@@ -34,7 +34,7 @@ type NaiveOutboundOptions struct {
ReceiveWindow *byteformats.MemoryBytes `json:"stream_receive_window,omitempty"`
UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"`
QUIC bool `json:"quic,omitempty"`
QUICCongestionControl string `json:"quic_congestion_control,omitempty"`
QUICCongestionControl string `json:"quic_congestion_control,omitempty" enum:"bbr,bbr2,cubic,reno"`
QUICSessionReceiveWindow *byteformats.MemoryBytes `json:"quic_session_receive_window,omitempty"`
OutboundTLSOptionsContainer
}
+16
View File
@@ -1,7 +1,10 @@
package option
import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -48,6 +51,19 @@ func (o *NetworkNamespace) UnmarshalJSON(content []byte) error {
return badjson.UnmarshallExcluded(content, (*_NetworkNamespace)(o), v)
}
func (o NetworkNamespace) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("NetworkNamespace", func() (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "type", false, []schema.UnionVariant{
{Value: C.NetNsTypeDefault, StructType: reflect.TypeFor[DefaultNetworkNamespaceOptions](), TypeOptional: true},
{Value: C.NetNsTypeUnshare, StructType: reflect.TypeFor[UnshareNetworkNamespaceOptions]()},
}, func(variant *schema.Node) error {
variant.Properties.Put("tag", schema.StringNode())
variant.Required = append(variant.Required, "tag")
return nil
})
})
}
type DefaultNetworkNamespaceOptions struct {
Path string `json:"path"`
}
+1 -1
View File
@@ -10,7 +10,7 @@ type OCMServiceOptions struct {
CredentialPath string `json:"credential_path,omitempty"`
Users []OCMUser `json:"users,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
Detour string `json:"detour,omitempty"`
Detour string `json:"detour,omitempty" reference:"outbound"`
UsagesPath string `json:"usages_path,omitempty"`
}
+3 -3
View File
@@ -11,7 +11,7 @@ type OpenConnectEndpointOptions struct {
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
Server string `json:"server"`
Flavor string `json:"flavor,omitempty"`
Flavor string `json:"flavor,omitempty" enum:"anyconnect,gp,fortinet,f5,pulse,nc"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
AuthGroup string `json:"auth_group,omitempty"`
@@ -29,7 +29,7 @@ type OpenConnectEndpointOptions struct {
NoUDP bool `json:"no_udp,omitempty"`
DTLSLocalPort uint16 `json:"dtls_local_port,omitempty"`
CompressionDisabled bool `json:"compression_disabled,omitempty"`
CompressionMode string `json:"compression_mode,omitempty"`
CompressionMode string `json:"compression_mode,omitempty" enum:"stateless,all"`
IPv6Disabled bool `json:"ipv6_disabled,omitempty"`
HTTPKeepAliveDisabled bool `json:"http_keepalive_disabled,omitempty"`
XMLPostDisabled bool `json:"xml_post_disabled,omitempty"`
@@ -49,7 +49,7 @@ type OpenConnectEndpointOptions struct {
}
type OpenConnectTokenOptions struct {
Mode string `json:"mode,omitempty"`
Mode string `json:"mode,omitempty" enum:"totp,hotp,stoken,oidc"`
Secret string `json:"secret,omitempty"`
SecretPath string `json:"secret_path,omitempty"`
PIN string `json:"pin,omitempty"`
+35 -35
View File
@@ -20,22 +20,22 @@ type OpenVPNClientEndpointOptions struct {
DialerOptions
ServerOptions
OpenVPNEndpointOptions
Mode string `json:"mode,omitempty"`
Network string `json:"network,omitempty"`
Mode string `json:"mode,omitempty" enum:"tls,static_key"`
Network string `json:"network,omitempty" enum:"udp,udp4,udp6,tcp,tcp4,tcp6"`
Servers []OpenVPNRemoteOptions `json:"servers,omitempty"`
RemoteRandom bool `json:"remote_random,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
PeerAddress badoption.Addr `json:"peer_address,omitempty"`
PeerAddressIPv6 badoption.Addr `json:"peer_address_ipv6,omitempty"`
Topology string `json:"topology,omitempty"`
Topology string `json:"topology,omitempty" enum:"net30,p2p,subnet"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
AuthRetry string `json:"auth_retry,omitempty"`
AuthRetry string `json:"auth_retry,omitempty" enum:"none,nointeract,interact"`
StaticChallenge string `json:"static_challenge,omitempty"`
StaticChallengeEcho bool `json:"static_challenge_echo,omitempty"`
StaticKey badoption.Listable[string] `json:"static_key,omitempty"`
StaticKeyPath string `json:"static_key_path,omitempty"`
KeyDirection string `json:"key_direction,omitempty"`
KeyDirection string `json:"key_direction,omitempty" enum:"server,client"`
TLS *OpenVPNOutboundTLSOptions `json:"tls,omitempty"`
Cipher string `json:"cipher,omitempty"`
DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"`
@@ -43,13 +43,13 @@ type OpenVPNClientEndpointOptions struct {
Auth string `json:"auth,omitempty"`
MSSFix uint32 `json:"mss_fix,omitempty"`
MSSFixDisabled bool `json:"mss_fix_disabled,omitempty"`
MSSFixMode string `json:"mss_fix_mode,omitempty"`
MSSFixMode string `json:"mss_fix_mode,omitempty" enum:"mtu,fixed"`
Fragment uint32 `json:"fragment,omitempty"`
ReplayWindow uint32 `json:"replay_window,omitempty"`
ReplayWindowTime badoption.Duration `json:"replay_window_time,omitempty"`
Compression string `json:"compression,omitempty"`
CompressionLZO string `json:"compression_lzo,omitempty"`
AllowCompression string `json:"allow_compression,omitempty"`
Compression string `json:"compression,omitempty" enum:"none,no,lz4,lz4-v2,stub,stub-v2,disabled,off"`
CompressionLZO string `json:"compression_lzo,omitempty" enum:"none,no,yes,adaptive,asym,disabled,off"`
AllowCompression string `json:"allow_compression,omitempty" enum:"no,asym,yes"`
RouteNoPull bool `json:"route_no_pull,omitempty"`
PullFilters []OpenVPNPullFilterOptions `json:"pull_filters,omitempty"`
Routes badoption.Listable[netip.Prefix] `json:"routes,omitempty"`
@@ -75,20 +75,20 @@ type OpenVPNClientEndpointOptions struct {
type OpenVPNServerEndpointOptions struct {
ListenOptions
OpenVPNEndpointOptions
Mode string `json:"mode,omitempty"`
Network string `json:"network,omitempty"`
Mode string `json:"mode,omitempty" enum:"tls,static_key"`
Network string `json:"network,omitempty" enum:"tcp,udp"`
Remote string `json:"remote,omitempty"`
RemotePort uint16 `json:"remote_port,omitempty"`
MaxClients int `json:"max_clients,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address"`
PeerAddress badoption.Addr `json:"peer_address,omitempty"`
PeerAddressIPv6 badoption.Addr `json:"peer_address_ipv6,omitempty"`
Topology string `json:"topology,omitempty"`
Topology string `json:"topology,omitempty" enum:"net30,p2p,subnet"`
DuplicateCN bool `json:"duplicate_cn,omitempty"`
Users []auth.User `json:"users,omitempty"`
StaticKey badoption.Listable[string] `json:"static_key,omitempty"`
StaticKeyPath string `json:"static_key_path,omitempty"`
KeyDirection string `json:"key_direction,omitempty"`
KeyDirection string `json:"key_direction,omitempty" enum:"server,client"`
TLS *OpenVPNInboundTLSOptions `json:"tls,omitempty"`
Cipher string `json:"cipher,omitempty"`
DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"`
@@ -96,7 +96,7 @@ type OpenVPNServerEndpointOptions struct {
Auth string `json:"auth,omitempty"`
MSSFix uint32 `json:"mss_fix,omitempty"`
MSSFixDisabled bool `json:"mss_fix_disabled,omitempty"`
MSSFixMode string `json:"mss_fix_mode,omitempty"`
MSSFixMode string `json:"mss_fix_mode,omitempty" enum:"mtu,fixed"`
ReplayWindow uint32 `json:"replay_window,omitempty"`
ReplayWindowTime badoption.Duration `json:"replay_window_time,omitempty"`
Push *OpenVPNPushOptions `json:"push,omitempty"`
@@ -111,17 +111,17 @@ type OpenVPNServerEndpointOptions struct {
type OpenVPNRemoteOptions struct {
ServerOptions
Network string `json:"network,omitempty"`
Network string `json:"network,omitempty" enum:"udp,udp4,udp6,tcp,tcp4,tcp6"`
}
type OpenVPNPullFilterOptions struct {
Action string `json:"action"`
Action string `json:"action" enum:"accept,ignore,reject"`
Text string `json:"text"`
}
type OpenVPNOutboundTLSOptions struct {
ServerName string `json:"server_name,omitempty"`
ServerNameType string `json:"server_name_type,omitempty"`
ServerNameType string `json:"server_name_type,omitempty" enum:"subject,name,name-prefix"`
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"`
@@ -132,11 +132,11 @@ type OpenVPNOutboundTLSOptions struct {
CRLPath string `json:"crl_path,omitempty"`
RemoteCertificateKU badoption.Listable[string] `json:"remote_certificate_ku,omitempty"`
RemoteCertificateEKU string `json:"remote_certificate_eku,omitempty"`
RemoteCertificateTLS string `json:"remote_certificate_tls,omitempty"`
CertificateProfile string `json:"certificate_profile,omitempty"`
NSCertificateType string `json:"ns_certificate_type,omitempty"`
VersionMin string `json:"version_min,omitempty"`
VersionMax string `json:"version_max,omitempty"`
RemoteCertificateTLS string `json:"remote_certificate_tls,omitempty" enum:"server,client,none"`
CertificateProfile string `json:"certificate_profile,omitempty" enum:"legacy,preferred,insecure,suiteb"`
NSCertificateType string `json:"ns_certificate_type,omitempty" enum:"server,client"`
VersionMin string `json:"version_min,omitempty" enum:"1.0,1.1,1.2,1.3"`
VersionMax string `json:"version_max,omitempty" enum:"1.0,1.1,1.2,1.3"`
Cipher string `json:"cipher,omitempty"`
Groups string `json:"groups,omitempty"`
ControlWrap *OpenVPNControlWrapOptions `json:"control_wrap,omitempty"`
@@ -149,35 +149,35 @@ type OpenVPNInboundTLSOptions struct {
KeyPath string `json:"key_path,omitempty"`
ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"`
ClientCertificatePath string `json:"client_certificate_path,omitempty"`
VerifyClientCertificate string `json:"verify_client_certificate,omitempty"`
VerifyClientCertificate string `json:"verify_client_certificate,omitempty" enum:"require,optional,none"`
ClientName string `json:"client_name,omitempty"`
ClientNameType string `json:"client_name_type,omitempty"`
ClientNameType string `json:"client_name_type,omitempty" enum:"subject,name,name-prefix"`
PeerFingerprint badoption.Listable[string] `json:"peer_fingerprint,omitempty"`
CRLPath string `json:"crl_path,omitempty"`
RemoteCertificateKU badoption.Listable[string] `json:"remote_certificate_ku,omitempty"`
RemoteCertificateEKU string `json:"remote_certificate_eku,omitempty"`
RemoteCertificateTLS string `json:"remote_certificate_tls,omitempty"`
CertificateProfile string `json:"certificate_profile,omitempty"`
NSCertificateType string `json:"ns_certificate_type,omitempty"`
VersionMin string `json:"version_min,omitempty"`
VersionMax string `json:"version_max,omitempty"`
RemoteCertificateTLS string `json:"remote_certificate_tls,omitempty" enum:"server,client,none"`
CertificateProfile string `json:"certificate_profile,omitempty" enum:"legacy,preferred,insecure,suiteb"`
NSCertificateType string `json:"ns_certificate_type,omitempty" enum:"server,client"`
VersionMin string `json:"version_min,omitempty" enum:"1.0,1.1,1.2,1.3"`
VersionMax string `json:"version_max,omitempty" enum:"1.0,1.1,1.2,1.3"`
Cipher string `json:"cipher,omitempty"`
Groups string `json:"groups,omitempty"`
ControlWrap *OpenVPNInboundControlWrapOptions `json:"control_wrap,omitempty"`
}
type OpenVPNControlWrapOptions struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"tls_auth,tls_crypt,tls_crypt_v2"`
Key badoption.Listable[string] `json:"key,omitempty"`
KeyPath string `json:"key_path,omitempty"`
Direction string `json:"direction,omitempty"`
Direction string `json:"direction,omitempty" enum:"server,client"`
}
type OpenVPNInboundControlWrapOptions struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"tls_auth,tls_crypt,tls_crypt_v2"`
Key badoption.Listable[string] `json:"key,omitempty"`
KeyPath string `json:"key_path,omitempty"`
Direction string `json:"direction,omitempty"`
Direction string `json:"direction,omitempty" enum:"server,client"`
ForceCookie bool `json:"force_cookie,omitempty"`
}
@@ -198,8 +198,8 @@ type OpenVPNPushDNSServerOptions struct {
Priority int `json:"priority"`
Addresses badoption.Listable[string] `json:"addresses"`
ResolveDomains badoption.Listable[string] `json:"resolve_domains,omitempty"`
DNSSEC string `json:"dnssec,omitempty"`
Transport string `json:"transport,omitempty"`
DNSSEC string `json:"dnssec,omitempty" enum:"yes,optional,no"`
Transport string `json:"transport,omitempty" enum:"plain,dot,doh"`
SNI string `json:"sni,omitempty"`
}
+15 -2
View File
@@ -3,7 +3,9 @@ package option
import (
"bytes"
"context"
"reflect"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
@@ -12,7 +14,7 @@ import (
type _Options struct {
RawMessage json.RawMessage `json:"-"`
CommentsSet *json.CommentSet `json:"-"`
Schema string `json:"$schema,omitempty"`
Schema string `json:"$schema,omitempty" examples:"https://sing-box.sagernet.org/schema.json"`
Log *LogOptions `json:"log,omitempty"`
DNS *DNSOptions `json:"dns,omitempty"`
NTP *NTPOptions `json:"ntp,omitempty"`
@@ -45,6 +47,17 @@ func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) erro
return checkOptions(o)
}
func (o Options) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
node := schema.StrictObject()
node.SchemaURI = "https://json-schema.org/draft/2020-12/schema"
node.ID = "https://sing-box.sagernet.org/schema.json"
err := builder.FlattenStruct(node, reflect.TypeFor[Options]())
if err != nil {
return nil, err
}
return node, nil
}
func (o Options) Comments() *json.CommentSet {
return o.CommentsSet
}
@@ -55,7 +68,7 @@ func (o *Options) SetComments(comments *json.CommentSet) {
type LogOptions struct {
Disabled bool `json:"disabled,omitempty"`
Level string `json:"level,omitempty"`
Level string `json:"level,omitempty" enum:"trace,debug,info,warn,warning,error,fatal,panic"`
Output string `json:"output,omitempty"`
Timestamp bool `json:"timestamp,omitempty"`
DisableColor bool `json:"-"`
+11 -2
View File
@@ -3,6 +3,7 @@ package option
import (
"strings"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
@@ -13,8 +14,8 @@ type CloudflareOriginCACertificateProviderOptions struct {
DataDirectory string `json:"data_directory,omitempty"`
APIToken string `json:"api_token,omitempty"`
OriginCAKey string `json:"origin_ca_key,omitempty"`
RequestType CloudflareOriginCARequestType `json:"request_type,omitempty"`
RequestedValidity CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"`
RequestType CloudflareOriginCARequestType `json:"request_type,omitempty" enum:"origin-rsa,origin-ecc"`
RequestedValidity CloudflareOriginCARequestValidity `json:"requested_validity,omitempty" enum:"0,7,30,90,365,730,1095,5475"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
}
@@ -41,6 +42,10 @@ func (t *CloudflareOriginCARequestType) UnmarshalJSON(data []byte) error {
return nil
}
func (t CloudflareOriginCARequestType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("", "origin-rsa", "origin-ecc"), nil
}
type CloudflareOriginCARequestValidity uint16
const (
@@ -74,3 +79,7 @@ func (v *CloudflareOriginCARequestValidity) UnmarshalJSON(data []byte) error {
}
return nil
}
func (v CloudflareOriginCARequestValidity) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return &schema.Node{Type: "integer", Enum: []any{0, 7, 30, 90, 365, 730, 1095, 5475}}, nil
}
+33 -4
View File
@@ -2,8 +2,10 @@ package option
import (
"context"
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -13,6 +15,7 @@ import (
)
type OutboundOptionsRegistry interface {
OptionTypes() []string
CreateOptions(outboundType string) (any, bool)
}
@@ -59,13 +62,27 @@ func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) err
return nil
}
func (h Outbound) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("Outbound", func() (*schema.Node, error) {
registry := service.FromContext[OutboundOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing outbound options registry in context")
}
return registryUnion(builder, registry, []string{C.TypeShadowsocksR, C.TypeWireGuard}, true)
})
}
type DialerOptionsWrapper interface {
TakeDialerOptions() DialerOptions
ReplaceDialerOptions(options DialerOptions)
}
type DialerOptions struct {
Detour string `json:"detour,omitempty"`
Detour string `json:"detour,omitempty" reference:"outbound"`
AbstractDialerOptions
}
type AbstractDialerOptions struct {
BindInterface string `json:"bind_interface,omitempty"`
Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"`
Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"`
@@ -73,7 +90,7 @@ type DialerOptions struct {
ProtectPath string `json:"protect_path,omitempty"`
RoutingMark FwMark `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,omitempty"`
NetNs string `json:"netns,omitempty"`
NetNs string `json:"netns,omitempty" reference:"network_namespace"`
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
@@ -91,11 +108,11 @@ type DialerOptions struct {
FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"`
// Deprecated: migrated to domain resolver
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty" schema:"omit"`
}
type _DomainResolveOptions struct {
Server string `json:"server"`
Server string `json:"server" reference:"dns_server"`
Timeout badoption.Duration `json:"timeout,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
@@ -138,6 +155,18 @@ func (o *DomainResolveOptions) UnmarshalJSON(bytes []byte) error {
return nil
}
func (o DomainResolveOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DomainResolver", func() (*schema.Node, error) {
objectForm := schema.StrictObject()
err := builder.FlattenStruct(objectForm, reflect.TypeFor[DomainResolveOptions]())
if err != nil {
return nil, err
}
objectForm.Required = []string{"server"}
return schema.AnyOf(schema.TagReferenceNode("dns_server"), objectForm), nil
})
}
func (o *DialerOptions) TakeDialerOptions() DialerOptions {
return *o
}
+9
View File
@@ -1,6 +1,7 @@
package option
import (
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
@@ -63,6 +64,10 @@ func (r *OnDemandRuleAction) UnmarshalJSON(bytes []byte) error {
return nil
}
func (r OnDemandRuleAction) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("connect", "disconnect", "evaluate_connection", "ignore"), nil
}
type OnDemandRuleInterfaceType int
func (r *OnDemandRuleInterfaceType) MarshalJSON() ([]byte, error) {
@@ -103,3 +108,7 @@ func (r *OnDemandRuleInterfaceType) UnmarshalJSON(bytes []byte) error {
*r = OnDemandRuleInterfaceType(interfaceTypeValue)
return nil
}
func (r OnDemandRuleInterfaceType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("any", "wifi", "cellular"), nil
}
+11
View File
@@ -3,7 +3,9 @@ package option
import (
"context"
"net/netip"
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
@@ -39,6 +41,15 @@ func (r *ResolvedServiceOptions) UnmarshalJSONContext(ctx context.Context, bytes
return nil
}
func (r ResolvedServiceOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
node := schema.StrictObject()
err := builder.FlattenStruct(node, reflect.TypeFor[ResolvedServiceOptions]())
if err != nil {
return nil, err
}
return node, nil
}
type ResolvedDNSServerOptions struct {
Service string `json:"service"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
+5 -5
View File
@@ -3,11 +3,11 @@ package option
import "github.com/sagernet/sing/common/json/badoption"
type RouteOptions struct {
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
Geosite *GeositeOptions `json:"geosite,omitempty"`
GeoIP *GeoIPOptions `json:"geoip,omitempty" schema:"omit"`
Geosite *GeositeOptions `json:"geosite,omitempty" schema:"omit"`
Rules []Rule `json:"rules,omitempty"`
RuleSet []RuleSet `json:"rule_set,omitempty"`
Final string `json:"final,omitempty"`
Final string `json:"final,omitempty" reference:"outbound"`
FindProcess bool `json:"find_process,omitempty"`
FindNeighbor bool `json:"find_neighbor,omitempty"`
DHCPLeaseFiles badoption.Listable[string] `json:"dhcp_lease_files,omitempty"`
@@ -26,11 +26,11 @@ type RouteOptions struct {
type GeoIPOptions struct {
Path string `json:"path,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
DownloadDetour string `json:"download_detour,omitempty" reference:"outbound"`
}
type GeositeOptions struct {
Path string `json:"path,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
DownloadDetour string `json:"download_detour,omitempty" reference:"outbound"`
}
+83 -11
View File
@@ -5,6 +5,7 @@ import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
@@ -13,7 +14,7 @@ import (
)
type _Rule struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"default,logical"`
DefaultOptions DefaultRule `json:"-"`
LogicalOptions LogicalRule `json:"-"`
}
@@ -65,20 +66,91 @@ func (r Rule) IsValid() bool {
}
}
func (r Rule) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("Rule", func() (*schema.Node, error) {
actionRef, err := builder.Define("RuleAction", func() (*schema.Node, error) {
return routeActionUnion(builder)
})
if err != nil {
return nil, err
}
nestedRef, err := builder.Define("NestedRule", func() (*schema.Node, error) {
return nestedRuleUnion(builder, reflect.TypeFor[RawDefaultRule](), "NestedRule")
})
if err != nil {
return nil, err
}
return ruleUnion(builder, reflect.TypeFor[RawDefaultRule](), nestedRef, actionRef)
})
}
// ruleUnion builds the top-level rule schema: match fields composed with rule
// actions via unevaluatedProperties, mirroring the badjson.UnmarshallExcluded
// composition in DefaultRule / LogicalRule.
func ruleUnion(builder schema.Builder, matchType reflect.Type, nestedRef *schema.Node, actionRef *schema.Node) (*schema.Node, error) {
defaultMatch := schema.LooseObject()
defaultMatch.Properties.Put("type", schema.StringEnum(C.RuleTypeDefault, ""))
err := builder.FlattenStruct(defaultMatch, matchType)
if err != nil {
return nil, err
}
defaultVariant := &schema.Node{
Type: "object",
AllOf: []*schema.Node{defaultMatch, actionRef},
UnevaluatedProperties: false,
}
logicalMatch := schema.LooseObject()
logicalMatch.Properties.Put("type", schema.StringConst(C.RuleTypeLogical))
logicalProperties(logicalMatch, nestedRef)
logicalMatch.Required = []string{"type", "mode", "rules"}
logicalVariant := &schema.Node{
Type: "object",
AllOf: []*schema.Node{logicalMatch, actionRef},
UnevaluatedProperties: false,
}
return schema.OneOf(defaultVariant, logicalVariant), nil
}
// nestedRuleUnion builds a match-only rule schema: nested rules reject rule
// actions, and headless rules never carry them.
func nestedRuleUnion(builder schema.Builder, matchType reflect.Type, selfName string) (*schema.Node, error) {
defaultVariant := schema.StrictObject()
defaultVariant.Properties.Put("type", schema.StringEnum(C.RuleTypeDefault, ""))
err := builder.FlattenStruct(defaultVariant, matchType)
if err != nil {
return nil, err
}
logicalVariant := schema.StrictObject()
logicalVariant.Properties.Put("type", schema.StringConst(C.RuleTypeLogical))
logicalProperties(logicalVariant, schema.RefNode(selfName))
logicalVariant.Required = []string{"type", "mode", "rules"}
return schema.OneOf(defaultVariant, logicalVariant), nil
}
func logicalProperties(node *schema.Node, nestedRef *schema.Node) {
node.Properties.Put("mode", schema.StringEnum(C.LogicalTypeAnd, C.LogicalTypeOr))
node.Properties.Put("rules", &schema.Node{Type: "array", Items: nestedRef})
node.Properties.Put("invert", schema.BooleanNode())
}
type RawDefaultRule struct {
Inbound badoption.Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty"`
Inbound badoption.Listable[string] `json:"inbound,omitempty" reference:"inbound"`
IPVersion int `json:"ip_version,omitempty" enum:"4,6"`
Network badoption.Listable[string] `json:"network,omitempty" enum:"tcp,udp,icmp"`
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty" enum:"tls,http,quic,dns,stun,bittorrent,dtls,ssh,rdp,ntp"`
Client badoption.Listable[string] `json:"client,omitempty"`
Domain badoption.Listable[string] `json:"domain,omitempty"`
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
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"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
@@ -106,12 +178,12 @@ type RawDefaultRule struct {
SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"`
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty" reference:"rule_set"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
Invert bool `json:"invert,omitempty"`
// Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty" schema:"omit"`
}
type DefaultRule struct {
@@ -138,7 +210,7 @@ func (r DefaultRule) IsValid() bool {
}
type RawLogicalRule struct {
Mode string `json:"mode"`
Mode string `json:"mode" enum:"and,or"`
Rules []Rule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
}
+109 -41
View File
@@ -4,9 +4,11 @@ import (
"context"
"fmt"
"net/netip"
"reflect"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -14,7 +16,7 @@ import (
)
type _RuleAction struct {
Action string `json:"action,omitempty"`
Action string `json:"action,omitempty" enum:"route,route-options,direct,bypass,reject,hijack-dns,sniff,resolve"`
RouteOptions RouteActionOptions `json:"-"`
RouteOptionsOptions RouteOptionsActionOptions `json:"-"`
DirectOptions DirectActionOptions `json:"-"`
@@ -97,9 +99,10 @@ func (r *RuleAction) UnmarshalJSON(data []byte) error {
}
type _DNSRuleAction struct {
Action string `json:"action,omitempty"`
Action string `json:"action,omitempty" enum:"route,evaluate,respond,route-options,reject,predefined"`
Race bool `json:"race,omitempty"`
RouteOptions DNSRouteActionOptions `json:"-"`
EvaluateOptions DNSEvaluateActionOptions `json:"-"`
RouteOptionsOptions DNSRouteOptionsActionOptions `json:"-"`
RejectOptions RejectActionOptions `json:"-"`
PredefinedOptions DNSRouteActionPredefined `json:"-"`
@@ -117,7 +120,7 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
r.Action = ""
v = r.RouteOptions
case C.RuleActionTypeEvaluate:
v = r.RouteOptions
v = r.EvaluateOptions
case C.RuleActionTypeRespond:
v = nil
case C.RuleActionTypeRouteOptions:
@@ -146,7 +149,7 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e
r.Action = C.RuleActionTypeRoute
v = &r.RouteOptions
case C.RuleActionTypeEvaluate:
v = &r.RouteOptions
v = &r.EvaluateOptions
case C.RuleActionTypeRespond:
v = nil
case C.RuleActionTypeRouteOptions:
@@ -165,14 +168,11 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e
if err != nil {
return err
}
if r.Action == C.RuleActionTypeRoute && r.RouteOptions.Tag != "" {
return E.New("`tag` is only available in the `evaluate` action")
}
return nil
}
type RouteActionOptions struct {
Outbound string `json:"outbound,omitempty"`
Outbound string `json:"outbound,omitempty" reference:"outbound"`
RawRouteOptionsActionOptions
}
@@ -191,7 +191,7 @@ type RawRouteOptionsActionOptions struct {
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
TLSRecordFragment bool `json:"tls_record_fragment,omitempty"`
TLSSpoof string `json:"tls_spoof,omitempty"`
TLSSpoofMethod string `json:"tls_spoof_method,omitempty"`
TLSSpoofMethod string `json:"tls_spoof_method,omitempty" enum:"wrong-sequence,wrong-checksum,wrong-ack,wrong-md5,wrong-timestamp"`
}
type RouteOptionsActionOptions RawRouteOptionsActionOptions
@@ -211,30 +211,31 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
}
type DNSRouteActionOptions struct {
Server string `json:"server,omitempty"`
Tag string `json:"tag,omitempty"`
Speculative bool `json:"speculative,omitempty"`
Server string `json:"server,omitempty" reference:"dns_server"`
Speculative bool `json:"speculative,omitempty"`
AbstractDNSRouteActionOptions
}
type DNSEvaluateActionOptions struct {
Server string `json:"server,omitempty" reference:"dns_server"`
Tag string `json:"tag,omitempty"`
Speculative bool `json:"speculative,omitempty"`
AbstractDNSRouteActionOptions
}
type AbstractDNSRouteActionOptions struct {
Timeout badoption.Duration `json:"timeout,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty" schema:"omit"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type _DNSRouteOptionsActionOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions
type DNSRouteOptionsActionOptions AbstractDNSRouteActionOptions
func (r *DNSRouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, (*_DNSRouteOptionsActionOptions)(r))
err := json.Unmarshal(data, (*AbstractDNSRouteActionOptions)(r))
if err != nil {
return err
}
@@ -244,9 +245,9 @@ func (r *DNSRouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
return nil
}
type _DirectActionOptions DialerOptions
type DirectActionOptions _DirectActionOptions
type DirectActionOptions struct {
AbstractDialerOptions
}
func (d DirectActionOptions) Descriptions() []string {
var descriptions []string
@@ -286,19 +287,8 @@ func (d DirectActionOptions) Descriptions() []string {
return descriptions
}
func (d *DirectActionOptions) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, (*_DirectActionOptions)(d))
if err != nil {
return err
}
if d.Detour != "" {
return E.New("detour is not available in the current context")
}
return nil
}
type _RejectActionOptions struct {
Method string `json:"method,omitempty"`
Method string `json:"method,omitempty" enum:"default,drop,reply"`
NoDrop bool `json:"no_drop,omitempty"`
}
@@ -332,12 +322,12 @@ func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error {
}
type RouteActionSniff struct {
Sniffer badoption.Listable[string] `json:"sniffer,omitempty"`
Sniffer badoption.Listable[string] `json:"sniffer,omitempty" enum:"tls,http,quic,dns,stun,bittorrent,dtls,ssh,rdp,ntp"`
Timeout badoption.Duration `json:"timeout,omitempty"`
}
type RouteActionResolve struct {
Server string `json:"server,omitempty"`
Server string `json:"server,omitempty" reference:"dns_server"`
Timeout badoption.Duration `json:"timeout,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
@@ -352,3 +342,81 @@ type DNSRouteActionPredefined struct {
Ns badoption.Listable[DNSRecordOptions] `json:"ns,omitempty"`
Extra badoption.Listable[DNSRecordOptions] `json:"extra,omitempty"`
}
type actionVariant struct {
action string
actionOptional bool
structType reflect.Type
build func(variant *schema.Node) error
}
func actionUnion(builder schema.Builder, variants []actionVariant) (*schema.Node, error) {
variantNodes := make([]*schema.Node, 0, len(variants))
for _, variant := range variants {
variantNode := schema.LooseObject()
variantNode.Properties.Put("action", schema.StringConst(variant.action))
if !variant.actionOptional {
variantNode.Required = []string{"action"}
}
if variant.build != nil {
err := variant.build(variantNode)
if err != nil {
return nil, err
}
}
if variant.structType != nil {
err := builder.FlattenStruct(variantNode, variant.structType)
if err != nil {
return nil, err
}
}
variantNodes = append(variantNodes, variantNode)
}
return schema.OneOf(variantNodes...), nil
}
func rejectProperties(variant *schema.Node) error {
variant.Properties.Put("method", schema.StringEnum(
"",
C.RuleActionRejectMethodDefault,
C.RuleActionRejectMethodDrop,
C.RuleActionRejectMethodReply,
))
variant.Properties.Put("no_drop", schema.BooleanNode())
return nil
}
func routeActionUnion(builder schema.Builder) (*schema.Node, error) {
return actionUnion(builder, []actionVariant{
{action: C.RuleActionTypeRoute, actionOptional: true, structType: reflect.TypeFor[RouteActionOptions]()},
{action: C.RuleActionTypeRouteOptions, structType: reflect.TypeFor[RawRouteOptionsActionOptions]()},
{action: C.RuleActionTypeDirect, structType: reflect.TypeFor[DirectActionOptions]()},
{action: C.RuleActionTypeBypass, structType: reflect.TypeFor[RouteActionOptions]()},
{action: C.RuleActionTypeReject, build: rejectProperties},
{action: C.RuleActionTypeHijackDNS},
{action: C.RuleActionTypeSniff, structType: reflect.TypeFor[RouteActionSniff]()},
{action: C.RuleActionTypeResolve, structType: reflect.TypeFor[RouteActionResolve]()},
})
}
func dnsActionUnion(builder schema.Builder) (*schema.Node, error) {
raceProperty := func(variant *schema.Node) error {
variant.Properties.Put("race", schema.BooleanNode())
return nil
}
rejectWithRace := func(variant *schema.Node) error {
err := raceProperty(variant)
if err != nil {
return err
}
return rejectProperties(variant)
}
return actionUnion(builder, []actionVariant{
{action: C.RuleActionTypeRoute, actionOptional: true, structType: reflect.TypeFor[DNSRouteActionOptions](), build: raceProperty},
{action: C.RuleActionTypeEvaluate, structType: reflect.TypeFor[DNSEvaluateActionOptions](), build: raceProperty},
{action: C.RuleActionTypeRespond, build: raceProperty},
{action: C.RuleActionTypeRouteOptions, structType: reflect.TypeFor[DNSRouteOptionsActionOptions](), build: raceProperty},
{action: C.RuleActionTypeReject, build: rejectWithRace},
{action: C.RuleActionTypePredefined, structType: reflect.TypeFor[DNSRouteActionPredefined](), build: raceProperty},
})
}
+36 -13
View File
@@ -5,6 +5,7 @@ import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
@@ -13,7 +14,7 @@ import (
)
type _DNSRule struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"default,logical"`
DefaultOptions DefaultDNSRule `json:"-"`
LogicalOptions LogicalDNSRule `json:"-"`
}
@@ -67,6 +68,24 @@ func (r DNSRule) IsValid() bool {
}
}
func (r DNSRule) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DNSRule", func() (*schema.Node, error) {
actionRef, err := builder.Define("DNSRuleAction", func() (*schema.Node, error) {
return dnsActionUnion(builder)
})
if err != nil {
return nil, err
}
nestedRef, err := builder.Define("NestedDNSRule", func() (*schema.Node, error) {
return nestedRuleUnion(builder, reflect.TypeFor[RawDefaultDNSRule](), "NestedDNSRule")
})
if err != nil {
return nil, err
}
return ruleUnion(builder, reflect.TypeFor[RawDefaultDNSRule](), nestedRef, actionRef)
})
}
type DNSRuleMatchResponse struct {
Enabled bool
Tag string
@@ -111,13 +130,17 @@ func (m *DNSRuleMatchResponse) ResponseTag() string {
return m.Tag
}
func (m DNSRuleMatchResponse) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.AnyOf(schema.BooleanNode(), schema.StringNode()), nil
}
type RawDefaultDNSRule struct {
Inbound badoption.Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Inbound badoption.Listable[string] `json:"inbound,omitempty" reference:"inbound"`
IPVersion int `json:"ip_version,omitempty" enum:"4,6"`
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty" enum:"tcp,udp"`
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty" enum:"tls,http,quic,dns,stun,bittorrent,dtls,ssh,rdp,ntp"`
Domain badoption.Listable[string] `json:"domain,omitempty"`
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
@@ -135,7 +158,7 @@ type RawDefaultDNSRule struct {
PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"`
User badoption.Listable[string] `json:"user,omitempty"`
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
Outbound badoption.Listable[string] `json:"outbound,omitempty"`
Outbound badoption.Listable[string] `json:"outbound,omitempty" reference:"outbound" schema:"omit"`
ClashMode string `json:"clash_mode,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
@@ -148,7 +171,7 @@ type RawDefaultDNSRule struct {
SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"`
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
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"`
@@ -161,13 +184,13 @@ type RawDefaultDNSRule struct {
Invert bool `json:"invert,omitempty"`
// Deprecated: removed in sing-box 1.12.0
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
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"`
// Deprecated: removed in sing-box 1.11.0
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty" schema:"omit"`
// Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty" schema:"omit"`
}
type DefaultDNSRule struct {
@@ -210,7 +233,7 @@ func (r DefaultDNSRule) IsValid() bool {
}
type RawLogicalDNSRule struct {
Mode string `json:"mode"`
Mode string `json:"mode" enum:"and,or"`
Rules []DNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
}
+1 -1
View File
@@ -20,7 +20,7 @@ const (
var (
routeRuleActionKeys = jsonFieldNames(reflect.TypeFor[_RuleAction](), reflect.TypeFor[RouteActionOptions]())
dnsRuleActionKeys = jsonFieldNames(reflect.TypeFor[_DNSRuleAction](), reflect.TypeFor[DNSRouteActionOptions]())
dnsRuleActionKeys = jsonFieldNames(reflect.TypeFor[_DNSRuleAction](), reflect.TypeFor[DNSRouteActionOptions](), reflect.TypeFor[DNSEvaluateActionOptions]())
)
func nestedRuleChildContext(ctx context.Context) context.Context {
+53 -7
View File
@@ -7,6 +7,7 @@ import (
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions"
@@ -19,9 +20,9 @@ import (
)
type _RuleSet struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"inline,local,remote"`
Tag badoption.Listable[string] `json:"tag"`
Format string `json:"format,omitempty"`
Format string `json:"format,omitempty" enum:"source,binary"`
InlineOptions PlainRuleSet `json:"-"`
LocalOptions LocalRuleSet `json:"-"`
RemoteOptions RemoteRuleSet `json:"-"`
@@ -131,6 +132,45 @@ func ruleSetDefaultFormat(path string) string {
}
}
func (r RuleSet) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("RuleSet", func() (*schema.Node, error) {
headlessRef, err := builder.Describe(reflect.TypeFor[HeadlessRule]())
if err != nil {
return nil, err
}
tagNode := schema.ListableOf(schema.StringNode())
formatNode := schema.StringEnum(C.RuleSetFormatSource, C.RuleSetFormatBinary)
inlineVariant := schema.StrictObject()
inlineVariant.Properties.Put("type", schema.StringEnum(C.RuleSetTypeInline, ""))
inlineVariant.Properties.Put("tag", tagNode)
inlineVariant.Properties.Put("rules", &schema.Node{Type: "array", Items: headlessRef})
inlineVariant.Required = []string{"tag"}
localVariant := schema.StrictObject()
localVariant.Properties.Put("type", schema.StringConst(C.RuleSetTypeLocal))
localVariant.Properties.Put("tag", tagNode)
localVariant.Properties.Put("format", formatNode)
err = builder.FlattenStruct(localVariant, reflect.TypeFor[LocalRuleSet]())
if err != nil {
return nil, err
}
localVariant.Required = []string{"type", "tag"}
remoteVariant := schema.StrictObject()
remoteVariant.Properties.Put("type", schema.StringConst(C.RuleSetTypeRemote))
remoteVariant.Properties.Put("tag", tagNode)
remoteVariant.Properties.Put("format", formatNode)
err = builder.FlattenStruct(remoteVariant, reflect.TypeFor[RemoteRuleSet]())
if err != nil {
return nil, err
}
remoteVariant.Required = []string{"type", "tag"}
return schema.OneOf(inlineVariant, localVariant, remoteVariant), nil
})
}
type LocalRuleSet struct {
Path string `json:"path,omitempty"`
}
@@ -140,11 +180,11 @@ type RemoteRuleSet struct {
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
// Deprecated: use http_client instead
DownloadDetour string `json:"download_detour,omitempty"`
DownloadDetour string `json:"download_detour,omitempty" reference:"outbound" schema:"omit"`
}
type _HeadlessRule struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"default,logical"`
DefaultOptions DefaultHeadlessRule `json:"-"`
LogicalOptions LogicalHeadlessRule `json:"-"`
}
@@ -198,9 +238,15 @@ func (r HeadlessRule) IsValid() bool {
}
}
func (r HeadlessRule) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("HeadlessRule", func() (*schema.Node, error) {
return nestedRuleUnion(builder, reflect.TypeFor[DefaultHeadlessRule](), "HeadlessRule")
})
}
type DefaultHeadlessRule struct {
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty" enum:"tcp,udp,icmp"`
Domain badoption.Listable[string] `json:"domain,omitempty"`
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
@@ -241,7 +287,7 @@ func (r DefaultHeadlessRule) IsValid() bool {
}
type LogicalHeadlessRule struct {
Mode string `json:"mode"`
Mode string `json:"mode" enum:"and,or"`
Rules []HeadlessRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
}
@@ -251,7 +297,7 @@ func (r LogicalHeadlessRule) IsValid() bool {
}
type _PlainRuleSetCompat struct {
Version uint8 `json:"version"`
Version uint8 `json:"version" enum:"1,2,3,4,5"`
Options PlainRuleSet `json:"-"`
RawMessage json.RawMessage `json:"-"`
}
+71
View File
@@ -0,0 +1,71 @@
package option
import (
"reflect"
"slices"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
)
type schemaTypeRegistry interface {
OptionTypes() []string
CreateOptions(itemType string) (any, bool)
}
func registryUnion(builder schema.Builder, registry schemaTypeRegistry, excludeTypes []string, withTag bool) (*schema.Node, error) {
var variants []*schema.Node
for _, itemType := range registry.OptionTypes() {
if slices.Contains(excludeTypes, itemType) {
continue
}
optionsValue, _ := registry.CreateOptions(itemType)
describer, isDescriber := optionsValue.(schema.Describer)
var variant *schema.Node
var err error
if isDescriber {
variant, err = describer.DescribeSchema(builder)
} else {
variant = schema.StrictObject()
err = builder.FlattenStruct(variant, reflect.TypeOf(optionsValue).Elem())
}
if err != nil {
return nil, E.Cause(err, itemType)
}
err = prependTypeTag(variant, itemType, withTag)
if err != nil {
return nil, E.Cause(err, itemType)
}
variants = append(variants, variant)
}
return schema.OneOf(variants...), nil
}
// prependTypeTag merges the polymorphic base fields into a variant produced
// from registry options, matching badjson.UnmarshallExcluded composition.
func prependTypeTag(variant *schema.Node, typeName string, withTag bool) error {
if variant.OneOf != nil {
for _, branch := range variant.OneOf {
err := prependTypeTag(branch, typeName, withTag)
if err != nil {
return err
}
}
return nil
}
if variant.Properties == nil {
return E.New("cannot merge type into non-object variant")
}
newProperties := new(badjson.TypedMap[string, *schema.Node])
newProperties.Put("type", schema.StringConst(typeName))
if withTag {
newProperties.Put("tag", schema.StringNode())
}
for _, entry := range variant.Properties.Entries() {
newProperties.Put(entry.Key, entry.Value)
}
variant.Properties = newProperties
variant.Required = append([]string{"type"}, variant.Required...)
return nil
}
+12
View File
@@ -3,6 +3,7 @@ package option
import (
"context"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -10,6 +11,7 @@ import (
)
type ServiceOptionsRegistry interface {
OptionTypes() []string
CreateOptions(serviceType string) (any, bool)
}
@@ -45,3 +47,13 @@ func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) erro
h.Options = options
return nil
}
func (h Service) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("Service", func() (*schema.Node, error) {
registry := service.FromContext[ServiceOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing service options registry in context")
}
return registryUnion(builder, registry, nil, true)
})
}
+2 -2
View File
@@ -3,7 +3,7 @@ package option
type ShadowsocksInboundOptions struct {
ListenOptions
Network NetworkList `json:"network,omitempty"`
Method string `json:"method"`
Method string `json:"method" enum:"none,aes-128-gcm,aes-192-gcm,aes-256-gcm,chacha20-ietf-poly1305,xchacha20-ietf-poly1305,2022-blake3-aes-128-gcm,2022-blake3-aes-256-gcm,2022-blake3-chacha20-poly1305"`
Password string `json:"password,omitempty"`
Users []ShadowsocksUser `json:"users,omitempty"`
Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
@@ -25,7 +25,7 @@ type ShadowsocksDestination struct {
type ShadowsocksOutboundOptions struct {
DialerOptions
ServerOptions
Method string `json:"method"`
Method string `json:"method" enum:"none,aes-128-gcm,aes-192-gcm,aes-256-gcm,chacha20-ietf-poly1305,xchacha20-ietf-poly1305,2022-blake3-aes-128-gcm,2022-blake3-aes-256-gcm,2022-blake3-chacha20-poly1305,aes-128-ctr,aes-192-ctr,aes-256-ctr,aes-128-cfb,aes-192-cfb,aes-256-cfb,rc4-md5,chacha20-ietf,xchacha20"`
Password string `json:"password"`
Plugin string `json:"plugin,omitempty"`
PluginOptions string `json:"plugin_opts,omitempty"`
+7 -2
View File
@@ -3,13 +3,14 @@ package option
import (
"encoding/json"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
)
type ShadowTLSInboundOptions struct {
ListenOptions
Version int `json:"version,omitempty"`
Version int `json:"version,omitempty" enum:"1,2,3"`
Password string `json:"password,omitempty"`
Users []ShadowTLSUser `json:"users,omitempty"`
Handshake ShadowTLSHandshakeOptions `json:"handshake,omitempty"`
@@ -62,6 +63,10 @@ func (w *WildcardSNI) UnmarshalJSON(bytes []byte) error {
return nil
}
func (w WildcardSNI) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("", "off", "authed", "all"), nil
}
type ShadowTLSUser struct {
Name string `json:"name,omitempty"`
Password string `json:"password,omitempty"`
@@ -75,7 +80,7 @@ type ShadowTLSHandshakeOptions struct {
type ShadowTLSOutboundOptions struct {
DialerOptions
ServerOptions
Version int `json:"version,omitempty"`
Version int `json:"version,omitempty" enum:"1,2,3"`
Password string `json:"password,omitempty"`
OutboundTLSOptionsContainer
}
+1 -1
View File
@@ -22,7 +22,7 @@ type HTTPMixedInboundOptions struct {
type SOCKSOutboundOptions struct {
DialerOptions
ServerOptions
Version string `json:"version,omitempty"`
Version string `json:"version,omitempty" enum:"4,4a,5"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
+43 -14
View File
@@ -1,20 +1,27 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
)
type _SnellInboundOptions struct {
ListenOptions
Version int `json:"version"`
PSK string `json:"psk"`
Users []SnellUser `json:"users,omitempty"`
Version int `json:"version" enum:"5,6"`
AbstractSnellInboundOptions
ObfsOptions SnellObfsServerOptions `json:"-"`
V6Options SnellV6Options `json:"-"`
}
type AbstractSnellInboundOptions struct {
ListenOptions
PSK string `json:"psk"`
Users []SnellUser `json:"users,omitempty"`
}
type SnellInboundOptions _SnellInboundOptions
func (o *SnellInboundOptions) UnmarshalJSON(content []byte) error {
@@ -51,18 +58,31 @@ func (o SnellInboundOptions) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects((_SnellInboundOptions)(o), versionOptions)
}
func (o SnellInboundOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "version", true, []schema.UnionVariant{
{Value: 5, StructType: reflect.TypeFor[SnellObfsServerOptions]()},
{Value: 6, StructType: reflect.TypeFor[SnellV6Options]()},
}, func(variant *schema.Node) error {
return builder.FlattenStruct(variant, reflect.TypeFor[AbstractSnellInboundOptions]())
})
}
type _SnellOutboundOptions struct {
DialerOptions
ServerOptions
Version int `json:"version"`
PSK string `json:"psk"`
UserKey string `json:"userkey,omitempty"`
Reuse bool `json:"reuse,omitempty"`
Network NetworkList `json:"network,omitempty"`
Version int `json:"version" enum:"4,6"`
AbstractSnellOutboundOptions
ObfsOptions SnellObfsClientOptions `json:"-"`
V6Options SnellV6Options `json:"-"`
}
type AbstractSnellOutboundOptions struct {
DialerOptions
ServerOptions
PSK string `json:"psk"`
UserKey string `json:"userkey,omitempty"`
Reuse bool `json:"reuse,omitempty"`
Network NetworkList `json:"network,omitempty"`
}
type SnellOutboundOptions _SnellOutboundOptions
func (o *SnellOutboundOptions) UnmarshalJSON(content []byte) error {
@@ -99,8 +119,17 @@ func (o SnellOutboundOptions) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects((_SnellOutboundOptions)(o), versionOptions)
}
func (o SnellOutboundOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "version", true, []schema.UnionVariant{
{Value: 4, StructType: reflect.TypeFor[SnellObfsClientOptions]()},
{Value: 6, StructType: reflect.TypeFor[SnellV6Options]()},
}, func(variant *schema.Node) error {
return builder.FlattenStruct(variant, reflect.TypeFor[AbstractSnellOutboundOptions]())
})
}
type SnellObfsServerOptions struct {
ObfsMode string `json:"obfs_mode,omitempty"`
ObfsMode string `json:"obfs_mode,omitempty" enum:"none,http,tls"`
}
type SnellUser struct {
@@ -109,10 +138,10 @@ type SnellUser struct {
}
type SnellObfsClientOptions struct {
ObfsMode string `json:"obfs_mode,omitempty"`
ObfsMode string `json:"obfs_mode,omitempty" enum:"none,http,tls"`
ObfsHost string `json:"obfs_host,omitempty"`
}
type SnellV6Options struct {
Mode string `json:"mode,omitempty"`
Mode string `json:"mode,omitempty" enum:"default,unshaped,unsafe-raw"`
}
+29
View File
@@ -3,7 +3,9 @@ package option
import (
"net/netip"
"net/url"
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
@@ -56,6 +58,15 @@ func (o *TailscaleSSHServerOptions) UnmarshalJSON(bytes []byte) error {
return json.UnmarshalDisallowUnknownFields(bytes, (*_TailscaleSSHServerOptions)(o))
}
func (o TailscaleSSHServerOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
objectForm := schema.StrictObject()
err := builder.FlattenStruct(objectForm, reflect.TypeFor[TailscaleSSHServerOptions]())
if err != nil {
return nil, err
}
return schema.AnyOf(schema.BooleanNode(), objectForm), nil
}
type TailscaleDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
@@ -127,6 +138,15 @@ func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error {
return nil
}
func (d DERPVerifyClientURLOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
objectForm, err := describeHTTPClientObject(builder)
if err != nil {
return nil, err
}
objectForm.Properties.Put("url", schema.StringNode())
return schema.AnyOf(schema.StringNode(), objectForm), nil
}
type DERPMeshOptions struct {
ServerOptions
Host string `json:"host,omitempty"`
@@ -165,3 +185,12 @@ func (d *DERPSTUNListenOptions) UnmarshalJSON(bytes []byte) error {
}
return json.Unmarshal(bytes, (*_DERPSTUNListenOptions)(d))
}
func (d DERPSTUNListenOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
objectForm := schema.StrictObject()
err := builder.FlattenStruct(objectForm, reflect.TypeFor[DERPSTUNListenOptions]())
if err != nil {
return nil, err
}
return schema.AnyOf(schema.UnsignedNode(16), objectForm), nil
}
+23 -14
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"strings"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
)
@@ -13,9 +14,9 @@ type InboundTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
ALPN badoption.Listable[string] `json:"alpn,omitempty"`
MinVersion string `json:"min_version,omitempty"`
MaxVersion string `json:"max_version,omitempty"`
ALPN badoption.Listable[string] `json:"alpn,omitempty" examples:"http/1.1,h2,h3"`
MinVersion string `json:"min_version,omitempty" enum:"1.0,1.1,1.2,1.3"`
MaxVersion string `json:"max_version,omitempty" enum:"1.0,1.1,1.2,1.3"`
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"`
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
@@ -32,7 +33,7 @@ type InboundTLSOptions struct {
CertificateProvider *CertificateProviderOptions `json:"certificate_provider,omitempty"`
// Deprecated: use certificate_provider
ACME *InboundACMEOptions `json:"acme,omitempty"`
ACME *InboundACMEOptions `json:"acme,omitempty" schema:"omit"`
ECH *InboundECHOptions `json:"ech,omitempty"`
Reality *InboundRealityOptions `json:"reality,omitempty"`
@@ -82,6 +83,10 @@ func (t *ClientAuthType) UnmarshalJSON(data []byte) error {
return nil
}
func (t ClientAuthType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("no", "request", "require-any", "verify-if-given", "require-and-verify"), nil
}
type InboundTLSOptionsContainer struct {
TLS *InboundTLSOptions `json:"tls,omitempty"`
}
@@ -101,13 +106,13 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
type OutboundTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
Engine string `json:"engine,omitempty"`
Engine string `json:"engine,omitempty" enum:"go,apple,windows"`
DisableSNI bool `json:"disable_sni,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
ALPN badoption.Listable[string] `json:"alpn,omitempty"`
MinVersion string `json:"min_version,omitempty"`
MaxVersion string `json:"max_version,omitempty"`
ALPN badoption.Listable[string] `json:"alpn,omitempty" examples:"http/1.1,h2,h3"`
MinVersion string `json:"min_version,omitempty" enum:"1.0,1.1,1.2,1.3"`
MaxVersion string `json:"max_version,omitempty" enum:"1.0,1.1,1.2,1.3"`
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"`
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
@@ -121,7 +126,7 @@ type OutboundTLSOptions struct {
FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"`
RecordFragment bool `json:"record_fragment,omitempty"`
Spoof string `json:"spoof,omitempty"`
SpoofMethod string `json:"spoof_method,omitempty"`
SpoofMethod string `json:"spoof_method,omitempty" enum:"wrong-sequence,wrong-checksum,wrong-ack,wrong-md5,wrong-timestamp"`
KernelTx bool `json:"kernel_tx,omitempty"`
KernelRx bool `json:"kernel_rx,omitempty"`
HandshakeTimeout badoption.Duration `json:"handshake_timeout,omitempty"`
@@ -199,6 +204,10 @@ func (c *CurvePreference) UnmarshalJSON(data []byte) error {
return nil
}
func (c CurvePreference) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("P256", "P384", "P521", "X25519", "X25519MLKEM768"), nil
}
type InboundRealityOptions struct {
Enabled bool `json:"enabled,omitempty"`
Handshake InboundRealityHandshakeOptions `json:"handshake,omitempty"`
@@ -218,9 +227,9 @@ type InboundECHOptions struct {
KeyPath string `json:"key_path,omitempty"`
// Deprecated: not supported by stdlib
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"`
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty" schema:"omit"`
// Deprecated: added by fault
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"`
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty" schema:"omit"`
}
type OutboundECHOptions struct {
@@ -230,14 +239,14 @@ type OutboundECHOptions struct {
QueryServerName string `json:"query_server_name,omitempty"`
// Deprecated: not supported by stdlib
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"`
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty" schema:"omit"`
// Deprecated: added by fault
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"`
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty" schema:"omit"`
}
type OutboundUTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Fingerprint string `json:"fingerprint,omitempty" enum:"chrome_psk,chrome_psk_shuffle,chrome_padding_psk_shuffle,chrome_pq,chrome_pq_psk,chrome,firefox,edge,safari,360,qq,ios,android,random,randomized"`
}
type OutboundRealityOptions struct {
+18 -1
View File
@@ -1,7 +1,10 @@
package option
import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -29,7 +32,7 @@ type ACMEExternalAccountOptions struct {
}
type _ACMEDNS01ChallengeOptions struct {
Provider string `json:"provider,omitempty"`
Provider string `json:"provider,omitempty" enum:"alidns,cloudflare,acmedns"`
AliDNSOptions ACMEDNS01AliDNSOptions `json:"-"`
CloudflareOptions ACMEDNS01CloudflareOptions `json:"-"`
ACMEDNSOptions ACMEDNS01ACMEDNSOptions `json:"-"`
@@ -77,6 +80,20 @@ func (o *ACMEDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error {
return nil
}
func acmeDNS01Variants() []schema.UnionVariant {
return []schema.UnionVariant{
{Value: C.DNSProviderAliDNS, StructType: reflect.TypeFor[ACMEDNS01AliDNSOptions]()},
{Value: C.DNSProviderCloudflare, StructType: reflect.TypeFor[ACMEDNS01CloudflareOptions]()},
{Value: C.DNSProviderACMEDNS, StructType: reflect.TypeFor[ACMEDNS01ACMEDNSOptions]()},
}
}
func (o ACMEDNS01ChallengeOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("ACMEDNS01Challenge", func() (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "provider", true, acmeDNS01Variants(), nil)
})
}
type ACMEDNS01AliDNSOptions struct {
AccessKeyID string `json:"access_key_id,omitempty"`
AccessKeySecret string `json:"access_key_secret,omitempty"`
+3 -3
View File
@@ -5,7 +5,7 @@ import "github.com/sagernet/sing/common/json/badoption"
type TUICInboundOptions struct {
ListenOptions
Users []TUICUser `json:"users,omitempty"`
CongestionControl string `json:"congestion_control,omitempty"`
CongestionControl string `json:"congestion_control,omitempty" enum:"cubic,new_reno,bbr"`
AuthTimeout badoption.Duration `json:"auth_timeout,omitempty"`
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
Heartbeat badoption.Duration `json:"heartbeat,omitempty"`
@@ -24,8 +24,8 @@ type TUICOutboundOptions struct {
ServerOptions
UUID string `json:"uuid,omitempty"`
Password string `json:"password,omitempty"`
CongestionControl string `json:"congestion_control,omitempty"`
UDPRelayMode string `json:"udp_relay_mode,omitempty"`
CongestionControl string `json:"congestion_control,omitempty" enum:"cubic,new_reno,bbr"`
UDPRelayMode string `json:"udp_relay_mode,omitempty" enum:"native,quic"`
UDPOverStream bool `json:"udp_over_stream,omitempty"`
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
Heartbeat badoption.Duration `json:"heartbeat,omitempty"`
+17 -12
View File
@@ -4,6 +4,7 @@ import (
"net/netip"
"strconv"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
@@ -12,10 +13,10 @@ import (
type TunInboundOptions struct {
InterfaceName string `json:"interface_name,omitempty"`
NetNs string `json:"netns,omitempty"`
NetNs string `json:"netns,omitempty" reference:"network_namespace"`
MTU uint32 `json:"mtu,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
DNSMode string `json:"dns_mode,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address,omitempty" examples:"172.19.0.1/30,fdfe:dcba:9876::1/126"`
DNSMode string `json:"dns_mode,omitempty" enum:"disabled,native,hijack"`
DNSAddress badoption.Listable[netip.Addr] `json:"dns_address,omitempty"`
AutoRoute bool `json:"auto_route,omitempty"`
IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
@@ -48,26 +49,26 @@ type TunInboundOptions struct {
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
Stack string `json:"stack,omitempty"`
Stack string `json:"stack,omitempty" enum:"system,gvisor,mixed"`
Platform *TunPlatformOptions `json:"platform,omitempty"`
InboundOptions
// Deprecated: removed
GSO bool `json:"gso,omitempty"`
GSO bool `json:"gso,omitempty" schema:"omit"`
// Deprecated: merged to Address
Inet4Address badoption.Listable[netip.Prefix] `json:"inet4_address,omitempty"`
Inet4Address badoption.Listable[netip.Prefix] `json:"inet4_address,omitempty" schema:"omit"`
// Deprecated: merged to Address
Inet6Address badoption.Listable[netip.Prefix] `json:"inet6_address,omitempty"`
Inet6Address badoption.Listable[netip.Prefix] `json:"inet6_address,omitempty" schema:"omit"`
// Deprecated: merged to RouteAddress
Inet4RouteAddress badoption.Listable[netip.Prefix] `json:"inet4_route_address,omitempty"`
Inet4RouteAddress badoption.Listable[netip.Prefix] `json:"inet4_route_address,omitempty" schema:"omit"`
// Deprecated: merged to RouteAddress
Inet6RouteAddress badoption.Listable[netip.Prefix] `json:"inet6_route_address,omitempty"`
Inet6RouteAddress badoption.Listable[netip.Prefix] `json:"inet6_route_address,omitempty" schema:"omit"`
// Deprecated: merged to RouteExcludeAddress
Inet4RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"`
Inet4RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty" schema:"omit"`
// Deprecated: merged to RouteExcludeAddress
Inet6RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"`
Inet6RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty" schema:"omit"`
// Deprecated: removed
EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"`
EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty" schema:"omit"`
}
type FwMark uint32
@@ -92,3 +93,7 @@ func (f *FwMark) UnmarshalJSON(bytes []byte) error {
*f = FwMark(intValue)
return nil
}
func (f FwMark) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.AnyOf(schema.UnsignedNode(32), schema.StringNode()), nil
}
+32
View File
@@ -1,9 +1,12 @@
package option
import (
"maps"
"slices"
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
@@ -43,6 +46,10 @@ func (v NetworkList) Build() []string {
return strings.Split(string(v), "\n")
}
func (v NetworkList) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.ListableOf(schema.StringEnum(N.NetworkTCP, N.NetworkUDP)), nil
}
type DomainStrategy C.DomainStrategy
func (s DomainStrategy) String() string {
@@ -105,6 +112,12 @@ func (s *DomainStrategy) UnmarshalJSON(bytes []byte) error {
return nil
}
func (s DomainStrategy) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DomainStrategy", func() (*schema.Node, error) {
return schema.StringEnum("", "as_is", "prefer_ipv4", "prefer_ipv6", "ipv4_only", "ipv6_only"), nil
})
}
type DNSQueryType uint16
func (t DNSQueryType) String() string {
@@ -142,6 +155,15 @@ func (t *DNSQueryType) UnmarshalJSON(bytes []byte) error {
return E.New("unknown DNS query type: ", string(bytes))
}
func (t DNSQueryType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DNSQueryType", func() (*schema.Node, error) {
return schema.AnyOf(
schema.UnsignedNode(16),
schema.StringEnum(slices.Sorted(maps.Keys(mDNS.StringToType))...),
), nil
})
}
func DNSQueryTypeToString(queryType uint16) string {
typeName, loaded := mDNS.TypeToString[queryType]
if loaded {
@@ -170,6 +192,10 @@ func (n *NetworkStrategy) UnmarshalJSON(content []byte) error {
return nil
}
func (n NetworkStrategy) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum(slices.Sorted(maps.Keys(C.StringToNetworkStrategy))...), nil
}
type InterfaceType C.InterfaceType
func (t InterfaceType) Build() C.InterfaceType {
@@ -193,3 +219,9 @@ func (t *InterfaceType) UnmarshalJSON(content []byte) error {
*t = InterfaceType(interfaceType)
return nil
}
func (t InterfaceType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("InterfaceType", func() (*schema.Node, error) {
return schema.StringEnum(slices.Sorted(maps.Keys(C.StringToInterfaceType))...), nil
})
}
+13 -1
View File
@@ -1,13 +1,16 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/uot"
)
type _UDPOverTCPOptions struct {
Enabled bool `json:"enabled,omitempty"`
Version uint8 `json:"version,omitempty"`
Version uint8 `json:"version,omitempty" enum:"1,2"`
}
type UDPOverTCPOptions _UDPOverTCPOptions
@@ -28,3 +31,12 @@ func (o *UDPOverTCPOptions) UnmarshalJSON(bytes []byte) error {
}
return json.UnmarshalDisallowUnknownFields(bytes, (*_UDPOverTCPOptions)(o))
}
func (o UDPOverTCPOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
objectForm := schema.StrictObject()
err := builder.FlattenStruct(objectForm, reflect.TypeFor[UDPOverTCPOptions]())
if err != nil {
return nil, err
}
return schema.AnyOf(schema.BooleanNode(), objectForm), nil
}
+13 -1
View File
@@ -1,6 +1,9 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -13,7 +16,7 @@ const (
type _USBIPServerServiceOptions struct {
ListenOptions
Provider string `json:"provider,omitempty"`
Provider string `json:"provider,omitempty" enum:"default,dynamic"`
Options any `json:"-"`
}
@@ -49,6 +52,15 @@ func (o *USBIPServerServiceOptions) UnmarshalJSON(content []byte) error {
return nil
}
func (o USBIPServerServiceOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "provider", false, []schema.UnionVariant{
{Value: USBIPProviderDefault, StructType: reflect.TypeFor[USBIPDefaultProviderOptions](), TypeOptional: true},
{Value: USBIPProviderDynamic, StructType: reflect.TypeFor[USBIPDynamicProviderOptions]()},
}, func(variant *schema.Node) error {
return builder.FlattenStruct(variant, reflect.TypeFor[ListenOptions]())
})
}
type USBIPClientServiceOptions struct {
DialerOptions
ServerOptions
+16 -1
View File
@@ -1,7 +1,10 @@
package option
import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -9,7 +12,7 @@ import (
)
type _V2RayTransportOptions struct {
Type string `json:"type"`
Type string `json:"type" enum:"http,ws,quic,grpc,httpupgrade"`
HTTPOptions V2RayHTTPOptions `json:"-"`
WebsocketOptions V2RayWebsocketOptions `json:"-"`
QUICOptions V2RayQUICOptions `json:"-"`
@@ -67,6 +70,18 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
return nil
}
func (o V2RayTransportOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("V2RayTransport", func() (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "type", true, []schema.UnionVariant{
{Value: C.V2RayTransportTypeHTTP, StructType: reflect.TypeFor[V2RayHTTPOptions]()},
{Value: C.V2RayTransportTypeWebsocket, StructType: reflect.TypeFor[V2RayWebsocketOptions]()},
{Value: C.V2RayTransportTypeQUIC, StructType: reflect.TypeFor[V2RayQUICOptions]()},
{Value: C.V2RayTransportTypeGRPC, StructType: reflect.TypeFor[V2RayGRPCOptions]()},
{Value: C.V2RayTransportTypeHTTPUpgrade, StructType: reflect.TypeFor[V2RayHTTPUpgradeOptions]()},
}, nil)
})
}
type V2RayHTTPOptions struct {
Host badoption.Listable[string] `json:"host,omitempty"`
Path string `json:"path,omitempty"`
+2 -2
View File
@@ -18,13 +18,13 @@ type VMessOutboundOptions struct {
DialerOptions
ServerOptions
UUID string `json:"uuid"`
Security string `json:"security"`
Security string `json:"security" enum:"auto,none,zero,aes-128-cfb,aes-128-gcm,chacha20-poly1305"`
AlterId int `json:"alter_id,omitempty"`
GlobalPadding bool `json:"global_padding,omitempty"`
AuthenticatedLength bool `json:"authenticated_length,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
PacketEncoding string `json:"packet_encoding,omitempty"`
PacketEncoding string `json:"packet_encoding,omitempty" enum:"packetaddr,xudp"`
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`
}
+3 -1
View File
@@ -72,7 +72,9 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
domainStrategy: C.DomainStrategy(options.DomainStrategy),
fallbackDelay: time.Duration(options.FallbackDelay),
dialer: outboundDialer.(dialer.ParallelInterfaceDialer),
isEmpty: reflect.DeepEqual(options.DialerOptions, option.DialerOptions{UDPFragmentDefault: true}),
isEmpty: reflect.DeepEqual(options.DialerOptions, option.DialerOptions{
AbstractDialerOptions: option.AbstractDialerOptions{UDPFragmentDefault: true},
}),
}
//nolint:staticcheck
if options.ProxyProtocol != 0 {
+5 -3
View File
@@ -184,9 +184,11 @@ func (s *ServerEndpoint) Start(stage adapter.StartStage) error {
listenAddress := s.options.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1}))
if listenAddress.IsUnspecified() && s.options.BindInterface == "" && s.options.RoutingMark == 0 && s.options.NetNs == "" {
udpDialer, dialerErr := dialer.NewDefault(s.ctx, option.DialerOptions{
ReuseAddr: s.options.ReuseAddr,
UDPFragment: s.options.UDPFragment,
UDPFragmentDefault: s.options.UDPFragmentDefault,
AbstractDialerOptions: option.AbstractDialerOptions{
ReuseAddr: s.options.ReuseAddr,
UDPFragment: s.options.UDPFragment,
UDPFragmentDefault: s.options.UDPFragmentDefault,
},
})
if dialerErr != nil {
return dialerErr
+3 -1
View File
@@ -320,7 +320,9 @@ func (t *Endpoint) start() error {
return err
}
systemDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: tunName,
AbstractDialerOptions: option.AbstractDialerOptions{
BindInterface: tunName,
},
})
if err != nil {
_ = systemTun.Close()
+3 -1
View File
@@ -98,7 +98,9 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
Dialer: outboundDialer,
CreateDialer: func(interfaceName string) N.Dialer {
return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{
BindInterface: interfaceName,
AbstractDialerOptions: option.AbstractDialerOptions{
BindInterface: interfaceName,
},
}))
},
Name: options.Name,
+12 -10
View File
@@ -74,7 +74,9 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
RuleActionRouteOptions: routeOptions,
}, nil
case C.RuleActionTypeDirect:
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
directDialer, err := dialer.New(ctx, option.DialerOptions{
AbstractDialerOptions: action.DirectOptions.AbstractDialerOptions,
}, false)
if err != nil {
return nil, err
}
@@ -141,16 +143,16 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
}
case C.RuleActionTypeEvaluate:
return &RuleActionEvaluate{
Server: action.RouteOptions.Server,
Tag: action.RouteOptions.Tag,
Speculative: action.RouteOptions.Speculative,
Server: action.EvaluateOptions.Server,
Tag: action.EvaluateOptions.Tag,
Speculative: action.EvaluateOptions.Speculative,
RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
Timeout: time.Duration(action.RouteOptions.Timeout),
DisableCache: action.RouteOptions.DisableCache,
DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache,
RewriteTTL: action.RouteOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
Strategy: C.DomainStrategy(action.EvaluateOptions.Strategy),
Timeout: time.Duration(action.EvaluateOptions.Timeout),
DisableCache: action.EvaluateOptions.DisableCache,
DisableOptimisticCache: action.EvaluateOptions.DisableOptimisticCache,
RewriteTTL: action.EvaluateOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.EvaluateOptions.ClientSubnet)),
},
}
case C.RuleActionTypeRespond:
+10 -2
View File
@@ -32,10 +32,14 @@ func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DN
return nil, E.New("`race` requires `match_response`")
}
switch options.DefaultOptions.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
case "", C.RuleActionTypeRoute:
if options.DefaultOptions.RouteOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
case C.RuleActionTypeEvaluate:
if options.DefaultOptions.EvaluateOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
}
return NewDefaultDNSRule(ctx, logger, options.DefaultOptions, legacyDNSMode)
case C.RuleTypeLogical:
@@ -50,10 +54,14 @@ func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DN
return nil, err
}
switch options.LogicalOptions.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
case "", C.RuleActionTypeRoute:
if options.LogicalOptions.RouteOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
case C.RuleActionTypeEvaluate:
if options.LogicalOptions.EvaluateOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
}
return NewLogicalDNSRule(ctx, logger, options.LogicalOptions, legacyDNSMode)
default:
+58
View File
@@ -0,0 +1,58 @@
package schema
import (
"context"
"reflect"
E "github.com/sagernet/sing/common/exceptions"
)
type Builder interface {
Context() context.Context
Describe(valueType reflect.Type) (*Node, error)
FlattenStruct(node *Node, structType reflect.Type) error
Define(name string, build func() (*Node, error)) (*Node, error)
}
type Describer interface {
DescribeSchema(builder Builder) (*Node, error)
}
type UnionVariant struct {
Value any
StructType reflect.Type
TypeOptional bool
}
func DiscriminatedUnion(builder Builder, discriminatorKey string, discriminatorRequired bool, variants []UnionVariant, buildBase func(variant *Node) error) (*Node, error) {
variantNodes := make([]*Node, 0, len(variants))
for _, variant := range variants {
variantNode := StrictObject()
if variant.TypeOptional {
stringValue, isString := variant.Value.(string)
if !isString {
return nil, E.New("optional discriminator requires a string value")
}
variantNode.Properties.Put(discriminatorKey, StringEnum(stringValue, ""))
} else {
variantNode.Properties.Put(discriminatorKey, &Node{Const: variant.Value})
}
if discriminatorRequired || !variant.TypeOptional {
variantNode.Required = append(variantNode.Required, discriminatorKey)
}
if buildBase != nil {
err := buildBase(variantNode)
if err != nil {
return nil, err
}
}
if variant.StructType != nil {
err := builder.FlattenStruct(variantNode, variant.StructType)
if err != nil {
return nil, err
}
}
variantNodes = append(variantNodes, variantNode)
}
return OneOf(variantNodes...), nil
}
+25
View File
@@ -0,0 +1,25 @@
package schema
import (
"context"
stdjson "encoding/json"
"reflect"
)
func Generate(ctx context.Context, rootType reflect.Type) ([]byte, error) {
g := &generator{
ctx: ctx,
defs: make(map[string]*Node),
defTypes: make(map[reflect.Type]string),
}
root, err := g.Describe(rootType)
if err != nil {
return nil, err
}
root.Defs = g.sortedDefs()
content, err := stdjson.MarshalIndent(root, "", " ")
if err != nil {
return nil, err
}
return append(content, '\n'), nil
}
+421
View File
@@ -0,0 +1,421 @@
package schema
import (
"context"
"encoding"
stdjson "encoding/json"
"reflect"
"slices"
"strconv"
"strings"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
var (
jsonUnmarshalerType = reflect.TypeFor[stdjson.Unmarshaler]()
contextUnmarshalerType = reflect.TypeFor[json.ContextUnmarshaler]()
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
durationType = reflect.TypeFor[badoption.Duration]()
addrType = reflect.TypeFor[badoption.Addr]()
prefixType = reflect.TypeFor[badoption.Prefix]()
prefixableType = reflect.TypeFor[badoption.Prefixable]()
httpHeaderType = reflect.TypeFor[badoption.HTTPHeader]()
memoryBytesType = reflect.TypeFor[byteformats.MemoryBytes]()
networkBytesCompatType = reflect.TypeFor[byteformats.NetworkBytesCompat]()
)
type generator struct {
ctx context.Context
defs map[string]*Node
defTypes map[reflect.Type]string
path []string
}
func (g *generator) Context() context.Context {
return g.ctx
}
func (g *generator) Define(name string, build func() (*Node, error)) (*Node, error) {
_, exists := g.defs[name]
if exists {
return RefNode(name), nil
}
g.defs[name] = nil
node, err := build()
if err != nil {
return nil, err
}
g.defs[name] = node
return RefNode(name), nil
}
func implementationOf[T any](valueType reflect.Type) (T, bool) {
interfaceType := reflect.TypeFor[T]()
if !valueType.Implements(interfaceType) && !reflect.PointerTo(valueType).Implements(interfaceType) {
var zeroValue T
return zeroValue, false
}
return reflect.New(valueType).Interface().(T), true
}
func (g *generator) Describe(valueType reflect.Type) (*Node, error) {
for valueType.Kind() == reflect.Pointer {
valueType = valueType.Elem()
}
describer, described := implementationOf[Describer](valueType)
if described {
return describer.DescribeSchema(g)
}
switch valueType {
case durationType:
return g.Define("Duration", func() (*Node, error) {
return DurationNode(), nil
})
case addrType, prefixType, prefixableType:
return StringNode(), nil
case httpHeaderType:
return g.Define("HTTPHeader", func() (*Node, error) {
return &Node{Type: "object", AdditionalProperties: ListableOf(StringNode())}, nil
})
case memoryBytesType, networkBytesCompatType:
return AnyOf(UnsignedNode(64), StringNode()), nil
}
if isListable(valueType) {
elementNode, err := g.Describe(valueType.Elem())
if err != nil {
return nil, err
}
return ListableOf(elementNode), nil
}
if isTypedMap(valueType) {
return g.typedMapNode(valueType)
}
pointerType := reflect.PointerTo(valueType)
if pointerType.Implements(jsonUnmarshalerType) || pointerType.Implements(contextUnmarshalerType) {
return nil, E.New("unmapped custom JSON type ", valueType.String(), " at ", strings.Join(g.path, "."))
}
if pointerType.Implements(textUnmarshalerType) {
return StringNode(), nil
}
switch valueType.Kind() {
case reflect.Struct:
if valueType.Name() == "" {
node := StrictObject()
err := g.FlattenStruct(node, valueType)
if err != nil {
return nil, err
}
return node, nil
}
return g.Define(g.defNameFor(valueType), func() (*Node, error) {
node := StrictObject()
err := g.FlattenStruct(node, valueType)
if err != nil {
return nil, err
}
return node, nil
})
case reflect.Slice, reflect.Array:
if valueType.Kind() == reflect.Slice && valueType.Elem().Kind() == reflect.Uint8 {
// encoding/json accepts both base64 strings and number arrays.
return AnyOf(StringNode(), &Node{Type: "array", Items: UnsignedNode(8)}), nil
}
elementNode, err := g.Describe(valueType.Elem())
if err != nil {
return nil, err
}
return &Node{Type: "array", Items: elementNode}, nil
case reflect.Map:
if valueType.Key().Kind() != reflect.String {
return nil, E.New("unsupported map key type ", valueType.String(), " at ", strings.Join(g.path, "."))
}
valueNode, err := g.Describe(valueType.Elem())
if err != nil {
return nil, err
}
return &Node{Type: "object", AdditionalProperties: valueNode}, nil
case reflect.Bool:
return BooleanNode(), nil
case reflect.String:
return StringNode(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return IntegerNode(), nil
case reflect.Uint, reflect.Uint64:
return UnsignedNode(64), nil
case reflect.Uint8:
return UnsignedNode(8), nil
case reflect.Uint16:
return UnsignedNode(16), nil
case reflect.Uint32:
return UnsignedNode(32), nil
case reflect.Float32, reflect.Float64:
return &Node{Type: "number"}, nil
default:
return nil, E.New("unsupported kind ", valueType.Kind().String(), " for ", valueType.String(), " at ", strings.Join(g.path, "."))
}
}
func (g *generator) defNameFor(fieldType reflect.Type) string {
existingName, loaded := g.defTypes[fieldType]
if loaded {
return existingName
}
name := fieldType.Name()
for otherType, otherName := range g.defTypes {
if otherName == name && otherType != fieldType {
name = pathBase(fieldType.PkgPath()) + "." + name
break
}
}
g.defTypes[fieldType] = name
return name
}
func pathBase(packagePath string) string {
index := strings.LastIndexByte(packagePath, '/')
if index < 0 {
return packagePath
}
return packagePath[index+1:]
}
// FlattenStruct merges the JSON fields of structType into node, following the
// same flattening semantics as badjson.MarshallObjects / anonymous embedding.
func (g *generator) FlattenStruct(node *Node, structType reflect.Type) error {
for structType.Kind() == reflect.Pointer {
structType = structType.Elem()
}
if structType.Kind() != reflect.Struct {
return E.New("cannot flatten non-struct type ", structType.String(), " at ", strings.Join(g.path, "."))
}
for i := range structType.NumField() {
field := structType.Field(i)
if !field.IsExported() && !field.Anonymous {
continue
}
tagValue := field.Tag.Get("json")
tagName, _, _ := strings.Cut(tagValue, ",")
if tagName == "-" {
continue
}
fieldType := field.Type
for fieldType.Kind() == reflect.Pointer {
fieldType = fieldType.Elem()
}
if field.Tag.Get("schema") == "omit" {
continue
}
if field.Anonymous && tagName == "" {
err := g.FlattenStruct(node, fieldType)
if err != nil {
return err
}
continue
}
if tagName == "" {
tagName = field.Name
}
enumTag := field.Tag.Get("enum")
examplesTag := field.Tag.Get("examples")
referenceTag := field.Tag.Get("reference")
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)
} else {
fieldNode, err = g.Describe(fieldType)
}
g.path = g.path[:len(g.path)-1]
if err != nil {
return err
}
node.Properties.Put(tagName, fieldNode)
}
return nil
}
func taggedFieldNode(fieldType reflect.Type, enumTag string, examplesTag string, referenceTag string) (*Node, error) {
elementType := fieldType
for elementType.Kind() == reflect.Pointer {
elementType = elementType.Elem()
}
listable := isListable(fieldType)
plainSlice := !listable && elementType.Kind() == reflect.Slice && elementType.Elem().Kind() == reflect.String
if listable {
elementType = fieldType.Elem()
} else if plainSlice {
elementType = elementType.Elem()
}
var element *Node
var err error
if 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
}
}
if examplesTag != "" {
examples, parseErr := taggedValues(elementType, strings.Split(examplesTag, ","))
if parseErr != nil {
return nil, parseErr
}
element.Examples = examples
}
if referenceTag != "" {
if elementType.Kind() != reflect.String {
return nil, E.New("reference tags require a string field, got ", fieldType.String())
}
element.TagReference = referenceTag
}
if listable {
return ListableOf(element), nil
}
if plainSlice {
return &Node{Type: "array", Items: element}, nil
}
return element, nil
}
func taggedValueNode(fieldType reflect.Type, values []string) (*Node, error) {
switch fieldType.Kind() {
case reflect.String:
node := StringNode()
for _, value := range values {
node.Enum = append(node.Enum, value)
}
return node, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
node := IntegerNode()
enumValues, err := taggedValues(fieldType, values)
if err != nil {
return nil, err
}
node.Enum = enumValues
return node, nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
node := UnsignedNode(fieldType.Bits())
enumValues, err := taggedValues(fieldType, values)
if err != nil {
return nil, err
}
node.Enum = enumValues
return node, nil
default:
node := StringNode()
for _, value := range values {
err := unmarshalTaggedValue(fieldType, value)
if err != nil {
return nil, err
}
node.Enum = append(node.Enum, value)
}
return node, nil
}
}
func unmarshalTaggedValue(fieldType reflect.Type, value string) error {
err := json.Unmarshal([]byte(strconv.Quote(value)), reflect.New(fieldType).Interface())
if err != nil {
return E.Cause(err, "unmarshal tagged value ", value, " as ", fieldType.String())
}
return nil
}
func taggedValues(fieldType reflect.Type, values []string) ([]any, error) {
result := make([]any, 0, len(values))
for _, value := range values {
switch fieldType.Kind() {
case reflect.String:
result = append(result, value)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
integerValue, err := strconv.ParseInt(value, 10, fieldType.Bits())
if err != nil {
return nil, E.Cause(err, "parse enum value ", value, " for ", fieldType.String())
}
result = append(result, integerValue)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
unsignedValue, err := strconv.ParseUint(value, 10, fieldType.Bits())
if err != nil {
return nil, E.Cause(err, "parse enum value ", value, " for ", fieldType.String())
}
result = append(result, unsignedValue)
default:
err := unmarshalTaggedValue(fieldType, value)
if err != nil {
return nil, err
}
result = append(result, value)
}
}
return result, nil
}
func isListable(fieldType reflect.Type) bool {
return fieldType.Kind() == reflect.Slice &&
fieldType.PkgPath() == "github.com/sagernet/sing/common/json/badoption" &&
strings.HasPrefix(fieldType.Name(), "Listable[")
}
func isTypedMap(fieldType reflect.Type) bool {
return fieldType.Kind() == reflect.Struct &&
fieldType.PkgPath() == "github.com/sagernet/sing/common/json/badjson" &&
strings.HasPrefix(fieldType.Name(), "TypedMap[")
}
func (g *generator) typedMapNode(fieldType reflect.Type) (*Node, error) {
mapField, found := fieldType.FieldByName("Map")
if !found {
return nil, E.New("unexpected TypedMap layout: missing Map in ", fieldType.String())
}
rawMapField, found := mapField.Type.FieldByName("rawMap")
if !found {
return nil, E.New("unexpected TypedMap layout: missing rawMap in ", fieldType.String())
}
keyType := rawMapField.Type.Key()
elementValueField, found := rawMapField.Type.Elem().Elem().FieldByName("Value")
if !found {
return nil, E.New("unexpected TypedMap layout: missing element value in ", fieldType.String())
}
entryValueField, found := elementValueField.Type.FieldByName("Value")
if !found {
return nil, E.New("unexpected TypedMap layout: missing entry value in ", fieldType.String())
}
valueNode, err := g.Describe(entryValueField.Type)
if err != nil {
return nil, err
}
node := &Node{Type: "object", AdditionalProperties: valueNode}
if keyType.Kind() != reflect.String || keyType.PkgPath() != "" {
keyNode, keyErr := g.Describe(keyType)
if keyErr != nil {
return nil, keyErr
}
node.PropertyNames = keyNode
}
return node, nil
}
func (g *generator) sortedDefs() *badjson.TypedMap[string, *Node] {
names := make([]string, 0, len(g.defs))
for name := range g.defs {
names = append(names, name)
}
slices.Sort(names)
result := new(badjson.TypedMap[string, *Node])
for _, name := range names {
result.Put(name, g.defs[name])
}
return result
}
+105
View File
@@ -0,0 +1,105 @@
package schema
import (
"github.com/sagernet/sing/common/json/badjson"
)
type Node struct {
SchemaURI string `json:"$schema,omitempty"`
ID string `json:"$id,omitempty"`
Ref string `json:"$ref,omitempty"`
Type any `json:"type,omitempty"`
Const any `json:"const,omitempty"`
Enum []any `json:"enum,omitempty"`
Pattern string `json:"pattern,omitempty"`
Minimum *int64 `json:"minimum,omitempty"`
Maximum *uint64 `json:"maximum,omitempty"`
Items *Node `json:"items,omitempty"`
Properties *badjson.TypedMap[string, *Node] `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
PropertyNames *Node `json:"propertyNames,omitempty"`
AdditionalProperties any `json:"additionalProperties,omitempty"`
UnevaluatedProperties any `json:"unevaluatedProperties,omitempty"`
AllOf []*Node `json:"allOf,omitempty"`
AnyOf []*Node `json:"anyOf,omitempty"`
OneOf []*Node `json:"oneOf,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
Examples []any `json:"examples,omitempty"`
TagReference string `json:"x-tag-reference,omitempty"`
Defs *badjson.TypedMap[string, *Node] `json:"$defs,omitempty"`
}
func StrictObject() *Node {
return &Node{
Type: "object",
Properties: new(badjson.TypedMap[string, *Node]),
AdditionalProperties: false,
}
}
func LooseObject() *Node {
return &Node{
Type: "object",
Properties: new(badjson.TypedMap[string, *Node]),
}
}
func StringNode() *Node {
return &Node{Type: "string"}
}
func TagReferenceNode(kind string) *Node {
return &Node{Type: "string", TagReference: kind}
}
func BooleanNode() *Node {
return &Node{Type: "boolean"}
}
func IntegerNode() *Node {
return &Node{Type: "integer"}
}
func UnsignedNode(bits int) *Node {
minimumValue := int64(0)
node := &Node{Type: "integer", Minimum: &minimumValue}
if bits < 64 {
maximumValue := uint64(1)<<bits - 1
node.Maximum = &maximumValue
}
return node
}
const durationPattern = `^[-+]?(((\d+(\.\d*)?|\.\d+)(ns|us|µs|μs|ms|s|m|h|d))+|0)$`
func DurationNode() *Node {
return &Node{Type: "string", Pattern: durationPattern}
}
func StringEnum(values ...string) *Node {
anyValues := make([]any, 0, len(values))
for _, value := range values {
anyValues = append(anyValues, value)
}
return &Node{Type: "string", Enum: anyValues}
}
func StringConst(value string) *Node {
return &Node{Const: value}
}
func AnyOf(nodes ...*Node) *Node {
return &Node{AnyOf: nodes}
}
func OneOf(nodes ...*Node) *Node {
return &Node{OneOf: nodes}
}
func ListableOf(element *Node) *Node {
return AnyOf(element, &Node{Type: "array", Items: element})
}
func RefNode(name string) *Node {
return &Node{Ref: "#/$defs/" + name}
}
+4 -2
View File
@@ -131,8 +131,10 @@ func (t *Transport) updateTransports(link *TransportLink) error {
}
}
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: link.iif.Name,
UDPFragmentDefault: true,
AbstractDialerOptions: option.AbstractDialerOptions{
BindInterface: link.iif.Name,
UDPFragmentDefault: true,
},
}))
var transports []adapter.DNSTransport
for _, address := range link.address {
+3 -1
View File
@@ -52,7 +52,9 @@ func TestTCPSlowOpen(t *testing.T) {
ServerPort: serverPort,
},
DialerOptions: option.DialerOptions{
TCPFastOpen: true,
AbstractDialerOptions: option.AbstractDialerOptions{
TCPFastOpen: true,
},
},
Method: method,
Password: password,
+3 -1
View File
@@ -46,7 +46,9 @@ func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
options.MTU = DefaultMTU
}
interfaceDialer, err := dialer.NewDefault(options.Context, option.DialerOptions{
BindInterface: options.Name,
AbstractDialerOptions: option.AbstractDialerOptions{
BindInterface: options.Name,
},
})
if err != nil {
return nil, err
+3 -1
View File
@@ -46,7 +46,9 @@ func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
options.MTU = DefaultMTU
}
interfaceDialer, err := dialer.NewDefault(options.Context, option.DialerOptions{
BindInterface: options.Name,
AbstractDialerOptions: option.AbstractDialerOptions{
BindInterface: options.Name,
},
})
if err != nil {
return nil, err