Browse Source

Add http/block outbound & Improve route

世界 3 years ago
parent
commit
4fc4eb09b0

+ 1 - 0
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

+ 0 - 1
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

+ 1 - 0
constant/proxy.go

@@ -2,6 +2,7 @@ package constant
 
 const (
 	TypeDirect      = "direct"
+	TypeBlock       = "block"
 	TypeSocks       = "socks"
 	TypeHTTP        = "http"
 	TypeMixed       = "mixed"

+ 1 - 1
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

+ 2 - 2
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=

+ 0 - 31
option/address.go

@@ -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
-}

+ 17 - 30
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
-}

+ 14 - 0
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

+ 0 - 27
option/listable.go

@@ -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
-}

+ 0 - 39
option/network.go

@@ -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
-}

+ 36 - 13
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"`
 }

+ 8 - 7
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) {

+ 90 - 0
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
+}

+ 52 - 0
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
+}

+ 4 - 0
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:

+ 5 - 2
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)

+ 1 - 1
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

+ 0 - 3
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,

+ 3 - 1
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

+ 57 - 0
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
+}

+ 3 - 1
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)

+ 3 - 3
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
 }
 

+ 93 - 27
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
+	}
+	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
+		}
+	}
+	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
+			}
+		}
 	}
-	for _, outbound := range outbounds {
-		outboundByTag[outbound.Tag()] = outbound
+	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.defaultOutbound = defaultOutbound
+	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
 }

+ 2 - 2
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)
 	}

+ 15 - 10
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,