package option import ( "net/http" "strings" Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption" "github.com/sagernet/sing-box/common/xray/utils" C "github.com/sagernet/sing-box/constant" 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 { Type string `json:"type"` HTTPOptions V2RayHTTPOptions `json:"-"` WebsocketOptions V2RayWebsocketOptions `json:"-"` QUICOptions V2RayQUICOptions `json:"-"` GRPCOptions V2RayGRPCOptions `json:"-"` HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"` XHTTPOptions V2RayXHTTPOptions `json:"-"` KCPOptions V2RayKCPOptions `json:"-"` } type V2RayTransportOptions _V2RayTransportOptions func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { var v any switch o.Type { case C.V2RayTransportTypeHTTP: v = o.HTTPOptions case C.V2RayTransportTypeWebsocket: v = o.WebsocketOptions case C.V2RayTransportTypeQUIC: v = o.QUICOptions case C.V2RayTransportTypeGRPC: v = o.GRPCOptions case C.V2RayTransportTypeHTTPUpgrade: v = o.HTTPUpgradeOptions case C.V2RayTransportTypeXHTTP: v = o.XHTTPOptions case C.V2RayTransportTypeKCP: v = o.KCPOptions case "": return nil, E.New("missing transport type") default: return nil, E.New("unknown transport type: " + o.Type) } return badjson.MarshallObjects(_V2RayTransportOptions(o), v) } func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_V2RayTransportOptions)(o)) if err != nil { return err } var v any switch o.Type { case C.V2RayTransportTypeHTTP: v = &o.HTTPOptions case C.V2RayTransportTypeWebsocket: v = &o.WebsocketOptions case C.V2RayTransportTypeQUIC: v = &o.QUICOptions case C.V2RayTransportTypeGRPC: v = &o.GRPCOptions case C.V2RayTransportTypeHTTPUpgrade: v = &o.HTTPUpgradeOptions case C.V2RayTransportTypeXHTTP: v = &o.XHTTPOptions case C.V2RayTransportTypeKCP: v = &o.KCPOptions default: return E.New("unknown transport type: " + o.Type) } err = badjson.UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v) if err != nil { return err } return nil } type V2RayHTTPOptions struct { 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 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 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 badoption.HTTPHeader `json:"headers,omitempty"` } type V2RayXHTTPBaseOptions struct { Host string `json:"host,omitempty"` Path string `json:"path,omitempty"` Headers map[string]string `json:"headers,omitempty"` DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` XPaddingBytes Xbadoption.Range `json:"x_padding_bytes"` NoGRPCHeader bool `json:"no_grpc_header,omitempty"` NoSSEHeader bool `json:"no_sse_header,omitempty"` ScMaxEachPostBytes *Xbadoption.Range `json:"sc_max_each_post_bytes"` ScMinPostsIntervalMs *Xbadoption.Range `json:"sc_min_posts_interval_ms"` ScMaxBufferedPosts int64 `json:"sc_max_buffered_posts,omitempty"` ScStreamUpServerSecs *Xbadoption.Range `json:"sc_stream_up_server_secs"` ServerMaxHeaderBytes int `json:"server_max_header_bytes"` TrustedXForwardedFor badoption.Listable[string] `json:"trusted_x_forwarded_for,omitempty"` Xmux *V2RayXHTTPXmuxOptions `json:"xmux"` XPaddingObfsMode bool `json:"x_padding_obfs_mode,omitempty"` XPaddingKey string `json:"x_padding_key,omitempty"` XPaddingHeader string `json:"x_padding_header,omitempty"` XPaddingPlacement string `json:"x_padding_placement,omitempty"` XPaddingMethod string `json:"x_padding_method,omitempty"` UplinkHTTPMethod string `json:"uplink_http_method,omitempty"` SessionPlacement string `json:"session_placement,omitempty"` SessionKey string `json:"session_key,omitempty"` SeqPlacement string `json:"seq_placement,omitempty"` SeqKey string `json:"seq_key,omitempty"` UplinkDataPlacement string `json:"uplink_data_placement,omitempty"` UplinkDataKey string `json:"uplink_data_key,omitempty"` UplinkChunkSize *Xbadoption.Range `json:"uplink_chunk_size,omitempty"` } type _V2RayXHTTPOptions struct { Mode string `json:"mode"` V2RayXHTTPBaseOptions Download *V2RayXHTTPDownloadOptions `json:"download"` } type V2RayXHTTPOptions _V2RayXHTTPOptions type V2RayXHTTPDownloadOptions struct { V2RayXHTTPBaseOptions ServerOptions OutboundTLSOptionsContainer Detour string `json:"detour,omitempty"` } const ( PlacementQueryInHeader = "queryInHeader" PlacementCookie = "cookie" PlacementHeader = "header" PlacementQuery = "query" PlacementPath = "path" PlacementBody = "body" PlacementAuto = "auto" ) func (c V2RayXHTTPOptions) MarshalJSON() ([]byte, error) { return json.Marshal((*_V2RayXHTTPOptions)(&c)) } func (c *V2RayXHTTPOptions) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_V2RayXHTTPOptions)(c)) if err != nil { return err } switch c.Mode { case "": c.Mode = "auto" case "auto", "packet-up", "stream-up", "stream-one": default: return E.New("unsupported mode: " + c.Mode) } err = checkV2RayXHTTPBaseOptions(c.Mode, &c.V2RayXHTTPBaseOptions) if err != nil { return err } if c.Download != nil { err = checkV2RayXHTTPBaseOptions(c.Mode, &c.Download.V2RayXHTTPBaseOptions) if err != nil { return err } } return nil } func checkV2RayXHTTPBaseOptions(mode string, options *V2RayXHTTPBaseOptions) error { // Priority (client): host > serverName > address for k := range options.Headers { if strings.ToLower(k) == "host" { return E.New(`"headers" can't contain "host"`) } } if options.XPaddingBytes.From <= 0 || options.XPaddingBytes.To <= 0 { return E.New("x_padding_bytes cannot be disabled") } if options.XPaddingKey == "" { options.XPaddingKey = "x_padding" } if options.XPaddingHeader == "" { options.XPaddingHeader = "X-Padding" } switch options.XPaddingPlacement { case "": options.XPaddingPlacement = "queryInHeader" case "cookie", "header", "query", "queryInHeader": default: return E.New("unsupported padding placement: " + options.XPaddingPlacement) } switch options.XPaddingMethod { case "": options.XPaddingMethod = "repeat-x" case "repeat-x", "tokenish": default: return E.New("unsupported padding method: " + options.XPaddingMethod) } switch options.UplinkDataPlacement { case "": options.UplinkDataPlacement = PlacementAuto case PlacementAuto, PlacementBody: case PlacementCookie, PlacementHeader: if mode != "packet-up" { return E.New("UplinkDataPlacement can be " + options.UplinkDataPlacement + " only in packet-up mode") } default: return E.New("unsupported uplink data placement: " + options.UplinkDataPlacement) } if options.UplinkHTTPMethod == "" { options.UplinkHTTPMethod = "POST" } options.UplinkHTTPMethod = strings.ToUpper(options.UplinkHTTPMethod) if options.UplinkHTTPMethod == "GET" && mode != "packet-up" { return E.New("uplink_http_method can be GET only in packet-up mode") } switch options.SessionPlacement { case "": options.SessionPlacement = "path" case "path", "cookie", "header", "query": default: return E.New("unsupported session placement: " + options.SessionPlacement) } switch options.SeqPlacement { case "": options.SeqPlacement = "path" case "path", "cookie", "header", "query": default: return E.New("unsupported seq placement: " + options.SeqPlacement) } if options.SessionPlacement != "path" && options.SessionKey == "" { switch options.SessionPlacement { case "cookie", "query": options.SessionKey = "x_session" case "header": options.SessionKey = "X-Session" } } if options.SeqPlacement != "path" && options.SeqKey == "" { switch options.SeqPlacement { case "cookie", "query": options.SeqKey = "x_seq" case "header": options.SeqKey = "X-Seq" } } if options.UplinkDataPlacement != PlacementBody && options.UplinkDataKey == "" { switch options.UplinkDataPlacement { case PlacementCookie: options.UplinkDataKey = "x_data" case PlacementAuto, PlacementHeader: options.UplinkDataKey = "X-Data" } } if options.ServerMaxHeaderBytes < 0 { return E.New("invalid negative value of maxHeaderBytes") } if options.Xmux == nil { options.Xmux = &V2RayXHTTPXmuxOptions{} options.Xmux.MaxConcurrency.From = 1 options.Xmux.MaxConcurrency.To = 1 options.Xmux.HMaxRequestTimes.From = 600 options.Xmux.HMaxRequestTimes.To = 900 options.Xmux.HMaxReusableSecs.From = 1800 options.Xmux.HMaxReusableSecs.To = 3000 } else if options.Xmux.MaxConnections.To > 0 && options.Xmux.MaxConcurrency.To > 0 { return E.New("max_connections cannot be specified together with max_concurrency") } return nil } func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string { pathAndQuery := strings.SplitN(c.Path, "?", 2) path := pathAndQuery[0] if path == "" || path[0] != '/' { path = "/" + path } if path[len(path)-1] != '/' { path = path + "/" } return path } func (c *V2RayXHTTPBaseOptions) GetNormalizedQuery() string { pathAndQuery := strings.SplitN(c.Path, "?", 2) query := "" if len(pathAndQuery) > 1 { query = pathAndQuery[1] } return query } func (c *V2RayXHTTPBaseOptions) GetRequestHeader() http.Header { header := http.Header{} for k, v := range c.Headers { header.Add(k, v) } utils.TryDefaultHeadersWith(header, "fetch") return header } func (c *V2RayXHTTPBaseOptions) GetNormalizedXPaddingBytes() Xbadoption.Range { if c.XPaddingBytes.To == 0 { return Xbadoption.Range{ From: 100, To: 1000, } } return c.XPaddingBytes } func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkHTTPMethod() string { if c.UplinkHTTPMethod == "" { return "POST" } return c.UplinkHTTPMethod } func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() Xbadoption.Range { if c.ScMaxEachPostBytes == nil { return Xbadoption.Range{ From: 1000000, To: 1000000, } } return *c.ScMaxEachPostBytes } func (c *V2RayXHTTPBaseOptions) GetNormalizedScMinPostsIntervalMs() Xbadoption.Range { if c.ScMinPostsIntervalMs == nil { return Xbadoption.Range{ From: 30, To: 30, } } return *c.ScMinPostsIntervalMs } func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxBufferedPosts() int { if c.ScMaxBufferedPosts == 0 { return 30 } return int(c.ScMaxBufferedPosts) } func (c *V2RayXHTTPBaseOptions) GetNormalizedScStreamUpServerSecs() Xbadoption.Range { if c.ScStreamUpServerSecs == nil { return Xbadoption.Range{ From: 20, To: 80, } } return *c.ScStreamUpServerSecs } func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkChunkSize() Xbadoption.Range { if c.UplinkChunkSize == nil || c.UplinkChunkSize.To == 0 { switch c.UplinkDataPlacement { case PlacementCookie: return Xbadoption.Range{ From: 2 * 1024, // 2 KiB To: 3 * 1024, // 3 KiB } case PlacementHeader: return Xbadoption.Range{ From: 3 * 1000, // 3 KB To: 4 * 1000, // 4 KB } default: return c.GetNormalizedScMaxEachPostBytes() } } else if c.UplinkChunkSize.From < 64 { return Xbadoption.Range{ From: 64, To: max(64, c.UplinkChunkSize.To), } } return *c.UplinkChunkSize } func (c *V2RayXHTTPBaseOptions) GetNormalizedServerMaxHeaderBytes() int { if c.ServerMaxHeaderBytes <= 0 { return 8192 } else { return c.ServerMaxHeaderBytes } } func (c *V2RayXHTTPBaseOptions) GetNormalizedSessionPlacement() string { if c.SessionPlacement == "" { return PlacementPath } return c.SessionPlacement } func (c *V2RayXHTTPBaseOptions) GetNormalizedSeqPlacement() string { if c.SeqPlacement == "" { return PlacementPath } return c.SeqPlacement } func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkDataPlacement() string { if c.UplinkDataPlacement == "" { return PlacementBody } return c.UplinkDataPlacement } func (c *V2RayXHTTPBaseOptions) GetNormalizedSessionKey() string { if c.SessionKey != "" { return c.SessionKey } switch c.GetNormalizedSessionPlacement() { case PlacementHeader: return "X-Session" case PlacementCookie, PlacementQuery: return "x_session" default: return "" } } func (c *V2RayXHTTPBaseOptions) GetNormalizedSeqKey() string { if c.SeqKey != "" { return c.SeqKey } switch c.GetNormalizedSeqPlacement() { case PlacementHeader: return "X-Seq" case PlacementCookie, PlacementQuery: return "x_seq" default: return "" } } type V2RayXHTTPXmuxOptions struct { MaxConcurrency Xbadoption.Range `json:"max_concurrency"` MaxConnections Xbadoption.Range `json:"max_connections"` CMaxReuseTimes Xbadoption.Range `json:"c_max_reuse_times"` HMaxRequestTimes Xbadoption.Range `json:"h_max_request_times"` HMaxReusableSecs Xbadoption.Range `json:"h_max_reusable_secs"` HKeepAlivePeriod int64 `json:"h_keep_alive_period"` } func (m *V2RayXHTTPXmuxOptions) GetNormalizedMaxConcurrency() Xbadoption.Range { return m.MaxConcurrency } func (m *V2RayXHTTPXmuxOptions) GetNormalizedMaxConnections() Xbadoption.Range { return m.MaxConnections } func (m *V2RayXHTTPXmuxOptions) GetNormalizedCMaxReuseTimes() Xbadoption.Range { return m.CMaxReuseTimes } func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxRequestTimes() Xbadoption.Range { return m.HMaxRequestTimes } func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxReusableSecs() Xbadoption.Range { return m.HMaxReusableSecs } type V2RayKCPOptions struct { MTU uint32 `json:"mtu,omitempty"` TTI uint32 `json:"tti,omitempty"` UplinkCapacity uint32 `json:"uplink_capacity,omitempty"` DownlinkCapacity uint32 `json:"downlink_capacity,omitempty"` Congestion bool `json:"congestion,omitempty"` ReadBufferSize uint32 `json:"read_buffer_size,omitempty"` WriteBufferSize uint32 `json:"write_buffer_size,omitempty"` HeaderType string `json:"header_type,omitempty"` Seed string `json:"seed,omitempty"` } func (k *V2RayKCPOptions) GetMTU() uint32 { if k.MTU == 0 { return 1350 } return k.MTU } func (k *V2RayKCPOptions) GetTTI() uint32 { if k.TTI == 0 { return 50 } return k.TTI } func (k *V2RayKCPOptions) GetUplinkCapacity() uint32 { if k.UplinkCapacity == 0 { return 12 } return k.UplinkCapacity } func (k *V2RayKCPOptions) GetDownlinkCapacity() uint32 { if k.DownlinkCapacity == 0 { return 100 } return k.DownlinkCapacity } func (k *V2RayKCPOptions) GetReadBufferSize() uint32 { if k.ReadBufferSize == 0 { return 1 } return k.ReadBufferSize } func (k *V2RayKCPOptions) GetWriteBufferSize() uint32 { if k.WriteBufferSize == 0 { return 1 } return k.WriteBufferSize } func (k *V2RayKCPOptions) GetHeaderType() string { if k.HeaderType == "" { return "none" } return k.HeaderType }