diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 94a7763d..eb21561a 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -13,6 +13,7 @@ import ( "github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" + gost "github.com/metacubex/mihomo/transport/gost-plugin" "github.com/metacubex/mihomo/transport/restls" obfs "github.com/metacubex/mihomo/transport/simple-obfs" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" @@ -34,6 +35,7 @@ type ShadowSocks struct { obfsMode string obfsOption *simpleObfsOption v2rayOption *v2rayObfs.Option + gostOption *gost.Option shadowTLSOption *shadowtls.ShadowTLSOption restlsConfig *restlsC.Config } @@ -71,6 +73,17 @@ type v2rayObfsOption struct { V2rayHttpUpgradeFastOpen bool `obfs:"v2ray-http-upgrade-fast-open,omitempty"` } +type gostObfsOption struct { + Mode string `obfs:"mode"` + Host string `obfs:"host,omitempty"` + Path string `obfs:"path,omitempty"` + TLS bool `obfs:"tls,omitempty"` + Fingerprint string `obfs:"fingerprint,omitempty"` + Headers map[string]string `obfs:"headers,omitempty"` + SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` + Mux bool `obfs:"mux,omitempty"` +} + type shadowTLSOption struct { Password string `obfs:"password"` Host string `obfs:"host"` @@ -97,7 +110,13 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) case "websocket": var err error - c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption) + if ss.v2rayOption != nil { + c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption) + } else if ss.gostOption != nil { + c, err = gost.NewGostWebsocket(ctx, c, ss.gostOption) + } else { + return nil, fmt.Errorf("plugin options is required") + } if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } @@ -240,6 +259,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { } var v2rayOption *v2rayObfs.Option + var gostOption *gost.Option var obfsOption *simpleObfsOption var shadowTLSOpt *shadowtls.ShadowTLSOption var restlsConfig *restlsC.Config @@ -281,6 +301,28 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.Fingerprint = opts.Fingerprint } + } else if option.Plugin == "gost-plugin" { + opts := gostObfsOption{Host: "bing.com", Mux: true} + if err := decoder.Decode(option.PluginOpts, &opts); err != nil { + return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err) + } + + if opts.Mode != "websocket" { + return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode) + } + obfsMode = opts.Mode + gostOption = &gost.Option{ + Host: opts.Host, + Path: opts.Path, + Headers: opts.Headers, + Mux: opts.Mux, + } + + if opts.TLS { + gostOption.TLS = true + gostOption.SkipCertVerify = opts.SkipCertVerify + gostOption.Fingerprint = opts.Fingerprint + } } else if option.Plugin == shadowtls.Mode { obfsMode = shadowtls.Mode opt := &shadowTLSOption{ @@ -336,6 +378,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { option: &option, obfsMode: obfsMode, v2rayOption: v2rayOption, + gostOption: gostOption, obfsOption: obfsOption, shadowTLSOption: shadowTLSOpt, restlsConfig: restlsConfig, diff --git a/docs/config.yaml b/docs/config.yaml index 88030a1b..263d67b6 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -449,6 +449,26 @@ proxies: # socks5 password: "shadow_tls_password" version: 2 # support 1/2/3 + - name: "ss5" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: gost-plugin + plugin-opts: + mode: websocket + # tls: true # wss + # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 配置指纹将实现 SSL Pining 效果 + # fingerprint: xxxx + # skip-cert-verify: true + # host: bing.com + # path: "/" + # mux: true + # headers: + # custom: value + - name: "ss-restls-tls13" type: ss server: [YOUR_SERVER_IP] diff --git a/transport/gost-plugin/websocket.go b/transport/gost-plugin/websocket.go new file mode 100644 index 00000000..40bf08f5 --- /dev/null +++ b/transport/gost-plugin/websocket.go @@ -0,0 +1,81 @@ +package gost + +import ( + "context" + "crypto/tls" + "net" + "net/http" + + "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/transport/vmess" + smux "github.com/sagernet/smux" +) + +// Option is options of gost websocket +type Option struct { + Host string + Port string + Path string + Headers map[string]string + TLS bool + SkipCertVerify bool + Fingerprint string + Mux bool +} + +// NewGostWebsocket return a gost websocket +func NewGostWebsocket(ctx context.Context, conn net.Conn, option *Option) (net.Conn, error) { + header := http.Header{} + for k, v := range option.Headers { + header.Add(k, v) + } + + config := &vmess.WebsocketConfig{ + Host: option.Host, + Port: option.Port, + Path: option.Path, + Headers: header, + } + + if option.TLS { + config.TLS = true + tlsConfig := &tls.Config{ + ServerName: option.Host, + InsecureSkipVerify: option.SkipCertVerify, + NextProtos: []string{"http/1.1"}, + } + var err error + config.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + if err != nil { + return nil, err + } + + if host := config.Headers.Get("Host"); host != "" { + config.TLSConfig.ServerName = host + } + } + + var err error + conn, err = vmess.StreamWebsocketConn(ctx, conn, config) + if err != nil { + return nil, err + } + + if option.Mux { + config := smux.DefaultConfig() + config.KeepAliveDisabled = true + + session, err := smux.Client(conn, config) + if err != nil { + return nil, err + } + + stream, err := session.OpenStream() + if err != nil { + return nil, err + } + + conn = stream + } + return conn, nil +}