diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index a809728e..c6cfa914 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -13,6 +13,7 @@ import ( C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/ntp" gost "github.com/metacubex/mihomo/transport/gost-plugin" + "github.com/metacubex/mihomo/transport/kcptun" "github.com/metacubex/mihomo/transport/restls" obfs "github.com/metacubex/mihomo/transport/simple-obfs" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" @@ -36,6 +37,7 @@ type ShadowSocks struct { gostOption *gost.Option shadowTLSOption *shadowtls.ShadowTLSOption restlsConfig *restls.Config + kcptunClient *kcptun.Client } type ShadowSocksOption struct { @@ -106,6 +108,32 @@ type restlsOption struct { RestlsScript string `obfs:"restls-script,omitempty"` } +type kcpTunOption struct { + Key string `obfs:"key,omitempty"` + Crypt string `obfs:"crypt,omitempty"` + Mode string `obfs:"mode,omitempty"` + Conn int `obfs:"conn,omitempty"` + AutoExpire int `obfs:"autoexpire,omitempty"` + ScavengeTTL int `obfs:"scavengettl,omitempty"` + MTU int `obfs:"mtu,omitempty"` + SndWnd int `obfs:"sndwnd,omitempty"` + RcvWnd int `obfs:"rcvwnd,omitempty"` + DataShard int `obfs:"datashard,omitempty"` + ParityShard int `obfs:"parityshard,omitempty"` + DSCP int `obfs:"dscp,omitempty"` + NoComp bool `obfs:"nocomp,omitempty"` + AckNodelay bool `obfs:"acknodelay,omitempty"` + NoDelay int `obfs:"nodelay,omitempty"` + Interval int `obfs:"interval,omitempty"` + Resend int `obfs:"resend,omitempty"` + NoCongestion int `obfs:"nc,omitempty"` + SockBuf int `obfs:"sockbuf,omitempty"` + SmuxVer int `obfs:"smuxver,omitempty"` + SmuxBuf int `obfs:"smuxbuf,omitempty"` + StreamBuf int `obfs:"streambuf,omitempty"` + KeepAlive int `obfs:"keepalive,omitempty"` +} + // StreamConnContext implements C.ProxyAdapter func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { useEarly := false @@ -174,7 +202,27 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale return nil, err } } - c, err := dialer.DialContext(ctx, "tcp", ss.addr) + var c net.Conn + if ss.kcptunClient != nil { + c, err = ss.kcptunClient.OpenStream(ctx, func(ctx context.Context) (net.PacketConn, net.Addr, error) { + if err = ss.ResolveUDP(ctx, metadata); err != nil { + return nil, nil, err + } + addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer) + if err != nil { + return nil, nil, err + } + + pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) + if err != nil { + return nil, nil, err + } + + return pc, addr, nil + }) + } else { + c, err = dialer.DialContext(ctx, "tcp", ss.addr) + } if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } @@ -256,6 +304,13 @@ func (ss *ShadowSocks) SupportUOT() bool { return ss.option.UDPOverTCP } +func (ss *ShadowSocks) Close() error { + if ss.kcptunClient != nil { + return ss.kcptunClient.Close() + } + return nil +} + func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) method, err := shadowsocks.CreateMethod(option.Cipher, shadowsocks.MethodOptions{ @@ -271,6 +326,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { var obfsOption *simpleObfsOption var shadowTLSOpt *shadowtls.ShadowTLSOption var restlsConfig *restls.Config + var kcptunClient *kcptun.Client obfsMode := "" decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) @@ -384,6 +440,39 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) } + } else if option.Plugin == kcptun.Mode { + obfsMode = kcptun.Mode + kcptunOpt := &kcpTunOption{} + if err := decoder.Decode(option.PluginOpts, kcptunOpt); err != nil { + return nil, fmt.Errorf("ss %s initialize kcptun-plugin error: %w", addr, err) + } + + kcptunClient = kcptun.NewClient(kcptun.Config{ + Key: kcptunOpt.Key, + Crypt: kcptunOpt.Crypt, + Mode: kcptunOpt.Mode, + Conn: kcptunOpt.Conn, + AutoExpire: kcptunOpt.AutoExpire, + ScavengeTTL: kcptunOpt.ScavengeTTL, + MTU: kcptunOpt.MTU, + SndWnd: kcptunOpt.SndWnd, + RcvWnd: kcptunOpt.RcvWnd, + DataShard: kcptunOpt.DataShard, + ParityShard: kcptunOpt.ParityShard, + DSCP: kcptunOpt.DSCP, + NoComp: kcptunOpt.NoComp, + AckNodelay: kcptunOpt.AckNodelay, + NoDelay: kcptunOpt.NoDelay, + Interval: kcptunOpt.Interval, + Resend: kcptunOpt.Resend, + NoCongestion: kcptunOpt.NoCongestion, + SockBuf: kcptunOpt.SockBuf, + SmuxVer: kcptunOpt.SmuxVer, + SmuxBuf: kcptunOpt.SmuxBuf, + StreamBuf: kcptunOpt.StreamBuf, + KeepAlive: kcptunOpt.KeepAlive, + }) + option.UDPOverTCP = true // must open uot } switch option.UDPOverTCPVersion { case uot.Version, uot.LegacyVersion: @@ -414,5 +503,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { obfsOption: obfsOption, shadowTLSOption: shadowTLSOpt, restlsConfig: restlsConfig, + kcptunClient: kcptunClient, }, nil } diff --git a/docs/config.yaml b/docs/config.yaml index adb7294c..b2d86c23 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -534,6 +534,37 @@ proxies: # socks5 version-hint: "tls12" restls-script: "1000?100<1,500~100,350~100,600~100,400~200" + - name: "ss-kcptun" + type: ss + server: [YOUR_SERVER_IP] + port: 443 + cipher: chacha20-ietf-poly1305 + password: [YOUR_SS_PASSWORD] + plugin: kcptun + plugin-opts: + key: it's a secrect # pre-shared secret between client and server + crypt: aes # aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, sm4, none, null + mode: fast # profiles: fast3, fast2, fast, normal, manual + conn: 1 # set num of UDP connections to server + autoexpire: 0 # set auto expiration time(in seconds) for a single UDP connection, 0 to disable + scavengettl: 600 # set how long an expired connection can live (in seconds) + mtu: 1350 # set maximum transmission unit for UDP packets + sndwnd: 128 # set send window size(num of packets) + rcvwnd: 512 # set receive window size(num of packets) + datashard: 10 # set reed-solomon erasure coding - datashard + parityshard: 3 # set reed-solomon erasure coding - parityshard + dscp: 0 # set DSCP(6bit) + nocomp: false # disable compression + acknodelay: false # flush ack immediately when a packet is received + nodelay: 0 + interval: 50 + resend: false + sockbuf: 4194304 # per-socket buffer in bytes + smuxver: 1 # specify smux version, available 1,2 + smuxbuf: 4194304 # the overall de-mux buffer in bytes + streambuf: 2097152 # per stream receive buffer in bytes, smux v2+ + keepalive: 10 # seconds between heartbeats + # vmess # cipher 支持 auto/aes-128-gcm/chacha20-poly1305/none - name: "vmess" @@ -1336,6 +1367,30 @@ listeners: # password: password # handshake: # dest: test.com:443 + # kcp-tun: + # enable: false + # key: it's a secrect # pre-shared secret between client and server + # crypt: aes # aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, sm4, none, null + # mode: fast # profiles: fast3, fast2, fast, normal, manual + # conn: 1 # set num of UDP connections to server + # autoexpire: 0 # set auto expiration time(in seconds) for a single UDP connection, 0 to disable + # scavengettl: 600 # set how long an expired connection can live (in seconds) + # mtu: 1350 # set maximum transmission unit for UDP packets + # sndwnd: 128 # set send window size(num of packets) + # rcvwnd: 512 # set receive window size(num of packets) + # datashard: 10 # set reed-solomon erasure coding - datashard + # parityshard: 3 # set reed-solomon erasure coding - parityshard + # dscp: 0 # set DSCP(6bit) + # nocomp: false # disable compression + # acknodelay: false # flush ack immediately when a packet is received + # nodelay: 0 + # interval: 50 + # resend: false + # sockbuf: 4194304 # per-socket buffer in bytes + # smuxver: 1 # specify smux version, available 1,2 + # smuxbuf: 4194304 # the overall de-mux buffer in bytes + # streambuf: 2097152 # per stream receive buffer in bytes, smux v2+ + # keepalive: 10 # seconds between heartbeats - name: vmess-in-1 type: vmess diff --git a/go.mod b/go.mod index 1f52ff8a..8d808cf7 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/go-chi/render v1.0.3 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.3.2 + github.com/golang/snappy v1.0.0 github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20 github.com/mdlayher/netlink v1.7.2 @@ -21,6 +22,7 @@ require ( github.com/metacubex/chacha v0.1.5 github.com/metacubex/fswatch v0.1.1 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 + github.com/metacubex/kcp-go v0.0.0-20250922034656-df9a2b90cdf7 github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 github.com/metacubex/randv2 v0.2.0 github.com/metacubex/restls-client-go v0.1.7 @@ -83,6 +85,8 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/josharian/native v1.1.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/klauspost/reedsolomon v1.12.3 // indirect github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/go.sum b/go.sum index b4a6bea8..f9e1c7af 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -77,6 +79,10 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/reedsolomon v1.12.3 h1:tzUznbfc3OFwJaTebv/QdhnFf2Xvb7gZ24XaHLBPmdc= +github.com/klauspost/reedsolomon v1.12.3/go.mod h1:3K5rXwABAvzGeR01r6pWZieUALXO/Tq7bFKGIb4m4WI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -106,6 +112,8 @@ github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvO github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 h1:N5GExQJqYAH3gOCshpp2u/J3CtNYzMctmlb0xK9wtbQ= github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= +github.com/metacubex/kcp-go v0.0.0-20250922034656-df9a2b90cdf7 h1:vGsrjQxlepSfkMALzJuvDzd+wp6NvKXpoyPuPb4SYCE= +github.com/metacubex/kcp-go v0.0.0-20250922034656-df9a2b90cdf7/go.mod h1:HIJZW4QMhbBqXuqC1ly6Hn0TEYT2SzRw58ns1yGhXTs= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA= github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 h1:8JVlYuE8uSJAvmyCd4TjvDxs57xjb0WxEoaWafK5+qs= @@ -216,6 +224,7 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= @@ -256,6 +265,7 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/listener/config/kcptun.go b/listener/config/kcptun.go new file mode 100644 index 00000000..02293dfa --- /dev/null +++ b/listener/config/kcptun.go @@ -0,0 +1,8 @@ +package config + +import "github.com/metacubex/mihomo/transport/kcptun" + +type KcpTun struct { + Enable bool `json:"enable"` + kcptun.Config `json:",inline"` +} diff --git a/listener/config/shadowsocks.go b/listener/config/shadowsocks.go index 442743ef..37bbb721 100644 --- a/listener/config/shadowsocks.go +++ b/listener/config/shadowsocks.go @@ -14,6 +14,7 @@ type ShadowsocksServer struct { Udp bool MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` + KcpTun KcpTun `yaml:"kcp-tun" json:"kcp-tun,omitempty"` } func (t ShadowsocksServer) String() string { diff --git a/listener/inbound/kcptun.go b/listener/inbound/kcptun.go new file mode 100644 index 00000000..e4098f96 --- /dev/null +++ b/listener/inbound/kcptun.go @@ -0,0 +1,64 @@ +package inbound + +import ( + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/transport/kcptun" +) + +type KcpTun struct { + Enable bool `inbound:"enable"` + Key string `inbound:"key,omitempty"` + Crypt string `inbound:"crypt,omitempty"` + Mode string `inbound:"mode,omitempty"` + Conn int `inbound:"conn,omitempty"` + AutoExpire int `inbound:"autoexpire,omitempty"` + ScavengeTTL int `inbound:"scavengettl,omitempty"` + MTU int `inbound:"mtu,omitempty"` + SndWnd int `inbound:"sndwnd,omitempty"` + RcvWnd int `inbound:"rcvwnd,omitempty"` + DataShard int `inbound:"datashard,omitempty"` + ParityShard int `inbound:"parityshard,omitempty"` + DSCP int `inbound:"dscp,omitempty"` + NoComp bool `inbound:"nocomp,omitempty"` + AckNodelay bool `inbound:"acknodelay,omitempty"` + NoDelay int `inbound:"nodelay,omitempty"` + Interval int `inbound:"interval,omitempty"` + Resend int `inbound:"resend,omitempty"` + NoCongestion int `inbound:"nc,omitempty"` + SockBuf int `inbound:"sockbuf,omitempty"` + SmuxVer int `inbound:"smuxver,omitempty"` + SmuxBuf int `inbound:"smuxbuf,omitempty"` + StreamBuf int `inbound:"streambuf,omitempty"` + KeepAlive int `inbound:"keepalive,omitempty"` +} + +func (c KcpTun) Build() LC.KcpTun { + return LC.KcpTun{ + Enable: c.Enable, + Config: kcptun.Config{ + Key: c.Key, + Crypt: c.Crypt, + Mode: c.Mode, + Conn: c.Conn, + AutoExpire: c.AutoExpire, + ScavengeTTL: c.ScavengeTTL, + MTU: c.MTU, + SndWnd: c.SndWnd, + RcvWnd: c.RcvWnd, + DataShard: c.DataShard, + ParityShard: c.ParityShard, + DSCP: c.DSCP, + NoComp: c.NoComp, + AckNodelay: c.AckNodelay, + NoDelay: c.NoDelay, + Interval: c.Interval, + Resend: c.Resend, + NoCongestion: c.NoCongestion, + SockBuf: c.SockBuf, + SmuxVer: c.SmuxVer, + SmuxBuf: c.SmuxBuf, + StreamBuf: c.StreamBuf, + KeepAlive: c.KeepAlive, + }, + } +} diff --git a/listener/inbound/shadowsocks.go b/listener/inbound/shadowsocks.go index 994f4c59..b88013a8 100644 --- a/listener/inbound/shadowsocks.go +++ b/listener/inbound/shadowsocks.go @@ -16,6 +16,7 @@ type ShadowSocksOption struct { UDP bool `inbound:"udp,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` + KcpTun KcpTun `inbound:"kcp-tun,omitempty"` } func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { @@ -45,6 +46,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) { Udp: options.UDP, MuxOption: options.MuxOption.Build(), ShadowTLS: options.ShadowTLS.Build(), + KcpTun: options.KcpTun.Build(), }, }, nil } diff --git a/listener/inbound/shadowsocks_test.go b/listener/inbound/shadowsocks_test.go index 7a26eeca..9950984d 100644 --- a/listener/inbound/shadowsocks_test.go +++ b/listener/inbound/shadowsocks_test.go @@ -10,6 +10,7 @@ import ( "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" + "github.com/metacubex/mihomo/transport/kcptun" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" shadowsocks "github.com/metacubex/sing-shadowsocks" @@ -21,7 +22,7 @@ import ( var noneList = []string{shadowsocks.MethodNone} var shadowsocksCipherLists = [][]string{noneList, shadowaead.List, shadowaead_2022.List, shadowstream.List} -var shadowsocksCipherShortLists = [][]string{noneList, shadowaead.List[:5]} // for test shadowTLS +var shadowsocksCipherShortLists = [][]string{noneList, shadowaead.List[:5]} // for test shadowTLS and kcptun var shadowsocksPassword32 string var shadowsocksPassword16 string @@ -32,11 +33,11 @@ func init() { shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16]) } -func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherLists [][]string) { +func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherLists [][]string, enableSingMux bool) { t.Parallel() for _, cipherList := range cipherLists { for i, cipher := range cipherList { - enableSingMux := i == 0 + enableSingMux := enableSingMux && i == 0 cipher := cipher t.Run(cipher, func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value @@ -100,19 +101,19 @@ func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOpt func TestInboundShadowSocks_Basic(t *testing.T) { inboundOptions := inbound.ShadowSocksOption{} outboundOptions := outbound.ShadowSocksOption{} - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherLists) + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherLists, true) } func testInboundShadowSocksShadowTls(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) { t.Parallel() t.Run("Conn", func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists) + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, true) }) t.Run("UConn", func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value outboundOptions.ClientFingerprint = "chrome" - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists) + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, true) }) } @@ -163,3 +164,17 @@ func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) { } testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions) } + +func TestInboundShadowSocks_KcpTun(t *testing.T) { + inboundOptions := inbound.ShadowSocksOption{ + KcpTun: inbound.KcpTun{ + Enable: true, + Key: shadowsocksPassword16, + }, + } + outboundOptions := outbound.ShadowSocksOption{ + Plugin: kcptun.Mode, + PluginOpts: map[string]any{"key": shadowsocksPassword16}, + } + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false) +} diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go index 65adce65..e106867d 100644 --- a/listener/sing_shadowsocks/server.go +++ b/listener/sing_shadowsocks/server.go @@ -14,6 +14,7 @@ import ( "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" + "github.com/metacubex/mihomo/transport/kcptun" shadowsocks "github.com/metacubex/sing-shadowsocks" "github.com/metacubex/sing-shadowsocks/shadowaead" @@ -138,6 +139,12 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi } } + var kcptunServer *kcptun.Server + if config.KcpTun.Enable { + kcptunServer = kcptun.NewServer(config.KcpTun.Config) + config.Udp = true + } + for _, addr := range strings.Split(config.Listen, ",") { addr := addr @@ -154,6 +161,14 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi sl.udpListeners = append(sl.udpListeners, ul) + if kcptunServer != nil { + go kcptunServer.Serve(ul, func(c net.Conn) { + sl.HandleConn(c, tunnel) + }) + + continue // skip tcp listener + } + go func() { conn := bufio.NewPacketConn(ul) rwOptions := network.NewReadWaitOptions(conn, sl.service) diff --git a/transport/kcptun/LICENSE.md b/transport/kcptun/LICENSE.md new file mode 100644 index 00000000..b0c255f1 --- /dev/null +++ b/transport/kcptun/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2016 xtaci + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/transport/kcptun/client.go b/transport/kcptun/client.go new file mode 100644 index 00000000..af84133e --- /dev/null +++ b/transport/kcptun/client.go @@ -0,0 +1,173 @@ +package kcptun + +import ( + "context" + "crypto/rand" + "encoding/binary" + "net" + "sync" + "time" + + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/kcp-go" + "github.com/metacubex/smux" +) + +const Mode = "kcptun" + +type DialFn func(ctx context.Context) (net.PacketConn, net.Addr, error) + +type Client struct { + once sync.Once + config Config + block kcp.BlockCrypt + + ctx context.Context + cancel context.CancelFunc + + numconn uint16 + muxes []timedSession + rr uint16 + connMu sync.Mutex + + chScavenger chan timedSession +} + +func NewClient(config Config) *Client { + config.FillDefaults() + block := config.NewBlock() + + ctx, cancel := context.WithCancel(context.Background()) + + return &Client{ + config: config, + block: block, + ctx: ctx, + cancel: cancel, + } +} + +func (c *Client) Close() error { + c.cancel() + return nil +} + +func (c *Client) createConn(ctx context.Context, dial DialFn) (*smux.Session, error) { + conn, addr, err := dial(ctx) + if err != nil { + return nil, err + } + + config := c.config + var convid uint32 + binary.Read(rand.Reader, binary.LittleEndian, &convid) + kcpconn, err := kcp.NewConn4(convid, addr, c.block, config.DataShard, config.ParityShard, true, conn) + if err != nil { + return nil, err + } + kcpconn.SetStreamMode(true) + kcpconn.SetWriteDelay(false) + kcpconn.SetNoDelay(config.NoDelay, config.Interval, config.Resend, config.NoCongestion) + kcpconn.SetWindowSize(config.SndWnd, config.RcvWnd) + kcpconn.SetMtu(config.MTU) + kcpconn.SetACKNoDelay(config.AckNodelay) + + _ = kcpconn.SetDSCP(config.DSCP) + _ = kcpconn.SetReadBuffer(config.SockBuf) + _ = kcpconn.SetWriteBuffer(config.SockBuf) + smuxConfig := smux.DefaultConfig() + smuxConfig.Version = config.SmuxVer + smuxConfig.MaxReceiveBuffer = config.SmuxBuf + smuxConfig.MaxStreamBuffer = config.StreamBuf + smuxConfig.KeepAliveInterval = time.Duration(config.KeepAlive) * time.Second + if smuxConfig.KeepAliveInterval >= smuxConfig.KeepAliveTimeout { + smuxConfig.KeepAliveTimeout = 3 * smuxConfig.KeepAliveInterval + } + + if err := smux.VerifyConfig(smuxConfig); err != nil { + return nil, err + } + + var netConn net.Conn = kcpconn + if !config.NoComp { + netConn = NewCompStream(netConn) + } + // stream multiplex + return smux.Client(netConn, smuxConfig) +} + +func (c *Client) OpenStream(ctx context.Context, dial DialFn) (*smux.Stream, error) { + c.once.Do(func() { + // start scavenger if autoexpire is set + c.chScavenger = make(chan timedSession, 128) + if c.config.AutoExpire > 0 { + go scavenger(c.ctx, c.chScavenger, &c.config) + } + + c.numconn = uint16(c.config.Conn) + c.muxes = make([]timedSession, c.config.Conn) + c.rr = uint16(0) + }) + + c.connMu.Lock() + idx := c.rr % c.numconn + + // do auto expiration && reconnection + if c.muxes[idx].session == nil || c.muxes[idx].session.IsClosed() || + (c.config.AutoExpire > 0 && time.Now().After(c.muxes[idx].expiryDate)) { + var err error + c.muxes[idx].session, err = c.createConn(ctx, dial) + if err != nil { + c.connMu.Unlock() + return nil, err + } + c.muxes[idx].expiryDate = time.Now().Add(time.Duration(c.config.AutoExpire) * time.Second) + if c.config.AutoExpire > 0 { // only when autoexpire set + c.chScavenger <- c.muxes[idx] + } + + } + c.rr++ + session := c.muxes[idx].session + c.connMu.Unlock() + + return session.OpenStream() +} + +// timedSession is a wrapper for smux.Session with expiry date +type timedSession struct { + session *smux.Session + expiryDate time.Time +} + +// scavenger goroutine is used to close expired sessions +func scavenger(ctx context.Context, ch chan timedSession, config *Config) { + ticker := time.NewTicker(scavengePeriod * time.Second) + defer ticker.Stop() + var sessionList []timedSession + for { + select { + case item := <-ch: + sessionList = append(sessionList, timedSession{ + item.session, + item.expiryDate.Add(time.Duration(config.ScavengeTTL) * time.Second)}) + case <-ticker.C: + var newList []timedSession + for k := range sessionList { + s := sessionList[k] + if s.session.IsClosed() { + log.Debugln("scavenger: session normally closed: %s", s.session.LocalAddr()) + } else if time.Now().After(s.expiryDate) { + s.session.Close() + log.Debugln("scavenger: session closed due to ttl: %s", s.session.LocalAddr()) + } else { + newList = append(newList, sessionList[k]) + } + } + sessionList = newList + case <-ctx.Done(): + return + } + } +} diff --git a/transport/kcptun/common.go b/transport/kcptun/common.go new file mode 100644 index 00000000..ec260f06 --- /dev/null +++ b/transport/kcptun/common.go @@ -0,0 +1,152 @@ +package kcptun + +import ( + "crypto/sha1" + + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/kcp-go" + "golang.org/x/crypto/pbkdf2" +) + +const ( + // SALT is use for pbkdf2 key expansion + SALT = "kcp-go" + // maximum supported smux version + maxSmuxVer = 2 + // scavenger check period + scavengePeriod = 5 +) + +type Config struct { + Key string `json:"key"` + Crypt string `json:"crypt"` + Mode string `json:"mode"` + Conn int `json:"conn"` + AutoExpire int `json:"autoexpire"` + ScavengeTTL int `json:"scavengettl"` + MTU int `json:"mtu"` + SndWnd int `json:"sndwnd"` + RcvWnd int `json:"rcvwnd"` + DataShard int `json:"datashard"` + ParityShard int `json:"parityshard"` + DSCP int `json:"dscp"` + NoComp bool `json:"nocomp"` + AckNodelay bool `json:"acknodelay"` + NoDelay int `json:"nodelay"` + Interval int `json:"interval"` + Resend int `json:"resend"` + NoCongestion int `json:"nc"` + SockBuf int `json:"sockbuf"` + SmuxVer int `json:"smuxver"` + SmuxBuf int `json:"smuxbuf"` + StreamBuf int `json:"streambuf"` + KeepAlive int `json:"keepalive"` +} + +func (config *Config) FillDefaults() { + if config.Key == "" { + config.Key = "it's a secrect" + } + if config.Crypt == "" { + config.Crypt = "aes" + } + if config.Mode == "" { + config.Mode = "fast" + } + if config.Conn == 0 { + config.Conn = 1 + } + if config.ScavengeTTL == 0 { + config.ScavengeTTL = 600 + } + if config.MTU == 0 { + config.MTU = 1350 + } + if config.SndWnd == 0 { + config.SndWnd = 128 + } + if config.RcvWnd == 0 { + config.RcvWnd = 512 + } + if config.DataShard == 0 { + config.DataShard = 10 + } + if config.ParityShard == 0 { + config.ParityShard = 3 + } + if config.Interval == 0 { + config.Interval = 50 + } + if config.SockBuf == 0 { + config.SockBuf = 4194304 + } + if config.SmuxVer == 0 { + config.SmuxVer = 1 + } + if config.SmuxBuf == 0 { + config.SmuxBuf = 4194304 + } + if config.StreamBuf == 0 { + config.StreamBuf = 2097152 + } + if config.KeepAlive == 0 { + config.KeepAlive = 10 + } + switch config.Mode { + case "normal": + config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 0, 40, 2, 1 + case "fast": + config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 0, 30, 2, 1 + case "fast2": + config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 1, 20, 2, 1 + case "fast3": + config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 1, 10, 2, 1 + } + + // SMUX Version check + if config.SmuxVer > maxSmuxVer { + log.Warnln("unsupported smux version: %d", config.SmuxVer) + config.SmuxVer = maxSmuxVer + } + + // Scavenge parameters check + if config.AutoExpire != 0 && config.ScavengeTTL > config.AutoExpire { + log.Warnln("WARNING: scavengettl is bigger than autoexpire, connections may race hard to use bandwidth.") + log.Warnln("Try limiting scavengettl to a smaller value.") + } +} + +func (config *Config) NewBlock() (block kcp.BlockCrypt) { + pass := pbkdf2.Key([]byte(config.Key), []byte(SALT), 4096, 32, sha1.New) + switch config.Crypt { + case "null": + block = nil + case "tea": + block, _ = kcp.NewTEABlockCrypt(pass[:16]) + case "xor": + block, _ = kcp.NewSimpleXORBlockCrypt(pass) + case "none": + block, _ = kcp.NewNoneBlockCrypt(pass) + case "aes-128": + block, _ = kcp.NewAESBlockCrypt(pass[:16]) + case "aes-192": + block, _ = kcp.NewAESBlockCrypt(pass[:24]) + case "blowfish": + block, _ = kcp.NewBlowfishBlockCrypt(pass) + case "twofish": + block, _ = kcp.NewTwofishBlockCrypt(pass) + case "cast5": + block, _ = kcp.NewCast5BlockCrypt(pass[:16]) + case "3des": + block, _ = kcp.NewTripleDESBlockCrypt(pass[:24]) + case "xtea": + block, _ = kcp.NewXTEABlockCrypt(pass[:16]) + case "salsa20": + block, _ = kcp.NewSalsa20BlockCrypt(pass) + default: + config.Crypt = "aes" + block, _ = kcp.NewAESBlockCrypt(pass) + } + return +} diff --git a/transport/kcptun/comp.go b/transport/kcptun/comp.go new file mode 100644 index 00000000..1efdd42a --- /dev/null +++ b/transport/kcptun/comp.go @@ -0,0 +1,63 @@ +package kcptun + +import ( + "net" + "time" + + "github.com/golang/snappy" +) + +// CompStream is a net.Conn wrapper that compresses data using snappy +type CompStream struct { + conn net.Conn + w *snappy.Writer + r *snappy.Reader +} + +func (c *CompStream) Read(p []byte) (n int, err error) { + return c.r.Read(p) +} + +func (c *CompStream) Write(p []byte) (n int, err error) { + if _, err := c.w.Write(p); err != nil { + return 0, err + } + + if err := c.w.Flush(); err != nil { + return 0, err + } + return len(p), err +} + +func (c *CompStream) Close() error { + return c.conn.Close() +} + +func (c *CompStream) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *CompStream) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *CompStream) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *CompStream) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *CompStream) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +// NewCompStream creates a new stream that compresses data using snappy +func NewCompStream(conn net.Conn) *CompStream { + c := new(CompStream) + c.conn = conn + c.w = snappy.NewBufferedWriter(conn) + c.r = snappy.NewReader(conn) + return c +} diff --git a/transport/kcptun/doc.go b/transport/kcptun/doc.go new file mode 100644 index 00000000..e0801d28 --- /dev/null +++ b/transport/kcptun/doc.go @@ -0,0 +1,5 @@ +// Package kcptun copy and modify from: +// https://github.com/xtaci/kcptun/tree/52492c72592627d0005cbedbc4ba37fc36a95c3f +// adopt for mihomo +// without SM4,QPP,tcpraw support +package kcptun diff --git a/transport/kcptun/server.go b/transport/kcptun/server.go new file mode 100644 index 00000000..5fcd440c --- /dev/null +++ b/transport/kcptun/server.go @@ -0,0 +1,79 @@ +package kcptun + +import ( + "net" + "time" + + "github.com/metacubex/kcp-go" + "github.com/metacubex/smux" +) + +type Server struct { + config Config + block kcp.BlockCrypt +} + +func NewServer(config Config) *Server { + config.FillDefaults() + block := config.NewBlock() + + return &Server{ + config: config, + block: block, + } +} + +func (s *Server) Serve(pc net.PacketConn, handler func(net.Conn)) error { + lis, err := kcp.ServeConn(s.block, s.config.DataShard, s.config.ParityShard, pc) + if err != nil { + return err + } + defer lis.Close() + _ = lis.SetDSCP(s.config.DSCP) + _ = lis.SetReadBuffer(s.config.SockBuf) + _ = lis.SetWriteBuffer(s.config.SockBuf) + for { + conn, err := lis.AcceptKCP() + if err != nil { + return err + } + conn.SetStreamMode(true) + conn.SetWriteDelay(false) + conn.SetNoDelay(s.config.NoDelay, s.config.Interval, s.config.Resend, s.config.NoCongestion) + conn.SetMtu(s.config.MTU) + conn.SetWindowSize(s.config.SndWnd, s.config.RcvWnd) + conn.SetACKNoDelay(s.config.AckNodelay) + + var netConn net.Conn = conn + if !s.config.NoComp { + netConn = NewCompStream(netConn) + } + + go func() { + // stream multiplex + smuxConfig := smux.DefaultConfig() + smuxConfig.Version = s.config.SmuxVer + smuxConfig.MaxReceiveBuffer = s.config.SmuxBuf + smuxConfig.MaxStreamBuffer = s.config.StreamBuf + smuxConfig.KeepAliveInterval = time.Duration(s.config.KeepAlive) * time.Second + if smuxConfig.KeepAliveInterval >= smuxConfig.KeepAliveTimeout { + smuxConfig.KeepAliveTimeout = 3 * smuxConfig.KeepAliveInterval + } + + mux, err := smux.Server(netConn, smuxConfig) + if err != nil { + return + } + defer mux.Close() + + for { + stream, err := mux.AcceptStream() + if err != nil { + return + } + go handler(stream) + } + }() + + } +}