├── README.md ├── conf.cnf ├── conf.go ├── conn.go ├── db.go ├── logsql.go ├── main.go ├── proxy.go ├── recycler.go └── test.sql /README.md: -------------------------------------------------------------------------------- 1 | # portproxy 2 | 3 | A TCP port proxy utility inspired by qtunnel(https://github.com/getqujing/qtunnel). 4 | 5 | **note:** `portproxy` does not suport ssl mode(mysql 5.7/8.0 client), only used in test environments. 6 | 7 | ## How to Install 8 | 9 | ``` 10 | go get github.com/arstercz/portproxy 11 | ``` 12 | 13 | ## Usage 14 | 15 | ``` 16 | Usage of ./portproxy: 17 | -backend string 18 | backend server ip and port (default "127.0.0.1:8003") 19 | -bind string 20 | locate ip and port (default ":8002") 21 | -buffer uint 22 | buffer size (default 4096) 23 | -conf string 24 | config file to verify database and record sql query 25 | -daemon 26 | run as daemon process 27 | -logTo string 28 | stdout or syslog (default "stdout") 29 | -verbose 30 | print verbose sql query 31 | ``` 32 | 33 | portproxy only print mysql queries when `conf` is not set: 34 | ``` 35 | ./portproxy -backend="10.0.21.5:3301" -bind=":3316" -buffer=16384 --verbose 36 | 2017/01/12 17:27:23 portproxy started. 37 | 2017/01/12 17:27:32 client: 10.0.21.7:29110 ==> 10.0.21.5:3316 38 | 2017/01/12 17:27:32 proxy: 10.0.21.5:18386 ==> 10.0.21.5:3301 39 | 2017/01/12 17:27:32 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: select @@version_comment limit 1 40 | 2017/01/12 17:27:48 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: SELECT DATABASE() 41 | 2017/01/12 17:27:48 From 10.0.21.7:29110 To 10.0.21.5:3301; schema: use percona 42 | 2017/01/12 17:27:48 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: show databases 43 | 2017/01/12 17:27:49 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: show tables 44 | 2017/01/12 17:27:49 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: table columns list: item 45 | 2017/01/12 17:27:49 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: table columns list: stock 46 | 2017/01/12 17:27:56 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: show tables 47 | 2017/01/12 17:28:01 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: show create table item 48 | 2017/01/12 17:28:04 From 10.0.21.7:29110 To 10.0.21.5:3301; Query: kill 2 49 | ``` 50 | 51 | ## changelog: 52 | ``` 53 | 20200423: skip error when does not set conf option 54 | 20170112: log mysql query 55 | ``` 56 | -------------------------------------------------------------------------------- /conf.cnf: -------------------------------------------------------------------------------- 1 | [backend] 2 | dsn = user_test:Aksj@qop@tcp(127.0.0.1:3306)/test?charset=utf8 3 | 4 | -------------------------------------------------------------------------------- /conf.go: -------------------------------------------------------------------------------- 1 | /*config read to verify normal user*/ 2 | package main 3 | 4 | import ( 5 | "github.com/arstercz/goconfig" 6 | ) 7 | 8 | func get_config(conf string) (c *goconfig.ConfigFile, err error) { 9 | c, err = goconfig.ReadConfigFile(conf) 10 | if err != nil { 11 | return c, err 12 | } 13 | return c, nil 14 | } 15 | 16 | func get_backend_dsn(c *goconfig.ConfigFile) (dsn string, err error) { 17 | dsn, err = c.GetString("backend", "dsn") 18 | if err != nil { 19 | return dsn, err 20 | } 21 | return dsn, nil 22 | } 23 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | type Conn struct { 9 | conn net.Conn 10 | pool *recycler 11 | } 12 | 13 | func NewConn(conn net.Conn, pool *recycler) *Conn { 14 | return &Conn{ 15 | conn: conn, 16 | pool: pool, 17 | } 18 | } 19 | 20 | func (c *Conn) Read(b []byte) (int, error) { 21 | c.conn.SetReadDeadline(time.Now().Add(30 * time.Minute)) 22 | n, err := c.conn.Read(b) 23 | return n, err 24 | } 25 | 26 | func (c *Conn) Write(b []byte) (int, error) { 27 | return c.conn.Write(b) 28 | } 29 | 30 | func (c *Conn) Close() { 31 | c.conn.Close() 32 | } 33 | 34 | func (c *Conn) CloseRead() { 35 | if conn, ok := c.conn.(*net.TCPConn); ok { 36 | conn.CloseRead() 37 | } 38 | } 39 | 40 | func (c *Conn) CloseWrite() { 41 | if conn, ok := c.conn.(*net.TCPConn); ok { 42 | conn.CloseWrite() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | /*config read to verify normal user*/ 2 | package main 3 | 4 | import ( 5 | "database/sql" 6 | "fmt" 7 | _ "github.com/go-sql-driver/mysql" 8 | "log" 9 | ) 10 | 11 | func dbh(dsn string) (db *sql.DB, err error) { 12 | db, err = sql.Open("mysql", dsn) 13 | if err != nil { 14 | return db, err 15 | } 16 | return db, nil 17 | } 18 | 19 | func Query(db *sql.DB, q string) (*sql.Rows, error) { 20 | if Verbose { 21 | log.Printf("Query: %s\n", q) 22 | } 23 | return db.Query(q) 24 | } 25 | 26 | func QueryRow(db *sql.DB, q string) *sql.Row { 27 | if Verbose { 28 | log.Printf("Query: %s", q) 29 | } 30 | return db.QueryRow(q) 31 | } 32 | 33 | func ExecQuery(db *sql.DB, q string) (sql.Result, error) { 34 | if Verbose { 35 | log.Printf("ExecQuery: %s\n", q) 36 | } 37 | return db.Exec(q) 38 | } 39 | 40 | func insertlog(db *sql.DB, t *query) bool { 41 | insertSql := ` 42 | insert into query_log(bindport, client, client_port, server, server_port, sql_type, 43 | sql_string, create_time) values (%d, '%s', %d, '%s', %d, '%s', '%s', now()) 44 | ` 45 | _, err := ExecQuery(db, fmt.Sprintf(insertSql, t.bindPort, t.client, t.cport, t.server, t.sport, t.sqlType, t.sqlString)) 46 | if err != nil { 47 | return false 48 | } 49 | return true 50 | } 51 | -------------------------------------------------------------------------------- /logsql.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | //read more client-server protocol from http://dev.mysql.com/doc/internals/en/text-protocol.html 11 | const ( 12 | comQuit byte = iota + 1 13 | comInitDB 14 | comQuery 15 | comFieldList 16 | comCreateDB 17 | comDropDB 18 | comRefresh 19 | comShutdown 20 | comStatistics 21 | comProcessInfo 22 | comConnect 23 | comProcessKill 24 | comDebug 25 | comPing 26 | comTime 27 | comDelayedInsert 28 | comChangeUser 29 | comBinlogDump 30 | comTableDump 31 | comConnectOut 32 | comRegiserSlave 33 | comStmtPrepare 34 | comStmtExecute 35 | comStmtSendLongData 36 | comStmtClose 37 | comStmtReset 38 | comSetOption 39 | comStmtFetch 40 | ) 41 | 42 | type query struct { 43 | bindPort int64 44 | client string 45 | cport int64 46 | server string 47 | sport int64 48 | sqlType string 49 | sqlString string 50 | } 51 | 52 | func ipPortFromNetAddr(s string) (ip string, port int64) { 53 | addrInfo := strings.SplitN(s, ":", 2) 54 | ip = addrInfo[0] 55 | port, _ = strconv.ParseInt(addrInfo[1], 10, 64) 56 | return 57 | } 58 | 59 | func converToUnixLine(sql string) string { 60 | sql = strings.Replace(sql, "\r\n", "\n", -1) 61 | sql = strings.Replace(sql, "\r", "\n", -1) 62 | return sql 63 | } 64 | 65 | func sql_escape(s string) string { 66 | var j int = 0 67 | if len(s) == 0 { 68 | return "" 69 | } 70 | 71 | tempStr := s[:] 72 | desc := make([]byte, len(tempStr)*2) 73 | for i := 0; i < len(tempStr); i++ { 74 | flag := false 75 | var escape byte 76 | switch tempStr[i] { 77 | case '\r': 78 | flag = true 79 | escape = '\r' 80 | break 81 | case '\n': 82 | flag = true 83 | escape = '\n' 84 | break 85 | case '\\': 86 | flag = true 87 | escape = '\\' 88 | break 89 | case '\'': 90 | flag = true 91 | escape = '\'' 92 | break 93 | case '"': 94 | flag = true 95 | escape = '"' 96 | break 97 | case '\032': 98 | flag = true 99 | escape = 'Z' 100 | break 101 | default: 102 | } 103 | if flag { 104 | desc[j] = '\\' 105 | desc[j+1] = escape 106 | j = j + 2 107 | } else { 108 | desc[j] = tempStr[i] 109 | j = j + 1 110 | } 111 | } 112 | return string(desc[0:j]) 113 | } 114 | 115 | func proxyLog(src, dst *Conn) { 116 | buffer := make([]byte, Bsize) 117 | var sqlInfo query 118 | sqlInfo.client, sqlInfo.cport = ipPortFromNetAddr(src.conn.RemoteAddr().String()) 119 | sqlInfo.server, sqlInfo.sport = ipPortFromNetAddr(dst.conn.RemoteAddr().String()) 120 | _, sqlInfo.bindPort = ipPortFromNetAddr(src.conn.LocalAddr().String()) 121 | 122 | for { 123 | n, err := src.Read(buffer) 124 | if err != nil { 125 | return 126 | } 127 | if n >= 5 { 128 | var verboseStr string 129 | switch buffer[4] { 130 | case comQuit: 131 | verboseStr = fmt.Sprintf("From %s To %s; Quit: %s\n", sqlInfo.client, sqlInfo.server, "user quit") 132 | sqlInfo.sqlType = "Quit" 133 | case comInitDB: 134 | verboseStr = fmt.Sprintf("From %s To %s; schema: use %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 135 | sqlInfo.sqlType = "Schema" 136 | case comQuery: 137 | verboseStr = fmt.Sprintf("From %s To %s; Query: %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 138 | sqlInfo.sqlType = "Query" 139 | //case comFieldList: 140 | // verboseStr = log.Printf("From %s To %s; Table columns list: %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 141 | // sqlInfo.sqlType = "Table columns list" 142 | case comCreateDB: 143 | verboseStr = fmt.Sprintf("From %s To %s; CreateDB: %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 144 | sqlInfo.sqlType = "CreateDB" 145 | case comDropDB: 146 | verboseStr = fmt.Sprintf("From %s To %s; DropDB: %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 147 | sqlInfo.sqlType = "DropDB" 148 | case comRefresh: 149 | verboseStr = fmt.Sprintf("From %s To %s; Refresh: %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 150 | sqlInfo.sqlType = "Refresh" 151 | case comStmtPrepare: 152 | verboseStr = fmt.Sprintf("From %s To %s; Prepare Query: %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 153 | sqlInfo.sqlType = "Prepare Query" 154 | case comStmtExecute: 155 | verboseStr = fmt.Sprintf("From %s To %s; Prepare Args: %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 156 | sqlInfo.sqlType = "Prepare Args" 157 | case comProcessKill: 158 | verboseStr = fmt.Sprintf("From %s To %s; Kill: kill conntion %s\n", sqlInfo.client, sqlInfo.server, string(buffer[5:n])) 159 | sqlInfo.sqlType = "Kill" 160 | default: 161 | } 162 | 163 | if Verbose { 164 | log.Print(verboseStr) 165 | } 166 | 167 | if strings.EqualFold(sqlInfo.sqlType, "Quit") { 168 | sqlInfo.sqlString = "user quit" 169 | } else { 170 | sqlInfo.sqlString = converToUnixLine(sql_escape(string(buffer[5:n]))) 171 | } 172 | 173 | if !strings.EqualFold(sqlInfo.sqlType, "") && Dbh != nil { 174 | insertlog(Dbh, &sqlInfo) 175 | } 176 | 177 | } 178 | 179 | _, err = dst.Write(buffer[0:n]) 180 | if err != nil { 181 | return 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Run as a tcp port proxy if there are multi datacentors in your 5 | production, Receive the traffic and redirect to real server. 6 | 7 | cz-20151119 8 | */ 9 | 10 | import ( 11 | "database/sql" 12 | "flag" 13 | "github.com/VividCortex/godaemon" 14 | "log" 15 | "log/syslog" 16 | "os" 17 | "os/signal" 18 | "syscall" 19 | "time" 20 | ) 21 | 22 | //ignore signal 23 | func waitSignal() { 24 | var sigChan = make(chan os.Signal, 1) 25 | signal.Notify(sigChan) 26 | for sig := range sigChan { 27 | if sig == syscall.SIGINT || sig == syscall.SIGTERM { 28 | log.Printf("terminated by signal %v\n", sig) 29 | } else { 30 | log.Printf("received signal: %v, ignore\n", sig) 31 | } 32 | } 33 | } 34 | 35 | //net timeout 36 | const timeout = time.Second * 2 37 | 38 | var Bsize uint 39 | var Verbose bool 40 | var Dbh *sql.DB 41 | 42 | func main() { 43 | // options 44 | var bind, backend, logTo string 45 | var buffer uint 46 | var daemon bool 47 | var verbose bool 48 | var conf string 49 | 50 | flag.StringVar(&bind, "bind", ":8002", "locate ip and port") 51 | flag.StringVar(&backend, "backend", "127.0.0.1:8003", "backend server ip and port") 52 | flag.StringVar(&logTo, "logTo", "stdout", "stdout or syslog") 53 | flag.UintVar(&buffer, "buffer", 4096, "buffer size") 54 | flag.BoolVar(&daemon, "daemon", false, "run as daemon process") 55 | flag.BoolVar(&verbose, "verbose", false, "print verbose sql query") 56 | flag.StringVar(&conf, "conf", "", "config file to verify database and record sql query") 57 | flag.Parse() 58 | Bsize = buffer 59 | Verbose = verbose 60 | 61 | conf_fh, err := get_config(conf) 62 | if err != nil { 63 | log.Printf("Can't get config info, skip insert log to mysql...\n") 64 | } else { 65 | backend_dsn, _ := get_backend_dsn(conf_fh) 66 | Dbh, err = dbh(backend_dsn) 67 | if err != nil { 68 | log.Printf("Can't get database handle, skip insert log to mysql...\n") 69 | } 70 | defer Dbh.Close() 71 | } 72 | 73 | log.SetOutput(os.Stdout) 74 | if logTo == "syslog" { 75 | w, err := syslog.New(syslog.LOG_INFO, "portproxy") 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | log.SetOutput(w) 80 | } 81 | 82 | if daemon == true { 83 | godaemon.MakeDaemon(&godaemon.DaemonAttr{}) 84 | } 85 | 86 | p := New(bind, backend, uint32(buffer)) 87 | log.Println("portproxy started.") 88 | go p.Start() 89 | waitSignal() 90 | } 91 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "strings" 8 | "sync/atomic" 9 | "time" 10 | ) 11 | 12 | type Proxy struct { 13 | bind, backend *net.TCPAddr 14 | sessionsCount int32 15 | pool *recycler 16 | } 17 | 18 | func New(bind, backend string, size uint32) *Proxy { 19 | a1, err := net.ResolveTCPAddr("tcp", bind) 20 | if err != nil { 21 | log.Fatalln("resolve bind error:", err) 22 | } 23 | 24 | a2, err := net.ResolveTCPAddr("tcp", backend) 25 | if err != nil { 26 | log.Fatalln("resolve backend error:", err) 27 | } 28 | 29 | return &Proxy{ 30 | bind: a1, 31 | backend: a2, 32 | sessionsCount: 0, 33 | pool: NewRecycler(size), 34 | } 35 | } 36 | 37 | func (t *Proxy) pipe(dst, src *Conn, c chan int64, tag string) { 38 | defer func() { 39 | dst.CloseWrite() 40 | dst.CloseRead() 41 | }() 42 | if strings.EqualFold(tag, "send") { 43 | proxyLog(src, dst) 44 | c <- 0 45 | } else { 46 | n, err := io.Copy(dst, src) 47 | if err != nil { 48 | log.Print(err) 49 | } 50 | c <- n 51 | } 52 | } 53 | 54 | func (t *Proxy) transport(conn net.Conn) { 55 | start := time.Now() 56 | conn2, err := net.DialTCP("tcp", nil, t.backend) 57 | if err != nil { 58 | log.Print(err) 59 | return 60 | } 61 | connectTime := time.Now().Sub(start) 62 | log.Printf("proxy: %s ==> %s", conn2.LocalAddr().String(), 63 | conn2.RemoteAddr().String()) 64 | start = time.Now() 65 | readChan := make(chan int64) 66 | writeChan := make(chan int64) 67 | var readBytes, writeBytes int64 68 | 69 | atomic.AddInt32(&t.sessionsCount, 1) 70 | var bindConn, backendConn *Conn 71 | bindConn = NewConn(conn, t.pool) 72 | backendConn = NewConn(conn2, t.pool) 73 | 74 | go t.pipe(backendConn, bindConn, writeChan, "send") 75 | go t.pipe(bindConn, backendConn, readChan, "receive") 76 | 77 | readBytes = <-readChan 78 | writeBytes = <-writeChan 79 | transferTime := time.Now().Sub(start) 80 | log.Printf("r: %d w:%d ct:%.3f t:%.3f [#%d]", readBytes, writeBytes, 81 | connectTime.Seconds(), transferTime.Seconds(), t.sessionsCount) 82 | atomic.AddInt32(&t.sessionsCount, -1) 83 | } 84 | 85 | func (t *Proxy) Start() { 86 | ln, err := net.ListenTCP("tcp", t.bind) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | defer ln.Close() 92 | for { 93 | conn, err := ln.AcceptTCP() 94 | if err != nil { 95 | log.Println("accept:", err) 96 | continue 97 | } 98 | log.Printf("client: %s ==> %s", conn.RemoteAddr().String(), 99 | conn.LocalAddr().String()) 100 | go t.transport(conn) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /recycler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "container/list" 5 | "time" 6 | ) 7 | 8 | type recyclerItem struct { 9 | when time.Time 10 | buf []byte 11 | } 12 | 13 | type recycler struct { 14 | q *list.List 15 | takeChan, giveChan chan []byte 16 | } 17 | 18 | func NewRecycler(size uint32) *recycler { 19 | r := &recycler{ 20 | q: new(list.List), 21 | takeChan: make(chan []byte), 22 | giveChan: make(chan []byte), 23 | } 24 | go r.cycle(size) 25 | return r 26 | } 27 | 28 | func (r *recycler) cycle(size uint32) { 29 | for { 30 | if r.q.Len() == 0 { 31 | //put to front so that we always use the most recent buf 32 | r.q.PushFront(recyclerItem{when: time.Now(), buf: make([]byte, size)}) 33 | } 34 | i := r.q.Front() 35 | timeout := time.NewTimer(time.Minute) 36 | select { 37 | case b := <-r.giveChan: 38 | timeout.Stop() 39 | r.q.PushFront(recyclerItem{when: time.Now(), buf: b}) 40 | case r.takeChan <- i.Value.(recyclerItem).buf: 41 | timeout.Stop() 42 | r.q.Remove(i) 43 | case <-timeout.C: 44 | i := r.q.Front() 45 | for i != nil { 46 | n := i.Next() 47 | if time.Since(i.Value.(recyclerItem).when) > time.Minute { 48 | r.q.Remove(i) 49 | i.Value = nil 50 | } 51 | i = n 52 | } 53 | } 54 | } 55 | } 56 | 57 | func (r *recycler) take() []byte { 58 | return <-r.takeChan 59 | } 60 | 61 | func (r *recycler) give(b []byte) { 62 | r.giveChan <- b 63 | } 64 | -------------------------------------------------------------------------------- /test.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `query_log` ( 2 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 3 | `bindport` smallint(5) unsigned NOT NULL, 4 | `client` char(15) NOT NULL DEFAULT '', 5 | `client_port` smallint(5) unsigned NOT NULL, 6 | `server` char(15) NOT NULL DEFAULT '', 7 | `server_port` smallint(5) unsigned NOT NULL, 8 | `sql_type` varchar(30) NOT NULL DEFAULT 'Query', 9 | `sql_string` text, 10 | `create_time` datetime NOT NULL, 11 | PRIMARY KEY (`id`), 12 | KEY `idx_client` (`client`), 13 | KEY `idx_server` (`server`), 14 | KEY `idx_cretime` (`create_time`) 15 | ) ENGINE=InnoDB AUTO_INCREMENT=9945 DEFAULT CHARSET=utf8 16 | --------------------------------------------------------------------------------