Quellcode durchsuchen

Add selector outbound

世界 vor 3 Jahren
Ursprung
Commit
8004ff51f0

+ 1 - 0
adapter/router.go

@@ -16,6 +16,7 @@ import (
 type Router interface {
 type Router interface {
 	Service
 	Service
 
 
+	Outbounds() []Outbound
 	Outbound(tag string) (Outbound, bool)
 	Outbound(tag string) (Outbound, bool)
 	DefaultOutbound(network string) Outbound
 	DefaultOutbound(network string) Outbound
 
 

+ 8 - 2
adapter/service.go

@@ -1,6 +1,12 @@
 package adapter
 package adapter
 
 
-type Service interface {
+import "io"
+
+type Starter interface {
 	Start() error
 	Start() error
-	Close() error
+}
+
+type Service interface {
+	Starter
+	io.Closer
 }
 }

+ 4 - 0
constant/proxy.go

@@ -13,3 +13,7 @@ const (
 	TypeShadowsocks = "shadowsocks"
 	TypeShadowsocks = "shadowsocks"
 	TypeVMess       = "vmess"
 	TypeVMess       = "vmess"
 )
 )
+
+const (
+	TypeSelector = "selector"
+)

+ 20 - 13
experimental/clashapi/configs.go

@@ -18,24 +18,31 @@ func configRouter(logFactory log.Factory) http.Handler {
 }
 }
 
 
 type configSchema struct {
 type configSchema struct {
-	Port        *int    `json:"port"`
-	SocksPort   *int    `json:"socks-port"`
-	RedirPort   *int    `json:"redir-port"`
-	TProxyPort  *int    `json:"tproxy-port"`
-	MixedPort   *int    `json:"mixed-port"`
-	AllowLan    *bool   `json:"allow-lan"`
-	BindAddress *string `json:"bind-address"`
-	Mode        string  `json:"mode"`
-	LogLevel    string  `json:"log-level"`
-	IPv6        *bool   `json:"ipv6"`
-	Tun         any     `json:"tun"`
+	Port        int            `json:"port"`
+	SocksPort   int            `json:"socks-port"`
+	RedirPort   int            `json:"redir-port"`
+	TProxyPort  int            `json:"tproxy-port"`
+	MixedPort   int            `json:"mixed-port"`
+	AllowLan    bool           `json:"allow-lan"`
+	BindAddress string         `json:"bind-address"`
+	Mode        string         `json:"mode"`
+	LogLevel    string         `json:"log-level"`
+	IPv6        bool           `json:"ipv6"`
+	Tun         map[string]any `json:"tun"`
 }
 }
 
 
 func getConfigs(logFactory log.Factory) func(w http.ResponseWriter, r *http.Request) {
 func getConfigs(logFactory log.Factory) func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
+		logLevel := logFactory.Level()
+		if logLevel == log.LevelTrace {
+			logLevel = log.LevelDebug
+		} else if logLevel > log.LevelError {
+			logLevel = log.LevelError
+		}
 		render.JSON(w, r, &configSchema{
 		render.JSON(w, r, &configSchema{
-			Mode:     "Rule",
-			LogLevel: log.FormatLevel(logFactory.Level()),
+			Mode:        "rule",
+			BindAddress: "*",
+			LogLevel:    log.FormatLevel(logLevel),
 		})
 		})
 	}
 	}
 }
 }

+ 33 - 6
experimental/clashapi/provider.go

@@ -4,13 +4,15 @@ import (
 	"context"
 	"context"
 	"net/http"
 	"net/http"
 
 
+	"github.com/sagernet/sing-box/adapter"
+
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/render"
 	"github.com/go-chi/render"
 )
 )
 
 
-func proxyProviderRouter() http.Handler {
+func proxyProviderRouter(server *Server, router adapter.Router) http.Handler {
 	r := chi.NewRouter()
 	r := chi.NewRouter()
-	r.Get("/", getProviders)
+	r.Get("/", getProviders(server, router))
 
 
 	r.Route("/{name}", func(r chi.Router) {
 	r.Route("/{name}", func(r chi.Router) {
 		r.Use(parseProviderName, findProviderByName)
 		r.Use(parseProviderName, findProviderByName)
@@ -21,10 +23,35 @@ func proxyProviderRouter() http.Handler {
 	return r
 	return r
 }
 }
 
 
-func getProviders(w http.ResponseWriter, r *http.Request) {
-	render.JSON(w, r, render.M{
-		"providers": []string{},
-	})
+func getProviders(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var proxies []any
+		proxies = append(proxies, render.M{
+			"history": []*DelayHistory{},
+			"name":    "DIRECT",
+			"type":    "Direct",
+			"udp":     true,
+		})
+		proxies = append(proxies, render.M{
+			"history": []*DelayHistory{},
+			"name":    "REJECT",
+			"type":    "Reject",
+			"udp":     true,
+		})
+		for _, detour := range router.Outbounds() {
+			proxies = append(proxies, proxyInfo(server, detour))
+		}
+		render.JSON(w, r, render.M{
+			"providers": render.M{
+				"default": render.M{
+					"name":        "default",
+					"type":        "Proxy",
+					"proxies":     proxies,
+					"vehicleType": "Compatible",
+				},
+			},
+		})
+	}
 }
 }
 
 
 func getProvider(w http.ResponseWriter, r *http.Request) {
 func getProvider(w http.ResponseWriter, r *http.Request) {

+ 220 - 55
experimental/clashapi/proxies.go

@@ -2,20 +2,33 @@ package clashapi
 
 
 import (
 import (
 	"context"
 	"context"
+	"fmt"
+	"net"
 	"net/http"
 	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/badjson"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing/common"
+	F "github.com/sagernet/sing/common/format"
+	M "github.com/sagernet/sing/common/metadata"
 
 
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/render"
 	"github.com/go-chi/render"
 )
 )
 
 
-func proxyRouter() http.Handler {
+func proxyRouter(server *Server, router adapter.Router) http.Handler {
 	r := chi.NewRouter()
 	r := chi.NewRouter()
-	r.Get("/", getProxies)
+	r.Get("/", getProxies(server, router))
 
 
 	r.Route("/{name}", func(r chi.Router) {
 	r.Route("/{name}", func(r chi.Router) {
-		r.Use(parseProxyName, findProxyByName)
-		r.Get("/", getProxy)
-		r.Get("/delay", getProxyDelay)
+		r.Use(parseProxyName, findProxyByName(router))
+		r.Get("/", getProxy(server))
+		r.Get("/delay", getProxyDelay(server))
 		r.Put("/", updateProxy)
 		r.Put("/", updateProxy)
 	})
 	})
 	return r
 	return r
@@ -29,33 +42,123 @@ func parseProxyName(next http.Handler) http.Handler {
 	})
 	})
 }
 }
 
 
-func findProxyByName(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		/*name := r.Context().Value(CtxKeyProxyName).(string)
-		proxies := tunnel.Proxies()
-		proxy, exist := proxies[name]
-		if !exist {*/
-		render.Status(r, http.StatusNotFound)
-		render.JSON(w, r, ErrNotFound)
-		return
-		//}
+func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			name := r.Context().Value(CtxKeyProxyName).(string)
+			proxy, exist := router.Outbound(name)
+			if !exist {
+				render.Status(r, http.StatusNotFound)
+				render.JSON(w, r, ErrNotFound)
+				return
+			}
+			ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy)
+			next.ServeHTTP(w, r.WithContext(ctx))
+		})
+	}
+}
 
 
-		// ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy)
-		// next.ServeHTTP(w, r.WithContext(ctx))
-	})
+func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
+	var info badjson.JSONObject
+	var clashType string
+	var isSelector bool
+	switch detour.Type() {
+	case C.TypeDirect:
+		clashType = "Direct"
+	case C.TypeBlock:
+		clashType = "Reject"
+	case C.TypeSocks:
+		clashType = "Socks"
+	case C.TypeHTTP:
+		clashType = "Http"
+	case C.TypeShadowsocks:
+		clashType = "Shadowsocks"
+	case C.TypeVMess:
+		clashType = "Vmess"
+	case C.TypeSelector:
+		clashType = "Selector"
+		isSelector = true
+	default:
+		clashType = "Unknown"
+	}
+	info.Put("type", clashType)
+	info.Put("name", detour.Tag())
+	info.Put("udp", common.Contains(detour.Network(), C.NetworkUDP))
+
+	delayHistory, loaded := server.delayHistory[detour.Tag()]
+	if loaded {
+		info.Put("history", []*DelayHistory{delayHistory, delayHistory})
+	} else {
+		info.Put("history", []*DelayHistory{{Time: time.Now()}, {Time: time.Now()}})
+	}
+
+	if isSelector {
+		selector := detour.(*outbound.Selector)
+		info.Put("now", selector.Now())
+		info.Put("all", selector.All())
+	}
+	return &info
 }
 }
 
 
-func getProxies(w http.ResponseWriter, r *http.Request) {
-	// proxies := tunnel.Proxies()
-	render.JSON(w, r, render.M{
-		"proxies": []string{},
-	})
+func getProxies(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var proxyMap badjson.JSONObject
+
+		// fix clash dashboard
+		proxyMap.Put("DIRECT", map[string]any{
+			"type":    "Direct",
+			"name":    "DIRECT",
+			"udp":     true,
+			"history": []*DelayHistory{},
+		})
+		proxyMap.Put("GLOBAL", map[string]any{
+			"type":    "Selector",
+			"name":    "GLOBAL",
+			"udp":     true,
+			"history": []*DelayHistory{},
+			"all":     []string{},
+			"now":     "",
+		})
+		proxyMap.Put("REJECT", map[string]any{
+			"type":    "Reject",
+			"name":    "REJECT",
+			"udp":     true,
+			"history": []*DelayHistory{},
+		})
+
+		outbounds := router.Outbounds()
+		for i, detour := range outbounds {
+			var tag string
+			if detour.Tag() == "" {
+				tag = F.ToString(i)
+			} else {
+				tag = detour.Tag()
+			}
+			proxyMap.Put(tag, proxyInfo(server, detour))
+		}
+		var responseMap badjson.JSONObject
+		responseMap.Put("proxies", &proxyMap)
+		response, err := responseMap.MarshalJSON()
+		if err != nil {
+			render.Status(r, http.StatusInternalServerError)
+			render.JSON(w, r, newError(err.Error()))
+			return
+		}
+		w.Write(response)
+	}
 }
 }
 
 
-func getProxy(w http.ResponseWriter, r *http.Request) {
-	/*	proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
-		render.JSON(w, r, proxy)*/
-	render.Status(r, http.StatusServiceUnavailable)
+func getProxy(server *Server) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
+		response, err := proxyInfo(server, proxy).MarshalJSON()
+		if err != nil {
+			render.Status(r, http.StatusInternalServerError)
+			render.JSON(w, r, newError(err.Error()))
+			return
+		}
+		w.Write(response)
+	}
 }
 }
 
 
 type UpdateProxyRequest struct {
 type UpdateProxyRequest struct {
@@ -63,33 +166,33 @@ type UpdateProxyRequest struct {
 }
 }
 
 
 func updateProxy(w http.ResponseWriter, r *http.Request) {
 func updateProxy(w http.ResponseWriter, r *http.Request) {
-	/*	req := UpdateProxyRequest{}
-		if err := render.DecodeJSON(r.Body, &req); err != nil {
-			render.Status(r, http.StatusBadRequest)
-			render.JSON(w, r, ErrBadRequest)
-			return
-		}
+	req := UpdateProxyRequest{}
+	if err := render.DecodeJSON(r.Body, &req); err != nil {
+		render.Status(r, http.StatusBadRequest)
+		render.JSON(w, r, ErrBadRequest)
+		return
+	}
 
 
-		proxy := r.Context().Value(CtxKeyProxy).(*adapter.Proxy)
-		selector, ok := proxy.ProxyAdapter.(*outboundgroup.Selector)
-		if !ok {
-			render.Status(r, http.StatusBadRequest)
-			render.JSON(w, r, newError("Must be a Selector"))
-			return
-		}
+	proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
+	selector, ok := proxy.(*outbound.Selector)
+	if !ok {
+		render.Status(r, http.StatusBadRequest)
+		render.JSON(w, r, newError("Must be a Selector"))
+		return
+	}
 
 
-		if err := selector.Set(req.Name); err != nil {
-			render.Status(r, http.StatusBadRequest)
-			render.JSON(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error())))
-			return
-		}
+	if !selector.SelectOutbound(req.Name) {
+		render.Status(r, http.StatusBadRequest)
+		render.JSON(w, r, newError(fmt.Sprintf("Selector update error: not found")))
+		return
+	}
 
 
-		cachefile.Cache().SetSelected(proxy.Name(), req.Name)*/
 	render.NoContent(w, r)
 	render.NoContent(w, r)
 }
 }
 
 
-func getProxyDelay(w http.ResponseWriter, r *http.Request) {
-	/*	query := r.URL.Query()
+func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		query := r.URL.Query()
 		url := query.Get("url")
 		url := query.Get("url")
 		timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
 		timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
 		if err != nil {
 		if err != nil {
@@ -98,12 +201,11 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 
 
-		proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
-
+		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
 		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
 		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
 		defer cancel()
 		defer cancel()
 
 
-		delay, err := proxy.URLTest(ctx, url)
+		delay, err := URLTest(ctx, url, proxy)
 		if ctx.Err() != nil {
 		if ctx.Err() != nil {
 			render.Status(r, http.StatusGatewayTimeout)
 			render.Status(r, http.StatusGatewayTimeout)
 			render.JSON(w, r, ErrRequestTimeout)
 			render.JSON(w, r, ErrRequestTimeout)
@@ -115,8 +217,71 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
 			render.JSON(w, r, newError("An error occurred in the delay test"))
 			render.JSON(w, r, newError("An error occurred in the delay test"))
 			return
 			return
 		}
 		}
-	*/
-	render.JSON(w, r, render.M{
-		"delay": 114514,
-	})
+
+		server.delayHistory[proxy.Tag()] = &DelayHistory{
+			Time:  time.Now(),
+			Delay: delay,
+		}
+
+		render.JSON(w, r, render.M{
+			"delay": delay,
+		})
+	}
+}
+
+func URLTest(ctx context.Context, link string, detour adapter.Outbound) (t uint16, err error) {
+	linkURL, err := url.Parse(link)
+	if err != nil {
+		return
+	}
+	hostname := linkURL.Hostname()
+	port := linkURL.Port()
+	if port == "" {
+		switch linkURL.Scheme {
+		case "http":
+			port = "80"
+		case "https":
+			port = "443"
+		}
+	}
+
+	start := time.Now()
+	instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port))
+	if err != nil {
+		return
+	}
+	defer instance.Close()
+
+	req, err := http.NewRequest(http.MethodHead, link, nil)
+	if err != nil {
+		return
+	}
+	req = req.WithContext(ctx)
+
+	transport := &http.Transport{
+		Dial: func(string, string) (net.Conn, error) {
+			return instance, nil
+		},
+		// from http.DefaultTransport
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+	}
+
+	client := http.Client{
+		Transport: transport,
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+	defer client.CloseIdleConnections()
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return
+	}
+	resp.Body.Close()
+	t = uint16(time.Since(start) / time.Millisecond)
+	return
 }
 }

+ 18 - 10
experimental/clashapi/server.go

@@ -31,11 +31,26 @@ type Server struct {
 	logger         log.Logger
 	logger         log.Logger
 	httpServer     *http.Server
 	httpServer     *http.Server
 	trafficManager *trafficontroll.Manager
 	trafficManager *trafficontroll.Manager
+	delayHistory   map[string]*DelayHistory
+}
+
+type DelayHistory struct {
+	Time  time.Time `json:"time"`
+	Delay uint16    `json:"delay"`
 }
 }
 
 
 func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) *Server {
 func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) *Server {
 	trafficManager := trafficontroll.NewManager()
 	trafficManager := trafficontroll.NewManager()
 	chiRouter := chi.NewRouter()
 	chiRouter := chi.NewRouter()
+	server := &Server{
+		logFactory.NewLogger("clash-api"),
+		&http.Server{
+			Addr:    options.ExternalController,
+			Handler: chiRouter,
+		},
+		trafficManager,
+		make(map[string]*DelayHistory),
+	}
 	cors := cors.New(cors.Options{
 	cors := cors.New(cors.Options{
 		AllowedOrigins: []string{"*"},
 		AllowedOrigins: []string{"*"},
 		AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
 		AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
@@ -50,10 +65,10 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
 		r.Get("/traffic", traffic(trafficManager))
 		r.Get("/traffic", traffic(trafficManager))
 		r.Get("/version", version)
 		r.Get("/version", version)
 		r.Mount("/configs", configRouter(logFactory))
 		r.Mount("/configs", configRouter(logFactory))
-		r.Mount("/proxies", proxyRouter())
+		r.Mount("/proxies", proxyRouter(server, router))
 		r.Mount("/rules", ruleRouter(router))
 		r.Mount("/rules", ruleRouter(router))
 		r.Mount("/connections", connectionRouter(trafficManager))
 		r.Mount("/connections", connectionRouter(trafficManager))
-		r.Mount("/providers/proxies", proxyProviderRouter())
+		r.Mount("/providers/proxies", proxyProviderRouter(server, router))
 		r.Mount("/providers/rules", ruleProviderRouter())
 		r.Mount("/providers/rules", ruleProviderRouter())
 		r.Mount("/script", scriptRouter())
 		r.Mount("/script", scriptRouter())
 		r.Mount("/profile", profileRouter())
 		r.Mount("/profile", profileRouter())
@@ -68,14 +83,7 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
 			})
 			})
 		})
 		})
 	}
 	}
-	return &Server{
-		logFactory.NewLogger("clash-api"),
-		&http.Server{
-			Addr:    options.ExternalController,
-			Handler: chiRouter,
-		},
-		trafficManager,
-	}
+	return server
 }
 }
 
 
 func (s *Server) Start() error {
 func (s *Server) Start() error {

+ 1 - 1
option/config.go

@@ -41,7 +41,7 @@ func (o Options) Equals(other Options) bool {
 	return common.ComparablePtrEquals(o.Log, other.Log) &&
 	return common.ComparablePtrEquals(o.Log, other.Log) &&
 		common.PtrEquals(o.DNS, other.DNS) &&
 		common.PtrEquals(o.DNS, other.DNS) &&
 		common.SliceEquals(o.Inbounds, other.Inbounds) &&
 		common.SliceEquals(o.Inbounds, other.Inbounds) &&
-		common.ComparableSliceEquals(o.Outbounds, other.Outbounds) &&
+		common.SliceEquals(o.Outbounds, other.Outbounds) &&
 		common.PtrEquals(o.Route, other.Route) &&
 		common.PtrEquals(o.Route, other.Route) &&
 		common.ComparablePtrEquals(o.Experimental, other.Experimental)
 		common.ComparablePtrEquals(o.Experimental, other.Experimental)
 }
 }

+ 27 - 0
option/outbound.go

@@ -2,6 +2,7 @@ package option
 
 
 import (
 import (
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 
 
@@ -16,10 +17,22 @@ type _Outbound struct {
 	HTTPOptions        HTTPOutboundOptions        `json:"-"`
 	HTTPOptions        HTTPOutboundOptions        `json:"-"`
 	ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"`
 	ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"`
 	VMessOptions       VMessOutboundOptions       `json:"-"`
 	VMessOptions       VMessOutboundOptions       `json:"-"`
+	SelectorOptions    SelectorOutboundOptions    `json:"-"`
 }
 }
 
 
 type Outbound _Outbound
 type Outbound _Outbound
 
 
+func (h Outbound) Equals(other Outbound) bool {
+	return h.Type == other.Type &&
+		h.Tag == other.Tag &&
+		h.DirectOptions == other.DirectOptions &&
+		h.SocksOptions == other.SocksOptions &&
+		h.HTTPOptions == other.HTTPOptions &&
+		h.ShadowsocksOptions == other.ShadowsocksOptions &&
+		h.VMessOptions == other.VMessOptions &&
+		common.Equals(h.SelectorOptions, other.SelectorOptions)
+}
+
 func (h Outbound) MarshalJSON() ([]byte, error) {
 func (h Outbound) MarshalJSON() ([]byte, error) {
 	var v any
 	var v any
 	switch h.Type {
 	switch h.Type {
@@ -35,6 +48,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
 		v = h.ShadowsocksOptions
 		v = h.ShadowsocksOptions
 	case C.TypeVMess:
 	case C.TypeVMess:
 		v = h.VMessOptions
 		v = h.VMessOptions
+	case C.TypeSelector:
+		v = h.SelectorOptions
 	default:
 	default:
 		return nil, E.New("unknown outbound type: ", h.Type)
 		return nil, E.New("unknown outbound type: ", h.Type)
 	}
 	}
@@ -60,6 +75,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.ShadowsocksOptions
 		v = &h.ShadowsocksOptions
 	case C.TypeVMess:
 	case C.TypeVMess:
 		v = &h.VMessOptions
 		v = &h.VMessOptions
+	case C.TypeSelector:
+		v = &h.SelectorOptions
 	default:
 	default:
 		return nil
 		return nil
 	}
 	}
@@ -144,3 +161,13 @@ type VMessOutboundOptions struct {
 	Network             NetworkList         `json:"network,omitempty"`
 	Network             NetworkList         `json:"network,omitempty"`
 	TLSOptions          *OutboundTLSOptions `json:"tls,omitempty"`
 	TLSOptions          *OutboundTLSOptions `json:"tls,omitempty"`
 }
 }
+
+type SelectorOutboundOptions struct {
+	Outbounds []string `json:"outbounds"`
+	Default   string   `json:"default,omitempty"`
+}
+
+func (o SelectorOutboundOptions) Equals(other SelectorOutboundOptions) bool {
+	return common.ComparableSliceEquals(o.Outbounds, other.Outbounds) &&
+		o.Default == other.Default
+}

+ 3 - 1
outbound/builder.go

@@ -10,7 +10,7 @@ import (
 )
 )
 
 
 func New(router adapter.Router, logger log.ContextLogger, options option.Outbound) (adapter.Outbound, error) {
 func New(router adapter.Router, logger log.ContextLogger, options option.Outbound) (adapter.Outbound, error) {
-	if common.IsEmpty(options) {
+	if common.IsEmptyByEquals(options) {
 		return nil, E.New("empty outbound config")
 		return nil, E.New("empty outbound config")
 	}
 	}
 	switch options.Type {
 	switch options.Type {
@@ -26,6 +26,8 @@ func New(router adapter.Router, logger log.ContextLogger, options option.Outboun
 		return NewShadowsocks(router, logger, options.Tag, options.ShadowsocksOptions)
 		return NewShadowsocks(router, logger, options.Tag, options.ShadowsocksOptions)
 	case C.TypeVMess:
 	case C.TypeVMess:
 		return NewVMess(router, logger, options.Tag, options.VMessOptions)
 		return NewVMess(router, logger, options.Tag, options.VMessOptions)
+	case C.TypeSelector:
+		return NewSelector(router, logger, options.Tag, options.SelectorOptions)
 	default:
 	default:
 		return nil, E.New("unknown outbound type: ", options.Type)
 		return nil, E.New("unknown outbound type: ", options.Type)
 	}
 	}

+ 103 - 0
outbound/selector.go

@@ -0,0 +1,103 @@
+package outbound
+
+import (
+	"context"
+	"net"
+
+	"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"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+var _ adapter.Outbound = (*Selector)(nil)
+
+type Selector struct {
+	myOutboundAdapter
+	router     adapter.Router
+	tags       []string
+	defaultTag string
+	outbounds  map[string]adapter.Outbound
+	selected   adapter.Outbound
+}
+
+func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) {
+	outbound := &Selector{
+		myOutboundAdapter: myOutboundAdapter{
+			protocol: C.TypeSelector,
+			logger:   logger,
+			tag:      tag,
+		},
+		router:     router,
+		tags:       options.Outbounds,
+		defaultTag: options.Default,
+		outbounds:  make(map[string]adapter.Outbound),
+	}
+	if len(outbound.tags) == 0 {
+		return nil, E.New("missing tags")
+	}
+	return outbound, nil
+}
+
+func (s *Selector) Network() []string {
+	if s.selected == nil {
+		return []string{C.NetworkTCP, C.NetworkUDP}
+	}
+	return s.selected.Network()
+}
+
+func (s *Selector) Start() error {
+	for i, tag := range s.tags {
+		detour, loaded := s.router.Outbound(tag)
+		if !loaded {
+			return E.New("outbound ", i, " not found: ", tag)
+		}
+		s.outbounds[tag] = detour
+	}
+	if s.defaultTag != "" {
+		detour, loaded := s.outbounds[s.defaultTag]
+		if !loaded {
+			return E.New("default outbound not found: ", s.defaultTag)
+		}
+		s.selected = detour
+	} else {
+		s.selected = s.outbounds[s.tags[0]]
+	}
+	return nil
+}
+
+func (s *Selector) Now() string {
+	return s.selected.Tag()
+}
+
+func (s *Selector) All() []string {
+	return s.tags
+}
+
+func (s *Selector) SelectOutbound(tag string) bool {
+	detour, loaded := s.outbounds[tag]
+	if !loaded {
+		return false
+	}
+	s.selected = detour
+	return true
+}
+
+func (s *Selector) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	return s.selected.DialContext(ctx, network, destination)
+}
+
+func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	return s.selected.ListenPacket(ctx, destination)
+}
+
+func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	return s.selected.NewConnection(ctx, conn, metadata)
+}
+
+func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return s.selected.NewPacketConnection(ctx, conn, metadata)
+}

+ 1 - 1
outbound/shadowsocks.go

@@ -32,7 +32,7 @@ func NewShadowsocks(router adapter.Router, logger log.ContextLogger, tag string,
 	}
 	}
 	return &Shadowsocks{
 	return &Shadowsocks{
 		myOutboundAdapter{
 		myOutboundAdapter{
-			protocol: C.TypeDirect,
+			protocol: C.TypeShadowsocks,
 			logger:   logger,
 			logger:   logger,
 			tag:      tag,
 			tag:      tag,
 			network:  options.Network.Build(),
 			network:  options.Network.Build(),

+ 1 - 1
outbound/vmess.go

@@ -42,7 +42,7 @@ func NewVMess(router adapter.Router, logger log.ContextLogger, tag string, optio
 	}
 	}
 	return &VMess{
 	return &VMess{
 		myOutboundAdapter{
 		myOutboundAdapter{
-			protocol: C.TypeDirect,
+			protocol: C.TypeVMess,
 			logger:   logger,
 			logger:   logger,
 			tag:      tag,
 			tag:      tag,
 			network:  options.Network.Build(),
 			network:  options.Network.Build(),

+ 22 - 0
route/router.go

@@ -42,6 +42,7 @@ type Router struct {
 	logger    log.ContextLogger
 	logger    log.ContextLogger
 	dnsLogger log.ContextLogger
 	dnsLogger log.ContextLogger
 
 
+	outbounds     []adapter.Outbound
 	outboundByTag map[string]adapter.Outbound
 	outboundByTag map[string]adapter.Outbound
 	rules         []adapter.Rule
 	rules         []adapter.Rule
 
 
@@ -267,6 +268,8 @@ func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func()
 		if defaultOutboundForPacketConnection == nil {
 		if defaultOutboundForPacketConnection == nil {
 			defaultOutboundForPacketConnection = detour
 			defaultOutboundForPacketConnection = detour
 		}
 		}
+		outbounds = append(outbounds, detour)
+		outboundByTag[detour.Tag()] = detour
 	}
 	}
 	if defaultOutboundForConnection != defaultOutboundForPacketConnection {
 	if defaultOutboundForConnection != defaultOutboundForPacketConnection {
 		var description string
 		var description string
@@ -284,6 +287,7 @@ func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func()
 		r.logger.Info("using ", defaultOutboundForConnection.Type(), "[", description, "] as default outbound for connection")
 		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.logger.Info("using ", defaultOutboundForPacketConnection.Type(), "[", packetDescription, "] as default outbound for packet connection")
 	}
 	}
+	r.outbounds = outbounds
 	r.defaultOutboundForConnection = defaultOutboundForConnection
 	r.defaultOutboundForConnection = defaultOutboundForConnection
 	r.defaultOutboundForPacketConnection = defaultOutboundForPacketConnection
 	r.defaultOutboundForPacketConnection = defaultOutboundForPacketConnection
 	r.outboundByTag = outboundByTag
 	r.outboundByTag = outboundByTag
@@ -292,9 +296,27 @@ func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func()
 			return E.New("outbound not found for rule[", i, "]: ", rule.Outbound())
 			return E.New("outbound not found for rule[", i, "]: ", rule.Outbound())
 		}
 		}
 	}
 	}
+	for i, detour := range r.outbounds {
+		if starter, isStarter := detour.(adapter.Starter); isStarter {
+			err := starter.Start()
+			if err != nil {
+				var tag string
+				if detour.Tag() == "" {
+					tag = F.ToString(i)
+				} else {
+					tag = detour.Tag()
+				}
+				return E.Cause(err, "initialize outbound/", detour.Type(), "[", tag, "]")
+			}
+		}
+	}
 	return nil
 	return nil
 }
 }
 
 
+func (r *Router) Outbounds() []adapter.Outbound {
+	return r.outbounds
+}
+
 func (r *Router) Start() error {
 func (r *Router) Start() error {
 	if r.needGeoIPDatabase {
 	if r.needGeoIPDatabase {
 		err := r.prepareGeoIPDatabase()
 		err := r.prepareGeoIPDatabase()