mirror of
https://github.com/kevwan/tproxy.git
synced 2026-05-23 07:40:35 +00:00
initial import
This commit is contained in:
+30
@@ -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
@@ -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"]
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# tproxy
|
||||
|
||||
[English](readme.md) | 简体中文
|
||||
|
||||
[](https://github.com/kevwan/tproxy/actions)
|
||||
[](https://codecov.io/gh/kevwan/tproxy)
|
||||
[](https://goreportcard.com/report/github.com/kevwan/tproxy)
|
||||
[](https://github.com/kevwan/tproxy)
|
||||
[](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
|
||||
|
||||

|
||||
|
||||
### 分析 MySQL 连接
|
||||
|
||||
```shell
|
||||
tproxy -p 3307 -r localhost:3306
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 欢迎 star!⭐
|
||||
|
||||
如果你正在使用或者觉得这个项目对你有帮助,请 **star** 支持,感谢!
|
||||
@@ -0,0 +1,77 @@
|
||||
# tproxy
|
||||
|
||||
English | [简体中文](readme-cn.md)
|
||||
|
||||
[](https://github.com/kevwan/tproxy/actions)
|
||||
[](https://codecov.io/gh/kevwan/tproxy)
|
||||
[](https://goreportcard.com/report/github.com/kevwan/tproxy)
|
||||
[](https://github.com/kevwan/tproxy)
|
||||
[](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
|
||||
|
||||

|
||||
|
||||
### Monitor MySQL connections
|
||||
|
||||
```shell
|
||||
tproxy -p 3307 -r localhost:3306
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Give a Star! ⭐
|
||||
|
||||
If you like or are using this project to learn or start your solution, please give it a star. Thanks!
|
||||
+23
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user