Add obfs-local and v2ray-plugin support for shadowsocks outbound

This commit is contained in:
世界 2022-09-12 14:51:20 +08:00
parent 5a9913eca5
commit ce567ffdde
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
9 changed files with 608 additions and 1 deletions

View file

@ -26,6 +26,8 @@ type ShadowsocksOutboundOptions struct {
ServerOptions ServerOptions
Method string `json:"method"` Method string `json:"method"`
Password string `json:"password"` Password string `json:"password"`
Plugin string `json:"plugin,omitempty"`
PluginOptions string `json:"plugin_opts,omitempty"`
Network NetworkList `json:"network,omitempty"` Network NetworkList `json:"network,omitempty"`
UoT bool `json:"udp_over_tcp,omitempty"` UoT bool `json:"udp_over_tcp,omitempty"`
MultiplexOptions *MultiplexOptions `json:"multiplex,omitempty"` MultiplexOptions *MultiplexOptions `json:"multiplex,omitempty"`

View file

@ -10,6 +10,7 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/sip003"
"github.com/sagernet/sing-shadowsocks" "github.com/sagernet/sing-shadowsocks"
"github.com/sagernet/sing-shadowsocks/shadowimpl" "github.com/sagernet/sing-shadowsocks/shadowimpl"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
@ -27,6 +28,7 @@ type Shadowsocks struct {
dialer N.Dialer dialer N.Dialer
method shadowsocks.Method method shadowsocks.Method
serverAddr M.Socksaddr serverAddr M.Socksaddr
plugin sip003.Plugin
uot bool uot bool
multiplexDialer N.Dialer multiplexDialer N.Dialer
} }
@ -49,6 +51,12 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
serverAddr: options.ServerOptions.Build(), serverAddr: options.ServerOptions.Build(),
uot: options.UoT, uot: options.UoT,
} }
if options.Plugin != "" {
outbound.plugin, err = sip003.CreatePlugin(options.Plugin, options.PluginOptions, router, outbound.dialer, outbound.serverAddr)
if err != nil {
return nil, err
}
}
if !options.UoT { if !options.UoT {
outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions)) outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions))
if err != nil { if err != nil {
@ -135,7 +143,13 @@ type shadowsocksDialer Shadowsocks
func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch N.NetworkName(network) { switch N.NetworkName(network) {
case N.NetworkTCP: case N.NetworkTCP:
outConn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) var outConn net.Conn
var err error
if h.plugin != nil {
outConn, err = h.plugin.DialContext(ctx)
} else {
outConn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -0,0 +1,4 @@
# simple-obfs
mod from https://github.com/Dreamacro/clash/transport/simple-obfs
version: 1.11.8

View file

@ -0,0 +1,94 @@
package obfs
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"net/http"
B "github.com/sagernet/sing/common/buf"
)
// HTTPObfs is shadowsocks http simple-obfs implementation
type HTTPObfs struct {
net.Conn
host string
port string
buf []byte
offset int
firstRequest bool
firstResponse bool
}
func (ho *HTTPObfs) Read(b []byte) (int, error) {
if ho.buf != nil {
n := copy(b, ho.buf[ho.offset:])
ho.offset += n
if ho.offset == len(ho.buf) {
B.Put(ho.buf)
ho.buf = nil
}
return n, nil
}
if ho.firstResponse {
buf := B.Get(B.BufferSize)
n, err := ho.Conn.Read(buf)
if err != nil {
B.Put(buf)
return 0, err
}
idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
if idx == -1 {
B.Put(buf)
return 0, io.EOF
}
ho.firstResponse = false
length := n - (idx + 4)
n = copy(b, buf[idx+4:n])
if length > n {
ho.buf = buf[:idx+4+length]
ho.offset = idx + 4 + n
} else {
B.Put(buf)
}
return n, nil
}
return ho.Conn.Read(b)
}
func (ho *HTTPObfs) Write(b []byte) (int, error) {
if ho.firstRequest {
randBytes := make([]byte, 16)
rand.Read(randBytes)
req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:]))
req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2))
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
req.Host = ho.host
if ho.port != "80" {
req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port)
}
req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes))
req.ContentLength = int64(len(b))
err := req.Write(ho.Conn)
ho.firstRequest = false
return len(b), err
}
return ho.Conn.Write(b)
}
// NewHTTPObfs return a HTTPObfs
func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn {
return &HTTPObfs{
Conn: conn,
firstRequest: true,
firstResponse: true,
host: host,
port: port,
}
}

View file

@ -0,0 +1,200 @@
package obfs
import (
"bytes"
"encoding/binary"
"io"
"math/rand"
"net"
"time"
B "github.com/sagernet/sing/common/buf"
)
func init() {
rand.Seed(time.Now().Unix())
}
const (
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
)
// TLSObfs is shadowsocks tls simple-obfs implementation
type TLSObfs struct {
net.Conn
server string
remain int
firstRequest bool
firstResponse bool
}
func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
buf := B.Get(discardN)
_, err := io.ReadFull(to.Conn, buf)
if err != nil {
return 0, err
}
B.Put(buf)
sizeBuf := make([]byte, 2)
_, err = io.ReadFull(to.Conn, sizeBuf)
if err != nil {
return 0, nil
}
length := int(binary.BigEndian.Uint16(sizeBuf))
if length > len(b) {
n, err := to.Conn.Read(b)
if err != nil {
return n, err
}
to.remain = length - n
return n, nil
}
return io.ReadFull(to.Conn, b[:length])
}
func (to *TLSObfs) Read(b []byte) (int, error) {
if to.remain > 0 {
length := to.remain
if length > len(b) {
length = len(b)
}
n, err := io.ReadFull(to.Conn, b[:length])
to.remain -= n
return n, err
}
if to.firstResponse {
// type + ver + lensize + 91 = 96
// type + ver + lensize + 1 = 6
// type + ver = 3
to.firstResponse = false
return to.read(b, 105)
}
// type + ver = 3
return to.read(b, 3)
}
func (to *TLSObfs) Write(b []byte) (int, error) {
length := len(b)
for i := 0; i < length; i += chunkSize {
end := i + chunkSize
if end > length {
end = length
}
n, err := to.write(b[i:end])
if err != nil {
return n, err
}
}
return length, nil
}
func (to *TLSObfs) write(b []byte) (int, error) {
if to.firstRequest {
helloMsg := makeClientHelloMsg(b, to.server)
_, err := to.Conn.Write(helloMsg)
to.firstRequest = false
return len(b), err
}
buf := B.NewSize(5 + len(b))
defer buf.Release()
buf.Write([]byte{0x17, 0x03, 0x03})
binary.Write(buf, binary.BigEndian, uint16(len(b)))
buf.Write(b)
_, err := to.Conn.Write(buf.Bytes())
return len(b), err
}
// NewTLSObfs return a SimpleObfs
func NewTLSObfs(conn net.Conn, server string) net.Conn {
return &TLSObfs{
Conn: conn,
server: server,
firstRequest: true,
firstResponse: true,
}
}
func makeClientHelloMsg(data []byte, server string) []byte {
random := make([]byte, 28)
sessionID := make([]byte, 32)
rand.Read(random)
rand.Read(sessionID)
buf := &bytes.Buffer{}
// handshake, TLS 1.0 version, length
buf.WriteByte(22)
buf.Write([]byte{0x03, 0x01})
length := uint16(212 + len(data) + len(server))
buf.WriteByte(byte(length >> 8))
buf.WriteByte(byte(length & 0xff))
// clientHello, length, TLS 1.2 version
buf.WriteByte(1)
buf.WriteByte(0)
binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server)))
buf.Write([]byte{0x03, 0x03})
// random with timestamp, sid len, sid
binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
buf.Write(random)
buf.WriteByte(32)
buf.Write(sessionID)
// cipher suites
buf.Write([]byte{0x00, 0x38})
buf.Write([]byte{
0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f,
0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a,
0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d,
0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff,
})
// compression
buf.Write([]byte{0x01, 0x00})
// extension length
binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server)))
// session ticket
buf.Write([]byte{0x00, 0x23})
binary.Write(buf, binary.BigEndian, uint16(len(data)))
buf.Write(data)
// server name
buf.Write([]byte{0x00, 0x00})
binary.Write(buf, binary.BigEndian, uint16(len(server)+5))
binary.Write(buf, binary.BigEndian, uint16(len(server)+3))
buf.WriteByte(0)
binary.Write(buf, binary.BigEndian, uint16(len(server)))
buf.Write([]byte(server))
// ec_point
buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02})
// groups
buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18})
// signature
buf.Write([]byte{
0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05,
0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01,
0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03,
})
// encrypt then mac
buf.Write([]byte{0x00, 0x16, 0x00, 0x00})
// extended master secret
buf.Write([]byte{0x00, 0x17, 0x00, 0x00})
return buf.Bytes()
}

119
transport/sip003/args.go Normal file
View file

@ -0,0 +1,119 @@
package sip003
import (
"bytes"
"fmt"
)
// mod from https://github.com/shadowsocks/v2ray-plugin/blob/master/args.go
// Args maps a string key to a list of values. It is similar to url.Values.
type Args map[string][]string
// Get the first value associated with the given key. If there are any values
// associated with the key, the value return has the value and ok is set to
// true. If there are no values for the given key, value is "" and ok is false.
// If you need access to multiple values, use the map directly.
func (args Args) Get(key string) (value string, ok bool) {
if args == nil {
return "", false
}
vals, ok := args[key]
if !ok || len(vals) == 0 {
return "", false
}
return vals[0], true
}
// Add Append value to the list of values for key.
func (args Args) Add(key, value string) {
args[key] = append(args[key], value)
}
// Return the index of the next unescaped byte in s that is in the term set, or
// else the length of the string if no terminators appear. Additionally return
// the unescaped string up to the returned index.
func indexUnescaped(s string, term []byte) (int, string, error) {
var i int
unesc := make([]byte, 0)
for i = 0; i < len(s); i++ {
b := s[i]
// A terminator byte?
if bytes.IndexByte(term, b) != -1 {
break
}
if b == '\\' {
i++
if i >= len(s) {
return 0, "", fmt.Errorf("nothing following final escape in %q", s)
}
b = s[i]
}
unesc = append(unesc, b)
}
return i, string(unesc), nil
}
// ParsePluginOptions Parse a namevalue mapping as from SS_PLUGIN_OPTIONS.
//
// "<value> is a k=v string value with options that are to be passed to the
// transport. semicolons, equal signs and backslashes must be escaped
// with a backslash."
// Example: secret=nou;cache=/tmp/cache;secret=yes
func ParsePluginOptions(s string) (opts Args, err error) {
opts = make(Args)
if len(s) == 0 {
return
}
i := 0
for {
var key, value string
var offset, begin int
if i >= len(s) {
break
}
begin = i
// Read the key.
offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'})
if err != nil {
return
}
if len(key) == 0 {
err = fmt.Errorf("empty key in %q", s[begin:i])
return
}
i += offset
// End of string or no equals sign?
if i >= len(s) || s[i] != '=' {
opts.Add(key, "1")
// Skip the semicolon.
i++
continue
}
// Skip the equals sign.
i++
// Read the value.
offset, value, err = indexUnescaped(s[i:], []byte{';'})
if err != nil {
return
}
i += offset
opts.Add(key, value)
// Skip the semicolon.
i++
}
return opts, nil
}
// Escape backslashes and all the bytes that are in set.
func backslashEscape(s string, set []byte) string {
var buf bytes.Buffer
for _, b := range []byte(s) {
if b == '\\' || bytes.IndexByte(set, b) != -1 {
buf.WriteByte('\\')
}
buf.WriteByte(b)
}
return buf.String()
}

59
transport/sip003/obfs.go Normal file
View file

@ -0,0 +1,59 @@
package sip003
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/transport/simple-obfs"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
var _ Plugin = (*ObfsLocal)(nil)
func init() {
RegisterPlugin("obfs-local", newObfsLocal)
}
func newObfsLocal(pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
var plugin ObfsLocal
mode := "http"
if obfsMode, loaded := pluginOpts.Get("obfs"); loaded {
mode = obfsMode
}
if obfsHost, loaded := pluginOpts.Get("obfs-host"); loaded {
plugin.host = obfsHost
}
switch mode {
case "http":
case "tls":
plugin.tls = true
default:
return nil, E.New("unknown obfs mode ", mode)
}
plugin.port = F.ToString(serverAddr.Port)
return &plugin, nil
}
type ObfsLocal struct {
dialer N.Dialer
serverAddr M.Socksaddr
tls bool
host string
port string
}
func (o *ObfsLocal) DialContext(ctx context.Context) (net.Conn, error) {
conn, err := o.dialer.DialContext(ctx, N.NetworkTCP, o.serverAddr)
if err != nil {
return nil, err
}
if !o.tls {
return obfs.NewHTTPObfs(conn, o.host, o.port), nil
} else {
return obfs.NewTLSObfs(conn, o.host), nil
}
}

View file

@ -0,0 +1,35 @@
package sip003
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type PluginConstructor func(pluginArgs Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error)
type Plugin interface {
DialContext(ctx context.Context) (net.Conn, error)
}
var plugins map[string]PluginConstructor
func RegisterPlugin(name string, constructor PluginConstructor) {
plugins[name] = constructor
}
func CreatePlugin(name string, pluginArgs string, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
pluginOptions, err := ParsePluginOptions(pluginArgs)
if err != nil {
return nil, E.Cause(err, "parse plugin_opts")
}
constructor, loaded := plugins[name]
if !loaded {
return nil, E.New("plugin not found: ", name)
}
return constructor(pluginOptions, router, dialer, serverAddr)
}

80
transport/sip003/v2ray.go Normal file
View file

@ -0,0 +1,80 @@
package sip003
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/v2ray"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func init() {
RegisterPlugin("v2ray-plugin", newV2RayPlugin)
}
func newV2RayPlugin(pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
var tlsOptions option.OutboundTLSOptions
if _, loaded := pluginOpts.Get("tls"); loaded {
tlsOptions.Enabled = true
}
if certPath, certLoaded := pluginOpts.Get("cert"); certLoaded {
tlsOptions.CertificatePath = certPath
}
if certRaw, certLoaded := pluginOpts.Get("certRaw"); certLoaded {
certHead := "-----BEGIN CERTIFICATE-----"
certTail := "-----END CERTIFICATE-----"
fixedCert := certHead + "\n" + certRaw + "\n" + certTail
tlsOptions.Certificate = fixedCert
}
mode := "websocket"
if modeOpt, loaded := pluginOpts.Get("mode"); loaded {
mode = modeOpt
}
host := "cloudfront.com"
path := "/"
if hostOpt, loaded := pluginOpts.Get("host"); loaded {
host = hostOpt
}
if pathOpt, loaded := pluginOpts.Get("path"); loaded {
path = pathOpt
}
var tlsClient tls.Config
var err error
if tlsOptions.Enabled {
tlsClient, err = tls.NewClient(router, serverAddr.AddrString(), tlsOptions)
if err != nil {
return nil, err
}
}
var transportOptions option.V2RayTransportOptions
switch mode {
case "websocket":
transportOptions = option.V2RayTransportOptions{
Type: C.V2RayTransportTypeWebsocket,
WebsocketOptions: option.V2RayWebsocketOptions{
Headers: map[string]string{
"Host": host,
},
Path: path,
},
}
case "quic":
transportOptions = option.V2RayTransportOptions{
Type: C.V2RayTransportTypeQUIC,
}
default:
return nil, E.New("v2ray-plugin: unknown mode: " + mode)
}
return v2ray.NewClientTransport(context.Background(), dialer, serverAddr, transportOptions, tlsClient)
}