使用Surge脚本 & 逻辑规则建立联网防火墙

使用Surge脚本 & 逻辑规则建立联网防火墙

前言

  1. 本文仅建议对Surge(iOS) APP有一定熟悉的用户阅读. 如您从未使用过, 则该文可能不适合您.

  2. 本文主要内容为 “手机APP(iOS) 隐私泄漏” 问题以及解决方案.

  3. 本文将会耗费大量篇幅讲解Surge脚本或Surge逻辑规则的运用、科普、 实现、以及原理, 并建立手机网络防火墙.


背景

有一个话题引起了我的关注, 一些国内APP (iOS), 如: X信, XX宝, X团.

用户安装以上APP后, 手机从蜂窝网络切换至WiFi的一瞬间(包括自动连接), 以上APP可能会利用iOS某个系统接口直接后台唤醒, 并向服务器上传网络请求, 经过抓包后发现, 该类请求都隶属于以上APP (查询ASN得知), 并且都经过强加密, 无法得知上传内容.

以上情况不管用户是否关闭后台关闭后台刷新, 都有可能会触发. 如果用户手动打开系统设置查看WiFi列表, 也可能会触发以上APP的某些联网请求.

  • 该类网络请求可能会上传:
  1. WiFi名称 (SSID)
  2. 定位数据
  3. 接入或断开WiFi的时间

以上基于常识推断; 由于笔者非iOS开发者, 无法得知APP在该情况下具有的权限, 不对的还请指正.

  • 企业对该数据可能会:
  1. 出售或共享, 建立大数据
  2. 推断出你的作息规律, 几点几分离开家/到公司, 并建立用户画像
  3. 基于以上两点精准推送广告

以上为背景介绍


应对

  • 借助Surge强大的脚本和规则系统, 目前有两种解决方案

方案一概括

APP关闭时(后台), 完全拦截手机所有APP联网请求; APP开启时(前台), 放行所有APP联网请求. (只允许用户主动开启APP表示我需要用到它,否则不允许乱联网)

方案二概括

手机从蜂窝网络切换至WiFi(手动/自动)的瞬间, 15秒内拦截所有APP联网请求(前台/后台), 15秒后恢复

该方案提供懒人Surge模块一键配置, 具体可查看本文实现方案二章节

  • 为了使读者更好的理解, 本文将会极其详细的描述以上两种方案里每一步的原理以及配置, 供读者参考.

实现方案一

需求:

  1. 打开手机内任何一个APP时(前台), 放行网络请求

  2. 关闭任何一个APP时(后台), 拦截所有请求

  3. 拦截的请求可以设置白名单

有两种思路:

  1. 捷径自动化触发, 当打开/关闭APP时, 运行捷径, 捷径内容为调用SurgeAPI(开启/关闭)模块, 模块内容为: 拒绝所有网络请求和白名单在内的规则

  2. 捷径自动化触发, 当打开/关闭APP时, 运行捷径, 捷径内容为运行Surge脚本, 脚本内容为: 储存当前APP状态(打开/关闭), 再使用Surge逻辑规则脚本规则共同决定网络请求是否需要拦截

由于Surge模块的特殊性, 开启/关闭模块时, 会导致:

  1. 触发重载配置 (有概率VPN断连)

  2. 丢失DNS缓存(TTL), 严重影响规则匹配效率

  3. 可能会打断活动的网络连接 (下载中断)

基于以上弊端, 本文将跳过(开启/关闭)模块的实现方式.


第一步: 配置捷径脚本

  • Surge内新建Cron类型脚本, Cron表达式填写 0 0 1 1 * 脚本名暂且设置为“APP防火墙”, 编辑并填入以下脚本, 保存后将该Cron脚本禁用(左划)
APP防火墙.js
1
2
3
const data = $intent.parameter;
const write = $persistentStore.write(data, "APP_BJ");
$done();

以下为科普脚本代码的具体用途及原理, 请选择性阅读.

APP防火墙.js
1
const data = $intent.parameter;

在JS语法中, 声明一个名为data的常量, 值为$intent.parameter, 该API实际内容为读取Surge捷径运行后传入的字符串; 如果Surge捷径的参数留空, 则该API实际内容为字符串


APP防火墙.js
1
const write = $persistentStore.write(data, "APP_BJ");

在JS语法中, 声明一个名为write的常量, 值为Surge的内部函数, 该函数的实际用途持久化储存, 接受2个参数, 需要写入的数据(字符串类型), 以及一个固定的读取键(字符串类型)

该行用途: 写入(储存)上一行代码中名为data的常量的值, 读取键为”APP_BJ”


APP防火墙.js
1
$done();

立即调用Surge内部函数, 该函数的实际用途为退出脚本执行; 使用Surge执行脚本时, 该API不可省略.


第二步: 配置捷径自动化

  1. 打开”捷径” > 自动化 > 加号 > 创建个人自动化 > APP > 选取”已打开” > 手动选取除了”设置”外的所有APP > 添加操作, 搜索”Surge”, 选取运行脚本 > 展开 > 脚本名为前面设置的”APP防火墙”, 参数留空 > 下一步 > 关闭”运行前询问” > 完成
  2. 重复第1步的过程, 不同的是, 需要选取APP已关闭时运行捷径, 并且需要把Surge动作里的参数填写 1 (重要)


第三步: 配置规则脚本

  • Surge内新建Rule类型脚本, 脚本名暂且设置为”RULE防火墙”, 编辑并填入以下脚本并保存
RULE防火墙.js
1
2
const read = $persistentStore.read("APP_BJ");
$done({ matched: Boolean(read) });

以下为科普脚本代码的具体用途及原理, 请选择性阅读.

RULE防火墙.js
1
const read = $persistentStore.read("APP_BJ");

在JS语法中, 声明一个名为read的常量, 值为Surge的内部函数, 该函数的实际用途为持久化读取(读取之前所写入的数据), 接受1个参数: 读取键 (字符串类型)

该行用途: 读取捷径脚本所写入”APP_BJ”键的持久化数据


RULE防火墙.js
1
$done({ matched: Boolean(read) });

立即调用Surge内部函数, 该函数的实际用途为退出脚本执行, 由于是Rule脚本(使用脚本进行规则判定), 需要额外返回一个对象键值对,对象属性键为 matched, 值为布尔值(true/false)表示是否匹配该规则; 在当前代码中, JS内部函数Boolean()用于转换布尔值

该行用途: 退出脚本并判断是否匹配规则, 如果常量的数据(捷径脚本所写入的数据), 能够转换为true, 则返回true(匹配规则), 否则返回false(不匹配规则)


第四步: 配置逻辑规则

规则定义


  • 逻辑规则可将多个规则相互嵌套(包含逻辑规则), 用于复杂的规则判断.

1
AND, ((#子规则1), (#子规则2), (#子规则3)), Policy

AND运算符表示: 如果所有子规则都匹配,则该规则匹配


1
OR, ((#子规则1), (#子规则2), (#子规则3)), Policy

OR运算符表示: 如果子规则其中一个匹配,则该规则匹配


1
NOT, ((#子规则)), Policy

NOT运算符表示: 如果子规则不匹配,则该规则匹配


理解以上定义后, 我们可以使用逻辑规则脚本规则共同决定网络请求是否需要拦截, 并设置白名单


配置规则

  • 编辑Surge配置文件, 在[Rule]段落放入以下逻辑规则:
[Rule]
1
AND,((NOT,((OR,((USER-AGENT,Surge*), (RULE-SET,https://raw.githubusercontent.com/NobyDa/Script/master/Surge/Apple.list))))), (SCRIPT,RULE防火墙)),REJECT

以上逻辑规则已包含Apple/Surge白名单(匹配后放行), 可匹配绝大多数系统App或Surge自身发送的请求

  • 可自行配置白名单规则:

Surge内点击该AND规则 > NOT > OR > 新增 > 添加规则或规则集 > 完成

  • 网络请求经过Surge规则系统时, 该规则判断逻辑:

  • 至此, 已实现方案一需求:
  1. 打开手机内任何一个APP时(前台), 放行网络请求
  2. 关闭任何一个APP时(后台), 拦截所有请求
  3. 拦截的请求可以设置白名单

方案一: 注意事项

  • 该方案仅针对有特殊需求的用户设计, 一般用户不应该使用. 因为会产生各种副作用, 包括但不限于:
  1. iOS14桌面/负一屏小组件无法联网
  2. Surge后台定时脚本任务无法联网
  3. 基于主屏幕的Web, 例如BoxJSSub-Store 无法联网
  4. 低版本iOS系统老旧机型打开APP时,可能会无法触发捷径自动化进而造成APP无法联网

前三点虽然可以抓取请求配置规则白名单, 但相对来说比较麻烦.

  • 捷径通知

当我们设定 iOS 捷径内的「个人自动化」功能时, 虽然可以关闭 “运行前询问” 但是当自动化脚本开始执行时, 系统会通知提醒自动化已开始执行. 如果不想每次都收到这种通知, 那么我们其实可以通过一个系统BUG来关闭, 具体可点击此处查看具体方法; 有一点需要注意, 捷径通知关闭后如果运行的捷径里包含通知动作, 则该捷径将直接中断.


实现方案二

由于方案一过于暴力, 方案二应运而生. 该方案主要为本文的中心需求.

需求:

  1. 从蜂窝网络连接WiFi的瞬间, 15秒内拦截所有请求, 15秒后放行网络请求

  2. 拦截的请求可以设置白名单

思路:

  • 网络环境改变时, 触发运行Surge事件(Event)类型脚本, 脚本内容为: 如果从蜂窝网络切换至WiFi, 则写入当前时间戳数据, 再使用逻辑规则脚本规则共同决定15秒内的网络请求是否需要拦截

该方案完全基于Surge, 如果您 “不想了解具体原理“ 或者 “想跳过繁琐的配置“, 可使用懒人Surge模块一键配置, 模块地址:

1
https://gist.githubusercontent.com/NobyDa/fb026a6d01fec146bd451d01b0c973d5/raw/NetworkFirewall.sgmodule

已包含Apple/Surge白名单(匹配后放行)


第一步: 配置事件脚本

  • Surge内新建Event类型脚本, 脚本名暂且设置为 “WIFI防火墙” , 事件名设置为 “network-changed” , 编辑并填入以下脚本并保存
WIFI防火墙.js
1
2
3
4
5
6
7
8
9
10
11
const network = $network.wifi.ssid;
const currentTime = Date.now();

$httpAPI("GET", "v1/traffic", null, (body) => {
if (network && (currentTime / 1000) - body.startTime >= 3) {
const time = JSON.stringify(currentTime);
const addTime = $persistentStore.write(time, "WiFi_Timer");
$notification.post('防火墙开始拦截', '', `已从蜂窝网络切换至 ${network}`);
}
$done();
})

以下为科普脚本代码的具体用途及原理, 请选择性阅读.

WIFI防火墙.js
1
const network = $network.wifi.ssid;

在JS语法中, 声明一个名为network的常量, 值为Surge内部API, 该$network实际内容为当前网络状态的总览. (加入.wifi.ssid则表示仅提取当前WIFI名称)

该行用途: 事件脚本运行后读取当前网络的WIFI名称


WIFI防火墙.js
1
const currentTime = Date.now();

在JS语法中, 声明一个名为currentTime的常量, 值为JS中的内部函数, 该函数返回Unix时间戳


WIFI防火墙.js
1
$httpAPI("GET", "v1/traffic", null, (body) => { ... })

立即调用Surge内部函数并传入四个参数(method: String, path: String, body: Object, callback: Function), 该$httpAPI暂时没有官方文档, 但与HTTP API用法类似, 具体可查看HTTP API官方文档

该行用途: 由于Surge每次开启VPN时, 都会触发一次network-changed事件脚本, 为了避免错误触发导致的问题, 以上$httpAPI回调的参数将返回开启VPN的时间, 用于稍后的判断


WIFI防火墙.js
1
if (network && (currentTime / 1000) - body.startTime >= 3) { ... }

在JS语法中, 表示条件语句块; 我们经常需要根据不同条件来执行不同的代码, 以上条件语句可以实现这一点.

该行用途: 脚本运行后, 如果当前为WiFi环境 并且”当前时间开启VPN时间“ 大于等于3秒, 则执行花括号内{ … }的代码, 否则跳过


WIFI防火墙.js
1
const time = JSON.stringify(currentTime);

在JS语法中, 声明一个名为time的常量, 值为JS中的内部函数, 该函数实际用途为”将对象转换为JSON字符串”; 在当前代码中, 表示转换名为currentTime的常量 (Unix时间戳)

该行用途: 读取声明的Unix时间戳(数字类型), 并将它转换成字符串类型, 便于储存


WIFI防火墙.js
1
const addTime = $persistentStore.write(time, "WiFi_Timer");

在JS语法中, 声明一个名为addTime的常量, 值为Surge的内部函数, 该函数的实际用途持久化储存, 接受2个参数, 需要写入的数据(字符串类型), 以及一个固定的读取键(字符串类型)

该行用途: 写入(储存)名为time常量的值(Unix时间戳), 读取键为”WiFi_Timer”


WIFI防火墙.js
1
$notification.post('防火墙开始拦截', '', `已从蜂窝网络切换至 ${network}`);

立即调用Surge内部函数推送一个Surge通知, 并传入三个参数, 分别为标题, 副标题, 内容. 三个参数仅接受字符串类型


WIFI防火墙.js
1
$done();

立即调用Surge内部函数, 该Surge函数的实际用途为退出脚本执行; 使用Surge执行脚本时, 该API不可省略.


第二步: 配置规则脚本

  • Surge内新建Rule类型脚本, 脚本名暂且设置为”TIME防火墙”, 编辑并填入以下脚本并保存
TIME防火墙.js
1
2
3
4
5
6
7
8
9
10
11
12
let block = { matched: false };
const readTimer = $persistentStore.read("WiFi_Timer");
if (readTimer) {
const currentTime = Date.now();
const markTime = parseInt(readTimer);
if (currentTime - markTime <= 15000) {
block.matched = true;
} else {
const delTime = $persistentStore.write("", "WiFi_Timer");
}
}
$done(block);

以下为科普脚本代码的具体用途及原理, 请选择性阅读.

TIME防火墙.js
1
let block = { matched: false };

在JS语法中, 声明一个名为block的变量, 并初始化对象

该行用途: 由于是Rule脚本(使用脚本进行规则判定), 脚本结束时需要额外返回一个对象键值对表示是否匹配该规则; 此处是为了节省代码(重复段), 避免多次使用$done. 初始化的对象键值对默认为false(不匹配规则), 如果重新赋值(改变)为true则匹配规则


TIME防火墙.js
1
const readTimer = $persistentStore.read("WiFi_Timer");

在JS语法中, 声明一个名为readTimer的常量, 值为Surge的内部函数, 该函数的实际用途为读取之前所写入的数据, 接受1个参数: 读取键(字符串类型)

该行用途: 读取事件脚本所写入”WiFi_Timer”键的持久化数据(Unix时间戳)


TIME防火墙.js
1
if (readTimer) { ... }

如果名为readTimer的常量可以转换成true(有Unix时间戳), 则执行花括号内{ … }的代码, 否则跳过


TIME防火墙.js
1
const currentTime = Date.now();

在JS语法中, 声明一个名为currentTime的常量, 值为JS的内部函数, 该函数实际的值为当前Unix时间戳(数字类型)


TIME防火墙.js
1
const markTime = parseInt(readTimer);

在JS语法中, 声明一个名为markTime的常量, 值为JS的内部函数, 该函数在当前代码中的实际用途为 将”字符串” 转换成 “数字”类型, (转换名为readTimer常量的值)

该行用途: 读取事件脚本所写入的Unix时间戳, 并将它转换成数字类型, 便于判断


TIME防火墙.js
1
2
3
4
5
6
7
if (currentTime - markTime <= 15000) { //代码块1
//重新赋值(改变)block变量中matched键的值, true表示匹配规则
block.matched = true;
} else { //代码块2
//清除事件脚本所写入的Unix时间戳
const delTime = $persistentStore.write("", "WiFi_Timer");
}

如果当前Unix时间戳 事件脚本所保存的Unix时间戳 小于等于15000毫秒(15秒), 则执行代码块1的代码: 否则则执行代码块2的代码


TIME防火墙.js
1
$done(block);

立即调用Surge内部函数, 该函数的实际用途为退出脚本执行, 并返回block变量(脚本第一行)表示是否匹配规则


第三步: 配置逻辑规则

规则定义在方案一中的配置逻辑规则章节里有详细描述, 这里不再赘述

  • 编辑Surge配置文件, 在[Rule]段落放入以下逻辑规则:
[Rule]
1
AND,((NOT,((OR,((USER-AGENT,Surge*), (RULE-SET,https://raw.githubusercontent.com/NobyDa/Script/master/Surge/Apple.list))))), (SCRIPT,TIME防火墙)),REJECT

以上逻辑规则已包含Apple/Surge白名单(匹配后放行), 可匹配绝大多数系统App或Surge自身发送的请求

  • 可自行配置白名单规则:

Surge内点击该AND规则 > NOT > OR > 新增 > 添加规则或规则集 > 完成

  • 网络请求经过Surge规则系统时, 该规则判断逻辑:

  • 至此, 已实现方案二需求:
  1. 从蜂窝网络连接WiFi的瞬间, 15秒内拦截所有请求, 15秒后放行网络请求
  2. 拦截的请求可以设置白名单

方案二: 注意事项

  • 该方案相对来说比较收敛, 配置后对正常使用几乎没有影响, 适合大多数有隐私需求的用户, 但有几点需要注意:
  1. 如果您的WIFI路由器信号不稳定, 则可能会错误触发事件脚本导致间歇性断网.
  2. 从WiFi切换至蜂窝可能会触发事件脚本导致断网. (小概率)
  3. 从蜂窝切换至WiFi的瞬间, 可能会漏掉极少部分网络请求, 导致极少部分的网络请求拦截失败.
  4. 无法阻止手动打开系统设置查看WiFi列表所触发的网络请求.

前3点是Surge自身触发事件脚本的逻辑所决定的, 无法改进; 如果您在意, 可以选择同时配置方案一和方案二, 以掌握绝对的APP联网权限.


结语

本文内容相对来说比较硬核, 所列出的只是极其小众的需求, 非科班出身的同学可能会有理解障碍, 但完全理解后你会发现非常有意思.

Surge脚本规则执行效率极高, 在笔者的测试环境中(iPhone12), 绝大多数网络请求经过逻辑规则+脚本规则仅耗时5ms以内, 某些复杂请求可能会在10ms以内, 基本不必担心因使用脚本规则而带来的性能问题.

文章内的需求仅仅只是Surge高级用法的其中之一; 借助Surge进行全面的网络接管并使用捷径、规则、脚本可以建立出无限可能性.

数据时代, 隐私安全将会带来巨大挑战, 而在中国, 有这样的一种软件, 不绑定手机将无法使用或受到限制; 之后他们发现所有用户都绑定了手机, 然后他们得出一个结论: 中国人更愿意用隐私换取效率.

这种论调放在一百年前大概就是 “工人愿意选择工作16个小时, 放弃所有休息时间多赚钱养家糊口”. 殊不知可悲的是, 很多时候人们并没有别的选择.

不是我们不在乎隐私, 而是没有能力去抵抗.

以上

使用Surge脚本 & 逻辑规则建立联网防火墙

https://nobyda.github.io/2021/06/08/Surge_network_firewall/

作者

NobyDa

发布于

2021-06-08

更新于

2021-06-08

许可协议

评论