Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Download binary from [release](https://github.com/Dream95/gotproxy/releases) or
```bash
sudo ./gotproxy [flags]
```
### Proxy / forwarding flags

| Flag | Description |
| :--- | :--- |
| **--cmd** | The command name to be proxied. If not provided, all traffic will be proxied globally. |
Expand All @@ -45,6 +47,20 @@ sudo ./gotproxy [flags]
| **--proto** | Proxy protocol selection: `both` (default) / `tcp` / `udp`. When set to `tcp`, only TCP traffic will be redirected; when set to `udp`, only UDP traffic will be redirected. |
| **--no-dns53** | Disable automatic UDP DNS rewrite from `127.0.0.53:53` to `1.1.1.1:53` (enabled by default). |

### Mirror (traffic mirroring) flags

Mirroring is independent of proxy forwarding: it best-effort duplicates the original traffic to a target address.

| Flag | Description |
| :--- | :--- |
| **--mirror-enable** | Enable best-effort traffic mirroring. |
| **--mirror-target** | Mirror destination address, for example `10.0.0.2:9000`. |
| **--mirror-proto** | Mirror protocol: `auto` (default, follows `--proto`) / `both` / `tcp` / `udp`. |
| **--mirror-timeout-ms** | Mirror write timeout in milliseconds (default: `100`). |
| **--mirror-queue** | Mirror async queue size (default: `1024`). |
| **--mirror-drop-on-full** | Drop mirrored packets when queue is full (default: `true`). |


Features Under Development:
IPv6 support

Expand Down Expand Up @@ -77,12 +93,18 @@ sudo ./gotproxy --proto tcp
sudo ./gotproxy --proto udp
```

5. Proxy by container name:
5. Proxy with traffic mirroring:

```bash
sudo ./gotproxy --proto both --mirror-enable --mirror-target 10.0.0.2:9000
```

6. Proxy by container name:
```bash
sudo ./gotproxy --container-name curl-test
```

6. Use container and pid together:
7. Use container and pid together:
```bash
sudo ./gotproxy --container-name curl-test --pids 1234
```
Expand Down
25 changes: 23 additions & 2 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
```bash
sudo ./gotproxy [flags]
```
### 代理 / 转发 flags

| Flag | 描述 |
| :--- | :--- |
| **--cmd** | 需要代理的进程名称. 如果没有配置,则会进行全局流量代理. |
Expand All @@ -46,6 +48,19 @@ sudo ./gotproxy [flags]
| **--proto** | 代理协议选择:`both`(默认)/ `tcp` / `udp`。当设置为 `tcp` 时只重定向 TCP 流量;设置为 `udp` 时只重定向 UDP 流量。 |
| **--no-dns53** | 关闭 UDP DNS 对 `127.0.0.53:53` 的自动改写。默认会自动改写为 `1.1.1.1:53`。 |

### Mirror(流量复制)flags

Mirror 与代理/转发功能相互独立:它会尽力将原始流量复制一份发送到指定目标。

| Flag | 描述 |
| :--- | :--- |
| **--mirror-enable** | 开启尽力而为的流量复制。 |
| **--mirror-target** | 复制目标地址,例如 `10.0.0.2:9000`。 |
| **--mirror-proto** | 复制协议:`auto`(默认,跟随 `--proto`)/ `both` / `tcp` / `udp`。 |
| **--mirror-timeout-ms** | 复制写超时时间(毫秒,默认 `100`)。 |
| **--mirror-queue** | 复制异步队列大小(默认 `1024`)。 |
| **--mirror-drop-on-full** | 当队列满时是否丢弃复制数据(默认 `true`)。 |


正在开发中的功能:
支持ipv6
Expand Down Expand Up @@ -82,12 +97,18 @@ sudo ./gotproxy --proto tcp
sudo ./gotproxy --proto udp
```

5. 按容器名称代理:
5. 代理并开启流量镜像(Mirror):

```bash
sudo ./gotproxy --proto both --mirror-enable --mirror-target 10.0.0.2:9000
```

6. 按容器名称代理:
```bash
sudo ./gotproxy --container-name curl-test
```

6. 容器名 + pid 同时过滤:
7. 容器名 + pid 同时过滤:
```bash
sudo ./gotproxy --container-name curl-test --pids 1234
```
Expand Down
40 changes: 40 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"log"
"os"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"
)
Expand All @@ -22,6 +24,12 @@ var (
socks5Pass string
proto string
noDNS53 bool
mirrorEnable bool
mirrorTarget string
mirrorProto string
mirrorTimeoutMs int
mirrorQueue int
mirrorDropFull bool
)

var rootCmd = &cobra.Command{
Expand All @@ -46,6 +54,24 @@ var rootCmd = &cobra.Command{
default:
log.Fatalf("Invalid --proto value %q, expected one of: both|tcp|udp", proto)
}
resolvedMirrorProto := strings.TrimSpace(strings.ToLower(mirrorProto))
if resolvedMirrorProto == "auto" {
resolvedMirrorProto = proto
}
switch resolvedMirrorProto {
case "both", "tcp", "udp":
default:
log.Fatalf("Invalid --mirror-proto value %q, expected one of: auto|both|tcp|udp", mirrorProto)
}
if mirrorEnable && strings.TrimSpace(mirrorTarget) == "" {
log.Fatalf("Invalid mirror config: --mirror-enable requires --mirror-target")
}
if mirrorTimeoutMs <= 0 {
log.Fatalf("Invalid --mirror-timeout-ms value %d, expected > 0", mirrorTimeoutMs)
}
if mirrorQueue <= 0 {
log.Fatalf("Invalid --mirror-queue value %d, expected > 0", mirrorQueue)
}

Options := &Options{
Command: command,
Expand All @@ -54,6 +80,14 @@ var rootCmd = &cobra.Command{
ContainerName: containerName,
EnableTCP: enableTCP,
EnableUDP: enableUDP,
Mirror: MirrorOptions{
Enabled: mirrorEnable,
Target: strings.TrimSpace(mirrorTarget),
Proto: resolvedMirrorProto,
Timeout: time.Duration(mirrorTimeoutMs) * time.Millisecond,
QueueSize: mirrorQueue,
DropOnFull: mirrorDropFull,
},
}

if ok, err := common.HasPermission(); err != nil {
Expand Down Expand Up @@ -106,4 +140,10 @@ func init() {
rootCmd.PersistentFlags().StringVar(&socks5Pass, "socks5-pass", "", "The SOCKS5 password. Requires --socks5-user.")
rootCmd.PersistentFlags().StringVar(&proto, "proto", "both", "Proxy protocol: both|tcp|udp")
rootCmd.PersistentFlags().BoolVar(&noDNS53, "no-dns53", false, "Disable UDP DNS destination rewrite from 127.0.0.53:53 to 1.1.1.1:53")
rootCmd.PersistentFlags().BoolVar(&mirrorEnable, "mirror-enable", false, "Enable traffic mirroring")
rootCmd.PersistentFlags().StringVar(&mirrorTarget, "mirror-target", "", "Mirror destination address, e.g. 10.0.0.2:9000")
rootCmd.PersistentFlags().StringVar(&mirrorProto, "mirror-proto", "auto", "Mirror protocol: auto|both|tcp|udp")
rootCmd.PersistentFlags().IntVar(&mirrorTimeoutMs, "mirror-timeout-ms", 100, "Mirror write timeout in milliseconds")
rootCmd.PersistentFlags().IntVar(&mirrorQueue, "mirror-queue", 1024, "Mirror async queue size")
rootCmd.PersistentFlags().BoolVar(&mirrorDropFull, "mirror-drop-on-full", true, "Drop mirrored packets when mirror queue is full")
}
3 changes: 2 additions & 1 deletion cmd/loadBpf.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Options struct {
Ip4Mask uint8
EnableTCP bool
EnableUDP bool
Mirror MirrorOptions
}

func LoadBpf(options *Options) {
Expand Down Expand Up @@ -85,7 +86,7 @@ func LoadBpf(options *Options) {

// Start TCP (and UDP) proxy so it can use objs.MapUdpDest for UDP original-dest lookup
if options.ProxyPid == 0 {
StartProxy(objs.MapUdpDest, options.EnableTCP, options.EnableUDP, proxyListenHost)
StartProxy(objs.MapUdpDest, options.EnableTCP, options.EnableUDP, proxyListenHost, options.Mirror)
}

// Attach eBPF programs to the root cgroup
Expand Down
111 changes: 111 additions & 0 deletions cmd/mirror.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package main

import (
"log"
"net"
"strings"
"sync/atomic"
"time"
)

type MirrorOptions struct {
Enabled bool
Target string
Proto string
Timeout time.Duration
QueueSize int
DropOnFull bool
}

type mirrorMessage struct {
network string
payload []byte
}

type MirrorDispatcher struct {
opts MirrorOptions
queue chan mirrorMessage
dropped atomic.Uint64
sent atomic.Uint64
sendError atomic.Uint64
}

func NewMirrorDispatcher(opts MirrorOptions) *MirrorDispatcher {
if !opts.Enabled || opts.Target == "" {
return nil
}
if opts.Timeout <= 0 {
opts.Timeout = 100 * time.Millisecond
}
if opts.QueueSize <= 0 {
opts.QueueSize = 1024
}
d := &MirrorDispatcher{
opts: opts,
queue: make(chan mirrorMessage, opts.QueueSize),
}
go d.run()
log.Printf("Mirror enabled: target=%s proto=%s direction=uplink queue=%d timeout=%s", opts.Target, opts.Proto, opts.QueueSize, opts.Timeout)
return d
}

func (d *MirrorDispatcher) ShouldMirror(network string) bool {
if d == nil {
return false
}
return matchMirrorProto(d.opts.Proto, network)
}

func (d *MirrorDispatcher) Enqueue(network string, payload []byte) {
if d == nil || len(payload) == 0 {
return
}
msg := mirrorMessage{
network: network,
payload: append([]byte(nil), payload...),
}
if d.opts.DropOnFull {
select {
case d.queue <- msg:
default:
d.dropped.Add(1)
}
return
}
d.queue <- msg
}

func (d *MirrorDispatcher) run() {
for msg := range d.queue {
if err := d.send(msg); err != nil {
d.sendError.Add(1)
log.Printf("Mirror send failed (%s -> %s): %v", msg.network, d.opts.Target, err)
continue
}
d.sent.Add(1)
}
}

func (d *MirrorDispatcher) send(msg mirrorMessage) error {
conn, err := net.DialTimeout(msg.network, d.opts.Target, d.opts.Timeout)
if err != nil {
return err
}
defer conn.Close()
_ = conn.SetWriteDeadline(time.Now().Add(d.opts.Timeout))
_, err = conn.Write(msg.payload)
return err
}

func matchMirrorProto(config string, network string) bool {
c := strings.ToLower(strings.TrimSpace(config))
n := strings.ToLower(strings.TrimSpace(network))
switch c {
case "both":
return n == "tcp" || n == "udp"
case "tcp", "udp":
return c == n
default:
return false
}
}
Loading
Loading