mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Merge tag 'v1.14.0'
This commit is contained in:
+125
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -10,7 +10,7 @@ type CCMServiceOptions struct {
|
||||
CredentialPath string `json:"credential_path,omitempty"`
|
||||
Users []CCMUser `json:"users,omitempty"`
|
||||
Headers badoption.HTTPHeader `json:"headers,omitempty"`
|
||||
Detour string `json:"detour,omitempty"`
|
||||
Detour string `json:"detour,omitempty" reference:"outbound"`
|
||||
UsagesPath string `json:"usages_path,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
+13
-1
@@ -1,13 +1,16 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/schema"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type _CertificateOptions struct {
|
||||
Store string `json:"store,omitempty"`
|
||||
Store string `json:"store,omitempty" enum:"system,mozilla,chrome,none"`
|
||||
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
|
||||
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
|
||||
@@ -34,3 +37,12 @@ func (o *CertificateOptions) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o CertificateOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
node := schema.StrictObject()
|
||||
err := builder.FlattenStruct(node, reflect.TypeFor[CertificateOptions]())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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:"-"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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{}{}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/sagernet/sing-box/schema"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
type schemaTypeRegistry interface {
|
||||
OptionTypes() []string
|
||||
CreateOptions(itemType string) (any, bool)
|
||||
}
|
||||
|
||||
func registryUnion(builder schema.Builder, registry schemaTypeRegistry, excludeTypes []string, withTag bool) (*schema.Node, error) {
|
||||
var variants []*schema.Node
|
||||
for _, itemType := range registry.OptionTypes() {
|
||||
if slices.Contains(excludeTypes, itemType) {
|
||||
continue
|
||||
}
|
||||
optionsValue, _ := registry.CreateOptions(itemType)
|
||||
describer, isDescriber := optionsValue.(schema.Describer)
|
||||
var variant *schema.Node
|
||||
var err error
|
||||
if isDescriber {
|
||||
variant, err = describer.DescribeSchema(builder)
|
||||
} else {
|
||||
variant = schema.StrictObject()
|
||||
err = builder.FlattenStruct(variant, reflect.TypeOf(optionsValue).Elem())
|
||||
}
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, itemType)
|
||||
}
|
||||
err = prependTypeTag(variant, itemType, withTag)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, itemType)
|
||||
}
|
||||
variants = append(variants, variant)
|
||||
}
|
||||
return schema.OneOf(variants...), nil
|
||||
}
|
||||
|
||||
// prependTypeTag merges the polymorphic base fields into a variant produced
|
||||
// from registry options, matching badjson.UnmarshallExcluded composition.
|
||||
func prependTypeTag(variant *schema.Node, typeName string, withTag bool) error {
|
||||
if variant.OneOf != nil {
|
||||
for _, branch := range variant.OneOf {
|
||||
err := prependTypeTag(branch, typeName, withTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if variant.Properties == nil {
|
||||
return E.New("cannot merge type into non-object variant")
|
||||
}
|
||||
newProperties := new(badjson.TypedMap[string, *schema.Node])
|
||||
newProperties.Put("type", schema.StringConst(typeName))
|
||||
if withTag {
|
||||
newProperties.Put("tag", schema.StringNode())
|
||||
}
|
||||
for _, entry := range variant.Properties.Entries() {
|
||||
newProperties.Put(entry.Key, entry.Value)
|
||||
}
|
||||
variant.Properties = newProperties
|
||||
variant.Required = append([]string{"type"}, variant.Required...)
|
||||
return nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package option
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/schema"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
type ServiceOptionsRegistry interface {
|
||||
OptionTypes() []string
|
||||
CreateOptions(serviceType string) (any, bool)
|
||||
}
|
||||
|
||||
@@ -45,3 +47,13 @@ func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) erro
|
||||
h.Options = options
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h Service) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
return builder.Define("Service", func() (*schema.Node, error) {
|
||||
registry := service.FromContext[ServiceOptionsRegistry](builder.Context())
|
||||
if registry == nil {
|
||||
return nil, E.New("missing service options registry in context")
|
||||
}
|
||||
return registryUnion(builder, registry, nil, true)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package option
|
||||
type ShadowsocksInboundOptions struct {
|
||||
ListenOptions
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Method string `json:"method" enum:"none,aes-128-gcm,aes-192-gcm,aes-256-gcm,chacha20-ietf-poly1305,xchacha20-ietf-poly1305,2022-blake3-aes-128-gcm,2022-blake3-aes-256-gcm,2022-blake3-chacha20-poly1305"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Users []ShadowsocksUser `json:"users,omitempty"`
|
||||
Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
|
||||
@@ -25,7 +25,7 @@ type ShadowsocksDestination struct {
|
||||
type ShadowsocksOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Method string `json:"method"`
|
||||
Method string `json:"method" enum:"none,aes-128-gcm,aes-192-gcm,aes-256-gcm,chacha20-ietf-poly1305,xchacha20-ietf-poly1305,2022-blake3-aes-128-gcm,2022-blake3-aes-256-gcm,2022-blake3-chacha20-poly1305,aes-128-ctr,aes-192-ctr,aes-256-ctr,aes-128-cfb,aes-192-cfb,aes-256-cfb,rc4-md5,chacha20-ietf,xchacha20"`
|
||||
Password string `json:"password"`
|
||||
Plugin string `json:"plugin,omitempty"`
|
||||
PluginOptions string `json:"plugin_opts,omitempty"`
|
||||
|
||||
+7
-2
@@ -3,13 +3,14 @@ package option
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sagernet/sing-box/schema"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
type ShadowTLSInboundOptions struct {
|
||||
ListenOptions
|
||||
Version int `json:"version,omitempty"`
|
||||
Version int `json:"version,omitempty" enum:"1,2,3"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Users []ShadowTLSUser `json:"users,omitempty"`
|
||||
Handshake ShadowTLSHandshakeOptions `json:"handshake,omitempty"`
|
||||
@@ -62,6 +63,10 @@ func (w *WildcardSNI) UnmarshalJSON(bytes []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w WildcardSNI) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
return schema.StringEnum("", "off", "authed", "all"), nil
|
||||
}
|
||||
|
||||
type ShadowTLSUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
@@ -75,7 +80,7 @@ type ShadowTLSHandshakeOptions struct {
|
||||
type ShadowTLSOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Version int `json:"version,omitempty"`
|
||||
Version int `json:"version,omitempty" enum:"1,2,3"`
|
||||
Password string `json:"password,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ type HTTPMixedInboundOptions struct {
|
||||
type SOCKSOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Version string `json:"version,omitempty"`
|
||||
Version string `json:"version,omitempty" enum:"4,4a,5"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
|
||||
+147
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/schema"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
@@ -43,6 +46,10 @@ func (v NetworkList) Build() []string {
|
||||
return strings.Split(string(v), "\n")
|
||||
}
|
||||
|
||||
func (v NetworkList) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
return schema.ListableOf(schema.StringEnum(N.NetworkTCP, N.NetworkUDP)), nil
|
||||
}
|
||||
|
||||
type DomainStrategy C.DomainStrategy
|
||||
|
||||
func (s DomainStrategy) String() string {
|
||||
@@ -105,6 +112,12 @@ func (s *DomainStrategy) UnmarshalJSON(bytes []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s DomainStrategy) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
return builder.Define("DomainStrategy", func() (*schema.Node, error) {
|
||||
return schema.StringEnum("", "as_is", "prefer_ipv4", "prefer_ipv6", "ipv4_only", "ipv6_only"), nil
|
||||
})
|
||||
}
|
||||
|
||||
type DNSQueryType uint16
|
||||
|
||||
func (t DNSQueryType) String() string {
|
||||
@@ -142,6 +155,15 @@ func (t *DNSQueryType) UnmarshalJSON(bytes []byte) error {
|
||||
return E.New("unknown DNS query type: ", string(bytes))
|
||||
}
|
||||
|
||||
func (t DNSQueryType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
return builder.Define("DNSQueryType", func() (*schema.Node, error) {
|
||||
return schema.AnyOf(
|
||||
schema.UnsignedNode(16),
|
||||
schema.StringEnum(slices.Sorted(maps.Keys(mDNS.StringToType))...),
|
||||
), nil
|
||||
})
|
||||
}
|
||||
|
||||
func DNSQueryTypeToString(queryType uint16) string {
|
||||
typeName, loaded := mDNS.TypeToString[queryType]
|
||||
if loaded {
|
||||
@@ -170,6 +192,10 @@ func (n *NetworkStrategy) UnmarshalJSON(content []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NetworkStrategy) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
return schema.StringEnum(slices.Sorted(maps.Keys(C.StringToNetworkStrategy))...), nil
|
||||
}
|
||||
|
||||
type InterfaceType C.InterfaceType
|
||||
|
||||
func (t InterfaceType) Build() C.InterfaceType {
|
||||
@@ -193,3 +219,9 @@ func (t *InterfaceType) UnmarshalJSON(content []byte) error {
|
||||
*t = InterfaceType(interfaceType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t InterfaceType) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
return builder.Define("InterfaceType", func() (*schema.Node, error) {
|
||||
return schema.StringEnum(slices.Sorted(maps.Keys(C.StringToInterfaceType))...), nil
|
||||
})
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,13 +1,16 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/sagernet/sing-box/schema"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/uot"
|
||||
)
|
||||
|
||||
type _UDPOverTCPOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Version uint8 `json:"version,omitempty"`
|
||||
Version uint8 `json:"version,omitempty" enum:"1,2"`
|
||||
}
|
||||
|
||||
type UDPOverTCPOptions _UDPOverTCPOptions
|
||||
@@ -28,3 +31,12 @@ func (o *UDPOverTCPOptions) UnmarshalJSON(bytes []byte) error {
|
||||
}
|
||||
return json.UnmarshalDisallowUnknownFields(bytes, (*_UDPOverTCPOptions)(o))
|
||||
}
|
||||
|
||||
func (o UDPOverTCPOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
|
||||
objectForm := schema.StrictObject()
|
||||
err := builder.FlattenStruct(objectForm, reflect.TypeFor[UDPOverTCPOptions]())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return schema.AnyOf(schema.BooleanNode(), objectForm), nil
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user