mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-03-04 21:07:30 +00:00
feat: support kcptun plugin for ss client/server
This commit is contained in:
9
transport/kcptun/LICENSE.md
Normal file
9
transport/kcptun/LICENSE.md
Normal 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
173
transport/kcptun/client.go
Normal 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
152
transport/kcptun/common.go
Normal 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
63
transport/kcptun/comp.go
Normal 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
5
transport/kcptun/doc.go
Normal 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
|
||||
79
transport/kcptun/server.go
Normal file
79
transport/kcptun/server.go
Normal 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)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user