mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-22 08:31:30 +00:00
Add shadowtls (#49)
* Add shadowtls outbound * Add shadowtls inbound * Add shadowtls example * Add shadowtls documentation
This commit is contained in:
parent
5a9c2b1e80
commit
5813e0ce7a
|
@ -18,6 +18,7 @@ const (
|
||||||
TypeHysteria = "hysteria"
|
TypeHysteria = "hysteria"
|
||||||
TypeTor = "tor"
|
TypeTor = "tor"
|
||||||
TypeSSH = "ssh"
|
TypeSSH = "ssh"
|
||||||
|
TypeShadowTLS = "shadowtls"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
31
docs/configuration/inbound/shadowtls.md
Normal file
31
docs/configuration/inbound/shadowtls.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "shadowtls",
|
||||||
|
"tag": "st-in",
|
||||||
|
|
||||||
|
... // Listen Fields
|
||||||
|
|
||||||
|
"handshake": {
|
||||||
|
"server": "google.com",
|
||||||
|
"server_port": 443,
|
||||||
|
|
||||||
|
... // Dial Fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listen Fields
|
||||||
|
|
||||||
|
See [Listen Fields](/configuration/shared/listen) for details.
|
||||||
|
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### handshake
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Handshake server address and [dial options](/configuration/shared/dial).
|
||||||
|
|
29
docs/configuration/inbound/shadowtls.zh.md
Normal file
29
docs/configuration/inbound/shadowtls.zh.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "shadowtls",
|
||||||
|
"tag": "st-in",
|
||||||
|
|
||||||
|
... // 监听字段
|
||||||
|
|
||||||
|
"handshake": {
|
||||||
|
"server": "google.com",
|
||||||
|
"server_port": 443,
|
||||||
|
|
||||||
|
... // 拨号字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 监听字段
|
||||||
|
|
||||||
|
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### handshake
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。
|
38
docs/configuration/outbound/shadowtls.md
Normal file
38
docs/configuration/outbound/shadowtls.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "shadowtls",
|
||||||
|
"tag": "st-out",
|
||||||
|
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 1080,
|
||||||
|
"tls": {},
|
||||||
|
|
||||||
|
... // Dial Fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### server
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
The server address.
|
||||||
|
|
||||||
|
#### server_port
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
The server port.
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||||
|
|
||||||
|
### Dial Fields
|
||||||
|
|
||||||
|
See [Dial Fields](/configuration/shared/dial) for details.
|
38
docs/configuration/outbound/shadowtls.zh.md
Normal file
38
docs/configuration/outbound/shadowtls.zh.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "shadowtls",
|
||||||
|
"tag": "st-out",
|
||||||
|
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 1080,
|
||||||
|
"tls": {},
|
||||||
|
|
||||||
|
... // 拨号字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### server
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
服务器地址。
|
||||||
|
|
||||||
|
#### server_port
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
服务器端口。
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||||
|
|
||||||
|
### 拨号字段
|
||||||
|
|
||||||
|
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
55
docs/examples/shadowtls.md
Normal file
55
docs/examples/shadowtls.md
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#### Server
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "shadowtls",
|
||||||
|
"listen": "::",
|
||||||
|
"listen_port": 4443,
|
||||||
|
"handshake": {
|
||||||
|
"server": "google.com",
|
||||||
|
"server_port": 443
|
||||||
|
},
|
||||||
|
"detour": "shadowsocks-in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shadowsocks",
|
||||||
|
"tag": "shadowsocks-in",
|
||||||
|
"listen": "127.0.0.1",
|
||||||
|
"method": "2022-blake3-aes-128-gcm",
|
||||||
|
"password": "8JCsPssfgS8tiRwiMlhARg=="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Client
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "shadowsocks",
|
||||||
|
"method": "2022-blake3-aes-128-gcm",
|
||||||
|
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||||
|
"detour": "shadowtls-out",
|
||||||
|
"multiplex": {
|
||||||
|
"enabled": 1,
|
||||||
|
"max_connections": 4,
|
||||||
|
"min_streams": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shadowtls",
|
||||||
|
"tag": "shadowtls-out",
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 4443,
|
||||||
|
"tls": {
|
||||||
|
"enabled": true,
|
||||||
|
"server_name": "google.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
|
@ -39,6 +39,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
|
||||||
return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions)
|
return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions)
|
||||||
case C.TypeHysteria:
|
case C.TypeHysteria:
|
||||||
return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
|
return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
|
||||||
|
case C.TypeShadowTLS:
|
||||||
|
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", options.Type)
|
return nil, E.New("unknown inbound type: ", options.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,15 +87,3 @@ func (h *Shadowsocks) NewPacket(ctx context.Context, conn N.PacketConn, buffer *
|
||||||
func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Shadowsocks) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
|
||||||
h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
|
|
||||||
return h.router.RouteConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Shadowsocks) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
|
||||||
ctx = log.ContextWithNewID(ctx)
|
|
||||||
h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
|
|
||||||
h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
|
|
||||||
return h.router.RoutePacketConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
|
|
93
inbound/shadowtls.go
Normal file
93
inbound/shadowtls.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
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"
|
||||||
|
"github.com/sagernet/sing/common/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShadowTLS struct {
|
||||||
|
myInboundAdapter
|
||||||
|
handshakeDialer N.Dialer
|
||||||
|
handshakeAddr M.Socksaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (*ShadowTLS, error) {
|
||||||
|
inbound := &ShadowTLS{
|
||||||
|
myInboundAdapter: myInboundAdapter{
|
||||||
|
protocol: C.TypeShadowTLS,
|
||||||
|
network: []string{N.NetworkTCP},
|
||||||
|
ctx: ctx,
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
tag: tag,
|
||||||
|
listenOptions: options.ListenOptions,
|
||||||
|
},
|
||||||
|
handshakeDialer: dialer.New(router, options.Handshake.DialerOptions),
|
||||||
|
handshakeAddr: options.Handshake.ServerOptions.Build(),
|
||||||
|
}
|
||||||
|
inbound.connHandler = inbound
|
||||||
|
return inbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
|
handshakeConn, err := s.handshakeDialer.DialContext(ctx, N.NetworkTCP, s.handshakeAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var handshake task.Group
|
||||||
|
handshake.Append("client handshake", func(ctx context.Context) error {
|
||||||
|
return s.copyUntilHandshakeFinished(handshakeConn, conn)
|
||||||
|
})
|
||||||
|
handshake.Append("server handshake", func(ctx context.Context) error {
|
||||||
|
return s.copyUntilHandshakeFinished(conn, handshakeConn)
|
||||||
|
})
|
||||||
|
handshake.FastFail()
|
||||||
|
err = handshake.Run(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.newConnection(ctx, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShadowTLS) copyUntilHandshakeFinished(dst io.Writer, src io.Reader) error {
|
||||||
|
const handshake = 0x16
|
||||||
|
const changeCipherSpec = 0x14
|
||||||
|
var hasSeenChangeCipherSpec bool
|
||||||
|
var tlsHdr [5]byte
|
||||||
|
for {
|
||||||
|
_, err := io.ReadFull(src, tlsHdr[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
length := binary.BigEndian.Uint16(tlsHdr[3:])
|
||||||
|
_, err = io.Copy(dst, io.MultiReader(bytes.NewReader(tlsHdr[:]), io.LimitReader(src, int64(length))))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tlsHdr[0] != handshake {
|
||||||
|
if tlsHdr[0] != changeCipherSpec {
|
||||||
|
return E.New("unexpected tls frame type: ", tlsHdr[0])
|
||||||
|
}
|
||||||
|
if !hasSeenChangeCipherSpec {
|
||||||
|
hasSeenChangeCipherSpec = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasSeenChangeCipherSpec {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,6 +68,7 @@ nav:
|
||||||
- Trojan: configuration/inbound/trojan.md
|
- Trojan: configuration/inbound/trojan.md
|
||||||
- Naive: configuration/inbound/naive.md
|
- Naive: configuration/inbound/naive.md
|
||||||
- Hysteria: configuration/inbound/hysteria.md
|
- Hysteria: configuration/inbound/hysteria.md
|
||||||
|
- ShadowTLS: configuration/inbound/shadowtls.md
|
||||||
- Tun: configuration/inbound/tun.md
|
- Tun: configuration/inbound/tun.md
|
||||||
- Redirect: configuration/inbound/redirect.md
|
- Redirect: configuration/inbound/redirect.md
|
||||||
- TProxy: configuration/inbound/tproxy.md
|
- TProxy: configuration/inbound/tproxy.md
|
||||||
|
@ -82,6 +83,7 @@ nav:
|
||||||
- Trojan: configuration/outbound/trojan.md
|
- Trojan: configuration/outbound/trojan.md
|
||||||
- WireGuard: configuration/outbound/wireguard.md
|
- WireGuard: configuration/outbound/wireguard.md
|
||||||
- Hysteria: configuration/outbound/hysteria.md
|
- Hysteria: configuration/outbound/hysteria.md
|
||||||
|
- ShadowTLS: configuration/outbound/shadowtls.md
|
||||||
- Tor: configuration/outbound/tor.md
|
- Tor: configuration/outbound/tor.md
|
||||||
- SSH: configuration/outbound/ssh.md
|
- SSH: configuration/outbound/ssh.md
|
||||||
- DNS: configuration/outbound/dns.md
|
- DNS: configuration/outbound/dns.md
|
||||||
|
@ -95,6 +97,7 @@ nav:
|
||||||
- Shadowsocks Server: examples/ss-server.md
|
- Shadowsocks Server: examples/ss-server.md
|
||||||
- Shadowsocks Client: examples/ss-client.md
|
- Shadowsocks Client: examples/ss-client.md
|
||||||
- Shadowsocks Tun: examples/ss-tun.md
|
- Shadowsocks Tun: examples/ss-tun.md
|
||||||
|
- ShadowTLS: examples/shadowtls.md
|
||||||
- DNS Hijack: examples/dns-hijack.md
|
- DNS Hijack: examples/dns-hijack.md
|
||||||
- Contributing:
|
- Contributing:
|
||||||
- contributing/index.md
|
- contributing/index.md
|
||||||
|
|
|
@ -21,6 +21,7 @@ type _Inbound struct {
|
||||||
TrojanOptions TrojanInboundOptions `json:"-"`
|
TrojanOptions TrojanInboundOptions `json:"-"`
|
||||||
NaiveOptions NaiveInboundOptions `json:"-"`
|
NaiveOptions NaiveInboundOptions `json:"-"`
|
||||||
HysteriaOptions HysteriaInboundOptions `json:"-"`
|
HysteriaOptions HysteriaInboundOptions `json:"-"`
|
||||||
|
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Inbound _Inbound
|
type Inbound _Inbound
|
||||||
|
@ -52,6 +53,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
|
||||||
v = h.NaiveOptions
|
v = h.NaiveOptions
|
||||||
case C.TypeHysteria:
|
case C.TypeHysteria:
|
||||||
v = h.HysteriaOptions
|
v = h.HysteriaOptions
|
||||||
|
case C.TypeShadowTLS:
|
||||||
|
v = h.ShadowTLSOptions
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", h.Type)
|
return nil, E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
|
@ -89,6 +92,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
|
||||||
v = &h.NaiveOptions
|
v = &h.NaiveOptions
|
||||||
case C.TypeHysteria:
|
case C.TypeHysteria:
|
||||||
v = &h.HysteriaOptions
|
v = &h.HysteriaOptions
|
||||||
|
case C.TypeShadowTLS:
|
||||||
|
v = &h.ShadowTLSOptions
|
||||||
default:
|
default:
|
||||||
return E.New("unknown inbound type: ", h.Type)
|
return E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ type _Outbound struct {
|
||||||
HysteriaOptions HysteriaOutboundOptions `json:"-"`
|
HysteriaOptions HysteriaOutboundOptions `json:"-"`
|
||||||
TorOptions TorOutboundOptions `json:"-"`
|
TorOptions TorOutboundOptions `json:"-"`
|
||||||
SSHOptions SSHOutboundOptions `json:"-"`
|
SSHOptions SSHOutboundOptions `json:"-"`
|
||||||
|
ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"`
|
||||||
SelectorOptions SelectorOutboundOptions `json:"-"`
|
SelectorOptions SelectorOutboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +51,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
|
||||||
v = h.TorOptions
|
v = h.TorOptions
|
||||||
case C.TypeSSH:
|
case C.TypeSSH:
|
||||||
v = h.SSHOptions
|
v = h.SSHOptions
|
||||||
|
case C.TypeShadowTLS:
|
||||||
|
v = h.ShadowTLSOptions
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
v = h.SelectorOptions
|
v = h.SelectorOptions
|
||||||
default:
|
default:
|
||||||
|
@ -87,6 +90,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
|
||||||
v = &h.TorOptions
|
v = &h.TorOptions
|
||||||
case C.TypeSSH:
|
case C.TypeSSH:
|
||||||
v = &h.SSHOptions
|
v = &h.SSHOptions
|
||||||
|
case C.TypeShadowTLS:
|
||||||
|
v = &h.ShadowTLSOptions
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
v = &h.SelectorOptions
|
v = &h.SelectorOptions
|
||||||
default:
|
default:
|
||||||
|
|
17
option/shadowtls.go
Normal file
17
option/shadowtls.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package option
|
||||||
|
|
||||||
|
type ShadowTLSInboundOptions struct {
|
||||||
|
ListenOptions
|
||||||
|
Handshake ShadowTLSHandshakeOptions `json:"handshake"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShadowTLSHandshakeOptions struct {
|
||||||
|
ServerOptions
|
||||||
|
DialerOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShadowTLSOutboundOptions struct {
|
||||||
|
OutboundDialerOptions
|
||||||
|
ServerOptions
|
||||||
|
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||||
|
}
|
|
@ -39,6 +39,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
|
||||||
return NewTor(ctx, router, logger, options.Tag, options.TorOptions)
|
return NewTor(ctx, router, logger, options.Tag, options.TorOptions)
|
||||||
case C.TypeSSH:
|
case C.TypeSSH:
|
||||||
return NewSSH(ctx, router, logger, options.Tag, options.SSHOptions)
|
return NewSSH(ctx, router, logger, options.Tag, options.SSHOptions)
|
||||||
|
case C.TypeShadowTLS:
|
||||||
|
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
return NewSelector(router, logger, options.Tag, options.SelectorOptions)
|
return NewSelector(router, logger, options.Tag, options.SelectorOptions)
|
||||||
default:
|
default:
|
||||||
|
|
84
outbound/shadowtls.go
Normal file
84
outbound/shadowtls.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.Outbound = (*ShadowTLS)(nil)
|
||||||
|
|
||||||
|
type ShadowTLS struct {
|
||||||
|
myOutboundAdapter
|
||||||
|
dialer N.Dialer
|
||||||
|
serverAddr M.Socksaddr
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (*ShadowTLS, error) {
|
||||||
|
outbound := &ShadowTLS{
|
||||||
|
myOutboundAdapter: myOutboundAdapter{
|
||||||
|
protocol: C.TypeShadowTLS,
|
||||||
|
network: []string{N.NetworkTCP},
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
tag: tag,
|
||||||
|
},
|
||||||
|
dialer: dialer.NewOutbound(router, options.OutboundDialerOptions),
|
||||||
|
serverAddr: options.ServerOptions.Build(),
|
||||||
|
}
|
||||||
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
|
return nil, C.ErrTLSRequired
|
||||||
|
}
|
||||||
|
options.TLS.MinVersion = "1.2"
|
||||||
|
options.TLS.MaxVersion = "1.2"
|
||||||
|
var err error
|
||||||
|
outbound.tlsConfig, err = dialer.TLSConfig(options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return outbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShadowTLS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkTCP:
|
||||||
|
default:
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
conn, err := s.dialer.DialContext(ctx, N.NetworkTCP, s.serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConn, err := dialer.TLSClient(ctx, conn, s.tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tlsConn.HandshakeContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShadowTLS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
|
return NewConnection(ctx, s, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShadowTLS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
|
@ -519,8 +519,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||||
if !common.Contains(injectable.Network(), N.NetworkTCP) {
|
if !common.Contains(injectable.Network(), N.NetworkTCP) {
|
||||||
return E.New("inject: TCP unsupported")
|
return E.New("inject: TCP unsupported")
|
||||||
}
|
}
|
||||||
metadata.InboundDetour = ""
|
|
||||||
metadata.LastInbound = metadata.Inbound
|
metadata.LastInbound = metadata.Inbound
|
||||||
|
metadata.Inbound = metadata.InboundDetour
|
||||||
|
metadata.InboundDetour = ""
|
||||||
err := injectable.NewConnection(ctx, conn, metadata)
|
err := injectable.NewConnection(ctx, conn, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "inject ", detour.Tag())
|
return E.Cause(err, "inject ", detour.Tag())
|
||||||
|
@ -599,8 +600,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||||
if !common.Contains(injectable.Network(), N.NetworkUDP) {
|
if !common.Contains(injectable.Network(), N.NetworkUDP) {
|
||||||
return E.New("inject: UDP unsupported")
|
return E.New("inject: UDP unsupported")
|
||||||
}
|
}
|
||||||
metadata.InboundDetour = ""
|
|
||||||
metadata.LastInbound = metadata.Inbound
|
metadata.LastInbound = metadata.Inbound
|
||||||
|
metadata.Inbound = metadata.InboundDetour
|
||||||
|
metadata.InboundDetour = ""
|
||||||
err := injectable.NewPacketConnection(ctx, conn, metadata)
|
err := injectable.NewPacketConnection(ctx, conn, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "inject ", detour.Tag())
|
return E.Cause(err, "inject ", detour.Tag())
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/sagernet/sing-box"
|
"github.com/sagernet/sing-box"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
"github.com/sagernet/sing/common/debug"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/protocol/socks"
|
"github.com/sagernet/sing/protocol/socks"
|
||||||
|
@ -17,6 +18,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func startInstance(t *testing.T, options option.Options) {
|
func startInstance(t *testing.T, options option.Options) {
|
||||||
|
if debug.Enabled {
|
||||||
|
options.Log = &option.LogOptions{
|
||||||
|
Level: "trace",
|
||||||
|
}
|
||||||
|
}
|
||||||
var instance *box.Box
|
var instance *box.Box
|
||||||
var err error
|
var err error
|
||||||
for retry := 0; retry < 3; retry++ {
|
for retry := 0; retry < 3; retry++ {
|
||||||
|
|
|
@ -36,6 +36,7 @@ const (
|
||||||
ImageBoringTun = "ghcr.io/ntkme/boringtun:edge"
|
ImageBoringTun = "ghcr.io/ntkme/boringtun:edge"
|
||||||
ImageHysteria = "tobyxdd/hysteria:latest"
|
ImageHysteria = "tobyxdd/hysteria:latest"
|
||||||
ImageNginx = "nginx:stable"
|
ImageNginx = "nginx:stable"
|
||||||
|
ImageShadowTLS = "ghcr.io/ihciah/shadow-tls:latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allImages = []string{
|
var allImages = []string{
|
||||||
|
@ -46,7 +47,8 @@ var allImages = []string{
|
||||||
ImageNaive,
|
ImageNaive,
|
||||||
ImageBoringTun,
|
ImageBoringTun,
|
||||||
ImageHysteria,
|
ImageHysteria,
|
||||||
// ImageNginx,
|
ImageNginx,
|
||||||
|
ImageShadowTLS,
|
||||||
}
|
}
|
||||||
|
|
||||||
var localIP = netip.MustParseAddr("127.0.0.1")
|
var localIP = netip.MustParseAddr("127.0.0.1")
|
||||||
|
|
170
test/shadowtls_test.go
Normal file
170
test/shadowtls_test.go
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShadowTLS(t *testing.T) {
|
||||||
|
method := shadowaead_2022.List[0]
|
||||||
|
password := mkBase64(t, 16)
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.ListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeShadowTLS,
|
||||||
|
Tag: "in",
|
||||||
|
ShadowTLSOptions: option.ShadowTLSInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.ListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: serverPort,
|
||||||
|
Detour: "detour",
|
||||||
|
},
|
||||||
|
Handshake: option.ShadowTLSHandshakeOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "google.com",
|
||||||
|
ServerPort: 443,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeShadowsocks,
|
||||||
|
Tag: "detour",
|
||||||
|
ShadowsocksOptions: option.ShadowsocksInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.ListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: otherPort,
|
||||||
|
},
|
||||||
|
Method: method,
|
||||||
|
Password: password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeShadowsocks,
|
||||||
|
ShadowsocksOptions: option.ShadowsocksOutboundOptions{
|
||||||
|
Method: method,
|
||||||
|
Password: password,
|
||||||
|
OutboundDialerOptions: option.OutboundDialerOptions{
|
||||||
|
DialerOptions: option.DialerOptions{
|
||||||
|
Detour: "detour",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeShadowTLS,
|
||||||
|
Tag: "detour",
|
||||||
|
ShadowTLSOptions: option.ShadowTLSOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "google.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
Tag: "direct",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{{
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Inbound: []string{"detour"},
|
||||||
|
Outbound: "direct",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testTCP(t, clientPort, testPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShadowTLSOutbound(t *testing.T) {
|
||||||
|
startDockerContainer(t, DockerOptions{
|
||||||
|
Image: ImageShadowTLS,
|
||||||
|
Ports: []uint16{serverPort, otherPort},
|
||||||
|
EntryPoint: "shadow-tls",
|
||||||
|
Cmd: []string{"--threads", "1", "server", "0.0.0.0:" + F.ToString(serverPort), "127.0.0.1:" + F.ToString(otherPort), "google.com:443"},
|
||||||
|
})
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.ListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
Tag: "detour",
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.ListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: otherPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeSocks,
|
||||||
|
SocksOptions: option.SocksOutboundOptions{
|
||||||
|
OutboundDialerOptions: option.OutboundDialerOptions{
|
||||||
|
DialerOptions: option.DialerOptions{
|
||||||
|
Detour: "detour",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeShadowTLS,
|
||||||
|
Tag: "detour",
|
||||||
|
ShadowTLSOptions: option.ShadowTLSOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "google.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
Tag: "direct",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{{
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Inbound: []string{"detour"},
|
||||||
|
Outbound: "direct",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testTCP(t, clientPort, testPort)
|
||||||
|
}
|
Loading…
Reference in a new issue