From 7ce8acd6c3eeb544452b77027b5575f998fc0bd8 Mon Sep 17 00:00:00 2001
From: Tobias Tangemann <tobias@tangemann.org>
Date: Tue, 7 Dec 2021 21:17:50 +0100
Subject: [PATCH] tunnel+conf+ui: periodic update of endpoint ip

---
 conf/config.go              |  2 ++
 conf/dnsresolver_windows.go |  7 +++++--
 conf/parser.go              | 26 ++++++++++++++++++++++++++
 conf/writer.go              | 25 +++++++++++++++++++++++++
 tunnel/service.go           | 33 +++++++++++++++++++++++++++++++++
 ui/confview.go              |  8 ++++++++
 ui/syntax/highlighter.go    | 13 +++++++++++++
 ui/syntax/syntaxedit.go     | 33 +++++++++++++++++----------------
 8 files changed, 129 insertions(+), 18 deletions(-)

diff --git a/conf/config.go b/conf/config.go
index e6cc6aac..1c91ee29 100644
--- a/conf/config.go
+++ b/conf/config.go
@@ -57,10 +57,12 @@ type Peer struct {
 	AllowedIPs          []netip.Prefix
 	Endpoint            Endpoint
 	PersistentKeepalive uint16
+	UpdateEndpointIP    uint16
 
 	RxBytes           Bytes
 	TxBytes           Bytes
 	LastHandshakeTime HandshakeTime
+	UnresolvedHost    string
 }
 
 func (conf *Config) IntersectsWith(other *Config) bool {
diff --git a/conf/dnsresolver_windows.go b/conf/dnsresolver_windows.go
index b09f4603..fcd27153 100644
--- a/conf/dnsresolver_windows.go
+++ b/conf/dnsresolver_windows.go
@@ -19,7 +19,7 @@ import (
 
 //sys	internetGetConnectedState(flags *uint32, reserved uint32) (connected bool) = wininet.InternetGetConnectedState
 
-func resolveHostname(name string) (resolvedIPString string, err error) {
+func ResolveHostname(name string) (resolvedIPString string, err error) {
 	maxTries := 10
 	if services.StartedAtBoot() {
 		maxTries *= 4
@@ -90,8 +90,11 @@ func (config *Config) ResolveEndpoints() error {
 		if config.Peers[i].Endpoint.IsEmpty() {
 			continue
 		}
+		if config.Peers[i].UpdateEndpointIP > 0 {
+			config.Peers[i].UnresolvedHost = config.Peers[i].Endpoint.Host
+		}
 		var err error
-		config.Peers[i].Endpoint.Host, err = resolveHostname(config.Peers[i].Endpoint.Host)
+		config.Peers[i].Endpoint.Host, err = ResolveHostname(config.Peers[i].Endpoint.Host)
 		if err != nil {
 			return err
 		}
diff --git a/conf/parser.go b/conf/parser.go
index 477a5205..e821e9d5 100644
--- a/conf/parser.go
+++ b/conf/parser.go
@@ -109,6 +109,20 @@ func parsePersistentKeepalive(s string) (uint16, error) {
 	return uint16(m), nil
 }
 
+func parseUpdateEndpointIP(s string) (uint16, error) {
+	if s == "off" {
+		return 0, nil
+	}
+	m, err := strconv.Atoi(s)
+	if err != nil {
+		return 0, err
+	}
+	if m < 0 || m > 65535 {
+		return 0, &ParseError{l18n.Sprintf("Invalid update endpoint IP"), s}
+	}
+	return uint16(m), nil
+}
+
 func parseTableOff(s string) (bool, error) {
 	if s == "off" {
 		return true, nil
@@ -294,6 +308,12 @@ func FromWgQuick(s string, name string) (*Config, error) {
 					return nil, err
 				}
 				peer.PersistentKeepalive = p
+			case "updateendpointip":
+				p, err := parseUpdateEndpointIP(val)
+				if err != nil {
+					return nil, err
+				}
+				peer.UpdateEndpointIP = p
 			case "endpoint":
 				e, err := parseEndpoint(val)
 				if err != nil {
@@ -378,6 +398,12 @@ func FromDriverConfiguration(interfaze *driver.Interface, existingConfig *Config
 		if p.Flags&driver.PeerHasPersistentKeepalive != 0 {
 			peer.PersistentKeepalive = p.PersistentKeepalive
 		}
+		for i := range existingConfig.Peers {
+			if existingConfig.Peers[i].PublicKey == peer.PublicKey && existingConfig.Peers[i].UpdateEndpointIP > 0 {
+				// Get UpdateEndpointIP option from config as it is cannot be retrieved from the driver
+				peer.UpdateEndpointIP = existingConfig.Peers[i].UpdateEndpointIP
+			}
+		}
 		peer.TxBytes = Bytes(p.TxBytes)
 		peer.RxBytes = Bytes(p.RxBytes)
 		if p.LastHandshake != 0 {
diff --git a/conf/writer.go b/conf/writer.go
index 61abb672..7feb536a 100644
--- a/conf/writer.go
+++ b/conf/writer.go
@@ -88,6 +88,10 @@ func (conf *Config) ToWgQuick() string {
 		if peer.PersistentKeepalive > 0 {
 			output.WriteString(fmt.Sprintf("PersistentKeepalive = %d\n", peer.PersistentKeepalive))
 		}
+
+		if peer.UpdateEndpointIP > 0 {
+			output.WriteString(fmt.Sprintf("UpdateEndpointIP = %d\n", peer.UpdateEndpointIP))
+		}
 	}
 	return output.String()
 }
@@ -139,3 +143,24 @@ func (config *Config) ToDriverConfiguration() (*driver.Interface, uint32) {
 	}
 	return c.Interface()
 }
+
+func PeerUpdateEndpointConfiguration(peer *Peer, newIP string) (*driver.Interface, uint32) {
+	preallocation := unsafe.Sizeof(driver.Interface{}) + uintptr(1)*unsafe.Sizeof(driver.Peer{})
+	var c driver.ConfigBuilder
+	c.Preallocate(uint32(preallocation))
+	c.AppendInterface(&driver.Interface{
+		PeerCount: uint32(1),
+	})
+
+	var endpoint winipcfg.RawSockaddrInet
+	addr, err := netip.ParseAddr(newIP)
+	if err == nil {
+		endpoint.SetAddrPort(netip.AddrPortFrom(addr, peer.Endpoint.Port))
+	}
+	c.AppendPeer(&driver.Peer{
+		Flags:     driver.PeerHasEndpoint | driver.PeerHasPublicKey,
+		PublicKey: peer.PublicKey,
+		Endpoint:  endpoint,
+	})
+	return c.Interface()
+}
diff --git a/tunnel/service.go b/tunnel/service.go
index 374d71d3..3ab52932 100644
--- a/tunnel/service.go
+++ b/tunnel/service.go
@@ -219,6 +219,8 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest,
 		return
 	}
 
+	startUpdateEndpointIP(adapter, config)
+
 	changes <- svc.Status{State: serviceState, Accepts: svc.AcceptStop | svc.AcceptShutdown}
 
 	var started bool
@@ -247,6 +249,37 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest,
 	}
 }
 
+func startUpdateEndpointIP(adapter *driver.Adapter, config *conf.Config) {
+	for i := range config.Peers {
+		if !config.Peers[i].Endpoint.IsEmpty() && config.Peers[i].UpdateEndpointIP > 0 {
+			go resolveDNSLoop(adapter, &config.Peers[i], i+1)
+		}
+	}
+}
+
+func resolveDNSLoop(adapter *driver.Adapter, peer *conf.Peer, peerNumber int) {
+	for {
+		time.Sleep(time.Second * time.Duration(peer.UpdateEndpointIP))
+
+		log.Printf("Checking DNS of endpoint for peer %d", peerNumber)
+		resolvedIPString, err := conf.ResolveHostname(peer.UnresolvedHost)
+		if err != nil {
+			log.Printf("Error resolving hostname of peer %d", peerNumber)
+			continue
+		}
+
+		if resolvedIPString != peer.Endpoint.Host {
+			peer.Endpoint.Host = resolvedIPString
+			err = adapter.SetConfiguration(conf.PeerUpdateEndpointConfiguration(peer, resolvedIPString))
+			if err == nil {
+				log.Printf("IP for peer %d updated (%v)", peerNumber, resolvedIPString)
+			} else {
+				log.Printf("Error updating IP for peer %d: %v", peerNumber, err)
+			}
+		}
+	}
+}
+
 func Run(confPath string) error {
 	name, err := conf.NameFromPath(confPath)
 	if err != nil {
diff --git a/ui/confview.go b/ui/confview.go
index 3d16f38f..9a0b7922 100644
--- a/ui/confview.go
+++ b/ui/confview.go
@@ -62,6 +62,7 @@ type peerView struct {
 	allowedIPs          *labelTextLine
 	endpoint            *labelTextLine
 	persistentKeepalive *labelTextLine
+	updateEndpointIP    *labelTextLine
 	latestHandshake     *labelTextLine
 	transfer            *labelTextLine
 	lines               []widgetsLine
@@ -337,6 +338,7 @@ func newPeerView(parent walk.Container) (*peerView, error) {
 		{l18n.Sprintf("Allowed IPs:"), &pv.allowedIPs},
 		{l18n.Sprintf("Endpoint:"), &pv.endpoint},
 		{l18n.Sprintf("Persistent keepalive:"), &pv.persistentKeepalive},
+		{l18n.Sprintf("Update endpoint IP:"), &pv.updateEndpointIP},
 		{l18n.Sprintf("Latest handshake:"), &pv.latestHandshake},
 		{l18n.Sprintf("Transfer:"), &pv.transfer},
 	}
@@ -476,6 +478,12 @@ func (pv *peerView) apply(c *conf.Peer) {
 		pv.persistentKeepalive.hide()
 	}
 
+	if c.UpdateEndpointIP > 0 {
+		pv.updateEndpointIP.show(strconv.Itoa(int(c.UpdateEndpointIP)))
+	} else {
+		pv.updateEndpointIP.hide()
+	}
+
 	if !c.LastHandshakeTime.IsEmpty() {
 		pv.latestHandshake.show(c.LastHandshakeTime.String())
 	} else {
diff --git a/ui/syntax/highlighter.go b/ui/syntax/highlighter.go
index 47946093..254a2ae2 100644
--- a/ui/syntax/highlighter.go
+++ b/ui/syntax/highlighter.go
@@ -24,6 +24,7 @@ const (
 	highlightPort
 	highlightMTU
 	highlightKeepalive
+	highlightUpdateEndpointIP
 	highlightComment
 	highlightDelimiter
 	highlightTable
@@ -268,6 +269,13 @@ func (s stringSpan) isValidPersistentKeepAlive() bool {
 	return s.isValidUint(false, 0, 65535)
 }
 
+func (s stringSpan) isValidUpdateEndpointIP() bool {
+	if s.isSame("off") {
+		return true
+	}
+	return s.isValidUint(false, 0, 65535)
+}
+
 // It's probably not worthwhile to try to validate a bash expression. So instead we just demand non-zero length.
 func (s stringSpan) isValidPrePostUpDown() bool {
 	return s.len != 0
@@ -376,6 +384,7 @@ const (
 	fieldAllowedIPs
 	fieldEndpoint
 	fieldPersistentKeepalive
+	fieldUpdateEndpointIP
 	fieldInvalid
 )
 
@@ -413,6 +422,8 @@ func (s stringSpan) field() field {
 		return fieldEndpoint
 	case s.isCaselessSame("PersistentKeepalive"):
 		return fieldPersistentKeepalive
+	case s.isCaselessSame("UpdateEndpointIP"):
+		return fieldUpdateEndpointIP
 	case s.isCaselessSame("PreUp"):
 		return fieldPreUp
 	case s.isCaselessSame("PostUp"):
@@ -524,6 +535,8 @@ func (hsa *highlightSpanArray) highlightValue(parent stringSpan, s stringSpan, s
 		hsa.append(parent.s, s, validateHighlight(s.isValidPort(), highlightPort))
 	case fieldPersistentKeepalive:
 		hsa.append(parent.s, s, validateHighlight(s.isValidPersistentKeepAlive(), highlightKeepalive))
+	case fieldUpdateEndpointIP:
+		hsa.append(parent.s, s, validateHighlight(s.isValidUpdateEndpointIP(), highlightUpdateEndpointIP))
 	case fieldEndpoint:
 		if !s.isValidEndpoint() {
 			hsa.append(parent.s, s, highlightError)
diff --git a/ui/syntax/syntaxedit.go b/ui/syntax/syntaxedit.go
index 42f6e7b7..f8447f1e 100644
--- a/ui/syntax/syntaxedit.go
+++ b/ui/syntax/syntaxedit.go
@@ -90,22 +90,23 @@ type spanStyle struct {
 }
 
 var stylemap = map[highlight]spanStyle{
-	highlightSection:      spanStyle{color: win.RGB(0x32, 0x6D, 0x74), effects: win.CFE_BOLD},
-	highlightField:        spanStyle{color: win.RGB(0x9B, 0x23, 0x93), effects: win.CFE_BOLD},
-	highlightPrivateKey:   spanStyle{color: win.RGB(0x64, 0x38, 0x20)},
-	highlightPublicKey:    spanStyle{color: win.RGB(0x64, 0x38, 0x20)},
-	highlightPresharedKey: spanStyle{color: win.RGB(0x64, 0x38, 0x20)},
-	highlightIP:           spanStyle{color: win.RGB(0x0E, 0x0E, 0xFF)},
-	highlightCidr:         spanStyle{color: win.RGB(0x81, 0x5F, 0x03)},
-	highlightHost:         spanStyle{color: win.RGB(0x0E, 0x0E, 0xFF)},
-	highlightPort:         spanStyle{color: win.RGB(0x81, 0x5F, 0x03)},
-	highlightMTU:          spanStyle{color: win.RGB(0x1C, 0x00, 0xCF)},
-	highlightTable:        spanStyle{color: win.RGB(0x1C, 0x00, 0xCF)},
-	highlightKeepalive:    spanStyle{color: win.RGB(0x1C, 0x00, 0xCF)},
-	highlightComment:      spanStyle{color: win.RGB(0x53, 0x65, 0x79), effects: win.CFE_ITALIC},
-	highlightDelimiter:    spanStyle{color: win.RGB(0x00, 0x00, 0x00)},
-	highlightCmd:          spanStyle{color: win.RGB(0x63, 0x75, 0x89)},
-	highlightError:        spanStyle{color: win.RGB(0xC4, 0x1A, 0x16), effects: win.CFE_UNDERLINE},
+	highlightSection:          spanStyle{color: win.RGB(0x32, 0x6D, 0x74), effects: win.CFE_BOLD},
+	highlightField:            spanStyle{color: win.RGB(0x9B, 0x23, 0x93), effects: win.CFE_BOLD},
+	highlightPrivateKey:       spanStyle{color: win.RGB(0x64, 0x38, 0x20)},
+	highlightPublicKey:        spanStyle{color: win.RGB(0x64, 0x38, 0x20)},
+	highlightPresharedKey:     spanStyle{color: win.RGB(0x64, 0x38, 0x20)},
+	highlightIP:               spanStyle{color: win.RGB(0x0E, 0x0E, 0xFF)},
+	highlightCidr:             spanStyle{color: win.RGB(0x81, 0x5F, 0x03)},
+	highlightHost:             spanStyle{color: win.RGB(0x0E, 0x0E, 0xFF)},
+	highlightPort:             spanStyle{color: win.RGB(0x81, 0x5F, 0x03)},
+	highlightMTU:              spanStyle{color: win.RGB(0x1C, 0x00, 0xCF)},
+	highlightTable:            spanStyle{color: win.RGB(0x1C, 0x00, 0xCF)},
+	highlightKeepalive:        spanStyle{color: win.RGB(0x1C, 0x00, 0xCF)},
+	highlightUpdateEndpointIP: spanStyle{color: win.RGB(0x1C, 0x00, 0xCF)},
+	highlightComment:          spanStyle{color: win.RGB(0x53, 0x65, 0x79), effects: win.CFE_ITALIC},
+	highlightDelimiter:        spanStyle{color: win.RGB(0x00, 0x00, 0x00)},
+	highlightCmd:              spanStyle{color: win.RGB(0x63, 0x75, 0x89)},
+	highlightError:            spanStyle{color: win.RGB(0xC4, 0x1A, 0x16), effects: win.CFE_UNDERLINE},
 }
 
 func (se *SyntaxEdit) evaluateUntunneledBlocking(cfg string, spans []highlightSpan) {
-- 
2.34.1.windows.1

