diff --git a/option/shadowsocks.go b/option/shadowsocks.go index fae2a98a..33528292 100644 --- a/option/shadowsocks.go +++ b/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"` diff --git a/outbound/shadowsocks.go b/outbound/shadowsocks.go index 23cf343a..b73e5143 100644 --- a/outbound/shadowsocks.go +++ b/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 } diff --git a/transport/simple-obfs/README.md b/transport/simple-obfs/README.md new file mode 100644 index 00000000..d438d0f9 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/transport/simple-obfs/http.go b/transport/simple-obfs/http.go new file mode 100644 index 00000000..f77a63a8 --- /dev/null +++ b/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, + } +} diff --git a/transport/simple-obfs/tls.go b/transport/simple-obfs/tls.go new file mode 100644 index 00000000..d166de8f --- /dev/null +++ b/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() +} diff --git a/transport/sip003/args.go b/transport/sip003/args.go new file mode 100644 index 00000000..b9fae3da --- /dev/null +++ b/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. +// +// " 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() +} diff --git a/transport/sip003/obfs.go b/transport/sip003/obfs.go new file mode 100644 index 00000000..d13f8516 --- /dev/null +++ b/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 + } +} diff --git a/transport/sip003/plugin.go b/transport/sip003/plugin.go new file mode 100644 index 00000000..53a63de1 --- /dev/null +++ b/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) +} diff --git a/transport/sip003/v2ray.go b/transport/sip003/v2ray.go new file mode 100644 index 00000000..5d498cbe --- /dev/null +++ b/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) +}