Skip to content

Commit

Permalink
[fix] WiFi Information Retrieval on macOS >= v14
Browse files Browse the repository at this point in the history
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
  • Loading branch information
stenya committed Mar 6, 2024
1 parent ebc4150 commit 4e33d1d
Show file tree
Hide file tree
Showing 78 changed files with 3,510 additions and 1,611 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ daemon/References/common/kem-helper/_out_linux/
daemon/References/common/kem-helper/_out_macos/
daemon/References/common/kem-helper/_out_windows/
daemon/References/Windows/v2ray/v2ray.exe
ui/build
ui/out/
ui/References/macOS/HelperProjects/launchAgent/_out
11 changes: 5 additions & 6 deletions cli/commands/wifi.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,22 +315,21 @@ func (c *CmdWiFi) printStatus(w *tabwriter.Writer) *tabwriter.Writer {
return boolToStrEx(&v, "Enabled", "Disabled", "")
}

curNetworkName := ""
curNetworkInfo := ""
curNet, err := _proto.GetWiFiCurrentNetwork()
if err != nil {
fmt.Println(err)
} else if len(curNet.Error) > 0 {
fmt.Printf("\n<<< ERROR: %s >>>\n\n", curNet.Error)
} else {
curNetworkName = fmt.Sprintf("%s", curNet.SSID)
curNetworkInfo := ""
curNetworkName := fmt.Sprintf("%s", curNet.SSID)
if curNet.IsInsecureNetwork {
curNetworkInfo = fmt.Sprintf(" (no encryption)")
}
fmt.Fprintf(w, "Connected WiFi network%s\t:\t%v\n", curNetworkInfo, curNetworkName)
}

wifiSettings := _proto.GetHelloResponse().DaemonSettings.WiFi
fmt.Fprintf(w, "Connected WiFi network%s\t:\t%v\n", curNetworkInfo, curNetworkName)

//fmt.Fprintf(w, "Allow background daemon to Apply WiFi Control settings\t:\t%v\n", boolToStr(wifiSettings.CanApplyInBackground))
if isInsecureNetworksSuppported() {
fmt.Fprintf(w, "Autoconnect on joining WiFi networks without encryption\t:\t%v\n", boolToStr(wifiSettings.CanApplyInBackground && wifiSettings.ConnectVPNOnInsecureNetwork))
}
Expand Down
1 change: 1 addition & 0 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/stretchr/testify v1.8.4 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Expand Down
2 changes: 2 additions & 0 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
Expand Down
13 changes: 7 additions & 6 deletions cli/protocol/client_private.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

"github.com/ivpn/desktop-app/cli/helpers"
"github.com/ivpn/desktop-app/daemon/logger"
daemonProtocol "github.com/ivpn/desktop-app/daemon/protocol"
"github.com/ivpn/desktop-app/daemon/protocol/types"
"github.com/ivpn/desktop-app/daemon/service/platform"
)
Expand All @@ -48,11 +49,11 @@ func (c *Client) ensureConnected() error {
return nil
}

func (c *Client) sendRecv(request interface{}, response interface{}) error {
func (c *Client) sendRecv(request daemonProtocol.ICommandBase, response interface{}) error {
return c.sendRecvTimeOut(request, response, c._defaultTimeout)
}

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

doJob := func() error {
var receiver *receiverChannel
Expand Down Expand Up @@ -112,12 +113,12 @@ func (c *Client) sendRecvTimeOut(request interface{}, response interface{}, time
return err
}

func (c *Client) sendRecvAny(request interface{}, waitingObjects ...interface{}) (data []byte, cmdBase types.CommandBase, err error) {
func (c *Client) sendRecvAny(request daemonProtocol.ICommandBase, waitingObjects ...interface{}) (data []byte, cmdBase types.CommandBase, err error) {
isIgnoreResponseIndex := true
return c.sendRecvAnyEx(request, isIgnoreResponseIndex, waitingObjects...)
}

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

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

func (c *Client) send(cmd interface{}, requestIdx int) error {
func (c *Client) send(cmd daemonProtocol.ICommandBase, requestIdx int) error {
cmdName := types.GetTypeName(cmd)

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

if err := types.Send(c._conn, cmd, requestIdx); err != nil {
if err := daemonProtocol.Send(c._conn, cmd, requestIdx); err != nil {
return fmt.Errorf("failed to send command '%s': %w", cmdName, err)
}
return nil
Expand Down
8 changes: 7 additions & 1 deletion daemon/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,13 @@ func launchService(secret uint64, startedOnPort chan<- int) {
activeProtocol = protocol

// initialize service
serv, err := service.CreateService(protocol, apiObj, updater, netDetector, wgKeysMgr, serviceEventsChan, systemLog)
serv, err := service.CreateService(protocol,
apiObj,
updater,
netDetector,
wgKeysMgr,
serviceEventsChan,
systemLog)
if err != nil {
log.Panic("Failed to initialize service:", err)
}
Expand Down
28 changes: 28 additions & 0 deletions daemon/oshelpers/macos/darwinhelpers/osversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package darwinhelpers

import (
"fmt"
"strconv"
"strings"

"golang.org/x/sys/unix"
)

func GetOsMajorVersion() (int, error) {
// Checking macOS version
var uts unix.Utsname
if err := unix.Uname(&uts); err != nil {
return 0, fmt.Errorf("Can not obtain macOS version: %w", err)
}
release := unix.ByteSliceToString(uts.Release[:])
dotPos := strings.Index(release, ".")
if dotPos == -1 {
return 0, fmt.Errorf("Can not obtain macOS version")
}
major := release[:dotPos]
majorVersion, err := strconv.Atoi(major)
if err != nil {
return 0, fmt.Errorf("Can not obtain macOS version: %w", err)
}
return majorVersion, nil
}
112 changes: 112 additions & 0 deletions daemon/protocol/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// Daemon for IVPN Client Desktop
// https://github.com/ivpn/desktop-app
//
// Created by Stelnykovych Alexandr.
// Copyright (c) 2023 IVPN Limited.
//
// This file is part of the Daemon for IVPN Client Desktop.
//
// The Daemon for IVPN Client Desktop is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// The Daemon for IVPN Client Desktop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License
// along with the Daemon for IVPN Client Desktop. If not, see <https://www.gnu.org/licenses/>.
//

package protocol

import (
"runtime"
"time"

"github.com/ivpn/desktop-app/daemon/protocol/types"
"github.com/ivpn/desktop-app/daemon/service/dns"
"github.com/ivpn/desktop-app/daemon/service/platform"
"github.com/ivpn/desktop-app/daemon/version"
"github.com/ivpn/desktop-app/daemon/vpn"
)

func (p *Protocol) createSettingsResponse() *types.SettingsResp {
prefs := p._service.Preferences()
return &types.SettingsResp{
IsAutoconnectOnLaunch: prefs.IsAutoconnectOnLaunch,
IsAutoconnectOnLaunchDaemon: prefs.IsAutoconnectOnLaunchDaemon,
UserDefinedOvpnFile: platform.OpenvpnUserParamsFile(),
UserPrefs: prefs.UserPrefs,
WiFi: prefs.WiFiControl,
IsLogging: prefs.IsLogging,
AntiTracker: p._service.GetAntiTrackerStatus(),
// TODO: implement the rest of daemon settings
}
}

func (p *Protocol) createHelloResponse() *types.HelloResp {
prefs := p._service.Preferences()

disabledFuncs := p._service.GetDisabledFunctions()

dnsOverHttps, dnsOverTls, err := dns.EncryptionAbilities()
if err != nil {
dnsOverHttps = false
dnsOverTls = false
log.Error(err)
}

// send back Hello message with account session info
helloResp := types.HelloResp{
ParanoidMode: types.ParanoidModeStatus{IsEnabled: p._eaa.IsEnabled()},
Version: version.Version(),
ProcessorArch: runtime.GOARCH,
Session: types.CreateSessionResp(prefs.Session),
Account: prefs.Account,
SettingsSessionUUID: prefs.SettingsSessionUUID,
DisabledFunctions: disabledFuncs,
Dns: types.DnsAbilities{
CanUseDnsOverTls: dnsOverTls,
CanUseDnsOverHttps: dnsOverHttps,
},
DaemonSettings: *p.createSettingsResponse(),
}
return &helloResp
}

func (p *Protocol) createConnectedResponse(state vpn.StateInfo) *types.ConnectedResp {
ipv6 := ""
if state.ClientIPv6 != nil {
ipv6 = state.ClientIPv6.String()
}

pausedTill := p._service.PausedTill()
pausedTillStr := pausedTill.Format(time.RFC3339)
if pausedTill.IsZero() {
pausedTillStr = ""
}

manualDns := dns.GetLastManualDNS()

ret := &types.ConnectedResp{
TimeSecFrom1970: state.Time,
ClientIP: state.ClientIP.String(),
ClientIPv6: ipv6,
ServerIP: state.ServerIP.String(),
ServerPort: state.ServerPort,
VpnType: state.VpnType,
ExitHostname: state.ExitHostname,
Dns: types.DnsStatus{Dns: manualDns, AntiTrackerStatus: p._service.GetAntiTrackerStatus()},
IsTCP: state.IsTCP,
Mtu: state.Mtu,
V2RayProxy: state.V2RayProxy,
Obfsproxy: state.Obfsproxy,
IsPaused: p._service.IsPaused(),
PausedTill: pausedTillStr,
}

return ret
}
31 changes: 13 additions & 18 deletions daemon/protocol/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ type Service interface {
WireGuardGenerateKeys(updateIfNecessary bool) error
WireGuardSetKeysRotationInterval(interval int64)

GetWiFiCurrentState() wifiNotifier.WifiInfo
GetWiFiAvailableNetworks() []string
GetWiFiCurrentState() (wifiNotifier.WifiInfo, error)
GetWiFiAvailableNetworks() ([]string, error)

GetDiagnosticLogs() (logActive string, logPrevSession string, extraInfo string, err error)
}
Expand Down Expand Up @@ -394,6 +394,7 @@ func (p *Protocol) processRequest(conn net.Conn, message string) {
}
}
}

log.Info("[<--] ", p.connLogID(conn), reqCmd.Command, fmt.Sprintf(" [%d]%s", reqCmd.Idx, cmdExtraInfo))

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

case "WiFiAvailableNetworks":
networks := p._service.GetWiFiAvailableNetworks()
nets := make([]types.WiFiNetworkInfo, 0, len(networks))
for _, ssid := range networks {
nets = append(nets, types.WiFiNetworkInfo{SSID: ssid})
}

p.notifyClients(&types.WiFiAvailableNetworksResp{Networks: nets})
p.sendResponse(conn, &types.EmptyResp{}, reqCmd.Idx)

case "KillSwitchGetStatus":
if status, err := p._service.KillSwitchState(); err != nil {
p.sendErrorResponse(conn, reqCmd, err)
Expand Down Expand Up @@ -1078,11 +1069,16 @@ func (p *Protocol) processRequest(conn net.Conn, message string) {
p.sendResponse(conn, &types.InstalledAppsResp{Apps: apps}, reqCmd.Idx)

case "WiFiCurrentNetwork":
// sending WIFI info
wifi := p._service.GetWiFiCurrentState()
p.sendResponse(conn, &types.WiFiCurrentNetworkResp{
SSID: wifi.SSID,
IsInsecureNetwork: wifi.IsInsecure}, reqCmd.Idx)
p.OnWiFiChanged(p._service.GetWiFiCurrentState())

case "WiFiAvailableNetworks":
networks, _ := p._service.GetWiFiAvailableNetworks()
nets := make([]types.WiFiNetworkInfo, 0, len(networks))
for _, ssid := range networks {
nets = append(nets, types.WiFiNetworkInfo{SSID: ssid})
}
p.notifyClients(&types.WiFiAvailableNetworksResp{Networks: nets})
p.sendResponse(conn, &types.EmptyResp{}, reqCmd.Idx)

case "WiFiSettings":
var r types.WiFiSettings
Expand All @@ -1095,7 +1091,6 @@ func (p *Protocol) processRequest(conn net.Conn, message string) {
return
}
p.sendResponse(conn, &types.EmptyResp{}, reqCmd.Idx)

// notify all clients about changed wifi settings
p.notifyClients(p.createHelloResponse())

Expand Down
11 changes: 8 additions & 3 deletions daemon/protocol/protocol_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,15 @@ func (p *Protocol) OnKillSwitchStateChanged() {
}

// OnWiFiChanged - handler of WiFi status change. Notifying clients.
func (p *Protocol) OnWiFiChanged(info wifiNotifier.WifiInfo) {
p.notifyClients(&types.WiFiCurrentNetworkResp{
func (p *Protocol) OnWiFiChanged(info wifiNotifier.WifiInfo, err error) {
msg := &types.WiFiCurrentNetworkResp{
SSID: info.SSID,
IsInsecureNetwork: info.IsInsecure})
IsInsecureNetwork: info.IsInsecure,
}
if err != nil {
msg.Error = err.Error()
}
p.notifyClients(msg)
}

// OnPingStatus - servers ping status
Expand Down
Loading

0 comments on commit 4e33d1d

Please sign in to comment.