Skip to content

Commit 4e33d1d

Browse files
committed
[fix] WiFi Information Retrieval on macOS >= v14
As of macOS 14 (Darwin v23.x.x), it is no longer possible to obtain WiFi SSID for background daemons. In modern macOS versions, to retrieve WiFi network information, the application must have Location Services privileges. However, these privileges are not available for privileged LaunchDaemons. To overcome this limitation, we use a separate **LaunchAgent** *(ui/References/macOS/HelperProjects/launchAgent)* that is installed in the user environment by the UI app. We employ XPC communication between the Agent and the Daemon. The Daemon acts as a "server", waiting for connections from Agents. Agents provide the Daemon with WiFi information upon request. The Electron UI app uses a custom **NAPI module** *(ui/addons/wifi-info-macos)* to install/uninstall the LaunchAgent and to request Location Services permission from the OS. ivpn/desktop-app-shadow#150
1 parent ebc4150 commit 4e33d1d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+3510
-1611
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ daemon/References/common/kem-helper/_out_linux/
1212
daemon/References/common/kem-helper/_out_macos/
1313
daemon/References/common/kem-helper/_out_windows/
1414
daemon/References/Windows/v2ray/v2ray.exe
15+
ui/build
1516
ui/out/
17+
ui/References/macOS/HelperProjects/launchAgent/_out

cli/commands/wifi.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -315,22 +315,21 @@ func (c *CmdWiFi) printStatus(w *tabwriter.Writer) *tabwriter.Writer {
315315
return boolToStrEx(&v, "Enabled", "Disabled", "")
316316
}
317317

318-
curNetworkName := ""
319-
curNetworkInfo := ""
320318
curNet, err := _proto.GetWiFiCurrentNetwork()
321319
if err != nil {
322320
fmt.Println(err)
321+
} else if len(curNet.Error) > 0 {
322+
fmt.Printf("\n<<< ERROR: %s >>>\n\n", curNet.Error)
323323
} else {
324-
curNetworkName = fmt.Sprintf("%s", curNet.SSID)
324+
curNetworkInfo := ""
325+
curNetworkName := fmt.Sprintf("%s", curNet.SSID)
325326
if curNet.IsInsecureNetwork {
326327
curNetworkInfo = fmt.Sprintf(" (no encryption)")
327328
}
329+
fmt.Fprintf(w, "Connected WiFi network%s\t:\t%v\n", curNetworkInfo, curNetworkName)
328330
}
329331

330332
wifiSettings := _proto.GetHelloResponse().DaemonSettings.WiFi
331-
fmt.Fprintf(w, "Connected WiFi network%s\t:\t%v\n", curNetworkInfo, curNetworkName)
332-
333-
//fmt.Fprintf(w, "Allow background daemon to Apply WiFi Control settings\t:\t%v\n", boolToStr(wifiSettings.CanApplyInBackground))
334333
if isInsecureNetworksSuppported() {
335334
fmt.Fprintf(w, "Autoconnect on joining WiFi networks without encryption\t:\t%v\n", boolToStr(wifiSettings.CanApplyInBackground && wifiSettings.ConnectVPNOnInsecureNetwork))
336335
}

cli/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/pmezard/go-difflib v1.0.0 // indirect
2020
github.com/rivo/uniseg v0.4.4 // indirect
2121
github.com/stretchr/testify v1.8.4 // indirect
22+
golang.org/x/sync v0.6.0 // indirect
2223
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
2324
gopkg.in/yaml.v3 v3.0.1 // indirect
2425
)

cli/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
2020
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
2121
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
2222
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
23+
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
24+
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
2325
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
2426
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2527
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=

cli/protocol/client_private.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232

3333
"github.com/ivpn/desktop-app/cli/helpers"
3434
"github.com/ivpn/desktop-app/daemon/logger"
35+
daemonProtocol "github.com/ivpn/desktop-app/daemon/protocol"
3536
"github.com/ivpn/desktop-app/daemon/protocol/types"
3637
"github.com/ivpn/desktop-app/daemon/service/platform"
3738
)
@@ -48,11 +49,11 @@ func (c *Client) ensureConnected() error {
4849
return nil
4950
}
5051

51-
func (c *Client) sendRecv(request interface{}, response interface{}) error {
52+
func (c *Client) sendRecv(request daemonProtocol.ICommandBase, response interface{}) error {
5253
return c.sendRecvTimeOut(request, response, c._defaultTimeout)
5354
}
5455

55-
func (c *Client) sendRecvTimeOut(request interface{}, response interface{}, timeout time.Duration) error {
56+
func (c *Client) sendRecvTimeOut(request daemonProtocol.ICommandBase, response interface{}, timeout time.Duration) error {
5657

5758
doJob := func() error {
5859
var receiver *receiverChannel
@@ -112,12 +113,12 @@ func (c *Client) sendRecvTimeOut(request interface{}, response interface{}, time
112113
return err
113114
}
114115

115-
func (c *Client) sendRecvAny(request interface{}, waitingObjects ...interface{}) (data []byte, cmdBase types.CommandBase, err error) {
116+
func (c *Client) sendRecvAny(request daemonProtocol.ICommandBase, waitingObjects ...interface{}) (data []byte, cmdBase types.CommandBase, err error) {
116117
isIgnoreResponseIndex := true
117118
return c.sendRecvAnyEx(request, isIgnoreResponseIndex, waitingObjects...)
118119
}
119120

120-
func (c *Client) sendRecvAnyEx(request interface{}, isIgnoreResponseIndex bool, waitingObjects ...interface{}) (data []byte, cmdBase types.CommandBase, err error) {
121+
func (c *Client) sendRecvAnyEx(request daemonProtocol.ICommandBase, isIgnoreResponseIndex bool, waitingObjects ...interface{}) (data []byte, cmdBase types.CommandBase, err error) {
121122

122123
doJob := func() (data []byte, cmdBase types.CommandBase, err error) {
123124
var receiver *receiverChannel
@@ -177,7 +178,7 @@ func (c *Client) sendRecvAnyEx(request interface{}, isIgnoreResponseIndex bool,
177178
return data, cmdBase, err
178179
}
179180

180-
func (c *Client) send(cmd interface{}, requestIdx int) error {
181+
func (c *Client) send(cmd daemonProtocol.ICommandBase, requestIdx int) error {
181182
cmdName := types.GetTypeName(cmd)
182183

183184
logger.Info("--> ", cmdName)
@@ -186,7 +187,7 @@ func (c *Client) send(cmd interface{}, requestIdx int) error {
186187
return err
187188
}
188189

189-
if err := types.Send(c._conn, cmd, requestIdx); err != nil {
190+
if err := daemonProtocol.Send(c._conn, cmd, requestIdx); err != nil {
190191
return fmt.Errorf("failed to send command '%s': %w", cmdName, err)
191192
}
192193
return nil

daemon/launcher.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,13 @@ func launchService(secret uint64, startedOnPort chan<- int) {
283283
activeProtocol = protocol
284284

285285
// initialize service
286-
serv, err := service.CreateService(protocol, apiObj, updater, netDetector, wgKeysMgr, serviceEventsChan, systemLog)
286+
serv, err := service.CreateService(protocol,
287+
apiObj,
288+
updater,
289+
netDetector,
290+
wgKeysMgr,
291+
serviceEventsChan,
292+
systemLog)
287293
if err != nil {
288294
log.Panic("Failed to initialize service:", err)
289295
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package darwinhelpers
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
8+
"golang.org/x/sys/unix"
9+
)
10+
11+
func GetOsMajorVersion() (int, error) {
12+
// Checking macOS version
13+
var uts unix.Utsname
14+
if err := unix.Uname(&uts); err != nil {
15+
return 0, fmt.Errorf("Can not obtain macOS version: %w", err)
16+
}
17+
release := unix.ByteSliceToString(uts.Release[:])
18+
dotPos := strings.Index(release, ".")
19+
if dotPos == -1 {
20+
return 0, fmt.Errorf("Can not obtain macOS version")
21+
}
22+
major := release[:dotPos]
23+
majorVersion, err := strconv.Atoi(major)
24+
if err != nil {
25+
return 0, fmt.Errorf("Can not obtain macOS version: %w", err)
26+
}
27+
return majorVersion, nil
28+
}

daemon/protocol/helpers.go

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// Daemon for IVPN Client Desktop
3+
// https://github.com/ivpn/desktop-app
4+
//
5+
// Created by Stelnykovych Alexandr.
6+
// Copyright (c) 2023 IVPN Limited.
7+
//
8+
// This file is part of the Daemon for IVPN Client Desktop.
9+
//
10+
// The Daemon for IVPN Client Desktop is free software: you can redistribute it and/or
11+
// modify it under the terms of the GNU General Public License as published by the Free
12+
// Software Foundation, either version 3 of the License, or (at your option) any later version.
13+
//
14+
// The Daemon for IVPN Client Desktop is distributed in the hope that it will be useful,
15+
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16+
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17+
// details.
18+
//
19+
// You should have received a copy of the GNU General Public License
20+
// along with the Daemon for IVPN Client Desktop. If not, see <https://www.gnu.org/licenses/>.
21+
//
22+
23+
package protocol
24+
25+
import (
26+
"runtime"
27+
"time"
28+
29+
"github.com/ivpn/desktop-app/daemon/protocol/types"
30+
"github.com/ivpn/desktop-app/daemon/service/dns"
31+
"github.com/ivpn/desktop-app/daemon/service/platform"
32+
"github.com/ivpn/desktop-app/daemon/version"
33+
"github.com/ivpn/desktop-app/daemon/vpn"
34+
)
35+
36+
func (p *Protocol) createSettingsResponse() *types.SettingsResp {
37+
prefs := p._service.Preferences()
38+
return &types.SettingsResp{
39+
IsAutoconnectOnLaunch: prefs.IsAutoconnectOnLaunch,
40+
IsAutoconnectOnLaunchDaemon: prefs.IsAutoconnectOnLaunchDaemon,
41+
UserDefinedOvpnFile: platform.OpenvpnUserParamsFile(),
42+
UserPrefs: prefs.UserPrefs,
43+
WiFi: prefs.WiFiControl,
44+
IsLogging: prefs.IsLogging,
45+
AntiTracker: p._service.GetAntiTrackerStatus(),
46+
// TODO: implement the rest of daemon settings
47+
}
48+
}
49+
50+
func (p *Protocol) createHelloResponse() *types.HelloResp {
51+
prefs := p._service.Preferences()
52+
53+
disabledFuncs := p._service.GetDisabledFunctions()
54+
55+
dnsOverHttps, dnsOverTls, err := dns.EncryptionAbilities()
56+
if err != nil {
57+
dnsOverHttps = false
58+
dnsOverTls = false
59+
log.Error(err)
60+
}
61+
62+
// send back Hello message with account session info
63+
helloResp := types.HelloResp{
64+
ParanoidMode: types.ParanoidModeStatus{IsEnabled: p._eaa.IsEnabled()},
65+
Version: version.Version(),
66+
ProcessorArch: runtime.GOARCH,
67+
Session: types.CreateSessionResp(prefs.Session),
68+
Account: prefs.Account,
69+
SettingsSessionUUID: prefs.SettingsSessionUUID,
70+
DisabledFunctions: disabledFuncs,
71+
Dns: types.DnsAbilities{
72+
CanUseDnsOverTls: dnsOverTls,
73+
CanUseDnsOverHttps: dnsOverHttps,
74+
},
75+
DaemonSettings: *p.createSettingsResponse(),
76+
}
77+
return &helloResp
78+
}
79+
80+
func (p *Protocol) createConnectedResponse(state vpn.StateInfo) *types.ConnectedResp {
81+
ipv6 := ""
82+
if state.ClientIPv6 != nil {
83+
ipv6 = state.ClientIPv6.String()
84+
}
85+
86+
pausedTill := p._service.PausedTill()
87+
pausedTillStr := pausedTill.Format(time.RFC3339)
88+
if pausedTill.IsZero() {
89+
pausedTillStr = ""
90+
}
91+
92+
manualDns := dns.GetLastManualDNS()
93+
94+
ret := &types.ConnectedResp{
95+
TimeSecFrom1970: state.Time,
96+
ClientIP: state.ClientIP.String(),
97+
ClientIPv6: ipv6,
98+
ServerIP: state.ServerIP.String(),
99+
ServerPort: state.ServerPort,
100+
VpnType: state.VpnType,
101+
ExitHostname: state.ExitHostname,
102+
Dns: types.DnsStatus{Dns: manualDns, AntiTrackerStatus: p._service.GetAntiTrackerStatus()},
103+
IsTCP: state.IsTCP,
104+
Mtu: state.Mtu,
105+
V2RayProxy: state.V2RayProxy,
106+
Obfsproxy: state.Obfsproxy,
107+
IsPaused: p._service.IsPaused(),
108+
PausedTill: pausedTillStr,
109+
}
110+
111+
return ret
112+
}

daemon/protocol/protocol.go

+13-18
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ type Service interface {
139139
WireGuardGenerateKeys(updateIfNecessary bool) error
140140
WireGuardSetKeysRotationInterval(interval int64)
141141

142-
GetWiFiCurrentState() wifiNotifier.WifiInfo
143-
GetWiFiAvailableNetworks() []string
142+
GetWiFiCurrentState() (wifiNotifier.WifiInfo, error)
143+
GetWiFiAvailableNetworks() ([]string, error)
144144

145145
GetDiagnosticLogs() (logActive string, logPrevSession string, extraInfo string, err error)
146146
}
@@ -394,6 +394,7 @@ func (p *Protocol) processRequest(conn net.Conn, message string) {
394394
}
395395
}
396396
}
397+
397398
log.Info("[<--] ", p.connLogID(conn), reqCmd.Command, fmt.Sprintf(" [%d]%s", reqCmd.Idx, cmdExtraInfo))
398399

399400
isDoSkipParanoidMode := func(commandName string) bool {
@@ -613,16 +614,6 @@ func (p *Protocol) processRequest(conn net.Conn, message string) {
613614
}
614615
p.sendResponse(conn, &types.CheckAccessiblePortsResponse{Ports: accessiblePorts}, req.Idx)
615616

616-
case "WiFiAvailableNetworks":
617-
networks := p._service.GetWiFiAvailableNetworks()
618-
nets := make([]types.WiFiNetworkInfo, 0, len(networks))
619-
for _, ssid := range networks {
620-
nets = append(nets, types.WiFiNetworkInfo{SSID: ssid})
621-
}
622-
623-
p.notifyClients(&types.WiFiAvailableNetworksResp{Networks: nets})
624-
p.sendResponse(conn, &types.EmptyResp{}, reqCmd.Idx)
625-
626617
case "KillSwitchGetStatus":
627618
if status, err := p._service.KillSwitchState(); err != nil {
628619
p.sendErrorResponse(conn, reqCmd, err)
@@ -1078,11 +1069,16 @@ func (p *Protocol) processRequest(conn net.Conn, message string) {
10781069
p.sendResponse(conn, &types.InstalledAppsResp{Apps: apps}, reqCmd.Idx)
10791070

10801071
case "WiFiCurrentNetwork":
1081-
// sending WIFI info
1082-
wifi := p._service.GetWiFiCurrentState()
1083-
p.sendResponse(conn, &types.WiFiCurrentNetworkResp{
1084-
SSID: wifi.SSID,
1085-
IsInsecureNetwork: wifi.IsInsecure}, reqCmd.Idx)
1072+
p.OnWiFiChanged(p._service.GetWiFiCurrentState())
1073+
1074+
case "WiFiAvailableNetworks":
1075+
networks, _ := p._service.GetWiFiAvailableNetworks()
1076+
nets := make([]types.WiFiNetworkInfo, 0, len(networks))
1077+
for _, ssid := range networks {
1078+
nets = append(nets, types.WiFiNetworkInfo{SSID: ssid})
1079+
}
1080+
p.notifyClients(&types.WiFiAvailableNetworksResp{Networks: nets})
1081+
p.sendResponse(conn, &types.EmptyResp{}, reqCmd.Idx)
10861082

10871083
case "WiFiSettings":
10881084
var r types.WiFiSettings
@@ -1095,7 +1091,6 @@ func (p *Protocol) processRequest(conn net.Conn, message string) {
10951091
return
10961092
}
10971093
p.sendResponse(conn, &types.EmptyResp{}, reqCmd.Idx)
1098-
10991094
// notify all clients about changed wifi settings
11001095
p.notifyClients(p.createHelloResponse())
11011096

daemon/protocol/protocol_handlers.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,15 @@ func (p *Protocol) OnKillSwitchStateChanged() {
6464
}
6565

6666
// OnWiFiChanged - handler of WiFi status change. Notifying clients.
67-
func (p *Protocol) OnWiFiChanged(info wifiNotifier.WifiInfo) {
68-
p.notifyClients(&types.WiFiCurrentNetworkResp{
67+
func (p *Protocol) OnWiFiChanged(info wifiNotifier.WifiInfo, err error) {
68+
msg := &types.WiFiCurrentNetworkResp{
6969
SSID: info.SSID,
70-
IsInsecureNetwork: info.IsInsecure})
70+
IsInsecureNetwork: info.IsInsecure,
71+
}
72+
if err != nil {
73+
msg.Error = err.Error()
74+
}
75+
p.notifyClients(msg)
7176
}
7277

7378
// OnPingStatus - servers ping status

0 commit comments

Comments
 (0)