Compare commits

..

21 Commits

Author SHA1 Message Date
Owen Schwartz
f346b6cc5d Bump actions/upload-artifact 2025-01-30 10:18:46 -05:00
Owen Schwartz
f20b9ebb14 Merge pull request #9 from fosrl/dev
CICD, --version & Bug Fixes
2025-01-30 10:14:31 -05:00
Owen Schwartz
39bfe5b230 Insert version CICD 2025-01-29 22:31:14 -05:00
Milo Schwartz
a1a3dd9ba2 Merge branch 'dev' of https://github.com/fosrl/newt into dev 2025-01-29 22:23:42 -05:00
Milo Schwartz
7b1492f327 add cicd 2025-01-29 22:23:03 -05:00
Owen Schwartz
4e50819785 Add --version check 2025-01-29 22:19:18 -05:00
Owen Schwartz
f8dccbec80 Fix save config 2025-01-29 22:15:28 -05:00
Owen Schwartz
0c5c59cf00 Fix removing udp sockets 2025-01-27 21:28:22 -05:00
Owen Schwartz
868bb55f87 Fix windows build in release 2025-01-20 21:40:55 -05:00
Owen Schwartz
5b4245402a Merge pull request #6 from fosrl/dev
Proxy Manager Rewrite
2025-01-20 21:15:31 -05:00
Owen Schwartz
f7a705e6f8 Remove starts 2025-01-20 21:13:09 -05:00
Owen Schwartz
3a63657822 Rewrite proxy manager 2025-01-20 21:11:06 -05:00
Owen Schwartz
759780508a Resolve TCP hanging but port is in use issue 2025-01-19 22:46:00 -05:00
Owen Schwartz
533886f2e4 Standarize makefile release 2025-01-16 07:41:56 -05:00
Owen Schwartz
79f8745909 Merge pull request #5 from fosrl/dev
Add tip and MTU set to 1280
2025-01-15 22:41:30 -05:00
Owen Schwartz
7b663027ac Add tip 2025-01-15 21:57:14 -05:00
Owen Schwartz
e90e55d982 Allow chaning mtu; set default low 2025-01-13 22:51:36 -05:00
Owen Schwartz
a46fb23cdd Add all arches and log level 2025-01-13 21:22:17 -05:00
Milo Schwartz
10982b47a5 fix typos in readme 2025-01-09 16:44:25 -05:00
Milo Schwartz
ab12098c9c Merge pull request #4 from fosrl/dev
add security policy
2025-01-08 21:57:45 -05:00
Milo Schwartz
446eb4d6f1 add security policy 2025-01-08 21:36:03 -05:00
12 changed files with 403 additions and 305 deletions

58
.github/workflows/cicd.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: CI/CD Pipeline
on:
push:
tags:
- "*"
jobs:
release:
name: Build and Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.23.1
- name: Update version in main.go
run: |
TAG=${{ env.TAG }}
if [ -f main.go ]; then
sed -i 's/Newt version replaceme/Newt version '"$TAG"'/' main.go
echo "Updated main.go with version $TAG"
else
echo "main.go not found"
fi
- name: Build and push Docker images
run: |
TAG=${{ env.TAG }}
make docker-build-release tag=$TAG
- name: Build binaries
run: |
make go-build-release
- name: Upload artifacts from /bin
uses: actions/upload-artifact@v4
with:
name: binaries
path: bin/

4
.gitignore vendored
View File

@@ -1 +1,3 @@
newt
newt
.DS_Store
bin/

View File

@@ -1,6 +1,14 @@
all: build push
docker-build-release:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-all tag=<tag>"; \
exit 1; \
fi
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/newt:latest -f Dockerfile --push .
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/newt:$(tag) -f Dockerfile --push .
build:
docker build -t fosrl/newt:latest .
@@ -11,7 +19,16 @@ test:
docker run fosrl/newt:latest
local:
CGO_ENABLED=0 go build -o newt
CGO_ENABLED=0 go build -o newt
go-build-release:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/newt_windows_amd64.exe
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64
clean:
rm newt

View File

@@ -19,7 +19,7 @@ _Sample output of a Newt container connected to Pangolin and hosting various res
### Registers with Pangolin
Using the Newt ID and a secret the client will make HTTP requests to Pangolin to receive a session token. Using that token it will connect to a websocket and maintain that connection. Control messages will be sent over the websocket.
Using the Newt ID and a secret, the client will make HTTP requests to Pangolin to receive a session token. Using that token, it will connect to a websocket and maintain that connection. Control messages will be sent over the websocket.
### Receives WireGuard Control Messages
@@ -27,7 +27,7 @@ When Newt receives WireGuard control messages, it will use the information encod
### Receives Proxy Control Messages
When Newt receives WireGuard control messages, it will use the information encoded to crate local low level TCP and UDP proxies attached to the virtual tunnel in order to relay traffic to programmed targets.
When Newt receives WireGuard control messages, it will use the information encoded to create a local low level TCP and UDP proxies attached to the virtual tunnel in order to relay traffic to programmed targets.
## CLI Args
@@ -98,4 +98,4 @@ Newt is dual licensed under the AGPLv3 and the Fossorial Commercial license. For
## Contributions
Please see [CONTRIBUTIONS](./CONTRIBUTING.md) in the repository for guidelines and best practices.
Please see [CONTRIBUTIONS](./CONTRIBUTING.md) in the repository for guidelines and best practices.

14
SECURITY.md Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
- Description and location of the vulnerability.
- Potential impact of the vulnerability.
- Steps to reproduce the vulnerability.
- Potential solutions to fix the vulnerability.
- Your name/handle and a link for recognition (optional).
We aim to address the issue as soon as possible.

View File

@@ -6,4 +6,5 @@ services:
environment:
- PANGOLIN_ENDPOINT=https://example.com
- NEWT_ID=2ix2t8xk22ubpfy
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
- LOG_LEVEL=DEBUG

1
go.mod
View File

@@ -10,6 +10,7 @@ require (
github.com/google/btree v1.1.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/time v0.7.0 // indirect

2
go.sum
View File

@@ -4,6 +4,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=

39
main.go
View File

@@ -12,6 +12,7 @@ import (
"net/netip"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
@@ -123,6 +124,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{})
err := ping(tnet, serverIP)
if err != nil {
logger.Warn("Periodic ping failed: %v", err)
logger.Warn("HINT: Do you have UDP port 51280 (or the port in config.yml) open on your Pangolin server?")
}
case <-stopChan:
logger.Info("Stopping ping check")
@@ -247,6 +249,8 @@ func main() {
endpoint string
id string
secret string
mtu string
mtuInt int
dns string
privateKey wgtypes.Key
err error
@@ -257,6 +261,7 @@ func main() {
endpoint = os.Getenv("PANGOLIN_ENDPOINT")
id = os.Getenv("NEWT_ID")
secret = os.Getenv("NEWT_SECRET")
mtu = os.Getenv("MTU")
dns = os.Getenv("DNS")
logLevel = os.Getenv("LOG_LEVEL")
@@ -269,21 +274,34 @@ func main() {
if secret == "" {
flag.StringVar(&secret, "secret", "", "Newt secret")
}
if mtu == "" {
flag.StringVar(&mtu, "mtu", "1280", "MTU to use")
}
if dns == "" {
flag.StringVar(&dns, "dns", "8.8.8.8", "DNS server to use")
}
if logLevel == "" {
flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
}
// do a --version check
version := flag.Bool("version", false, "Print the version")
flag.Parse()
if *version {
fmt.Println("Newt version replaceme")
os.Exit(0)
}
logger.Init()
loggerLevel := parseLogLevel(logLevel)
logger.GetLogger().SetLevel(parseLogLevel(logLevel))
// Validate required fields
if endpoint == "" || id == "" || secret == "" {
logger.Fatal("endpoint, id, and secret are required either via CLI flags or environment variables")
// parse the mtu string into an int
mtuInt, err = strconv.Atoi(mtu)
if err != nil {
logger.Fatal("Failed to parse MTU: %v", err)
}
privateKey, err = wgtypes.GeneratePrivateKey()
@@ -333,7 +351,8 @@ func main() {
err = pingWithRetry(tnet, wgData.ServerIP)
if err != nil {
// Handle complete failure after all retries
logger.Error("Failed to ping %s: %v", wgData.ServerIP, err)
logger.Warn("Failed to ping %s: %v", wgData.ServerIP, err)
logger.Warn("HINT: Do you have UDP port 51280 (or the port in config.yml) open on your Pangolin server?")
}
return
}
@@ -353,7 +372,7 @@ func main() {
tun, tnet, err = netstack.CreateNetTUN(
[]netip.Addr{netip.MustParseAddr(wgData.TunnelIP)},
[]netip.Addr{netip.MustParseAddr(dns)},
1420)
mtuInt)
if err != nil {
logger.Error("Failed to create TUN device: %v", err)
}
@@ -440,11 +459,6 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
if len(targetData.Targets) > 0 {
updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData)
}
err = pm.Start()
if err != nil {
logger.Error("Failed to start proxy manager: %v", err)
}
})
client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) {
@@ -465,11 +479,6 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
if len(targetData.Targets) > 0 {
updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData)
}
err = pm.Start()
if err != nil {
logger.Error("Failed to start proxy manager: %v", err)
}
})
client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) {

View File

@@ -9,326 +9,344 @@ import (
"time"
"github.com/fosrl/newt/logger"
"golang.zx2c4.com/wireguard/tun/netstack"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
)
// Target represents a proxy target with its address and port
type Target struct {
Address string
Port int
}
// ProxyManager handles the creation and management of proxy connections
type ProxyManager struct {
tnet *netstack.Net
tcpTargets map[string]map[int]string // map[listenIP]map[port]targetAddress
udpTargets map[string]map[int]string
listeners []*gonet.TCPListener
udpConns []*gonet.UDPConn
running bool
mutex sync.RWMutex
}
// NewProxyManager creates a new proxy manager instance
func NewProxyManager(tnet *netstack.Net) *ProxyManager {
return &ProxyManager{
tnet: tnet,
tnet: tnet,
tcpTargets: make(map[string]map[int]string),
udpTargets: make(map[string]map[int]string),
listeners: make([]*gonet.TCPListener, 0),
udpConns: make([]*gonet.UDPConn, 0),
}
}
func (pm *ProxyManager) AddTarget(protocol, listen string, port int, target string) {
pm.Lock()
defer pm.Unlock()
// AddTarget adds a new target for proxying
func (pm *ProxyManager) AddTarget(proto, listenIP string, port int, targetAddr string) error {
pm.mutex.Lock()
defer pm.mutex.Unlock()
logger.Info("Adding target: %s://%s:%d -> %s", protocol, listen, port, target)
newTarget := ProxyTarget{
Protocol: protocol,
Listen: listen,
Port: port,
Target: target,
cancel: make(chan struct{}),
done: make(chan struct{}),
switch proto {
case "tcp":
if pm.tcpTargets[listenIP] == nil {
pm.tcpTargets[listenIP] = make(map[int]string)
}
pm.tcpTargets[listenIP][port] = targetAddr
case "udp":
if pm.udpTargets[listenIP] == nil {
pm.udpTargets[listenIP] = make(map[int]string)
}
pm.udpTargets[listenIP][port] = targetAddr
default:
return fmt.Errorf("unsupported protocol: %s", proto)
}
pm.targets = append(pm.targets, newTarget)
if pm.running {
return pm.startTarget(proto, listenIP, port, targetAddr)
} else {
logger.Info("Not adding target because not running")
}
return nil
}
func (pm *ProxyManager) RemoveTarget(protocol, listen string, port int) error {
pm.Lock()
defer pm.Unlock()
func (pm *ProxyManager) RemoveTarget(proto, listenIP string, port int) error {
pm.mutex.Lock()
defer pm.mutex.Unlock()
protocol = strings.ToLower(protocol)
if protocol != "tcp" && protocol != "udp" {
return fmt.Errorf("unsupported protocol: %s", protocol)
}
for i, target := range pm.targets {
if target.Listen == listen &&
target.Port == port &&
strings.ToLower(target.Protocol) == protocol {
// Signal the serving goroutine to stop
select {
case <-target.cancel:
// Channel is already closed, no need to close it again
default:
close(target.cancel)
}
// Close the appropriate listener/connection based on protocol
target.Lock()
switch protocol {
case "tcp":
if target.listener != nil {
select {
case <-target.cancel:
// Listener was already closed by Stop()
default:
target.listener.Close()
}
}
case "udp":
if target.udpConn != nil {
select {
case <-target.cancel:
// Connection was already closed by Stop()
default:
target.udpConn.Close()
}
switch proto {
case "tcp":
if targets, ok := pm.tcpTargets[listenIP]; ok {
delete(targets, port)
// Remove and close the corresponding TCP listener
for i, listener := range pm.listeners {
if addr, ok := listener.Addr().(*net.TCPAddr); ok && addr.Port == port {
listener.Close()
time.Sleep(50 * time.Millisecond)
// Remove from slice
pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...)
break
}
}
target.Unlock()
// Wait for the target to fully stop
<-target.done
// Remove the target from the slice
pm.targets = append(pm.targets[:i], pm.targets[i+1:]...)
return nil
}
}
return fmt.Errorf("target not found for %s %s:%d", protocol, listen, port)
}
func (pm *ProxyManager) Start() error {
pm.RLock()
defer pm.RUnlock()
for i := range pm.targets {
target := &pm.targets[i]
target.Lock()
// If target is already running, skip it
if target.listener != nil || target.udpConn != nil {
target.Unlock()
continue
}
// Mark the target as starting by creating a nil listener/connection
// This prevents other goroutines from trying to start it
if strings.ToLower(target.Protocol) == "tcp" {
target.listener = nil
} else {
target.udpConn = nil
return fmt.Errorf("target not found: %s:%d", listenIP, port)
}
target.Unlock()
case "udp":
if targets, ok := pm.udpTargets[listenIP]; ok {
delete(targets, port)
// Remove and close the corresponding UDP connection
for i, conn := range pm.udpConns {
if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok && addr.Port == port {
conn.Close()
time.Sleep(50 * time.Millisecond)
// Remove from slice
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
break
}
}
} else {
return fmt.Errorf("target not found: %s:%d", listenIP, port)
}
default:
return fmt.Errorf("unsupported protocol: %s", proto)
}
return nil
}
switch strings.ToLower(target.Protocol) {
case "tcp":
go pm.serveTCP(target)
case "udp":
go pm.serveUDP(target)
default:
return fmt.Errorf("unsupported protocol: %s", target.Protocol)
// Start begins listening for all configured proxy targets
func (pm *ProxyManager) Start() error {
pm.mutex.Lock()
defer pm.mutex.Unlock()
if pm.running {
return nil
}
// Start TCP targets
for listenIP, targets := range pm.tcpTargets {
for port, targetAddr := range targets {
if err := pm.startTarget("tcp", listenIP, port, targetAddr); err != nil {
return fmt.Errorf("failed to start TCP target: %v", err)
}
}
}
// Start UDP targets
for listenIP, targets := range pm.udpTargets {
for port, targetAddr := range targets {
if err := pm.startTarget("udp", listenIP, port, targetAddr); err != nil {
return fmt.Errorf("failed to start UDP target: %v", err)
}
}
}
pm.running = true
return nil
}
func (pm *ProxyManager) Stop() error {
pm.Lock()
defer pm.Unlock()
pm.mutex.Lock()
defer pm.mutex.Unlock()
var wg sync.WaitGroup
for i := range pm.targets {
target := &pm.targets[i]
wg.Add(1)
go func(t *ProxyTarget) {
defer wg.Done()
close(t.cancel)
t.Lock()
if t.listener != nil {
t.listener.Close()
}
if t.udpConn != nil {
t.udpConn.Close()
}
t.Unlock()
// Wait for the target to fully stop
<-t.done
}(target)
if !pm.running {
return nil
}
wg.Wait()
// Set running to false first to signal handlers to stop
pm.running = false
// Close TCP listeners
for i := len(pm.listeners) - 1; i >= 0; i-- {
listener := pm.listeners[i]
if err := listener.Close(); err != nil {
logger.Error("Error closing TCP listener: %v", err)
}
// Remove from slice
pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...)
}
// Close UDP connections
for i := len(pm.udpConns) - 1; i >= 0; i-- {
conn := pm.udpConns[i]
if err := conn.Close(); err != nil {
logger.Error("Error closing UDP connection: %v", err)
}
// Remove from slice
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
}
// Clear the target maps
for k := range pm.tcpTargets {
delete(pm.tcpTargets, k)
}
for k := range pm.udpTargets {
delete(pm.udpTargets, k)
}
// Give active connections a chance to close gracefully
time.Sleep(100 * time.Millisecond)
return nil
}
func (pm *ProxyManager) serveTCP(target *ProxyTarget) {
defer close(target.done) // Signal that this target is fully stopped
func (pm *ProxyManager) startTarget(proto, listenIP string, port int, targetAddr string) error {
switch proto {
case "tcp":
listener, err := pm.tnet.ListenTCP(&net.TCPAddr{Port: port})
if err != nil {
return fmt.Errorf("failed to create TCP listener: %v", err)
}
listener, err := pm.tnet.ListenTCP(&net.TCPAddr{
IP: net.ParseIP(target.Listen),
Port: target.Port,
})
if err != nil {
logger.Info("Failed to start TCP listener for %s:%d: %v", target.Listen, target.Port, err)
return
pm.listeners = append(pm.listeners, listener)
go pm.handleTCPProxy(listener, targetAddr)
case "udp":
addr := &net.UDPAddr{Port: port}
conn, err := pm.tnet.ListenUDP(addr)
if err != nil {
return fmt.Errorf("failed to create UDP listener: %v", err)
}
pm.udpConns = append(pm.udpConns, conn)
go pm.handleUDPProxy(conn, targetAddr)
default:
return fmt.Errorf("unsupported protocol: %s", proto)
}
target.Lock()
target.listener = listener
target.Unlock()
logger.Info("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr)
defer listener.Close()
logger.Info("TCP proxy listening on %s", listener.Addr())
var activeConns sync.WaitGroup
acceptDone := make(chan struct{})
// Goroutine to handle shutdown signal
go func() {
<-target.cancel
close(acceptDone)
listener.Close()
}()
return nil
}
func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string) {
for {
conn, err := listener.Accept()
if err != nil {
select {
case <-target.cancel:
// Wait for active connections to finish
activeConns.Wait()
// Check if we're shutting down or the listener was closed
if !pm.running {
return
default:
logger.Info("Failed to accept TCP connection: %v", err)
// Don't return here, try to accept new connections
time.Sleep(time.Second)
continue
}
// Check for specific network errors that indicate the listener is closed
if ne, ok := err.(net.Error); ok && !ne.Temporary() {
logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr())
return
}
logger.Error("Error accepting TCP connection: %v", err)
// Don't hammer the CPU if we hit a temporary error
time.Sleep(100 * time.Millisecond)
continue
}
activeConns.Add(1)
go func() {
defer activeConns.Done()
pm.handleTCPConnection(conn, target.Target, acceptDone)
target, err := net.Dial("tcp", targetAddr)
if err != nil {
logger.Error("Error connecting to target: %v", err)
conn.Close()
return
}
// Create a WaitGroup to ensure both copy operations complete
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
io.Copy(target, conn)
target.Close()
}()
go func() {
defer wg.Done()
io.Copy(conn, target)
conn.Close()
}()
// Wait for both copies to complete
wg.Wait()
}()
}
}
func (pm *ProxyManager) handleTCPConnection(clientConn net.Conn, target string, done chan struct{}) {
defer clientConn.Close()
serverConn, err := net.Dial("tcp", target)
if err != nil {
logger.Info("Failed to connect to target %s: %v", target, err)
return
}
defer serverConn.Close()
var wg sync.WaitGroup
wg.Add(2)
// Client -> Server
go func() {
defer wg.Done()
select {
case <-done:
return
default:
io.Copy(serverConn, clientConn)
}
}()
// Server -> Client
go func() {
defer wg.Done()
select {
case <-done:
return
default:
io.Copy(clientConn, serverConn)
}
}()
wg.Wait()
}
func (pm *ProxyManager) serveUDP(target *ProxyTarget) {
defer close(target.done) // Signal that this target is fully stopped
addr := &net.UDPAddr{
IP: net.ParseIP(target.Listen),
Port: target.Port,
}
conn, err := pm.tnet.ListenUDP(addr)
if err != nil {
logger.Info("Failed to start UDP listener for %s:%d: %v", target.Listen, target.Port, err)
return
}
target.Lock()
target.udpConn = conn
target.Unlock()
defer conn.Close()
logger.Info("UDP proxy listening on %s", conn.LocalAddr())
buffer := make([]byte, 65535)
var activeConns sync.WaitGroup
func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
buffer := make([]byte, 65507) // Max UDP packet size
clientConns := make(map[string]*net.UDPConn)
var clientsMutex sync.RWMutex
for {
select {
case <-target.cancel:
activeConns.Wait() // Wait for all active UDP handlers to complete
return
default:
n, remoteAddr, err := conn.ReadFrom(buffer)
if err != nil {
select {
case <-target.cancel:
activeConns.Wait()
return
default:
logger.Info("Failed to read UDP packet: %v", err)
continue
}
n, remoteAddr, err := conn.ReadFrom(buffer)
if err != nil {
if !pm.running {
return
}
targetAddr, err := net.ResolveUDPAddr("udp", target.Target)
// Check for connection closed conditions
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
logger.Info("UDP connection closed, stopping proxy handler")
// Clean up existing client connections
clientsMutex.Lock()
for _, targetConn := range clientConns {
targetConn.Close()
}
clientConns = nil
clientsMutex.Unlock()
return
}
logger.Error("Error reading UDP packet: %v", err)
continue
}
clientKey := remoteAddr.String()
clientsMutex.RLock()
targetConn, exists := clientConns[clientKey]
clientsMutex.RUnlock()
if !exists {
targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr)
if err != nil {
logger.Info("Failed to resolve target address %s: %v", target.Target, err)
logger.Error("Error resolving target address: %v", err)
continue
}
activeConns.Add(1)
go func(data []byte, remote net.Addr) {
defer activeConns.Done()
targetConn, err := net.DialUDP("udp", nil, targetAddr)
if err != nil {
logger.Info("Failed to connect to target %s: %v", target.Target, err)
return
}
defer targetConn.Close()
targetConn, err = net.DialUDP("udp", nil, targetUDPAddr)
if err != nil {
logger.Error("Error connecting to target: %v", err)
continue
}
select {
case <-target.cancel:
return
default:
_, err = targetConn.Write(data)
clientsMutex.Lock()
clientConns[clientKey] = targetConn
clientsMutex.Unlock()
go func() {
buffer := make([]byte, 65507)
for {
n, _, err := targetConn.ReadFromUDP(buffer)
if err != nil {
logger.Info("Failed to write to target: %v", err)
logger.Error("Error reading from target: %v", err)
return
}
response := make([]byte, 65535)
n, err := targetConn.Read(response)
_, err = conn.WriteTo(buffer[:n], remoteAddr)
if err != nil {
logger.Info("Failed to read response from target: %v", err)
logger.Error("Error writing to client: %v", err)
return
}
_, err = conn.WriteTo(response[:n], remote)
if err != nil {
logger.Info("Failed to write response to client: %v", err)
}
}
}(buffer[:n], remoteAddr)
}()
}
_, err = targetConn.Write(buffer[:n])
if err != nil {
logger.Error("Error writing to target: %v", err)
targetConn.Close()
clientsMutex.Lock()
delete(clientConns, clientKey)
clientsMutex.Unlock()
}
}
}

View File

@@ -1,28 +0,0 @@
package proxy
import (
"log"
"net"
"sync"
"golang.zx2c4.com/wireguard/tun/netstack"
)
type ProxyTarget struct {
Protocol string
Listen string
Port int
Target string
cancel chan struct{} // Channel to signal shutdown
done chan struct{} // Channel to signal completion
listener net.Listener // For TCP
udpConn net.PacketConn // For UDP
sync.Mutex // Protect access to connection
}
type ProxyManager struct {
targets []ProxyTarget
tnet *netstack.Net
log *log.Logger
sync.RWMutex // Protect access to targets slice
}

View File

@@ -305,6 +305,10 @@ func (c *Client) establishConnection() error {
go c.readPump()
if c.onConnect != nil {
err := c.saveConfig()
if err != nil {
logger.Error("Failed to save config: %v", err)
}
if err := c.onConnect(); err != nil {
logger.Error("OnConnect callback failed: %v", err)
}