├── .gitignore ├── LICENSE ├── README.md ├── config ├── setting.go └── setting.json ├── controller ├── boost.go ├── normal.go ├── regex.go ├── roundrobin.go └── server.go ├── go.mod ├── go.sum ├── run.go └── utils └── log.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # goland 18 | .idea 19 | 20 | # mac os file 21 | .DS_store 22 | 23 | # go build 24 | go_build_run_go 25 | moto 26 | # moto log 27 | moto.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 cppla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moto 2 | 端口转发、正则匹配[端口复用]转发、智能加速、轮询加速。TCP转发,零拷贝转发。 3 | high-speed motorcycle,可以上高速的摩托车🏍️~ 4 | 5 | # Usage 6 | ```diff 7 | 普通模式[normal]:逐一连接目标地址,成功为止 8 | 正则模式[regex]:利用正则匹配第一个数据报文来实现端口复用 9 | 智能加速[boost]:多线路多TCP主动竞争最优TCP通道,大幅降低网络丢包、中断、切换、出口高低峰的影响! 10 | 轮询模式[roundrobin]:分散连接到所有目标地址 11 | ``` 12 | 13 | #### 智能加速模式演示,自动择路 14 | 15 | ```bash 16 | `work from home(china telecom)`: 17 | {"level":"debug","ts":"2022-06-08 12:17:59.444","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :49751","targetAddr":"47.241.9.9 [新加坡 阿里云] :85","decisionTime(ms)":79} 18 | {"level":"debug","ts":"2022-06-08 12:18:05.050","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :49774","targetAddr":"47.241.9.9 [新加坡 阿里云] :85","decisionTime(ms)":81} 19 | {"level":"debug","ts":"2022-06-08 12:18:05.493","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :49783","targetAddr":"34.124.1.1 [美国 得克萨斯州] :85","decisionTime(ms)":75} 20 | {"level":"debug","ts":"2022-06-08 12:18:05.838","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :49792","targetAddr":"47.241.9.9 [新加坡 阿里云] :85","decisionTime(ms)":84} 21 | {"level":"debug","ts":"2022-06-08 12:18:05.838","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :49790","targetAddr":"47.241.9.9 [新加坡 阿里云] :85","decisionTime(ms)":84} 22 | {"level":"debug","ts":"2022-06-08 12:18:09.176","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :49810","targetAddr":"34.124.1.1 [美国 得克萨斯州] :85","decisionTime(ms)":81} 23 | 24 | `in office(china unicom)`: 25 | {"level":"debug","ts":"2022-06-09 19:24:43.216","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :63847","targetAddr":"119.28.5.2 [香港 腾讯云] :85","decisionTime(ms)":66} 26 | {"level":"debug","ts":"2022-06-09 19:24:49.412","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :63878","targetAddr":"119.28.5.2 [香港 腾讯云] :85","decisionTime(ms)":49} 27 | {"level":"debug","ts":"2022-06-09 19:24:57.356","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :63905","targetAddr":"119.28.5.2 [香港 腾讯云] :85","decisionTime(ms)":55} 28 | {"level":"debug","ts":"2022-06-09 19:27:06.394","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :64245","targetAddr":"119.28.5.2 [香港 腾讯云] :85","decisionTime(ms)":51} 29 | {"level":"debug","ts":"2022-06-09 19:27:07.666","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :64255","targetAddr":"119.28.5.2 [香港 腾讯云] :85","decisionTime(ms)":55} 30 | {"level":"debug","ts":"2022-06-09 19:27:07.666","msg":"establish connection","ruleName":"智能加速","remoteAddr":"127.0.0.1 [本机地址] :64256","targetAddr":"119.28.5.2 [香港 腾讯云] :85","decisionTime(ms)":55} 31 | ``` 32 | 33 | #### 常见协议正则表达式 34 | |协议|正则表达式| 35 | | --- | ---| 36 | |HTTP|^(GET\|POST\|HEAD\|DELETE\|PUT\|CONNECT\|OPTIONS\|TRACE)| 37 | |SSH|^SSH| 38 | |HTTPS(SSL)|^\x16\x03| 39 | |RDP|^\x03\x00\x00| 40 | |SOCKS5|^\x05| 41 | |HTTP代理|(^CONNECT)\|(Proxy-Connection:)| 42 | 43 | 1、复制到JSON中记得注意特殊符号,例如^\\x16\\x03得改成^\\\\x16\\\\x03** 44 | 2、正则模式的原理是根据客户端建立连接后第一个数据包的特征进行判断是什么协议,该方式不支持连接建立之后服务器主动握手的协议,例如VNC,FTP,MYSQL,被动SSH等。** 45 | 46 | # Example 47 | ``` 48 | { 49 | "log": { 50 | "level": "info", 51 | "path": "./moto.log", 52 | "version": "1.0.0", 53 | "date": "2022-06-08" 54 | }, 55 | "rules": [ 56 | { 57 | "name": "普通模式", 58 | "listen": ":81", 59 | "mode": "normal", 60 | "timeout": 3000, 61 | "blacklist": null, 62 | "targets": [ 63 | { 64 | "address": "1.1.1.1:85" 65 | }, 66 | { 67 | "address": "2.2.2.2:85" 68 | } 69 | ] 70 | }, 71 | { 72 | "name": "正则模式", 73 | "listen": ":82", 74 | "mode": "regex", 75 | "timeout": 3000, 76 | "blacklist": null, 77 | "targets": [ 78 | { 79 | "regexp": "^(GET|POST|HEAD|DELETE|PUT|CONNECT|OPTIONS|TRACE)", 80 | "address": "1.1.1.1:80" 81 | }, 82 | { 83 | "regexp": "^SSH", 84 | "address": "2.2.2.2:22" 85 | } 86 | ] 87 | }, 88 | { 89 | "name": "智能加速", 90 | "listen": ":83", 91 | "mode": "boost", 92 | "timeout": 150, 93 | "blacklist": null, 94 | "targets": [ 95 | { 96 | "address": "1.1.1.1:85" 97 | }, 98 | { 99 | "address": "2.2.2.2:85" 100 | } 101 | ] 102 | }, 103 | { 104 | "name": "轮询模式", 105 | "listen": ":84", 106 | "mode": "roundrobin", 107 | "timeout": 150, 108 | "blacklist": null, 109 | "targets": [ 110 | { 111 | "address": "1.1.1.1:85" 112 | }, 113 | { 114 | "address": "2.2.2.2:85" 115 | } 116 | ] 117 | } 118 | ] 119 | } 120 | ``` 121 | 122 | 123 | # Build 124 | #### build for linux 125 | 126 | CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo 127 | 128 | #### build for macos 129 | 130 | CGO_ENABLED=0 GOOS=darwin go build -a -installsuffix cgo 131 | 132 | #### build for windows 133 | 134 | CGO_ENABLED=0 GOOS=windows go build -a -installsuffix cgo 135 | 136 | # Make Better 137 | 138 | * todo 139 | * better way for tcp relay: https://hostloc.com/thread-969397-1-1.html 140 | * switcher: https://github.com/crabkun/switcher 141 | 142 | # Jetbrains 143 | 144 | 145 | -------------------------------------------------------------------------------- /config/setting.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "regexp" 8 | ) 9 | 10 | type projectConfig struct { 11 | Log log `json:"log"` 12 | Rules []*Rule `json:"rules"` 13 | Wafs []*Waf `json:"wafs"` 14 | } 15 | 16 | type log struct { 17 | Level string `json:"level"` 18 | Path string `json:"path"` 19 | Version string `json:"version"` 20 | Date string `json:"date"` 21 | } 22 | 23 | type Waf struct { 24 | Name string `json:"name"` 25 | Blackcountry []string `json:"blackcountry"` 26 | Threshold uint64 `json:"threshold"` 27 | Findtime uint64 `json:"findtime"` 28 | Bantime uint64 `json:"bantime"` 29 | } 30 | 31 | type Rule struct { 32 | Name string `json:"name"` 33 | Listen string `json:"listen"` 34 | Mode string `json:"mode"` 35 | Targets []*struct { 36 | Regexp string `json:"regexp"` 37 | Re *regexp.Regexp `json:"-"` 38 | Address string `json:"address"` 39 | } `json:"targets"` 40 | Timeout uint64 `json:"timeout"` 41 | Blacklist map[string]bool `json:"blacklist"` 42 | } 43 | 44 | var GlobalCfg *projectConfig 45 | 46 | func init() { 47 | buf, err := ioutil.ReadFile("config/setting.json") 48 | if err != nil { 49 | fmt.Errorf("failed to load setting.json: %s", err.Error()) 50 | } 51 | 52 | if err := json.Unmarshal(buf, &GlobalCfg); err != nil { 53 | fmt.Errorf("failed to load setting.json: %s", err.Error()) 54 | } 55 | 56 | if len(GlobalCfg.Rules) == 0 { 57 | fmt.Errorf("empty rule") 58 | } 59 | 60 | for i, v := range GlobalCfg.Rules { 61 | if err := v.verify(); err != nil { 62 | fmt.Errorf("verity rule failed at pos %d : %s", i, err.Error()) 63 | } 64 | } 65 | 66 | for i, v := range GlobalCfg.Wafs { 67 | if v.Name == "" { 68 | fmt.Errorf("empty waf name at pos %d", i) 69 | } 70 | if v.Threshold == 0 { 71 | fmt.Errorf("invalid threshold at pos %d", i) 72 | } 73 | if v.Findtime == 0 { 74 | fmt.Errorf("invalid findtime at pos %d", i) 75 | } 76 | if v.Bantime == 0 { 77 | fmt.Errorf("invalid bantime at pos %d", i) 78 | } 79 | fmt.Println(v) 80 | } 81 | } 82 | 83 | func (c *Rule) verify() error { 84 | if c.Name == "" { 85 | return fmt.Errorf("empty name") 86 | } 87 | if c.Listen == "" { 88 | return fmt.Errorf("invalid listen address") 89 | } 90 | if len(c.Targets) == 0 { 91 | return fmt.Errorf("invalid targets") 92 | } 93 | if c.Mode == "regex" { 94 | if c.Timeout == 0 { 95 | c.Timeout = 500 96 | } 97 | } 98 | for i, v := range c.Targets { 99 | if v.Address == "" { 100 | return fmt.Errorf("invalid address at pos %d", i) 101 | } 102 | if c.Mode == "regex" { 103 | r, err := regexp.Compile(v.Regexp) 104 | if err != nil { 105 | return fmt.Errorf("invalid regexp at pos %d : %s", i, err.Error()) 106 | } 107 | v.Re = r 108 | } 109 | } 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /config/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "level": "debug", 4 | "path": "./moto.log", 5 | "version": "1.0.1", 6 | "date": "2024-07-23" 7 | }, 8 | "wafs": [ 9 | { 10 | "name": "限制单位时间内总请求次数@threshold", 11 | "blackcountry": [ 12 | "US" 13 | ], 14 | "threshold": 200, 15 | "findtime": 30, 16 | "bantime": 86400 17 | }, 18 | { 19 | "name": "限制单位时间内总数据量@threshold", 20 | "blackcountry": [ 21 | "TW", 22 | "US" 23 | ], 24 | "threshold": 10240, 25 | "findtime": 30, 26 | "bantime": 86400 27 | } 28 | ], 29 | "rules": [ 30 | { 31 | "name": "正常模式", 32 | "listen": ":81", 33 | "mode": "normal", 34 | "timeout": 3000, 35 | "blacklist": null, 36 | "targets": [ 37 | { 38 | "address": "1.1.1.1:85" 39 | }, 40 | { 41 | "address": "2.2.2.2:85" 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "正则模式", 47 | "listen": ":82", 48 | "mode": "regex", 49 | "timeout": 3000, 50 | "blacklist": null, 51 | "targets": [ 52 | { 53 | "regexp": "^(GET|POST|HEAD|DELETE|PUT|CONNECT|OPTIONS|TRACE)", 54 | "address": "1.1.1.1:80" 55 | }, 56 | { 57 | "regexp": "^SSH", 58 | "address": "2.2.2.2:22" 59 | } 60 | ] 61 | }, 62 | { 63 | "name": "智能加速", 64 | "listen": ":83", 65 | "mode": "boost", 66 | "timeout": 300, 67 | "blacklist": null, 68 | "targets": [ 69 | { 70 | "address": "1.1.1.1:85" 71 | }, 72 | { 73 | "address": "2.2.2.2:85" 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "轮询模式", 79 | "listen": ":84", 80 | "mode": "roundrobin", 81 | "timeout": 300, 82 | "blacklist": null, 83 | "targets": [ 84 | { 85 | "address": "1.1.1.1:85" 86 | }, 87 | { 88 | "address": "2.2.2.2:85" 89 | } 90 | ] 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /controller/boost.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "go.uber.org/zap" 6 | "io" 7 | "moto/config" 8 | "moto/utils" 9 | "net" 10 | "time" 11 | ) 12 | 13 | func HandleBoost(conn net.Conn, rule *config.Rule) { 14 | defer conn.Close() 15 | 16 | decisionBegin := time.Now() 17 | //智能选择最先连上的优质线路。 未用的TCP主动关闭连接。 18 | //决策时间超过timeout主动关闭,超过300ms🚀没有意义 19 | //todo: 这里如何保持长久连接? 20 | ctx, cancel := context.WithCancel(context.Background()) 21 | defer cancel() 22 | switchBetter := make(chan net.Conn, 1) 23 | for _, v := range rule.Targets { 24 | go func(address string) { 25 | if tryGetQuickConn, err := net.Dial("tcp", address); err == nil { 26 | select { 27 | case switchBetter <- tryGetQuickConn: 28 | case <-ctx.Done(): 29 | tryGetQuickConn.Close() 30 | } 31 | } 32 | }(v.Address) 33 | } 34 | //全部连接失败: 最恶劣的情况,全部线路延迟大或中断。 35 | var target net.Conn 36 | dtx, dance := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(rule.Timeout)) 37 | defer dance() 38 | select { 39 | case target = <-switchBetter: 40 | cancel() 41 | case <-dtx.Done(): 42 | utils.Logger.Error("Boost Decision Failed!All Online Network Disconnect!", 43 | zap.String("ruleName", rule.Name)) 44 | return 45 | } 46 | 47 | utils.Logger.Debug("ESTABLISHED", 48 | zap.String("ruleName", rule.Name), 49 | zap.String("remoteAddr", conn.RemoteAddr().String()), 50 | zap.String("targetAddr", target.RemoteAddr().String()), 51 | zap.Int64("decisionTime(ms)", time.Now().Sub(decisionBegin).Milliseconds())) 52 | 53 | defer target.Close() 54 | 55 | go func() { 56 | io.Copy(conn, target) 57 | conn.Close() 58 | target.Close() 59 | }() 60 | io.Copy(target, conn) 61 | } 62 | -------------------------------------------------------------------------------- /controller/normal.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "io" 6 | "moto/config" 7 | "moto/utils" 8 | "net" 9 | ) 10 | 11 | func HandleNormal(conn net.Conn, rule *config.Rule) { 12 | defer conn.Close() 13 | 14 | var target net.Conn 15 | //正常模式下挨个连接直到成功连接 16 | for _, v := range rule.Targets { 17 | c, err := net.Dial("tcp", v.Address) 18 | if err != nil { 19 | utils.Logger.Error("unable to establish connection, try next target", 20 | zap.String("ruleName", rule.Name), 21 | zap.String("remoteAddr", conn.RemoteAddr().String()), 22 | zap.String("targetAddr", v.Address)) 23 | continue 24 | } 25 | target = c 26 | break 27 | } 28 | if target == nil { 29 | utils.Logger.Error("all targets connected failed,so can't to handle connection", 30 | zap.String("ruleName", rule.Name), 31 | zap.String("remoteAddr", conn.RemoteAddr().String())) 32 | return 33 | } 34 | utils.Logger.Debug("establish connection", 35 | zap.String("ruleName", rule.Name), 36 | zap.String("remoteAddr", conn.RemoteAddr().String()), 37 | zap.String("targetAddr", target.RemoteAddr().String())) 38 | 39 | defer target.Close() 40 | 41 | go func() { 42 | io.Copy(conn, target) 43 | conn.Close() 44 | target.Close() 45 | }() 46 | io.Copy(target, conn) 47 | } 48 | -------------------------------------------------------------------------------- /controller/regex.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "go.uber.org/zap" 6 | "io" 7 | "moto/config" 8 | "moto/utils" 9 | "net" 10 | "time" 11 | ) 12 | 13 | func HandleRegexp(conn net.Conn, rule *config.Rule) { 14 | defer conn.Close() 15 | 16 | //正则模式下需要客户端的第一个数据包判断特征,所以需要设置一个超时 17 | conn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(rule.Timeout))) 18 | //获取第一个数据包 19 | firstPacket := new(bytes.Buffer) 20 | if _, err := io.CopyN(firstPacket, conn, 4096); err != nil { 21 | utils.Logger.Error("unable to handle connection, failed to get first packet", 22 | zap.String("ruleName", rule.Name), 23 | zap.String("remoteAddr", conn.RemoteAddr().String()), 24 | zap.Error(err)) 25 | return 26 | } 27 | 28 | var target net.Conn 29 | //挨个匹配正则 30 | for _, v := range rule.Targets { 31 | if !v.Re.Match(firstPacket.Bytes()) { 32 | continue 33 | } 34 | c, err := net.Dial("tcp", v.Address) 35 | if err != nil { 36 | utils.Logger.Error("unable to establish connection", 37 | zap.String("ruleName", rule.Name), 38 | zap.String("remoteAddr", conn.RemoteAddr().String()), 39 | zap.String("targetAddr", v.Address)) 40 | continue 41 | } 42 | target = c 43 | break 44 | } 45 | if target == nil { 46 | utils.Logger.Error("can't match target , so can't handle connection", 47 | zap.String("ruleName", rule.Name), 48 | zap.String("remoteAddr", conn.RemoteAddr().String())) 49 | return 50 | } 51 | 52 | utils.Logger.Debug("establish connection", 53 | zap.String("ruleName", rule.Name), 54 | zap.String("remoteAddr", conn.RemoteAddr().String()), 55 | zap.String("targetAddr", target.RemoteAddr().String())) 56 | //匹配到了,去除掉刚才设定的超时 57 | conn.SetReadDeadline(time.Time{}) 58 | //把第一个数据包发送给目标 59 | io.Copy(target, firstPacket) 60 | 61 | defer target.Close() 62 | 63 | go func() { 64 | io.Copy(conn, target) 65 | conn.Close() 66 | target.Close() 67 | }() 68 | io.Copy(target, conn) 69 | } 70 | -------------------------------------------------------------------------------- /controller/roundrobin.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "io" 6 | "moto/config" 7 | "moto/utils" 8 | "net" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | var tcpCounter uint64 14 | 15 | func HandleRoundrobin(conn net.Conn, rule *config.Rule) { 16 | defer conn.Close() 17 | 18 | index := atomic.AddUint64(&tcpCounter, 1) % uint64(len(rule.Targets)) 19 | if tcpCounter >= 100*uint64(len(rule.Targets)) { 20 | atomic.StoreUint64(&tcpCounter, 1) 21 | } 22 | 23 | v := rule.Targets[index] 24 | 25 | roundrobinBegin := time.Now() 26 | target, err := net.Dial("tcp", v.Address) 27 | if err != nil { 28 | utils.Logger.Error("unable to establish connection, Smart switch boost mode", 29 | zap.String("ruleName", rule.Name), 30 | zap.String("remoteAddr", conn.RemoteAddr().String()), 31 | zap.String("targetAddr", v.Address), 32 | zap.Int64("failedTime(ms)", time.Now().Sub(roundrobinBegin).Milliseconds())) 33 | HandleBoost(conn, rule) 34 | return 35 | } 36 | utils.Logger.Debug("establish connection", 37 | zap.String("ruleName", rule.Name), 38 | zap.String("remoteAddr", conn.RemoteAddr().String()), 39 | zap.String("targetAddr", target.RemoteAddr().String()), 40 | zap.Int64("roundrobinTime(ms)", time.Now().Sub(roundrobinBegin).Milliseconds())) 41 | 42 | defer target.Close() 43 | 44 | go func() { 45 | io.Copy(conn, target) 46 | conn.Close() 47 | target.Close() 48 | }() 49 | io.Copy(target, conn) 50 | } 51 | -------------------------------------------------------------------------------- /controller/server.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/patrickmn/go-cache" 5 | "moto/config" 6 | "moto/utils" 7 | "net" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var ipCache = cache.New(30*time.Second, 1*time.Minute) 14 | 15 | func Listen(rule *config.Rule, wg *sync.WaitGroup) { 16 | defer wg.Done() 17 | //监听 18 | listener, err := net.Listen("tcp", rule.Listen) 19 | if err != nil { 20 | utils.Logger.Error(rule.Name + " failed to listen at " + rule.Listen) 21 | return 22 | } 23 | utils.Logger.Info(rule.Name + " listing at " + rule.Listen) 24 | for { 25 | //处理客户端连接 26 | conn, err := listener.Accept() 27 | if err != nil { 28 | utils.Logger.Error(rule.Name + " failed to accept at " + rule.Listen) 29 | time.Sleep(time.Second * 1) 30 | continue 31 | } 32 | //判断黑名单 33 | if len(rule.Blacklist) != 0 { 34 | clientIP := conn.RemoteAddr().String() 35 | clientIP = clientIP[0:strings.LastIndex(clientIP, ":")] 36 | if rule.Blacklist[clientIP] { 37 | utils.Logger.Info(rule.Name + " disconnected ip in blacklist: " + clientIP) 38 | conn.Close() 39 | continue 40 | } 41 | } 42 | //todo: WAF策略:限制单一IP 30秒内请求不能超过200次, no debug,wait fix 43 | clientIP := conn.RemoteAddr().String() 44 | clientIP = clientIP[0:strings.LastIndex(clientIP, ":")] 45 | if count, found := ipCache.Get(clientIP); found && count.(int) >= 200 { 46 | utils.Logger.Warn("WAF: too many requests from " + clientIP) 47 | conn.Close() 48 | continue 49 | } else { 50 | if found { 51 | ipCache.Increment(clientIP, 1) 52 | } else { 53 | ipCache.Set(clientIP, 1, cache.DefaultExpiration) 54 | } 55 | } 56 | //选择运行模式 57 | switch rule.Mode { 58 | case "normal": 59 | go HandleNormal(conn, rule) 60 | case "regex": 61 | go HandleRegexp(conn, rule) 62 | case "boost": 63 | go HandleBoost(conn, rule) 64 | case "roundrobin": 65 | go HandleRoundrobin(conn, rule) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module moto 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/natefinch/lumberjack v2.0.0+incompatible 7 | go.uber.org/zap v1.17.0 8 | ) 9 | 10 | require ( 11 | github.com/BurntSushi/toml v0.3.1 // indirect 12 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 13 | github.com/pkg/errors v0.9.1 // indirect 14 | github.com/stretchr/testify v1.7.1 // indirect 15 | go.uber.org/atomic v1.7.0 // indirect 16 | go.uber.org/multierr v1.6.0 // indirect 17 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 18 | gopkg.in/yaml.v2 v2.4.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= 7 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= 8 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 9 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 10 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 12 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 17 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 19 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 21 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 22 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 23 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 24 | go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= 25 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 28 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 29 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 30 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 31 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 35 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "moto/config" 5 | "moto/controller" 6 | "moto/utils" 7 | "sync" 8 | ) 9 | 10 | func main() { 11 | defer utils.Logger.Sync() 12 | 13 | utils.Logger.Info("MOTO start...") 14 | wg := &sync.WaitGroup{} 15 | for _, v := range config.GlobalCfg.Rules { 16 | wg.Add(1) 17 | go controller.Listen(v, wg) 18 | } 19 | wg.Wait() 20 | utils.Logger.Info("MOTO close...") 21 | } 22 | -------------------------------------------------------------------------------- /utils/log.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/natefinch/lumberjack" 5 | "go.uber.org/zap" 6 | "go.uber.org/zap/zapcore" 7 | "moto/config" 8 | "time" 9 | ) 10 | 11 | var ( 12 | Logger *zap.Logger 13 | ) 14 | 15 | func init() { 16 | highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool{ 17 | return lvl >= levelMap[config.GlobalCfg.Log.Level] 18 | }) 19 | 20 | //lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { 21 | // return lvl >= zapcore.DebugLevel 22 | //}) 23 | 24 | hook := lumberjack.Logger{ 25 | Filename: config.GlobalCfg.Log.Path, 26 | MaxSize: 1024, 27 | MaxBackups: 5, 28 | MaxAge: 30, 29 | Compress: true, 30 | } 31 | 32 | //consoles := zapcore.AddSync(os.Stdout) 33 | files := zapcore.AddSync(&hook) 34 | 35 | 36 | encoderConfig := zapcore.EncoderConfig{ 37 | TimeKey: "ts", 38 | LevelKey: "level", 39 | NameKey: "logger", 40 | //CallerKey: "caller", 41 | MessageKey: "msg", 42 | StacktraceKey: "stacktrace", 43 | LineEnding: zapcore.DefaultLineEnding, 44 | EncodeLevel: zapcore.LowercaseLevelEncoder, 45 | //EncodeLevel: zapcore.CapitalColorLevelEncoder, 46 | EncodeTime: TimeEncoder, 47 | EncodeDuration: zapcore.SecondsDurationEncoder, 48 | EncodeCaller: zapcore.ShortCallerEncoder, 49 | } 50 | 51 | //consoleEncoder := zapcore.NewJSONEncoder(encoderConfig) 52 | fileEncoder := zapcore.NewJSONEncoder(encoderConfig) 53 | 54 | core := zapcore.NewTee( 55 | //zapcore.NewCore(consoleEncoder, consoles, lowPriority), 56 | zapcore.NewCore(fileEncoder, files, highPriority), 57 | ) 58 | 59 | Logger = zap.New( 60 | core, 61 | zap.AddCaller(), 62 | zap.Development()) 63 | 64 | } 65 | 66 | var levelMap = map[string]zapcore.Level{ 67 | "debug": zapcore.DebugLevel, 68 | "info": zapcore.InfoLevel, 69 | "warn": zapcore.WarnLevel, 70 | "error": zapcore.ErrorLevel, 71 | "dpanic": zapcore.DPanicLevel, 72 | "panic": zapcore.PanicLevel, 73 | "fatal": zapcore.FatalLevel, 74 | } 75 | 76 | func TimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 77 | enc.AppendString(t.Format("2006-01-02 15:04:05.000")) 78 | } --------------------------------------------------------------------------------