commit dc4d42007b5998f927c0295b069651abadbeddab Author: xtaci Date: Mon Jan 23 22:30:54 2017 +0800 init diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 0000000..b45d668 --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,27 @@ +问问题前先搜索ISSUE,并搞清楚下面的问题: + +1. 检查 ```-key xxx``` 至少三遍, ***保证***两边一致。 +2. 保证```-nocomp, -datashard, -parityshard, -key, -crypt```两边一致。 +3. 是否在服务器端,正确设定了转发的目标服务器地址 ***--target***。 +4. 如果第3条不确定,尝试在服务器上telnet target port试试。 +5. 防火墙是否关闭了UDP通信。 +6. 两端的版本是否一致? +7. 是不是最新版本? +8. 两端分别是什么操作系统? +9. 两端的输出日志是什么? + +Before firing issue, make sure you figured out the following common questions. + +PLEASE DO SEARCH FIRST. + +1. Check your ```-key xxx``` for at least 3 times, ***MAKE SURE*** both sides share the same secret. +2. ```-nocomp, -datashard, -parityshard, -key, -crypt``` ***must be the same*** on both side. +3. Did you correctly set the ***-target*** on the server side? +4. ***MAKE SURE*** ```telnet target port``` on your server successful(don't ask me why couldn't). +5. Does your ***firewall allows UDP*** communications? (including your ISP Cable-Modem) +6. Are you using the **same version** for both client & server +7. Are you using the **latest release**? +8. Which **OS** do you use? +9. Which end for this issue related to, **client or server**? + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..389256f --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +client/client +server/server diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3961fc3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: go +go: + - 1.6 +before_install: + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +install: + - go get github.com/xtaci/kcptun/client + - go get github.com/xtaci/kcptun/server +before_script: +script: + - cd $HOME/gopath/src/github.com/xtaci/kcptun/client + - $HOME/gopath/bin/goveralls -service=travis-ci + - cd $HOME/gopath/src/github.com/xtaci/kcptun/server + - $HOME/gopath/bin/goveralls -service=travis-ci + - exit 0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..42a3d1f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM golang:alpine +MAINTAINER xtaci +RUN apk update && \ + apk upgrade && \ + apk add git +RUN go get github.com/xtaci/kcptun/client && go get github.com/xtaci/kcptun/server +EXPOSE 29900/udp +EXPOSE 12948 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..88a8eea --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Daniel Fu + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..98d23b9 --- /dev/null +++ b/README.md @@ -0,0 +1,245 @@ +# kcptun +[![Release][1]][2] [![Powered][7]][8] [![Build Status][3]][4] [![Go Report Card][5]][6] +[1]: https://img.shields.io/github/release/xtaci/kcptun.svg +[2]: https://github.com/xtaci/kcptun/releases/latest +[3]: https://travis-ci.org/xtaci/kcptun.svg?branch=master +[4]: https://travis-ci.org/xtaci/kcptun +[5]: https://goreportcard.com/badge/github.com/xtaci/kcptun +[6]: https://goreportcard.com/report/github.com/xtaci/kcptun +[7]: https://img.shields.io/badge/KCP-Powered-blue.svg +[8]: https://github.com/skywind3000/kcp + +A tool for converting tcp stream into kcp+udp stream, :zap: ***[download address](https://github.com/xtaci/kcptun/releases/latest)***:zap: + +![kcptun](kcptun.png) + +***kcptun is based on [kcp-go](https://github.com/xtaci/kcp-go)*** + +### *QuickStart* :lollipop: +Client, server, respectively, download the corresponding platform binary compression package, and extract, through the following command to start port forwarding. +``` +Server: ./server_linux_amd64 -t "SERVER_IP:8388" -l ":4000" -mode fast2 +Client: ./client_darwin_amd64 -r "SERVER_IP:4000" -l ":8388" -mode fast2 +``` +The above command can establish 8388/tcp port forwarding (through 4000/udp port). + + +### *Performance* :lollipop: +fast.com +* Speed tested with: https://fast.com +* WAN Link Speed: 100M ADSL +* WIFI: 5GHz TL-WDR3320 + + +### *Usage* :lollipop: +Help output under MacOS X: +``` +$ ./client_darwin_amd64 -h +NAME: + kcptun - client(with SMUX) + +USAGE: + client_darwin_amd64 [global options] command [command options] [arguments...] + +VERSION: + 20170120 + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --localaddr value, -l value local listen address (default: ":12948") + --remoteaddr value, -r value kcp server address (default: "vps:29900") + --key value pre-shared secret between client and server (default: "it's a secrect") [$KCPTUN_KEY] + --crypt value aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none (default: "aes") + --mode value profiles: fast3, fast2, fast, normal (default: "fast") + --conn value set num of UDP connections to server (default: 1) + --autoexpire value set auto expiration time(in seconds) for a single UDP connection, 0 to disable (default: 0) + --mtu value set maximum transmission unit for UDP packets (default: 1350) + --sndwnd value set send window size(num of packets) (default: 128) + --rcvwnd value set receive window size(num of packets) (default: 512) + --datashard value, --ds value set reed-solomon erasure coding - datashard (default: 10) + --parityshard value, --ps value set reed-solomon erasure coding - parityshard (default: 3) + --dscp value set DSCP(6bit) (default: 0) + --nocomp disable compression + --snmplog value collect snmp to file, aware of timeformat in golang, like: ./snmp-20060102.log + --snmpperiod value snmp collect period, in seconds (default: 60) + --log value specify a log file to output, default goes to stderr + -c value config from json file, which will override the command from shell + --help, -h show help + --version, -v print the version + +$ ./server_darwin_amd64 -h +NAME: + kcptun - server(with SMUX) + +USAGE: + server_darwin_amd64 [global options] command [command options] [arguments...] + +VERSION: + 20170120 + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --listen value, -l value kcp server listen address (default: ":29900") + --target value, -t value target server address (default: "127.0.0.1:12948") + --key value pre-shared secret between client and server (default: "it's a secrect") [$KCPTUN_KEY] + --crypt value aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none (default: "aes") + --mode value profiles: fast3, fast2, fast, normal (default: "fast") + --mtu value set maximum transmission unit for UDP packets (default: 1350) + --sndwnd value set send window size(num of packets) (default: 1024) + --rcvwnd value set receive window size(num of packets) (default: 1024) + --datashard value, --ds value set reed-solomon erasure coding - datashard (default: 10) + --parityshard value, --ps value set reed-solomon erasure coding - parityshard (default: 3) + --dscp value set DSCP(6bit) (default: 0) + --nocomp disable compression + --snmplog value collect snmp to file, aware of timeformat in golang, like: ./snmp-20060102.log + --snmpperiod value snmp collect period, in seconds (default: 60) + --log value specify a log file to output, default goes to stderr + -c value config from json file, which will override the command from shell + --help, -h show help + --version, -v print the version +``` + +#### *Parameters by Layers* :lollipop: + +

params

+ +### *Parameters* :lollipop: + +***Both sides must agree on the following parameters:*** +* datashard +* parityshard +* nocomp +* key +* crypt + +other parameters can be set independently. + +*How to optimize*: +> Step 1:Increase client rcvwnd & server sndwnd simultaneously & gradually。 +> Step 2:Try download something and observer, if the bandwidth usage is close the limit then stop, otherwise goto step 1. + +***NOTICE: if too much retranmission happens, it's quite possible the windows are too large*** + +### *Embeded Mode* :lollipop: + +Latency: +*fast3 >* ***[fast2]*** *> fast > normal > default* +Payload Ratio: +*default > normal > fast >* ***[fast2]*** *> fast3* +Parameters in middle is balanced for latency & payload ratio, the faster you get the more wasteful you are. +Manual control is supported with hidden parameters, you must understand KCP protocol before doing this. +``` + -mode manual -nodelay 1 -resend 2 -nc 1 -interval 20 +``` +I suggest fast2 for high-loss network, normal for low-loss network. + +### *Forward Error Correction* :lollipop: + +In coding theory, the Reed–Solomon code belongs to the class of non-binary cyclic error-correcting codes. The Reed–Solomon code is based on univariate polynomials over finite fields. + +It is able to detect and correct multiple symbol errors. By adding t check symbols to the data, a Reed–Solomon code can detect any combination of up to t erroneous symbols, or correct up to ⌊t/2⌋ symbols. As an erasure code, it can correct up to t known erasures, or it can detect and correct combinations of errors and erasures. Furthermore, Reed–Solomon codes are suitable as multiple-burst bit-error correcting codes, since a sequence of b + 1 consecutive bit errors can affect at most two symbols of size b. The choice of t is up to the designer of the code, and may be selected within wide limits. + +![reed-solomon](rs.png) + +Setting parameters of RS-Code with ```-datashard m -parityshard n``` + +### *DSCP* :lollipop: + +Differentiated services or DiffServ is a computer networking architecture that specifies a simple, scalable and coarse-grained mechanism for classifying and managing network traffic and providing quality of service (QoS) on modern IP networks. DiffServ can, for example, be used to provide low-latency to critical network traffic such as voice or streaming media while providing simple best-effort service to non-critical services such as web traffic or file transfers. + +DiffServ uses a 6-bit differentiated services code point (DSCP) in the 8-bit differentiated services field (DS field) in the IP header for packet classification purposes. The DS field and ECN field replace the outdated IPv4 TOS field.[1] + +setting each side with ```-dscp value```. + + +### *Security* :lollipop: + +No matter what encryption you are using for application layer, if you specify ```-crypt none``` to kcptun, +the header will be ***PLAINTEXT*** to everyone; I suggest ```-crypt aes-128``` for encryption at least . + +NOTICE: ```-crypt xor``` is also insecure, do not use this unless you know what you are doing. + +### *Memory Control* :lollipop: + +Routers, mobile devices are sensitive to memory consumption; by setting GOGC environment(eg: GOGC=20) will lower memory consumption. +Reference: https://blog.golang.org/go15gc + +### *Traffic Control* :lollipop: + +***Intended audience : for those server's bandwidth is quite limited.*** + +Example: To limit outgoing bandwidth to 32mbit/s on server. +``` +root@kcptun:~# cat tc.sh +tc qdisc del dev eth0 root +tc qdisc add dev eth0 root handle 1: htb +tc class add dev eth0 parent 1: classid 1:1 htb rate 32mbit +tc filter add dev eth0 protocol ip parent 1:0 prio 1 handle 10 fw flowid 1:1 +iptables -t mangle -A POSTROUTING -o eth0 -j MARK --set-mark 10 +root@kcptun:~# +``` + +### *Snappy Stream Compression* :lollipop: + +> Snappy is a compression/decompression library. It does not aim for maximum +> compression, or compatibility with any other compression library; instead, +> it aims for very high speeds and reasonable compression. For instance, +> compared to the fastest mode of zlib, Snappy is an order of magnitude faster +> for most inputs, but the resulting compressed files are anywhere from 20% to +> 100% bigger. + +> Reference: http://google.github.io/snappy/ + +disable compression by setting ```-nocomp``` on both side. + +> Tips: Turning off compression may reduce latency. + +### *SNMP* :lollipop: + +```go +// Snmp defines network statistics indicator +type Snmp struct { + BytesSent uint64 // payload bytes sent + BytesReceived uint64 + MaxConn uint64 + ActiveOpens uint64 + PassiveOpens uint64 + CurrEstab uint64 + InErrs uint64 + InCsumErrors uint64 // checksum errors + InSegs uint64 + OutSegs uint64 + OutBytes uint64 // udp bytes sent + RetransSegs uint64 + FastRetransSegs uint64 + EarlyRetransSegs uint64 + LostSegs uint64 + RepeatSegs uint64 + FECRecovered uint64 + FECErrs uint64 + FECSegs uint64 // fec segments received +} +``` + +Sending a signal by ```kill -SIGUSR1 pid``` will give SNMP information for KCP,useful for fine-grained adjustment. +Of which ```RetransSegs,FastRetransSegs,LostSegs,OutSegs``` is the most useful. + +### *References* :paperclip: + +1. https://github.com/skywind3000/kcp -- KCP - A Fast and Reliable ARQ Protocol. +2. https://github.com/klauspost/reedsolomon -- Reed-Solomon Erasure Coding in Go. +3. https://en.wikipedia.org/wiki/Differentiated_services -- DSCP. +4. http://google.github.io/snappy/ -- A fast compressor/decompressor. +5. https://www.backblaze.com/blog/reed-solomon/ -- Reed-Solomon Explained. +6. http://www.qualcomm.cn/products/raptorq -- RaptorQ Forward Error Correction Scheme for Object Delivery. +7. https://en.wikipedia.org/wiki/PBKDF2 -- Key stretching. +8. http://blog.appcanary.com/2016/encrypt-or-compress.html -- Should you encrypt or compress first? +9. https://github.com/hashicorp/yamux -- Connection multiplexing library. +10. https://tools.ietf.org/html/rfc6937 -- Proportional Rate Reduction for TCP. +11. https://tools.ietf.org/html/rfc5827 -- Early Retransmit for TCP and Stream Control Transmission Protocol (SCTP). +12. http://http2.github.io/ -- What is HTTP/2? +13. http://www.lartc.org/ -- Linux Advanced Routing & Traffic Control diff --git a/build-release.sh b/build-release.sh new file mode 100755 index 0000000..4a935b9 --- /dev/null +++ b/build-release.sh @@ -0,0 +1,54 @@ +#!/bin/bash +MD5='md5sum' +unamestr=`uname` +if [[ "$unamestr" == 'Darwin' ]]; then + MD5='md5' +fi + +UPX=false +if hash upx 2>/dev/null; then + UPX=true +fi + +VERSION=`date -u +%Y%m%d` +LDFLAGS="-X main.VERSION=$VERSION -s -w" +GCFLAGS="" + +OSES=(linux darwin windows freebsd) +ARCHS=(amd64 386) +for os in ${OSES[@]}; do + for arch in ${ARCHS[@]}; do + suffix="" + if [ "$os" == "windows" ] + then + suffix=".exe" + fi + env CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o client_${os}_${arch}${suffix} github.com/xtaci/kcptun/client + env CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o server_${os}_${arch}${suffix} github.com/xtaci/kcptun/server + if $UPX; then upx -9 client_${os}_${arch}${suffix} server_${os}_${arch}${suffix};fi + tar -zcf kcptun-${os}-${arch}-$VERSION.tar.gz client_${os}_${arch}${suffix} server_${os}_${arch}${suffix} + $MD5 kcptun-${os}-${arch}-$VERSION.tar.gz + done +done + +# ARM +ARMS=(5 6 7) +for v in ${ARMS[@]}; do + env CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=$v go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o client_linux_arm$v github.com/xtaci/kcptun/client + env CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=$v go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o server_linux_arm$v github.com/xtaci/kcptun/server +done +if $UPX; then upx -9 client_linux_arm* server_linux_arm*;fi +tar -zcf kcptun-linux-arm-$VERSION.tar.gz client_linux_arm* server_linux_arm* +$MD5 kcptun-linux-arm-$VERSION.tar.gz + +#MIPS32LE +env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o client_linux_mipsle github.com/xtaci/kcptun/client +env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o server_linux_mipsle github.com/xtaci/kcptun/server +env CGO_ENABLED=0 GOOS=linux GOARCH=mips go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o client_linux_mips github.com/xtaci/kcptun/client +env CGO_ENABLED=0 GOOS=linux GOARCH=mips go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o server_linux_mips github.com/xtaci/kcptun/server + +if $UPX; then upx -9 client_linux_mips* server_linux_mips*;fi +tar -zcf kcptun-linux-mipsle-$VERSION.tar.gz client_linux_mipsle server_linux_mipsle +tar -zcf kcptun-linux-mips-$VERSION.tar.gz client_linux_mips server_linux_mips +$MD5 kcptun-linux-mipsle-$VERSION.tar.gz +$MD5 kcptun-linux-mips-$VERSION.tar.gz diff --git a/client/config.go b/client/config.go new file mode 100644 index 0000000..faad77c --- /dev/null +++ b/client/config.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "os" +) + +// Config for client +type Config struct { + LocalAddr string `json:"localaddr"` + RemoteAddr string `json:"remoteaddr"` + Key string `json:"key"` + Crypt string `json:"crypt"` + Mode string `json:"mode"` + Conn int `json:"conn"` + AutoExpire int `json:"autoexpire"` + 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"` + KeepAlive int `json:"keepalive"` + Log string `json:"log"` + SnmpLog string `json:"snmplog"` + SnmpPeriod int `json:"snmpperiod"` +} + +func parseJSONConfig(config *Config, path string) error { + file, err := os.Open(path) // For read access. + if err != nil { + return err + } + defer file.Close() + + return json.NewDecoder(file).Decode(config) +} diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..dfc4d76 --- /dev/null +++ b/client/main.go @@ -0,0 +1,487 @@ +package main + +import ( + "crypto/sha1" + "encoding/csv" + "fmt" + "io" + "log" + "math/rand" + "net" + "os" + "time" + + "golang.org/x/crypto/pbkdf2" + + "github.com/golang/snappy" + "github.com/pkg/errors" + "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 +} + +func handleClient(sess *smux.Session, p1 io.ReadWriteCloser) { + p2, err := sess.OpenStream() + if err != nil { + return + } + + 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 = "client(with SMUX)" + myApp.Version = VERSION + myApp.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "localaddr,l", + Value: ":12948", + Usage: "local listen address", + }, + cli.StringFlag{ + Name: "remoteaddr, r", + Value: "vps:29900", + Usage: "kcp 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: "conn", + Value: 1, + Usage: "set num of UDP connections to server", + }, + cli.IntFlag{ + Name: "autoexpire", + Value: 0, + Usage: "set auto expiration time(in seconds) for a single UDP connection, 0 to disable", + }, + cli.IntFlag{ + Name: "mtu", + Value: 1350, + Usage: "set maximum transmission unit for UDP packets", + }, + cli.IntFlag{ + Name: "sndwnd", + Value: 128, + Usage: "set send window size(num of packets)", + }, + cli.IntFlag{ + Name: "rcvwnd", + Value: 512, + Usage: "set receive window size(num of packets)", + }, + cli.IntFlag{ + Name: "datashard,ds", + Value: 10, + Usage: "set reed-solomon erasure coding - datashard", + }, + cli.IntFlag{ + Name: "parityshard,ps", + 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: "snmplog", + Value: "", + Usage: "collect snmp to file, aware of timeformat in golang, like: ./snmp-20060102.log", + }, + cli.IntFlag{ + Name: "snmpperiod", + Value: 60, + Usage: "snmp collect period, in seconds", + }, + 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.LocalAddr = c.String("localaddr") + config.RemoteAddr = c.String("remoteaddr") + config.Key = c.String("key") + config.Crypt = c.String("crypt") + config.Mode = c.String("mode") + config.Conn = c.Int("conn") + config.AutoExpire = c.Int("autoexpire") + 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") + config.SnmpLog = c.String("snmplog") + config.SnmpPeriod = c.Int("snmpperiod") + + if c.String("c") != "" { + 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) + addr, err := net.ResolveTCPAddr("tcp", config.LocalAddr) + checkError(err) + listener, err := net.ListenTCP("tcp", addr) + checkError(err) + + 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) + } + + log.Println("listening on:", listener.Addr()) + log.Println("encryption:", config.Crypt) + log.Println("nodelay parameters:", config.NoDelay, config.Interval, config.Resend, config.NoCongestion) + log.Println("remote address:", config.RemoteAddr) + 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) + log.Println("conn:", config.Conn) + log.Println("autoexpire:", config.AutoExpire) + log.Println("snmplog:", config.SnmpLog) + log.Println("snmpperiod:", config.SnmpPeriod) + + smuxConfig := smux.DefaultConfig() + smuxConfig.MaxReceiveBuffer = config.SockBuf + + createConn := func() (*smux.Session, error) { + kcpconn, err := kcp.DialWithOptions(config.RemoteAddr, block, config.DataShard, config.ParityShard) + if err != nil { + return nil, errors.Wrap(err, "createConn()") + } + kcpconn.SetStreamMode(true) + 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.SetKeepAlive(config.KeepAlive) + + if err := kcpconn.SetDSCP(config.DSCP); err != nil { + log.Println("SetDSCP:", err) + } + if err := kcpconn.SetReadBuffer(config.SockBuf); err != nil { + log.Println("SetReadBuffer:", err) + } + if err := kcpconn.SetWriteBuffer(config.SockBuf); err != nil { + log.Println("SetWriteBuffer:", err) + } + + // stream multiplex + var session *smux.Session + if config.NoComp { + session, err = smux.Client(kcpconn, smuxConfig) + } else { + session, err = smux.Client(newCompStream(kcpconn), smuxConfig) + } + if err != nil { + return nil, errors.Wrap(err, "createConn()") + } + return session, nil + } + + // wait until a connection is ready + waitConn := func() *smux.Session { + for { + if session, err := createConn(); err == nil { + return session + } else { + time.Sleep(time.Second) + } + } + } + + numconn := uint16(config.Conn) + muxes := make([]struct { + session *smux.Session + ttl time.Time + }, numconn) + + for k := range muxes { + sess, err := createConn() + checkError(err) + muxes[k].session = sess + muxes[k].ttl = time.Now().Add(time.Duration(config.AutoExpire) * time.Second) + } + + chScavenger := make(chan *smux.Session, 128) + go scavenger(chScavenger) + go snmpLogger(config.SnmpLog, config.SnmpPeriod) + rr := uint16(0) + for { + p1, err := listener.AcceptTCP() + if err != nil { + log.Fatalln(err) + } + if err := p1.SetReadBuffer(config.SockBuf); err != nil { + log.Println("TCP SetReadBuffer:", err) + } + if err := p1.SetWriteBuffer(config.SockBuf); err != nil { + log.Println("TCP SetWriteBuffer:", err) + } + checkError(err) + idx := rr % numconn + + // do auto expiration && reconnection + if muxes[idx].session.IsClosed() || (config.AutoExpire > 0 && time.Now().After(muxes[idx].ttl)) { + chScavenger <- muxes[idx].session + muxes[idx].session = waitConn() + muxes[idx].ttl = time.Now().Add(time.Duration(config.AutoExpire) * time.Second) + } + + go handleClient(muxes[idx].session, p1) + rr++ + } + } + myApp.Run(os.Args) +} + +type scavengeSession struct { + session *smux.Session + ttl time.Time +} + +const ( + maxScavengeTTL = 10 * time.Minute +) + +func scavenger(ch chan *smux.Session) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + var sessionList []scavengeSession + for { + select { + case sess := <-ch: + sessionList = append(sessionList, scavengeSession{sess, time.Now()}) + case <-ticker.C: + var newList []scavengeSession + for k := range sessionList { + s := sessionList[k] + if s.session.NumStreams() == 0 || s.session.IsClosed() || time.Since(s.ttl) > maxScavengeTTL { + log.Println("session scavenged") + s.session.Close() + } else { + newList = append(newList, sessionList[k]) + } + } + sessionList = newList + } + } +} + +func snmpLogger(path string, interval int) { + if path == "" || interval == 0 { + return + } + ticker := time.NewTicker(time.Duration(interval) * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + f, err := os.OpenFile(time.Now().Format(path), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Println(err) + return + } + w := csv.NewWriter(f) + // write header in empty file + if stat, err := f.Stat(); err == nil && stat.Size() == 0 { + if err := w.Write(append([]string{"Unix"}, kcp.DefaultSnmp.Header()...)); err != nil { + log.Println(err) + } + } + if err := w.Write(append([]string{fmt.Sprint(time.Now().Unix())}, kcp.DefaultSnmp.ToSlice()...)); err != nil { + log.Println(err) + } + kcp.DefaultSnmp.Reset() + w.Flush() + f.Close() + } + } +} diff --git a/client/signal.go b/client/signal.go new file mode 100644 index 0000000..c7b1063 --- /dev/null +++ b/client/signal.go @@ -0,0 +1,28 @@ +// +build linux darwin freebsd + +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + kcp "github.com/xtaci/kcp-go" +) + +func init() { + go sigHandler() +} + +func sigHandler() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGUSR1) + + for { + switch <-ch { + case syscall.SIGUSR1: + log.Printf("KCP SNMP:%+v", kcp.DefaultSnmp.Copy()) + } + } +} diff --git a/fast.png b/fast.png new file mode 100644 index 0000000..d5ed3c7 Binary files /dev/null and b/fast.png differ diff --git a/kcptun.png b/kcptun.png new file mode 100644 index 0000000..d951a91 Binary files /dev/null and b/kcptun.png differ diff --git a/layeredparams.png b/layeredparams.png new file mode 100644 index 0000000..d6a350c Binary files /dev/null and b/layeredparams.png differ diff --git a/logo-small.png b/logo-small.png new file mode 100644 index 0000000..3f8736a Binary files /dev/null and b/logo-small.png differ diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..4b5d1e0 Binary files /dev/null and b/logo.png differ diff --git a/rs.png b/rs.png new file mode 100644 index 0000000..3d228c0 Binary files /dev/null and b/rs.png differ diff --git a/server/config.go b/server/config.go new file mode 100644 index 0000000..0972498 --- /dev/null +++ b/server/config.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "os" +) + +// Config for server +type Config struct { + Listen string `json:"listen"` + Target string `json:"target"` + Key string `json:"key"` + Crypt string `json:"crypt"` + Mode string `json:"mode"` + 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"` + KeepAlive int `json:"keepalive"` + Log string `json:"log"` + SnmpLog string `json:"snmplog"` + SnmpPeriod int `json:"snmpperiod"` +} + +func parseJSONConfig(config *Config, path string) error { + file, err := os.Open(path) // For read access. + if err != nil { + return err + } + defer file.Close() + + return json.NewDecoder(file).Decode(config) +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..55d33cb --- /dev/null +++ b/server/main.go @@ -0,0 +1,405 @@ +package main + +import ( + "crypto/sha1" + "encoding/csv" + "fmt" + "io" + "log" + "math/rand" + "net" + "os" + "time" + + "golang.org/x/crypto/pbkdf2" + + "github.com/golang/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,ds", + Value: 10, + Usage: "set reed-solomon erasure coding - datashard", + }, + cli.IntFlag{ + Name: "parityshard,ps", + 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: "snmplog", + Value: "", + Usage: "collect snmp to file, aware of timeformat in golang, like: ./snmp-20060102.log", + }, + cli.IntFlag{ + Name: "snmpperiod", + Value: 60, + Usage: "snmp collect period, in seconds", + }, + 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") + config.SnmpLog = c.String("snmplog") + config.SnmpPeriod = c.Int("snmpperiod") + + 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) + log.Println("snmplog:", config.SnmpLog) + log.Println("snmpperiod:", config.SnmpPeriod) + + 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) + } + + go snmpLogger(config.SnmpLog, config.SnmpPeriod) + 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) +} + +func snmpLogger(path string, interval int) { + if path == "" || interval == 0 { + return + } + ticker := time.NewTicker(time.Duration(interval) * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + f, err := os.OpenFile(time.Now().Format(path), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Println(err) + return + } + w := csv.NewWriter(f) + // write header in empty file + if stat, err := f.Stat(); err == nil && stat.Size() == 0 { + if err := w.Write(append([]string{"Unix"}, kcp.DefaultSnmp.Header()...)); err != nil { + log.Println(err) + } + } + if err := w.Write(append([]string{fmt.Sprint(time.Now().Unix())}, kcp.DefaultSnmp.ToSlice()...)); err != nil { + log.Println(err) + } + kcp.DefaultSnmp.Reset() + w.Flush() + f.Close() + } + } +} diff --git a/server/signal.go b/server/signal.go new file mode 100644 index 0000000..c7b1063 --- /dev/null +++ b/server/signal.go @@ -0,0 +1,28 @@ +// +build linux darwin freebsd + +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + kcp "github.com/xtaci/kcp-go" +) + +func init() { + go sigHandler() +} + +func sigHandler() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGUSR1) + + for { + switch <-ch { + case syscall.SIGUSR1: + log.Printf("KCP SNMP:%+v", kcp.DefaultSnmp.Copy()) + } + } +}