From 729dbe72fa7530687e4ef47d23065ca29765974a Mon Sep 17 00:00:00 2001 From: PuerNya Date: Fri, 20 Oct 2023 17:27:19 +0800 Subject: [PATCH] add `relay` outbound --- adapter/experimental.go | 5 ++ adapter/outbound.go | 4 ++ common/dialer/detour.go | 6 ++ constant/proxy.go | 3 + option/clash.go | 5 ++ option/outbound.go | 5 ++ outbound/builder.go | 2 + outbound/http.go | 18 +++++- outbound/hysteria.go | 14 +++++ outbound/hysteria2.go | 14 +++++ outbound/relay.go | 133 +++++++++++++++++++++++++++++++++++++++ outbound/shadowsocks.go | 12 +++- outbound/shadowsocksr.go | 12 +++- outbound/shadowtls.go | 18 +++++- outbound/socks.go | 15 +++++ outbound/ssh.go | 8 +++ outbound/tor.go | 24 ++++++- outbound/trojan.go | 12 +++- outbound/tuic.go | 15 +++++ outbound/vless.go | 12 +++- outbound/vmess.go | 12 +++- outbound/wireguard.go | 15 +++++ 22 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 outbound/relay.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 80650c2797..a1a5f93da2 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -48,6 +48,11 @@ type URLTestGroup interface { URLTest(ctx context.Context, url string) (map[string]uint16, error) } +type RelayGroup interface { + OutboundGroup + IsRelay() bool +} + func OutboundTag(detour Outbound) string { if group, isGroup := detour.(OutboundGroup); isGroup { return group.Now() diff --git a/adapter/outbound.go b/adapter/outbound.go index e0b6c74d7b..67f1e6779f 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -24,3 +24,7 @@ type Outbound interface { type OutboundUseIP interface { UseIP() bool } + +type OutboundRelay interface { + SetRelay(detour N.Dialer) Outbound +} diff --git a/common/dialer/detour.go b/common/dialer/detour.go index 9b1b86cbbb..eb5a8f0709 100644 --- a/common/dialer/detour.go +++ b/common/dialer/detour.go @@ -24,6 +24,12 @@ func NewDetour(router adapter.Router, detour string) N.Dialer { return &DetourDialer{router: router, detour: detour} } +func NewDetourWithDialer(router adapter.Router, detour adapter.Outbound) N.Dialer { + d := DetourDialer{router: router, detour: detour.Tag()} + d.initOnce.Do(func() { d.dialer = detour }) + return &d +} + func (d *DetourDialer) Start() error { _, err := d.Dialer() return err diff --git a/constant/proxy.go b/constant/proxy.go index 1e9baee298..8daf9876f3 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -28,6 +28,7 @@ const ( const ( TypeSelector = "selector" TypeURLTest = "urltest" + TypeRelay = "relay" ) func ProxyDisplayName(proxyType string) string { @@ -72,6 +73,8 @@ func ProxyDisplayName(proxyType string) string { return "Selector" case TypeURLTest: return "URLTest" + case TypeRelay: + return "Relay" default: return "Unknown" } diff --git a/option/clash.go b/option/clash.go index 49299250af..f608e1fb98 100644 --- a/option/clash.go +++ b/option/clash.go @@ -38,3 +38,8 @@ type URLTestOutboundOptions struct { Tolerance uint16 `json:"tolerance,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` } + +type RelayOutboundOptions struct { + Outbounds []string `json:"outbounds"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/outbound.go b/option/outbound.go index 2985319ea5..e4c40d8bd4 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -27,6 +27,7 @@ type _Outbound struct { Hysteria2Options Hysteria2OutboundOptions `json:"-"` SelectorOptions SelectorOutboundOptions `json:"-"` URLTestOptions URLTestOutboundOptions `json:"-"` + RelayOptions RelayOutboundOptions `json:"-"` } type Outbound _Outbound @@ -70,6 +71,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) { v = h.SelectorOptions case C.TypeURLTest: v = h.URLTestOptions + case C.TypeRelay: + v = h.RelayOptions default: return nil, E.New("unknown outbound type: ", h.Type) } @@ -119,6 +122,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error { v = &h.SelectorOptions case C.TypeURLTest: v = &h.URLTestOptions + case C.TypeRelay: + v = &h.RelayOptions default: return E.New("unknown outbound type: ", h.Type) } diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..c31ae653dc 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -59,6 +59,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t return NewSelector(router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) + case C.TypeRelay: + return NewRelay(router, logger, tag, options.RelayOptions) default: return nil, E.New("unknown outbound type: ", options.Type) } diff --git a/outbound/http.go b/outbound/http.go index 8a632da96f..3863a1931d 100644 --- a/outbound/http.go +++ b/outbound/http.go @@ -4,6 +4,7 @@ import ( "context" "net" "os" + "reflect" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -17,7 +18,10 @@ import ( sHTTP "github.com/sagernet/sing/protocol/http" ) -var _ adapter.Outbound = (*HTTP)(nil) +var ( + _ adapter.Outbound = (*HTTP)(nil) + _ adapter.OutboundRelay = (*HTTP)(nil) +) type HTTP struct { myOutboundAdapter @@ -73,3 +77,15 @@ func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, metadata adapte func (h *HTTP) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return os.ErrInvalid } + +func (h *HTTP) SetRelay(detour N.Dialer) adapter.Outbound { + c := *h.client + client := c + r := reflect.ValueOf(client) + r.FieldByName("dialer").Set(reflect.ValueOf(detour)) + outbound := HTTP{ + myOutboundAdapter: h.myOutboundAdapter, + client: &client, + } + return &outbound +} diff --git a/outbound/hysteria.go b/outbound/hysteria.go index 6450c5243c..b9fd800136 100644 --- a/outbound/hysteria.go +++ b/outbound/hysteria.go @@ -6,6 +6,7 @@ import ( "context" "net" "os" + "reflect" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -24,6 +25,7 @@ import ( var ( _ adapter.Outbound = (*Hysteria)(nil) + _ adapter.OutboundRelay = (*Hysteria)(nil) _ adapter.InterfaceUpdateListener = (*Hysteria)(nil) ) @@ -138,3 +140,15 @@ func (h *Hysteria) InterfaceUpdated() { func (h *Hysteria) Close() error { return h.client.CloseWithError(os.ErrClosed) } + +func (h *Hysteria) SetRelay(detour N.Dialer) adapter.Outbound { + c := *h.client + client := c + r := reflect.ValueOf(client) + r.FieldByName("dialer").Set(reflect.ValueOf(detour)) + outbound := Hysteria{ + myOutboundAdapter: h.myOutboundAdapter, + client: &client, + } + return &outbound +} diff --git a/outbound/hysteria2.go b/outbound/hysteria2.go index 9fb0607c03..9b8bee5106 100644 --- a/outbound/hysteria2.go +++ b/outbound/hysteria2.go @@ -6,6 +6,7 @@ import ( "context" "net" "os" + "reflect" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -24,6 +25,7 @@ import ( var ( _ adapter.Outbound = (*Hysteria2)(nil) + _ adapter.OutboundRelay = (*Hysteria2)(nil) _ adapter.InterfaceUpdateListener = (*Hysteria2)(nil) ) @@ -124,3 +126,15 @@ func (h *Hysteria2) InterfaceUpdated() { func (h *Hysteria2) Close() error { return h.client.CloseWithError(os.ErrClosed) } + +func (h *Hysteria2) SetRelay(detour N.Dialer) adapter.Outbound { + c := *h.client + client := c + r := reflect.ValueOf(client) + r.FieldByName("dialer").Set(reflect.ValueOf(detour)) + outbound := Hysteria2{ + myOutboundAdapter: h.myOutboundAdapter, + client: &client, + } + return &outbound +} diff --git a/outbound/relay.go b/outbound/relay.go new file mode 100644 index 0000000000..e6ba88f028 --- /dev/null +++ b/outbound/relay.go @@ -0,0 +1,133 @@ +package outbound + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/interrupt" + 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 = (*Relay)(nil) + _ adapter.OutboundGroup = (*Relay)(nil) + _ adapter.RelayGroup = (*Relay)(nil) +) + +type Relay struct { + myOutboundAdapter + tags []string + interruptGroup *interrupt.Group + interruptExternalConnections bool +} + +func NewRelay(router adapter.Router, logger log.ContextLogger, tag string, options option.RelayOutboundOptions) (*Relay, error) { + outbound := &Relay{ + myOutboundAdapter: myOutboundAdapter{ + protocol: C.TypeRelay, + router: router, + logger: logger, + tag: tag, + dependencies: options.Outbounds, + }, + tags: options.Outbounds, + interruptGroup: interrupt.NewGroup(), + interruptExternalConnections: options.InterruptExistConnections, + } + if len(outbound.tags) == 0 { + return nil, E.New("missing tags") + } + return outbound, nil +} + +func (r *Relay) Network() []string { + detour, _ := r.router.Outbound(r.tags[0]) + return detour.Network() +} + +func (r *Relay) Start() error { + for i, tag := range r.tags { + outbound, loaded := r.router.Outbound(tag) + if !loaded { + return E.New("outbound ", i, " not found: ", tag) + } + if _, isRelay := outbound.(adapter.RelayGroup); isRelay { + return E.New("relay outbound invalid: ", tag) + } + } + return nil +} + +func (r *Relay) Now() string { + return "" +} + +func (r *Relay) All() []string { + return r.tags +} + +func (s *Relay) UpdateOutbounds(tag string) error { + return nil +} + +func (s *Relay) IsRelay() bool { + return true +} + +func (r *Relay) SelectedOutbound(network string) adapter.Outbound { + detour, _ := r.router.Outbound(r.tags[len(r.tags)-1]) + return detour +} + +func (r *Relay) createRelayChain(network string) adapter.Outbound { + len := len(r.tags) + detour, _ := r.router.Outbound(r.tags[0]) + tag := RealOutboundTag(detour, network) + detour, _ = r.router.OutboundWithProvider(tag) + for i := 1; i < len; i++ { + out, _ := r.router.Outbound(r.tags[i]) + tag := RealOutboundTag(out, network) + out, _ = r.router.OutboundWithProvider(tag) + outbound := out.(adapter.OutboundRelay) + d := dialer.NewDetourWithDialer(r.router, detour) + detour = outbound.SetRelay(d) + } + return detour +} + +func (r *Relay) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + detour := r.createRelayChain(network) + conn, err := detour.DialContext(ctx, network, destination) + if err != nil { + return nil, err + } + return r.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil +} + +func (r *Relay) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + detour := r.createRelayChain(N.NetworkUDP) + conn, err := detour.ListenPacket(ctx, destination) + if err != nil { + return nil, err + } + return r.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil +} + +func (r *Relay) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + detour := r.createRelayChain(metadata.Network) + ctx = interrupt.ContextWithIsExternalConnection(ctx) + return detour.NewConnection(ctx, conn, metadata) +} + +func (r *Relay) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + detour := r.createRelayChain(metadata.Network) + ctx = interrupt.ContextWithIsExternalConnection(ctx) + return detour.NewPacketConnection(ctx, conn, metadata) +} diff --git a/outbound/shadowsocks.go b/outbound/shadowsocks.go index f0aa949dcd..64db5c52c0 100644 --- a/outbound/shadowsocks.go +++ b/outbound/shadowsocks.go @@ -20,7 +20,10 @@ import ( "github.com/sagernet/sing/common/uot" ) -var _ adapter.Outbound = (*Shadowsocks)(nil) +var ( + _ adapter.Outbound = (*Shadowsocks)(nil) + _ adapter.OutboundRelay = (*Shadowsocks)(nil) +) type Shadowsocks struct { myOutboundAdapter @@ -187,3 +190,10 @@ func (h *shadowsocksDialer) ListenPacket(ctx context.Context, destination M.Sock } return h.method.DialPacketConn(outConn), nil } + +func (s *Shadowsocks) SetRelay(detour N.Dialer) adapter.Outbound { + ss := *s + outbound := ss + outbound.dialer = detour + return &outbound +} diff --git a/outbound/shadowsocksr.go b/outbound/shadowsocksr.go index f49139fdf5..b7b276f1f4 100644 --- a/outbound/shadowsocksr.go +++ b/outbound/shadowsocksr.go @@ -25,7 +25,10 @@ import ( "github.com/Dreamacro/clash/transport/socks5" ) -var _ adapter.Outbound = (*ShadowsocksR)(nil) +var ( + _ adapter.Outbound = (*ShadowsocksR)(nil) + _ adapter.OutboundRelay = (*ShadowsocksR)(nil) +) type ShadowsocksR struct { myOutboundAdapter @@ -193,3 +196,10 @@ func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { copy(b, b[len(addr):]) return n - len(addr), udpAddr, e } + +func (h *ShadowsocksR) SetRelay(detour N.Dialer) adapter.Outbound { + ssr := *h + outbound := ssr + outbound.dialer = detour + return &outbound +} diff --git a/outbound/shadowtls.go b/outbound/shadowtls.go index 15f4514cc7..51302d13c1 100644 --- a/outbound/shadowtls.go +++ b/outbound/shadowtls.go @@ -4,6 +4,7 @@ import ( "context" "net" "os" + "reflect" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -17,7 +18,10 @@ import ( N "github.com/sagernet/sing/common/network" ) -var _ adapter.Outbound = (*ShadowTLS)(nil) +var ( + _ adapter.Outbound = (*ShadowTLS)(nil) + _ adapter.OutboundRelay = (*ShadowTLS)(nil) +) type ShadowTLS struct { myOutboundAdapter @@ -115,3 +119,15 @@ func (h *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata a func (h *ShadowTLS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return os.ErrInvalid } + +func (h *ShadowTLS) SetRelay(detour N.Dialer) adapter.Outbound { + c := *h.client + client := c + r := reflect.ValueOf(client) + r.FieldByName("dialer").Set(reflect.ValueOf(detour)) + outbound := ShadowTLS{ + myOutboundAdapter: h.myOutboundAdapter, + client: &client, + } + return &outbound +} diff --git a/outbound/socks.go b/outbound/socks.go index f29e0a6469..e290d8b769 100644 --- a/outbound/socks.go +++ b/outbound/socks.go @@ -3,6 +3,7 @@ package outbound import ( "context" "net" + "reflect" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -136,3 +137,17 @@ func (h *Socks) NewPacketConnection(ctx context.Context, conn N.PacketConn, meta func (h *Socks) UseIP() bool { return h.resolve } + +func (h *Socks) SetRelay(detour N.Dialer) adapter.Outbound { + c := *h.client + client := c + r := reflect.ValueOf(client) + r.FieldByName("dialer").Set(reflect.ValueOf(detour)) + outbound := Socks{ + myOutboundAdapter: h.myOutboundAdapter, + client: &client, + resolve: h.resolve, + uotClient: h.uotClient, + } + return &outbound +} diff --git a/outbound/ssh.go b/outbound/ssh.go index 33e7ba1016..e2bd8582b6 100644 --- a/outbound/ssh.go +++ b/outbound/ssh.go @@ -26,6 +26,7 @@ import ( var ( _ adapter.Outbound = (*SSH)(nil) + _ adapter.OutboundRelay = (*SSH)(nil) _ adapter.InterfaceUpdateListener = (*SSH)(nil) ) @@ -208,3 +209,10 @@ func (s *SSH) NewConnection(ctx context.Context, conn net.Conn, metadata adapter func (s *SSH) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return os.ErrInvalid } + +func (s *SSH) SetRelay(detour N.Dialer) adapter.Outbound { + ssh := *s + outbound := ssh + outbound.dialer = detour + return &outbound +} diff --git a/outbound/tor.go b/outbound/tor.go index 76c7955da7..3f3c78311e 100644 --- a/outbound/tor.go +++ b/outbound/tor.go @@ -5,6 +5,7 @@ import ( "net" "os" "path/filepath" + "reflect" "strings" "github.com/sagernet/sing-box/adapter" @@ -24,7 +25,10 @@ import ( "github.com/cretz/bine/tor" ) -var _ adapter.Outbound = (*Tor)(nil) +var ( + _ adapter.Outbound = (*Tor)(nil) + _ adapter.OutboundRelay = (*Tor)(nil) +) type Tor struct { myOutboundAdapter @@ -215,3 +219,21 @@ func (t *Tor) NewConnection(ctx context.Context, conn net.Conn, metadata adapter func (t *Tor) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return os.ErrInvalid } + +func (t *Tor) SetRelay(detour N.Dialer) adapter.Outbound { + c := *t.socksClient + client := c + r := reflect.ValueOf(client) + r.FieldByName("dialer").Set(reflect.ValueOf(detour)) + outbound := Tor{ + myOutboundAdapter: t.myOutboundAdapter, + ctx: t.ctx, + proxy: t.proxy, + startConf: t.startConf, + options: t.options, + events: t.events, + instance: t.instance, + socksClient: &client, + } + return &outbound +} diff --git a/outbound/trojan.go b/outbound/trojan.go index 39de455918..a78a0bed40 100644 --- a/outbound/trojan.go +++ b/outbound/trojan.go @@ -20,7 +20,10 @@ import ( N "github.com/sagernet/sing/common/network" ) -var _ adapter.Outbound = (*Trojan)(nil) +var ( + _ adapter.Outbound = (*Trojan)(nil) + _ adapter.OutboundRelay = (*Trojan)(nil) +) type Trojan struct { myOutboundAdapter @@ -156,3 +159,10 @@ func (h *trojanDialer) ListenPacket(ctx context.Context, destination M.Socksaddr } return conn.(net.PacketConn), nil } + +func (h *Trojan) SetRelay(detour N.Dialer) adapter.Outbound { + trojan := *h + outbound := trojan + outbound.dialer = detour + return &outbound +} diff --git a/outbound/tuic.go b/outbound/tuic.go index c098332375..19c933f108 100644 --- a/outbound/tuic.go +++ b/outbound/tuic.go @@ -6,6 +6,7 @@ import ( "context" "net" "os" + "reflect" "time" "github.com/sagernet/sing-box/adapter" @@ -27,6 +28,7 @@ import ( var ( _ adapter.Outbound = (*TUIC)(nil) + _ adapter.OutboundRelay = (*TUIC)(nil) _ adapter.InterfaceUpdateListener = (*TUIC)(nil) ) @@ -151,3 +153,16 @@ func (h *TUIC) InterfaceUpdated() { func (h *TUIC) Close() error { return h.client.CloseWithError(os.ErrClosed) } + +func (h *TUIC) SetRelay(detour N.Dialer) adapter.Outbound { + c := *h.client + client := c + r := reflect.ValueOf(client) + r.FieldByName("dialer").Set(reflect.ValueOf(detour)) + outbound := TUIC{ + myOutboundAdapter: h.myOutboundAdapter, + client: &client, + udpStream: h.udpStream, + } + return &outbound +} diff --git a/outbound/vless.go b/outbound/vless.go index 7ccc5a8cef..4c2fb2f67d 100644 --- a/outbound/vless.go +++ b/outbound/vless.go @@ -21,7 +21,10 @@ import ( N "github.com/sagernet/sing/common/network" ) -var _ adapter.Outbound = (*VLESS)(nil) +var ( + _ adapter.Outbound = (*VLESS)(nil) + _ adapter.OutboundRelay = (*VLESS)(nil) +) type VLESS struct { myOutboundAdapter @@ -216,3 +219,10 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) return h.client.DialEarlyPacketConn(conn, destination) } } + +func (h *VLESS) SetRelay(detour N.Dialer) adapter.Outbound { + vless := *h + outbound := vless + outbound.dialer = detour + return &outbound +} diff --git a/outbound/vmess.go b/outbound/vmess.go index 98d60c277e..924f7e4810 100644 --- a/outbound/vmess.go +++ b/outbound/vmess.go @@ -21,7 +21,10 @@ import ( "github.com/sagernet/sing/common/ntp" ) -var _ adapter.Outbound = (*VMess)(nil) +var ( + _ adapter.Outbound = (*VMess)(nil) + _ adapter.OutboundRelay = (*VMess)(nil) +) type VMess struct { myOutboundAdapter @@ -210,3 +213,10 @@ func (h *vmessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) return h.client.DialEarlyPacketConn(conn, destination), nil } } + +func (h *VMess) SetRelay(detour N.Dialer) adapter.Outbound { + vmess := *h + outbound := vmess + outbound.dialer = detour + return &outbound +} diff --git a/outbound/wireguard.go b/outbound/wireguard.go index 03293dcb08..cac0a94c8c 100644 --- a/outbound/wireguard.go +++ b/outbound/wireguard.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "fmt" "net" + "reflect" "strings" "github.com/sagernet/sing-box/adapter" @@ -251,3 +252,17 @@ func (w *WireGuard) Close() error { func (w *WireGuard) UseIP() bool { return true } + +func (w *WireGuard) SetRelay(detour N.Dialer) adapter.Outbound { + c := *w.bind + client := c + r := reflect.ValueOf(client) + r.FieldByName("dialer").Set(reflect.ValueOf(detour)) + outbound := WireGuard{ + myOutboundAdapter: w.myOutboundAdapter, + bind: &client, + device: w.device, + tunDevice: w.tunDevice, + } + return &outbound +}