Basic holepunch working

This commit is contained in:
Owen
2025-02-23 00:59:51 -05:00
parent 8795c57b2e
commit f6429b6eee
5 changed files with 295 additions and 106 deletions

1
go.mod
View File

@@ -16,6 +16,7 @@ require (
require (
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect

14
go.sum
View File

@@ -2,6 +2,8 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -18,22 +20,34 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=

202
network/network.go Normal file
View File

@@ -0,0 +1,202 @@
package network
import (
"encoding/binary"
"encoding/json"
"fmt"
"log"
"net"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/vishvananda/netlink"
"golang.org/x/net/bpf"
"golang.org/x/net/ipv4"
)
const (
udpProtocol = 17
// EmptyUDPSize is the size of an empty UDP packet
EmptyUDPSize = 28
timeout = time.Second * 10
)
// Server stores data relating to the server
type Server struct {
Hostname string
Addr *net.IPAddr
Port uint16
}
// PeerNet stores data about a peer's endpoint
type PeerNet struct {
Resolved bool
IP net.IP
Port uint16
NewtID string
}
// GetClientIP gets source ip address that will be used when sending data to dstIP
func GetClientIP(dstIP net.IP) net.IP {
routes, err := netlink.RouteGet(dstIP)
if err != nil {
log.Fatalln("Error getting route:", err)
}
return routes[0].Src
}
// HostToAddr resolves a hostname, whether DNS or IP to a valid net.IPAddr
func HostToAddr(hostStr string) *net.IPAddr {
remoteAddrs, err := net.LookupHost(hostStr)
if err != nil {
log.Fatalln("Error parsing remote address:", err)
}
for _, addrStr := range remoteAddrs {
if remoteAddr, err := net.ResolveIPAddr("ip4", addrStr); err == nil {
return remoteAddr
}
}
return nil
}
// SetupRawConn creates an ipv4 and udp only RawConn and applies packet filtering
func SetupRawConn(server *Server, client *PeerNet) *ipv4.RawConn {
packetConn, err := net.ListenPacket("ip4:udp", client.IP.String())
if err != nil {
log.Fatalln("Error creating packetConn:", err)
}
rawConn, err := ipv4.NewRawConn(packetConn)
if err != nil {
log.Fatalln("Error creating rawConn:", err)
}
ApplyBPF(rawConn, server, client)
return rawConn
}
// ApplyBPF constructs a BPF program and applies it to the RawConn
func ApplyBPF(rawConn *ipv4.RawConn, server *Server, client *PeerNet) {
const ipv4HeaderLen = 20
const srcIPOffset = 12
const srcPortOffset = ipv4HeaderLen + 0
const dstPortOffset = ipv4HeaderLen + 2
ipArr := []byte(server.Addr.IP.To4())
ipInt := uint32(ipArr[0])<<(3*8) + uint32(ipArr[1])<<(2*8) + uint32(ipArr[2])<<8 + uint32(ipArr[3])
bpfRaw, err := bpf.Assemble([]bpf.Instruction{
bpf.LoadAbsolute{Off: srcIPOffset, Size: 4},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: ipInt, SkipFalse: 5, SkipTrue: 0},
bpf.LoadAbsolute{Off: srcPortOffset, Size: 2},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(server.Port), SkipFalse: 3, SkipTrue: 0},
bpf.LoadAbsolute{Off: dstPortOffset, Size: 2},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(client.Port), SkipFalse: 1, SkipTrue: 0},
bpf.RetConstant{Val: 1<<(8*4) - 1},
bpf.RetConstant{Val: 0},
})
if err != nil {
log.Fatalln("Error assembling BPF:", err)
}
err = rawConn.SetBPF(bpfRaw)
if err != nil {
log.Fatalln("Error setting BPF:", err)
}
}
// MakePacket constructs a request packet to send to the server
func MakePacket(payload []byte, server *Server, client *PeerNet) []byte {
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
ipHeader := layers.IPv4{
SrcIP: client.IP,
DstIP: server.Addr.IP,
Version: 4,
TTL: 64,
Protocol: layers.IPProtocolUDP,
}
udpHeader := layers.UDP{
SrcPort: layers.UDPPort(client.Port),
DstPort: layers.UDPPort(server.Port),
}
payloadLayer := gopacket.Payload(payload)
udpHeader.SetNetworkLayerForChecksum(&ipHeader)
gopacket.SerializeLayers(buf, opts, &ipHeader, &udpHeader, &payloadLayer)
return buf.Bytes()
}
// SendPacket sends packet to the Server
func SendPacket(packet []byte, conn *ipv4.RawConn, server *Server, client *PeerNet) error {
fullPacket := MakePacket(packet, server, client)
_, err := conn.WriteToIP(fullPacket, server.Addr)
return err
}
// SendDataPacket sends a JSON payload to the Server
func SendDataPacket(data interface{}, conn *ipv4.RawConn, server *Server, client *PeerNet) error {
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal payload: %v", err)
}
return SendPacket(jsonData, conn, server, client)
}
// RecvPacket receives a UDP packet from server
func RecvPacket(conn *ipv4.RawConn, server *Server, client *PeerNet) ([]byte, int, error) {
err := conn.SetReadDeadline(time.Now().Add(timeout))
if err != nil {
return nil, 0, err
}
response := make([]byte, 4096)
n, err := conn.Read(response)
if err != nil {
return nil, n, err
}
return response, n, nil
}
// RecvDataPacket receives and unmarshals a JSON packet from server
func RecvDataPacket(conn *ipv4.RawConn, server *Server, client *PeerNet) ([]byte, error) {
response, n, err := RecvPacket(conn, server, client)
if err != nil {
return nil, err
}
// Extract payload from UDP packet
payload := response[EmptyUDPSize:n]
return payload, nil
}
// ParseResponse takes a response packet and parses it into an IP and port
func ParseResponse(response []byte) (net.IP, uint16) {
ip := net.IP(response[:4])
port := binary.BigEndian.Uint16(response[4:6])
return ip, port
}
func parseForBPF(response []byte) (srcIP net.IP, srcPort uint16, dstPort uint16) {
srcIP = net.IP(response[12:16])
srcPort = binary.BigEndian.Uint16(response[20:22])
dstPort = binary.BigEndian.Uint16(response[22:24])
return
}

25
nohup.out Normal file
View File

@@ -0,0 +1,25 @@
INFO: 2025/02/22 23:25:47 Requesting WireGuard configuration from remote server
INFO: 2025/02/22 23:25:47 Sent registration message
INFO: 2025/02/22 23:25:47 Received message: {newt/wg/receive-config map[ipAddress:100.90.128.1/24 listenPort:51822 peers:[]]}
INFO: 2025/02/22 23:25:47 Created WireGuard interface wg1
INFO: 2025/02/22 23:25:47 Assigning IP address 100.90.128.1/24 to interface wg1
INFO: 2025/02/22 23:25:47 WireGuard interface wg1 created and configured
INFO: 2025/02/22 23:25:47 Received registration message
INFO: 2025/02/22 23:25:47 Received: {Type:newt/wg/connect Data:map[endpoint:pangolin.fosrl.io:51820 publicKey:tng9Z/BN32flFjqwwT1yAxN/twFkmgbZA+D9N+YqdjM= serverIP:100.89.128.1 targets:map[tcp:[] udp:[]] tunnelIP:100.89.128.4]}
INFO: 2025/02/22 23:25:47 WireGuard device created. Lets ping the server now...
INFO: 2025/02/22 23:25:47 Ping attempt 1 of 5
INFO: 2025/02/22 23:25:47 Pinging 100.89.128.1
INFO: 2025/02/22 23:25:47 Ping latency: 9.00105ms
INFO: 2025/02/22 23:25:47 Starting ping check
INFO: 2025/02/22 23:26:48 Peer P9pacnRfUSfvDibaQTdTk59q27eRpgtbMMmMpkNwKl0= removed successfully
INFO: 2025/02/22 23:26:48 Peer NMrcorGgTTi4tAUZ1lLru0qISNrt9D9JdsFGyDYlcSQ= added successfully
INFO: 2025/02/22 23:28:58 Peer NMrcorGgTTi4tAUZ1lLru0qISNrt9D9JdsFGyDYlcSQ= removed successfully
INFO: 2025/02/22 23:28:58 Peer n8ZKTG8vsROL/OiqHYJELU/Rg9XDifz0YjE/lQsL0m0= added successfully
INFO: 2025/02/22 23:33:59 Peer n8ZKTG8vsROL/OiqHYJELU/Rg9XDifz0YjE/lQsL0m0= removed successfully
INFO: 2025/02/22 23:33:59 Peer /i8YTgrLkZh08HKXLXqNFQJsyg1E8I2ELXqF0zuP9D8= added successfully
INFO: 2025/02/22 23:34:06 Peer /i8YTgrLkZh08HKXLXqNFQJsyg1E8I2ELXqF0zuP9D8= removed successfully
INFO: 2025/02/22 23:34:06 Peer 50+RB00sDoSG+KAKzl/baaqPkKGOe7upX7uqRCKqsRo= added successfully
INFO: 2025/02/22 23:35:07 Peer 50+RB00sDoSG+KAKzl/baaqPkKGOe7upX7uqRCKqsRo= removed successfully
INFO: 2025/02/22 23:35:07 Peer Aa2Y2NEmc+SITlT89+fsOeqDkXJVu9RBY14+77TXa3w= added successfully
INFO: 2025/02/23 00:55:55 Peer Aa2Y2NEmc+SITlT89+fsOeqDkXJVu9RBY14+77TXa3w= removed successfully
INFO: 2025/02/23 00:55:55 Peer 2AXNjMQzT7GGvdbIG6MJVFpO3FIzQ+qCqZkdSnBA3DE= added successfully

159
wg/wg.go
View File

@@ -5,10 +5,13 @@ import (
"fmt"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/network"
"github.com/fosrl/newt/websocket"
"github.com/vishvananda/netlink"
"golang.org/x/exp/rand"
@@ -214,7 +217,7 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) {
s.config = config
// stop the holepunch
close(s.stopHolepunch)
// close(s.stopHolepunch)
// Ensure the WireGuard interface and peers are configured
if err := s.ensureWireguardInterface(config); err != nil {
@@ -373,94 +376,6 @@ func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error {
return nil
}
// func (s *WireGuardService) ensureMSSClamping() error {
// // Calculate MSS value (MTU - 40 for IPv4 header (20) and TCP header (20))
// mssValue := mtuInt - 40
// // Rules to be managed - just the chains, we'll construct the full command separately
// chains := []string{"INPUT", "OUTPUT", "FORWARD"}
// // First, try to delete any existing rules
// for _, chain := range chains {
// deleteCmd := exec.Command("/usr/sbin/iptables",
// "-t", "mangle",
// "-D", chain,
// "-p", "tcp",
// "--tcp-flags", "SYN,RST", "SYN",
// "-j", "TCPMSS",
// "--set-mss", fmt.Sprintf("%d", mssValue))
// logger.Info("Attempting to delete existing MSS clamping rule for chain %s", chain)
// // Try deletion multiple times to handle multiple existing rules
// for i := 0; i < 3; i++ {
// out, err := deleteCmd.CombinedOutput()
// if err != nil {
// // Convert exit status 1 to string for better logging
// if exitErr, ok := err.(*exec.ExitError); ok {
// logger.Debug("Deletion stopped for chain %s: %v (output: %s)",
// chain, exitErr.String(), string(out))
// }
// break // No more rules to delete
// }
// logger.Info("Deleted MSS clamping rule for chain %s (attempt %d)", chain, i+1)
// }
// }
// // Then add the new rules
// var errors []error
// for _, chain := range chains {
// addCmd := exec.Command("/usr/sbin/iptables",
// "-t", "mangle",
// "-A", chain,
// "-p", "tcp",
// "--tcp-flags", "SYN,RST", "SYN",
// "-j", "TCPMSS",
// "--set-mss", fmt.Sprintf("%d", mssValue))
// logger.Info("Adding MSS clamping rule for chain %s", chain)
// if out, err := addCmd.CombinedOutput(); err != nil {
// errMsg := fmt.Sprintf("Failed to add MSS clamping rule for chain %s: %v (output: %s)",
// chain, err, string(out))
// logger.Error(errMsg)
// errors = append(errors, fmt.Errorf(errMsg))
// continue
// }
// // Verify the rule was added
// checkCmd := exec.Command("/usr/sbin/iptables",
// "-t", "mangle",
// "-C", chain,
// "-p", "tcp",
// "--tcp-flags", "SYN,RST", "SYN",
// "-j", "TCPMSS",
// "--set-mss", fmt.Sprintf("%d", mssValue))
// if out, err := checkCmd.CombinedOutput(); err != nil {
// errMsg := fmt.Sprintf("Rule verification failed for chain %s: %v (output: %s)",
// chain, err, string(out))
// logger.Error(errMsg)
// errors = append(errors, fmt.Errorf(errMsg))
// continue
// }
// logger.Info("Successfully added and verified MSS clamping rule for chain %s", chain)
// }
// // If we encountered any errors, return them combined
// if len(errors) > 0 {
// var errMsgs []string
// for _, err := range errors {
// errMsgs = append(errMsgs, err.Error())
// }
// return fmt.Errorf("MSS clamping setup encountered errors:\n%s",
// strings.Join(errMsgs, "\n"))
// }
// return nil
// }
func (s *WireGuardService) handleAddPeer(msg websocket.WSMessage) {
var peer Peer
@@ -681,40 +596,72 @@ func (s *WireGuardService) reportPeerBandwidth() error {
}
func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error {
// Bind to specific local port
localAddr := &net.UDPAddr{
Port: int(s.port),
IP: net.IPv4zero,
// Parse server address
serverSplit := strings.Split(serverAddr, ":")
if len(serverSplit) < 2 {
return fmt.Errorf("invalid server address format, expected hostname:port")
}
remoteAddr, err := net.ResolveUDPAddr("udp", serverAddr)
serverHostname := serverSplit[0]
serverPort, err := strconv.ParseUint(serverSplit[1], 10, 16)
if err != nil {
return fmt.Errorf("failed to resolve UDP address: %v", err)
return fmt.Errorf("failed to parse server port: %v", err)
}
conn, err := net.ListenUDP("udp", localAddr)
if err != nil {
return fmt.Errorf("failed to bind UDP socket: %v", err)
// Resolve server hostname to IP
serverIPAddr := network.HostToAddr(serverHostname)
if serverIPAddr == nil {
return fmt.Errorf("failed to resolve server hostname")
}
defer conn.Close()
// Get client IP based on route to server
clientIP := network.GetClientIP(serverIPAddr.IP)
// Create server and client configs
server := &network.Server{
Hostname: serverHostname,
Addr: serverIPAddr,
Port: uint16(serverPort),
}
client := &network.PeerNet{
IP: clientIP,
Port: s.port,
NewtID: s.newtId,
}
// Setup raw connection with BPF filtering
rawConn := network.SetupRawConn(server, client)
defer rawConn.Close()
// Create JSON payload
payload := struct {
NewtID string `json:"newtId"`
}{
NewtID: s.newtId,
}
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %v", err)
}
_, err = conn.WriteToUDP(data, remoteAddr)
// Send the packet using the raw connection
err = network.SendDataPacket(payload, rawConn, server, client)
if err != nil {
return fmt.Errorf("failed to send UDP packet: %v", err)
}
logger.Info("Sent UDP hole punch to %s", serverAddr)
// logger.Info("Sent UDP hole punch to %s", serverAddr)
// // Wait for response if needed
// response, err := network.RecvDataPacket(rawConn, server, client)
// if err != nil {
// if err, ok := err.(net.Error); ok && err.Timeout() {
// return fmt.Errorf("connection to %s timed out", serverAddr)
// }
// return fmt.Errorf("error receiving response: %v", err)
// }
// // Process response if needed
// if len(response) > 0 {
// logger.Info("Received response from server")
// }
return nil
}