package main import ( "crypto/sha1" "io" "log" "math/rand" "net" "os" "time" "golang.org/x/crypto/pbkdf2" "github.com/klauspost/compress/snappy" "github.com/urfave/cli" kcp "github.com/xtaci/kcp-go" "github.com/xtaci/smux" ) var ( // VERSION is injected by buildflags VERSION = "SELFBUILD" // SALT is use for pbkdf2 key expansion SALT = "kcp-go" ) 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) { n, err = c.w.Write(p) err = c.w.Flush() return n, err } func (c *compStream) Close() error { return c.conn.Close() } func newCompStream(conn net.Conn) *compStream { c := new(compStream) c.conn = conn c.w = snappy.NewBufferedWriter(conn) c.r = snappy.NewReader(conn) return c } // handle multiplex-ed connection func handleMux(conn io.ReadWriteCloser, config *Config) { // stream multiplex smuxConfig := smux.DefaultConfig() smuxConfig.MaxReceiveBuffer = config.SockBuf mux, err := smux.Server(conn, smuxConfig) if err != nil { log.Println(err) return } defer mux.Close() for { p1, err := mux.AcceptStream() if err != nil { log.Println(err) return } p2, err := net.DialTimeout("tcp", config.Target, 5*time.Second) if err != nil { p1.Close() log.Println(err) continue } if err := p2.(*net.TCPConn).SetReadBuffer(config.SockBuf); err != nil { log.Println("TCP SetReadBuffer:", err) } if err := p2.(*net.TCPConn).SetWriteBuffer(config.SockBuf); err != nil { log.Println("TCP SetWriteBuffer:", err) } go handleClient(p1, p2) } } func handleClient(p1, p2 io.ReadWriteCloser) { log.Println("stream opened") defer log.Println("stream closed") defer p1.Close() defer p2.Close() // start tunnel p1die := make(chan struct{}) go func() { io.Copy(p1, p2); close(p1die) }() p2die := make(chan struct{}) go func() { io.Copy(p2, p1); close(p2die) }() // wait for tunnel termination select { case <-p1die: case <-p2die: } } func checkError(err error) { if err != nil { log.Printf("%+v\n", err) os.Exit(-1) } } func main() { rand.Seed(int64(time.Now().Nanosecond())) if VERSION == "SELFBUILD" { // add more log flags for debugging log.SetFlags(log.LstdFlags | log.Lshortfile) } myApp := cli.NewApp() myApp.Name = "kcptun" myApp.Usage = "server(with SMUX)" myApp.Version = VERSION myApp.Flags = []cli.Flag{ cli.StringFlag{ Name: "listen,l", Value: ":29900", Usage: "kcp server listen address", }, cli.StringFlag{ Name: "target, t", Value: "127.0.0.1:12948", Usage: "target server address", }, cli.StringFlag{ Name: "key", Value: "it's a secrect", Usage: "pre-shared secret between client and server", EnvVar: "KCPTUN_KEY", }, cli.StringFlag{ Name: "crypt", Value: "aes", Usage: "aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none", }, cli.StringFlag{ Name: "mode", Value: "fast", Usage: "profiles: fast3, fast2, fast, normal", }, cli.IntFlag{ Name: "mtu", Value: 1350, Usage: "set maximum transmission unit for UDP packets", }, cli.IntFlag{ Name: "sndwnd", Value: 1024, Usage: "set send window size(num of packets)", }, cli.IntFlag{ Name: "rcvwnd", Value: 1024, Usage: "set receive window size(num of packets)", }, cli.IntFlag{ Name: "datashard", Value: 10, Usage: "set reed-solomon erasure coding - datashard", }, cli.IntFlag{ Name: "parityshard", Value: 3, Usage: "set reed-solomon erasure coding - parityshard", }, cli.IntFlag{ Name: "dscp", Value: 0, Usage: "set DSCP(6bit)", }, cli.BoolFlag{ Name: "nocomp", Usage: "disable compression", }, cli.BoolFlag{ Name: "acknodelay", Usage: "flush ack immediately when a packet is received", Hidden: true, }, cli.IntFlag{ Name: "nodelay", Value: 0, Hidden: true, }, cli.IntFlag{ Name: "interval", Value: 40, Hidden: true, }, cli.IntFlag{ Name: "resend", Value: 0, Hidden: true, }, cli.IntFlag{ Name: "nc", Value: 0, Hidden: true, }, cli.IntFlag{ Name: "sockbuf", Value: 4194304, // socket buffer size in bytes Hidden: true, }, cli.IntFlag{ Name: "keepalive", Value: 10, // nat keepalive interval in seconds Hidden: true, }, cli.StringFlag{ Name: "log", Value: "", Usage: "specify a log file to output, default goes to stderr", }, cli.StringFlag{ Name: "c", Value: "", // when the value is not empty, the config path must exists Usage: "config from json file, which will override the command from shell", }, } myApp.Action = func(c *cli.Context) error { config := Config{} config.Listen = c.String("listen") config.Target = c.String("target") config.Key = c.String("key") config.Crypt = c.String("crypt") config.Mode = c.String("mode") config.MTU = c.Int("mtu") config.SndWnd = c.Int("sndwnd") config.RcvWnd = c.Int("rcvwnd") config.DataShard = c.Int("datashard") config.ParityShard = c.Int("parityshard") config.DSCP = c.Int("dscp") config.NoComp = c.Bool("nocomp") config.AckNodelay = c.Bool("acknodelay") config.NoDelay = c.Int("nodelay") config.Interval = c.Int("interval") config.Resend = c.Int("resend") config.NoCongestion = c.Int("nc") config.SockBuf = c.Int("sockbuf") config.KeepAlive = c.Int("keepalive") config.Log = c.String("log") if c.String("c") != "" { //Now only support json config file err := parseJSONConfig(&config, c.String("c")) checkError(err) } // log redirect if config.Log != "" { f, err := os.OpenFile(config.Log, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) checkError(err) defer f.Close() log.SetOutput(f) } switch config.Mode { case "normal": config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 0, 30, 2, 1 case "fast": config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 0, 20, 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 } log.Println("version:", VERSION) pass := pbkdf2.Key([]byte(config.Key), []byte(SALT), 4096, 32, sha1.New) var block kcp.BlockCrypt switch config.Crypt { 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) } lis, err := kcp.ListenWithOptions(config.Listen, block, config.DataShard, config.ParityShard) checkError(err) log.Println("listening on:", lis.Addr()) log.Println("target:", config.Target) log.Println("encryption:", config.Crypt) log.Println("nodelay parameters:", config.NoDelay, config.Interval, config.Resend, config.NoCongestion) log.Println("sndwnd:", config.SndWnd, "rcvwnd:", config.RcvWnd) log.Println("compression:", !config.NoComp) log.Println("mtu:", config.MTU) log.Println("datashard:", config.DataShard, "parityshard:", config.ParityShard) log.Println("acknodelay:", config.AckNodelay) log.Println("dscp:", config.DSCP) log.Println("sockbuf:", config.SockBuf) log.Println("keepalive:", config.KeepAlive) if err := lis.SetDSCP(config.DSCP); err != nil { log.Println("SetDSCP:", err) } if err := lis.SetReadBuffer(config.SockBuf); err != nil { log.Println("SetReadBuffer:", err) } if err := lis.SetWriteBuffer(config.SockBuf); err != nil { log.Println("SetWriteBuffer:", err) } for { if conn, err := lis.AcceptKCP(); err == nil { log.Println("remote address:", conn.RemoteAddr()) conn.SetStreamMode(true) conn.SetNoDelay(config.NoDelay, config.Interval, config.Resend, config.NoCongestion) conn.SetMtu(config.MTU) conn.SetWindowSize(config.SndWnd, config.RcvWnd) conn.SetACKNoDelay(config.AckNodelay) conn.SetKeepAlive(config.KeepAlive) if config.NoComp { go handleMux(conn, &config) } else { go handleMux(newCompStream(conn), &config) } } else { log.Printf("%+v", err) } } } myApp.Run(os.Args) }