Add shadowtls (#49)

* Add shadowtls outbound

* Add shadowtls inbound

* Add shadowtls example

* Add shadowtls documentation
This commit is contained in:
世界 2022-08-31 14:21:53 +08:00 committed by GitHub
parent 5a9c2b1e80
commit 5813e0ce7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 586 additions and 15 deletions

View file

@ -18,6 +18,7 @@ const (
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
)
const (

View 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).

View 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/)。

View 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.

View 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/)。

View 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"
}
}
]
}
```

View file

@ -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)
case C.TypeHysteria:
return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
case C.TypeShadowTLS:
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
default:
return nil, E.New("unknown inbound type: ", options.Type)
}

View file

@ -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 {
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
View 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
}
}
}

View file

@ -68,6 +68,7 @@ nav:
- Trojan: configuration/inbound/trojan.md
- Naive: configuration/inbound/naive.md
- Hysteria: configuration/inbound/hysteria.md
- ShadowTLS: configuration/inbound/shadowtls.md
- Tun: configuration/inbound/tun.md
- Redirect: configuration/inbound/redirect.md
- TProxy: configuration/inbound/tproxy.md
@ -82,6 +83,7 @@ nav:
- Trojan: configuration/outbound/trojan.md
- WireGuard: configuration/outbound/wireguard.md
- Hysteria: configuration/outbound/hysteria.md
- ShadowTLS: configuration/outbound/shadowtls.md
- Tor: configuration/outbound/tor.md
- SSH: configuration/outbound/ssh.md
- DNS: configuration/outbound/dns.md
@ -95,6 +97,7 @@ nav:
- Shadowsocks Server: examples/ss-server.md
- Shadowsocks Client: examples/ss-client.md
- Shadowsocks Tun: examples/ss-tun.md
- ShadowTLS: examples/shadowtls.md
- DNS Hijack: examples/dns-hijack.md
- Contributing:
- contributing/index.md

View file

@ -21,6 +21,7 @@ type _Inbound struct {
TrojanOptions TrojanInboundOptions `json:"-"`
NaiveOptions NaiveInboundOptions `json:"-"`
HysteriaOptions HysteriaInboundOptions `json:"-"`
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
}
type Inbound _Inbound
@ -52,6 +53,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
v = h.NaiveOptions
case C.TypeHysteria:
v = h.HysteriaOptions
case C.TypeShadowTLS:
v = h.ShadowTLSOptions
default:
return nil, E.New("unknown inbound type: ", h.Type)
}
@ -89,6 +92,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
v = &h.NaiveOptions
case C.TypeHysteria:
v = &h.HysteriaOptions
case C.TypeShadowTLS:
v = &h.ShadowTLSOptions
default:
return E.New("unknown inbound type: ", h.Type)
}

View file

@ -20,6 +20,7 @@ type _Outbound struct {
HysteriaOptions HysteriaOutboundOptions `json:"-"`
TorOptions TorOutboundOptions `json:"-"`
SSHOptions SSHOutboundOptions `json:"-"`
ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"`
SelectorOptions SelectorOutboundOptions `json:"-"`
}
@ -50,6 +51,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
v = h.TorOptions
case C.TypeSSH:
v = h.SSHOptions
case C.TypeShadowTLS:
v = h.ShadowTLSOptions
case C.TypeSelector:
v = h.SelectorOptions
default:
@ -87,6 +90,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
v = &h.TorOptions
case C.TypeSSH:
v = &h.SSHOptions
case C.TypeShadowTLS:
v = &h.ShadowTLSOptions
case C.TypeSelector:
v = &h.SelectorOptions
default:

17
option/shadowtls.go Normal file
View 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"`
}

View file

@ -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)
case C.TypeSSH:
return NewSSH(ctx, router, logger, options.Tag, options.SSHOptions)
case C.TypeShadowTLS:
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
case C.TypeSelector:
return NewSelector(router, logger, options.Tag, options.SelectorOptions)
default:

84
outbound/shadowtls.go Normal file
View 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
}

View file

@ -519,8 +519,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
if !common.Contains(injectable.Network(), N.NetworkTCP) {
return E.New("inject: TCP unsupported")
}
metadata.InboundDetour = ""
metadata.LastInbound = metadata.Inbound
metadata.Inbound = metadata.InboundDetour
metadata.InboundDetour = ""
err := injectable.NewConnection(ctx, conn, metadata)
if err != nil {
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) {
return E.New("inject: UDP unsupported")
}
metadata.InboundDetour = ""
metadata.LastInbound = metadata.Inbound
metadata.Inbound = metadata.InboundDetour
metadata.InboundDetour = ""
err := injectable.NewPacketConnection(ctx, conn, metadata)
if err != nil {
return E.Cause(err, "inject ", detour.Tag())

View file

@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/debug"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/protocol/socks"
@ -17,6 +18,11 @@ import (
)
func startInstance(t *testing.T, options option.Options) {
if debug.Enabled {
options.Log = &option.LogOptions{
Level: "trace",
}
}
var instance *box.Box
var err error
for retry := 0; retry < 3; retry++ {

View file

@ -36,6 +36,7 @@ const (
ImageBoringTun = "ghcr.io/ntkme/boringtun:edge"
ImageHysteria = "tobyxdd/hysteria:latest"
ImageNginx = "nginx:stable"
ImageShadowTLS = "ghcr.io/ihciah/shadow-tls:latest"
)
var allImages = []string{
@ -46,7 +47,8 @@ var allImages = []string{
ImageNaive,
ImageBoringTun,
ImageHysteria,
// ImageNginx,
ImageNginx,
ImageShadowTLS,
}
var localIP = netip.MustParseAddr("127.0.0.1")

170
test/shadowtls_test.go Normal file
View 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)
}