Browse Source

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

世界 3 năm trước cách đây
mục cha
commit
ce567ffdde

+ 2 - 0
option/shadowsocks.go

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

+ 15 - 1
outbound/shadowsocks.go

@@ -10,6 +10,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/sip003"
 	"github.com/sagernet/sing-shadowsocks"
 	"github.com/sagernet/sing-shadowsocks/shadowimpl"
 	"github.com/sagernet/sing/common"
@@ -27,6 +28,7 @@ type Shadowsocks struct {
 	dialer          N.Dialer
 	method          shadowsocks.Method
 	serverAddr      M.Socksaddr
+	plugin          sip003.Plugin
 	uot             bool
 	multiplexDialer N.Dialer
 }
@@ -49,6 +51,12 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 		serverAddr: options.ServerOptions.Build(),
 		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 {
 		outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions))
 		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) {
 	switch N.NetworkName(network) {
 	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 {
 			return nil, err
 		}

+ 4 - 0
transport/simple-obfs/README.md

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

+ 94 - 0
transport/simple-obfs/http.go

@@ -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,
+	}
+}

+ 200 - 0
transport/simple-obfs/tls.go

@@ -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 - 0
transport/sip003/args.go

@@ -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 name–value 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 - 0
transport/sip003/obfs.go

@@ -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
+	}
+}

+ 35 - 0
transport/sip003/plugin.go

@@ -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 - 0
transport/sip003/v2ray.go

@@ -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)
+}