Skip to content

Commit

Permalink
Add relay type outbound
Browse files Browse the repository at this point in the history
  • Loading branch information
PuerNya committed Dec 24, 2023
1 parent fb6e1d8 commit 9776cc2
Show file tree
Hide file tree
Showing 26 changed files with 406 additions and 8 deletions.
5 changes: 5 additions & 0 deletions adapter/experimental.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ type URLTestGroup interface {
URLTest(ctx context.Context) (map[string]uint16, error)
}

type RelayGroup interface {
OutboundGroup
IsRelay() bool
}

func OutboundTag(detour Outbound) string {
if group, isGroup := detour.(OutboundGroup); isGroup {
return group.Now()
Expand Down
4 changes: 4 additions & 0 deletions adapter/outbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ type Outbound interface {
type OutboundUseIP interface {
UseIP() bool
}

type OutboundRelay interface {
SetRelay(detour N.Dialer) Outbound
}
6 changes: 6 additions & 0 deletions common/dialer/detour.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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
Expand Down
3 changes: 3 additions & 0 deletions constant/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
const (
TypeSelector = "selector"
TypeURLTest = "urltest"
TypeRelay = "relay"
)

func ProxyDisplayName(proxyType string) string {
Expand Down Expand Up @@ -72,6 +73,8 @@ func ProxyDisplayName(proxyType string) string {
return "Selector"
case TypeURLTest:
return "URLTest"
case TypeRelay:
return "Relay"
default:
return "Unknown"
}
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/outbound/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
| `dns` | [DNS](./dns/) |
| `selector` | [Selector](./selector/) |
| `urltest` | [URLTest](./urltest/) |
| `relay` | [Relay](./relay) |

#### tag

Expand Down
1 change: 1 addition & 0 deletions docs/configuration/outbound/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
| `dns` | [DNS](./dns/) |
| `selector` | [Selector](./selector/) |
| `urltest` | [URLTest](./urltest/) |
| `relay` | [Relay](./relay) |

#### tag

Expand Down
29 changes: 29 additions & 0 deletions docs/configuration/outbound/relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
### Structure

```json
{
"type": "relay",
"tag": "relay",

"outbounds": [
"proxy-a",
"proxy-b",
"proxy-c"
],
"interrupt_exist_connections": false
}
```

### Fields

#### outbounds

==Required==

List of outbound tags to relay.

#### interrupt_exist_connections

Interrupt existing connections when the selected outbound has changed.

Only inbound connections are affected by this setting, internal connections will always be interrupted.
29 changes: 29 additions & 0 deletions docs/configuration/outbound/relay.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
### 结构

```json
{
"type": "relay",
"tag": "relay",

"outbounds": [
"proxy-a",
"proxy-b",
"proxy-c"
],
"interrupt_exist_connections": false
}
```

### 字段

#### outbounds

==必填==

用于链接的出站标签列表。

#### interrupt_exist_connections

当选定的出站发生更改时,中断现有连接。

仅入站连接受此设置影响,内部连接将始终被中断。
5 changes: 5 additions & 0 deletions option/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ type URLTestOutboundOptions struct {
IdleTimeout Duration `json:"idle_timeout,omitempty"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
}

type RelayOutboundOptions struct {
Outbounds []string `json:"outbounds"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
}
3 changes: 3 additions & 0 deletions option/outbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type _Outbound struct {
Hysteria2Options Hysteria2OutboundOptions `json:"-"`
SelectorOptions SelectorOutboundOptions `json:"-"`
URLTestOptions URLTestOutboundOptions `json:"-"`
RelayOptions RelayOutboundOptions `json:"-"`
}

type Outbound _Outbound
Expand Down Expand Up @@ -70,6 +71,8 @@ func (h *Outbound) RawOptions() (any, error) {
rawOptionsPtr = &h.SelectorOptions
case C.TypeURLTest:
rawOptionsPtr = &h.URLTestOptions
case C.TypeRelay:
rawOptionsPtr = &h.RelayOptions
case "":
return nil, E.New("missing outbound type")
default:
Expand Down
2 changes: 2 additions & 0 deletions outbound/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
return NewSelector(ctx, 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)
}
Expand Down
18 changes: 17 additions & 1 deletion outbound/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"net"
"os"
"reflect"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
Expand All @@ -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
Expand Down Expand Up @@ -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
}
14 changes: 14 additions & 0 deletions outbound/hysteria.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"net"
"os"
"reflect"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
Expand All @@ -24,6 +25,7 @@ import (

var (
_ adapter.Outbound = (*Hysteria)(nil)
_ adapter.OutboundRelay = (*Hysteria)(nil)
_ adapter.InterfaceUpdateListener = (*Hysteria)(nil)
)

Expand Down Expand Up @@ -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
}
14 changes: 14 additions & 0 deletions outbound/hysteria2.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"net"
"os"
"reflect"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
Expand All @@ -24,6 +25,7 @@ import (

var (
_ adapter.Outbound = (*Hysteria2)(nil)
_ adapter.OutboundRelay = (*Hysteria2)(nil)
_ adapter.InterfaceUpdateListener = (*Hysteria2)(nil)
)

Expand Down Expand Up @@ -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
}
133 changes: 133 additions & 0 deletions outbound/relay.go
Original file line number Diff line number Diff line change
@@ -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.Outbound(tag)
for i := 1; i < len; i++ {
out, _ := r.router.Outbound(r.tags[i])
tag := RealOutboundTag(out, network)
out, _ = r.router.Outbound(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)
}
Loading

0 comments on commit 9776cc2

Please sign in to comment.