initial import

This commit is contained in:
kevin
2022-06-14 09:23:51 +08:00
parent 592903990e
commit cf495c2409
15 changed files with 551 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# Ignore all
*
# Unignore all with extensions
!*.*
!**/Dockerfile
!**/Makefile
# Unignore all dirs
!*/
!api
# ignore
.idea
**/.DS_Store
**/logs
modd.conf
# for test purpose
**/adhoc
app
go.work
go.work.sum
# gitlab ci
.cache
# vim auto backup file
*~
!OWNERS
+29
View File
@@ -0,0 +1,29 @@
FROM golang:alpine AS builder
LABEL stage=gobuilder
ENV CGO_ENABLED 0
ENV GOPROXY https://goproxy.cn,direct
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk update --no-cache && apk add --no-cache tzdata
WORKDIR /build
ADD go.mod .
ADD go.sum .
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /app/tproxy .
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
ENV TZ Asia/Shanghai
WORKDIR /app
COPY --from=builder /app/tproxy /app/tproxy
CMD ["./tproxy"]
+111
View File
@@ -0,0 +1,111 @@
package main
import (
"fmt"
"io"
"net"
"sync"
"github.com/kevwan/tproxy/display"
"github.com/kevwan/tproxy/protocol"
)
const (
serverSide = "SERVER"
clientSide = "CLIENT"
useOfClosedConn = "use of closed network connection"
)
type PairedConnection struct {
id int
cliConn net.Conn
svrConn net.Conn
once sync.Once
}
func NewPairedConnection(id int, cliConn net.Conn) *PairedConnection {
return &PairedConnection{
id: id,
cliConn: cliConn,
}
}
func (c *PairedConnection) handleClientMessage() {
r, w := io.Pipe()
tee := io.MultiWriter(c.svrConn, w)
go protocol.NewDumper(r, clientSide, c.id, settings.Silent, protocol.CreateInterop(settings.Protocol)).Dump()
_, e := io.Copy(tee, c.cliConn)
if e != nil && e != io.EOF {
fmt.Printf("handleClientMessage: io.Copy error: %v", e)
}
}
func (c *PairedConnection) handleServerMessage() {
r, w := io.Pipe()
tee := io.MultiWriter(c.cliConn, w)
go protocol.NewDumper(r, serverSide, c.id, settings.Silent, protocol.CreateInterop(settings.Protocol)).Dump()
_, e := io.Copy(tee, c.svrConn)
if e != nil && e != io.EOF {
netOpError, ok := e.(*net.OpError)
if ok && netOpError.Err.Error() != useOfClosedConn {
fmt.Printf("handleServerMessage: io.Copy error: %v", e)
}
}
c.stop()
}
func (c *PairedConnection) process() {
conn, err := net.Dial("tcp", settings.RemoteHost)
if err != nil {
display.PrintfWithTime("[x][%d] Couldn't connect to server: %v\n", c.id, err)
return
}
display.PrintfWithTime("[*][%d] Connected to server: %s\n", c.id, conn.RemoteAddr())
c.svrConn = conn
go c.handleServerMessage()
c.handleClientMessage()
c.stop()
}
func (c *PairedConnection) stop() {
c.once.Do(func() {
if c.cliConn != nil {
display.PrintfWithTime("[*][%d] Client connection closed\n", c.id)
c.cliConn.Close()
}
if c.svrConn != nil {
display.PrintfWithTime("[*][%d] Server connection closed\n", c.id)
c.svrConn.Close()
}
})
}
func startListener() error {
conn, err := net.Listen("tcp", fmt.Sprint(settings.LocalHost, ":", settings.LocalPort))
if err != nil {
return fmt.Errorf("failed to start listener: %w", err)
}
display.PrintlnWithTime("[*] Listening...")
defer conn.Close()
var connIndex int
for {
cliConn, err := conn.Accept()
if err != nil {
return fmt.Errorf("server: accept: %w", err)
}
connIndex++
display.PrintfWithTime("[*][%d] Accepted from: %s\n", connIndex, cliConn.RemoteAddr())
pconn := NewPairedConnection(connIndex, cliConn)
go pconn.process()
}
}
+18
View File
@@ -0,0 +1,18 @@
package display
import (
"fmt"
"time"
)
const TimeFormat = "15:04:05.000"
func PrintfWithTime(format string, args ...interface{}) {
args = append([]interface{}{time.Now().Format(TimeFormat)}, args...)
fmt.Printf("%s "+format, args...)
}
func PrintlnWithTime(args ...interface{}) {
args = append([]interface{}{time.Now().Format(TimeFormat)}, args...)
fmt.Println(args...)
}
+16
View File
@@ -0,0 +1,16 @@
module github.com/kevwan/tproxy
go 1.18
require (
github.com/fatih/color v1.13.0
github.com/grpc/grpc/tools/http2_interop v0.0.0-20220611023707-932878b1ce81
github.com/olekukonko/tablewriter v0.0.5
)
require (
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
)
+17
View File
@@ -0,0 +1,17 @@
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/grpc/grpc/tools/http2_interop v0.0.0-20220611023707-932878b1ce81 h1:sYa3IEJkUhQaWW0VK2vomHvYe6GT7aGcBlP98Fq8+w8=
github.com/grpc/grpc/tools/http2_interop v0.0.0-20220611023707-932878b1ce81/go.mod h1:BeiyA4wp8QEh/jcnxJlpfalCT/XiQ5bbFhikEDrXQcw=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

+33
View File
@@ -0,0 +1,33 @@
package main
import (
"flag"
"fmt"
"os"
)
var settings Settings
func main() {
var (
localPort = flag.Int("p", 0, "Local port to listen on")
localHost = flag.String("l", "localhost", "Local address to listen on")
remote = flag.String("r", "", "Remote address (host:port) to connect")
protocol = flag.String("t", "", "the type of protocol, currently support grpc")
silent = flag.Bool("silent", false, "Only prints connection open/close and stats, default false")
)
flag.Parse()
saveSettings(*localHost, *localPort, *remote, *protocol, *silent)
if settings.RemoteHost == "" {
fmt.Fprintln(os.Stderr, "[x] Remote host required")
flag.PrintDefaults()
os.Exit(1)
}
if err := startListener(); err != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to start listener: %v\n", err)
os.Exit(1)
}
}
+75
View File
@@ -0,0 +1,75 @@
package protocol
import (
"encoding/hex"
"fmt"
"io"
"github.com/fatih/color"
"github.com/kevwan/tproxy/display"
)
const (
bufferSize = 1024
grpcProtocol = "grpc"
)
type Interop interface {
Interop(b []byte) (string, bool)
Protocol() string
}
func CreateInterop(protocol string) Interop {
switch protocol {
case grpcProtocol:
return new(GrpcInterop)
default:
return NilInterop{}
}
}
type Dumper struct {
r io.Reader
source string
id int
silent bool
interop Interop
}
func NewDumper(r io.Reader, source string, id int, silent bool, interop Interop) Dumper {
return Dumper{
r: r,
source: source,
id: id,
silent: silent,
interop: interop,
}
}
func (d Dumper) Dump() {
data := make([]byte, bufferSize)
for {
n, err := d.r.Read(data)
if n > 0 && !d.silent {
prot := d.interop.Protocol()
frameType, ok := d.interop.Interop(data)
if ok {
display.PrintfWithTime("from %s [%d] %s%s%s:\n",
d.source,
d.id,
color.HiBlueString("%s:(", prot),
frameType, color.HiBlueString(")"))
} else {
display.PrintfWithTime("from %s [%d]:\n", d.source, d.id)
}
fmt.Println(hex.Dump(data[:n]))
}
if err != nil && err != io.EOF {
fmt.Printf("unable to read data %v", err)
break
}
if n == 0 {
break
}
}
}
+34
View File
@@ -0,0 +1,34 @@
package protocol
import (
"strings"
"github.com/fatih/color"
"github.com/grpc/grpc/tools/http2_interop"
)
const grpcHeaderLen = 9
type GrpcInterop struct{}
func (i *GrpcInterop) Interop(b []byte) (string, bool) {
if len(b) < grpcHeaderLen {
return "", false
}
var frame http2interop.FrameHeader
// ignore errors
if err := frame.UnmarshalBinary(b[:9]); err == nil {
if frame.Type == http2interop.FrameType(32) {
return color.HiYellowString("http2:PRI"), true
}
return color.HiYellowString(strings.ToLower(frame.Type.String())), true
}
return "", false
}
func (i *GrpcInterop) Protocol() string {
return grpcProtocol
}
+11
View File
@@ -0,0 +1,11 @@
package protocol
type NilInterop struct{}
func (i NilInterop) Interop(_ []byte) (string, bool) {
return "", false
}
func (i NilInterop) Protocol() string {
return "unknown"
}
+77
View File
@@ -0,0 +1,77 @@
# tproxy
[English](readme.md) | 简体中文
[![Go](https://github.com/kevwan/tproxy/workflows/Go/badge.svg?branch=main)](https://github.com/kevwan/tproxy/actions)
[![codecov](https://codecov.io/gh/kevwan/tproxy/branch/main/graph/badge.svg)](https://codecov.io/gh/kevwan/tproxy)
[![Go Report Card](https://goreportcard.com/badge/github.com/kevwan/tproxy)](https://goreportcard.com/report/github.com/kevwan/tproxy)
[![Release](https://img.shields.io/github/v/release/kevwan/tproxy.svg?style=flat-square)](https://github.com/kevwan/tproxy)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## 为啥写这个工具
当我在做后端开发或者写 [go-zero](https://github.com/zeromicro/go-zero) 的时候,经常会需要监控网络连接,分析请求内容。比如:
1. 分析 gRPC 连接何时连接、何时重连
2. 分析 MySQL 连接池,当前多少连接,连接的生命周期是什么策略
3. 也可以用来观察和分析任何 TCP 连接
## 安装
```shell
$ go install github.com/kevwan/tproxy@latest
```
或者使用 docker 镜像:
```shell
docker run --rm -it -p <listen-port>:<listen-port> -p <remote-port>:<remote-port> kevinwan/tproxy:v1 tproxy -l 0.0.0.0 -p <listen-port> -r host.docker.internal:<remote-port>
```
arm64 系统:
```shell
docker run --rm -it -p <listen-port>:<listen-port> -p <remote-port>:<remote-port> kevinwan/tproxy:v1-arm64 tproxy -l 0.0.0.0 -p <listen-port> -r host.docker.internal:<remote-port>
```
## 用法
```shell
$ tproxy --help
Usage of tproxy:
-l string
Local address to listen on (default "localhost")
-p int
Local port to listen on
-r string
Remote address (host:port) to connect
-silent
Only prints connection open/close and stats, default false
-t string
the type of protocol, currently support grpc
```
## 示例
### 分析 gRPC 连接
```shell
tproxy -p 8088 -r localhost:8081 -t grpc
```
- 侦听在 localhost 和 8088 端口
- 重定向请求到 localhost:8081
- 识别数据包格式为 gRPC
![grpc](images/grpc.png)
### 分析 MySQL 连接
```shell
tproxy -p 3307 -r localhost:3306
```
![mysql](images/mysql.png)
## 欢迎 star!⭐
如果你正在使用或者觉得这个项目对你有帮助,请 **star** 支持,感谢!
+77
View File
@@ -0,0 +1,77 @@
# tproxy
English | [简体中文](readme-cn.md)
[![Go](https://github.com/kevwan/tproxy/workflows/Go/badge.svg?branch=main)](https://github.com/kevwan/tproxy/actions)
[![codecov](https://codecov.io/gh/kevwan/tproxy/branch/main/graph/badge.svg)](https://codecov.io/gh/kevwan/tproxy)
[![Go Report Card](https://goreportcard.com/badge/github.com/kevwan/tproxy)](https://goreportcard.com/report/github.com/kevwan/tproxy)
[![Release](https://img.shields.io/github/v/release/kevwan/tproxy.svg?style=flat-square)](https://github.com/kevwan/tproxy)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## Why I wrote this tool
When I develop backend services and write [go-zero](https://github.com/zeromicro/go-zero), I often need to monitor the network traffic. For example:
1. monitoring gRPC connections, when to connect and when to reconnect
2. monitoring MySQL connection pools, how many connections and figure out the lifetime policy
3. monitoring any TCP connections on the fly
## Installation
```shell
$ go install github.com/kevwan/tproxy@latest
```
Or use docker images:
```shell
docker run --rm -it -p <listen-port>:<listen-port> -p <remote-port>:<remote-port> kevinwan/tproxy:v1 tproxy -l 0.0.0.0 -p <listen-port> -r host.docker.internal:<remote-port>
```
For arm64:
```shell
docker run --rm -it -p <listen-port>:<listen-port> -p <remote-port>:<remote-port> kevinwan/tproxy:v1-arm64 tproxy -l 0.0.0.0 -p <listen-port> -r host.docker.internal:<remote-port>
```
## Usages
```shell
$ tproxy --help
Usage of tproxy:
-l string
Local address to listen on (default "localhost")
-p int
Local port to listen on
-r string
Remote address (host:port) to connect
-silent
Only prints connection open/close and stats, default false
-t string
the type of protocol, currently support grpc
```
## Examples
### Monitor gRPC connections
```shell
tproxy -p 8088 -r localhost:8081 -t grpc
```
- listen on localhost and port 8088
- redirect the traffic to localhost:8081
- protocol type to be gRPC
![grpc](images/grpc.png)
### Monitor MySQL connections
```shell
tproxy -p 3307 -r localhost:3306
```
![mysql](images/mysql.png)
## Give a Star! ⭐
If you like or are using this project to learn or start your solution, please give it a star. Thanks!
+23
View File
@@ -0,0 +1,23 @@
package main
type Settings struct {
RemoteHost string
LocalHost string
LocalPort int
Protocol string
Silent bool
}
func saveSettings(localHost string, localPort int, remoteHost, protocol string, silent bool) {
if localHost != "" {
settings.LocalHost = localHost
}
if localPort != 0 {
settings.LocalPort = localPort
}
if remoteHost != "" {
settings.RemoteHost = remoteHost
}
settings.Protocol = protocol
settings.Silent = silent
}