diff --git a/adapter/router.go b/adapter/router.go index 741f3a45..354ed6d3 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -16,6 +16,7 @@ import ( type Router interface { Service + Outbounds() []Outbound Outbound(tag string) (Outbound, bool) DefaultOutbound(network string) Outbound diff --git a/adapter/service.go b/adapter/service.go index 5ed0798d..78c82205 100644 --- a/adapter/service.go +++ b/adapter/service.go @@ -1,6 +1,12 @@ package adapter -type Service interface { +import "io" + +type Starter interface { Start() error - Close() error +} + +type Service interface { + Starter + io.Closer } diff --git a/constant/proxy.go b/constant/proxy.go index 305db405..4f25fd93 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -13,3 +13,7 @@ const ( TypeShadowsocks = "shadowsocks" TypeVMess = "vmess" ) + +const ( + TypeSelector = "selector" +) diff --git a/experimental/clashapi/configs.go b/experimental/clashapi/configs.go index 7280758e..6fc4b966 100644 --- a/experimental/clashapi/configs.go +++ b/experimental/clashapi/configs.go @@ -18,24 +18,31 @@ func configRouter(logFactory log.Factory) http.Handler { } 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) { 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{ - Mode: "Rule", - LogLevel: log.FormatLevel(logFactory.Level()), + Mode: "rule", + BindAddress: "*", + LogLevel: log.FormatLevel(logLevel), }) } } diff --git a/experimental/clashapi/provider.go b/experimental/clashapi/provider.go index 91b35f3e..32ecf1e2 100644 --- a/experimental/clashapi/provider.go +++ b/experimental/clashapi/provider.go @@ -4,13 +4,15 @@ import ( "context" "net/http" + "github.com/sagernet/sing-box/adapter" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func proxyProviderRouter() http.Handler { +func proxyProviderRouter(server *Server, router adapter.Router) http.Handler { r := chi.NewRouter() - r.Get("/", getProviders) + r.Get("/", getProviders(server, router)) r.Route("/{name}", func(r chi.Router) { r.Use(parseProviderName, findProviderByName) @@ -21,10 +23,35 @@ func proxyProviderRouter() http.Handler { 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) { diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index f1ebd64a..53bd0faa 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -2,20 +2,33 @@ package clashapi import ( "context" + "fmt" + "net" "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/render" ) -func proxyRouter() http.Handler { +func proxyRouter(server *Server, router adapter.Router) http.Handler { r := chi.NewRouter() - r.Get("/", getProxies) + r.Get("/", getProxies(server, 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) }) 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 - //} - - // ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy) - // next.ServeHTTP(w, r.WithContext(ctx)) - }) +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)) + }) + } } -func getProxies(w http.ResponseWriter, r *http.Request) { - // proxies := tunnel.Proxies() - render.JSON(w, r, render.M{ - "proxies": []string{}, - }) +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 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 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(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 { @@ -63,33 +166,33 @@ type UpdateProxyRequest struct { } 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) } -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") timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) if err != nil { @@ -98,12 +201,11 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) { 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)) defer cancel() - delay, err := proxy.URLTest(ctx, url) + delay, err := URLTest(ctx, url, proxy) if ctx.Err() != nil { render.Status(r, http.StatusGatewayTimeout) 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")) 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 } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index c333ae68..31f4b984 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -31,11 +31,26 @@ type Server struct { logger log.Logger httpServer *http.Server 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 { trafficManager := trafficontroll.NewManager() 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{ AllowedOrigins: []string{"*"}, 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("/version", version) r.Mount("/configs", configRouter(logFactory)) - r.Mount("/proxies", proxyRouter()) + r.Mount("/proxies", proxyRouter(server, router)) r.Mount("/rules", ruleRouter(router)) r.Mount("/connections", connectionRouter(trafficManager)) - r.Mount("/providers/proxies", proxyProviderRouter()) + r.Mount("/providers/proxies", proxyProviderRouter(server, router)) r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) 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 { diff --git a/option/config.go b/option/config.go index f149cb2c..52672e11 100644 --- a/option/config.go +++ b/option/config.go @@ -41,7 +41,7 @@ func (o Options) Equals(other Options) bool { return common.ComparablePtrEquals(o.Log, other.Log) && common.PtrEquals(o.DNS, other.DNS) && 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.ComparablePtrEquals(o.Experimental, other.Experimental) } diff --git a/option/outbound.go b/option/outbound.go index 7e7c857e..ce012642 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -2,6 +2,7 @@ package option import ( C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -16,10 +17,22 @@ type _Outbound struct { HTTPOptions HTTPOutboundOptions `json:"-"` ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"` VMessOptions VMessOutboundOptions `json:"-"` + SelectorOptions SelectorOutboundOptions `json:"-"` } 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) { var v any switch h.Type { @@ -35,6 +48,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) { v = h.ShadowsocksOptions case C.TypeVMess: v = h.VMessOptions + case C.TypeSelector: + v = h.SelectorOptions default: return nil, E.New("unknown outbound type: ", h.Type) } @@ -60,6 +75,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error { v = &h.ShadowsocksOptions case C.TypeVMess: v = &h.VMessOptions + case C.TypeSelector: + v = &h.SelectorOptions default: return nil } @@ -144,3 +161,13 @@ type VMessOutboundOptions struct { Network NetworkList `json:"network,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 +} diff --git a/outbound/builder.go b/outbound/builder.go index 0887996d..98de1c70 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -10,7 +10,7 @@ import ( ) 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") } 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) case C.TypeVMess: return NewVMess(router, logger, options.Tag, options.VMessOptions) + case C.TypeSelector: + return NewSelector(router, logger, options.Tag, options.SelectorOptions) default: return nil, E.New("unknown outbound type: ", options.Type) } diff --git a/outbound/selector.go b/outbound/selector.go new file mode 100644 index 00000000..8d74320e --- /dev/null +++ b/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) +} diff --git a/outbound/shadowsocks.go b/outbound/shadowsocks.go index efcbf014..41eac85c 100644 --- a/outbound/shadowsocks.go +++ b/outbound/shadowsocks.go @@ -32,7 +32,7 @@ func NewShadowsocks(router adapter.Router, logger log.ContextLogger, tag string, } return &Shadowsocks{ myOutboundAdapter{ - protocol: C.TypeDirect, + protocol: C.TypeShadowsocks, logger: logger, tag: tag, network: options.Network.Build(), diff --git a/outbound/vmess.go b/outbound/vmess.go index 110346b4..31fd38ae 100644 --- a/outbound/vmess.go +++ b/outbound/vmess.go @@ -42,7 +42,7 @@ func NewVMess(router adapter.Router, logger log.ContextLogger, tag string, optio } return &VMess{ myOutboundAdapter{ - protocol: C.TypeDirect, + protocol: C.TypeVMess, logger: logger, tag: tag, network: options.Network.Build(), diff --git a/route/router.go b/route/router.go index 25e42d69..f8275036 100644 --- a/route/router.go +++ b/route/router.go @@ -42,6 +42,7 @@ type Router struct { logger log.ContextLogger dnsLogger log.ContextLogger + outbounds []adapter.Outbound outboundByTag map[string]adapter.Outbound rules []adapter.Rule @@ -267,6 +268,8 @@ func (r *Router) Initialize(outbounds []adapter.Outbound, defaultOutbound func() if defaultOutboundForPacketConnection == nil { defaultOutboundForPacketConnection = detour } + outbounds = append(outbounds, detour) + outboundByTag[detour.Tag()] = detour } if defaultOutboundForConnection != defaultOutboundForPacketConnection { 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 ", defaultOutboundForPacketConnection.Type(), "[", packetDescription, "] as default outbound for packet connection") } + r.outbounds = outbounds r.defaultOutboundForConnection = defaultOutboundForConnection r.defaultOutboundForPacketConnection = defaultOutboundForPacketConnection 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()) } } + 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 } +func (r *Router) Outbounds() []adapter.Outbound { + return r.outbounds +} + func (r *Router) Start() error { if r.needGeoIPDatabase { err := r.prepareGeoIPDatabase()