diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c32a112 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +# Description: Makefile for building the project + +BINARY_NAME=newt + +ll: build + +build: + go build -o bin/$(BINARY_NAME) -v \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..421d3be --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module newt + +go 1.23.1 + +toolchain go1.23.2 + +require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 + +require ( + github.com/google/btree v1.1.2 // indirect + golang.org/x/crypto v0.28.0 // 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 + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3bf4a5d --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +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= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= diff --git a/main.go b/main.go new file mode 100644 index 0000000..98be895 --- /dev/null +++ b/main.go @@ -0,0 +1,323 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "flag" + "fmt" + "io" + "log" + "math/rand" + "net" + "net/netip" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun/netstack" +) + +type ProxyTarget struct { + Protocol string + Listen string + Targets []string +} + +type ProxyManager struct { + targets []ProxyTarget + tnet *netstack.Net +} + +func NewProxyManager(tnet *netstack.Net) *ProxyManager { + return &ProxyManager{ + tnet: tnet, + } +} + +func (pm *ProxyManager) AddTarget(protocol, listen string, targets []string) { + pm.targets = append(pm.targets, ProxyTarget{ + Protocol: protocol, + Listen: listen, + Targets: targets, + }) +} + +func (pm *ProxyManager) Start() error { + for _, target := range pm.targets { + 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) + } + } + return nil +} + +func (pm *ProxyManager) serveTCP(target ProxyTarget) { + listener, err := pm.tnet.ListenTCP(&net.TCPAddr{ + IP: net.ParseIP(target.Listen), + Port: 0, + }) + if err != nil { + log.Printf("Failed to start TCP listener for %s: %v", target.Listen, err) + return + } + defer listener.Close() + + log.Printf("TCP proxy listening on %s", listener.Addr()) + + for { + conn, err := listener.Accept() + if err != nil { + log.Printf("Failed to accept TCP connection: %v", err) + continue + } + + go pm.handleTCPConnection(conn, target.Targets) + } +} + +func (pm *ProxyManager) handleTCPConnection(clientConn net.Conn, targets []string) { + defer clientConn.Close() + + // Round-robin through targets + targetIndex := 0 + target := targets[targetIndex] + targetIndex = (targetIndex + 1) % len(targets) + + serverConn, err := net.Dial("tcp", target) + if err != nil { + log.Printf("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() + io.Copy(serverConn, clientConn) + }() + + // Server -> Client + go func() { + defer wg.Done() + io.Copy(clientConn, serverConn) + }() + + wg.Wait() +} + +func (pm *ProxyManager) serveUDP(target ProxyTarget) { + addr := &net.UDPAddr{ + IP: net.ParseIP(target.Listen), + Port: 0, + } + + conn, err := pm.tnet.ListenUDP(addr) + if err != nil { + log.Printf("Failed to start UDP listener for %s: %v", target.Listen, err) + return + } + defer conn.Close() + + log.Printf("UDP proxy listening on %s", conn.LocalAddr()) + + buffer := make([]byte, 65535) + targetIndex := 0 + + for { + // Read from the UDP connection + n, remoteAddr, err := conn.ReadFrom(buffer) + if err != nil { + log.Printf("Failed to read UDP packet: %v", err) + continue + } + + t := target.Targets[targetIndex] + targetIndex = (targetIndex + 1) % len(target.Targets) + + targetAddr, err := net.ResolveUDPAddr("udp", t) + if err != nil { + log.Printf("Failed to resolve target address %s: %v", target, err) + continue + } + + go func(data []byte, remote net.Addr) { + targetConn, err := net.DialUDP("udp", nil, targetAddr) + if err != nil { + log.Printf("Failed to connect to target %s: %v", target, err) + return + } + defer targetConn.Close() + + _, err = targetConn.Write(data) + if err != nil { + log.Printf("Failed to write to target: %v", err) + return + } + + response := make([]byte, 65535) + n, err := targetConn.Read(response) + if err != nil { + log.Printf("Failed to read response from target: %v", err) + return + } + + _, err = conn.WriteTo(response[:n], remote) + if err != nil { + log.Printf("Failed to write response to client: %v", err) + } + }(buffer[:n], remoteAddr) + } +} + +func fixKey(key string) string { + // Remove any whitespace + key = strings.TrimSpace(key) + + // Decode from base64 + decoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + log.Fatal("Error decoding base64:", err) + } + + // Convert to hex + return hex.EncodeToString(decoded) +} + +func ping(tnet *netstack.Net, dst string) { + socket, err := tnet.Dial("ping4", dst) + if err != nil { + log.Panic(err) + } + requestPing := icmp.Echo{ + Seq: rand.Intn(1 << 16), + Data: []byte("gopher burrow"), + } + icmpBytes, _ := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) + socket.SetReadDeadline(time.Now().Add(time.Second * 10)) + start := time.Now() + _, err = socket.Write(icmpBytes) + if err != nil { + log.Panic(err) + } + n, err := socket.Read(icmpBytes[:]) + if err != nil { + log.Panic(err) + } + replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) + if err != nil { + log.Panic(err) + } + replyPing, ok := replyPacket.Body.(*icmp.Echo) + if !ok { + log.Panicf("invalid reply type: %v", replyPacket) + } + if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq { + log.Panicf("invalid ping reply: %v", replyPing) + } + log.Printf("Ping latency: %v", time.Since(start)) +} + +func main() { + var ( + tunnelIP string + privateKey string + publicKey string + endpoint string + tcpTargets string + udpTargets string + listenIP string + serverIP string + dns string + ) + + flag.StringVar(&tunnelIP, "tunnel-ip", "", "Tunnel IP address") + flag.StringVar(&privateKey, "private-key", "", "WireGuard private key") + flag.StringVar(&publicKey, "public-key", "", "WireGuard public key") + flag.StringVar(&endpoint, "endpoint", "", "WireGuard endpoint (host:port)") + flag.StringVar(&tcpTargets, "tcp-targets", "", "Comma-separated list of TCP targets (host:port)") + flag.StringVar(&udpTargets, "udp-targets", "", "Comma-separated list of UDP targets (host:port)") + flag.StringVar(&listenIP, "listen-ip", "", "IP to listen for incoming connections") + flag.StringVar(&serverIP, "server-ip", "", "IP to filter and ping on the server side. Inside tunnel...") + flag.StringVar(&dns, "dns", "8.8.8.8", "DNS server to use") + + flag.Parse() + + // Create TUN device and network stack + tun, tnet, err := netstack.CreateNetTUN( + []netip.Addr{netip.MustParseAddr(tunnelIP)}, + []netip.Addr{netip.MustParseAddr(dns)}, + 1420) + if err != nil { + log.Panic(err) + } + + // Create WireGuard device + dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelVerbose, "")) + + // Configure WireGuard + config := fmt.Sprintf(`private_key=%s +public_key=%s +allowed_ip=%s/32 +endpoint=%s +persistent_keepalive_interval=5 +`, fixKey(privateKey), fixKey(publicKey), serverIP, endpoint) + + err = dev.IpcSet(config) + if err != nil { + log.Panic(err) + } + + // Bring up the device + err = dev.Up() + if err != nil { + log.Panic(err) + } + + // Ping to bring the tunnel up on the server side quickly + ping(tnet, serverIP) + + // Create proxy manager + pm := NewProxyManager(tnet) + + // Add TCP targets + if tcpTargets != "" { + targets := strings.Split(tcpTargets, ",") + pm.AddTarget("tcp", listenIP, targets) + } + + // Add UDP targets + if udpTargets != "" { + targets := strings.Split(udpTargets, ",") + pm.AddTarget("udp", listenIP, targets) + } + + // Start proxies + err = pm.Start() + if err != nil { + log.Panic(err) + } + + // Wait for interrupt signal + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh + + // Cleanup + dev.Close() +} diff --git a/newt b/newt new file mode 100755 index 0000000..0ce5c93 Binary files /dev/null and b/newt differ diff --git a/test/cleanup.sh b/test/cleanup.sh new file mode 100644 index 0000000..472e9a7 --- /dev/null +++ b/test/cleanup.sh @@ -0,0 +1 @@ +ip link del dev wg0 \ No newline at end of file diff --git a/test/key b/test/key new file mode 100644 index 0000000..585033a --- /dev/null +++ b/test/key @@ -0,0 +1 @@ +eN6oRymkBFTCLOwlpEgB9zkCJpl0zb6NL5TRogXzNlk= \ No newline at end of file diff --git a/test/newt_client.sh b/test/newt_client.sh new file mode 100644 index 0000000..46d9419 --- /dev/null +++ b/test/newt_client.sh @@ -0,0 +1,9 @@ +./newt \ + --tunnel-ip=192.168.4.28 \ + "--private-key=kAexrEV1OHlMYQU3BZatZxNfKGAbzo+ATspAdtOcRks=" \ + "--public-key=Kn4eD0kvcTwjO//zqH/CtNVkMNdMiUkbqFxysEym2D8=" \ + --endpoint=192.168.1.16:51820 \ + --tcp-targets=127.0.0.1:8080 \ + --udp-targets=127.0.0.1:53 \ + --listen-ip=192.168.4.28 \ + --server-ip=192.168.4.1 \ No newline at end of file diff --git a/test/wg_server.sh b/test/wg_server.sh new file mode 100644 index 0000000..279846c --- /dev/null +++ b/test/wg_server.sh @@ -0,0 +1,6 @@ +ip link add dev wg0 type wireguard +ip addr add 192.168.4.1/24 dev wg0 +ip link set up dev wg0 +wg set wg0 private-key ./key +wg set wg0 listen-port 51820 +wg set wg0 peer 3QfirSdDVihYCAz66t6DTAtFtsh+9WVVu7ItlL750hI= allowed-ips 192.168.4.28 \ No newline at end of file