@sunjiehao/zyhy-network 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # ZYHY-Network
2
+
3
+ 当前版本:`V0.2.1`
4
+
5
+ 办公时不用再在「网线」和「公司 Wi-Fi」之间来回切。
6
+
7
+ - 看网页、刷视频、走代理:用网线
8
+ - 打开 OA、邮箱、iHR:自动走公司 Wi-Fi `ZYHY-Private`
9
+
10
+ 两个可以同时开着。
11
+
12
+ ## 你需要
13
+
14
+ 1. 一台 Mac
15
+ 2. 插上公司网线
16
+ 3. Wi-Fi 连上 **ZYHY-Private**(不要连别的公司 Wi-Fi)
17
+
18
+ ## 怎么装
19
+
20
+ 打开「终端」,把下面三行粘贴进去,回车,输入开机密码:
21
+
22
+ ```bash
23
+ git clone https://github.com/lampardrodgers/ZYHY-Network.git
24
+ cd ZYHY-Network
25
+ sudo ./install.sh
26
+ ```
27
+
28
+ 装完后:
29
+
30
+ 1. 用 **新标签页** 打开 OA、邮箱(不要刷新刚才失败的那一页)
31
+ 2. 如果用的是 Chrome:设置 → 隐私和安全 → 安全 → 关掉「使用安全 DNS」
32
+
33
+ 能打开就行,不用再配域名。
34
+
35
+ ## 用的时候注意
36
+
37
+ - 网线要插着,Wi-Fi 必须是 `ZYHY-Private`,分流才会开
38
+ - 拔掉网线、或换成别的 Wi-Fi:全部按系统原来的方式上网,内网入口可能会打不开
39
+ - 插回网线并连回 `ZYHY-Private`:等几秒会自动恢复
40
+
41
+ 后台会监听 macOS 的网络状态变化,只在有线网、公司 Wi-Fi、网关或 DNS
42
+ 等实际发生变化时更新分流配置。另有每 5 分钟一次的低频校验,用来补偿
43
+ 偶尔漏掉的系统事件和内网域名解析变化;配置内容没变时不会重写网络设置。
44
+
45
+ GitHub 安装和 npm 安装提供完全相同的命令。需要立即重新解析内网 DNS
46
+ 并校验全部分流配置时:
47
+
48
+ ```bash
49
+ zyhy-network refresh
50
+ ```
51
+
52
+ ## npm 命令行版本
53
+
54
+ 也可以从 npm 安装:
55
+
56
+ ```bash
57
+ npm install -g @sunjiehao/zyhy-network
58
+ zyhy-network install
59
+ ```
60
+
61
+ 无论使用哪种安装方式,需要修改系统配置时都会自动请求管理员权限。常用命令:
62
+
63
+ ```bash
64
+ zyhy-network status
65
+ zyhy-network refresh
66
+ zyhy-network add '*.hq.cmcc'
67
+ zyhy-network uninstall
68
+ ```
69
+
70
+ ## 不想用了
71
+
72
+ ```bash
73
+ zyhy-network uninstall
74
+ ```
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # zyhy-network managed GitHub-install wrapper
3
+ set -euo pipefail
4
+
5
+ TARGET="/usr/local/sbin/zyhy-private-split"
6
+
7
+ if [[ ! -x "$TARGET" ]]; then
8
+ echo "zyhy-network is not installed; run sudo ./install.sh first" >&2
9
+ exit 1
10
+ fi
11
+
12
+ command=${1:-status}
13
+ case "$command" in
14
+ add|apply|clear|install|refresh|remove|uninstall)
15
+ if [[ $(id -u) -ne 0 ]]; then
16
+ exec /usr/bin/sudo "$TARGET" "$@"
17
+ fi
18
+ ;;
19
+ esac
20
+
21
+ exec "$TARGET" "$@"
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+ const packageJson = require('../package.json');
8
+
9
+ const args = process.argv.slice(2);
10
+
11
+ if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
12
+ console.log(packageJson.version);
13
+ process.exit(0);
14
+ }
15
+
16
+ if (process.platform !== 'darwin') {
17
+ console.error('zyhy-network only supports macOS.');
18
+ process.exit(1);
19
+ }
20
+
21
+ const script = path.join(__dirname, '..', 'zyhy-private-split');
22
+ const command = args[0] || 'status';
23
+ const rootCommands = new Set([
24
+ 'add',
25
+ 'apply',
26
+ 'clear',
27
+ 'install',
28
+ 'refresh',
29
+ 'remove',
30
+ 'uninstall'
31
+ ]);
32
+
33
+ let executable = '/bin/bash';
34
+ let commandArgs = [script, ...args];
35
+
36
+ if (rootCommands.has(command) && process.getuid?.() !== 0) {
37
+ executable = '/usr/bin/sudo';
38
+ commandArgs = ['/bin/bash', script, ...args];
39
+ }
40
+
41
+ const result = spawnSync(executable, commandArgs, { stdio: 'inherit' });
42
+
43
+ if (result.error) {
44
+ console.error(`failed to run zyhy-network: ${result.error.message}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ if (result.signal) {
49
+ if (result.signal === 'SIGPIPE') {
50
+ process.exit(0);
51
+ }
52
+ console.error(`zyhy-network stopped by signal ${result.signal}`);
53
+ process.exit(1);
54
+ }
55
+
56
+ process.exit(result.status ?? 1);
package/install.sh ADDED
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ # 开箱安装:拷配置并启用分流。需要 sudo。
3
+ set -euo pipefail
4
+ if [[ $(id -u) -ne 0 ]]; then
5
+ echo "请用: sudo $0" >&2
6
+ exit 1
7
+ fi
8
+ cd "$(dirname "$0")"
9
+ chmod 755 zyhy-private-split bin/zyhy-network
10
+ mkdir -p /usr/local/bin /usr/local/etc /usr/local/sbin
11
+ cp zyhy-private-split.conf /usr/local/etc/zyhy-private-split.conf
12
+ ./zyhy-private-split install
13
+ if [[ -e /usr/local/bin/zyhy-network ]] && \
14
+ ! grep -q '^# zyhy-network managed GitHub-install wrapper$' /usr/local/bin/zyhy-network 2>/dev/null; then
15
+ echo "保留已有的 /usr/local/bin/zyhy-network(它不是本项目安装的文件)。" >&2
16
+ else
17
+ cp bin/zyhy-network /usr/local/bin/zyhy-network
18
+ chmod 755 /usr/local/bin/zyhy-network
19
+ fi
20
+ echo
21
+ echo "装好了。新开浏览器标签访问 OA / 邮箱即可。"
22
+ echo "查看状态: zyhy-network status"
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@sunjiehao/zyhy-network",
3
+ "version": "0.2.1",
4
+ "description": "macOS Ethernet and ZYHY-Private Wi-Fi split routing CLI",
5
+ "bin": {
6
+ "zyhy-network": "bin/zyhy-network.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "zyhy-private-split",
11
+ "zyhy-private-split.conf",
12
+ "install.sh",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "bash -n zyhy-private-split install.sh bin/zyhy-network && node --check bin/zyhy-network.js",
17
+ "prepack": "npm test"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/lampardrodgers/ZYHY-Network.git"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/lampardrodgers/ZYHY-Network/issues"
28
+ },
29
+ "homepage": "https://github.com/lampardrodgers/ZYHY-Network#readme",
30
+ "keywords": [
31
+ "macos",
32
+ "network",
33
+ "split-routing",
34
+ "launchd"
35
+ ],
36
+ "author": "sunjiehao (lampardrodgers)",
37
+ "license": "UNLICENSED",
38
+ "os": [
39
+ "darwin"
40
+ ],
41
+ "engines": {
42
+ "node": ">=18"
43
+ }
44
+ }
@@ -0,0 +1,987 @@
1
+ #!/bin/bash
2
+ # Split-route intranet via Wi-Fi only when Ethernet is up AND Wi-Fi DHCP
3
+ # domain is ZYHY-Private. Otherwise keep system order (Ethernet first,
4
+ # Wi-Fi if the cable is unplugged). Does not edit Shadowrocket.
5
+ #
6
+ # sudo zyhy-private-split install
7
+ # sudo zyhy-private-split add oa.hq.cmcc
8
+ # sudo zyhy-private-split add '*.hq.cmcc' # whole suffix
9
+ # sudo zyhy-private-split add 172.21.0.0/16 # whole CIDR
10
+ # sudo zyhy-private-split remove oa.hq.cmcc
11
+ # sudo zyhy-private-split refresh
12
+ # zyhy-private-split list
13
+ # zyhy-private-split status
14
+ # zyhy-private-split bundle ~/Desktop/zyhy-private-split.tgz
15
+ # sudo zyhy-private-split uninstall
16
+
17
+ set -euo pipefail
18
+
19
+ LABEL="com.zyhy.network.split"
20
+ VERSION="0.2.1"
21
+ PLIST="/Library/LaunchDaemons/${LABEL}.plist"
22
+ RECONCILE_LABEL="${LABEL}.reconcile"
23
+ RECONCILE_PLIST="/Library/LaunchDaemons/${RECONCILE_LABEL}.plist"
24
+ LEGACY_LABELS="com.misakamikoto.zyhy-private-split"
25
+ INSTALL_BIN="/usr/local/sbin/zyhy-private-split"
26
+ INSTALL_CLI="/usr/local/bin/zyhy-network"
27
+ CONF="/usr/local/etc/zyhy-private-split.conf"
28
+ STATE_DIR="/var/run/zyhy-private-split"
29
+ STATE_FILE="${STATE_DIR}/applied"
30
+ INPUT_STATE_FILE="${STATE_DIR}/network-inputs"
31
+ RESOLVED_STATE_FILE="${STATE_DIR}/resolved-hosts"
32
+ RESOLVER_LIST="${STATE_DIR}/resolvers"
33
+ LOCK_DIR="/var/run/zyhy-private-split.apply.lock"
34
+ WATCH_DIR="/var/run/zyhy-private-split-watch"
35
+ WATCH_COMMAND_FIFO="${WATCH_DIR}/commands"
36
+ WATCH_EVENT_FIFO="${WATCH_DIR}/events"
37
+ HOSTS_BEGIN="# BEGIN zyhy-private-split"
38
+ HOSTS_END="# END zyhy-private-split"
39
+
40
+ # Defaults (overridden by $CONF).
41
+ WIFI_DOMAIN="ZYHY-Private"
42
+ DOMAINS="oa.hq.cmcc mail.cmhi.chinamobile.com"
43
+ SUFFIXES="hq.cmcc cmhi.chinamobile.com"
44
+ CIDRS="172.21.0.0/16 172.16.0.0/16"
45
+ RECONCILE_INTERVAL="300"
46
+ EVENT_SETTLE_SECONDS="2"
47
+
48
+ FILES_CHANGED=0
49
+ WATCH_CHILD_PID=""
50
+
51
+ log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >&2; }
52
+
53
+ require_root() {
54
+ if [[ $(id -u) -ne 0 ]]; then
55
+ echo "need root: sudo $0 $*" >&2
56
+ exit 1
57
+ fi
58
+ }
59
+
60
+ normalize_words() {
61
+ echo "$*" | tr ' \t' '\n' | sed '/^$/d' | awk '!seen[$0]++'
62
+ }
63
+
64
+ load_config() {
65
+ if [[ -f "$CONF" ]]; then
66
+ # shellcheck disable=SC1090
67
+ . "$CONF"
68
+ fi
69
+ DOMAINS=$(normalize_words ${DOMAINS-} | tr '\n' ' ')
70
+ SUFFIXES=$(normalize_words ${SUFFIXES-} | tr '\n' ' ')
71
+ CIDRS=$(normalize_words ${CIDRS-} | tr '\n' ' ')
72
+ WIFI_DOMAIN=${WIFI_DOMAIN:-ZYHY-Private}
73
+ RECONCILE_INTERVAL=${RECONCILE_INTERVAL:-300}
74
+ EVENT_SETTLE_SECONDS=${EVENT_SETTLE_SECONDS:-2}
75
+ [[ "$RECONCILE_INTERVAL" =~ ^[0-9]+$ ]] || RECONCILE_INTERVAL=300
76
+ [[ "$EVENT_SETTLE_SECONDS" =~ ^[0-9]+$ ]] || EVENT_SETTLE_SECONDS=2
77
+ DOMAINS=${DOMAINS% }
78
+ SUFFIXES=${SUFFIXES% }
79
+ CIDRS=${CIDRS% }
80
+ }
81
+
82
+ save_config() {
83
+ mkdir -p "$(dirname "$CONF")"
84
+ cat >"$CONF" <<EOF
85
+ # zyhy-private-split config
86
+ # After editing: sudo zyhy-private-split apply
87
+ # add hostname: sudo zyhy-private-split add foo.hq.cmcc
88
+ # add suffix: sudo zyhy-private-split add '*.hq.cmcc'
89
+ # add CIDR: sudo zyhy-private-split add 172.21.0.0/16
90
+
91
+ WIFI_DOMAIN="${WIFI_DOMAIN}"
92
+ DOMAINS="${DOMAINS}"
93
+ SUFFIXES="${SUFFIXES}"
94
+ CIDRS="${CIDRS}"
95
+ RECONCILE_INTERVAL="${RECONCILE_INTERVAL}"
96
+ EVENT_SETTLE_SECONDS="${EVENT_SETTLE_SECONDS}"
97
+ EOF
98
+ chmod 644 "$CONF"
99
+ }
100
+
101
+ words_has() {
102
+ local needle=$1
103
+ shift
104
+ printf '%s\n' $* | grep -qxF "$needle"
105
+ }
106
+
107
+ add_word() {
108
+ local cur=$1 word=$2
109
+ if [[ -z "$cur" ]]; then
110
+ echo "$word"
111
+ return
112
+ fi
113
+ if printf '%s\n' $cur | grep -qxF "$word"; then
114
+ echo "$cur"
115
+ return
116
+ fi
117
+ echo "$cur $word"
118
+ }
119
+
120
+ remove_word() {
121
+ local cur=$1 word=$2
122
+ printf '%s\n' $cur | grep -vxF "$word" | tr '\n' ' ' | sed 's/[[:space:]]*$//'
123
+ }
124
+
125
+ wifi_device() {
126
+ networksetup -listallhardwareports | awk '
127
+ /^Hardware Port: Wi-Fi/{getline; if ($1=="Device:") {print $2; exit}}
128
+ '
129
+ }
130
+
131
+ ethernet_devices() {
132
+ networksetup -listallhardwareports | awk '
133
+ /^Hardware Port: / {
134
+ port=$0
135
+ sub(/^Hardware Port: /,"",port)
136
+ getline
137
+ if ($1=="Device:") dev=$2
138
+ if (port=="Ethernet" || port ~ /^USB /) print dev
139
+ }
140
+ '
141
+ }
142
+
143
+ iface_active_ipv4() {
144
+ local ifc=$1
145
+ ifconfig "$ifc" 2>/dev/null | awk '
146
+ /status: active/{up=1}
147
+ $1=="inet"{ip=$2}
148
+ END{exit !(up && ip!="")}
149
+ '
150
+ }
151
+
152
+ wifi_is_private() {
153
+ local ifc=$1 domain
154
+ domain=$(ipconfig getoption "$ifc" domain_name 2>/dev/null || true)
155
+ [[ "$domain" == "$WIFI_DOMAIN" ]]
156
+ }
157
+
158
+ wired_up() {
159
+ local d
160
+ for d in $(ethernet_devices); do
161
+ if iface_active_ipv4 "$d"; then
162
+ echo "$d"
163
+ return 0
164
+ fi
165
+ done
166
+ return 1
167
+ }
168
+
169
+ split_should_run() {
170
+ local wifi eth
171
+ wifi=$(wifi_device)
172
+ [[ -n "$wifi" ]] || return 1
173
+ iface_active_ipv4 "$wifi" || return 1
174
+ wifi_is_private "$wifi" || return 1
175
+ eth=$(wired_up) || return 1
176
+ echo "$eth $wifi"
177
+ }
178
+
179
+ add_host_v4() {
180
+ local ip=$1 gw=$2
181
+ if route -n get -host "$ip" 2>/dev/null | grep -q "gateway: ${gw}$"; then
182
+ return 0
183
+ fi
184
+ route -n delete -host "$ip" >/dev/null 2>&1 || true
185
+ route -n add -host "$ip" "$gw" >/dev/null
186
+ }
187
+
188
+ del_host_v4() {
189
+ route -n delete -host "$1" >/dev/null 2>&1 || true
190
+ }
191
+
192
+ add_net_v4() {
193
+ local cidr=$1 gw=$2
194
+ if route -n get -net "$cidr" 2>/dev/null | grep -q "gateway: ${gw}$"; then
195
+ return 0
196
+ fi
197
+ route -n delete -net "$cidr" >/dev/null 2>&1 || true
198
+ route -n add -net "$cidr" "$gw" >/dev/null
199
+ }
200
+
201
+ del_net_v4() {
202
+ route -n delete -net "$1" >/dev/null 2>&1 || true
203
+ }
204
+
205
+ del_reject_v6() {
206
+ route -n delete -inet6 -host "$1" >/dev/null 2>&1 || true
207
+ }
208
+
209
+ strip_hosts_block() {
210
+ awk -v b="$HOSTS_BEGIN" -v e="$HOSTS_END" '
211
+ $0==b {skip=1; next}
212
+ $0==e {skip=0; next}
213
+ skip==0 {print}
214
+ ' /etc/hosts
215
+ }
216
+
217
+ write_hosts_block() {
218
+ local body=$1 tmp
219
+ tmp=$(mktemp)
220
+ {
221
+ strip_hosts_block
222
+ if [[ -n "$body" ]]; then
223
+ printf '%s\n%s%s\n' "$HOSTS_BEGIN" "$body" "$HOSTS_END"
224
+ fi
225
+ } >"$tmp"
226
+ if cmp -s "$tmp" /etc/hosts; then
227
+ rm -f "$tmp"
228
+ return 0
229
+ fi
230
+ cat "$tmp" >/etc/hosts
231
+ rm -f "$tmp"
232
+ FILES_CHANGED=1
233
+ }
234
+
235
+ resolver_names() {
236
+ printf '%s\n' $DOMAINS $SUFFIXES | sed '/^$/d' | awk 'NF && !seen[$0]++'
237
+ }
238
+
239
+ clear_resolvers() {
240
+ local name
241
+ while IFS= read -r name; do
242
+ [[ -n "$name" ]] || continue
243
+ if [[ -e "/etc/resolver/${name}" ]]; then
244
+ rm -f "/etc/resolver/${name}"
245
+ FILES_CHANGED=1
246
+ fi
247
+ done < <(
248
+ {
249
+ [[ -f "$RESOLVER_LIST" ]] && cat "$RESOLVER_LIST"
250
+ resolver_names
251
+ } | awk 'NF && !seen[$0]++'
252
+ )
253
+ }
254
+
255
+ write_resolvers() {
256
+ local dns=$1 name old_name desired_list resolver_tmp
257
+ mkdir -p /etc/resolver
258
+ desired_list=$(mktemp)
259
+ resolver_names >"$desired_list"
260
+
261
+ if [[ -f "$RESOLVER_LIST" ]]; then
262
+ while IFS= read -r old_name; do
263
+ [[ -n "$old_name" ]] || continue
264
+ if ! grep -qxF "$old_name" "$desired_list" && [[ -e "/etc/resolver/${old_name}" ]]; then
265
+ rm -f "/etc/resolver/${old_name}"
266
+ FILES_CHANGED=1
267
+ fi
268
+ done <"$RESOLVER_LIST"
269
+ fi
270
+
271
+ while IFS= read -r name; do
272
+ [[ -n "$name" ]] || continue
273
+ resolver_tmp=$(mktemp)
274
+ cat >"$resolver_tmp" <<EOF
275
+ nameserver ${dns}
276
+ timeout 2
277
+ EOF
278
+ if ! cmp -s "$resolver_tmp" "/etc/resolver/${name}"; then
279
+ cat "$resolver_tmp" >"/etc/resolver/${name}"
280
+ FILES_CHANGED=1
281
+ fi
282
+ rm -f "$resolver_tmp"
283
+ done <"$desired_list"
284
+
285
+ if ! cmp -s "$desired_list" "$RESOLVER_LIST"; then
286
+ mv "$desired_list" "$RESOLVER_LIST"
287
+ else
288
+ rm -f "$desired_list"
289
+ fi
290
+ }
291
+
292
+ flush_dns() {
293
+ dscacheutil -flushcache >/dev/null 2>&1 || true
294
+ killall -HUP mDNSResponder >/dev/null 2>&1 || true
295
+ }
296
+
297
+ resolve_via_wifi() {
298
+ local wifi_ip=$1 dns=$2 name=$3
299
+ dig -b "$wifi_ip" @"$dns" +short +time=2 +tries=1 "$name" A \
300
+ | awk '/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/{print}' \
301
+ | sort -u
302
+ }
303
+
304
+ wanted_proxy_exceptions() {
305
+ local d s
306
+ for d in $DOMAINS; do
307
+ printf '%s\n' "$d" "*.${d}"
308
+ done
309
+ for s in $SUFFIXES; do
310
+ printf '%s\n' "$s" "*.${s}"
311
+ done
312
+ }
313
+
314
+ http_proxy_keys() {
315
+ local key
316
+ echo "State:/Network/Global/Proxies"
317
+ while IFS= read -r key; do
318
+ [[ -n "$key" ]] || continue
319
+ if echo "show ${key}" | scutil | grep -q 'HTTPEnable : 1'; then
320
+ printf '%s\n' "$key"
321
+ fi
322
+ done < <(echo "list State:/Network/Service/.*/Proxies" | scutil | awk -F'= ' '/subKey/{print $2}')
323
+ }
324
+
325
+ read_proxy_exceptions() {
326
+ local key=${1-}
327
+ [[ -n "$key" ]] || key=$(http_proxy_keys | awk 'NR==2{print; exit} END{if(NR==1) print}')
328
+ echo "show ${key}" | scutil | awk '
329
+ /ExceptionsList[[:space:]]*:/ {inlist=1; next}
330
+ inlist && /}/ {exit}
331
+ inlist && /: / {
332
+ sub(/^[^:]+:[[:space:]]*/, "")
333
+ if ($0 != "") print
334
+ }
335
+ '
336
+ }
337
+
338
+ write_proxy_exceptions() {
339
+ local items=$*
340
+ local key
341
+ [[ -n "$items" ]] || return 0
342
+ while IFS= read -r key; do
343
+ [[ -n "$key" ]] || continue
344
+ scutil >/dev/null <<EOF
345
+ open
346
+ d.init
347
+ get ${key}
348
+ d.add ExceptionsList * ${items}
349
+ set ${key}
350
+ quit
351
+ EOF
352
+ done < <(http_proxy_keys | awk 'NF && !seen[$0]++')
353
+ }
354
+
355
+ list_has() {
356
+ local needle=$1
357
+ shift
358
+ local y
359
+ for y in "$@"; do
360
+ [[ "$y" == "$needle" ]] && return 0
361
+ done
362
+ return 1
363
+ }
364
+
365
+ ensure_proxy_exceptions() {
366
+ local -a cur=() want=() merged=()
367
+ local x
368
+ while IFS= read -r x; do
369
+ [[ -n "$x" ]] && cur+=("$x")
370
+ done < <(read_proxy_exceptions "")
371
+ while IFS= read -r x; do
372
+ [[ -n "$x" ]] && want+=("$x")
373
+ done < <(wanted_proxy_exceptions | awk 'NF && !seen[$0]++')
374
+ if [[ ${#cur[@]} -gt 0 ]]; then
375
+ merged=("${cur[@]}")
376
+ fi
377
+ for x in "${want[@]}"; do
378
+ if [[ ${#cur[@]} -gt 0 ]] && list_has "$x" "${cur[@]}"; then
379
+ continue
380
+ fi
381
+ merged+=("$x")
382
+ done
383
+ if [[ ${#merged[@]} -eq ${#cur[@]} ]]; then
384
+ return 0
385
+ fi
386
+ write_proxy_exceptions "${merged[*]}"
387
+ log "proxy bypass +${want[*]}"
388
+ }
389
+
390
+ remove_proxy_exceptions() {
391
+ local -a cur=() keep=() want=()
392
+ local x
393
+ while IFS= read -r x; do
394
+ [[ -n "$x" ]] && cur+=("$x")
395
+ done < <(read_proxy_exceptions "")
396
+ [[ ${#cur[@]} -gt 0 ]] || return 0
397
+ while IFS= read -r x; do
398
+ [[ -n "$x" ]] && want+=("$x")
399
+ done < <(wanted_proxy_exceptions)
400
+ for x in "${cur[@]}"; do
401
+ if [[ ${#want[@]} -gt 0 ]] && list_has "$x" "${want[@]}"; then
402
+ continue
403
+ fi
404
+ keep+=("$x")
405
+ done
406
+ if [[ ${#keep[@]} -eq ${#cur[@]} ]]; then
407
+ return 0
408
+ fi
409
+ write_proxy_exceptions "${keep[*]}"
410
+ log "proxy bypass restored"
411
+ }
412
+
413
+ drop_state_entry() {
414
+ local kind=$1 target=$2
415
+ case "$kind" in
416
+ v6) del_reject_v6 "$target" ;;
417
+ net) del_net_v4 "$target" ;;
418
+ *) del_host_v4 "$target" ;;
419
+ esac
420
+ }
421
+
422
+ clear_applied() {
423
+ local kind ip
424
+ if [[ -f "$STATE_FILE" ]]; then
425
+ while read -r kind ip; do
426
+ [[ -n "$kind" ]] || continue
427
+ if [[ -z "$ip" ]]; then
428
+ ip=$kind
429
+ kind=v4
430
+ fi
431
+ drop_state_entry "$kind" "$ip"
432
+ done <"$STATE_FILE"
433
+ fi
434
+ write_hosts_block ""
435
+ clear_resolvers
436
+ remove_proxy_exceptions
437
+ rm -rf "$STATE_DIR"
438
+ flush_dns
439
+ }
440
+
441
+ cmd_clear() {
442
+ require_root
443
+ load_config
444
+ clear_applied
445
+ log "cleared split routes"
446
+ }
447
+
448
+ write_network_inputs() {
449
+ local dest=$1 eth=$2 wifi=$3 wifi_ip=$4 wifi_gw=$5 wifi_dns=$6
450
+ {
451
+ printf 'ethernet=%s\n' "$eth"
452
+ printf 'ethernet_ip=%s\n' "$(ipconfig getifaddr "$eth" 2>/dev/null || true)"
453
+ printf 'wifi=%s\n' "$wifi"
454
+ printf 'wifi_ip=%s\n' "$wifi_ip"
455
+ printf 'wifi_gateway=%s\n' "$wifi_gw"
456
+ printf 'wifi_dns=%s\n' "$wifi_dns"
457
+ printf 'wifi_domain=%s\n' "$(ipconfig getoption "$wifi" domain_name 2>/dev/null || true)"
458
+ printf 'domains=%s\n' "$DOMAINS"
459
+ printf 'suffixes=%s\n' "$SUFFIXES"
460
+ printf 'cidrs=%s\n' "$CIDRS"
461
+ } >"$dest"
462
+ }
463
+
464
+ acquire_apply_lock() {
465
+ local old_pid=""
466
+ if mkdir "$LOCK_DIR" 2>/dev/null; then
467
+ printf '%s\n' "$$" >"${LOCK_DIR}/pid"
468
+ return 0
469
+ fi
470
+ [[ -r "${LOCK_DIR}/pid" ]] && old_pid=$(<"${LOCK_DIR}/pid")
471
+ if [[ "$old_pid" =~ ^[0-9]+$ ]] && kill -0 "$old_pid" 2>/dev/null; then
472
+ return 1
473
+ fi
474
+ rm -f "${LOCK_DIR}/pid"
475
+ rmdir "$LOCK_DIR" 2>/dev/null || return 1
476
+ mkdir "$LOCK_DIR"
477
+ printf '%s\n' "$$" >"${LOCK_DIR}/pid"
478
+ }
479
+
480
+ release_apply_lock() {
481
+ rm -f "${LOCK_DIR}/pid"
482
+ rmdir "$LOCK_DIR" 2>/dev/null || true
483
+ }
484
+
485
+ cmd_apply_locked() {
486
+ local force=${1-}
487
+ load_config
488
+ mkdir -p "$STATE_DIR"
489
+ FILES_CHANGED=0
490
+ local pair eth wifi wifi_ip wifi_gw wifi_dns
491
+ if ! pair=$(split_should_run); then
492
+ if [[ -f "$STATE_FILE" ]]; then
493
+ log "condition not met; tearing down"
494
+ clear_applied
495
+ fi
496
+ return 0
497
+ fi
498
+ eth=${pair%% *}
499
+ wifi=${pair##* }
500
+ wifi_ip=$(ipconfig getifaddr "$wifi")
501
+ wifi_gw=$(ipconfig getoption "$wifi" router)
502
+ wifi_dns=$(ipconfig getoption "$wifi" domain_name_server | awk '{print $1}')
503
+ if [[ -z "$wifi_ip" || -z "$wifi_gw" || -z "$wifi_dns" ]]; then
504
+ log "wifi details incomplete; skip"
505
+ return 0
506
+ fi
507
+
508
+ local input_tmp
509
+ input_tmp=$(mktemp)
510
+ write_network_inputs "$input_tmp" "$eth" "$wifi" "$wifi_ip" "$wifi_gw" "$wifi_dns"
511
+ if [[ "$force" != "--force" && -f "$STATE_FILE" && -f "$INPUT_STATE_FILE" ]] \
512
+ && cmp -s "$input_tmp" "$INPUT_STATE_FILE"; then
513
+ rm -f "$input_tmp"
514
+ return 0
515
+ fi
516
+
517
+ local -a lines=()
518
+ local hosts_body="" d ip first cidr resolved resolved_tmp previous
519
+ resolved_tmp=$(mktemp)
520
+ add_host_v4 "$wifi_dns" "$wifi_gw"
521
+ lines+=("v4 $wifi_dns")
522
+
523
+ for cidr in $CIDRS; do
524
+ add_net_v4 "$cidr" "$wifi_gw"
525
+ lines+=("net $cidr")
526
+ done
527
+
528
+ for d in $DOMAINS; do
529
+ first=""
530
+ resolved=$(resolve_via_wifi "$wifi_ip" "$wifi_dns" "$d" || true)
531
+ if [[ -z "$resolved" ]]; then
532
+ previous=""
533
+ if [[ -f "$RESOLVED_STATE_FILE" ]]; then
534
+ previous=$(awk -v domain="$d" '$1==domain {print $2}' "$RESOLVED_STATE_FILE")
535
+ fi
536
+ if [[ -z "$previous" ]]; then
537
+ log "WARN: no A record for $d via $wifi_dns"
538
+ continue
539
+ fi
540
+ log "WARN: no A record for $d via $wifi_dns; keeping previous result"
541
+ resolved=$previous
542
+ fi
543
+ while read -r ip; do
544
+ [[ -n "$ip" ]] || continue
545
+ printf '%s %s\n' "$d" "$ip" >>"$resolved_tmp"
546
+ add_host_v4 "$ip" "$wifi_gw"
547
+ lines+=("v4 $ip")
548
+ if [[ -z "$first" ]]; then
549
+ first=$ip
550
+ hosts_body+="${ip} ${d}"$'\n'
551
+ fi
552
+ done <<<"$resolved"
553
+ done
554
+
555
+ write_resolvers "$wifi_dns"
556
+ write_hosts_block "$hosts_body"
557
+ ensure_proxy_exceptions
558
+
559
+ local tmp state_changed=0
560
+ tmp=$(mktemp)
561
+ printf '%s\n' "${lines[@]}" | awk 'NF && !seen[$0]++' | sort >"$tmp"
562
+ if [[ -f "$STATE_FILE" ]]; then
563
+ while read -r kind ip; do
564
+ [[ -n "$kind" ]] || continue
565
+ if [[ -z "$ip" ]]; then
566
+ ip=$kind
567
+ kind=v4
568
+ fi
569
+ if grep -qx "$kind $ip" "$tmp"; then
570
+ continue
571
+ fi
572
+ drop_state_entry "$kind" "$ip"
573
+ done <"$STATE_FILE"
574
+ fi
575
+ if [[ -f "$STATE_FILE" ]] && cmp -s "$tmp" "$STATE_FILE"; then
576
+ rm -f "$tmp"
577
+ else
578
+ mv "$tmp" "$STATE_FILE"
579
+ state_changed=1
580
+ fi
581
+
582
+ if ! cmp -s "$input_tmp" "$INPUT_STATE_FILE"; then
583
+ mv "$input_tmp" "$INPUT_STATE_FILE"
584
+ else
585
+ rm -f "$input_tmp"
586
+ fi
587
+ if ! cmp -s "$resolved_tmp" "$RESOLVED_STATE_FILE"; then
588
+ mv "$resolved_tmp" "$RESOLVED_STATE_FILE"
589
+ else
590
+ rm -f "$resolved_tmp"
591
+ fi
592
+
593
+ chmod 755 "$STATE_DIR" 2>/dev/null || true
594
+ chmod 644 "$STATE_FILE" "$INPUT_STATE_FILE" "$RESOLVED_STATE_FILE" "$RESOLVER_LIST" 2>/dev/null || true
595
+ if [[ "$FILES_CHANGED" -eq 1 ]]; then
596
+ flush_dns
597
+ fi
598
+ if [[ "$state_changed" -eq 1 || "$FILES_CHANGED" -eq 1 ]]; then
599
+ log "applied via ${wifi} gw=${wifi_gw} dns=${wifi_dns} (wired ${eth}): $(tr '\n' ' ' <"$STATE_FILE")"
600
+ fi
601
+ }
602
+
603
+ cmd_apply() {
604
+ require_root
605
+ if ! acquire_apply_lock; then
606
+ return 0
607
+ fi
608
+ local rc=0
609
+ cmd_apply_locked "${1-}" || rc=$?
610
+ release_apply_lock
611
+ return "$rc"
612
+ }
613
+
614
+ cmd_refresh() {
615
+ require_root
616
+ cmd_apply --force
617
+ echo "refreshed: routes, DNS, hosts, resolvers and proxy exceptions checked"
618
+ }
619
+
620
+ cmd_status() {
621
+ load_config
622
+ local wifi eth pair
623
+ wifi=$(wifi_device)
624
+ echo "wifi device : ${wifi:-none}"
625
+ if [[ -n "$wifi" ]]; then
626
+ echo "wifi ip : $(ipconfig getifaddr "$wifi" 2>/dev/null || echo none)"
627
+ echo "wifi domain : $(ipconfig getoption "$wifi" domain_name 2>/dev/null || echo none)"
628
+ echo "wifi dns : $(ipconfig getoption "$wifi" domain_name_server 2>/dev/null || echo none)"
629
+ echo "wifi gw : $(ipconfig getoption "$wifi" router 2>/dev/null || echo none)"
630
+ fi
631
+ echo "wired ifaces: $(ethernet_devices | tr '\n' ' ')"
632
+ if eth=$(wired_up); then
633
+ echo "wired up : $eth $(ipconfig getifaddr "$eth")"
634
+ else
635
+ echo "wired up : no"
636
+ fi
637
+ if pair=$(split_should_run); then
638
+ echo "split cond : YES ($pair)"
639
+ else
640
+ echo "split cond : no"
641
+ fi
642
+ echo "config : $CONF"
643
+ echo "domains : ${DOMAINS:-<none>}"
644
+ echo "suffixes : ${SUFFIXES:-<none>}"
645
+ echo "cidrs : ${CIDRS:-<none>}"
646
+ echo "state file : $( [[ -f $STATE_FILE ]] && echo present || echo absent )"
647
+ if [[ -r "$STATE_FILE" ]]; then
648
+ echo "routed:"
649
+ sed 's/^/ /' "$STATE_FILE"
650
+ elif [[ -f "$STATE_FILE" ]]; then
651
+ echo "routed: (not readable without root)"
652
+ fi
653
+ echo "hosts block :"
654
+ awk -v b="$HOSTS_BEGIN" -v e="$HOSTS_END" '
655
+ $0==b{on=1}
656
+ on{print " "$0}
657
+ $0==e{on=0}
658
+ ' /etc/hosts || true
659
+ echo "watcher : $(launchctl print "system/${LABEL}" >/dev/null 2>&1 && echo loaded || echo not-loaded)"
660
+ echo "reconcile : $(launchctl print "system/${RECONCILE_LABEL}" >/dev/null 2>&1 && echo "loaded (${RECONCILE_INTERVAL}s)" || echo not-loaded)"
661
+ echo "sys proxy : $(scutil --proxy | awk '/HTTPEnable/{e=$3} /HTTPProxy/{p=$3} /HTTPPort/{n=$3} END{if(e==1) print p":"n; else print "off"}')"
662
+ echo "proxy bypass:"
663
+ read_proxy_exceptions | sed 's/^/ /' || true
664
+ local d
665
+ for d in $DOMAINS; do
666
+ echo "resolve $d:"
667
+ dscacheutil -q host -a name "$d" 2>/dev/null | sed 's/^/ /' || true
668
+ done
669
+ }
670
+
671
+ cmd_list() {
672
+ load_config
673
+ echo "config : $CONF"
674
+ echo "domains :"
675
+ printf '%s\n' $DOMAINS | sed 's/^/ /'
676
+ echo "suffixes :"
677
+ printf '%s\n' $SUFFIXES | sed 's/^/ /'
678
+ echo "cidrs :"
679
+ printf '%s\n' $CIDRS | sed 's/^/ /'
680
+ }
681
+
682
+ classify_item() {
683
+ local item=$1
684
+ case "$item" in
685
+ */*) echo cidr ;;
686
+ \*.*) echo suffix ;;
687
+ *) echo domain ;;
688
+ esac
689
+ }
690
+
691
+ normalize_item() {
692
+ local kind=$1 item=$2
693
+ case "$kind" in
694
+ suffix) echo "${item#\*.}" ;;
695
+ *) echo "$item" ;;
696
+ esac
697
+ }
698
+
699
+ cmd_add() {
700
+ require_root
701
+ load_config
702
+ local item=${1-} kind
703
+ [[ -n "$item" ]] || { echo "usage: $0 add <host|*.suffix|cidr>" >&2; exit 2; }
704
+ kind=$(classify_item "$item")
705
+ item=$(normalize_item "$kind" "$item")
706
+ case "$kind" in
707
+ cidr) DOMAINS="$DOMAINS"; CIDRS=$(add_word "$CIDRS" "$item") ;;
708
+ suffix) SUFFIXES=$(add_word "$SUFFIXES" "$item") ;;
709
+ domain) DOMAINS=$(add_word "$DOMAINS" "$item") ;;
710
+ esac
711
+ save_config
712
+ log "added $kind $item"
713
+ cmd_apply
714
+ }
715
+
716
+ cmd_remove() {
717
+ require_root
718
+ load_config
719
+ local item=${1-} kind
720
+ [[ -n "$item" ]] || { echo "usage: $0 remove <host|*.suffix|cidr>" >&2; exit 2; }
721
+ kind=$(classify_item "$item")
722
+ item=$(normalize_item "$kind" "$item")
723
+ case "$kind" in
724
+ cidr) CIDRS=$(remove_word "$CIDRS" "$item") ;;
725
+ suffix) SUFFIXES=$(remove_word "$SUFFIXES" "$item") ;;
726
+ domain) DOMAINS=$(remove_word "$DOMAINS" "$item") ;;
727
+ esac
728
+ save_config
729
+ log "removed $kind $item"
730
+ cmd_apply
731
+ }
732
+
733
+ close_network_event_stream() {
734
+ { exec 3>&-; } 2>/dev/null || true
735
+ { exec 4<&-; } 2>/dev/null || true
736
+ if [[ "$WATCH_CHILD_PID" =~ ^[0-9]+$ ]]; then
737
+ kill "$WATCH_CHILD_PID" >/dev/null 2>&1 || true
738
+ wait "$WATCH_CHILD_PID" 2>/dev/null || true
739
+ fi
740
+ WATCH_CHILD_PID=""
741
+ rm -f "$WATCH_COMMAND_FIFO" "$WATCH_EVENT_FIFO"
742
+ rmdir "$WATCH_DIR" 2>/dev/null || true
743
+ }
744
+
745
+ stop_watch() {
746
+ close_network_event_stream
747
+ exit 0
748
+ }
749
+
750
+ start_network_event_stream() {
751
+ mkdir -p "$WATCH_DIR"
752
+ rm -f "$WATCH_COMMAND_FIFO" "$WATCH_EVENT_FIFO"
753
+ mkfifo "$WATCH_COMMAND_FIFO" "$WATCH_EVENT_FIFO"
754
+ /usr/sbin/scutil <"$WATCH_COMMAND_FIFO" >"$WATCH_EVENT_FIFO" 2>/dev/null &
755
+ WATCH_CHILD_PID=$!
756
+ exec 3>"$WATCH_COMMAND_FIFO"
757
+ exec 4<"$WATCH_EVENT_FIFO"
758
+ cat >&3 <<'EOF'
759
+ n.add ^State:/Network/(Global/(IPv4|DNS)|Interface/.*/(IPv4|Link|AirPort)|Service/.*/(DHCP|DNS|IPv4))$ pattern
760
+ n.add ^Setup:/Network/Service/.*/(DNS|IPv4)$ pattern
761
+ n.watch
762
+ EOF
763
+ }
764
+
765
+ cmd_watch() {
766
+ require_root
767
+ trap stop_watch INT TERM
768
+ log "network watcher started"
769
+ cmd_apply --force
770
+ local line now last_apply=0
771
+ while :; do
772
+ start_network_event_stream
773
+ while IFS= read -r line <&4; do
774
+ case "$line" in
775
+ *"changedKey ["*) ;;
776
+ *) continue ;;
777
+ esac
778
+ now=$(date +%s)
779
+ if (( now - last_apply <= EVENT_SETTLE_SECONDS )); then
780
+ continue
781
+ fi
782
+ sleep "$EVENT_SETTLE_SECONDS"
783
+ cmd_apply
784
+ last_apply=$(date +%s)
785
+ done
786
+ close_network_event_stream
787
+ log "WARN: network event stream stopped; retrying"
788
+ sleep 5
789
+ done
790
+ }
791
+
792
+ write_install_plists() {
793
+ local bin=$1
794
+ cat >"$PLIST" <<EOF
795
+ <?xml version="1.0" encoding="UTF-8"?>
796
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
797
+ <plist version="1.0">
798
+ <dict>
799
+ <key>Label</key>
800
+ <string>${LABEL}</string>
801
+ <key>ProgramArguments</key>
802
+ <array>
803
+ <string>${bin}</string>
804
+ <string>watch</string>
805
+ </array>
806
+ <key>RunAtLoad</key>
807
+ <true/>
808
+ <key>KeepAlive</key>
809
+ <true/>
810
+ <key>ThrottleInterval</key>
811
+ <integer>10</integer>
812
+ <key>StandardOutPath</key>
813
+ <string>/var/log/zyhy-private-split.log</string>
814
+ <key>StandardErrorPath</key>
815
+ <string>/var/log/zyhy-private-split.log</string>
816
+ </dict>
817
+ </plist>
818
+ EOF
819
+
820
+ cat >"$RECONCILE_PLIST" <<EOF
821
+ <?xml version="1.0" encoding="UTF-8"?>
822
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
823
+ <plist version="1.0">
824
+ <dict>
825
+ <key>Label</key>
826
+ <string>${RECONCILE_LABEL}</string>
827
+ <key>ProgramArguments</key>
828
+ <array>
829
+ <string>${bin}</string>
830
+ <string>apply</string>
831
+ <string>--force</string>
832
+ </array>
833
+ <key>StartInterval</key>
834
+ <integer>${RECONCILE_INTERVAL}</integer>
835
+ <key>StandardOutPath</key>
836
+ <string>/var/log/zyhy-private-split.log</string>
837
+ <key>StandardErrorPath</key>
838
+ <string>/var/log/zyhy-private-split.log</string>
839
+ </dict>
840
+ </plist>
841
+ EOF
842
+ chmod 644 "$PLIST" "$RECONCILE_PLIST"
843
+ }
844
+
845
+ bootstrap_with_retry() {
846
+ local plist=$1 attempt=1 output=""
847
+ while (( attempt <= 5 )); do
848
+ if output=$(launchctl bootstrap system "$plist" 2>&1); then
849
+ return 0
850
+ fi
851
+ if (( attempt == 5 )); then
852
+ echo "$output" >&2
853
+ return 1
854
+ fi
855
+ log "WARN: launchd bootstrap retry ${attempt}/5 for $(basename "$plist")"
856
+ sleep 1
857
+ attempt=$((attempt + 1))
858
+ done
859
+ }
860
+
861
+ cmd_install() {
862
+ require_root
863
+ local self legacy_label
864
+ self=$(cd "$(dirname "$0")" && pwd)/$(basename "$0")
865
+ launchctl bootout "system/${LABEL}" >/dev/null 2>&1 || true
866
+ launchctl bootout "system/${RECONCILE_LABEL}" >/dev/null 2>&1 || true
867
+ for legacy_label in $LEGACY_LABELS; do
868
+ launchctl bootout "system/${legacy_label}" >/dev/null 2>&1 || true
869
+ done
870
+ mkdir -p /usr/local/sbin /usr/local/etc
871
+ if [[ "$self" != "$INSTALL_BIN" ]]; then
872
+ cp "$self" "$INSTALL_BIN"
873
+ fi
874
+ chmod 755 "$INSTALL_BIN"
875
+ load_config
876
+ save_config
877
+ for legacy_label in $LEGACY_LABELS; do
878
+ rm -f "/Library/LaunchDaemons/${legacy_label}.plist"
879
+ done
880
+ write_install_plists "$INSTALL_BIN"
881
+ cmd_apply --force
882
+ bootstrap_with_retry "$PLIST"
883
+ if ! bootstrap_with_retry "$RECONCILE_PLIST"; then
884
+ launchctl bootout "system/${LABEL}" >/dev/null 2>&1 || true
885
+ return 1
886
+ fi
887
+ log "installed ${INSTALL_BIN} + ${CONF}"
888
+ }
889
+
890
+ cmd_uninstall() {
891
+ require_root
892
+ load_config
893
+ launchctl bootout "system/${LABEL}" >/dev/null 2>&1 || true
894
+ launchctl bootout "system/${RECONCILE_LABEL}" >/dev/null 2>&1 || true
895
+ local legacy_label
896
+ for legacy_label in $LEGACY_LABELS; do
897
+ launchctl bootout "system/${legacy_label}" >/dev/null 2>&1 || true
898
+ rm -f "/Library/LaunchDaemons/${legacy_label}.plist"
899
+ done
900
+ rm -f "$PLIST" "$RECONCILE_PLIST"
901
+ if [[ -f "$INSTALL_CLI" ]] && \
902
+ grep -q '^# zyhy-network managed GitHub-install wrapper$' "$INSTALL_CLI" 2>/dev/null; then
903
+ rm -f "$INSTALL_CLI"
904
+ fi
905
+ clear_applied
906
+ log "uninstalled (config kept at ${CONF})"
907
+ }
908
+
909
+ cmd_bundle() {
910
+ load_config
911
+ local dest=${1-} tmp
912
+ if [[ -z "$dest" ]]; then
913
+ dest="$HOME/Downloads/zyhy-private-split.tgz"
914
+ fi
915
+ tmp=$(mktemp -d)
916
+ mkdir -p "$tmp/zyhy-private-split"
917
+ local src
918
+ src=$(cd "$(dirname "$0")" && pwd)/$(basename "$0")
919
+ cp "$src" "$tmp/zyhy-private-split/zyhy-private-split"
920
+ chmod 755 "$tmp/zyhy-private-split/zyhy-private-split"
921
+ if [[ -f "$CONF" ]]; then
922
+ cp "$CONF" "$tmp/zyhy-private-split/zyhy-private-split.conf"
923
+ else
924
+ cat >"$tmp/zyhy-private-split/zyhy-private-split.conf" <<EOF
925
+ WIFI_DOMAIN="${WIFI_DOMAIN}"
926
+ DOMAINS="${DOMAINS}"
927
+ SUFFIXES="${SUFFIXES}"
928
+ CIDRS="${CIDRS}"
929
+ EVENT_SETTLE_SECONDS="${EVENT_SETTLE_SECONDS}"
930
+ RECONCILE_INTERVAL="${RECONCILE_INTERVAL}"
931
+ EOF
932
+ fi
933
+ cat >"$tmp/zyhy-private-split/INSTALL.txt" <<'EOF'
934
+ macOS only. Target machine also needs Ethernet + Wi-Fi ZYHY-Private.
935
+
936
+ chmod 755 zyhy-private-split
937
+ sudo mkdir -p /usr/local/etc
938
+ sudo cp zyhy-private-split.conf /usr/local/etc/zyhy-private-split.conf
939
+ sudo ./zyhy-private-split install
940
+
941
+ Then:
942
+ zyhy-private-split status
943
+ sudo zyhy-private-split add '*.hq.cmcc'
944
+ sudo zyhy-private-split add 172.21.0.0/16
945
+ EOF
946
+ tar -C "$tmp" -czf "$dest" zyhy-private-split
947
+ rm -rf "$tmp"
948
+ echo "bundle: $dest"
949
+ }
950
+
951
+ usage() {
952
+ cat <<EOF
953
+ usage: $0 <command>
954
+
955
+ status show split state
956
+ list show domains / suffixes / CIDRs
957
+ add <item> add host, '*.suffix', or CIDR (needs sudo)
958
+ remove <item> remove one of the above (needs sudo)
959
+ refresh force DNS resolution and reconcile now (needs sudo)
960
+ apply [--force] apply now; --force also reconciles DNS (needs sudo)
961
+ clear tear down routes (needs sudo)
962
+ install copy to /usr/local/sbin and load daemon (needs sudo)
963
+ uninstall remove daemon and routes; keep config (needs sudo)
964
+ bundle [path.tgz] pack script+config for another Mac
965
+ EOF
966
+ exit 2
967
+ }
968
+
969
+ cmd=${1:-status}
970
+ shift || true
971
+ load_config
972
+ case "$cmd" in
973
+ apply) cmd_apply "${1-}" ;;
974
+ refresh) cmd_refresh ;;
975
+ watch) cmd_watch ;;
976
+ clear) cmd_clear ;;
977
+ status) cmd_status ;;
978
+ list) cmd_list ;;
979
+ add) cmd_add "${1-}" ;;
980
+ remove) cmd_remove "${1-}" ;;
981
+ install) cmd_install ;;
982
+ uninstall) cmd_uninstall ;;
983
+ bundle) cmd_bundle "${1-}" ;;
984
+ help|-h|--help) usage ;;
985
+ -v|--version) echo "$VERSION" ;;
986
+ *) usage ;;
987
+ esac
@@ -0,0 +1,10 @@
1
+ # 这台已验证可用的配置,安装脚本会原样拷到 /usr/local/etc/
2
+
3
+ WIFI_DOMAIN="ZYHY-Private"
4
+ DOMAINS="oa.hq.cmcc mail.cmhi.chinamobile.com"
5
+ SUFFIXES="hq.cmcc cmhi.chinamobile.com"
6
+ CIDRS="172.21.0.0/16 172.16.0.0/16"
7
+
8
+ # 网络变化后等待接口稳定的秒数;另每 300 秒做一次只读为主的兜底校验。
9
+ EVENT_SETTLE_SECONDS="2"
10
+ RECONCILE_INTERVAL="300"