Repository: netbirdio/netbird

Issue:
win11  Settings ->Advanced Settings cannot be opened
**Describe the problem**

After pulling the code, I tracked the issue and found that due to the default font loading order, the tray icon, Settings ->Advanced Settings cannot be opened

Just move the code from line 113 in the    client/ui/cron_ui.go   file to line 93, and the problem will be solved

I used the fixed code, but it still reports this error on Win10 with only comments
client.setDefaultFonts()
Later, the management page can only be opened, and I don't know how to fix it

After reviewing the information, I found that the reason is that FYNE does not support the ttc font very well

Because I am not very good at using Git and do not know how to submit the code I have modified, I am raising this issue. Once the issue is resolved, I will close it


The following is the repair process

![Image](https://github.com/user-attachments/assets/ae3946c8-f8b9-4bf9-afec-d13649554271)


The following is the console log after the error occurred

![Image](https://github.com/user-attachments/assets/8f3a6c11-83a3-472b-9c51-e2c58cd2aaaa)




**Expected behavior**

Successful images when running the program after repair

![Image](https://github.com/user-attachments/assets/8063ff26-7e22-4212-aaa8-c3c599b0f38a)



**Are you using NetBird Cloud?**

Please specify whether you use NetBird Cloud or self-host NetBird's control plane.

**NetBird version**

`netbird version`

**NetBird status -dA output:**

If applicable, add the `netbird status -dA' command output.

**Do you face any (non-mobile) client issues?**

Please provide the file created by `netbird debug for 1m -AS`.
We advise reviewing the anonymized files for any remaining PII.

**Screenshots**

If applicable, add screenshots to help explain your problem.

**Additional context**

Add any other context about the problem here.


Discussion:
@mlsmaycon: @moqiu365 This week, we fixed an issue related to the peer sessions and the settings menu. Can you confirm if you have tested with the most recent code in our main repo and that the problem persists without your proposal? @moqiu365: > 本周，我们修复了与对等会话和设置菜单相关的问题。您能否确认您是否已经使用我们主存储库中的最新代码进行了测试，并且问题在没有您的提议的情况下仍然存在？

I have deleted the local code, created a new folder, and pulled the latest code. Even after deployment, I still have this problem. Here are my steps

//Clean up the previous cache
go clean -modcache
//Delete the pulled code (delete the entire folder) and re pull it

git clone  https://ghproxy.cfd/https://github.com/netbirdio/netbird.git
cd netbird/
go mod tidy
cd client/
go build -x -v -o netbird-ui.exe .\ui\

Then using the administrator to run it, clicking on Settings ->Advanced Settings, the above problem appeared. Strangely, I followed my previous solution and still encountered this problem, only commenting it out
client.setDefaultFonts()
This line of code is required to open advanced settings


Upon further investigation, it was found that the issue was caused by zh CN using ttc format fonts in client \ ui \ font_windows.go, which is not well supported by the fyne framework for ttc format fonts

Many Chinese fonts that come with the Windows system are not allowed for commercial use

Suggest changing the font in zh CN format to
https://github.com/adobe-fonts/source-han-sans/tree/release
This ttf format font avoids commercial disputes，@moqiu365: There is another solution, in
client\ui\font_windows.go   
Annotate lines 28, 32, and 33 to use the default font, which will also enable it to open advanced settings normally

![Image](https://github.com/user-attachments/assets/1939fe81-dfe5-44d7-8d93-5a3dc0e6b3b3)@wgetnz: I have the same problem@AtaLuZiK: same problem +1@levindecaro: same problem +1

@RoyLi-Pvt: 0.38 same problem@lpqoang: The issue still exists v0.38.0@betaxab: The issue still exists in Windows11 24H2 Simple Chinese edition, nb client windows version: v0.38.0@lpqoang: > The issue still exists v0.38.0

v0.39.1 fixed

Solution patch:
diff --git a/README.md b/README.md
index a063c821..e39382ac 100644
--- a/README.md
+++ b/README.md
@@ -34,8 +34,8 @@
  
 </strong>
 <br>
-<a href="https://netbird.io/webinars/achieve-zero-trust-access-to-k8s?utm_source=github&utm_campaign=2502%20-%20webinar%20-%20How%20to%20Achieve%20Zero%20Trust%20Access%20to%20Kubernetes%20-%20Effortlessly&utm_medium=github">
-    Webinar: Securely Access Kubernetes without Port Forwarding and Jump Hosts
+<a href="https://github.com/netbirdio/kubernetes-operator">
+    New: NetBird Kubernetes Operator
   </a> 
 </p>
 
diff --git a/client/anonymize/anonymize.go b/client/anonymize/anonymize.go
index 89552724..2fc9d49d 100644
--- a/client/anonymize/anonymize.go
+++ b/client/anonymize/anonymize.go
@@ -26,7 +26,7 @@ type Anonymizer struct {
 }
 
 func DefaultAddresses() (netip.Addr, netip.Addr) {
-	// 192.51.100.0, 100::
+	// 198.51.100.0, 100::
 	return netip.AddrFrom4([4]byte{198, 51, 100, 0}), netip.AddrFrom16([16]byte{0x01})
 }
 
diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go
index b91cfe33..42f71ed2 100644
--- a/client/internal/peer/conn.go
+++ b/client/internal/peer/conn.go
@@ -114,6 +114,9 @@ type Conn struct {
 
 	guard     *guard.Guard
 	semaphore *semaphoregroup.SemaphoreGroup
+
+	// debug purpose
+	dumpState *stateDump
 }
 
 // NewConn creates a new not opened Conn to the remote peer.
@@ -137,10 +140,11 @@ func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Statu
 		statusRelay:    NewAtomicConnStatus(),
 		statusICE:      NewAtomicConnStatus(),
 		semaphore:      semaphore,
+		dumpState:      newStateDump(connLog),
 	}
 
 	ctrl := isController(config)
-	conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, conn, relayManager)
+	conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, conn, relayManager, conn.dumpState)
 
 	relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally()
 	workerICE, err := NewWorkerICE(ctx, connLog, config, conn, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally)
@@ -160,6 +164,7 @@ func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Statu
 
 	go conn.handshaker.Listen()
 
+	go conn.dumpState.Start(ctx)
 	return conn, nil
 }
 
@@ -193,6 +198,7 @@ func (conn *Conn) startHandshakeAndReconnect(ctx context.Context) {
 	defer conn.semaphore.Done(conn.ctx)
 	conn.waitInitialRandomSleepTime(ctx)
 
+	conn.dumpState.SendOffer()
 	err := conn.handshaker.sendOffer()
 	if err != nil {
 		conn.log.Errorf("failed to send initial offer: %v", err)
@@ -251,12 +257,14 @@ func (conn *Conn) Close() {
 // OnRemoteAnswer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
 // doesn't block, discards the message if connection wasn't ready
 func (conn *Conn) OnRemoteAnswer(answer OfferAnswer) bool {
-	conn.log.Debugf("OnRemoteAnswer, status ICE: %s, status relay: %s", conn.statusICE, conn.statusRelay)
+	conn.dumpState.RemoteAnswer()
+	conn.log.Infof("OnRemoteAnswer, status ICE: %s, status relay: %s", conn.statusICE, conn.statusRelay)
 	return conn.handshaker.OnRemoteAnswer(answer)
 }
 
 // OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
 func (conn *Conn) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HAMap) {
+	conn.dumpState.RemoteCandidate()
 	conn.workerICE.OnRemoteCandidate(candidate, haRoutes)
 }
 
@@ -278,7 +286,8 @@ func (conn *Conn) SetOnDisconnected(handler func(remotePeer string)) {
 }
 
 func (conn *Conn) OnRemoteOffer(offer OfferAnswer) bool {
-	conn.log.Debugf("OnRemoteOffer, on status ICE: %s, status Relay: %s", conn.statusICE, conn.statusRelay)
+	conn.dumpState.RemoteOffer()
+	conn.log.Infof("OnRemoteOffer, on status ICE: %s, status Relay: %s", conn.statusICE, conn.statusRelay)
 	return conn.handshaker.OnRemoteOffer(offer)
 }
 
@@ -322,6 +331,7 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC
 	}
 
 	conn.log.Infof("set ICE to active connection")
+	conn.dumpState.P2PConnected()
 
 	var (
 		ep      *net.UDPAddr
@@ -329,6 +339,7 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC
 		err     error
 	)
 	if iceConnInfo.RelayedOnLocal {
+		conn.dumpState.NewLocalProxy()
 		wgProxy, err = conn.newProxy(iceConnInfo.RemoteConn)
 		if err != nil {
 			conn.log.Errorf("failed to add turn net.Conn to local proxy: %v", err)
@@ -390,6 +401,7 @@ func (conn *Conn) onICEStateDisconnected() {
 	// switch back to relay connection
 	if conn.isReadyToUpgrade() {
 		conn.log.Infof("ICE disconnected, set Relay to active connection")
+		conn.dumpState.SwitchToRelay()
 		conn.wgProxyRelay.Work()
 
 		if err := conn.configureWGEndpoint(conn.wgProxyRelay.EndpointAddr()); err != nil {
@@ -432,6 +444,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
 		return
 	}
 
+	conn.dumpState.RelayConnected()
 	conn.log.Debugf("Relay connection has been established, setup the WireGuard")
 
 	wgProxy, err := conn.newProxy(rci.relayedConn)
@@ -439,6 +452,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
 		conn.log.Errorf("failed to add relayed net.Conn to local proxy: %v", err)
 		return
 	}
+	conn.dumpState.NewLocalProxy()
 
 	conn.log.Infof("created new wgProxy for relay connection: %s", wgProxy.EndpointAddr().String())
 
@@ -481,10 +495,10 @@ func (conn *Conn) onRelayDisconnected() {
 		return
 	}
 
-	conn.log.Debugf("relay connection is disconnected")
+	conn.log.Infof("relay connection is disconnected")
 
 	if conn.currentConnPriority == connPriorityRelay {
-		conn.log.Debugf("clean up WireGuard config")
+		conn.log.Infof("clean up WireGuard config")
 		if err := conn.removeWgPeer(); err != nil {
 			conn.log.Errorf("failed to remove wg endpoint: %v", err)
 		}
@@ -516,7 +530,8 @@ func (conn *Conn) listenGuardEvent(ctx context.Context) {
 	for {
 		select {
 		case <-conn.guard.Reconnect:
-			conn.log.Debugf("send offer to peer")
+			conn.log.Infof("send offer to peer")
+			conn.dumpState.SendOffer()
 			if err := conn.handshaker.SendOffer(); err != nil {
 				conn.log.Errorf("failed to send offer: %v", err)
 			}
diff --git a/client/internal/peer/handshaker.go b/client/internal/peer/handshaker.go
index 545f8196..d23727e9 100644
--- a/client/internal/peer/handshaker.go
+++ b/client/internal/peer/handshaker.go
@@ -76,19 +76,19 @@ func (h *Handshaker) AddOnNewOfferListener(offer func(remoteOfferAnswer *OfferAn
 
 func (h *Handshaker) Listen() {
 	for {
-		h.log.Debugf("wait for remote offer confirmation")
+		h.log.Info("wait for remote offer confirmation")
 		remoteOfferAnswer, err := h.waitForRemoteOfferConfirmation()
 		if err != nil {
 			var connectionClosedError *ConnectionClosedError
 			if errors.As(err, &connectionClosedError) {
-				h.log.Tracef("stop handshaker")
+				h.log.Info("exit from handshaker")
 				return
 			}
 			h.log.Errorf("failed to received remote offer confirmation: %s", err)
 			continue
 		}
 
-		h.log.Debugf("received connection confirmation, running version %s and with remote WireGuard listen port %d", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort)
+		h.log.Infof("received connection confirmation, running version %s and with remote WireGuard listen port %d", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort)
 		for _, listener := range h.onNewOfferListeners {
 			go listener(remoteOfferAnswer)
 		}
@@ -108,7 +108,7 @@ func (h *Handshaker) OnRemoteOffer(offer OfferAnswer) bool {
 	case h.remoteOffersCh <- offer:
 		return true
 	default:
-		h.log.Debugf("OnRemoteOffer skipping message because is not ready")
+		h.log.Warnf("OnRemoteOffer skipping message because is not ready")
 		// connection might not be ready yet to receive so we ignore the message
 		return false
 	}
@@ -131,8 +131,7 @@ func (h *Handshaker) waitForRemoteOfferConfirmation() (*OfferAnswer, error) {
 	select {
 	case remoteOfferAnswer := <-h.remoteOffersCh:
 		// received confirmation from the remote peer -> ready to proceed
-		err := h.sendAnswer()
-		if err != nil {
+		if err := h.sendAnswer(); err != nil {
 			return nil, err
 		}
 		return &remoteOfferAnswer, nil
@@ -168,7 +167,7 @@ func (h *Handshaker) sendOffer() error {
 }
 
 func (h *Handshaker) sendAnswer() error {
-	h.log.Debugf("sending answer")
+	h.log.Infof("sending answer")
 	uFrag, pwd := h.ice.GetLocalUserCredentials()
 
 	answer := OfferAnswer{
diff --git a/client/internal/peer/state_dump.go b/client/internal/peer/state_dump.go
new file mode 100644
index 00000000..9c148a85
--- /dev/null
+++ b/client/internal/peer/state_dump.go
@@ -0,0 +1,112 @@
+package peer
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+)
+
+type stateDump struct {
+	log *log.Entry
+
+	sentOffer       int
+	remoteOffer     int
+	remoteAnswer    int
+	remoteCandidate int
+	p2pConnected    int
+	switchToRelay   int
+	wgCheckSuccess  int
+	relayConnected  int
+	localProxies    int
+
+	mu sync.Mutex
+}
+
+func newStateDump(log *log.Entry) *stateDump {
+	return &stateDump{
+		log: log,
+	}
+}
+
+func (s *stateDump) Start(ctx context.Context) {
+	ticker := time.NewTicker(10 * time.Minute)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			s.dumpState()
+		case <-ctx.Done():
+			return
+		}
+	}
+}
+
+func (s *stateDump) RemoteOffer() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.remoteOffer++
+}
+
+func (s *stateDump) RemoteCandidate() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.remoteCandidate++
+}
+
+func (s *stateDump) SendOffer() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.sentOffer++
+}
+
+func (s *stateDump) dumpState() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.log.Infof("Dump stat: SentOffer: %d, RemoteOffer: %d, RemoteAnswer: %d, RemoteCandidate: %d, P2PConnected: %d, SwitchToRelay: %d, WGCheckSuccess: %d, RelayConnected: %d, LocalProxies: %d",
+		s.sentOffer, s.remoteOffer, s.remoteAnswer, s.remoteCandidate, s.p2pConnected, s.switchToRelay, s.wgCheckSuccess, s.relayConnected, s.localProxies)
+}
+
+func (s *stateDump) RemoteAnswer() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.remoteAnswer++
+}
+
+func (s *stateDump) P2PConnected() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.p2pConnected++
+}
+
+func (s *stateDump) SwitchToRelay() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.switchToRelay++
+}
+
+func (s *stateDump) WGcheckSuccess() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.wgCheckSuccess++
+}
+
+func (s *stateDump) RelayConnected() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.relayConnected++
+}
+
+func (s *stateDump) NewLocalProxy() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.localProxies++
+}
diff --git a/client/internal/peer/wg_watcher.go b/client/internal/peer/wg_watcher.go
index 6670c651..49049b3d 100644
--- a/client/internal/peer/wg_watcher.go
+++ b/client/internal/peer/wg_watcher.go
@@ -27,6 +27,7 @@ type WGWatcher struct {
 	log           *log.Entry
 	wgIfaceStater WGInterfaceStater
 	peerKey       string
+	stateDump     *stateDump
 
 	ctx       context.Context
 	ctxCancel context.CancelFunc
@@ -34,11 +35,12 @@ type WGWatcher struct {
 	waitGroup sync.WaitGroup
 }
 
-func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string) *WGWatcher {
+func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string, stateDump *stateDump) *WGWatcher {
 	return &WGWatcher{
 		log:           log,
 		wgIfaceStater: wgIfaceStater,
 		peerKey:       peerKey,
+		stateDump:     stateDump,
 	}
 }
 
@@ -105,6 +107,7 @@ func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, ctxCancel contex
 
 			resetTime := time.Until(handshake.Add(checkPeriod))
 			timer.Reset(resetTime)
+			w.stateDump.WGcheckSuccess()
 
 			w.log.Debugf("WireGuard watcher reset timer: %v", resetTime)
 		case <-ctx.Done():
diff --git a/client/internal/peer/worker_relay.go b/client/internal/peer/worker_relay.go
index 56c19cd1..aa8f7d63 100644
--- a/client/internal/peer/worker_relay.go
+++ b/client/internal/peer/worker_relay.go
@@ -33,14 +33,14 @@ type WorkerRelay struct {
 	wgWatcher *WGWatcher
 }
 
-func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService) *WorkerRelay {
+func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService, stateDump *stateDump) *WorkerRelay {
 	r := &WorkerRelay{
 		log:          log,
 		isController: ctrl,
 		config:       config,
 		conn:         conn,
 		relayManager: relayManager,
-		wgWatcher:    NewWGWatcher(log, config.WgConfig.WgInterface, config.Key),
+		wgWatcher:    NewWGWatcher(log, config.WgConfig.WgInterface, config.Key, stateDump),
 	}
 	return r
 }
diff --git a/client/server/debug.go b/client/server/debug.go
index 749220d6..39a67c92 100644
--- a/client/server/debug.go
+++ b/client/server/debug.go
@@ -53,7 +53,7 @@ The files in this bundle have been anonymized to protect sensitive information.
 
 IP Addresses
 
-IPv4 addresses are replaced with addresses starting from 192.51.100.0
+IPv4 addresses are replaced with addresses starting from 198.51.100.0
 IPv6 addresses are replaced with addresses starting from 100::
 
 IP addresses from non public ranges and well known addresses are not anonymized (e.g. 8.8.8.8, 100.64.0.0/10, addresses starting with 192.168., 172.16., 10., etc.).
diff --git a/client/server/network.go b/client/server/network.go
index d310f4da..1b7962b7 100644
--- a/client/server/network.go
+++ b/client/server/network.go
@@ -36,8 +36,13 @@ func (s *Server) ListNetworks(context.Context, *proto.ListNetworksRequest) (*pro
 		return nil, fmt.Errorf("not connected")
 	}
 
-	routesMap := engine.GetRouteManager().GetClientRoutesWithNetID()
-	routeSelector := engine.GetRouteManager().GetRouteSelector()
+	routeMgr := engine.GetRouteManager()
+	if routeMgr == nil {
+		return nil, fmt.Errorf("no route manager")
+	}
+
+	routesMap := routeMgr.GetClientRoutesWithNetID()
+	routeSelector := routeMgr.GetRouteSelector()
 
 	var routes []*selectRoute
 	for id, rt := range routesMap {
@@ -123,6 +128,10 @@ func (s *Server) SelectNetworks(_ context.Context, req *proto.SelectNetworksRequ
 	}
 
 	routeManager := engine.GetRouteManager()
+	if routeManager == nil {
+		return nil, fmt.Errorf("no route manager")
+	}
+
 	routeSelector := routeManager.GetRouteSelector()
 	if req.GetAll() {
 		routeSelector.SelectAllRoutes()
@@ -165,6 +174,10 @@ func (s *Server) DeselectNetworks(_ context.Context, req *proto.SelectNetworksRe
 	}
 
 	routeManager := engine.GetRouteManager()
+	if routeManager == nil {
+		return nil, fmt.Errorf("no route manager")
+	}
+
 	routeSelector := routeManager.GetRouteSelector()
 	if req.GetAll() {
 		routeSelector.DeselectAllRoutes()
diff --git a/client/ui/font_windows.go b/client/ui/font_windows.go
index c37a5455..93b23a21 100644
--- a/client/ui/font_windows.go
+++ b/client/ui/font_windows.go
@@ -25,12 +25,12 @@ func (s *serviceClient) getWindowsFontFilePath() string {
 		fontFolder  = "C:/Windows/Fonts"
 		fontMapping = map[string]string{
 			"default":     "Segoeui.ttf",
-			"zh-CN":       "Msyh.ttc",
+			"zh-CN":       "Segoeui.ttf",
 			"am-ET":       "Ebrima.ttf",
 			"nirmala":     "Nirmala.ttf",
 			"chr-CHER-US": "Gadugi.ttf",
-			"zh-HK":       "Msjh.ttc",
-			"zh-TW":       "Msjh.ttc",
+			"zh-HK":       "Segoeui.ttf",
+			"zh-TW":       "Segoeui.ttf",
 			"ja-JP":       "Yugothm.ttc",
 			"km-KH":       "Leelawui.ttf",
 			"ko-KR":       "Malgun.ttf",



Test patch:
diff --git a/client/internal/peer/wg_watcher_test.go b/client/internal/peer/wg_watcher_test.go
index a5b9026a..47348594 100644
--- a/client/internal/peer/wg_watcher_test.go
+++ b/client/internal/peer/wg_watcher_test.go
@@ -43,7 +43,7 @@ func TestWGWatcher_EnableWgWatcher(t *testing.T) {
 
 	mlog := log.WithField("peer", "tet")
 	mocWgIface := &MocWgIface{}
-	watcher := NewWGWatcher(mlog, mocWgIface, "")
+	watcher := NewWGWatcher(mlog, mocWgIface, "", newStateDump(mlog))
 
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
@@ -72,7 +72,7 @@ func TestWGWatcher_ReEnable(t *testing.T) {
 
 	mlog := log.WithField("peer", "tet")
 	mocWgIface := &MocWgIface{}
-	watcher := NewWGWatcher(mlog, mocWgIface, "")
+	watcher := NewWGWatcher(mlog, mocWgIface, "", newStateDump(mlog))
 
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()