diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 6d271c8..1daf108 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -31,7 +31,7 @@ jobs: run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: 1.25 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46704a1..1b9637e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: '1.25' diff --git a/go.mod b/go.mod index b02d29a..e473a24 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/fosrl/gerbil go 1.25 require ( + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/vishvananda/netlink v1.3.1 - golang.org/x/crypto v0.36.0 + golang.org/x/crypto v0.42.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 ) @@ -14,10 +15,9 @@ require ( github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/vishvananda/netns v0.0.5 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sys v0.36.0 // indirect golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect ) diff --git a/go.sum b/go.sum index a089b8e..2ce1c9f 100644 --- a/go.sum +++ b/go.sum @@ -16,16 +16,16 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= diff --git a/relay/relay.go b/relay/relay.go index 322276a..e74ed87 100644 --- a/relay/relay.go +++ b/relay/relay.go @@ -64,6 +64,17 @@ type WireGuardSession struct { LastSeen time.Time } +// Type for tracking bidirectional communication patterns to rebuild sessions +type CommunicationPattern struct { + FromClient *net.UDPAddr // The client address + ToDestination *net.UDPAddr // The destination address + ClientIndex uint32 // The receiver index seen from client + DestIndex uint32 // The receiver index seen from destination + LastFromClient time.Time // Last packet from client to destination + LastFromDest time.Time // Last packet from destination to client + PacketCount int // Number of packets observed +} + type InitialMappings struct { Mappings map[string]ProxyMapping `json:"mappings"` // key is "ip:port" } @@ -105,6 +116,9 @@ type UDPProxyServer struct { // Session tracking for WireGuard peers // Key format: "senderIndex:receiverIndex" wgSessions sync.Map + // Communication pattern tracking for rebuilding sessions + // Key format: "clientIP:clientPort-destIP:destPort" + commPatterns sync.Map // ReachableAt is the URL where this server can be reached ReachableAt string } @@ -156,6 +170,9 @@ func (s *UDPProxyServer) Start() error { // Start the proxy mapping cleanup routine go s.cleanupIdleProxyMappings() + // Start the communication pattern cleanup routine + go s.cleanupIdleCommunicationPatterns() + return nil } @@ -445,6 +462,9 @@ func (s *UDPProxyServer) handleWireGuardPacket(packet []byte, remoteAddr *net.UD return } + // Track communication pattern for session rebuilding + s.trackCommunicationPattern(remoteAddr, destAddr, receiverIndex, true) + _, err = conn.Write(packet) if err != nil { logger.Debug("Failed to forward transport data: %v", err) @@ -465,6 +485,9 @@ func (s *UDPProxyServer) handleWireGuardPacket(packet []byte, remoteAddr *net.UD continue } + // Track communication pattern for session rebuilding + s.trackCommunicationPattern(remoteAddr, destAddr, receiverIndex, true) + _, err = conn.Write(packet) if err != nil { logger.Debug("Failed to forward transport data: %v", err) @@ -548,6 +571,9 @@ func (s *UDPProxyServer) handleResponses(conn *net.UDPConn, destAddr *net.UDPAdd LastSeen: time.Now(), }) logger.Debug("Stored session mapping: %s -> %s", sessionKey, destAddr.String()) + } else if ok && buffer[0] == WireGuardMessageTypeTransportData { + // Track communication pattern for session rebuilding (reverse direction) + s.trackCommunicationPattern(destAddr, remoteAddr, receiverIndex, false) } } @@ -823,3 +849,117 @@ func (s *UDPProxyServer) UpdateDestinationInMappings(oldDest, newDest PeerDestin return updatedCount } + +// trackCommunicationPattern tracks bidirectional communication patterns to rebuild sessions +func (s *UDPProxyServer) trackCommunicationPattern(fromAddr, toAddr *net.UDPAddr, receiverIndex uint32, fromClient bool) { + var clientAddr, destAddr *net.UDPAddr + var clientIndex, destIndex uint32 + + if fromClient { + clientAddr = fromAddr + destAddr = toAddr + clientIndex = receiverIndex + destIndex = 0 // We don't know the destination index yet + } else { + clientAddr = toAddr + destAddr = fromAddr + clientIndex = 0 // We don't know the client index yet + destIndex = receiverIndex + } + + patternKey := fmt.Sprintf("%s-%s", clientAddr.String(), destAddr.String()) + now := time.Now() + + if existingPattern, ok := s.commPatterns.Load(patternKey); ok { + pattern := existingPattern.(*CommunicationPattern) + + // Update the pattern + if fromClient { + pattern.LastFromClient = now + if pattern.ClientIndex == 0 { + pattern.ClientIndex = clientIndex + } + } else { + pattern.LastFromDest = now + if pattern.DestIndex == 0 { + pattern.DestIndex = destIndex + } + } + + pattern.PacketCount++ + s.commPatterns.Store(patternKey, pattern) + + // Check if we have bidirectional communication and can rebuild a session + s.tryRebuildSession(pattern) + } else { + // Create new pattern + pattern := &CommunicationPattern{ + FromClient: clientAddr, + ToDestination: destAddr, + ClientIndex: clientIndex, + DestIndex: destIndex, + PacketCount: 1, + } + + if fromClient { + pattern.LastFromClient = now + } else { + pattern.LastFromDest = now + } + + s.commPatterns.Store(patternKey, pattern) + } +} + +// tryRebuildSession attempts to rebuild a WireGuard session from communication patterns +func (s *UDPProxyServer) tryRebuildSession(pattern *CommunicationPattern) { + // Check if we have bidirectional communication within a reasonable time window + timeDiff := pattern.LastFromClient.Sub(pattern.LastFromDest) + if timeDiff < 0 { + timeDiff = -timeDiff + } + + // Only rebuild if we have recent bidirectional communication and both indices + if timeDiff < 30*time.Second && pattern.ClientIndex != 0 && pattern.DestIndex != 0 && pattern.PacketCount >= 4 { + // Create session mapping: client's index maps to destination + sessionKey := fmt.Sprintf("%d:%d", pattern.DestIndex, pattern.ClientIndex) + + // Check if we already have this session + if _, exists := s.wgSessions.Load(sessionKey); !exists { + session := &WireGuardSession{ + ReceiverIndex: pattern.DestIndex, + SenderIndex: pattern.ClientIndex, + DestAddr: pattern.ToDestination, + LastSeen: time.Now(), + } + + s.wgSessions.Store(sessionKey, session) + logger.Info("Rebuilt WireGuard session from communication pattern: %s -> %s (packets: %d)", + sessionKey, pattern.ToDestination.String(), pattern.PacketCount) + } + } +} + +// cleanupIdleCommunicationPatterns periodically removes idle communication patterns +func (s *UDPProxyServer) cleanupIdleCommunicationPatterns() { + ticker := time.NewTicker(10 * time.Minute) + for range ticker.C { + now := time.Now() + s.commPatterns.Range(func(key, value interface{}) bool { + pattern := value.(*CommunicationPattern) + + // Get the most recent activity + lastActivity := pattern.LastFromClient + if pattern.LastFromDest.After(lastActivity) { + lastActivity = pattern.LastFromDest + } + + // Remove patterns that haven't had activity in 20 minutes + if now.Sub(lastActivity) > 20*time.Minute { + s.commPatterns.Delete(key) + logger.Debug("Removed idle communication pattern: %s", key) + } + return true + }) + } +}