feat: support kcptun plugin for ss client/server

This commit is contained in:
wwqgtxx 2025-09-22 23:54:30 +08:00
parent e28c8e6a51
commit abe6c3bb35
16 changed files with 752 additions and 7 deletions

View File

@ -13,6 +13,7 @@ import (
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/ntp"
gost "github.com/metacubex/mihomo/transport/gost-plugin" gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/kcptun"
"github.com/metacubex/mihomo/transport/restls" "github.com/metacubex/mihomo/transport/restls"
obfs "github.com/metacubex/mihomo/transport/simple-obfs" obfs "github.com/metacubex/mihomo/transport/simple-obfs"
shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls"
@ -36,6 +37,7 @@ type ShadowSocks struct {
gostOption *gost.Option gostOption *gost.Option
shadowTLSOption *shadowtls.ShadowTLSOption shadowTLSOption *shadowtls.ShadowTLSOption
restlsConfig *restls.Config restlsConfig *restls.Config
kcptunClient *kcptun.Client
} }
type ShadowSocksOption struct { type ShadowSocksOption struct {
@ -106,6 +108,32 @@ type restlsOption struct {
RestlsScript string `obfs:"restls-script,omitempty"` 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 // StreamConnContext implements C.ProxyAdapter
func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
useEarly := false useEarly := false
@ -174,7 +202,27 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale
return nil, err 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
@ -256,6 +304,13 @@ func (ss *ShadowSocks) SupportUOT() bool {
return ss.option.UDPOverTCP 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) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
method, err := shadowsocks.CreateMethod(option.Cipher, shadowsocks.MethodOptions{ method, err := shadowsocks.CreateMethod(option.Cipher, shadowsocks.MethodOptions{
@ -271,6 +326,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
var obfsOption *simpleObfsOption var obfsOption *simpleObfsOption
var shadowTLSOpt *shadowtls.ShadowTLSOption var shadowTLSOpt *shadowtls.ShadowTLSOption
var restlsConfig *restls.Config var restlsConfig *restls.Config
var kcptunClient *kcptun.Client
obfsMode := "" obfsMode := ""
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) 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) 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 { switch option.UDPOverTCPVersion {
case uot.Version, uot.LegacyVersion: case uot.Version, uot.LegacyVersion:
@ -414,5 +503,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
obfsOption: obfsOption, obfsOption: obfsOption,
shadowTLSOption: shadowTLSOpt, shadowTLSOption: shadowTLSOpt,
restlsConfig: restlsConfig, restlsConfig: restlsConfig,
kcptunClient: kcptunClient,
}, nil }, nil
} }

View File

@ -534,6 +534,37 @@ proxies: # socks5
version-hint: "tls12" version-hint: "tls12"
restls-script: "1000?100<1,500~100,350~100,600~100,400~200" 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 # vmess
# cipher 支持 auto/aes-128-gcm/chacha20-poly1305/none # cipher 支持 auto/aes-128-gcm/chacha20-poly1305/none
- name: "vmess" - name: "vmess"
@ -1336,6 +1367,30 @@ listeners:
# password: password # password: password
# handshake: # handshake:
# dest: test.com:443 # 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 - name: vmess-in-1
type: vmess type: vmess

4
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/gobwas/ws v1.4.0 github.com/gobwas/ws v1.4.0
github.com/gofrs/uuid/v5 v5.3.2 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/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20 github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20
github.com/mdlayher/netlink v1.7.2 github.com/mdlayher/netlink v1.7.2
@ -21,6 +22,7 @@ require (
github.com/metacubex/chacha v0.1.5 github.com/metacubex/chacha v0.1.5
github.com/metacubex/fswatch v0.1.1 github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 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/quic-go v0.54.1-0.20250730114134-a1ae705fe295
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/restls-client-go v0.1.7 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/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/josharian/native v1.1.0 // 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/kr/text v0.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect

10
go.sum
View File

@ -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/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.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 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 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 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= 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/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 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/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 h1:N5GExQJqYAH3gOCshpp2u/J3CtNYzMctmlb0xK9wtbQ=
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= 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 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA= 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= 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/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 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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= 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-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.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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -0,0 +1,8 @@
package config
import "github.com/metacubex/mihomo/transport/kcptun"
type KcpTun struct {
Enable bool `json:"enable"`
kcptun.Config `json:",inline"`
}

View File

@ -14,6 +14,7 @@ type ShadowsocksServer struct {
Udp bool Udp bool
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"`
KcpTun KcpTun `yaml:"kcp-tun" json:"kcp-tun,omitempty"`
} }
func (t ShadowsocksServer) String() string { func (t ShadowsocksServer) String() string {

View File

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

View File

@ -16,6 +16,7 @@ type ShadowSocksOption struct {
UDP bool `inbound:"udp,omitempty"` UDP bool `inbound:"udp,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"`
ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"`
KcpTun KcpTun `inbound:"kcp-tun,omitempty"`
} }
func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { func (o ShadowSocksOption) Equal(config C.InboundConfig) bool {
@ -45,6 +46,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) {
Udp: options.UDP, Udp: options.UDP,
MuxOption: options.MuxOption.Build(), MuxOption: options.MuxOption.Build(),
ShadowTLS: options.ShadowTLS.Build(), ShadowTLS: options.ShadowTLS.Build(),
KcpTun: options.KcpTun.Build(),
}, },
}, nil }, nil
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound" "github.com/metacubex/mihomo/listener/inbound"
"github.com/metacubex/mihomo/transport/kcptun"
shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls"
shadowsocks "github.com/metacubex/sing-shadowsocks" shadowsocks "github.com/metacubex/sing-shadowsocks"
@ -21,7 +22,7 @@ import (
var noneList = []string{shadowsocks.MethodNone} var noneList = []string{shadowsocks.MethodNone}
var shadowsocksCipherLists = [][]string{noneList, shadowaead.List, shadowaead_2022.List, shadowstream.List} 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 shadowsocksPassword32 string
var shadowsocksPassword16 string var shadowsocksPassword16 string
@ -32,11 +33,11 @@ func init() {
shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16]) 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() t.Parallel()
for _, cipherList := range cipherLists { for _, cipherList := range cipherLists {
for i, cipher := range cipherList { for i, cipher := range cipherList {
enableSingMux := i == 0 enableSingMux := enableSingMux && i == 0
cipher := cipher cipher := cipher
t.Run(cipher, func(t *testing.T) { t.Run(cipher, func(t *testing.T) {
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value 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) { func TestInboundShadowSocks_Basic(t *testing.T) {
inboundOptions := inbound.ShadowSocksOption{} inboundOptions := inbound.ShadowSocksOption{}
outboundOptions := outbound.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) { func testInboundShadowSocksShadowTls(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) {
t.Parallel() t.Parallel()
t.Run("Conn", func(t *testing.T) { t.Run("Conn", func(t *testing.T) {
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value 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) { t.Run("UConn", func(t *testing.T) {
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value
outboundOptions.ClientFingerprint = "chrome" 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) 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)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/ntp"
"github.com/metacubex/mihomo/transport/kcptun"
shadowsocks "github.com/metacubex/sing-shadowsocks" shadowsocks "github.com/metacubex/sing-shadowsocks"
"github.com/metacubex/sing-shadowsocks/shadowaead" "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, ",") { for _, addr := range strings.Split(config.Listen, ",") {
addr := addr addr := addr
@ -154,6 +161,14 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi
sl.udpListeners = append(sl.udpListeners, ul) 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() { go func() {
conn := bufio.NewPacketConn(ul) conn := bufio.NewPacketConn(ul)
rwOptions := network.NewReadWaitOptions(conn, sl.service) rwOptions := network.NewReadWaitOptions(conn, sl.service)

View File

@ -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.

173
transport/kcptun/client.go Normal file
View File

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

152
transport/kcptun/common.go Normal file
View File

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

63
transport/kcptun/comp.go Normal file
View File

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

5
transport/kcptun/doc.go Normal file
View File

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

View File

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