Development discussion of WireGuard
 help / color / mirror / Atom feed
* [PATCH] Periodically update endpoint IP in windows app (dynamic DNS)
@ 2021-12-08 14:26 Tobias Tangemann
  0 siblings, 0 replies; only message in thread
From: Tobias Tangemann @ 2021-12-08 14:26 UTC (permalink / raw)
  To: wireguard

[-- Attachment #1: Type: text/plain, Size: 2647 bytes --]

Hi,

I know of the reresolvedns script for Linux but there is no nice
solution for the other platforms. I think this is a feature many users
would benefit from if directly integrated into the wireguard apps
(including me).

I implemented a new config option which periodically updates the
endpoint IP in the same way the reresolvedns script does it, but
inside the windows app.
A go routine gets created inside the tunnel manager for each peer
which has the option set. if the option is not set for any peer there
is no change in behavior.
Kudos to the well-crafted build system and awesome build.bat for
windows, this made it very easy to get started.
I think there are some points left open:
- The name of the option cannot easily be changed later as it would
require additional migration code (I’m not tied to the name “Update
endpoint IP”).
- Is the amount of logging this feature currently creates ok?
- I haven’t updated the localization to not blow this patch up to
much, this can be done after settling on a name for the option.
- Most of the code is ui related and pretty straight forward. I
implemented the PeerUpdateEndpointConfiguration based on my analysis
of the wireguard-tools set.c source and it seems to work but I think
someone with a deeper understanding of the driver interface should
check that. During my testing the feature seems to work fine with 1
peer, maybe someone can test this in a more complicated setup.
- I think this will not work if all traffic is routed through the VPN
including DNS therefor once the IP of the peer changes, the link
breaks and updating the endpoint IP will not work. I’m not sure if
there is a way to fix this.
- I’m not sure on the position of config options only being supported
on specific clients. I think this is already the case but I don’t have
a good overview.
It would be nice to also have this feature in the other clients like
iOS/macOS and android. I think it is not necessary to update the Linux
tool. If desired I think it could be included in wg-quick.
I’m unsure how to tackle this feature for macOS/iOS, my assumption is
that it would need to be included in the PacketTunnelProvider and be
part of the NetworkExtension or maybe in the userland driver
wireguard-go?
Sadly, due to all the apple signing and provisioning hoops you have to
jump through I was not able to get even the macOS app to build using
my free developer account.

I hope this patch can be reviewed and upstreamed.

The patch can also be found here:
https://github.com/WireGuard/wireguard-windows/pull/18

Best regards - Tobias Tangemann

[-- Attachment #2: 0001-tunnel-conf-ui-periodic-update-of-endpoint-ip.patch --]
[-- Type: application/octet-stream, Size: 12617 bytes --]

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


^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2021-12-08 14:51 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-12-08 14:26 [PATCH] Periodically update endpoint IP in windows app (dynamic DNS) Tobias Tangemann

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).