Merge tag 'v1.14.0'

This commit is contained in:
Shtorm
2026-09-04 01:08:30 +03:00
987 changed files with 141481 additions and 11302 deletions
+125
View File
@@ -0,0 +1,125 @@
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"
"github.com/sagernet/sing/common/json/badoption"
)
type ACMECertificateProviderOptions struct {
Domain badoption.Listable[string] `json:"domain,omitempty"`
DataDirectory string `json:"data_directory,omitempty"`
DefaultServerName string `json:"default_server_name,omitempty"`
Email string `json:"email,omitempty"`
Provider string `json:"provider,omitempty"`
AccountKey string `json:"account_key,omitempty"`
DisableHTTPChallenge bool `json:"disable_http_challenge,omitempty"`
DisableTLSALPNChallenge bool `json:"disable_tls_alpn_challenge,omitempty"`
AlternativeHTTPPort uint16 `json:"alternative_http_port,omitempty"`
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" 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"`
}
type ACMEProviderDNS01ChallengeOptions _ACMEProviderDNS01ChallengeOptions
func (o ACMEProviderDNS01ChallengeOptions) MarshalJSON() ([]byte, error) {
var v any
switch o.Provider {
case C.DNSProviderAliDNS:
v = o.AliDNSOptions
case C.DNSProviderCloudflare:
v = o.CloudflareOptions
case C.DNSProviderACMEDNS:
v = o.ACMEDNSOptions
case "":
return nil, E.New("missing provider type")
default:
return nil, E.New("unknown provider type: ", o.Provider)
}
return badjson.MarshallObjects(_ACMEProviderDNS01ChallengeOptions(o), v)
}
func (o *ACMEProviderDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_ACMEProviderDNS01ChallengeOptions)(o))
if err != nil {
return err
}
var v any
switch o.Provider {
case C.DNSProviderAliDNS:
v = &o.AliDNSOptions
case C.DNSProviderCloudflare:
v = &o.CloudflareOptions
case C.DNSProviderACMEDNS:
v = &o.ACMEDNSOptions
case "":
return E.New("missing provider type")
default:
return E.New("unknown provider type: ", o.Provider)
}
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 (
ACMEKeyTypeED25519 = ACMEKeyType("ed25519")
ACMEKeyTypeP256 = ACMEKeyType("p256")
ACMEKeyTypeP384 = ACMEKeyType("p384")
ACMEKeyTypeRSA2048 = ACMEKeyType("rsa2048")
ACMEKeyTypeRSA4096 = ACMEKeyType("rsa4096")
)
func (t *ACMEKeyType) UnmarshalJSON(data []byte) error {
var value string
err := json.Unmarshal(data, &value)
if err != nil {
return err
}
value = strings.ToLower(value)
switch ACMEKeyType(value) {
case "", ACMEKeyTypeED25519, ACMEKeyTypeP256, ACMEKeyTypeP384, ACMEKeyTypeRSA2048, ACMEKeyTypeRSA4096:
*t = ACMEKeyType(value)
default:
return E.New("unknown ACME key type: ", value)
}
return nil
}
func (t ACMEKeyType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("", "ed25519", "p256", "p384", "rsa2048", "rsa4096"), nil
}
+62
View File
@@ -0,0 +1,62 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
)
type APIServiceOptions struct {
ListenOptions
Secret string `json:"secret,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
}
type _APIDashboardOptions struct {
Enabled bool `json:"enabled,omitempty"`
Path string `json:"path,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
}
type APIDashboardOptions _APIDashboardOptions
func (o APIDashboardOptions) MarshalJSON() ([]byte, error) {
if o.DownloadURL == "" && o.HTTPClient == nil && o.UpdateInterval == 0 {
if o.Path == "" {
return json.Marshal(o.Enabled)
}
if o.Enabled {
return json.Marshal(o.Path)
}
}
return json.Marshal(_APIDashboardOptions(o))
}
func (o *APIDashboardOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, &o.Enabled)
if err == nil {
return nil
}
err = json.Unmarshal(bytes, &o.Path)
if err == nil {
o.Enabled = true
return nil
}
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
}
+8
View File
@@ -0,0 +1,8 @@
package option
type BridgeOutboundOptions struct {
Interface string `json:"interface,omitempty"`
BridgeName string `json:"bridge_name,omitempty"`
IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
}
+1 -1
View File
@@ -10,7 +10,7 @@ type CCMServiceOptions struct {
CredentialPath string `json:"credential_path,omitempty"`
Users []CCMUser `json:"users,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
Detour string `json:"detour,omitempty"`
Detour string `json:"detour,omitempty" reference:"outbound"`
UsagesPath string `json:"usages_path,omitempty"`
}
+13 -1
View File
@@ -1,13 +1,16 @@
package option
import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
)
type _CertificateOptions struct {
Store string `json:"store,omitempty"`
Store string `json:"store,omitempty" enum:"system,mozilla,chrome,none"`
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
@@ -34,3 +37,12 @@ func (o *CertificateOptions) UnmarshalJSON(data []byte) error {
}
return nil
}
func (o CertificateOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
node := schema.StrictObject()
err := builder.FlattenStruct(node, reflect.TypeFor[CertificateOptions]())
if err != nil {
return nil, err
}
return node, nil
}
+126
View File
@@ -0,0 +1,126 @@
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"
"github.com/sagernet/sing/service"
)
type CertificateProviderOptionsRegistry interface {
OptionTypes() []string
CreateOptions(providerType string) (any, bool)
}
type _CertificateProvider struct {
Type string `json:"type"`
Tag string `json:"tag,omitempty"`
Options any `json:"-"`
}
type CertificateProvider _CertificateProvider
func (h *CertificateProvider) MarshalJSONContext(ctx context.Context) ([]byte, error) {
return badjson.MarshallObjectsContext(ctx, (*_CertificateProvider)(h), h.Options)
}
func (h *CertificateProvider) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, (*_CertificateProvider)(h))
if err != nil {
return err
}
registry := service.FromContext[CertificateProviderOptionsRegistry](ctx)
if registry == nil {
return E.New("missing certificate provider options registry in context")
}
options, loaded := registry.CreateOptions(h.Type)
if !loaded {
return E.New("unknown certificate provider type: ", h.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, (*_CertificateProvider)(h), options)
if err != nil {
return err
}
h.Options = options
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:"-"`
Options any `json:"-"`
}
type _CertificateProviderInline struct {
Type string `json:"type"`
}
func (o *CertificateProviderOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
if o.Tag != "" {
return json.Marshal(o.Tag)
}
return badjson.MarshallObjectsContext(ctx, _CertificateProviderInline{Type: o.Type}, o.Options)
}
func (o *CertificateProviderOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
if len(content) == 0 {
return E.New("empty certificate_provider value")
}
if content[0] == '"' {
return json.UnmarshalContext(ctx, content, &o.Tag)
}
var inline _CertificateProviderInline
err := json.UnmarshalContext(ctx, content, &inline)
if err != nil {
return err
}
o.Type = inline.Type
if o.Type == "" {
return E.New("missing certificate provider type")
}
registry := service.FromContext[CertificateProviderOptionsRegistry](ctx)
if registry == nil {
return E.New("missing certificate provider options registry in context")
}
options, loaded := registry.CreateOptions(o.Type)
if !loaded {
return E.New("unknown certificate provider type: ", o.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, &inline, options)
if err != nil {
return err
}
o.Options = options
return nil
}
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
})
}
+16
View File
@@ -0,0 +1,16 @@
package option
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" enum:"auto,quic,http2,h2mux"`
PostQuantum bool `json:"post_quantum,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"`
TunnelDialer DialerOptions `json:"tunnel_dialer,omitempty"`
}
+3 -3
View File
@@ -17,11 +17,11 @@ type DirectInboundOptions struct {
type _DirectOutboundOptions struct {
DialerOptions
// Deprecated: Use Route Action instead
OverrideAddress string `json:"override_address,omitempty"`
OverrideAddress string `json:"override_address,omitempty" schema:"omit"`
// Deprecated: Use Route Action instead
OverridePort uint16 `json:"override_port,omitempty"`
OverridePort uint16 `json:"override_port,omitempty" schema:"omit"`
// Deprecated: removed
ProxyProtocol uint8 `json:"proxy_protocol,omitempty"`
ProxyProtocol uint8 `json:"proxy_protocol,omitempty" schema:"omit"`
}
type DirectOutboundOptions _DirectOutboundOptions
+73 -271
View File
@@ -3,121 +3,106 @@ package option
import (
"context"
"net/netip"
"net/url"
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing/common"
"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"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
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
}
type LegacyDNSOptions struct {
FakeIP *LegacyDNSFakeIPOptions `json:"fakeip,omitempty"`
}
type DNSOptions struct {
RawDNSOptions
LegacyDNSOptions
}
type contextKeyDontUpgrade struct{}
const (
legacyDNSFakeIPRemovedMessage = "legacy DNS fakeip options are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats"
legacyDNSServerRemovedMessage = "legacy DNS server formats are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats"
)
func ContextWithDontUpgrade(ctx context.Context) context.Context {
return context.WithValue(ctx, (*contextKeyDontUpgrade)(nil), true)
}
func dontUpgradeFromContext(ctx context.Context) bool {
return ctx.Value((*contextKeyDontUpgrade)(nil)) == true
type removedLegacyDNSOptions struct {
FakeIP json.RawMessage `json:"fakeip,omitempty"`
}
func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, &o.LegacyDNSOptions)
var legacyOptions removedLegacyDNSOptions
err := json.UnmarshalContext(ctx, content, &legacyOptions)
if err != nil {
return err
}
dontUpgrade := dontUpgradeFromContext(ctx)
legacyOptions := o.LegacyDNSOptions
if !dontUpgrade {
if o.FakeIP != nil && o.FakeIP.Enabled {
deprecated.Report(ctx, deprecated.OptionLegacyDNSFakeIPOptions)
ctx = context.WithValue(ctx, (*LegacyDNSFakeIPOptions)(nil), o.FakeIP)
}
o.LegacyDNSOptions = LegacyDNSOptions{}
if len(legacyOptions.FakeIP) != 0 {
return E.New(legacyDNSFakeIPRemovedMessage)
}
err = badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions)
if err != nil {
return err
}
if !dontUpgrade {
rcodeMap := make(map[string]int)
o.Servers = common.Filter(o.Servers, func(it DNSServerOptions) bool {
if it.Type == C.DNSTypeLegacyRcode {
rcodeMap[it.Tag] = it.Options.(int)
return false
}
return true
})
if len(rcodeMap) > 0 {
for i := 0; i < len(o.Rules); i++ {
rewriteRcode(rcodeMap, &o.Rules[i])
}
}
}
return nil
return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions)
}
func rewriteRcode(rcodeMap map[string]int, rule *DNSRule) {
switch rule.Type {
case C.RuleTypeDefault:
rewriteRcodeAction(rcodeMap, &rule.DefaultOptions.DNSRuleAction)
case C.RuleTypeLogical:
rewriteRcodeAction(rcodeMap, &rule.LogicalOptions.DNSRuleAction)
}
}
func rewriteRcodeAction(rcodeMap map[string]int, ruleAction *DNSRuleAction) {
if ruleAction.Action != C.RuleActionTypeRoute {
return
}
rcode, loaded := rcodeMap[ruleAction.RouteOptions.Server]
if !loaded {
return
}
ruleAction.Action = C.RuleActionTypePredefined
ruleAction.PredefinedOptions.Rcode = common.Ptr(DNSRCode(rcode))
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"`
}
type LegacyDNSFakeIPOptions struct {
Enabled bool `json:"enabled,omitempty"`
Inet4Range *badoption.Prefix `json:"inet4_range,omitempty"`
Inet6Range *badoption.Prefix `json:"inet6_range,omitempty"`
type _OptimisticDNSOptions struct {
Enabled bool `json:"enabled,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
}
type OptimisticDNSOptions _OptimisticDNSOptions
func (o OptimisticDNSOptions) MarshalJSON() ([]byte, error) {
if o.Timeout == 0 {
return json.Marshal(o.Enabled)
}
return json.Marshal(_OptimisticDNSOptions(o))
}
func (o *OptimisticDNSOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, &o.Enabled)
if err == nil {
return nil
}
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 {
@@ -129,10 +114,6 @@ type _DNSServerOptions struct {
type DNSServerOptions _DNSServerOptions
func (o *DNSServerOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
switch o.Type {
case C.DNSTypeLegacy:
o.Type = ""
}
return badjson.MarshallObjectsContext(ctx, (*_DNSServerOptions)(o), o.Options)
}
@@ -148,9 +129,7 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b
var options any
switch o.Type {
case "", C.DNSTypeLegacy:
o.Type = C.DNSTypeLegacy
options = new(LegacyDNSServerOptions)
deprecated.Report(ctx, deprecated.OptionLegacyDNSTransport)
return E.New(legacyDNSServerRemovedMessage)
default:
var loaded bool
options, loaded = registry.CreateOptions(o.Type)
@@ -163,183 +142,17 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b
return err
}
o.Options = options
if o.Type == C.DNSTypeLegacy && !dontUpgradeFromContext(ctx) {
err = o.Upgrade(ctx)
if err != nil {
return err
}
}
return nil
}
func (o *DNSServerOptions) Upgrade(ctx context.Context) error {
if o.Type != C.DNSTypeLegacy {
return nil
}
options := o.Options.(*LegacyDNSServerOptions)
serverURL, _ := url.Parse(options.Address)
var serverType string
if serverURL != nil && serverURL.Scheme != "" {
serverType = serverURL.Scheme
} else {
switch options.Address {
case "local", "fakeip":
serverType = options.Address
default:
serverType = C.DNSTypeUDP
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")
}
}
remoteOptions := RemoteDNSServerOptions{
RawLocalDNSServerOptions: RawLocalDNSServerOptions{
DialerOptions: DialerOptions{
Detour: options.Detour,
DomainResolver: &DomainResolveOptions{
Server: options.AddressResolver,
Strategy: options.AddressStrategy,
},
FallbackDelay: options.AddressFallbackDelay,
},
Legacy: true,
LegacyStrategy: options.Strategy,
LegacyDefaultDialer: options.Detour == "",
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
},
LegacyAddressResolver: options.AddressResolver,
LegacyAddressStrategy: options.AddressStrategy,
LegacyAddressFallbackDelay: options.AddressFallbackDelay,
}
switch serverType {
case C.DNSTypeLocal:
o.Type = C.DNSTypeLocal
o.Options = &LocalDNSServerOptions{
RawLocalDNSServerOptions: remoteOptions.RawLocalDNSServerOptions,
}
case C.DNSTypeUDP:
o.Type = C.DNSTypeUDP
o.Options = &remoteOptions
var serverAddr M.Socksaddr
if serverURL == nil || serverURL.Scheme == "" {
serverAddr = M.ParseSocksaddr(options.Address)
} else {
serverAddr = M.ParseSocksaddr(serverURL.Host)
}
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
remoteOptions.Server = serverAddr.AddrString()
if serverAddr.Port != 0 && serverAddr.Port != 53 {
remoteOptions.ServerPort = serverAddr.Port
}
case C.DNSTypeTCP:
o.Type = C.DNSTypeTCP
o.Options = &remoteOptions
if serverURL == nil {
return E.New("invalid server address")
}
serverAddr := M.ParseSocksaddr(serverURL.Host)
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
remoteOptions.Server = serverAddr.AddrString()
if serverAddr.Port != 0 && serverAddr.Port != 53 {
remoteOptions.ServerPort = serverAddr.Port
}
case C.DNSTypeTLS, C.DNSTypeQUIC:
o.Type = serverType
if serverURL == nil {
return E.New("invalid server address")
}
serverAddr := M.ParseSocksaddr(serverURL.Host)
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
remoteOptions.Server = serverAddr.AddrString()
if serverAddr.Port != 0 && serverAddr.Port != 853 {
remoteOptions.ServerPort = serverAddr.Port
}
o.Options = &RemoteTLSDNSServerOptions{
RemoteDNSServerOptions: remoteOptions,
}
case C.DNSTypeHTTPS, C.DNSTypeHTTP3:
o.Type = serverType
httpsOptions := RemoteHTTPSDNSServerOptions{
RemoteTLSDNSServerOptions: RemoteTLSDNSServerOptions{
RemoteDNSServerOptions: remoteOptions,
},
}
o.Options = &httpsOptions
if serverURL == nil {
return E.New("invalid server address")
}
serverAddr := M.ParseSocksaddr(serverURL.Host)
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
httpsOptions.Server = serverAddr.AddrString()
if serverAddr.Port != 0 && serverAddr.Port != 443 {
httpsOptions.ServerPort = serverAddr.Port
}
if serverURL.Path != "/dns-query" {
httpsOptions.Path = serverURL.Path
}
case "rcode":
var rcode int
if serverURL == nil {
return E.New("invalid server address")
}
switch serverURL.Host {
case "success":
rcode = dns.RcodeSuccess
case "format_error":
rcode = dns.RcodeFormatError
case "server_failure":
rcode = dns.RcodeServerFailure
case "name_error":
rcode = dns.RcodeNameError
case "not_implemented":
rcode = dns.RcodeNotImplemented
case "refused":
rcode = dns.RcodeRefused
default:
return E.New("unknown rcode: ", serverURL.Host)
}
o.Type = C.DNSTypeLegacyRcode
o.Options = rcode
case C.DNSTypeDHCP:
o.Type = C.DNSTypeDHCP
dhcpOptions := DHCPDNSServerOptions{}
if serverURL == nil {
return E.New("invalid server address")
}
if serverURL.Host != "" && serverURL.Host != "auto" {
dhcpOptions.Interface = serverURL.Host
}
o.Options = &dhcpOptions
case C.DNSTypeFakeIP:
o.Type = C.DNSTypeFakeIP
fakeipOptions := FakeIPDNSServerOptions{}
if legacyOptions, loaded := ctx.Value((*LegacyDNSFakeIPOptions)(nil)).(*LegacyDNSFakeIPOptions); loaded {
fakeipOptions.Inet4Range = legacyOptions.Inet4Range
fakeipOptions.Inet6Range = legacyOptions.Inet6Range
}
o.Options = &fakeipOptions
case C.DNSTypeSDNS:
o.Type = C.DNSTypeSDNS
if serverURL == nil {
return E.New("invalid server address")
}
serverAddr := M.ParseSocksaddr(serverURL.Host)
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
o.Options = &SDNSDNSServerOptions{
RemoteDNSServerOptions: remoteOptions,
Stamp: serverAddr.AddrString(),
}
default:
return E.New("unsupported DNS server scheme: ", serverType)
}
return nil
return registryUnion(builder, registry, nil, true)
})
}
type DNSServerAddressOptions struct {
@@ -363,16 +176,6 @@ func (o *DNSServerAddressOptions) ReplaceServerOptions(options ServerOptions) {
*o = DNSServerAddressOptions(options)
}
type LegacyDNSServerOptions struct {
Address string `json:"address"`
AddressResolver string `json:"address_resolver,omitempty"`
AddressStrategy DomainStrategy `json:"address_strategy,omitempty"`
AddressFallbackDelay badoption.Duration `json:"address_fallback_delay,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
Detour string `json:"detour,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type HostsDNSServerOptions struct {
Path badoption.Listable[string] `json:"path,omitempty"`
Predefined *badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"`
@@ -380,23 +183,17 @@ type HostsDNSServerOptions struct {
type RawLocalDNSServerOptions struct {
DialerOptions
Legacy bool `json:"-"`
LegacyStrategy DomainStrategy `json:"-"`
LegacyDefaultDialer bool `json:"-"`
LegacyClientSubnet netip.Prefix `json:"-"`
}
type LocalDNSServerOptions struct {
RawLocalDNSServerOptions
PreferGo bool `json:"prefer_go,omitempty"`
PreferGo bool `json:"prefer_go,omitempty"`
NeighborDomain badoption.Listable[string] `json:"neighbor_domain,omitempty"`
}
type RemoteDNSServerOptions struct {
RawLocalDNSServerOptions
DNSServerAddressOptions
LegacyAddressResolver string `json:"-"`
LegacyAddressStrategy DomainStrategy `json:"-"`
LegacyAddressFallbackDelay badoption.Duration `json:"-"`
}
type RemoteTLSDNSServerOptions struct {
@@ -412,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 {
@@ -431,3 +228,8 @@ type FallbackDNSServerOptions struct {
Strategy string `json:"strategy,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
}
type MDNSDNSServerOptions struct {
LocalDNSServerOptions
Interface badoption.Listable[string] `json:"interface,omitempty"`
}
+68 -1
View File
@@ -1,8 +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"
@@ -11,6 +15,8 @@ import (
"github.com/miekg/dns"
)
const defaultDNSRecordTTL uint32 = 3600
type DNSRCode int
func (r DNSRCode) MarshalJSON() ([]byte, error) {
@@ -48,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
@@ -76,10 +119,13 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error {
if err == nil {
return o.unmarshalBase64(binary)
}
record, err := dns.NewRR(stringValue)
record, err := parseDNSRecord(stringValue)
if err != nil {
return err
}
if record == nil {
return E.New("empty DNS record")
}
if a, isA := record.(*dns.A); isA {
a.A = M.AddrFromIP(a.A).Unmap().AsSlice()
}
@@ -87,6 +133,16 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error {
return nil
}
func parseDNSRecord(stringValue string) (dns.RR, error) {
if len(stringValue) > 0 && stringValue[len(stringValue)-1] != '\n' {
stringValue += "\n"
}
parser := dns.NewZoneParser(strings.NewReader(stringValue), "", "")
parser.SetDefaultTTL(defaultDNSRecordTTL)
record, _ := parser.Next()
return record, parser.Err()
}
func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error {
record, _, err := dns.UnpackRR(binary, 0)
if err != nil {
@@ -100,3 +156,14 @@ func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error {
func (o DNSRecordOptions) Build() dns.RR {
return o.RR
}
func (o DNSRecordOptions) Match(record dns.RR) bool {
if o.RR == nil || record == nil {
return false
}
return dns.IsDuplicate(o.RR, record)
}
func (o DNSRecordOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringNode(), nil
}
+40
View File
@@ -0,0 +1,40 @@
package option
import (
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
func mustRecordOptions(t *testing.T, record string) DNSRecordOptions {
t.Helper()
var value DNSRecordOptions
require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`)))
return value
}
func TestDNSRecordOptionsUnmarshalJSONRejectsRelativeNames(t *testing.T) {
t.Parallel()
for _, record := range []string{
"@ IN A 1.1.1.1",
"www IN CNAME example.com.",
"example.com. IN CNAME @",
"example.com. IN CNAME www",
} {
var value DNSRecordOptions
err := value.UnmarshalJSON([]byte(`"` + record + `"`))
require.Error(t, err)
}
}
func TestDNSRecordOptionsMatchIgnoresTTL(t *testing.T) {
t.Parallel()
expected := mustRecordOptions(t, "example.com. 600 IN A 1.1.1.1")
record, err := dns.NewRR("example.com. 60 IN A 1.1.1.1")
require.NoError(t, err)
require.True(t, expected.Match(record))
}
+58
View File
@@ -0,0 +1,58 @@
package option
import (
"context"
"testing"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/stretchr/testify/require"
)
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:
return new(RemoteDNSServerOptions), true
case C.DNSTypeFakeIP:
return new(FakeIPDNSServerOptions), true
default:
return nil, false
}
}
func TestDNSOptionsRejectsLegacyFakeIPOptions(t *testing.T) {
t.Parallel()
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
var options DNSOptions
err := json.UnmarshalContext(ctx, []byte(`{
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/15"
}
}`), &options)
require.EqualError(t, err, legacyDNSFakeIPRemovedMessage)
}
func TestDNSServerOptionsRejectsLegacyFormats(t *testing.T) {
t.Parallel()
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
testCases := []string{
`{"address":"1.1.1.1"}`,
`{"type":"legacy","address":"1.1.1.1"}`,
}
for _, content := range testCases {
var options DNSServerOptions
err := json.UnmarshalContext(ctx, []byte(content), &options)
require.EqualError(t, err, legacyDNSServerRemovedMessage)
}
}
+12
View File
@@ -3,6 +3,7 @@ package option
import (
"context"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -10,6 +11,7 @@ import (
)
type EndpointOptionsRegistry interface {
OptionTypes() []string
CreateOptions(endpointType string) (any, bool)
}
@@ -45,3 +47,13 @@ func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) err
h.Options = options
return nil
}
func (h Endpoint) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("Endpoint", func() (*schema.Node, error) {
registry := service.FromContext[EndpointOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing endpoint options registry in context")
}
return registryUnion(builder, registry, nil, true)
})
}
+8 -7
View File
@@ -15,17 +15,18 @@ 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"`
StoreWARPConfig bool `json:"store_warp_config,omitempty"`
StoreMASQUEConfig bool `json:"store_masque_config,omitempty"`
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
StoreDNS bool `json:"store_dns,omitempty"`
}
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:"-"`
@@ -33,15 +34,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 {
+2 -2
View File
@@ -4,7 +4,7 @@ import "github.com/sagernet/sing/common/json/badoption"
type SelectorOutboundOptions struct {
GroupCommonOption
Default string `json:"default,omitempty"`
Default string `json:"default,omitempty" reference:"outbound"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
}
@@ -23,7 +23,7 @@ type FallbackOutboundOptions struct {
}
type GroupCommonOption struct {
Outbounds []string `json:"outbounds"`
Outbounds []string `json:"outbounds" reference:"outbound"`
Providers badoption.Listable[string] `json:"providers,omitempty"`
Exclude *badoption.Regexp `json:"exclude,omitempty"`
Include *badoption.Regexp `json:"include,omitempty"`
+158
View File
@@ -0,0 +1,158 @@
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"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
type HTTP2Options struct {
IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"`
KeepAlivePeriod badoption.Duration `json:"keep_alive_period,omitempty"`
StreamReceiveWindow byteformats.MemoryBytes `json:"stream_receive_window,omitempty"`
ConnectionReceiveWindow byteformats.MemoryBytes `json:"connection_receive_window,omitempty"`
MaxConcurrentStreams int `json:"max_concurrent_streams,omitempty"`
}
type QUICOptions struct {
HTTP2Options
InitialPacketSize int `json:"initial_packet_size,omitempty"`
DisablePathMTUDiscovery bool `json:"disable_path_mtu_discovery,omitempty"`
}
type _HTTPClientOptions struct {
Tag string `json:"tag,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:"-"`
HTTP3Options QUICOptions `json:"-"`
DefaultOutbound bool `json:"-"`
DisableEmptyDirectCheck bool `json:"-"`
ResolveOnDetour bool `json:"-"`
DirectResolver bool `json:"-"`
OutboundTLSOptionsContainer
DialerOptions
}
type (
HTTPClient _HTTPClientOptions
HTTPClientOptions _HTTPClientOptions
)
func (h HTTPClient) Options() HTTPClientOptions {
options := HTTPClientOptions(h)
options.Tag = ""
return options
}
func (o HTTPClientOptions) IsEmpty() bool {
if o.Tag != "" {
return false
}
o.DefaultOutbound = false
o.ResolveOnDetour = false
o.DirectResolver = false
return reflect.ValueOf(_HTTPClientOptions(o)).IsZero()
}
func (o HTTPClientOptions) MarshalJSON() ([]byte, error) {
if o.Tag != "" {
return json.Marshal(o.Tag)
}
return badjson.MarshallObjects(_HTTPClientOptions(o), httpClientVariant(_HTTPClientOptions(o)))
}
func (o *HTTPClientOptions) UnmarshalJSON(content []byte) error {
if len(content) > 0 && content[0] == '"' {
*o = HTTPClientOptions{}
return json.Unmarshal(content, &o.Tag)
}
var options _HTTPClientOptions
err := json.Unmarshal(content, &options)
if err != nil {
return err
}
err = unmarshalHTTPClientVersionOptions(content, &options, &options)
if err != nil {
return err
}
options.Tag = ""
*o = HTTPClientOptions(options)
return nil
}
func (h HTTPClient) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects(_HTTPClientOptions(h), httpClientVariant(_HTTPClientOptions(h)))
}
func (h *HTTPClient) UnmarshalJSON(content []byte) error {
err := json.Unmarshal(content, (*_HTTPClientOptions)(h))
if err != nil {
return err
}
return unmarshalHTTPClientVersionOptions(content, (*_HTTPClientOptions)(h), (*_HTTPClientOptions)(h))
}
func unmarshalHTTPClientVersionOptions(content []byte, baseStruct any, options *_HTTPClientOptions) error {
switch options.Version {
case 1:
return json.UnmarshalDisallowUnknownFields(content, baseStruct)
case 0, 2:
options.Version = 2
return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP2Options)
case 3:
return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP3Options)
default:
return E.New("unknown HTTP version: ", options.Version)
}
}
func httpClientVariant(options _HTTPClientOptions) any {
switch options.Version {
case 1:
return nil
case 0, 2:
return options.HTTP2Options
case 3:
return options.HTTP3Options
default:
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
})
}
+32 -23
View File
@@ -7,17 +7,22 @@ import (
type HysteriaInboundOptions struct {
ListenOptions
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs string `json:"obfs,omitempty"`
Users []HysteriaUser `json:"users,omitempty"`
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"`
MaxConnClient int `json:"max_conn_client,omitempty"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs string `json:"obfs,omitempty"`
Users []HysteriaUser `json:"users,omitempty"`
// Deprecated: use QUIC fields instead
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
MaxConnClient int `json:"max_conn_client,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty" schema:"omit"`
InboundTLSOptionsContainer
QUICOptions
}
type HysteriaUser struct {
@@ -29,18 +34,22 @@ type HysteriaUser struct {
type HysteriaOutboundOptions struct {
DialerOptions
ServerOptions
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs string `json:"obfs,omitempty"`
Auth []byte `json:"auth,omitempty"`
AuthString string `json:"auth_str,omitempty"`
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
ReceiveWindow uint64 `json:"recv_window,omitempty"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
Network NetworkList `json:"network,omitempty"`
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs string `json:"obfs,omitempty"`
Auth []byte `json:"auth,omitempty"`
AuthString string `json:"auth_str,omitempty"`
// Deprecated: use QUIC fields instead
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
ReceiveWindow uint64 `json:"recv_window,omitempty" schema:"omit"`
// Deprecated: use QUIC fields instead
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty" schema:"omit"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
QUICOptions
}
+122 -14
View File
@@ -2,8 +2,10 @@ package option
import (
"net/url"
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -18,13 +20,89 @@ type Hysteria2InboundOptions struct {
Users []Hysteria2User `json:"users,omitempty"`
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
InboundTLSOptionsContainer
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
QUICOptions
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
BBRProfile string `json:"bbr_profile,omitempty" enum:"standard,conservative,aggressive"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
Realm *Hysteria2InboundRealm `json:"realm,omitempty"`
}
type Hysteria2Obfs struct {
Type string `json:"type,omitempty"`
Password string `json:"password,omitempty"`
type Hysteria2Realm struct {
ServerURL string `json:"server_url"`
Token string `json:"token,omitempty"`
RealmID string `json:"realm_id"`
STUNServers badoption.Listable[string] `json:"stun_servers"`
IPVersion int `json:"ip_version,omitempty" enum:"0,4,6"`
PortMapping *Hysteria2RealmPortMapping `json:"port_mapping,omitempty"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
}
type Hysteria2RealmPortMapping struct {
Enabled bool `json:"enabled,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
Lifetime badoption.Duration `json:"lifetime,omitempty"`
}
type Hysteria2InboundRealm struct {
Hysteria2Realm
STUNDomainResolver *DomainResolveOptions `json:"stun_domain_resolver,omitempty"`
}
type Hysteria2ObfsGecko struct {
MinPacketSize int `json:"min_packet_size,omitempty"`
MaxPacketSize int `json:"max_packet_size,omitempty"`
}
type _Hysteria2Obfs struct {
Type string `json:"type,omitempty" enum:"salamander,gecko"`
Password string `json:"password,omitempty"`
GeckoOptions Hysteria2ObfsGecko `json:"-"`
}
type Hysteria2Obfs _Hysteria2Obfs
func (o Hysteria2Obfs) MarshalJSON() ([]byte, error) {
var v any
switch o.Type {
case C.Hysteria2ObfsTypeSalamander:
case C.Hysteria2ObfsTypeGecko:
v = o.GeckoOptions
default:
return nil, E.New("unknown obfs type: ", o.Type)
}
if v == nil {
return json.Marshal(_Hysteria2Obfs(o))
}
return badjson.MarshallObjects(_Hysteria2Obfs(o), v)
}
func (o *Hysteria2Obfs) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_Hysteria2Obfs)(o))
if err != nil {
return err
}
var v any
switch o.Type {
case C.Hysteria2ObfsTypeSalamander:
case C.Hysteria2ObfsTypeGecko:
v = &o.GeckoOptions
default:
return E.New("unknown obfs type: ", o.Type)
}
if v == nil {
return nil
}
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 {
@@ -33,7 +111,7 @@ type Hysteria2User struct {
}
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:"-"`
@@ -94,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"`
}
@@ -112,13 +202,31 @@ type Hysteria2MasqueradeString struct {
type Hysteria2OutboundOptions struct {
DialerOptions
ServerOptions
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
HopIntervalMax badoption.Duration `json:"hop_interval_max,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
BrutalDebug bool `json:"brutal_debug,omitempty"`
QUICOptions
BBRProfile string `json:"bbr_profile,omitempty" enum:"standard,conservative,aggressive"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
DisableChromeParrot bool `json:"disable_chrome_parrot,omitempty"`
Realm *Hysteria2Realm `json:"realm,omitempty"`
}
type HysteriaRealmUser struct {
Name string `json:"name"`
Token string `json:"token"`
MaxRealms int `json:"max_realms,omitempty"`
}
type HysteriaRealmServiceOptions struct {
ListenOptions
InboundTLSOptionsContainer
HTTP2Options
Users []HysteriaRealmUser `json:"users"`
}
+77 -10
View File
@@ -4,6 +4,8 @@ import (
"context"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -12,6 +14,7 @@ import (
)
type InboundOptionsRegistry interface {
OptionTypes() []string
CreateOptions(outboundType string) (any, bool)
}
@@ -54,13 +57,23 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
return nil
}
func (h Inbound) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("Inbound", func() (*schema.Node, error) {
registry := service.FromContext[InboundOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing inbound options registry in context")
}
return registryUnion(builder, registry, []string{C.TypeShadowsocksR}, true)
})
}
// Deprecated: Use rule action instead
type InboundOptions struct {
SniffEnabled bool `json:"sniff,omitempty"`
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"`
SniffTimeout badoption.Duration `json:"sniff_timeout,omitempty"`
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"`
SniffEnabled bool `json:"sniff,omitempty" schema:"omit"`
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty" schema:"omit"`
SniffTimeout badoption.Duration `json:"sniff_timeout,omitempty" schema:"omit"`
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty" schema:"omit"`
UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty" schema:"omit"`
}
type ListenOptions struct {
@@ -69,7 +82,7 @@ type ListenOptions struct {
BindInterface string `json:"bind_interface,omitempty"`
RoutingMark FwMark `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,omitempty"`
NetNs string `json:"netns,omitempty"`
NetNs string `json:"netns,omitempty" reference:"network_namespace"`
DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"`
TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"`
TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"`
@@ -78,13 +91,61 @@ 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
const (
UDPNATBehaviorEndpointIndependent UDPNATBehavior = iota
UDPNATBehaviorAddressDependent
UDPNATBehaviorAddressAndPortDependent
)
func (b UDPNATBehavior) MarshalJSON() ([]byte, error) {
var value string
switch b {
case UDPNATBehaviorEndpointIndependent:
value = "endpoint_independent"
case UDPNATBehaviorAddressDependent:
value = "address_dependent"
case UDPNATBehaviorAddressAndPortDependent:
value = "address_and_port_dependent"
default:
return nil, E.New("unknown UDP NAT behavior: ", uint8(b))
}
return json.Marshal(value)
}
func (b *UDPNATBehavior) UnmarshalJSON(data []byte) error {
var value string
err := json.Unmarshal(data, &value)
if err != nil {
return err
}
switch value {
case "", "endpoint_independent":
*b = UDPNATBehaviorEndpointIndependent
case "address_dependent":
*b = UDPNATBehaviorAddressDependent
case "address_and_port_dependent":
*b = UDPNATBehaviorAddressAndPortDependent
default:
return E.New("unknown UDP NAT behavior: ", value)
}
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
@@ -103,6 +164,12 @@ func (c *UDPTimeoutCompat) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, (*badoption.Duration)(c))
}
func (c UDPTimeoutCompat) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("UDPTimeout", func() (*schema.Node, error) {
return schema.AnyOf(schema.UnsignedNode(32), schema.DurationNode()), nil
})
}
type ListenOptionsWrapper interface {
TakeListenOptions() ListenOptions
ReplaceListenOptions(options ListenOptions)
+1 -1
View File
@@ -8,7 +8,7 @@ type InboundMultiplexOptions struct {
type OutboundMultiplexOptions struct {
Enabled bool `json:"enabled,omitempty"`
Protocol string `json:"protocol,omitempty"`
Protocol string `json:"protocol,omitempty" enum:"h2mux,smux,yamux"`
MaxConnections int `json:"max_connections,omitempty"`
MinStreams int `json:"min_streams,omitempty"`
MaxStreams int `json:"max_streams,omitempty"`
+2 -12
View File
@@ -6,21 +6,11 @@ import (
"github.com/sagernet/sing/common/json/badoption"
)
type QuicheCongestionControl string
const (
QuicheCongestionControlDefault QuicheCongestionControl = ""
QuicheCongestionControlBBR QuicheCongestionControl = "TBBR"
QuicheCongestionControlBBRv2 QuicheCongestionControl = "B2ON"
QuicheCongestionControlCubic QuicheCongestionControl = "QBIC"
QuicheCongestionControlReno QuicheCongestionControl = "RENO"
)
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,cubic,reno"`
InboundTLSOptionsContainer
}
@@ -34,7 +24,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
}
+73
View File
@@ -0,0 +1,73 @@
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"
)
type _NetworkNamespace struct {
Type string `json:"type,omitempty"`
Tag string `json:"tag"`
DefaultOptions DefaultNetworkNamespaceOptions `json:"-"`
UnshareOptions UnshareNetworkNamespaceOptions `json:"-"`
}
type NetworkNamespace _NetworkNamespace
func (o NetworkNamespace) MarshalJSON() ([]byte, error) {
var v any
switch o.Type {
case C.NetNsTypeDefault:
o.Type = ""
v = o.DefaultOptions
case C.NetNsTypeUnshare:
v = o.UnshareOptions
default:
return nil, E.New("unknown network namespace type: ", o.Type)
}
return badjson.MarshallObjects(_NetworkNamespace(o), v)
}
func (o *NetworkNamespace) UnmarshalJSON(content []byte) error {
err := json.Unmarshal(content, (*_NetworkNamespace)(o))
if err != nil {
return err
}
var v any
switch o.Type {
case "", C.NetNsTypeDefault:
o.Type = C.NetNsTypeDefault
v = &o.DefaultOptions
case C.NetNsTypeUnshare:
v = &o.UnshareOptions
default:
return E.New("unknown network namespace type: ", o.Type)
}
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"`
}
type UnshareNetworkNamespaceOptions struct {
PidFile string `json:"pid_file,omitempty"`
}
+1 -1
View File
@@ -10,7 +10,7 @@ type OCMServiceOptions struct {
CredentialPath string `json:"credential_path,omitempty"`
Users []OCMUser `json:"users,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
Detour string `json:"detour,omitempty"`
Detour string `json:"detour,omitempty" reference:"outbound"`
UsagesPath string `json:"usages_path,omitempty"`
}
+6 -5
View File
@@ -6,9 +6,10 @@ import (
)
type OOMKillerServiceOptions struct {
MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"`
SafetyMargin *byteformats.MemoryBytes `json:"safety_margin,omitempty"`
MinInterval badoption.Duration `json:"min_interval,omitempty"`
MaxInterval badoption.Duration `json:"max_interval,omitempty"`
ChecksBeforeLimit int `json:"checks_before_limit,omitempty"`
MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"`
SafetyMargin *byteformats.MemoryBytes `json:"safety_margin,omitempty"`
MinInterval badoption.Duration `json:"min_interval,omitempty"`
MaxInterval badoption.Duration `json:"max_interval,omitempty"`
KillerDisabled bool `json:"-"`
MemoryLimitOverride uint64 `json:"-"`
}
+124
View File
@@ -0,0 +1,124 @@
package option
import "github.com/sagernet/sing/common/json/badoption"
type OpenConnectEndpointOptions struct {
DialerOptions
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
Server string `json:"server"`
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"`
Cookie string `json:"cookie,omitempty"`
Token *OpenConnectTokenOptions `json:"token,omitempty"`
ReportedOS string `json:"reported_os,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Version string `json:"version,omitempty"`
LocalHostname string `json:"local_hostname,omitempty"`
Mobile *OpenConnectMobileOptions `json:"mobile,omitempty"`
CSD *OpenConnectCSDOptions `json:"csd,omitempty"`
HIP *OpenConnectHIPOptions `json:"hip,omitempty"`
TNCC *OpenConnectTNCCOptions `json:"tncc,omitempty"`
FortinetHostCheck *OpenConnectFortinetHostCheckOptions `json:"fortinet_host_check,omitempty"`
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" enum:"stateless,all"`
IPv6Disabled bool `json:"ipv6_disabled,omitempty"`
HTTPKeepAliveDisabled bool `json:"http_keepalive_disabled,omitempty"`
XMLPostDisabled bool `json:"xml_post_disabled,omitempty"`
ExternalAuthDisabled bool `json:"external_auth_disabled,omitempty"`
PasswordAuthenticationDisabled bool `json:"password_authentication_disabled,omitempty"`
TCPKeepAliveEnabled bool `json:"tcp_keep_alive_enabled,omitempty"`
PFS bool `json:"pfs,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
BaseMTU uint32 `json:"base_mtu,omitempty"`
DPDInterval badoption.Duration `json:"dpd_interval,omitempty"`
ReconnectTimeout badoption.Duration `json:"reconnect_timeout,omitempty"`
TrojanInterval badoption.Duration `json:"trojan_interval,omitempty"`
QueueLength uint32 `json:"queue_length,omitempty"`
AllowInsecureCrypto bool `json:"allow_insecure_crypto,omitempty"`
TLS OpenConnectTLSOptions `json:"tls,omitempty"`
FormEntries []OpenConnectFormEntryOptions `json:"form_entries,omitempty"`
}
type OpenConnectTokenOptions struct {
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"`
Password string `json:"password,omitempty"`
DeviceID string `json:"device_id,omitempty"`
Counter uint64 `json:"counter,omitempty"`
}
type OpenConnectMobileOptions struct {
PlatformVersion string `json:"platform_version"`
DeviceType string `json:"device_type"`
DeviceUniqueID string `json:"device_unique_id"`
}
type OpenConnectCSDOptions struct {
WrapperPath string `json:"wrapper_path,omitempty"`
}
type OpenConnectHIPOptions struct {
WrapperPath string `json:"wrapper_path,omitempty"`
}
type OpenConnectTNCCOptions struct {
WrapperPath string `json:"wrapper_path,omitempty"`
DeviceID string `json:"device_id,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
MachineIdentificationEnabled bool `json:"machine_identification_enabled,omitempty"`
Certificates []OpenConnectTNCCCertificateOptions `json:"certificates,omitempty"`
}
type OpenConnectFortinetHostCheckOptions struct {
HostCheck string `json:"hostcheck,omitempty"`
CheckVirtualDesktop string `json:"check_virtual_desktop,omitempty"`
}
type OpenConnectTNCCCertificateOptions struct {
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
}
type OpenConnectTLSOptions struct {
Insecure bool `json:"insecure,omitempty"`
ServerName string `json:"server_name,omitempty"`
PeerFingerprint badoption.Listable[string] `json:"peer_fingerprint,omitempty"`
SystemTrustDisabled bool `json:"system_trust_disabled,omitempty"`
CertificateAuthority badoption.Listable[string] `json:"certificate_authority,omitempty"`
CertificateAuthorityPath string `json:"certificate_authority_path,omitempty"`
ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"`
ClientCertificatePath string `json:"client_certificate_path,omitempty"`
ClientKey badoption.Listable[string] `json:"client_key,omitempty"`
ClientKeyPath string `json:"client_key_path,omitempty"`
ClientKeyPassword string `json:"client_key_password,omitempty"`
MCACertificate badoption.Listable[string] `json:"mca_certificate,omitempty"`
MCACertificatePath string `json:"mca_certificate_path,omitempty"`
MCAKey badoption.Listable[string] `json:"mca_key,omitempty"`
MCAKeyPath string `json:"mca_key_path,omitempty"`
MCAKeyPassword string `json:"mca_key_password,omitempty"`
}
type OpenConnectFormEntryOptions struct {
FormID string `json:"form_id,omitempty"`
SubmissionKey string `json:"submission_key,omitempty"`
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Promote bool `json:"promote,omitempty"`
}
type OpenConnectDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
AcceptSearchDomain bool `json:"accept_search_domain,omitempty"`
}
+210
View File
@@ -0,0 +1,210 @@
package option
import (
"net/netip"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/json/badoption"
)
type OpenVPNEndpointOptions struct {
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
}
type OpenVPNClientEndpointOptions struct {
DialerOptions
ServerOptions
OpenVPNEndpointOptions
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" enum:"net30,p2p,subnet"`
Username string `json:"username,omitempty"`
Password string `json:"password,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" enum:"server,client"`
TLS *OpenVPNOutboundTLSOptions `json:"tls,omitempty"`
Cipher string `json:"cipher,omitempty"`
DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"`
DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"`
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" 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" 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"`
RouteGateway *badoption.Addr `json:"route_gateway,omitempty"`
RouteMetric int `json:"route_metric,omitempty"`
RedirectGateway bool `json:"redirect_gateway,omitempty"`
RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"`
RedirectPrivate bool `json:"redirect_private,omitempty"`
BlockIPv6 bool `json:"block_ipv6,omitempty"`
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
PingRestart badoption.Duration `json:"ping_restart,omitempty"`
PingRestartDisabled bool `json:"ping_restart_disabled,omitempty"`
RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"`
RenegotiateDisabled bool `json:"renegotiate_disabled,omitempty"`
RenegotiateBytes uint64 `json:"renegotiate_bytes,omitempty"`
RenegotiatePackets uint64 `json:"renegotiate_packets,omitempty"`
TLSTimeout badoption.Duration `json:"tls_timeout,omitempty"`
HandshakeWindow badoption.Duration `json:"handshake_window,omitempty"`
ExplicitExitNotify uint32 `json:"explicit_exit_notify,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
}
type OpenVPNServerEndpointOptions struct {
ListenOptions
OpenVPNEndpointOptions
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" 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" enum:"server,client"`
TLS *OpenVPNInboundTLSOptions `json:"tls,omitempty"`
Cipher string `json:"cipher,omitempty"`
DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"`
DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"`
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" enum:"mtu,fixed"`
ReplayWindow uint32 `json:"replay_window,omitempty"`
ReplayWindowTime badoption.Duration `json:"replay_window_time,omitempty"`
Push *OpenVPNPushOptions `json:"push,omitempty"`
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
PingRestart badoption.Duration `json:"ping_restart,omitempty"`
RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"`
RenegotiateDisabled bool `json:"renegotiate_disabled,omitempty"`
RenegotiateBytes uint64 `json:"renegotiate_bytes,omitempty"`
RenegotiatePackets uint64 `json:"renegotiate_packets,omitempty"`
HandshakeWindow badoption.Duration `json:"handshake_window,omitempty"`
}
type OpenVPNRemoteOptions struct {
ServerOptions
Network string `json:"network,omitempty" enum:"udp,udp4,udp6,tcp,tcp4,tcp6"`
}
type OpenVPNPullFilterOptions struct {
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" 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"`
ClientCertificatePath string `json:"client_certificate_path,omitempty"`
ClientKey badoption.Listable[string] `json:"client_key,omitempty"`
ClientKeyPath string `json:"client_key_path,omitempty"`
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" 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"`
}
type OpenVPNInboundTLSOptions struct {
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
Key badoption.Listable[string] `json:"key,omitempty"`
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" enum:"require,optional,none"`
ClientName string `json:"client_name,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" 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" 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" enum:"server,client"`
}
type OpenVPNInboundControlWrapOptions struct {
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" enum:"server,client"`
ForceCookie bool `json:"force_cookie,omitempty"`
}
type OpenVPNPushOptions struct {
Routes badoption.Listable[netip.Prefix] `json:"routes,omitempty"`
DNS badoption.Listable[netip.Addr] `json:"dns,omitempty"`
DNSServers []OpenVPNPushDNSServerOptions `json:"dns_servers,omitempty"`
SearchDomains badoption.Listable[string] `json:"search_domains,omitempty"`
DHCPOptions badoption.Listable[string] `json:"dhcp_options,omitempty"`
RedirectGateway bool `json:"redirect_gateway,omitempty"`
RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"`
BlockOutsideDNS bool `json:"block_outside_dns,omitempty"`
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
PingRestart badoption.Duration `json:"ping_restart,omitempty"`
}
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" enum:"yes,optional,no"`
Transport string `json:"transport,omitempty" enum:"plain,dot,doh"`
SNI string `json:"sni,omitempty"`
}
type OpenVPNDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
AcceptSearchDomain bool `json:"accept_search_domain,omitempty"`
}
+80 -14
View File
@@ -3,30 +3,40 @@ 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"
)
type _Options struct {
RawMessage json.RawMessage `json:"-"`
Schema string `json:"$schema,omitempty"`
Log *LogOptions `json:"log,omitempty"`
DNS *DNSOptions `json:"dns,omitempty"`
NTP *NTPOptions `json:"ntp,omitempty"`
Certificate *CertificateOptions `json:"certificate,omitempty"`
Endpoints []Endpoint `json:"endpoints,omitempty"`
Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"`
Providers []Provider `json:"providers,omitempty"`
Route *RouteOptions `json:"route,omitempty"`
Services []Service `json:"services,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
RawMessage json.RawMessage `json:"-"`
CommentsSet *json.CommentSet `json:"-"`
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"`
Certificate *CertificateOptions `json:"certificate,omitempty"`
CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"`
HTTPClients []HTTPClient `json:"http_clients,omitempty"`
NetworkNamespaces []NetworkNamespace `json:"network_namespaces,omitempty"`
Endpoints []Endpoint `json:"endpoints,omitempty"`
Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"`
Providers []Provider `json:"providers,omitempty"`
Route *RouteOptions `json:"route,omitempty"`
Services []Service `json:"services,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
}
type Options _Options
func (o Options) MarshalJSONContext(ctx context.Context) ([]byte, error) {
return json.MarshalContext(ctx, _Options(o))
}
func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) error {
decoder := json.NewDecoderContext(ctx, bytes.NewReader(content))
decoder.DisallowUnknownFields()
@@ -38,9 +48,28 @@ 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
}
func (o *Options) SetComments(comments *json.CommentSet) {
o.CommentsSet = comments
}
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:"-"`
@@ -57,6 +86,43 @@ func checkOptions(options *Options) error {
if err != nil {
return err
}
err = checkCertificateProviders(options.CertificateProviders)
if err != nil {
return err
}
err = checkHTTPClients(options.HTTPClients)
if err != nil {
return err
}
return nil
}
func checkCertificateProviders(providers []CertificateProvider) error {
seen := make(map[string]bool)
for i, provider := range providers {
tag := provider.Tag
if tag == "" {
tag = F.ToString(i)
}
if seen[tag] {
return E.New("duplicate certificate provider tag: ", tag)
}
seen[tag] = true
}
return nil
}
func checkHTTPClients(clients []HTTPClient) error {
seen := make(map[string]bool)
for _, client := range clients {
if client.Tag == "" {
return E.New("missing http client tag")
}
if seen[client.Tag] {
return E.New("duplicate http client tag: ", client.Tag)
}
seen[client.Tag] = true
}
return nil
}
+85
View File
@@ -0,0 +1,85 @@
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"
)
type CloudflareOriginCACertificateProviderOptions struct {
Domain badoption.Listable[string] `json:"domain,omitempty"`
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" 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"`
}
type CloudflareOriginCARequestType string
const (
CloudflareOriginCARequestTypeOriginRSA = CloudflareOriginCARequestType("origin-rsa")
CloudflareOriginCARequestTypeOriginECC = CloudflareOriginCARequestType("origin-ecc")
)
func (t *CloudflareOriginCARequestType) UnmarshalJSON(data []byte) error {
var value string
err := json.Unmarshal(data, &value)
if err != nil {
return err
}
value = strings.ToLower(value)
switch CloudflareOriginCARequestType(value) {
case "", CloudflareOriginCARequestTypeOriginRSA, CloudflareOriginCARequestTypeOriginECC:
*t = CloudflareOriginCARequestType(value)
default:
return E.New("unsupported Cloudflare Origin CA request type: ", value)
}
return nil
}
func (t CloudflareOriginCARequestType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("", "origin-rsa", "origin-ecc"), nil
}
type CloudflareOriginCARequestValidity uint16
const (
CloudflareOriginCARequestValidity7 = CloudflareOriginCARequestValidity(7)
CloudflareOriginCARequestValidity30 = CloudflareOriginCARequestValidity(30)
CloudflareOriginCARequestValidity90 = CloudflareOriginCARequestValidity(90)
CloudflareOriginCARequestValidity365 = CloudflareOriginCARequestValidity(365)
CloudflareOriginCARequestValidity730 = CloudflareOriginCARequestValidity(730)
CloudflareOriginCARequestValidity1095 = CloudflareOriginCARequestValidity(1095)
CloudflareOriginCARequestValidity5475 = CloudflareOriginCARequestValidity(5475)
)
func (v *CloudflareOriginCARequestValidity) UnmarshalJSON(data []byte) error {
var value uint16
err := json.Unmarshal(data, &value)
if err != nil {
return err
}
switch CloudflareOriginCARequestValidity(value) {
case 0,
CloudflareOriginCARequestValidity7,
CloudflareOriginCARequestValidity30,
CloudflareOriginCARequestValidity90,
CloudflareOriginCARequestValidity365,
CloudflareOriginCARequestValidity730,
CloudflareOriginCARequestValidity1095,
CloudflareOriginCARequestValidity5475:
*v = CloudflareOriginCARequestValidity(value)
default:
return E.New("unsupported Cloudflare Origin CA requested validity: ", value)
}
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
}
+63 -28
View File
@@ -2,8 +2,10 @@ package option
import (
"context"
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -13,6 +15,7 @@ import (
)
type OutboundOptionsRegistry interface {
OptionTypes() []string
CreateOptions(outboundType string) (any, bool)
}
@@ -59,45 +62,63 @@ 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"`
BindInterface string `json:"bind_interface,omitempty"`
Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"`
Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"`
BindAddressNoPort bool `json:"bind_address_no_port,omitempty"`
ProtectPath string `json:"protect_path,omitempty"`
RoutingMark FwMark `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,omitempty"`
NetNs string `json:"netns,omitempty"`
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
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"`
UDPFragment *bool `json:"udp_fragment,omitempty"`
UDPFragmentDefault bool `json:"-"`
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"`
FallbackDelay badoption.Duration `json:"fallback_delay,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"`
BindAddressNoPort bool `json:"bind_address_no_port,omitempty"`
ProtectPath string `json:"protect_path,omitempty"`
RoutingMark FwMark `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,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"`
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"`
TCPKeepAliveSystemDefaults bool `json:"-"`
UDPBindPort uint16 `json:"-"`
UDPFragment *bool `json:"udp_fragment,omitempty"`
UDPFragmentDefault bool `json:"-"`
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"`
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"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
Server string `json:"server" reference:"dns_server"`
Timeout badoption.Duration `json:"timeout,omitempty"`
Strategy DomainStrategy `json:"strategy,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 DomainResolveOptions _DomainResolveOptions
@@ -106,7 +127,9 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) {
if o.Server == "" {
return []byte("{}"), nil
} else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) &&
o.Timeout == 0 &&
!o.DisableCache &&
!o.DisableOptimisticCache &&
o.RewriteTTL == nil &&
o.ClientSubnet == nil {
return json.Marshal(o.Server)
@@ -132,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
}
-105
View File
@@ -1,105 +0,0 @@
package option
import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
)
type OnDemandOptions struct {
Enabled bool `json:"enabled,omitempty"`
Rules []OnDemandRule `json:"rules,omitempty"`
}
type OnDemandRule struct {
Action *OnDemandRuleAction `json:"action,omitempty"`
DNSSearchDomainMatch badoption.Listable[string] `json:"dns_search_domain_match,omitempty"`
DNSServerAddressMatch badoption.Listable[string] `json:"dns_server_address_match,omitempty"`
InterfaceTypeMatch *OnDemandRuleInterfaceType `json:"interface_type_match,omitempty"`
SSIDMatch badoption.Listable[string] `json:"ssid_match,omitempty"`
ProbeURL string `json:"probe_url,omitempty"`
}
type OnDemandRuleAction int
func (r *OnDemandRuleAction) MarshalJSON() ([]byte, error) {
if r == nil {
return nil, nil
}
value := *r
var actionName string
switch value {
case 1:
actionName = "connect"
case 2:
actionName = "disconnect"
case 3:
actionName = "evaluate_connection"
default:
return nil, E.New("unknown action: ", value)
}
return json.Marshal(actionName)
}
func (r *OnDemandRuleAction) UnmarshalJSON(bytes []byte) error {
var actionName string
if err := json.Unmarshal(bytes, &actionName); err != nil {
return err
}
var actionValue int
switch actionName {
case "connect":
actionValue = 1
case "disconnect":
actionValue = 2
case "evaluate_connection":
actionValue = 3
case "ignore":
actionValue = 4
default:
return E.New("unknown action name: ", actionName)
}
*r = OnDemandRuleAction(actionValue)
return nil
}
type OnDemandRuleInterfaceType int
func (r *OnDemandRuleInterfaceType) MarshalJSON() ([]byte, error) {
if r == nil {
return nil, nil
}
value := *r
var interfaceTypeName string
switch value {
case 1:
interfaceTypeName = "any"
case 2:
interfaceTypeName = "wifi"
case 3:
interfaceTypeName = "cellular"
default:
return nil, E.New("unknown interface type: ", value)
}
return json.Marshal(interfaceTypeName)
}
func (r *OnDemandRuleInterfaceType) UnmarshalJSON(bytes []byte) error {
var interfaceTypeName string
if err := json.Unmarshal(bytes, &interfaceTypeName); err != nil {
return err
}
var interfaceTypeValue int
switch interfaceTypeName {
case "any":
interfaceTypeValue = 1
case "wifi":
interfaceTypeValue = 2
case "cellular":
interfaceTypeValue = 3
default:
return E.New("unknown interface type name: ", interfaceTypeName)
}
*r = OnDemandRuleInterfaceType(interfaceTypeValue)
return nil
}
+4 -1
View File
@@ -6,5 +6,8 @@ type RedirectInboundOptions struct {
type TProxyInboundOptions struct {
ListenOptions
Network NetworkList `json:"network,omitempty"`
Network NetworkList `json:"network,omitempty"`
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
}
+11
View File
@@ -3,7 +3,9 @@ package option
import (
"context"
"net/netip"
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
@@ -39,6 +41,15 @@ func (r *ResolvedServiceOptions) UnmarshalJSONContext(ctx context.Context, bytes
return nil
}
func (r ResolvedServiceOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
node := schema.StrictObject()
err := builder.FlattenStruct(node, reflect.TypeFor[ResolvedServiceOptions]())
if err != nil {
return nil, err
}
return node, nil
}
type ResolvedDNSServerOptions struct {
Service string `json:"service"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
+8 -5
View File
@@ -3,12 +3,14 @@ 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"`
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
DefaultInterface string `json:"default_interface,omitempty"`
@@ -18,16 +20,17 @@ type RouteOptions struct {
DefaultNetworkType badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"`
DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"`
DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"`
DefaultHTTPClient string `json:"default_http_client,omitempty"`
}
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"`
}
+153 -21
View File
@@ -1,9 +1,11 @@
package option
import (
"context"
"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"
@@ -12,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:"-"`
}
@@ -33,26 +35,24 @@ func (r Rule) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects(_Rule(r), v)
}
func (r *Rule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_Rule)(r))
func (r *Rule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
err := json.UnmarshalContext(ctx, bytes, (*_Rule)(r))
if err != nil {
return err
}
payload, err := rulePayloadWithoutType(ctx, bytes)
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
return unmarshalDefaultRuleContext(ctx, payload, &r.DefaultOptions)
case C.RuleTypeLogical:
v = &r.LogicalOptions
return unmarshalLogicalRuleContext(ctx, payload, &r.LogicalOptions)
default:
return E.New("unknown rule type: " + r.Type)
}
err = badjson.UnmarshallExcluded(bytes, (*_Rule)(r), v)
if err != nil {
return err
}
return nil
}
func (r Rule) IsValid() bool {
@@ -66,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"`
@@ -92,6 +163,7 @@ type RawDefaultRule struct {
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"`
User badoption.Listable[string] `json:"user,omitempty"`
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
@@ -103,13 +175,15 @@ type RawDefaultRule struct {
InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"`
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
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 {
@@ -136,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"`
}
@@ -158,6 +232,64 @@ func (r *LogicalRule) UnmarshalJSON(data []byte) error {
return badjson.UnmarshallExcluded(data, &r.RawLogicalRule, &r.RuleAction)
}
func rulePayloadWithoutType(ctx context.Context, data []byte) ([]byte, error) {
var content badjson.JSONObject
err := content.UnmarshalJSONContext(ctx, data)
if err != nil {
return nil, err
}
content.Remove("type")
return content.MarshalJSONContext(ctx)
}
func unmarshalDefaultRuleContext(ctx context.Context, data []byte, rule *DefaultRule) error {
rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data)
if err != nil {
return err
}
err = rejectNestedRouteRuleAction(ctx, data)
if err != nil {
return err
}
depth := nestedRuleDepth(ctx)
err = json.UnmarshalContext(ctx, data, &rule.RawDefaultRule)
if err != nil {
return err
}
err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawDefaultRule, &rule.RuleAction)
if err != nil {
return err
}
if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) {
rule.RuleAction = RuleAction{}
}
return nil
}
func unmarshalLogicalRuleContext(ctx context.Context, data []byte, rule *LogicalRule) error {
rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data)
if err != nil {
return err
}
err = rejectNestedRouteRuleAction(ctx, data)
if err != nil {
return err
}
depth := nestedRuleDepth(ctx)
err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &rule.RawLogicalRule)
if err != nil {
return err
}
err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawLogicalRule, &rule.RuleAction)
if err != nil {
return err
}
if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) {
rule.RuleAction = RuleAction{}
}
return nil
}
func (r *LogicalRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid)
}
+138 -37
View File
@@ -4,9 +4,11 @@ import (
"context"
"fmt"
"net/netip"
"reflect"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -14,7 +16,7 @@ import (
)
type _RuleAction struct {
Action string `json:"action,omitempty"`
Action string `json:"action,omitempty" enum:"route,route-options,direct,bypass,reject,hijack-dns,sniff,resolve"`
RouteOptions RouteActionOptions `json:"-"`
RouteOptionsOptions RouteOptionsActionOptions `json:"-"`
DirectOptions DirectActionOptions `json:"-"`
@@ -97,8 +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:"-"`
@@ -115,6 +119,10 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
case C.RuleActionTypeRoute:
r.Action = ""
v = r.RouteOptions
case C.RuleActionTypeEvaluate:
v = r.EvaluateOptions
case C.RuleActionTypeRespond:
v = nil
case C.RuleActionTypeRouteOptions:
v = r.RouteOptionsOptions
case C.RuleActionTypeReject:
@@ -124,6 +132,9 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
default:
return nil, E.New("unknown DNS rule action: " + r.Action)
}
if v == nil {
return badjson.MarshallObjects(_DNSRuleAction(r))
}
return badjson.MarshallObjects(_DNSRuleAction(r), v)
}
@@ -137,6 +148,10 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e
case "", C.RuleActionTypeRoute:
r.Action = C.RuleActionTypeRoute
v = &r.RouteOptions
case C.RuleActionTypeEvaluate:
v = &r.EvaluateOptions
case C.RuleActionTypeRespond:
v = nil
case C.RuleActionTypeRouteOptions:
v = &r.RouteOptionsOptions
case C.RuleActionTypeReject:
@@ -146,11 +161,18 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e
default:
return E.New("unknown DNS rule action: " + r.Action)
}
return badjson.UnmarshallExcludedContext(ctx, data, (*_DNSRuleAction)(r), v)
if v == nil {
return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{})
}
err = badjson.UnmarshallExcludedContext(ctx, data, (*_DNSRuleAction)(r), v)
if err != nil {
return err
}
return nil
}
type RouteActionOptions struct {
Outbound string `json:"outbound,omitempty"`
Outbound string `json:"outbound,omitempty" reference:"outbound"`
RawRouteOptionsActionOptions
}
@@ -170,6 +192,8 @@ type RawRouteOptionsActionOptions struct {
TLSFragment bool `json:"tls_fragment,omitempty"`
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" enum:"wrong-sequence,wrong-checksum,wrong-ack,wrong-md5,wrong-timestamp"`
}
type RouteOptionsActionOptions RawRouteOptionsActionOptions
@@ -189,24 +213,32 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
}
type DNSRouteActionOptions struct {
Server string `json:"server,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
Server string `json:"server,omitempty" reference:"dns_server"`
Speculative bool `json:"speculative,omitempty"`
AbstractDNSRouteActionOptions
}
type _DNSRouteOptionsActionOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
type DNSEvaluateActionOptions struct {
Server string `json:"server,omitempty" reference:"dns_server"`
Tag string `json:"tag,omitempty"`
Speculative bool `json:"speculative,omitempty"`
AbstractDNSRouteActionOptions
}
type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions
type AbstractDNSRouteActionOptions struct {
Timeout badoption.Duration `json:"timeout,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"`
RemoveClientSubnet bool `json:"remove_client_subnet,omitempty"`
}
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
}
@@ -216,9 +248,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
@@ -258,19 +290,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"`
}
@@ -304,16 +325,18 @@ 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"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,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"`
DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type DNSRouteActionPredefined struct {
@@ -322,3 +345,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},
})
}
+29
View File
@@ -0,0 +1,29 @@
package option
import (
"context"
"testing"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/json"
"github.com/stretchr/testify/require"
)
func TestDNSRuleActionRespondUnmarshalJSON(t *testing.T) {
t.Parallel()
var action DNSRuleAction
err := json.UnmarshalContext(context.Background(), []byte(`{"action":"respond"}`), &action)
require.NoError(t, err)
require.Equal(t, C.RuleActionTypeRespond, action.Action)
require.Equal(t, DNSRouteActionOptions{}, action.RouteOptions)
}
func TestDNSRuleActionRespondRejectsUnknownFields(t *testing.T) {
t.Parallel()
var action DNSRuleAction
err := json.UnmarshalContext(context.Background(), []byte(`{"action":"respond","disable_cache":true}`), &action)
require.ErrorContains(t, err, "unknown field")
}
+133 -21
View File
@@ -5,6 +5,7 @@ import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
@@ -13,7 +14,7 @@ import (
)
type _DNSRule struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty" enum:"default,logical"`
DefaultOptions DefaultDNSRule `json:"-"`
LogicalOptions LogicalDNSRule `json:"-"`
}
@@ -35,7 +36,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) {
}
func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
err := json.Unmarshal(bytes, (*_DNSRule)(r))
err := json.UnmarshalContext(ctx, bytes, (*_DNSRule)(r))
if err != nil {
return err
}
@@ -67,23 +68,85 @@ 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
}
func (m *DNSRuleMatchResponse) UnmarshalJSON(content []byte) error {
var boolValue bool
err := json.Unmarshal(content, &boolValue)
if err == nil {
m.Enabled = boolValue
m.Tag = ""
return nil
}
var stringValue string
err = json.Unmarshal(content, &stringValue)
if err != nil {
return E.New("invalid match_response value")
}
if stringValue == "" {
return E.New("empty match_response tag")
}
m.Enabled = true
m.Tag = stringValue
return nil
}
func (m DNSRuleMatchResponse) MarshalJSON() ([]byte, error) {
if m.Tag != "" {
return json.Marshal(m.Tag)
}
return json.Marshal(m.Enabled)
}
func (m *DNSRuleMatchResponse) IsEnabled() bool {
return m != nil && m.Enabled
}
func (m *DNSRuleMatchResponse) ResponseTag() string {
if m == nil {
return ""
}
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"`
QueryClientSubnet badoption.Listable[*badoption.Prefixable] `json:"query_client_subnet,omitempty"`
QueryDNSSEC bool `json:"query_dnssec,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"`
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"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
@@ -94,9 +157,10 @@ type RawDefaultDNSRule struct {
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
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"`
@@ -106,13 +170,29 @@ type RawDefaultDNSRule struct {
InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"`
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
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" reference:"rule_set"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
MatchResponse *DNSRuleMatchResponse `json:"match_response,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
ResponseRcode *DNSRCode `json:"response_rcode,omitempty"`
ResponseAnswer badoption.Listable[DNSRecordOptions] `json:"response_answer,omitempty"`
ResponseNs badoption.Listable[DNSRecordOptions] `json:"response_ns,omitempty"`
ResponseExtra badoption.Listable[DNSRecordOptions] `json:"response_extra,omitempty"`
Invert bool `json:"invert,omitempty"`
// Deprecated: removed in sing-box 1.12.0
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" 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 {
@@ -125,11 +205,27 @@ func (r DefaultDNSRule) MarshalJSON() ([]byte, error) {
}
func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
err := json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule)
rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data)
if err != nil {
return err
}
return badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
err = rejectNestedDNSRuleAction(ctx, data)
if err != nil {
return err
}
depth := nestedRuleDepth(ctx)
err = json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule)
if err != nil {
return err
}
err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
if err != nil {
return err
}
if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) {
r.DNSRuleAction = DNSRuleAction{}
}
return nil
}
func (r DefaultDNSRule) IsValid() bool {
@@ -139,7 +235,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"`
}
@@ -154,11 +250,27 @@ func (r LogicalDNSRule) MarshalJSON() ([]byte, error) {
}
func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
err := json.Unmarshal(data, &r.RawLogicalDNSRule)
rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data)
if err != nil {
return err
}
return badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction)
err = rejectNestedDNSRuleAction(ctx, data)
if err != nil {
return err
}
depth := nestedRuleDepth(ctx)
err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &r.RawLogicalDNSRule)
if err != nil {
return err
}
err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction)
if err != nil {
return err
}
if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) {
r.DNSRuleAction = DNSRuleAction{}
}
return nil
}
func (r *LogicalDNSRule) IsValid() bool {
+129
View File
@@ -0,0 +1,129 @@
package option
import (
"context"
"reflect"
"slices"
"strings"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
)
type nestedRuleDepthContextKey struct{}
const (
RouteRuleActionNestedUnsupportedMessage = "rule action is not supported in nested rules"
DNSRuleActionNestedUnsupportedMessage = "DNS rule action is not supported in nested rules"
)
var (
routeRuleActionKeys = jsonFieldNames(reflect.TypeFor[_RuleAction](), reflect.TypeFor[RouteActionOptions]())
dnsRuleActionKeys = jsonFieldNames(reflect.TypeFor[_DNSRuleAction](), reflect.TypeFor[DNSRouteActionOptions](), reflect.TypeFor[DNSEvaluateActionOptions]())
)
func nestedRuleChildContext(ctx context.Context) context.Context {
return context.WithValue(ctx, nestedRuleDepthContextKey{}, nestedRuleDepth(ctx)+1)
}
func rejectNestedRouteRuleAction(ctx context.Context, content []byte) error {
return rejectNestedRuleAction(ctx, content, routeRuleActionKeys, RouteRuleActionNestedUnsupportedMessage)
}
func rejectNestedDNSRuleAction(ctx context.Context, content []byte) error {
return rejectNestedRuleAction(ctx, content, dnsRuleActionKeys, DNSRuleActionNestedUnsupportedMessage)
}
func nestedRuleDepth(ctx context.Context) int {
depth, _ := ctx.Value(nestedRuleDepthContextKey{}).(int)
return depth
}
func rejectNestedRuleAction(ctx context.Context, content []byte, keys []string, message string) error {
if nestedRuleDepth(ctx) == 0 {
return nil
}
hasActionKey, err := hasAnyJSONKey(ctx, content, keys...)
if err != nil {
return err
}
if hasActionKey {
return E.New(message)
}
return nil
}
func hasAnyJSONKey(ctx context.Context, content []byte, keys ...string) (bool, error) {
var object badjson.JSONObject
err := object.UnmarshalJSONContext(ctx, content)
if err != nil {
return false, err
}
return slices.ContainsFunc(keys, object.ContainsKey), nil
}
func inspectRouteRuleAction(ctx context.Context, content []byte) (string, RouteActionOptions, error) {
var rawAction _RuleAction
err := json.UnmarshalContext(ctx, content, &rawAction)
if err != nil {
return "", RouteActionOptions{}, err
}
var routeOptions RouteActionOptions
err = json.UnmarshalContext(ctx, content, &routeOptions)
if err != nil {
return "", RouteActionOptions{}, err
}
return rawAction.Action, routeOptions, nil
}
func inspectDNSRuleAction(ctx context.Context, content []byte) (string, DNSRouteActionOptions, error) {
var rawAction _DNSRuleAction
err := json.UnmarshalContext(ctx, content, &rawAction)
if err != nil {
return "", DNSRouteActionOptions{}, err
}
var routeOptions DNSRouteActionOptions
err = json.UnmarshalContext(ctx, content, &routeOptions)
if err != nil {
return "", DNSRouteActionOptions{}, err
}
return rawAction.Action, routeOptions, nil
}
func jsonFieldNames(types ...reflect.Type) []string {
fieldMap := make(map[string]struct{})
for _, fieldType := range types {
appendJSONFieldNames(fieldMap, fieldType)
}
fieldNames := make([]string, 0, len(fieldMap))
for fieldName := range fieldMap {
fieldNames = append(fieldNames, fieldName)
}
return fieldNames
}
func appendJSONFieldNames(fieldMap map[string]struct{}, fieldType reflect.Type) {
for fieldType.Kind() == reflect.Pointer {
fieldType = fieldType.Elem()
}
if fieldType.Kind() != reflect.Struct {
return
}
for i := range fieldType.NumField() {
field := fieldType.Field(i)
tagValue := field.Tag.Get("json")
tagName, _, _ := strings.Cut(tagValue, ",")
if tagName == "-" {
continue
}
if field.Anonymous && tagName == "" {
appendJSONFieldNames(fieldMap, field.Type)
continue
}
if tagName == "" {
tagName = field.Name
}
fieldMap[tagName] = struct{}{}
}
}
+68
View File
@@ -0,0 +1,68 @@
package option
import (
"context"
"testing"
"github.com/sagernet/sing/common/json"
"github.com/stretchr/testify/require"
)
func TestRuleRejectsNestedDefaultRuleAction(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "outbound": "direct"}
]
}`), &rule)
require.ErrorContains(t, err, RouteRuleActionNestedUnsupportedMessage)
}
func TestRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "foo": "bar"}
]
}`), &rule)
require.ErrorContains(t, err, "unknown field")
require.NotContains(t, err.Error(), RouteRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedDefaultRuleAction(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "server": "default"}
]
}`), &rule)
require.ErrorContains(t, err, DNSRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "foo": "bar"}
]
}`), &rule)
require.ErrorContains(t, err, "unknown field")
require.NotContains(t, err.Error(), DNSRuleActionNestedUnsupportedMessage)
}
+83 -15
View File
@@ -4,8 +4,10 @@ import (
"net/url"
"path/filepath"
"reflect"
"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"
@@ -18,12 +20,12 @@ import (
)
type _RuleSet struct {
Type string `json:"type,omitempty"`
Tag string `json:"tag"`
Format string `json:"format,omitempty"`
InlineOptions PlainRuleSet `json:"-"`
LocalOptions LocalRuleSet `json:"-"`
RemoteOptions RemoteRuleSet `json:"-"`
Type string `json:"type,omitempty" enum:"inline,local,remote"`
Tag badoption.Listable[string] `json:"tag"`
Format string `json:"format,omitempty" enum:"source,binary"`
InlineOptions PlainRuleSet `json:"-"`
LocalOptions LocalRuleSet `json:"-"`
RemoteOptions RemoteRuleSet `json:"-"`
}
type RuleSet _RuleSet
@@ -61,7 +63,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error {
if err != nil {
return err
}
if r.Tag == "" {
if len(r.Tag) == 0 || common.Any(r.Tag, func(tag string) bool { return tag == "" }) {
return E.New("missing tag")
}
var v any
@@ -99,6 +101,23 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error {
} else {
r.Format = ""
}
if len(r.Tag) > 1 {
switch r.Type {
case C.RuleSetTypeInline:
return E.New("inline rule-set does not support multiple tags")
case C.RuleSetTypeLocal:
if !strings.Contains(r.LocalOptions.Path, C.RuleSetTagPlaceholder) {
return E.New("missing ", C.RuleSetTagPlaceholder, " placeholder in path")
}
case C.RuleSetTypeRemote:
if !strings.Contains(r.RemoteOptions.URL, C.RuleSetTagPlaceholder) {
return E.New("missing ", C.RuleSetTagPlaceholder, " placeholder in url")
}
if r.RemoteOptions.InitialPath != "" && !strings.Contains(r.RemoteOptions.InitialPath, C.RuleSetTagPlaceholder) {
return E.New("missing ", C.RuleSetTagPlaceholder, " placeholder in initial_path")
}
}
}
return nil
}
@@ -116,18 +135,60 @@ 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"`
}
type RemoteRuleSet struct {
URL string `json:"url"`
DownloadDetour string `json:"download_detour,omitempty"`
InitialPath string `json:"initial_path,omitempty"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
// Deprecated: use http_client instead
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:"-"`
}
@@ -181,9 +242,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"`
@@ -198,6 +265,7 @@ type DefaultHeadlessRule struct {
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
@@ -223,7 +291,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"`
}
@@ -233,7 +301,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:"-"`
}
@@ -243,7 +311,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat
func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) {
var v any
switch r.Version {
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5:
v = r.Options
default:
return nil, E.New("unknown rule-set version: ", r.Version)
@@ -258,7 +326,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
}
var v any
switch r.Version {
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5:
v = &r.Options
case 0:
return E.New("missing rule-set version")
@@ -275,7 +343,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) {
switch r.Version {
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5:
default:
return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version))
}
+71
View File
@@ -0,0 +1,71 @@
package option
import (
"reflect"
"slices"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
)
type schemaTypeRegistry interface {
OptionTypes() []string
CreateOptions(itemType string) (any, bool)
}
func registryUnion(builder schema.Builder, registry schemaTypeRegistry, excludeTypes []string, withTag bool) (*schema.Node, error) {
var variants []*schema.Node
for _, itemType := range registry.OptionTypes() {
if slices.Contains(excludeTypes, itemType) {
continue
}
optionsValue, _ := registry.CreateOptions(itemType)
describer, isDescriber := optionsValue.(schema.Describer)
var variant *schema.Node
var err error
if isDescriber {
variant, err = describer.DescribeSchema(builder)
} else {
variant = schema.StrictObject()
err = builder.FlattenStruct(variant, reflect.TypeOf(optionsValue).Elem())
}
if err != nil {
return nil, E.Cause(err, itemType)
}
err = prependTypeTag(variant, itemType, withTag)
if err != nil {
return nil, E.Cause(err, itemType)
}
variants = append(variants, variant)
}
return schema.OneOf(variants...), nil
}
// prependTypeTag merges the polymorphic base fields into a variant produced
// from registry options, matching badjson.UnmarshallExcluded composition.
func prependTypeTag(variant *schema.Node, typeName string, withTag bool) error {
if variant.OneOf != nil {
for _, branch := range variant.OneOf {
err := prependTypeTag(branch, typeName, withTag)
if err != nil {
return err
}
}
return nil
}
if variant.Properties == nil {
return E.New("cannot merge type into non-object variant")
}
newProperties := new(badjson.TypedMap[string, *schema.Node])
newProperties.Put("type", schema.StringConst(typeName))
if withTag {
newProperties.Put("tag", schema.StringNode())
}
for _, entry := range variant.Properties.Entries() {
newProperties.Put(entry.Key, entry.Value)
}
variant.Properties = newProperties
variant.Required = append([]string{"type"}, variant.Required...)
return nil
}
+12
View File
@@ -3,6 +3,7 @@ package option
import (
"context"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -10,6 +11,7 @@ import (
)
type ServiceOptionsRegistry interface {
OptionTypes() []string
CreateOptions(serviceType string) (any, bool)
}
@@ -45,3 +47,13 @@ func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) erro
h.Options = options
return nil
}
func (h Service) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("Service", func() (*schema.Node, error) {
registry := service.FromContext[ServiceOptionsRegistry](builder.Context())
if registry == nil {
return nil, E.New("missing service options registry in context")
}
return registryUnion(builder, registry, nil, true)
})
}
+2 -2
View File
@@ -3,7 +3,7 @@ package option
type ShadowsocksInboundOptions struct {
ListenOptions
Network NetworkList `json:"network,omitempty"`
Method string `json:"method"`
Method string `json:"method" enum:"none,aes-128-gcm,aes-192-gcm,aes-256-gcm,chacha20-ietf-poly1305,xchacha20-ietf-poly1305,2022-blake3-aes-128-gcm,2022-blake3-aes-256-gcm,2022-blake3-chacha20-poly1305"`
Password string `json:"password,omitempty"`
Users []ShadowsocksUser `json:"users,omitempty"`
Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
@@ -25,7 +25,7 @@ type ShadowsocksDestination struct {
type ShadowsocksOutboundOptions struct {
DialerOptions
ServerOptions
Method string `json:"method"`
Method string `json:"method" enum:"none,aes-128-gcm,aes-192-gcm,aes-256-gcm,chacha20-ietf-poly1305,xchacha20-ietf-poly1305,2022-blake3-aes-128-gcm,2022-blake3-aes-256-gcm,2022-blake3-chacha20-poly1305,aes-128-ctr,aes-192-ctr,aes-256-ctr,aes-128-cfb,aes-192-cfb,aes-256-cfb,rc4-md5,chacha20-ietf,xchacha20"`
Password string `json:"password"`
Plugin string `json:"plugin,omitempty"`
PluginOptions string `json:"plugin_opts,omitempty"`
+7 -2
View File
@@ -3,13 +3,14 @@ package option
import (
"encoding/json"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
)
type ShadowTLSInboundOptions struct {
ListenOptions
Version int `json:"version,omitempty"`
Version int `json:"version,omitempty" enum:"1,2,3"`
Password string `json:"password,omitempty"`
Users []ShadowTLSUser `json:"users,omitempty"`
Handshake ShadowTLSHandshakeOptions `json:"handshake,omitempty"`
@@ -62,6 +63,10 @@ func (w *WildcardSNI) UnmarshalJSON(bytes []byte) error {
return nil
}
func (w WildcardSNI) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum("", "off", "authed", "all"), nil
}
type ShadowTLSUser struct {
Name string `json:"name,omitempty"`
Password string `json:"password,omitempty"`
@@ -75,7 +80,7 @@ type ShadowTLSHandshakeOptions struct {
type ShadowTLSOutboundOptions struct {
DialerOptions
ServerOptions
Version int `json:"version,omitempty"`
Version int `json:"version,omitempty" enum:"1,2,3"`
Password string `json:"password,omitempty"`
OutboundTLSOptionsContainer
}
+1 -1
View File
@@ -22,7 +22,7 @@ type HTTPMixedInboundOptions struct {
type SOCKSOutboundOptions struct {
DialerOptions
ServerOptions
Version string `json:"version,omitempty"`
Version string `json:"version,omitempty" enum:"4,4a,5"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
+147
View File
@@ -0,0 +1,147 @@
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 {
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 {
err := json.Unmarshal(content, (*_SnellInboundOptions)(o))
if err != nil {
return err
}
var versionOptions any
switch o.Version {
case 5:
versionOptions = &o.ObfsOptions
case 6:
versionOptions = &o.V6Options
case 0:
return E.New("snell: missing version")
default:
return E.New("snell: unsupported version: ", o.Version)
}
return badjson.UnmarshallExcluded(content, (*_SnellInboundOptions)(o), versionOptions)
}
func (o SnellInboundOptions) MarshalJSON() ([]byte, error) {
var versionOptions any
switch o.Version {
case 5:
versionOptions = o.ObfsOptions
case 6:
versionOptions = o.V6Options
case 0:
return nil, E.New("snell: missing version")
default:
return nil, E.New("snell: unsupported version: ", o.Version)
}
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 {
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 {
err := json.Unmarshal(content, (*_SnellOutboundOptions)(o))
if err != nil {
return err
}
var versionOptions any
switch o.Version {
case 4:
versionOptions = &o.ObfsOptions
case 6:
versionOptions = &o.V6Options
case 0:
return E.New("snell: missing version")
default:
return E.New("snell: unsupported version: ", o.Version)
}
return badjson.UnmarshallExcluded(content, (*_SnellOutboundOptions)(o), versionOptions)
}
func (o SnellOutboundOptions) MarshalJSON() ([]byte, error) {
var versionOptions any
switch o.Version {
case 4:
versionOptions = o.ObfsOptions
case 6:
versionOptions = o.V6Options
case 0:
return nil, E.New("snell: missing version")
default:
return nil, E.New("snell: unsupported version: ", o.Version)
}
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" enum:"none,http,tls"`
}
type SnellUser struct {
Name string `json:"name,omitempty"`
UserKey string `json:"userkey"`
}
type SnellObfsClientOptions struct {
ObfsMode string `json:"obfs_mode,omitempty" enum:"none,http,tls"`
ObfsHost string `json:"obfs_host,omitempty"`
}
type SnellV6Options struct {
Mode string `json:"mode,omitempty" enum:"default,unshaped,unsafe-raw"`
}
+3
View File
@@ -46,4 +46,7 @@ type SSHOutboundOptions struct {
HostKey badoption.Listable[string] `json:"host_key,omitempty"`
HostKeyAlgorithms badoption.Listable[string] `json:"host_key_algorithms,omitempty"`
ClientVersion string `json:"client_version,omitempty"`
Cipher badoption.Listable[string] `json:"cipher,omitempty"`
MAC badoption.Listable[string] `json:"mac,omitempty"`
KexAlgorithm badoption.Listable[string] `json:"kex_algorithm,omitempty"`
}
+83 -7
View File
@@ -5,7 +5,9 @@ import (
"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"
M "github.com/sagernet/sing/common/metadata"
)
@@ -23,17 +25,58 @@ type TailscaleEndpointOptions struct {
AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"`
AdvertiseExitNode bool `json:"advertise_exit_node,omitempty"`
AdvertiseTags badoption.Listable[string] `json:"advertise_tags,omitempty"`
ListenPort uint16 `json:"listen_port,omitempty"`
RelayServerPort *uint16 `json:"relay_server_port,omitempty"`
RelayServerStaticEndpoints []netip.AddrPort `json:"relay_server_static_endpoints,omitempty"`
SystemInterface bool `json:"system_interface,omitempty"`
SystemInterfaceName string `json:"system_interface_name,omitempty"`
SystemInterfaceMTU uint32 `json:"system_interface_mtu,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
SSHServer *TailscaleSSHServerOptions `json:"ssh_server,omitempty"`
TaildropDirectory string `json:"taildrop_directory,omitempty"`
}
type _TailscaleSSHServerOptions struct {
Enabled bool `json:"enabled,omitempty"`
DisablePTY bool `json:"disable_pty,omitempty"`
DisableSFTP bool `json:"disable_sftp,omitempty"`
DisableForwarding bool `json:"disable_forwarding,omitempty"`
}
type TailscaleSSHServerOptions _TailscaleSSHServerOptions
func (o TailscaleSSHServerOptions) MarshalJSON() ([]byte, error) {
if !o.DisablePTY && !o.DisableSFTP && !o.DisableForwarding {
return json.Marshal(o.Enabled)
}
return json.Marshal(_TailscaleSSHServerOptions(o))
}
func (o *TailscaleSSHServerOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, &o.Enabled)
if err == nil {
return nil
}
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"`
AcceptSearchDomain bool `json:"accept_search_domain,omitempty"`
}
type TailscaleCertificateProviderOptions struct {
Endpoint string `json:"endpoint,omitempty"`
}
type DERPServiceOptions struct {
@@ -49,9 +92,13 @@ type DERPServiceOptions struct {
STUN *DERPSTUNListenOptions `json:"stun,omitempty"`
}
type _DERPVerifyClientURLOptions struct {
type _DERPVerifyClientURLBase struct {
URL string `json:"url,omitempty"`
DialerOptions
}
type _DERPVerifyClientURLOptions struct {
_DERPVerifyClientURLBase
HTTPClientOptions
}
type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions
@@ -65,21 +112,41 @@ func (d DERPVerifyClientURLOptions) ServerIsDomain() bool {
}
func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) {
if reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) {
if d.URL != "" && d.HTTPClientOptions.IsEmpty() {
return json.Marshal(d.URL)
} else {
return json.Marshal(_DERPVerifyClientURLOptions(d))
}
return badjson.MarshallObjects(d._DERPVerifyClientURLBase, HTTPClient(d.HTTPClientOptions))
}
func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err == nil {
d.URL = stringValue
*d = DERPVerifyClientURLOptions{
_DERPVerifyClientURLBase: _DERPVerifyClientURLBase{URL: stringValue},
}
return nil
}
return json.Unmarshal(bytes, (*_DERPVerifyClientURLOptions)(d))
err = json.Unmarshal(bytes, &d._DERPVerifyClientURLBase)
if err != nil {
return err
}
var client HTTPClient
err = badjson.UnmarshallExcluded(bytes, &d._DERPVerifyClientURLBase, &client)
if err != nil {
return err
}
d.HTTPClientOptions = HTTPClientOptions(client)
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 {
@@ -120,3 +187,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
}
+32 -14
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"strings"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
)
@@ -13,9 +14,9 @@ type InboundTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
ALPN badoption.Listable[string] `json:"alpn,omitempty"`
MinVersion string `json:"min_version,omitempty"`
MaxVersion string `json:"max_version,omitempty"`
ALPN badoption.Listable[string] `json:"alpn,omitempty" examples:"http/1.1,h2,h3"`
MinVersion string `json:"min_version,omitempty" enum:"1.0,1.1,1.2,1.3"`
MaxVersion string `json:"max_version,omitempty" enum:"1.0,1.1,1.2,1.3"`
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"`
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
@@ -28,9 +29,14 @@ type InboundTLSOptions struct {
KeyPath string `json:"key_path,omitempty"`
KernelTx bool `json:"kernel_tx,omitempty"`
KernelRx bool `json:"kernel_rx,omitempty"`
ACME *InboundACMEOptions `json:"acme,omitempty"`
ECH *InboundECHOptions `json:"ech,omitempty"`
Reality *InboundRealityOptions `json:"reality,omitempty"`
HandshakeTimeout badoption.Duration `json:"handshake_timeout,omitempty"`
CertificateProvider *CertificateProviderOptions `json:"certificate_provider,omitempty"`
// Deprecated: use certificate_provider
ACME *InboundACMEOptions `json:"acme,omitempty" schema:"omit"`
ECH *InboundECHOptions `json:"ech,omitempty"`
Reality *InboundRealityOptions `json:"reality,omitempty"`
}
type ClientAuthType tls.ClientAuthType
@@ -77,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"`
}
@@ -96,12 +106,13 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
type OutboundTLSOptions struct {
Enabled bool `json:"enabled,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"`
@@ -114,8 +125,11 @@ type OutboundTLSOptions struct {
Fragment bool `json:"fragment,omitempty"`
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" 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"`
ECH *OutboundECHOptions `json:"ech,omitempty"`
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
Reality *OutboundRealityOptions `json:"reality,omitempty"`
@@ -190,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"`
@@ -209,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 {
@@ -221,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 {
+19 -1
View File
@@ -1,7 +1,10 @@
package option
import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
@@ -20,6 +23,7 @@ type InboundACMEOptions struct {
AlternativeTLSPort uint16 `json:"alternative_tls_port,omitempty"`
ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"`
DNS01Challenge *ACMEDNS01ChallengeOptions `json:"dns01_challenge,omitempty"`
Profile string `json:"profile,omitempty"`
}
type ACMEExternalAccountOptions struct {
@@ -28,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:"-"`
@@ -76,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"`
+5 -3
View File
@@ -5,11 +5,12 @@ 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"`
InboundTLSOptionsContainer
QUICOptions
}
type TUICUser struct {
@@ -23,11 +24,12 @@ 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"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
QUICOptions
}
+23 -10
View File
@@ -4,6 +4,7 @@ import (
"net/netip"
"strconv"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
@@ -12,8 +13,11 @@ import (
type TunInboundOptions struct {
InterfaceName string `json:"interface_name,omitempty"`
NetNs string `json:"netns,omitempty" reference:"network_namespace"`
MTU uint32 `json:"mtu,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address,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"`
IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
@@ -39,27 +43,32 @@ type TunInboundOptions struct {
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"`
IncludeMACAddress badoption.Listable[string] `json:"include_mac_address,omitempty"`
ExcludeMACAddress badoption.Listable[string] `json:"exclude_mac_address,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
Stack string `json:"stack,omitempty"`
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,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
@@ -84,3 +93,7 @@ func (f *FwMark) UnmarshalJSON(bytes []byte) error {
*f = FwMark(intValue)
return nil
}
func (f FwMark) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.AnyOf(schema.UnsignedNode(32), schema.StringNode()), nil
}
+32
View File
@@ -1,9 +1,12 @@
package option
import (
"maps"
"slices"
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/schema"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
@@ -43,6 +46,10 @@ func (v NetworkList) Build() []string {
return strings.Split(string(v), "\n")
}
func (v NetworkList) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.ListableOf(schema.StringEnum(N.NetworkTCP, N.NetworkUDP)), nil
}
type DomainStrategy C.DomainStrategy
func (s DomainStrategy) String() string {
@@ -105,6 +112,12 @@ func (s *DomainStrategy) UnmarshalJSON(bytes []byte) error {
return nil
}
func (s DomainStrategy) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DomainStrategy", func() (*schema.Node, error) {
return schema.StringEnum("", "as_is", "prefer_ipv4", "prefer_ipv6", "ipv4_only", "ipv6_only"), nil
})
}
type DNSQueryType uint16
func (t DNSQueryType) String() string {
@@ -142,6 +155,15 @@ func (t *DNSQueryType) UnmarshalJSON(bytes []byte) error {
return E.New("unknown DNS query type: ", string(bytes))
}
func (t DNSQueryType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("DNSQueryType", func() (*schema.Node, error) {
return schema.AnyOf(
schema.UnsignedNode(16),
schema.StringEnum(slices.Sorted(maps.Keys(mDNS.StringToType))...),
), nil
})
}
func DNSQueryTypeToString(queryType uint16) string {
typeName, loaded := mDNS.TypeToString[queryType]
if loaded {
@@ -170,6 +192,10 @@ func (n *NetworkStrategy) UnmarshalJSON(content []byte) error {
return nil
}
func (n NetworkStrategy) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return schema.StringEnum(slices.Sorted(maps.Keys(C.StringToNetworkStrategy))...), nil
}
type InterfaceType C.InterfaceType
func (t InterfaceType) Build() C.InterfaceType {
@@ -193,3 +219,9 @@ func (t *InterfaceType) UnmarshalJSON(content []byte) error {
*t = InterfaceType(interfaceType)
return nil
}
func (t InterfaceType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("InterfaceType", func() (*schema.Node, error) {
return schema.StringEnum(slices.Sorted(maps.Keys(C.StringToInterfaceType))...), nil
})
}
+13 -1
View File
@@ -1,13 +1,16 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/uot"
)
type _UDPOverTCPOptions struct {
Enabled bool `json:"enabled,omitempty"`
Version uint8 `json:"version,omitempty"`
Version uint8 `json:"version,omitempty" enum:"1,2"`
}
type UDPOverTCPOptions _UDPOverTCPOptions
@@ -28,3 +31,12 @@ func (o *UDPOverTCPOptions) UnmarshalJSON(bytes []byte) error {
}
return json.UnmarshalDisallowUnknownFields(bytes, (*_UDPOverTCPOptions)(o))
}
func (o UDPOverTCPOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
objectForm := schema.StrictObject()
err := builder.FlattenStruct(objectForm, reflect.TypeFor[UDPOverTCPOptions]())
if err != nil {
return nil, err
}
return schema.AnyOf(schema.BooleanNode(), objectForm), nil
}
+81
View File
@@ -0,0 +1,81 @@
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"
)
const (
USBIPProviderDefault = "default"
USBIPProviderDynamic = "dynamic"
)
type _USBIPServerServiceOptions struct {
ListenOptions
Provider string `json:"provider,omitempty" enum:"default,dynamic"`
Options any `json:"-"`
}
type USBIPServerServiceOptions _USBIPServerServiceOptions
func (o USBIPServerServiceOptions) MarshalJSON() ([]byte, error) {
if o.Options == nil {
return json.Marshal(_USBIPServerServiceOptions(o))
}
return badjson.MarshallObjects(_USBIPServerServiceOptions(o), o.Options)
}
func (o *USBIPServerServiceOptions) UnmarshalJSON(content []byte) error {
err := json.Unmarshal(content, (*_USBIPServerServiceOptions)(o))
if err != nil {
return err
}
var options any
switch o.Provider {
case "", USBIPProviderDefault:
o.Provider = USBIPProviderDefault
options = new(USBIPDefaultProviderOptions)
case USBIPProviderDynamic:
options = new(USBIPDynamicProviderOptions)
default:
return E.New("unknown usbip provider type: ", o.Provider)
}
err = badjson.UnmarshallExcluded(content, (*_USBIPServerServiceOptions)(o), options)
if err != nil {
return err
}
o.Options = options
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
Devices []USBIPDeviceMatch `json:"devices,omitempty"`
}
type USBIPDeviceMatch struct {
BusID string `json:"bus_id,omitempty"`
VendorID uint16 `json:"vendor_id,omitempty"`
ProductID uint16 `json:"product_id,omitempty"`
Serial string `json:"serial,omitempty"`
}
type USBIPDefaultProviderOptions struct {
Devices []USBIPDeviceMatch `json:"devices,omitempty"`
}
type USBIPDynamicProviderOptions struct{}
+15 -1
View File
@@ -2,10 +2,12 @@ package option
import (
"net/http"
"reflect"
"strings"
"github.com/sagernet/sing-box/common/xray/utils"
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,7 +15,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:"-"`
@@ -81,6 +83,18 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
return nil
}
func (o V2RayTransportOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
return builder.Define("V2RayTransport", func() (*schema.Node, error) {
return schema.DiscriminatedUnion(builder, "type", true, []schema.UnionVariant{
{Value: C.V2RayTransportTypeHTTP, StructType: reflect.TypeFor[V2RayHTTPOptions]()},
{Value: C.V2RayTransportTypeWebsocket, StructType: reflect.TypeFor[V2RayWebsocketOptions]()},
{Value: C.V2RayTransportTypeQUIC, StructType: reflect.TypeFor[V2RayQUICOptions]()},
{Value: C.V2RayTransportTypeGRPC, StructType: reflect.TypeFor[V2RayGRPCOptions]()},
{Value: C.V2RayTransportTypeHTTPUpgrade, StructType: reflect.TypeFor[V2RayHTTPUpgradeOptions]()},
}, nil)
})
}
type V2RayHTTPOptions struct {
Host badoption.Listable[string] `json:"host,omitempty"`
Path string `json:"path,omitempty"`
+2 -2
View File
@@ -18,13 +18,13 @@ type VMessOutboundOptions struct {
DialerOptions
ServerOptions
UUID string `json:"uuid"`
Security string `json:"security"`
Security string `json:"security" enum:"auto,none,zero,aes-128-cfb,aes-128-gcm,chacha20-poly1305"`
AlterId int `json:"alter_id,omitempty"`
GlobalPadding bool `json:"global_padding,omitempty"`
AuthenticatedLength bool `json:"authenticated_length,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
PacketEncoding string `json:"packet_encoding,omitempty"`
PacketEncoding string `json:"packet_encoding,omitempty" enum:"packetaddr,xudp"`
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`
}
+3
View File
@@ -15,6 +15,9 @@ type WireGuardEndpointOptions struct {
ListenPort uint16 `json:"listen_port,omitempty"`
Peers []WireGuardPeer `json:"peers,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
Workers int `json:"workers,omitempty"`
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
DisablePauses bool `json:"disable_pauses,omitempty"`