Macのネットワーク環境を、WiFi接続先により自動で切り替える

MacOSはネットワーク周りの設定をまとめて管理できる、 **「ネットワーク環境」** と言う仕組みを持っています。これは繋ぐネットワーク先によって手動で設定する必要がある場合に、簡単に切り替えることができて **一応便利** です。
ところが何故か接続先に応じて自動でネットワーク環境が切り替わる機能がありません。そこで以下のページにあるスクリプトを元に手直しした物を使わせて頂いています。

https://qiita.com/naname/items/3e69efb187be22109c69

時が経ちオリジナルのままでは動作が怪しいところも出てきたので、何かの参考になればと思い私のバージョンを公開します。

# 動作環境
- MacBook Pro 14inch(2021)/macOS Monterey(12.4)
- MacBook 12inch(2016)/macOS Mojave

# ソース

### `wifienv_selector.sh`
（オリジナル：`wifi_environment_auto_selector.sh`）

```shell:wifienv_selector.sh
#!/bin/sh

# Set PATH strictly to avoid security hole
PATH=/usr/bin:/bin:/usr/sbin:/sbin:

#
# Configuration

logfile=${HOME}/logs/wifienv.log
ssidfile=${HOME}/.wifienv_ssid
wifiIF="en0"

log_enabled=1
# 0: do not log messages, 1: log messages

use_alert=1
# for sending messages to user,
#   0: use notification, 1: use alert dialog

#
# functions

plog(){
    if [ $log_enabled -ne 0 ]; then
	    /bin/echo -n `date +'%Y/%m/%d %H:%M:%S'`" " >> $logfile
	    /bin/echo $1 >> $logfile
    fi
}

last_ssid(){
    if [ -r $ssidfile ]; then
	    read ssid < $ssidfile
	    echo $ssid
    else
	    echo ""
    fi
}

save_ssid(){
    echo $1 > $ssidfile
}

dialog(){
    if [ $use_alert -ne 0 ]; then
	    osascript -e 'display alert "'"${1}"'" message "'"${2}"'" as critical giving up after 5' >/dev/null 2>&1
    else
	    osascript -e 'display notification "'"${2}"'" with title "'"${1}"'" subtitle "'"${3}"'"' >/dev/null 2>&1
    fi
}

switch2loc() {
    newloc=$1
    newssid=$2
    curloc=`networksetup -getcurrentlocation`
    lastssid=`last_ssid`
    save_ssid ${2}

    if [ x${lastssid} = x${newssid} ]; then
	    plog "SSID is not changed: ${newssid}"
	    return
    fi
    
    plog "SSID is changed: ${lastssid} -> ${newssid}"
    
    if [ x${curloc} = x${newloc} ]; then
	    plog "location is identical: (${curloc})"
	    dialog "SSIDが“${2}”になりました" \
	           "ネットワーク環境（${curloc}）は変更ありません"
	    return
    fi
    
    if networksetup -switchtolocation ${newloc}; then
	    plog "location is changed: ${curloc} -> ${newloc}"
	    dialog "SSIDが“${newssid}”になりました" \
	           "ネットワーク環境を“${newloc}”に変更しました" 
    else
	    plog "failed to switch location from {$curloc} to ${newloc}"
    fi
}

IsWiFiPwrOff() {
    networksetup -getairportpower $1 | awk 'tolower($4) ~ /on/ { exit 1 }'
}

getSSID() {
    count=5

    while [ $count -gt 0 ]; do
		let count=count-1
		if ssid=`networksetup -getairportnetwork ${wifiIF} |\
                 awk '/^Current/ { print $NF; exit 0 }
       	   	 	  		   	     { exit 1 }'`; then
	        break
	    fi
	    sleep 1
    done

    if [ $count -le 0 ]; then
	    plog "failed to get SSID"
	    dialog "エラー" "SSIDが取得できません"
	    echo ""
    else
	    echo $ssid
    fi
}

#
# main

# check WiFi Power
if IsWiFiPwrOff ${wifiIF} ; then
    plog "WiFi is off"
    exit 0
fi

ssid=`getSSID`

case ${ssid} in
    "MyHome_a" | "MyHome_bg")
	    switch2loc "自宅" ${ssid}
	    ;;
    "Office_8F" | "Office_9F")
        switch2loc "Office" ${ssid}
    *)
	    switch2loc "自動" ${ssid}
	    ;;
esac

```

### `${HOME}/Library/LaunchAgents/wifienv_selector.plist`
（旧：`wifi_environment_auto_selector.plist`）

```xml:wifienv_selector.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>wifienv_selector</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/yourname/bin/wifienv_selector.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>WatchPaths</key>
    <array>
        <!-- string>/var/run/resolv.conf</string -->
        <string>/Library/Preferences/com.apple.wifi.known-networks.plist</string>
    </array>
</dict>
</plist>
```

:::note info
`<key>ProgramArguments</key>`内の`<string>/Users/yourname/bin/wifienv_selector.sh</string>`はwifienv_selector.shのpathです。適宜変更して下さい。
:::

### `/etc/newsyslog.d/wifienv.conf`
```conf:/etc/newsyslog.d/wifienv.conf
# logfilename: $HOME/logs/wifienv.log
# [owner:group]: yourname:staff
# mode: 640
# count: 12 (1 year)
# size: *
# when: $M1D0 (0:00 on 1st day of month)
# flags: J (compressed by bzip2)
/Users/yourname/logs/wifienv.log yourname:staff 640 12 * $M1D0 J
```
:::note info
logfilename（`/Users/yourname/logs/wifienv.log`）や`[owner:group]`（`yourname:staff`）は適宜変更して下さい。
:::

# 使い方
上記ファイルを用意した後、シェルで 
```shell:
$ launchctl load ~/Library/LaunchAgents/wifienv_selector.plist
```

を実行します。
なお二度目以降は、以下の通り一旦unloadしてからloadします。

```shell: 
$ launchctl unload ~/Library/LaunchAgents/wifienv_selector.plist
$ launchctl load ~/Library/LaunchAgents/wifienv_selector.plist
```

# 主な変更点

## `wifienv_selector.sh ` 
（オリジナル：`wifi_environment_auto_selector.sh`）

#### 通知はalert dialogで行う
私は良くnotificationを見落としてしまいます。また集中モードによっては、表示されない事もあります。その為alert dialogで通知するようにしました。なおOKボタンを押さずとも、5秒で閉じます。
notificationの方がお好みの方は、`use_alert=1`を`0`に変更して下さい。

#### SSIDのみの変更でも通知する
切り替わったのに気が付かないのは、やはり気持ち悪いので。

#### ログを取る
正常に動作しているのか不安になった時に、確認したい事もあります。不要な場合は`log_enabled=1`を`0`にして下さい。ファイル名は`logfile`で設定されています。

#### その他
- `scselect`でネットワーク環境を変更していたのを、`networksetup -switchtolocation`で変更するようにしました。

- `networksetup -getairportnetwork`でSSIDを得ていますが、タイミングによっては失敗する事があったので、修正しました。これはコマンドの出力が以下のように変化する為です。

（変更前）　`Current Wi-Fi Network: <last ssid>`
　　↓
（一旦切断） `You are not associated with an AirPort network.`
　　↓
（再接続） 　`Current Wi-Fi Network: <new ssid>`


## `wifienv_selector.plist`
（オリジナル：`wifi_environment_auto_selector.plist`）

#### `WatchPaths`を変更... しない方が良い
以下は接続先のすべてでDHCPが用意されている場合です。
従ってDHCPが用意されていないネットワークにつなぐ場合には機能しません。この場合は`WatchPaths`を`/Library/Preferences/com.apple.wifi.known-networks.plist`にすべきです。

> OSのバージョンによって、WiFi I/Fが変わった時に変更されるファイルは異なるようです。しかしUNIX系OS標準の`/etc/resolv.conf`なら、DHCPでnameserverが変わる度に必ず更新される筈です。
> 調べてみたところ、
> ```shell-session:
> $ LC_ALL=C \ls -l /etc/resolv.conf
> lrwxr-xr-x  1 root  wheel  22 May 10 06:30  /etc/resolv.conf -> ../var/run/resolv.conf
> ```
>と`/var/run/resolv.conf`へのシンボリックリンクになっていました。この`/var/run/resolv.conf`を`WatchPaths`に設定した所、問題なく動いています。

# 課題など

### Ethernetを扱えるか？
未調査ですが、Ethernetを接続した場合でもこれで扱えるようにしたい物です。

### firewallの切り替え
`wifienv_selector.sh`の最後の`case`文の所を改変すれば、firewallの制御もできます。MacOS純正のfirewallなら、コマンドラインから切り替える事ができそうです。

> [こちら](https://idkblogs.com/others/68/Turn-On-Off-firewall-of-your-mac-using-terminal-CMD)などによれば、firewallをon/offするには：
> ```shell-session:
> # firewall OFF
> $ sudo sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 0
> # firewall ON
> $ sudo sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1
>```
> なのだそうです。そこで `/Library/Preferences/com.apple.alf`と言うファイルが無いか調べてみると、
>```shell-session:
>$ ls /Library/Preferences/com.apple.alf*
> /Library/Preferences/com.apple.alf.plist
> $ file /Library/Preferences/com.apple.alf.plist
> /Library/Preferences/com.apple.alf.plist: Apple binary property list
>```
>と`/Library/Preferences/com.apple.alf.plist`と言うproperty list ファイルが見つかりました。これを `plutil -p`で覗いてみると、firewallの設定らしきものが書かれています。
> よってこのファイルを複数用意し、差し替える事ができるのでは無いかと予想しています。

ただ私が使っているAnti Virus（ESET Cyber Security Pro）には接続先により動作を変えられるfirewallがあるので、実際に作ってはいません。予想される問題としてはfirewallを設定変更するにはroot権限が必要になるので、security holeにならないように注意する必要がある所でしょうか。

# 参考
- [Wifi接続先に応じてネットワーク環境設定を自動で切り替える - Qiita](https://qiita.com/naname/items/3e69efb187be22109c69)
- Mac Automation Scripting Guide
    - [Mac Automation Scripting Guide: Displaying Dialogs and Alerts](https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/DisplayDialogsandAlerts.html#//apple_ref/doc/uid/TP40016239-CH15-SW1)
    - [Mac Automation Scripting Guide: Displaying Notifications](https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/DisplayNotifications.html#//apple_ref/doc/uid/TP40016239-CH61-SW1)
- MacOSのマニュアル
    - `man networksetup`
    - `man scselect`
    - `man osascript`