فهرست منبع

Migrate bad options to library

世界 11 ماه پیش
والد
کامیت
f12a294fb7

+ 3 - 2
common/dialer/default.go

@@ -3,6 +3,7 @@ package dialer
 import (
 	"context"
 	"net"
+	"net/netip"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -102,7 +103,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
 		udpAddr4   string
 	)
 	if options.Inet4BindAddress != nil {
-		bindAddr := options.Inet4BindAddress.Build()
+		bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified())
 		dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
 		udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
 		udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
@@ -113,7 +114,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
 		udpAddr6   string
 	)
 	if options.Inet6BindAddress != nil {
-		bindAddr := options.Inet6BindAddress.Build()
+		bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified())
 		dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
 		udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
 		udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()

+ 2 - 1
common/listener/listener.go

@@ -3,6 +3,7 @@ package listener
 import (
 	"context"
 	"net"
+	"net/netip"
 	"sync/atomic"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -92,7 +93,7 @@ func (l *Listener) Start() error {
 	if l.setSystemProxy {
 		listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port
 		var listenAddrString string
-		listenAddr := l.listenOptions.Listen.Build()
+		listenAddr := l.listenOptions.Listen.Build(netip.IPv4Unspecified())
 		if listenAddr.IsUnspecified() {
 			listenAddrString = "127.0.0.1"
 		} else {

+ 2 - 1
common/listener/listener_tcp.go

@@ -2,6 +2,7 @@ package listener
 
 import (
 	"net"
+	"net/netip"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -16,7 +17,7 @@ import (
 
 func (l *Listener) ListenTCP() (net.Listener, error) {
 	var err error
-	bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
+	bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
 	var tcpListener net.Listener
 	var listenConfig net.ListenConfig
 	if l.listenOptions.TCPKeepAlive >= 0 {

+ 2 - 1
common/listener/listener_udp.go

@@ -2,6 +2,7 @@ package listener
 
 import (
 	"net"
+	"net/netip"
 	"os"
 
 	"github.com/sagernet/sing/common/buf"
@@ -12,7 +13,7 @@ import (
 )
 
 func (l *Listener) ListenUDP() (net.PacketConn, error) {
-	bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
+	bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
 	var lc net.ListenConfig
 	var udpFragment bool
 	if l.listenOptions.UDPFragment != nil {

+ 1 - 0
experimental/deprecated/manager.go

@@ -2,6 +2,7 @@ package deprecated
 
 import (
 	"context"
+
 	"github.com/sagernet/sing/service"
 )
 

+ 18 - 14
option/dns.go

@@ -1,6 +1,10 @@
 package option
 
-import "net/netip"
+import (
+	"net/netip"
+
+	"github.com/sagernet/sing/common/json/badoption"
+)
 
 type DNSOptions struct {
 	Servers        []DNSServerOptions `json:"servers,omitempty"`
@@ -12,22 +16,22 @@ type DNSOptions struct {
 }
 
 type DNSServerOptions struct {
-	Tag                  string         `json:"tag,omitempty"`
-	Address              string         `json:"address"`
-	AddressResolver      string         `json:"address_resolver,omitempty"`
-	AddressStrategy      DomainStrategy `json:"address_strategy,omitempty"`
-	AddressFallbackDelay Duration       `json:"address_fallback_delay,omitempty"`
-	Strategy             DomainStrategy `json:"strategy,omitempty"`
-	Detour               string         `json:"detour,omitempty"`
-	ClientSubnet         *AddrPrefix    `json:"client_subnet,omitempty"`
+	Tag                  string                `json:"tag,omitempty"`
+	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 DNSClientOptions struct {
-	Strategy         DomainStrategy `json:"strategy,omitempty"`
-	DisableCache     bool           `json:"disable_cache,omitempty"`
-	DisableExpire    bool           `json:"disable_expire,omitempty"`
-	IndependentCache bool           `json:"independent_cache,omitempty"`
-	ClientSubnet     *AddrPrefix    `json:"client_subnet,omitempty"`
+	Strategy         DomainStrategy        `json:"strategy,omitempty"`
+	DisableCache     bool                  `json:"disable_cache,omitempty"`
+	DisableExpire    bool                  `json:"disable_expire,omitempty"`
+	IndependentCache bool                  `json:"independent_cache,omitempty"`
+	ClientSubnet     *badoption.Prefixable `json:"client_subnet,omitempty"`
 }
 
 type DNSFakeIPOptions struct {

+ 17 - 15
option/experimental.go

@@ -1,5 +1,7 @@
 package option
 
+import "github.com/sagernet/sing/common/json/badoption"
+
 type ExperimentalOptions struct {
 	CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
 	ClashAPI  *ClashAPIOptions  `json:"clash_api,omitempty"`
@@ -8,24 +10,24 @@ type ExperimentalOptions struct {
 }
 
 type CacheFileOptions struct {
-	Enabled     bool     `json:"enabled,omitempty"`
-	Path        string   `json:"path,omitempty"`
-	CacheID     string   `json:"cache_id,omitempty"`
-	StoreFakeIP bool     `json:"store_fakeip,omitempty"`
-	StoreRDRC   bool     `json:"store_rdrc,omitempty"`
-	RDRCTimeout Duration `json:"rdrc_timeout,omitempty"`
+	Enabled     bool               `json:"enabled,omitempty"`
+	Path        string             `json:"path,omitempty"`
+	CacheID     string             `json:"cache_id,omitempty"`
+	StoreFakeIP bool               `json:"store_fakeip,omitempty"`
+	StoreRDRC   bool               `json:"store_rdrc,omitempty"`
+	RDRCTimeout badoption.Duration `json:"rdrc_timeout,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"`
-	Secret                           string           `json:"secret,omitempty"`
-	DefaultMode                      string           `json:"default_mode,omitempty"`
-	ModeList                         []string         `json:"-"`
-	AccessControlAllowOrigin         Listable[string] `json:"access_control_allow_origin,omitempty"`
-	AccessControlAllowPrivateNetwork bool             `json:"access_control_allow_private_network,omitempty"`
+	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"`
+	Secret                           string                     `json:"secret,omitempty"`
+	DefaultMode                      string                     `json:"default_mode,omitempty"`
+	ModeList                         []string                   `json:"-"`
+	AccessControlAllowOrigin         badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
+	AccessControlAllowPrivateNetwork bool                       `json:"access_control_allow_private_network,omitempty"`
 
 	// Deprecated: migrated to global cache file
 	CacheFile string `json:"cache_file,omitempty"`

+ 8 - 6
option/group.go

@@ -1,5 +1,7 @@
 package option
 
+import "github.com/sagernet/sing/common/json/badoption"
+
 type SelectorOutboundOptions struct {
 	Outbounds                 []string `json:"outbounds"`
 	Default                   string   `json:"default,omitempty"`
@@ -7,10 +9,10 @@ type SelectorOutboundOptions struct {
 }
 
 type URLTestOutboundOptions struct {
-	Outbounds                 []string `json:"outbounds"`
-	URL                       string   `json:"url,omitempty"`
-	Interval                  Duration `json:"interval,omitempty"`
-	Tolerance                 uint16   `json:"tolerance,omitempty"`
-	IdleTimeout               Duration `json:"idle_timeout,omitempty"`
-	InterruptExistConnections bool     `json:"interrupt_exist_connections,omitempty"`
+	Outbounds                 []string           `json:"outbounds"`
+	URL                       string             `json:"url,omitempty"`
+	Interval                  badoption.Duration `json:"interval,omitempty"`
+	Tolerance                 uint16             `json:"tolerance,omitempty"`
+	IdleTimeout               badoption.Duration `json:"idle_timeout,omitempty"`
+	InterruptExistConnections bool               `json:"interrupt_exist_connections,omitempty"`
 }

+ 18 - 17
option/inbound.go

@@ -7,6 +7,7 @@ import (
 	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"
 	"github.com/sagernet/sing/service"
 )
 
@@ -49,24 +50,24 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
 
 // Deprecated: Use rule action instead
 type InboundOptions struct {
-	SniffEnabled              bool           `json:"sniff,omitempty"`
-	SniffOverrideDestination  bool           `json:"sniff_override_destination,omitempty"`
-	SniffTimeout              Duration       `json:"sniff_timeout,omitempty"`
-	DomainStrategy            DomainStrategy `json:"domain_strategy,omitempty"`
-	UDPDisableDomainUnmapping bool           `json:"udp_disable_domain_unmapping,omitempty"`
-	Detour                    string         `json:"detour,omitempty"`
+	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"`
+	Detour                    string             `json:"detour,omitempty"`
 }
 
 type ListenOptions struct {
-	Listen               *ListenAddress   `json:"listen,omitempty"`
-	ListenPort           uint16           `json:"listen_port,omitempty"`
-	TCPKeepAlive         Duration         `json:"tcp_keep_alive,omitempty"`
-	TCPKeepAliveInterval Duration         `json:"tcp_keep_alive_interval,omitempty"`
-	TCPFastOpen          bool             `json:"tcp_fast_open,omitempty"`
-	TCPMultiPath         bool             `json:"tcp_multi_path,omitempty"`
-	UDPFragment          *bool            `json:"udp_fragment,omitempty"`
-	UDPFragmentDefault   bool             `json:"-"`
-	UDPTimeout           UDPTimeoutCompat `json:"udp_timeout,omitempty"`
+	Listen               *badoption.Addr    `json:"listen,omitempty"`
+	ListenPort           uint16             `json:"listen_port,omitempty"`
+	TCPKeepAlive         badoption.Duration `json:"tcp_keep_alive,omitempty"`
+	TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"`
+	TCPFastOpen          bool               `json:"tcp_fast_open,omitempty"`
+	TCPMultiPath         bool               `json:"tcp_multi_path,omitempty"`
+	UDPFragment          *bool              `json:"udp_fragment,omitempty"`
+	UDPFragmentDefault   bool               `json:"-"`
+	UDPTimeout           UDPTimeoutCompat   `json:"udp_timeout,omitempty"`
 
 	// Deprecated: removed
 	ProxyProtocol bool `json:"proxy_protocol,omitempty"`
@@ -75,7 +76,7 @@ type ListenOptions struct {
 	InboundOptions
 }
 
-type UDPTimeoutCompat Duration
+type UDPTimeoutCompat badoption.Duration
 
 func (c UDPTimeoutCompat) MarshalJSON() ([]byte, error) {
 	return json.Marshal((time.Duration)(c).String())
@@ -88,7 +89,7 @@ func (c *UDPTimeoutCompat) UnmarshalJSON(data []byte) error {
 		*c = UDPTimeoutCompat(time.Second * time.Duration(valueNumber))
 		return nil
 	}
-	return json.Unmarshal(data, (*Duration)(c))
+	return json.Unmarshal(data, (*badoption.Duration)(c))
 }
 
 type ListenOptionsWrapper interface {

+ 5 - 3
option/ntp.go

@@ -1,9 +1,11 @@
 package option
 
+import "github.com/sagernet/sing/common/json/badoption"
+
 type NTPOptions struct {
-	Enabled       bool     `json:"enabled,omitempty"`
-	Interval      Duration `json:"interval,omitempty"`
-	WriteToSystem bool     `json:"write_to_system,omitempty"`
+	Enabled       bool               `json:"enabled,omitempty"`
+	Interval      badoption.Duration `json:"interval,omitempty"`
+	WriteToSystem bool               `json:"write_to_system,omitempty"`
 	ServerOptions
 	DialerOptions
 }

+ 16 - 15
option/outbound.go

@@ -8,6 +8,7 @@ import (
 	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"
 )
@@ -64,21 +65,21 @@ type DialerOptionsWrapper interface {
 }
 
 type DialerOptions struct {
-	Detour              string         `json:"detour,omitempty"`
-	BindInterface       string         `json:"bind_interface,omitempty"`
-	Inet4BindAddress    *ListenAddress `json:"inet4_bind_address,omitempty"`
-	Inet6BindAddress    *ListenAddress `json:"inet6_bind_address,omitempty"`
-	ProtectPath         string         `json:"protect_path,omitempty"`
-	RoutingMark         uint32         `json:"routing_mark,omitempty"`
-	ReuseAddr           bool           `json:"reuse_addr,omitempty"`
-	ConnectTimeout      Duration       `json:"connect_timeout,omitempty"`
-	TCPFastOpen         bool           `json:"tcp_fast_open,omitempty"`
-	TCPMultiPath        bool           `json:"tcp_multi_path,omitempty"`
-	UDPFragment         *bool          `json:"udp_fragment,omitempty"`
-	UDPFragmentDefault  bool           `json:"-"`
-	DomainStrategy      DomainStrategy `json:"domain_strategy,omitempty"`
-	FallbackDelay       Duration       `json:"fallback_delay,omitempty"`
-	IsWireGuardListener bool           `json:"-"`
+	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"`
+	ProtectPath         string             `json:"protect_path,omitempty"`
+	RoutingMark         uint32             `json:"routing_mark,omitempty"`
+	ReuseAddr           bool               `json:"reuse_addr,omitempty"`
+	ConnectTimeout      badoption.Duration `json:"connect_timeout,omitempty"`
+	TCPFastOpen         bool               `json:"tcp_fast_open,omitempty"`
+	TCPMultiPath        bool               `json:"tcp_multi_path,omitempty"`
+	UDPFragment         *bool              `json:"udp_fragment,omitempty"`
+	UDPFragmentDefault  bool               `json:"-"`
+	DomainStrategy      DomainStrategy     `json:"domain_strategy,omitempty"`
+	FallbackDelay       badoption.Duration `json:"fallback_delay,omitempty"`
+	IsWireGuardListener bool               `json:"-"`
 }
 
 func (o *DialerOptions) TakeDialerOptions() DialerOptions {

+ 4 - 3
option/platform.go

@@ -3,6 +3,7 @@ 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 {
@@ -12,10 +13,10 @@ type OnDemandOptions struct {
 
 type OnDemandRule struct {
 	Action                *OnDemandRuleAction        `json:"action,omitempty"`
-	DNSSearchDomainMatch  Listable[string]           `json:"dns_search_domain_match,omitempty"`
-	DNSServerAddressMatch Listable[string]           `json:"dns_server_address_match,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             Listable[string]           `json:"ssid_match,omitempty"`
+	SSIDMatch             badoption.Listable[string] `json:"ssid_match,omitempty"`
 	ProbeURL              string                     `json:"probe_url,omitempty"`
 }
 

+ 34 - 33
option/rule.go

@@ -8,6 +8,7 @@ import (
 	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 _Rule struct {
@@ -66,39 +67,39 @@ func (r Rule) IsValid() bool {
 }
 
 type RawDefaultRule struct {
-	Inbound                  Listable[string] `json:"inbound,omitempty"`
-	IPVersion                int              `json:"ip_version,omitempty"`
-	Network                  Listable[string] `json:"network,omitempty"`
-	AuthUser                 Listable[string] `json:"auth_user,omitempty"`
-	Protocol                 Listable[string] `json:"protocol,omitempty"`
-	Client                   Listable[string] `json:"client,omitempty"`
-	Domain                   Listable[string] `json:"domain,omitempty"`
-	DomainSuffix             Listable[string] `json:"domain_suffix,omitempty"`
-	DomainKeyword            Listable[string] `json:"domain_keyword,omitempty"`
-	DomainRegex              Listable[string] `json:"domain_regex,omitempty"`
-	Geosite                  Listable[string] `json:"geosite,omitempty"`
-	SourceGeoIP              Listable[string] `json:"source_geoip,omitempty"`
-	GeoIP                    Listable[string] `json:"geoip,omitempty"`
-	SourceIPCIDR             Listable[string] `json:"source_ip_cidr,omitempty"`
-	SourceIPIsPrivate        bool             `json:"source_ip_is_private,omitempty"`
-	IPCIDR                   Listable[string] `json:"ip_cidr,omitempty"`
-	IPIsPrivate              bool             `json:"ip_is_private,omitempty"`
-	SourcePort               Listable[uint16] `json:"source_port,omitempty"`
-	SourcePortRange          Listable[string] `json:"source_port_range,omitempty"`
-	Port                     Listable[uint16] `json:"port,omitempty"`
-	PortRange                Listable[string] `json:"port_range,omitempty"`
-	ProcessName              Listable[string] `json:"process_name,omitempty"`
-	ProcessPath              Listable[string] `json:"process_path,omitempty"`
-	ProcessPathRegex         Listable[string] `json:"process_path_regex,omitempty"`
-	PackageName              Listable[string] `json:"package_name,omitempty"`
-	User                     Listable[string] `json:"user,omitempty"`
-	UserID                   Listable[int32]  `json:"user_id,omitempty"`
-	ClashMode                string           `json:"clash_mode,omitempty"`
-	WIFISSID                 Listable[string] `json:"wifi_ssid,omitempty"`
-	WIFIBSSID                Listable[string] `json:"wifi_bssid,omitempty"`
-	RuleSet                  Listable[string] `json:"rule_set,omitempty"`
-	RuleSetIPCIDRMatchSource bool             `json:"rule_set_ip_cidr_match_source,omitempty"`
-	Invert                   bool             `json:"invert,omitempty"`
+	Inbound                  badoption.Listable[string] `json:"inbound,omitempty"`
+	IPVersion                int                        `json:"ip_version,omitempty"`
+	Network                  badoption.Listable[string] `json:"network,omitempty"`
+	AuthUser                 badoption.Listable[string] `json:"auth_user,omitempty"`
+	Protocol                 badoption.Listable[string] `json:"protocol,omitempty"`
+	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"`
+	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"`
+	IPIsPrivate              bool                       `json:"ip_is_private,omitempty"`
+	SourcePort               badoption.Listable[uint16] `json:"source_port,omitempty"`
+	SourcePortRange          badoption.Listable[string] `json:"source_port_range,omitempty"`
+	Port                     badoption.Listable[uint16] `json:"port,omitempty"`
+	PortRange                badoption.Listable[string] `json:"port_range,omitempty"`
+	ProcessName              badoption.Listable[string] `json:"process_name,omitempty"`
+	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"`
+	User                     badoption.Listable[string] `json:"user,omitempty"`
+	UserID                   badoption.Listable[int32]  `json:"user_id,omitempty"`
+	ClashMode                string                     `json:"clash_mode,omitempty"`
+	WIFISSID                 badoption.Listable[string] `json:"wifi_ssid,omitempty"`
+	WIFIBSSID                badoption.Listable[string] `json:"wifi_bssid,omitempty"`
+	RuleSet                  badoption.Listable[string] `json:"rule_set,omitempty"`
+	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"`

+ 10 - 8
option/rule_action.go

@@ -3,6 +3,7 @@ package option
 import (
 	"context"
 	"fmt"
+	"net/netip"
 	"time"
 
 	C "github.com/sagernet/sing-box/constant"
@@ -11,6 +12,7 @@ import (
 	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 _RuleAction struct {
@@ -177,7 +179,7 @@ type _DNSRouteActionOptions struct {
 	// Deprecated: Use DNSRouteOptionsActionOptions instead.
 	RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
 	// Deprecated: Use DNSRouteOptionsActionOptions instead.
-	ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"`
+	ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
 }
 
 type DNSRouteActionOptions _DNSRouteActionOptions
@@ -197,9 +199,9 @@ func (r *DNSRouteActionOptions) UnmarshalJSONContext(ctx context.Context, data [
 }
 
 type _DNSRouteOptionsActionOptions struct {
-	DisableCache bool        `json:"disable_cache,omitempty"`
-	RewriteTTL   *uint32     `json:"rewrite_ttl,omitempty"`
-	ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"`
+	DisableCache bool                  `json:"disable_cache,omitempty"`
+	RewriteTTL   *uint32               `json:"rewrite_ttl,omitempty"`
+	ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
 }
 
 type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions
@@ -225,10 +227,10 @@ func (d DirectActionOptions) Descriptions() []string {
 		descriptions = append(descriptions, "bind_interface="+d.BindInterface)
 	}
 	if d.Inet4BindAddress != nil {
-		descriptions = append(descriptions, "inet4_bind_address="+d.Inet4BindAddress.Build().String())
+		descriptions = append(descriptions, "inet4_bind_address="+d.Inet4BindAddress.Build(netip.IPv4Unspecified()).String())
 	}
 	if d.Inet6BindAddress != nil {
-		descriptions = append(descriptions, "inet6_bind_address="+d.Inet6BindAddress.Build().String())
+		descriptions = append(descriptions, "inet6_bind_address="+d.Inet6BindAddress.Build(netip.IPv6Unspecified()).String())
 	}
 	if d.RoutingMark != 0 {
 		descriptions = append(descriptions, "routing_mark="+fmt.Sprintf("0x%x", d.RoutingMark))
@@ -294,8 +296,8 @@ func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error {
 }
 
 type RouteActionSniff struct {
-	Sniffer Listable[string] `json:"sniffer,omitempty"`
-	Timeout Duration         `json:"timeout,omitempty"`
+	Sniffer badoption.Listable[string] `json:"sniffer,omitempty"`
+	Timeout badoption.Duration         `json:"timeout,omitempty"`
 }
 
 type RouteActionResolve struct {

+ 36 - 35
option/rule_dns.go

@@ -9,6 +9,7 @@ import (
 	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 _DNSRule struct {
@@ -67,41 +68,41 @@ func (r DNSRule) IsValid() bool {
 }
 
 type RawDefaultDNSRule struct {
-	Inbound                  Listable[string]       `json:"inbound,omitempty"`
-	IPVersion                int                    `json:"ip_version,omitempty"`
-	QueryType                Listable[DNSQueryType] `json:"query_type,omitempty"`
-	Network                  Listable[string]       `json:"network,omitempty"`
-	AuthUser                 Listable[string]       `json:"auth_user,omitempty"`
-	Protocol                 Listable[string]       `json:"protocol,omitempty"`
-	Domain                   Listable[string]       `json:"domain,omitempty"`
-	DomainSuffix             Listable[string]       `json:"domain_suffix,omitempty"`
-	DomainKeyword            Listable[string]       `json:"domain_keyword,omitempty"`
-	DomainRegex              Listable[string]       `json:"domain_regex,omitempty"`
-	Geosite                  Listable[string]       `json:"geosite,omitempty"`
-	SourceGeoIP              Listable[string]       `json:"source_geoip,omitempty"`
-	GeoIP                    Listable[string]       `json:"geoip,omitempty"`
-	IPCIDR                   Listable[string]       `json:"ip_cidr,omitempty"`
-	IPIsPrivate              bool                   `json:"ip_is_private,omitempty"`
-	SourceIPCIDR             Listable[string]       `json:"source_ip_cidr,omitempty"`
-	SourceIPIsPrivate        bool                   `json:"source_ip_is_private,omitempty"`
-	SourcePort               Listable[uint16]       `json:"source_port,omitempty"`
-	SourcePortRange          Listable[string]       `json:"source_port_range,omitempty"`
-	Port                     Listable[uint16]       `json:"port,omitempty"`
-	PortRange                Listable[string]       `json:"port_range,omitempty"`
-	ProcessName              Listable[string]       `json:"process_name,omitempty"`
-	ProcessPath              Listable[string]       `json:"process_path,omitempty"`
-	ProcessPathRegex         Listable[string]       `json:"process_path_regex,omitempty"`
-	PackageName              Listable[string]       `json:"package_name,omitempty"`
-	User                     Listable[string]       `json:"user,omitempty"`
-	UserID                   Listable[int32]        `json:"user_id,omitempty"`
-	Outbound                 Listable[string]       `json:"outbound,omitempty"`
-	ClashMode                string                 `json:"clash_mode,omitempty"`
-	WIFISSID                 Listable[string]       `json:"wifi_ssid,omitempty"`
-	WIFIBSSID                Listable[string]       `json:"wifi_bssid,omitempty"`
-	RuleSet                  Listable[string]       `json:"rule_set,omitempty"`
-	RuleSetIPCIDRMatchSource bool                   `json:"rule_set_ip_cidr_match_source,omitempty"`
-	RuleSetIPCIDRAcceptEmpty bool                   `json:"rule_set_ip_cidr_accept_empty,omitempty"`
-	Invert                   bool                   `json:"invert,omitempty"`
+	Inbound                  badoption.Listable[string]       `json:"inbound,omitempty"`
+	IPVersion                int                              `json:"ip_version,omitempty"`
+	QueryType                badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
+	Network                  badoption.Listable[string]       `json:"network,omitempty"`
+	AuthUser                 badoption.Listable[string]       `json:"auth_user,omitempty"`
+	Protocol                 badoption.Listable[string]       `json:"protocol,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"`
+	IPCIDR                   badoption.Listable[string]       `json:"ip_cidr,omitempty"`
+	IPIsPrivate              bool                             `json:"ip_is_private,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"`
+	SourcePortRange          badoption.Listable[string]       `json:"source_port_range,omitempty"`
+	Port                     badoption.Listable[uint16]       `json:"port,omitempty"`
+	PortRange                badoption.Listable[string]       `json:"port_range,omitempty"`
+	ProcessName              badoption.Listable[string]       `json:"process_name,omitempty"`
+	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"`
+	User                     badoption.Listable[string]       `json:"user,omitempty"`
+	UserID                   badoption.Listable[int32]        `json:"user_id,omitempty"`
+	Outbound                 badoption.Listable[string]       `json:"outbound,omitempty"`
+	ClashMode                string                           `json:"clash_mode,omitempty"`
+	WIFISSID                 badoption.Listable[string]       `json:"wifi_ssid,omitempty"`
+	WIFIBSSID                badoption.Listable[string]       `json:"wifi_bssid,omitempty"`
+	RuleSet                  badoption.Listable[string]       `json:"rule_set,omitempty"`
+	RuleSetIPCIDRMatchSource bool                             `json:"rule_set_ip_cidr_match_source,omitempty"`
+	RuleSetIPCIDRAcceptEmpty bool                             `json:"rule_set_ip_cidr_accept_empty,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"`

+ 25 - 24
option/rule_set.go

@@ -10,6 +10,7 @@ import (
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json"
 	"github.com/sagernet/sing/common/json/badjson"
+	"github.com/sagernet/sing/common/json/badoption"
 
 	"go4.org/netipx"
 )
@@ -84,9 +85,9 @@ type LocalRuleSet struct {
 }
 
 type RemoteRuleSet struct {
-	URL            string   `json:"url"`
-	DownloadDetour string   `json:"download_detour,omitempty"`
-	UpdateInterval Duration `json:"update_interval,omitempty"`
+	URL            string             `json:"url"`
+	DownloadDetour string             `json:"download_detour,omitempty"`
+	UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
 }
 
 type _HeadlessRule struct {
@@ -145,32 +146,32 @@ func (r HeadlessRule) IsValid() bool {
 }
 
 type DefaultHeadlessRule struct {
-	QueryType        Listable[DNSQueryType] `json:"query_type,omitempty"`
-	Network          Listable[string]       `json:"network,omitempty"`
-	Domain           Listable[string]       `json:"domain,omitempty"`
-	DomainSuffix     Listable[string]       `json:"domain_suffix,omitempty"`
-	DomainKeyword    Listable[string]       `json:"domain_keyword,omitempty"`
-	DomainRegex      Listable[string]       `json:"domain_regex,omitempty"`
-	SourceIPCIDR     Listable[string]       `json:"source_ip_cidr,omitempty"`
-	IPCIDR           Listable[string]       `json:"ip_cidr,omitempty"`
-	SourcePort       Listable[uint16]       `json:"source_port,omitempty"`
-	SourcePortRange  Listable[string]       `json:"source_port_range,omitempty"`
-	Port             Listable[uint16]       `json:"port,omitempty"`
-	PortRange        Listable[string]       `json:"port_range,omitempty"`
-	ProcessName      Listable[string]       `json:"process_name,omitempty"`
-	ProcessPath      Listable[string]       `json:"process_path,omitempty"`
-	ProcessPathRegex Listable[string]       `json:"process_path_regex,omitempty"`
-	PackageName      Listable[string]       `json:"package_name,omitempty"`
-	WIFISSID         Listable[string]       `json:"wifi_ssid,omitempty"`
-	WIFIBSSID        Listable[string]       `json:"wifi_bssid,omitempty"`
-	Invert           bool                   `json:"invert,omitempty"`
+	QueryType        badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
+	Network          badoption.Listable[string]       `json:"network,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"`
+	SourceIPCIDR     badoption.Listable[string]       `json:"source_ip_cidr,omitempty"`
+	IPCIDR           badoption.Listable[string]       `json:"ip_cidr,omitempty"`
+	SourcePort       badoption.Listable[uint16]       `json:"source_port,omitempty"`
+	SourcePortRange  badoption.Listable[string]       `json:"source_port_range,omitempty"`
+	Port             badoption.Listable[uint16]       `json:"port,omitempty"`
+	PortRange        badoption.Listable[string]       `json:"port_range,omitempty"`
+	ProcessName      badoption.Listable[string]       `json:"process_name,omitempty"`
+	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"`
+	WIFISSID         badoption.Listable[string]       `json:"wifi_ssid,omitempty"`
+	WIFIBSSID        badoption.Listable[string]       `json:"wifi_bssid,omitempty"`
+	Invert           bool                             `json:"invert,omitempty"`
 
 	DomainMatcher *domain.Matcher `json:"-"`
 	SourceIPSet   *netipx.IPSet   `json:"-"`
 	IPSet         *netipx.IPSet   `json:"-"`
 
-	AdGuardDomain        Listable[string]       `json:"-"`
-	AdGuardDomainMatcher *domain.AdGuardMatcher `json:"-"`
+	AdGuardDomain        badoption.Listable[string] `json:"-"`
+	AdGuardDomainMatcher *domain.AdGuardMatcher     `json:"-"`
 }
 
 func (r DefaultHeadlessRule) IsValid() bool {

+ 6 - 3
option/simple.go

@@ -1,6 +1,9 @@
 package option
 
-import "github.com/sagernet/sing/common/auth"
+import (
+	"github.com/sagernet/sing/common/auth"
+	"github.com/sagernet/sing/common/json/badoption"
+)
 
 type SocksInboundOptions struct {
 	ListenOptions
@@ -30,6 +33,6 @@ type HTTPOutboundOptions struct {
 	Username string `json:"username,omitempty"`
 	Password string `json:"password,omitempty"`
 	OutboundTLSOptionsContainer
-	Path    string     `json:"path,omitempty"`
-	Headers HTTPHeader `json:"headers,omitempty"`
+	Path    string               `json:"path,omitempty"`
+	Headers badoption.HTTPHeader `json:"headers,omitempty"`
 }

+ 10 - 8
option/ssh.go

@@ -1,14 +1,16 @@
 package option
 
+import "github.com/sagernet/sing/common/json/badoption"
+
 type SSHOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	User                 string           `json:"user,omitempty"`
-	Password             string           `json:"password,omitempty"`
-	PrivateKey           Listable[string] `json:"private_key,omitempty"`
-	PrivateKeyPath       string           `json:"private_key_path,omitempty"`
-	PrivateKeyPassphrase string           `json:"private_key_passphrase,omitempty"`
-	HostKey              Listable[string] `json:"host_key,omitempty"`
-	HostKeyAlgorithms    Listable[string] `json:"host_key_algorithms,omitempty"`
-	ClientVersion        string           `json:"client_version,omitempty"`
+	User                 string                     `json:"user,omitempty"`
+	Password             string                     `json:"password,omitempty"`
+	PrivateKey           badoption.Listable[string] `json:"private_key,omitempty"`
+	PrivateKeyPath       string                     `json:"private_key_path,omitempty"`
+	PrivateKeyPassphrase string                     `json:"private_key_passphrase,omitempty"`
+	HostKey              badoption.Listable[string] `json:"host_key,omitempty"`
+	HostKeyAlgorithms    badoption.Listable[string] `json:"host_key_algorithms,omitempty"`
+	ClientVersion        string                     `json:"client_version,omitempty"`
 }

+ 0 - 226
option/time_unit.go

@@ -1,226 +0,0 @@
-package option
-
-import (
-	"errors"
-	"time"
-)
-
-// Copyright 2010 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-const durationDay = 24 * time.Hour
-
-var unitMap = map[string]uint64{
-	"ns": uint64(time.Nanosecond),
-	"us": uint64(time.Microsecond),
-	"µs": uint64(time.Microsecond), // U+00B5 = micro symbol
-	"μs": uint64(time.Microsecond), // U+03BC = Greek letter mu
-	"ms": uint64(time.Millisecond),
-	"s":  uint64(time.Second),
-	"m":  uint64(time.Minute),
-	"h":  uint64(time.Hour),
-	"d":  uint64(durationDay),
-}
-
-// ParseDuration parses a duration string.
-// A duration string is a possibly signed sequence of
-// decimal numbers, each with optional fraction and a unit suffix,
-// such as "300ms", "-1.5h" or "2h45m".
-// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
-func ParseDuration(s string) (Duration, error) {
-	// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
-	orig := s
-	var d uint64
-	neg := false
-
-	// Consume [-+]?
-	if s != "" {
-		c := s[0]
-		if c == '-' || c == '+' {
-			neg = c == '-'
-			s = s[1:]
-		}
-	}
-	// Special case: if all that is left is "0", this is zero.
-	if s == "0" {
-		return 0, nil
-	}
-	if s == "" {
-		return 0, errors.New("time: invalid duration " + quote(orig))
-	}
-	for s != "" {
-		var (
-			v, f  uint64      // integers before, after decimal point
-			scale float64 = 1 // value = v + f/scale
-		)
-
-		var err error
-
-		// The next character must be [0-9.]
-		if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
-			return 0, errors.New("time: invalid duration " + quote(orig))
-		}
-		// Consume [0-9]*
-		pl := len(s)
-		v, s, err = leadingInt(s)
-		if err != nil {
-			return 0, errors.New("time: invalid duration " + quote(orig))
-		}
-		pre := pl != len(s) // whether we consumed anything before a period
-
-		// Consume (\.[0-9]*)?
-		post := false
-		if s != "" && s[0] == '.' {
-			s = s[1:]
-			pl := len(s)
-			f, scale, s = leadingFraction(s)
-			post = pl != len(s)
-		}
-		if !pre && !post {
-			// no digits (e.g. ".s" or "-.s")
-			return 0, errors.New("time: invalid duration " + quote(orig))
-		}
-
-		// Consume unit.
-		i := 0
-		for ; i < len(s); i++ {
-			c := s[i]
-			if c == '.' || '0' <= c && c <= '9' {
-				break
-			}
-		}
-		if i == 0 {
-			return 0, errors.New("time: missing unit in duration " + quote(orig))
-		}
-		u := s[:i]
-		s = s[i:]
-		unit, ok := unitMap[u]
-		if !ok {
-			return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig))
-		}
-		if v > 1<<63/unit {
-			// overflow
-			return 0, errors.New("time: invalid duration " + quote(orig))
-		}
-		v *= unit
-		if f > 0 {
-			// float64 is needed to be nanosecond accurate for fractions of hours.
-			// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
-			v += uint64(float64(f) * (float64(unit) / scale))
-			if v > 1<<63 {
-				// overflow
-				return 0, errors.New("time: invalid duration " + quote(orig))
-			}
-		}
-		d += v
-		if d > 1<<63 {
-			return 0, errors.New("time: invalid duration " + quote(orig))
-		}
-	}
-	if neg {
-		return -Duration(d), nil
-	}
-	if d > 1<<63-1 {
-		return 0, errors.New("time: invalid duration " + quote(orig))
-	}
-	return Duration(d), nil
-}
-
-var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
-
-// leadingInt consumes the leading [0-9]* from s.
-func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) {
-	i := 0
-	for ; i < len(s); i++ {
-		c := s[i]
-		if c < '0' || c > '9' {
-			break
-		}
-		if x > 1<<63/10 {
-			// overflow
-			return 0, rem, errLeadingInt
-		}
-		x = x*10 + uint64(c) - '0'
-		if x > 1<<63 {
-			// overflow
-			return 0, rem, errLeadingInt
-		}
-	}
-	return x, s[i:], nil
-}
-
-// leadingFraction consumes the leading [0-9]* from s.
-// It is used only for fractions, so does not return an error on overflow,
-// it just stops accumulating precision.
-func leadingFraction(s string) (x uint64, scale float64, rem string) {
-	i := 0
-	scale = 1
-	overflow := false
-	for ; i < len(s); i++ {
-		c := s[i]
-		if c < '0' || c > '9' {
-			break
-		}
-		if overflow {
-			continue
-		}
-		if x > (1<<63-1)/10 {
-			// It's possible for overflow to give a positive number, so take care.
-			overflow = true
-			continue
-		}
-		y := x*10 + uint64(c) - '0'
-		if y > 1<<63 {
-			overflow = true
-			continue
-		}
-		x = y
-		scale *= 10
-	}
-	return x, scale, s[i:]
-}
-
-// These are borrowed from unicode/utf8 and strconv and replicate behavior in
-// that package, since we can't take a dependency on either.
-const (
-	lowerhex  = "0123456789abcdef"
-	runeSelf  = 0x80
-	runeError = '\uFFFD'
-)
-
-func quote(s string) string {
-	buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes
-	buf[0] = '"'
-	for i, c := range s {
-		if c >= runeSelf || c < ' ' {
-			// This means you are asking us to parse a time.Duration or
-			// time.Location with unprintable or non-ASCII characters in it.
-			// We don't expect to hit this case very often. We could try to
-			// reproduce strconv.Quote's behavior with full fidelity but
-			// given how rarely we expect to hit these edge cases, speed and
-			// conciseness are better.
-			var width int
-			if c == runeError {
-				width = 1
-				if i+2 < len(s) && s[i:i+3] == string(runeError) {
-					width = 3
-				}
-			} else {
-				width = len(string(c))
-			}
-			for j := 0; j < width; j++ {
-				buf = append(buf, `\x`...)
-				buf = append(buf, lowerhex[s[i+j]>>4])
-				buf = append(buf, lowerhex[s[i+j]&0xF])
-			}
-		} else {
-			if c == '"' || c == '\\' {
-				buf = append(buf, '\\')
-			}
-			buf = append(buf, string(c)...)
-		}
-	}
-	buf = append(buf, '"')
-	return string(buf)
-}

+ 41 - 39
option/tls.go

@@ -1,20 +1,22 @@
 package option
 
+import "github.com/sagernet/sing/common/json/badoption"
+
 type InboundTLSOptions struct {
-	Enabled         bool                   `json:"enabled,omitempty"`
-	ServerName      string                 `json:"server_name,omitempty"`
-	Insecure        bool                   `json:"insecure,omitempty"`
-	ALPN            Listable[string]       `json:"alpn,omitempty"`
-	MinVersion      string                 `json:"min_version,omitempty"`
-	MaxVersion      string                 `json:"max_version,omitempty"`
-	CipherSuites    Listable[string]       `json:"cipher_suites,omitempty"`
-	Certificate     Listable[string]       `json:"certificate,omitempty"`
-	CertificatePath string                 `json:"certificate_path,omitempty"`
-	Key             Listable[string]       `json:"key,omitempty"`
-	KeyPath         string                 `json:"key_path,omitempty"`
-	ACME            *InboundACMEOptions    `json:"acme,omitempty"`
-	ECH             *InboundECHOptions     `json:"ech,omitempty"`
-	Reality         *InboundRealityOptions `json:"reality,omitempty"`
+	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"`
+	CipherSuites    badoption.Listable[string] `json:"cipher_suites,omitempty"`
+	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"`
+	ACME            *InboundACMEOptions        `json:"acme,omitempty"`
+	ECH             *InboundECHOptions         `json:"ech,omitempty"`
+	Reality         *InboundRealityOptions     `json:"reality,omitempty"`
 }
 
 type InboundTLSOptionsContainer struct {
@@ -35,19 +37,19 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
 }
 
 type OutboundTLSOptions struct {
-	Enabled         bool                    `json:"enabled,omitempty"`
-	DisableSNI      bool                    `json:"disable_sni,omitempty"`
-	ServerName      string                  `json:"server_name,omitempty"`
-	Insecure        bool                    `json:"insecure,omitempty"`
-	ALPN            Listable[string]        `json:"alpn,omitempty"`
-	MinVersion      string                  `json:"min_version,omitempty"`
-	MaxVersion      string                  `json:"max_version,omitempty"`
-	CipherSuites    Listable[string]        `json:"cipher_suites,omitempty"`
-	Certificate     Listable[string]        `json:"certificate,omitempty"`
-	CertificatePath string                  `json:"certificate_path,omitempty"`
-	ECH             *OutboundECHOptions     `json:"ech,omitempty"`
-	UTLS            *OutboundUTLSOptions    `json:"utls,omitempty"`
-	Reality         *OutboundRealityOptions `json:"reality,omitempty"`
+	Enabled         bool                       `json:"enabled,omitempty"`
+	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"`
+	CipherSuites    badoption.Listable[string] `json:"cipher_suites,omitempty"`
+	Certificate     badoption.Listable[string] `json:"certificate,omitempty"`
+	CertificatePath string                     `json:"certificate_path,omitempty"`
+	ECH             *OutboundECHOptions        `json:"ech,omitempty"`
+	UTLS            *OutboundUTLSOptions       `json:"utls,omitempty"`
+	Reality         *OutboundRealityOptions    `json:"reality,omitempty"`
 }
 
 type OutboundTLSOptionsContainer struct {
@@ -71,8 +73,8 @@ type InboundRealityOptions struct {
 	Enabled           bool                           `json:"enabled,omitempty"`
 	Handshake         InboundRealityHandshakeOptions `json:"handshake,omitempty"`
 	PrivateKey        string                         `json:"private_key,omitempty"`
-	ShortID           Listable[string]               `json:"short_id,omitempty"`
-	MaxTimeDifference Duration                       `json:"max_time_difference,omitempty"`
+	ShortID           badoption.Listable[string]     `json:"short_id,omitempty"`
+	MaxTimeDifference badoption.Duration             `json:"max_time_difference,omitempty"`
 }
 
 type InboundRealityHandshakeOptions struct {
@@ -81,19 +83,19 @@ type InboundRealityHandshakeOptions struct {
 }
 
 type InboundECHOptions struct {
-	Enabled                     bool             `json:"enabled,omitempty"`
-	PQSignatureSchemesEnabled   bool             `json:"pq_signature_schemes_enabled,omitempty"`
-	DynamicRecordSizingDisabled bool             `json:"dynamic_record_sizing_disabled,omitempty"`
-	Key                         Listable[string] `json:"key,omitempty"`
-	KeyPath                     string           `json:"key_path,omitempty"`
+	Enabled                     bool                       `json:"enabled,omitempty"`
+	PQSignatureSchemesEnabled   bool                       `json:"pq_signature_schemes_enabled,omitempty"`
+	DynamicRecordSizingDisabled bool                       `json:"dynamic_record_sizing_disabled,omitempty"`
+	Key                         badoption.Listable[string] `json:"key,omitempty"`
+	KeyPath                     string                     `json:"key_path,omitempty"`
 }
 
 type OutboundECHOptions struct {
-	Enabled                     bool             `json:"enabled,omitempty"`
-	PQSignatureSchemesEnabled   bool             `json:"pq_signature_schemes_enabled,omitempty"`
-	DynamicRecordSizingDisabled bool             `json:"dynamic_record_sizing_disabled,omitempty"`
-	Config                      Listable[string] `json:"config,omitempty"`
-	ConfigPath                  string           `json:"config_path,omitempty"`
+	Enabled                     bool                       `json:"enabled,omitempty"`
+	PQSignatureSchemesEnabled   bool                       `json:"pq_signature_schemes_enabled,omitempty"`
+	DynamicRecordSizingDisabled bool                       `json:"dynamic_record_sizing_disabled,omitempty"`
+	Config                      badoption.Listable[string] `json:"config,omitempty"`
+	ConfigPath                  string                     `json:"config_path,omitempty"`
 }
 
 type OutboundUTLSOptions struct {

+ 2 - 1
option/tls_acme.go

@@ -5,10 +5,11 @@ import (
 	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 InboundACMEOptions struct {
-	Domain                  Listable[string]            `json:"domain,omitempty"`
+	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"`

+ 15 - 13
option/tuic.go

@@ -1,12 +1,14 @@
 package option
 
+import "github.com/sagernet/sing/common/json/badoption"
+
 type TUICInboundOptions struct {
 	ListenOptions
-	Users             []TUICUser `json:"users,omitempty"`
-	CongestionControl string     `json:"congestion_control,omitempty"`
-	AuthTimeout       Duration   `json:"auth_timeout,omitempty"`
-	ZeroRTTHandshake  bool       `json:"zero_rtt_handshake,omitempty"`
-	Heartbeat         Duration   `json:"heartbeat,omitempty"`
+	Users             []TUICUser         `json:"users,omitempty"`
+	CongestionControl string             `json:"congestion_control,omitempty"`
+	AuthTimeout       badoption.Duration `json:"auth_timeout,omitempty"`
+	ZeroRTTHandshake  bool               `json:"zero_rtt_handshake,omitempty"`
+	Heartbeat         badoption.Duration `json:"heartbeat,omitempty"`
 	InboundTLSOptionsContainer
 }
 
@@ -19,13 +21,13 @@ type TUICUser struct {
 type TUICOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	UUID              string      `json:"uuid,omitempty"`
-	Password          string      `json:"password,omitempty"`
-	CongestionControl string      `json:"congestion_control,omitempty"`
-	UDPRelayMode      string      `json:"udp_relay_mode,omitempty"`
-	UDPOverStream     bool        `json:"udp_over_stream,omitempty"`
-	ZeroRTTHandshake  bool        `json:"zero_rtt_handshake,omitempty"`
-	Heartbeat         Duration    `json:"heartbeat,omitempty"`
-	Network           NetworkList `json:"network,omitempty"`
+	UUID              string             `json:"uuid,omitempty"`
+	Password          string             `json:"password,omitempty"`
+	CongestionControl string             `json:"congestion_control,omitempty"`
+	UDPRelayMode      string             `json:"udp_relay_mode,omitempty"`
+	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
 }

+ 35 - 34
option/tun.go

@@ -7,51 +7,52 @@ import (
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badoption"
 )
 
 type TunInboundOptions struct {
-	InterfaceName          string                 `json:"interface_name,omitempty"`
-	MTU                    uint32                 `json:"mtu,omitempty"`
-	GSO                    bool                   `json:"gso,omitempty"`
-	Address                Listable[netip.Prefix] `json:"address,omitempty"`
-	AutoRoute              bool                   `json:"auto_route,omitempty"`
-	IPRoute2TableIndex     int                    `json:"iproute2_table_index,omitempty"`
-	IPRoute2RuleIndex      int                    `json:"iproute2_rule_index,omitempty"`
-	AutoRedirect           bool                   `json:"auto_redirect,omitempty"`
-	AutoRedirectInputMark  FwMark                 `json:"auto_redirect_input_mark,omitempty"`
-	AutoRedirectOutputMark FwMark                 `json:"auto_redirect_output_mark,omitempty"`
-	StrictRoute            bool                   `json:"strict_route,omitempty"`
-	RouteAddress           Listable[netip.Prefix] `json:"route_address,omitempty"`
-	RouteAddressSet        Listable[string]       `json:"route_address_set,omitempty"`
-	RouteExcludeAddress    Listable[netip.Prefix] `json:"route_exclude_address,omitempty"`
-	RouteExcludeAddressSet Listable[string]       `json:"route_exclude_address_set,omitempty"`
-	IncludeInterface       Listable[string]       `json:"include_interface,omitempty"`
-	ExcludeInterface       Listable[string]       `json:"exclude_interface,omitempty"`
-	IncludeUID             Listable[uint32]       `json:"include_uid,omitempty"`
-	IncludeUIDRange        Listable[string]       `json:"include_uid_range,omitempty"`
-	ExcludeUID             Listable[uint32]       `json:"exclude_uid,omitempty"`
-	ExcludeUIDRange        Listable[string]       `json:"exclude_uid_range,omitempty"`
-	IncludeAndroidUser     Listable[int]          `json:"include_android_user,omitempty"`
-	IncludePackage         Listable[string]       `json:"include_package,omitempty"`
-	ExcludePackage         Listable[string]       `json:"exclude_package,omitempty"`
-	EndpointIndependentNat bool                   `json:"endpoint_independent_nat,omitempty"`
-	UDPTimeout             UDPTimeoutCompat       `json:"udp_timeout,omitempty"`
-	Stack                  string                 `json:"stack,omitempty"`
-	Platform               *TunPlatformOptions    `json:"platform,omitempty"`
+	InterfaceName          string                           `json:"interface_name,omitempty"`
+	MTU                    uint32                           `json:"mtu,omitempty"`
+	GSO                    bool                             `json:"gso,omitempty"`
+	Address                badoption.Listable[netip.Prefix] `json:"address,omitempty"`
+	AutoRoute              bool                             `json:"auto_route,omitempty"`
+	IPRoute2TableIndex     int                              `json:"iproute2_table_index,omitempty"`
+	IPRoute2RuleIndex      int                              `json:"iproute2_rule_index,omitempty"`
+	AutoRedirect           bool                             `json:"auto_redirect,omitempty"`
+	AutoRedirectInputMark  FwMark                           `json:"auto_redirect_input_mark,omitempty"`
+	AutoRedirectOutputMark FwMark                           `json:"auto_redirect_output_mark,omitempty"`
+	StrictRoute            bool                             `json:"strict_route,omitempty"`
+	RouteAddress           badoption.Listable[netip.Prefix] `json:"route_address,omitempty"`
+	RouteAddressSet        badoption.Listable[string]       `json:"route_address_set,omitempty"`
+	RouteExcludeAddress    badoption.Listable[netip.Prefix] `json:"route_exclude_address,omitempty"`
+	RouteExcludeAddressSet badoption.Listable[string]       `json:"route_exclude_address_set,omitempty"`
+	IncludeInterface       badoption.Listable[string]       `json:"include_interface,omitempty"`
+	ExcludeInterface       badoption.Listable[string]       `json:"exclude_interface,omitempty"`
+	IncludeUID             badoption.Listable[uint32]       `json:"include_uid,omitempty"`
+	IncludeUIDRange        badoption.Listable[string]       `json:"include_uid_range,omitempty"`
+	ExcludeUID             badoption.Listable[uint32]       `json:"exclude_uid,omitempty"`
+	ExcludeUIDRange        badoption.Listable[string]       `json:"exclude_uid_range,omitempty"`
+	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"`
+	EndpointIndependentNat bool                             `json:"endpoint_independent_nat,omitempty"`
+	UDPTimeout             UDPTimeoutCompat                 `json:"udp_timeout,omitempty"`
+	Stack                  string                           `json:"stack,omitempty"`
+	Platform               *TunPlatformOptions              `json:"platform,omitempty"`
 	InboundOptions
 
 	// Deprecated: merged to Address
-	Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"`
+	Inet4Address badoption.Listable[netip.Prefix] `json:"inet4_address,omitempty"`
 	// Deprecated: merged to Address
-	Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"`
+	Inet6Address badoption.Listable[netip.Prefix] `json:"inet6_address,omitempty"`
 	// Deprecated: merged to RouteAddress
-	Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"`
+	Inet4RouteAddress badoption.Listable[netip.Prefix] `json:"inet4_route_address,omitempty"`
 	// Deprecated: merged to RouteAddress
-	Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"`
+	Inet6RouteAddress badoption.Listable[netip.Prefix] `json:"inet6_route_address,omitempty"`
 	// Deprecated: merged to RouteExcludeAddress
-	Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"`
+	Inet4RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"`
 	// Deprecated: merged to RouteExcludeAddress
-	Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"`
+	Inet6RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"`
 }
 
 type FwMark uint32

+ 4 - 2
option/tun_platform.go

@@ -1,5 +1,7 @@
 package option
 
+import "github.com/sagernet/sing/common/json/badoption"
+
 type TunPlatformOptions struct {
 	HTTPProxy *HTTPProxyOptions `json:"http_proxy,omitempty"`
 }
@@ -7,6 +9,6 @@ type TunPlatformOptions struct {
 type HTTPProxyOptions struct {
 	Enabled bool `json:"enabled,omitempty"`
 	ServerOptions
-	BypassDomain Listable[string] `json:"bypass_domain,omitempty"`
-	MatchDomain  Listable[string] `json:"match_domain,omitempty"`
+	BypassDomain badoption.Listable[string] `json:"bypass_domain,omitempty"`
+	MatchDomain  badoption.Listable[string] `json:"match_domain,omitempty"`
 }

+ 0 - 132
option/types.go

@@ -1,10 +1,7 @@
 package option
 
 import (
-	"net/http"
-	"net/netip"
 	"strings"
-	"time"
 
 	"github.com/sagernet/sing-dns"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -15,79 +12,6 @@ import (
 	mDNS "github.com/miekg/dns"
 )
 
-type ListenAddress netip.Addr
-
-func NewListenAddress(addr netip.Addr) *ListenAddress {
-	address := ListenAddress(addr)
-	return &address
-}
-
-func (a ListenAddress) MarshalJSON() ([]byte, error) {
-	addr := netip.Addr(a)
-	if !addr.IsValid() {
-		return nil, nil
-	}
-	return json.Marshal(addr.String())
-}
-
-func (a *ListenAddress) UnmarshalJSON(content []byte) error {
-	var value string
-	err := json.Unmarshal(content, &value)
-	if err != nil {
-		return err
-	}
-	addr, err := netip.ParseAddr(value)
-	if err != nil {
-		return err
-	}
-	*a = ListenAddress(addr)
-	return nil
-}
-
-func (a *ListenAddress) Build() netip.Addr {
-	if a == nil {
-		return netip.AddrFrom4([4]byte{127, 0, 0, 1})
-	}
-	return (netip.Addr)(*a)
-}
-
-type AddrPrefix netip.Prefix
-
-func (a AddrPrefix) MarshalJSON() ([]byte, error) {
-	prefix := netip.Prefix(a)
-	if prefix.Bits() == prefix.Addr().BitLen() {
-		return json.Marshal(prefix.Addr().String())
-	} else {
-		return json.Marshal(prefix.String())
-	}
-}
-
-func (a *AddrPrefix) UnmarshalJSON(content []byte) error {
-	var value string
-	err := json.Unmarshal(content, &value)
-	if err != nil {
-		return err
-	}
-	prefix, prefixErr := netip.ParsePrefix(value)
-	if prefixErr == nil {
-		*a = AddrPrefix(prefix)
-		return nil
-	}
-	addr, addrErr := netip.ParseAddr(value)
-	if addrErr == nil {
-		*a = AddrPrefix(netip.PrefixFrom(addr, addr.BitLen()))
-		return nil
-	}
-	return prefixErr
-}
-
-func (a *AddrPrefix) Build() netip.Prefix {
-	if a == nil {
-		return netip.Prefix{}
-	}
-	return netip.Prefix(*a)
-}
-
 type NetworkList string
 
 func (v *NetworkList) UnmarshalJSON(content []byte) error {
@@ -120,30 +44,6 @@ func (v NetworkList) Build() []string {
 	return strings.Split(string(v), "\n")
 }
 
-type Listable[T any] []T
-
-func (l Listable[T]) MarshalJSON() ([]byte, error) {
-	arrayList := []T(l)
-	if len(arrayList) == 1 {
-		return json.Marshal(arrayList[0])
-	}
-	return json.Marshal(arrayList)
-}
-
-func (l *Listable[T]) UnmarshalJSON(content []byte) error {
-	err := json.UnmarshalDisallowUnknownFields(content, (*[]T)(l))
-	if err == nil {
-		return nil
-	}
-	var singleItem T
-	newError := json.UnmarshalDisallowUnknownFields(content, &singleItem)
-	if newError != nil {
-		return E.Errors(err, newError)
-	}
-	*l = []T{singleItem}
-	return nil
-}
-
 type DomainStrategy dns.DomainStrategy
 
 func (s DomainStrategy) String() string {
@@ -206,26 +106,6 @@ func (s *DomainStrategy) UnmarshalJSON(bytes []byte) error {
 	return nil
 }
 
-type Duration time.Duration
-
-func (d Duration) MarshalJSON() ([]byte, error) {
-	return json.Marshal((time.Duration)(d).String())
-}
-
-func (d *Duration) UnmarshalJSON(bytes []byte) error {
-	var value string
-	err := json.Unmarshal(bytes, &value)
-	if err != nil {
-		return err
-	}
-	duration, err := ParseDuration(value)
-	if err != nil {
-		return err
-	}
-	*d = Duration(duration)
-	return nil
-}
-
 type DNSQueryType uint16
 
 func (t DNSQueryType) String() string {
@@ -270,15 +150,3 @@ func DNSQueryTypeToString(queryType uint16) string {
 	}
 	return F.ToString(queryType)
 }
-
-type HTTPHeader map[string]Listable[string]
-
-func (h HTTPHeader) Build() http.Header {
-	header := make(http.Header)
-	for name, values := range h {
-		for _, value := range values {
-			header.Add(name, value)
-		}
-	}
-	return header
-}

+ 19 - 18
option/v2ray_transport.go

@@ -5,6 +5,7 @@ import (
 	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 _V2RayTransportOptions struct {
@@ -67,33 +68,33 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
 }
 
 type V2RayHTTPOptions struct {
-	Host        Listable[string] `json:"host,omitempty"`
-	Path        string           `json:"path,omitempty"`
-	Method      string           `json:"method,omitempty"`
-	Headers     HTTPHeader       `json:"headers,omitempty"`
-	IdleTimeout Duration         `json:"idle_timeout,omitempty"`
-	PingTimeout Duration         `json:"ping_timeout,omitempty"`
+	Host        badoption.Listable[string] `json:"host,omitempty"`
+	Path        string                     `json:"path,omitempty"`
+	Method      string                     `json:"method,omitempty"`
+	Headers     badoption.HTTPHeader       `json:"headers,omitempty"`
+	IdleTimeout badoption.Duration         `json:"idle_timeout,omitempty"`
+	PingTimeout badoption.Duration         `json:"ping_timeout,omitempty"`
 }
 
 type V2RayWebsocketOptions struct {
-	Path                string     `json:"path,omitempty"`
-	Headers             HTTPHeader `json:"headers,omitempty"`
-	MaxEarlyData        uint32     `json:"max_early_data,omitempty"`
-	EarlyDataHeaderName string     `json:"early_data_header_name,omitempty"`
+	Path                string               `json:"path,omitempty"`
+	Headers             badoption.HTTPHeader `json:"headers,omitempty"`
+	MaxEarlyData        uint32               `json:"max_early_data,omitempty"`
+	EarlyDataHeaderName string               `json:"early_data_header_name,omitempty"`
 }
 
 type V2RayQUICOptions struct{}
 
 type V2RayGRPCOptions struct {
-	ServiceName         string   `json:"service_name,omitempty"`
-	IdleTimeout         Duration `json:"idle_timeout,omitempty"`
-	PingTimeout         Duration `json:"ping_timeout,omitempty"`
-	PermitWithoutStream bool     `json:"permit_without_stream,omitempty"`
-	ForceLite           bool     `json:"-"` // for test
+	ServiceName         string             `json:"service_name,omitempty"`
+	IdleTimeout         badoption.Duration `json:"idle_timeout,omitempty"`
+	PingTimeout         badoption.Duration `json:"ping_timeout,omitempty"`
+	PermitWithoutStream bool               `json:"permit_without_stream,omitempty"`
+	ForceLite           bool               `json:"-"` // for test
 }
 
 type V2RayHTTPUpgradeOptions struct {
-	Host    string     `json:"host,omitempty"`
-	Path    string     `json:"path,omitempty"`
-	Headers HTTPHeader `json:"headers,omitempty"`
+	Host    string               `json:"host,omitempty"`
+	Path    string               `json:"path,omitempty"`
+	Headers badoption.HTTPHeader `json:"headers,omitempty"`
 }

+ 15 - 11
option/wireguard.go

@@ -1,15 +1,19 @@
 package option
 
-import "net/netip"
+import (
+	"net/netip"
+
+	"github.com/sagernet/sing/common/json/badoption"
+)
 
 type WireGuardOutboundOptions struct {
 	DialerOptions
-	SystemInterface bool                   `json:"system_interface,omitempty"`
-	GSO             bool                   `json:"gso,omitempty"`
-	InterfaceName   string                 `json:"interface_name,omitempty"`
-	LocalAddress    Listable[netip.Prefix] `json:"local_address"`
-	PrivateKey      string                 `json:"private_key"`
-	Peers           []WireGuardPeer        `json:"peers,omitempty"`
+	SystemInterface bool                             `json:"system_interface,omitempty"`
+	GSO             bool                             `json:"gso,omitempty"`
+	InterfaceName   string                           `json:"interface_name,omitempty"`
+	LocalAddress    badoption.Listable[netip.Prefix] `json:"local_address"`
+	PrivateKey      string                           `json:"private_key"`
+	Peers           []WireGuardPeer                  `json:"peers,omitempty"`
 	ServerOptions
 	PeerPublicKey string      `json:"peer_public_key"`
 	PreSharedKey  string      `json:"pre_shared_key,omitempty"`
@@ -21,8 +25,8 @@ type WireGuardOutboundOptions struct {
 
 type WireGuardPeer struct {
 	ServerOptions
-	PublicKey    string           `json:"public_key,omitempty"`
-	PreSharedKey string           `json:"pre_shared_key,omitempty"`
-	AllowedIPs   Listable[string] `json:"allowed_ips,omitempty"`
-	Reserved     []uint8          `json:"reserved,omitempty"`
+	PublicKey    string                     `json:"public_key,omitempty"`
+	PreSharedKey string                     `json:"pre_shared_key,omitempty"`
+	AllowedIPs   badoption.Listable[string] `json:"allowed_ips,omitempty"`
+	Reserved     []uint8                    `json:"reserved,omitempty"`
 }

+ 2 - 1
protocol/tun/inbound.go

@@ -21,6 +21,7 @@ import (
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/json/badoption"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/ranges"
@@ -257,7 +258,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 	return inbound, nil
 }
 
-func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] {
+func uidToRange(uidList badoption.Listable[uint32]) []ranges.Range[uint32] {
 	return common.Map(uidList, func(uid uint32) ranges.Range[uint32] {
 		return ranges.NewSingle(uid)
 	})

+ 2 - 2
route/router.go

@@ -239,9 +239,9 @@ func NewRouter(
 			}
 			var clientSubnet netip.Prefix
 			if server.ClientSubnet != nil {
-				clientSubnet = server.ClientSubnet.Build()
+				clientSubnet = netip.Prefix(common.PtrValueOrDefault(server.ClientSubnet))
 			} else if dnsOptions.ClientSubnet != nil {
-				clientSubnet = dnsOptions.ClientSubnet.Build()
+				clientSubnet = netip.Prefix(common.PtrValueOrDefault(dnsOptions.ClientSubnet))
 			}
 			if serverProtocol == "" {
 				serverProtocol = "transport"

+ 2 - 2
route/rule/rule_action.go

@@ -88,13 +88,13 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
 			Server:       action.RouteOptions.Server,
 			DisableCache: action.RouteOptions.DisableCache,
 			RewriteTTL:   action.RouteOptions.RewriteTTL,
-			ClientSubnet: action.RouteOptions.ClientSubnet.Build(),
+			ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
 		}
 	case C.RuleActionTypeRouteOptions:
 		return &RuleActionDNSRouteOptions{
 			DisableCache: action.RouteOptionsOptions.DisableCache,
 			RewriteTTL:   action.RouteOptionsOptions.RewriteTTL,
-			ClientSubnet: action.RouteOptionsOptions.ClientSubnet.Build(),
+			ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)),
 		}
 	case C.RuleActionTypeReject:
 		return &RuleActionReject{

+ 2 - 1
transport/sip003/v2ray.go

@@ -12,6 +12,7 @@ import (
 	"github.com/sagernet/sing-box/transport/v2ray"
 	"github.com/sagernet/sing-vmess"
 	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/json/badoption"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
@@ -67,7 +68,7 @@ func newV2RayPlugin(ctx context.Context, pluginOpts Args, router adapter.Router,
 		transportOptions = option.V2RayTransportOptions{
 			Type: C.V2RayTransportTypeWebsocket,
 			WebsocketOptions: option.V2RayWebsocketOptions{
-				Headers: map[string]option.Listable[string]{
+				Headers: map[string]badoption.Listable[string]{
 					"Host": []string{host},
 				},
 				Path: path,