diff --git a/adapter/outbound.go b/adapter/outbound.go index 0b0ea6ba..8be9f9f8 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -11,6 +11,7 @@ import ( type Outbound interface { Type() string Tag() string + Network() []string N.Dialer NewConnection(ctx context.Context, conn net.Conn, destination M.Socksaddr) error NewPacketConnection(ctx context.Context, conn N.PacketConn, destination M.Socksaddr) error diff --git a/adapter/router.go b/adapter/router.go index 722b7a8d..49019656 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -12,7 +12,6 @@ type Router interface { Start() error Close() error - DefaultOutbound() Outbound Outbound(tag string) (Outbound, bool) RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error diff --git a/constant/proxy.go b/constant/proxy.go index 90b33b4f..e5b0569f 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -2,6 +2,7 @@ package constant const ( TypeDirect = "direct" + TypeBlock = "block" TypeSocks = "socks" TypeHTTP = "http" TypeMixed = "mixed" diff --git a/go.mod b/go.mod index a71d952f..66b0212b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/goccy/go-json v0.9.8 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/oschwald/geoip2-golang v1.7.0 - github.com/sagernet/sing v0.0.0-20220703114149-368e41b67bc4 + github.com/sagernet/sing v0.0.0-20220703122912-677c52f01aba github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.5.0 diff --git a/go.sum b/go.sum index ee39dd12..ad1bd5c6 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagernet/sing v0.0.0-20220703114149-368e41b67bc4 h1:ePp3j7E71+yJfuIxDLzYkngK1AelkP2jITjkMKaHoBs= -github.com/sagernet/sing v0.0.0-20220703114149-368e41b67bc4/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c= +github.com/sagernet/sing v0.0.0-20220703122912-677c52f01aba h1:ffb+Es7ddyDDOYUXKoJz5vpA+9C80GK7f7sjYN9rFvY= +github.com/sagernet/sing v0.0.0-20220703122912-677c52f01aba/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c= github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k= github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= diff --git a/option/address.go b/option/address.go deleted file mode 100644 index cf11ff71..00000000 --- a/option/address.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -import ( - "net/netip" - - "github.com/goccy/go-json" -) - -type ListenAddress netip.Addr - -func (a ListenAddress) MarshalJSON() ([]byte, error) { - addr := netip.Addr(a) - if !addr.IsValid() { - return json.Marshal("") - } - return json.Marshal(addr.String()) -} - -func (a *ListenAddress) UnmarshalJSON(bytes []byte) error { - var value string - err := json.Unmarshal(bytes, &value) - if err != nil { - return err - } - addr, err := netip.ParseAddr(value) - if err != nil { - return err - } - *a = ListenAddress(addr) - return nil -} diff --git a/option/inbound.go b/option/inbound.go index bf0e1785..f897355b 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -2,14 +2,15 @@ package option import ( "github.com/goccy/go-json" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" ) type _Inbound struct { - Tag string `json:"tag,omitempty"` Type string `json:"type"` + Tag string `json:"tag,omitempty"` DirectOptions DirectInboundOptions `json:"-"` SocksOptions SimpleInboundOptions `json:"-"` HTTPOptions SimpleInboundOptions `json:"-"` @@ -22,25 +23,25 @@ type Inbound _Inbound func (h Inbound) Equals(other Inbound) bool { return h.Type == other.Type && h.Tag == other.Tag && - common.Equals(h.DirectOptions, other.DirectOptions) && - common.Equals(h.SocksOptions, other.SocksOptions) && - common.Equals(h.HTTPOptions, other.HTTPOptions) && - common.Equals(h.MixedOptions, other.MixedOptions) && - common.Equals(h.ShadowsocksOptions, other.ShadowsocksOptions) + h.DirectOptions == other.DirectOptions && + h.SocksOptions.Equals(other.SocksOptions) && + h.HTTPOptions.Equals(other.HTTPOptions) && + h.MixedOptions.Equals(other.MixedOptions) && + h.ShadowsocksOptions == other.ShadowsocksOptions } func (h Inbound) MarshalJSON() ([]byte, error) { var v any switch h.Type { - case "direct": + case C.TypeDirect: v = h.DirectOptions - case "socks": + case C.TypeSocks: v = h.SocksOptions - case "http": + case C.TypeHTTP: v = h.HTTPOptions - case "mixed": + case C.TypeMixed: v = h.MixedOptions - case "shadowsocks": + case C.TypeShadowsocks: v = h.ShadowsocksOptions default: return nil, E.New("unknown inbound type: ", h.Type) @@ -55,15 +56,15 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { } var v any switch h.Type { - case "direct": + case C.TypeDirect: v = &h.DirectOptions - case "socks": + case C.TypeSocks: v = &h.SocksOptions - case "http": + case C.TypeHTTP: v = &h.HTTPOptions - case "mixed": + case C.TypeMixed: v = &h.MixedOptions - case "shadowsocks": + case C.TypeShadowsocks: v = &h.ShadowsocksOptions default: return nil @@ -99,23 +100,9 @@ type DirectInboundOptions struct { OverridePort uint16 `json:"override_port,omitempty"` } -func (o DirectInboundOptions) Equals(other DirectInboundOptions) bool { - return o.ListenOptions == other.ListenOptions && - common.ComparableSliceEquals(o.Network, other.Network) && - o.OverrideAddress == other.OverrideAddress && - o.OverridePort == other.OverridePort -} - type ShadowsocksInboundOptions struct { ListenOptions Network NetworkList `json:"network,omitempty"` Method string `json:"method"` Password string `json:"password"` } - -func (o ShadowsocksInboundOptions) Equals(other ShadowsocksInboundOptions) bool { - return o.ListenOptions == other.ListenOptions && - common.ComparableSliceEquals(o.Network, other.Network) && - o.Method == other.Method && - o.Password == other.Password -} diff --git a/option/json.go b/option/json.go index 43e44fb5..a735a305 100644 --- a/option/json.go +++ b/option/json.go @@ -5,6 +5,8 @@ import ( "github.com/goccy/go-json" "github.com/sagernet/sing-box/common/badjson" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" ) func ToMap(v any) (*badjson.JSONObject, error) { @@ -33,6 +35,12 @@ func MergeObjects(objects ...any) (*badjson.JSONObject, error) { } func MarshallObjects(objects ...any) ([]byte, error) { + objects = common.Filter(objects, func(v any) bool { + return v != nil + }) + if len(objects) == 1 { + return json.Marshal(objects[0]) + } content, err := MergeObjects(objects...) if err != nil { return nil, err @@ -53,6 +61,12 @@ func UnmarshallExcluded(inputContent []byte, parentObject any, object any) error for _, key := range parentContent.Keys() { content.Remove(key) } + if object == nil { + if content.IsEmpty() { + return nil + } + return E.New("unexpected key: ", content.Keys()[0]) + } inputContent, err = content.MarshalJSON() if err != nil { return err diff --git a/option/listable.go b/option/listable.go deleted file mode 100644 index e93d6c63..00000000 --- a/option/listable.go +++ /dev/null @@ -1,27 +0,0 @@ -package option - -import "github.com/goccy/go-json" - -type Listable[T comparable] []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(bytes []byte) error { - err := json.Unmarshal(bytes, (*[]T)(l)) - if err == nil { - return nil - } - var singleItem T - err = json.Unmarshal(bytes, &singleItem) - if err != nil { - return err - } - *l = []T{singleItem} - return nil -} diff --git a/option/network.go b/option/network.go deleted file mode 100644 index 8d7181c8..00000000 --- a/option/network.go +++ /dev/null @@ -1,39 +0,0 @@ -package option - -import ( - "github.com/goccy/go-json" - C "github.com/sagernet/sing-box/constant" - E "github.com/sagernet/sing/common/exceptions" -) - -type NetworkList []string - -func (v *NetworkList) UnmarshalJSON(data []byte) error { - var networkList []string - err := json.Unmarshal(data, &networkList) - if err != nil { - var networkItem string - err = json.Unmarshal(data, &networkItem) - if err != nil { - return err - } - networkList = []string{networkItem} - } - for _, networkName := range networkList { - switch networkName { - case C.NetworkTCP, C.NetworkUDP: - break - default: - return E.New("unknown network: " + networkName) - } - } - *v = networkList - return nil -} - -func (v *NetworkList) Build() []string { - if len(*v) == 0 { - return []string{C.NetworkTCP, C.NetworkUDP} - } - return *v -} diff --git a/option/outbound.go b/option/outbound.go index 1a940574..8ffe7b41 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -2,6 +2,7 @@ package option import ( "github.com/goccy/go-json" + C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" ) @@ -11,6 +12,7 @@ type _Outbound struct { Type string `json:"type,omitempty"` DirectOptions DirectOutboundOptions `json:"-"` SocksOptions SocksOutboundOptions `json:"-"` + HTTPOptions HTTPOutboundOptions `json:"-"` ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"` } @@ -19,12 +21,16 @@ type Outbound _Outbound func (h Outbound) MarshalJSON() ([]byte, error) { var v any switch h.Type { - case "direct": + case C.TypeDirect: v = h.DirectOptions - case "socks": + case C.TypeSocks: v = h.SocksOptions - case "shadowsocks": + case C.TypeHTTP: + v = h.HTTPOptions + case C.TypeShadowsocks: v = h.ShadowsocksOptions + case C.TypeBlock: + v = nil default: return nil, E.New("unknown outbound type: ", h.Type) } @@ -38,12 +44,16 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error { } var v any switch h.Type { - case "direct": + case C.TypeDirect: v = &h.DirectOptions - case "socks": + case C.TypeSocks: v = &h.SocksOptions - case "shadowsocks": + case C.TypeHTTP: + v = &h.HTTPOptions + case C.TypeShadowsocks: v = &h.ShadowsocksOptions + case C.TypeBlock: + v = nil default: return nil } @@ -71,10 +81,8 @@ type OverrideStreamOptions struct { UDPOverTCP bool `json:"udp_over_tcp,omitempty"` } -type DirectOutboundOptions struct { - DialerOptions - OverrideAddress string `json:"override_address,omitempty"` - OverridePort uint16 `json:"override_port,omitempty"` +func (o *OverrideStreamOptions) IsValid() bool { + return o != nil && (o.TLS || o.UDPOverTCP) } type ServerOptions struct { @@ -86,10 +94,24 @@ func (o ServerOptions) Build() M.Socksaddr { return M.ParseSocksaddrHostPort(o.Server, o.ServerPort) } +type DirectOutboundOptions struct { + DialerOptions + OverrideAddress string `json:"override_address,omitempty"` + OverridePort uint16 `json:"override_port,omitempty"` +} + type SocksOutboundOptions struct { DialerOptions ServerOptions - Version string `json:"version,omitempty"` + Version string `json:"version,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` +} + +type HTTPOutboundOptions struct { + DialerOptions + ServerOptions Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` } @@ -97,6 +119,7 @@ type SocksOutboundOptions struct { type ShadowsocksOutboundOptions struct { DialerOptions ServerOptions - Method string `json:"method"` - Password string `json:"password"` + Method string `json:"method"` + Password string `json:"password"` + Network NetworkList `json:"network,omitempty"` } diff --git a/option/route.go b/option/route.go index ab4164b9..4a3fdb9f 100644 --- a/option/route.go +++ b/option/route.go @@ -8,8 +8,9 @@ import ( ) type RouteOptions struct { - GeoIP *GeoIPOptions `json:"geoip,omitempty"` - Rules []Rule `json:"rules,omitempty"` + GeoIP *GeoIPOptions `json:"geoip,omitempty"` + Rules []Rule `json:"rules,omitempty"` + DefaultDetour string `json:"default_detour,omitempty"` } func (o RouteOptions) Equals(other RouteOptions) bool { @@ -24,17 +25,17 @@ type GeoIPOptions struct { } type _Rule struct { - Type string `json:"type,omitempty"` - DefaultOptions *DefaultRule `json:"-"` - LogicalOptions *LogicalRule `json:"-"` + Type string `json:"type,omitempty"` + DefaultOptions DefaultRule `json:"-"` + LogicalOptions LogicalRule `json:"-"` } type Rule _Rule func (r Rule) Equals(other Rule) bool { return r.Type == other.Type && - common.PtrEquals(r.DefaultOptions, other.DefaultOptions) && - common.PtrEquals(r.LogicalOptions, other.LogicalOptions) + r.DefaultOptions.Equals(other.DefaultOptions) && + r.LogicalOptions.Equals(other.LogicalOptions) } func (r Rule) MarshalJSON() ([]byte, error) { diff --git a/option/types.go b/option/types.go new file mode 100644 index 00000000..ba5ff5a3 --- /dev/null +++ b/option/types.go @@ -0,0 +1,90 @@ +package option + +import ( + "net/netip" + "strings" + + "github.com/goccy/go-json" + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" +) + +type ListenAddress netip.Addr + +func (a ListenAddress) MarshalJSON() ([]byte, error) { + addr := netip.Addr(a) + if !addr.IsValid() { + return json.Marshal("") + } + 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 +} + +type NetworkList string + +func (v *NetworkList) UnmarshalJSON(content []byte) error { + var networkList []string + err := json.Unmarshal(content, &networkList) + if err != nil { + var networkItem string + err = json.Unmarshal(content, &networkItem) + if err != nil { + return err + } + networkList = []string{networkItem} + } + for _, networkName := range networkList { + switch networkName { + case C.NetworkTCP, C.NetworkUDP: + break + default: + return E.New("unknown network: " + networkName) + } + } + *v = NetworkList(strings.Join(networkList, "\n")) + return nil +} + +func (v NetworkList) Build() []string { + if v == "" { + return []string{C.NetworkTCP, C.NetworkUDP} + } + return strings.Split(string(v), "\n") +} + +type Listable[T comparable] []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.Unmarshal(content, (*[]T)(l)) + if err == nil { + return nil + } + var singleItem T + err = json.Unmarshal(content, &singleItem) + if err != nil { + return err + } + *l = []T{singleItem} + return nil +} diff --git a/outbound/block.go b/outbound/block.go new file mode 100644 index 00000000..dc3cad53 --- /dev/null +++ b/outbound/block.go @@ -0,0 +1,52 @@ +package outbound + +import ( + "context" + "io" + "net" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.Outbound = (*Block)(nil) + +type Block struct { + myOutboundAdapter +} + +func NewBlock(logger log.Logger, tag string) *Block { + return &Block{ + myOutboundAdapter{ + protocol: C.TypeBlock, + logger: logger, + tag: tag, + network: []string{C.NetworkTCP, C.NetworkUDP}, + }, + } +} + +func (h *Block) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + h.logger.WithContext(ctx).Info("blocked connection to ", destination) + return nil, io.EOF +} + +func (h *Block) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + h.logger.WithContext(ctx).Info("blocked packet connection to ", destination) + return nil, io.EOF +} + +func (h *Block) NewConnection(ctx context.Context, conn net.Conn, destination M.Socksaddr) error { + conn.Close() + h.logger.WithContext(ctx).Info("blocked connection to ", destination) + return nil +} + +func (h *Block) NewPacketConnection(ctx context.Context, conn N.PacketConn, destination M.Socksaddr) error { + conn.Close() + h.logger.WithContext(ctx).Info("blocked packet connection to ", destination) + return nil +} diff --git a/outbound/builder.go b/outbound/builder.go index 6f45be65..e5817aa3 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -24,8 +24,12 @@ func New(router adapter.Router, logger log.Logger, index int, options option.Out switch options.Type { case C.TypeDirect: return NewDirect(router, outboundLogger, options.Tag, options.DirectOptions), nil + case C.TypeBlock: + return NewBlock(outboundLogger, options.Tag), nil case C.TypeSocks: return NewSocks(router, outboundLogger, options.Tag, options.SocksOptions) + case C.TypeHTTP: + return NewHTTP(router, outboundLogger, options.Tag, options.HTTPOptions), nil case C.TypeShadowsocks: return NewShadowsocks(router, outboundLogger, options.Tag, options.ShadowsocksOptions) default: diff --git a/outbound/default.go b/outbound/default.go index ffd751d3..7acd3ef7 100644 --- a/outbound/default.go +++ b/outbound/default.go @@ -11,14 +11,13 @@ import ( "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" ) type myOutboundAdapter struct { protocol string logger log.Logger tag string - dialer N.Dialer + network []string } func (a *myOutboundAdapter) Type() string { @@ -29,6 +28,10 @@ func (a *myOutboundAdapter) Tag() string { return a.tag } +func (a *myOutboundAdapter) Network() []string { + return a.network +} + func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error { _payload := buf.StackNew() payload := common.Dup(_payload) diff --git a/outbound/dialer/dialer.go b/outbound/dialer/dialer.go index b71c62dc..0b2fa7b1 100644 --- a/outbound/dialer/dialer.go +++ b/outbound/dialer/dialer.go @@ -14,7 +14,7 @@ func New(router adapter.Router, options option.DialerOptions) N.Dialer { } else { dialer = newDetour(router, options) } - if options.OverrideOptions != nil { + if options.OverrideOptions.IsValid() { dialer = newOverride(dialer, common.PtrValueOrDefault(options.OverrideOptions)) } return dialer diff --git a/outbound/dialer/override.go b/outbound/dialer/override.go index bc38f283..053bddfc 100644 --- a/outbound/dialer/override.go +++ b/outbound/dialer/override.go @@ -22,9 +22,6 @@ type overrideDialer struct { } func newOverride(upstream N.Dialer, options option.OverrideStreamOptions) N.Dialer { - if !options.TLS && !options.UDPOverTCP { - return upstream - } return &overrideDialer{ upstream, options.TLS, diff --git a/outbound/direct.go b/outbound/direct.go index de532d71..d2c58572 100644 --- a/outbound/direct.go +++ b/outbound/direct.go @@ -18,6 +18,7 @@ var _ adapter.Outbound = (*Direct)(nil) type Direct struct { myOutboundAdapter + dialer N.Dialer overrideOption int overrideDestination M.Socksaddr } @@ -28,8 +29,9 @@ func NewDirect(router adapter.Router, logger log.Logger, tag string, options opt protocol: C.TypeDirect, logger: logger, tag: tag, - dialer: dialer.New(router, options.DialerOptions), + network: []string{C.NetworkTCP, C.NetworkUDP}, }, + dialer: dialer.New(router, options.DialerOptions), } if options.OverrideAddress != "" && options.OverridePort != 0 { outbound.overrideOption = 1 diff --git a/outbound/http.go b/outbound/http.go new file mode 100644 index 00000000..23ad9383 --- /dev/null +++ b/outbound/http.go @@ -0,0 +1,57 @@ +package outbound + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/outbound/dialer" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/http" +) + +var _ adapter.Outbound = (*HTTP)(nil) + +type HTTP struct { + myOutboundAdapter + client *http.Client +} + +func NewHTTP(router adapter.Router, logger log.Logger, tag string, options option.HTTPOutboundOptions) *HTTP { + return &HTTP{ + myOutboundAdapter{ + protocol: C.TypeHTTP, + logger: logger, + tag: tag, + network: []string{C.NetworkTCP}, + }, + http.NewClient(dialer.New(router, options.DialerOptions), M.ParseSocksaddrHostPort(options.Server, options.ServerPort), options.Username, options.Password), + } +} + +func (h *HTTP) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + h.logger.WithContext(ctx).Info("outbound connection to ", destination) + return h.client.DialContext(ctx, network, destination) +} + +func (h *HTTP) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, destination M.Socksaddr) error { + outConn, err := h.DialContext(ctx, C.NetworkTCP, destination) + if err != nil { + return err + } + return bufio.CopyConn(ctx, conn, outConn) +} + +func (h *HTTP) NewPacketConnection(ctx context.Context, conn N.PacketConn, destination M.Socksaddr) error { + return os.ErrInvalid +} diff --git a/outbound/shadowsocks.go b/outbound/shadowsocks.go index f210f3c1..2163cb0c 100644 --- a/outbound/shadowsocks.go +++ b/outbound/shadowsocks.go @@ -21,6 +21,7 @@ var _ adapter.Outbound = (*Shadowsocks)(nil) type Shadowsocks struct { myOutboundAdapter + dialer N.Dialer method shadowsocks.Method serverAddr M.Socksaddr } @@ -31,8 +32,9 @@ func NewShadowsocks(router adapter.Router, logger log.Logger, tag string, option protocol: C.TypeDirect, logger: logger, tag: tag, - dialer: dialer.New(router, options.DialerOptions), + network: options.Network.Build(), }, + dialer: dialer.New(router, options.DialerOptions), } var err error outbound.method, err = shadowimpl.FetchMethod(options.Method, options.Password) diff --git a/outbound/socks.go b/outbound/socks.go index 50b68f73..c6972f8a 100644 --- a/outbound/socks.go +++ b/outbound/socks.go @@ -23,7 +23,7 @@ type Socks struct { } func NewSocks(router adapter.Router, logger log.Logger, tag string, options option.SocksOutboundOptions) (*Socks, error) { - dialer := dialer.New(router, options.DialerOptions) + detour := dialer.New(router, options.DialerOptions) var version socks.Version var err error if options.Version != "" { @@ -39,9 +39,9 @@ func NewSocks(router adapter.Router, logger log.Logger, tag string, options opti protocol: C.TypeSocks, logger: logger, tag: tag, - dialer: dialer, + network: options.Network.Build(), }, - socks.NewClient(dialer, M.ParseSocksaddrHostPort(options.Server, options.ServerPort), version, options.Username, options.Password), + socks.NewClient(detour, M.ParseSocksaddrHostPort(options.Server, options.ServerPort), version, options.Username, options.Password), }, nil } diff --git a/route/router.go b/route/router.go index 9d700f6e..7af53144 100644 --- a/route/router.go +++ b/route/router.go @@ -16,6 +16,7 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -23,11 +24,15 @@ import ( var _ adapter.Router = (*Router)(nil) type Router struct { - ctx context.Context - logger log.Logger - defaultOutbound adapter.Outbound - outboundByTag map[string]adapter.Outbound - rules []adapter.Rule + ctx context.Context + logger log.Logger + + outboundByTag map[string]adapter.Outbound + rules []adapter.Rule + + defaultDetour string + defaultOutboundForConnection adapter.Outbound + defaultOutboundForPacketConnection adapter.Outbound needGeoDatabase bool geoOptions option.GeoIPOptions @@ -42,6 +47,7 @@ func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptio rules: make([]adapter.Rule, 0, len(options.Rules)), needGeoDatabase: hasGeoRule(options.Rules), geoOptions: common.PtrValueOrDefault(options.GeoIP), + defaultDetour: options.DefaultDetour, } for i, ruleOptions := range options.Rules { rule, err := NewRule(router, logger, ruleOptions) @@ -55,11 +61,12 @@ func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptio func hasGeoRule(rules []option.Rule) bool { for _, rule := range rules { - if rule.DefaultOptions != nil { - if isGeoRule(common.PtrValueOrDefault(rule.DefaultOptions)) { + switch rule.Type { + case C.RuleTypeDefault: + if isGeoRule(rule.DefaultOptions) { return true } - } else if rule.LogicalOptions != nil { + case C.RuleTypeLogical: for _, subRule := range rule.LogicalOptions.Rules { if isGeoRule(subRule) { return true @@ -78,17 +85,73 @@ func notPrivateNode(code string) bool { return code == "private" } -func (r *Router) UpdateOutbounds(outbounds []adapter.Outbound) { - var defaultOutbound adapter.Outbound +func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func() adapter.Outbound) error { outboundByTag := make(map[string]adapter.Outbound) - if len(outbounds) > 0 { - defaultOutbound = outbounds[0] + for _, detour := range outbounds { + outboundByTag[detour.Tag()] = detour } - for _, outbound := range outbounds { - outboundByTag[outbound.Tag()] = outbound + var defaultOutboundForConnection adapter.Outbound + var defaultOutboundForPacketConnection adapter.Outbound + if r.defaultDetour != "" { + detour, loaded := outboundByTag[r.defaultDetour] + if !loaded { + return E.New("default detour not found: ", r.defaultDetour) + } + if common.Contains(detour.Network(), C.NetworkTCP) { + defaultOutboundForConnection = detour + } + if common.Contains(detour.Network(), C.NetworkUDP) { + defaultOutboundForPacketConnection = detour + } } - r.defaultOutbound = defaultOutbound + var index, packetIndex int + if defaultOutboundForConnection == nil { + for i, detour := range outbounds { + if common.Contains(detour.Network(), C.NetworkTCP) { + index = i + defaultOutboundForConnection = detour + break + } + } + } + if defaultOutboundForPacketConnection == nil { + for i, detour := range outbounds { + if common.Contains(detour.Network(), C.NetworkUDP) { + packetIndex = i + defaultOutboundForPacketConnection = detour + break + } + } + } + if defaultOutboundForConnection == nil || defaultOutboundForPacketConnection == nil { + detour := defaultOutbound() + if defaultOutboundForConnection == nil { + defaultOutboundForConnection = detour + } + if defaultOutboundForPacketConnection == nil { + defaultOutboundForPacketConnection = detour + } + } + if defaultOutboundForConnection != defaultOutboundForPacketConnection { + var description string + if defaultOutboundForConnection.Tag() != "" { + description = defaultOutboundForConnection.Tag() + } else { + description = F.ToString(index) + } + var packetDescription string + if defaultOutboundForPacketConnection.Tag() != "" { + packetDescription = defaultOutboundForPacketConnection.Tag() + } else { + packetDescription = F.ToString(packetIndex) + } + r.logger.Info("using ", defaultOutboundForConnection.Type(), "[", description, "] as default outbound for connection") + r.logger.Info("using ", defaultOutboundForPacketConnection.Type(), "[", packetDescription, "] as default outbound for packet connection") + } + r.defaultOutboundForConnection = defaultOutboundForConnection + r.defaultOutboundForPacketConnection = defaultOutboundForPacketConnection r.outboundByTag = outboundByTag + return nil } func (r *Router) Start() error { @@ -158,7 +221,7 @@ func (r *Router) downloadGeoIPDatabase(savePath string) error { } detour = outbound } else { - detour = r.defaultOutbound + detour = r.defaultOutboundForConnection } if parentDir := filepath.Dir(savePath); parentDir != "" { @@ -190,27 +253,30 @@ func (r *Router) downloadGeoIPDatabase(savePath string) error { return err } -func (r *Router) DefaultOutbound() adapter.Outbound { - if r.defaultOutbound == nil { - panic("missing default outbound") - } - return r.defaultOutbound -} - func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { outbound, loaded := r.outboundByTag[tag] return outbound, loaded } func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return r.match(ctx, metadata).NewConnection(ctx, conn, metadata.Destination) + detour := r.match(ctx, metadata, r.defaultOutboundForConnection) + if !common.Contains(detour.Network(), C.NetworkTCP) { + conn.Close() + return E.New("missing supported outbound, closing connection") + } + return detour.NewConnection(ctx, conn, metadata.Destination) } func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return r.match(ctx, metadata).NewPacketConnection(ctx, conn, metadata.Destination) + detour := r.match(ctx, metadata, r.defaultOutboundForPacketConnection) + if !common.Contains(detour.Network(), C.NetworkUDP) { + conn.Close() + return E.New("missing supported outbound, closing packet connection") + } + return detour.NewPacketConnection(ctx, conn, metadata.Destination) } -func (r *Router) match(ctx context.Context, metadata adapter.InboundContext) adapter.Outbound { +func (r *Router) match(ctx context.Context, metadata adapter.InboundContext, defaultOutbound adapter.Outbound) adapter.Outbound { for i, rule := range r.rules { if rule.Match(&metadata) { detour := rule.Outbound() @@ -222,5 +288,5 @@ func (r *Router) match(ctx context.Context, metadata adapter.InboundContext) ada } } r.logger.WithContext(ctx).Info("no match") - return r.defaultOutbound + return defaultOutbound } diff --git a/route/rule.go b/route/rule.go index 83d27a95..225ea210 100644 --- a/route/rule.go +++ b/route/rule.go @@ -24,7 +24,7 @@ func NewRule(router adapter.Router, logger log.Logger, options option.Rule) (ada if options.DefaultOptions.Outbound == "" { return nil, E.New("missing outbound field") } - return NewDefaultRule(router, logger, common.PtrValueOrDefault(options.DefaultOptions)) + return NewDefaultRule(router, logger, options.DefaultOptions) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") @@ -32,7 +32,7 @@ func NewRule(router adapter.Router, logger log.Logger, options option.Rule) (ada if options.LogicalOptions.Outbound == "" { return nil, E.New("missing outbound field") } - return NewLogicalRule(router, logger, common.PtrValueOrDefault(options.LogicalOptions)) + return NewLogicalRule(router, logger, options.LogicalOptions) default: return nil, E.New("unknown rule type: ", options.Type) } diff --git a/service.go b/service.go index 34fefd8f..79665784 100644 --- a/service.go +++ b/service.go @@ -7,7 +7,7 @@ import ( "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - outbound2 "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/route" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" @@ -34,25 +34,30 @@ func NewService(ctx context.Context, options option.Options) (*Service, error) { inbounds := make([]adapter.Inbound, 0, len(options.Inbounds)) outbounds := make([]adapter.Outbound, 0, len(options.Outbounds)) for i, inboundOptions := range options.Inbounds { - var inboundService adapter.Inbound - inboundService, err = inbound.New(ctx, router, logger, i, inboundOptions) + var in adapter.Inbound + in, err = inbound.New(ctx, router, logger, i, inboundOptions) if err != nil { return nil, E.Cause(err, "parse inbound[", i, "]") } - inbounds = append(inbounds, inboundService) + inbounds = append(inbounds, in) } for i, outboundOptions := range options.Outbounds { - var outboundService adapter.Outbound - outboundService, err = outbound2.New(router, logger, i, outboundOptions) + var out adapter.Outbound + out, err = outbound.New(router, logger, i, outboundOptions) if err != nil { return nil, E.Cause(err, "parse outbound[", i, "]") } - outbounds = append(outbounds, outboundService) + outbounds = append(outbounds, out) } - if len(outbounds) == 0 { - outbounds = append(outbounds, outbound2.NewDirect(nil, logger, "direct", option.DirectOutboundOptions{})) + err = router.Initialize(outbounds, func() adapter.Outbound { + out, oErr := outbound.New(router, logger, 0, option.Outbound{Type: "direct", Tag: "default"}) + common.Must(oErr) + outbounds = append(outbounds, out) + return out + }) + if err != nil { + return nil, err } - router.UpdateOutbounds(outbounds) return &Service{ router: router, logger: logger,