mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Add JSON schema support
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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/) |
|
||||
|
||||
@@ -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/) |
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
```
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:"-"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user