From 92e0a06dee2854fc8d8652c7a252f5c46d3a9196 Mon Sep 17 00:00:00 2001
From: Siyuan Miao <i@xswan.net>
Date: Mon, 10 Feb 2025 17:09:22 +0100
Subject: [PATCH] feat: add ICE servers and local IP address returned by the
 API to fix connectivity issues behind NAT

---
 cloud.go  |  7 ++++---
 config.go | 26 ++++++++++++++++----------
 web.go    |  8 +++++---
 webrtc.go | 21 ++++++++++++++++++---
 4 files changed, 43 insertions(+), 19 deletions(-)

diff --git a/cloud.go b/cloud.go
index db47727..638ceb5 100644
--- a/cloud.go
+++ b/cloud.go
@@ -7,13 +7,14 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-	"github.com/coder/websocket/wsjson"
 	"time"
 
+	"github.com/coder/websocket/wsjson"
+
 	"github.com/coreos/go-oidc/v3/oidc"
 
-	"github.com/gin-gonic/gin"
 	"github.com/coder/websocket"
+	"github.com/gin-gonic/gin"
 )
 
 type CloudRegisterRequest struct {
@@ -187,7 +188,7 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
 		return fmt.Errorf("google identity mismatch")
 	}
 
-	session, err := newSession()
+	session, err := newSession(req.ICEServers, req.IP)
 	if err != nil {
 		_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
 		return err
diff --git a/config.go b/config.go
index 1636434..30789ee 100644
--- a/config.go
+++ b/config.go
@@ -12,16 +12,17 @@ type WakeOnLanDevice struct {
 }
 
 type Config struct {
-	CloudURL          string            `json:"cloud_url"`
-	CloudToken        string            `json:"cloud_token"`
-	GoogleIdentity    string            `json:"google_identity"`
-	JigglerEnabled    bool              `json:"jiggler_enabled"`
-	AutoUpdateEnabled bool              `json:"auto_update_enabled"`
-	IncludePreRelease bool              `json:"include_pre_release"`
-	HashedPassword    string            `json:"hashed_password"`
-	LocalAuthToken    string            `json:"local_auth_token"`
-	LocalAuthMode     string            `json:"localAuthMode"` //TODO: fix it with migration
-	WakeOnLanDevices  []WakeOnLanDevice `json:"wake_on_lan_devices"`
+	CloudURL           string            `json:"cloud_url"`
+	CloudToken         string            `json:"cloud_token"`
+	GoogleIdentity     string            `json:"google_identity"`
+	JigglerEnabled     bool              `json:"jiggler_enabled"`
+	AutoUpdateEnabled  bool              `json:"auto_update_enabled"`
+	IncludePreRelease  bool              `json:"include_pre_release"`
+	HashedPassword     string            `json:"hashed_password"`
+	LocalAuthToken     string            `json:"local_auth_token"`
+	LocalAuthMode      string            `json:"localAuthMode"` //TODO: fix it with migration
+	WakeOnLanDevices   []WakeOnLanDevice `json:"wake_on_lan_devices"`
+	FallbackICEServers []string          `json:"fallback_ice_servers"`
 }
 
 const configPath = "/userdata/kvm_config.json"
@@ -29,6 +30,11 @@ const configPath = "/userdata/kvm_config.json"
 var defaultConfig = &Config{
 	CloudURL:          "https://api.jetkvm.com",
 	AutoUpdateEnabled: true, // Set a default value
+	FallbackICEServers: []string{
+		"stun:stun.cloudflare.com:3478",
+		"stun:stun.cloudflare.com:53",
+		"stun:stun.l.google.com:19302",
+	},
 }
 
 var config *Config
diff --git a/web.go b/web.go
index 64f8de7..6436b46 100644
--- a/web.go
+++ b/web.go
@@ -17,8 +17,10 @@ import (
 var staticFiles embed.FS
 
 type WebRTCSessionRequest struct {
-	Sd         string `json:"sd"`
-	OidcGoogle string `json:"OidcGoogle,omitempty"`
+	Sd         string   `json:"sd"`
+	OidcGoogle string   `json:"OidcGoogle,omitempty"`
+	IP         string   `json:"ip,omitempty"`
+	ICEServers []string `json:"iceServers,omitempty"`
 }
 
 type SetPasswordRequest struct {
@@ -116,7 +118,7 @@ func handleWebRTCSession(c *gin.Context) {
 		return
 	}
 
-	session, err := newSession()
+	session, err := newSession(nil, "")
 	if err != nil {
 		c.JSON(http.StatusInternalServerError, gin.H{"error": err})
 		return
diff --git a/webrtc.go b/webrtc.go
index 20ffb99..4e79902 100644
--- a/webrtc.go
+++ b/webrtc.go
@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"net"
 	"strings"
 
 	"github.com/pion/webrtc/v4"
@@ -61,9 +62,23 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
 	return base64.StdEncoding.EncodeToString(localDescription), nil
 }
 
-func newSession() (*Session, error) {
-	peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
-		ICEServers: []webrtc.ICEServer{{}},
+func newSession(iceServers []string, localIP string) (*Session, error) {
+	if iceServers == nil {
+		iceServers = config.FallbackICEServers
+		fmt.Printf("ICE Servers not provided, using fallback %v\n", iceServers)
+	}
+
+	webrtcSettingEngine := webrtc.SettingEngine{}
+	if localIP != "" || net.ParseIP(localIP) == nil {
+		fmt.Printf("Local IP address not provided or invalid, won't set NAT1To1IPs\n")
+	} else {
+		webrtcSettingEngine.SetNAT1To1IPs([]string{localIP}, webrtc.ICECandidateTypeSrflx)
+	}
+
+	// create
+	api := webrtc.NewAPI(webrtc.WithSettingEngine(webrtcSettingEngine))
+	peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
+		ICEServers: []webrtc.ICEServer{{URLs: iceServers}},
 	})
 	if err != nil {
 		return nil, err