Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HotspotGate · 热点门

把一台闲置的 Android 手机变成常开的 Wi-Fi 热点网关——插上电放在角落, 开机自动开热点、掉线自动重开,连上的人扫码即入网, 你随时能看到谁在用、用了多少、还剩多少额度。

为什么会有这个东西

手机自带的「个人热点」当临时救急很好用,但真要长期当路由器使,会撞上四件事:

  1. 它会自己关掉。系统有空闲超时(实测 10 分钟没人连就断),你人不在旁边就没了。
  2. 看不见谁在用。系统只给一个设备数,没有名字、没有分设备流量, 蹭网的和自己的设备长得一模一样。
  3. 不知道流量花在哪。等运营商短信提醒时,套餐已经用超了。
  4. 改个配置要拿起手机。人在电脑前,想换个频段还得去够手机。

HotspotGate 把这四件事补齐:保活轮询顶掉空闲超时;靠 mDNS / DHCP 指纹认出 「MacBook Air」「iPhone」而不是一串随机 MAC;分层时序库记下每台设备每个时段的消耗; 连上热点的任何设备打开浏览器就能改配置——它本身就是个可运维的网关,而不只是一个开关

需求见 docs/requirements.md


长什么样

局域网 Web 后台

连上热点的任何设备,浏览器打开 http://<网关IP>:8080 就是这个页面。 不需要装任何东西,资源全部本地打包、不依赖 CDN——连上来的设备很可能根本没有外网。

Web 后台概览

顶部四格是最常看的读数:累计流量、在线设备数、实时上下行网速、服务连续运行了多久。 下面是连接二维码和热点参数,右下角可以直接改配置、重开或关掉热点。

设备列表 流量趋势
设备:自动认出机型、配品牌图标,可改名、打标签、分组 流量:1 小时到 30 天的消耗曲线 + 设备用量排行
SIM 卡管理 热点设置
SIM:信号强度、4G/5G 切换、按卡设流量上限、超限自动断这张卡 热点设置:改 SSID、密码、频段、加密方式

开了访问控制之后,打开后台要先登录。账号在手机 App 上设:

登录页

截图里的二维码是演示用的占位码,不是真实密码。

手机 App

概览 读数 设备
扫码入网 实时读数 设备识别
SIM 设置
SIM 卡管理 设置

App 里的组件是在 Compose 里复刻的 shadcn/ui,不是 Material3 的默认外观—— 没有紫色、没有填充式输入框、没有涟漪,和 Web 端共用同一套设计 token。


它能做什么

  • 开机自启:系统启动后自动拉起前台服务并打开热点,不用手动开 App
  • 掉线自愈:热点被系统关掉(比如空闲超时)会自动重开;你自己主动关的则不会被拉起来
  • 扫码入网:按 Wi-Fi QR 标准生成二维码,iPhone 相机和 Android 系统扫码都能直接连
  • 自动认设备:靠 mDNS / DHCP 指纹 / 浏览器 UA 判断 iPhone、iPad、Mac、Android、Windows, 配对应品牌图标,还能拿到「小明的 MacBook Air」这样的真实设备名
  • 重命名与分组:给设备起名、打标签、分组,按 MAC 记住,重连不丢
  • 实时网速:SSE 推送,总网速和每台设备的网速都能看
  • 流量趋势:分层降采样的时序库,1 小时到 30 天的消耗曲线与设备用量排行
  • SIM 卡管理:每张卡的运营商、信号强度、4G/5G 制式;可锁 4G、切数据卡、单独断某张卡的网
  • 按卡设流量上限:每张 SIM 独立额度与账单日,超限只断那一张卡(热点不断,设备仍在局域网内)
  • 用量可校准:App 往往装得比卡开通晚,之前的流量它无从得知;照着运营商 App 的数字填进来即可
  • 局域网 Web 后台:连上热点的任何设备打开 http://<网关IP>:8080 就能看状态, 也能直接改 SSID / 密码 / 频段、重开热点
  • 后台登录保护:账号密码在手机 App 上设,密码只存 PBKDF2 哈希,连错 5 次锁 5 分钟
  • 运行时长:看服务连续运行了多久——归零就说明它中途被系统杀过
  • 常驻通知:通知栏显示热点状态、设备数和后台地址

环境要求

要求
设备 已 root(Magisk),开发机型为 Redmi K40 / LineageOS
系统 Android 8.0+(minSdk 26),实测 Android 16
JDK 17(AGP 8.13 不支持更高版本,用 25 会构建失败)
Android SDK platform-36、build-tools 36.0.0
Node 18+(只有改 Web 前端时才需要)

编译

1. 配置 SDK 路径

echo "sdk.dir=$HOME/Library/Android/sdk" > local.properties

如果你的 SDK 装在别处(例如 Homebrew 的 android-commandlinetools),把路径换掉即可。

2. 构建 APK

JAVA_HOME=/opt/homebrew/opt/openjdk@17 ./gradlew :app:assembleDebug

产物在 app/build/outputs/apk/debug/app-debug.apk

3. 安装

adb install -r app/build/outputs/apk/debug/app-debug.apk

修改 Web 前端(可选)

Web 后台是独立的 Vite + React + Tailwind + shadcn/ui 工程,构建产物已经提交在 app/src/main/assets/web/只改 Android 代码的话不需要跑这一步

cd web
npm install
npm run build

产物会直接输出到 app/src/main/assets/web/,再重新构建 APK 即可。 所有资源都本地打包,不依赖任何 CDN —— 连上热点的设备可能根本没有外网。


首次使用

0. 记下随机生成的密码

首次启动时会给这台设备随机生成一个 12 位密码(去掉了 0/o/1/l/i 这些容易认错的字符), 在「设置 → 密码 → 显示」里能看到,也可以直接改成自己想要的。

这里没有写死一个「出厂默认密码」,是因为那个值会随开源代码公开—— 所有没改过密码的安装就都共用同一把钥匙了。

热点密码就是这套系统的总钥匙:连上来的人默认能打开 Web 后台。 下一步的访问控制能把后台再锁一层,但热点密码本身给谁,还是要想清楚。

1. 授予 root(必须)

App 需要 root 才能开关系统热点。首次启动时 Magisk 会弹授权框,点「允许」。

如果没弹框,或者不小心点了拒绝(比如弹框时手机是息屏的,会超时自动拒绝):

打开 Magisk → 底部「超级用户」→ 找到 HotspotGate → 把右边的开关打开

被拒绝过一次后 Magisk 会记住,不会再弹框,只能去这里手动打开。

排查用:

adb shell su -c '/debug_ramdisk/magisk --sqlite "SELECT * FROM policies"'

policy=2 是允许,policy=1 是拒绝。

2. 开启后台登录(建议)

「设置 → Web 后台访问控制 → 需要登录」,设一个用户名和密码(至少 8 位)。

不开的话,连上热点的任何人打开 http://<网关IP>:8080 就能看到全部状态, 还能改你的 SSID 和密码。默认关着只是因为总得让人先有机会设密码,不是因为它不重要。

密码只存不可逆的哈希,忘了就在这里重设一个——改凭据会让所有已登录的浏览器重新登录。

3. 允许通知(建议)

Android 13+ 会请求通知权限。拒绝的话服务照常运行,只是看不到常驻通知。

4. 开机自启

默认开启,在「设置 → 启动与保活」里。服务拿到 root 后会自动做这些加固:

  • 加入 Doze 白名单,避免深度待机时轮询被冻结
  • 保持在 active 待机桶里,不被 App Standby 降级

部分 ROM(尤其是 MIUI 系)还需要在系统设置里单独允许「自启动」和「后台运行」。


访问 Web 后台

  1. 用另一台设备连上热点
  2. 浏览器打开 App 概览页显示的地址,形如 http://192.168.x.x:8080

网关 IP 不是固定的 —— Android 12 起 tethering 网段是随机的(实测会分到 10.x.x.x/24), 所以以 App 里显示的为准,别硬记 192.168.43.1

Web 服务只绑定在热点网卡的 IP 上,并且会拒绝非本网段来源的请求,不会暴露到公网或移动数据。 开了访问控制之后还要再过一道登录,见下文。

端口被占用时 App 会明确提示,在「设置 → Web 后台与统计」里换一个即可。


设备识别是怎么做的

现代手机连 Wi-Fi 默认用随机 MAC(本地管理地址),所以查 OUI 厂商表这条路是死的—— 很多同类工具就栽在这。这里靠三路信号融合,按可信度取高的:

信号 能认出 可信度
mDNS 的 _companion-link TXT 机型标识 iPhone14,5 / MacBookAir10,1 95
浏览器 User-Agent(打开过本后台的设备) 全平台 99
DHCP Option 60 vendor class android-dhcp-16 → Android,MSFT 5.0 → Windows 95
mDNS / DHCP 主机名关键词 iPhone、MacBook、DESKTOP-* 等 85
mDNS 服务类型(_remotepairing 等) Apple 生态、iOS 17+ 35–60

有个坑值得记一笔:mDNS 报文里的名字不一定属于发送者。设备查询别人的服务时, 问题段里写的是对方的名字——iPhone 找 小明的MacBook Air._companion-link._tcp.local 时, 那串是 Mac 的名字。所以命名只认「自报家门」的部分:先用 A 记录里 rdata 等于来源 IP 的那条 锁定主机名,再取指向该主机名的 SRV 实例名。


SIM 卡与流量上限

用量口径是整张卡的全部移动数据——手机自己刷视频的、后台同步的、经热点转发的全算在内。 运营商就是这么计费的,只统计热点转发的部分会漏掉大头,额度永远等不到。

实现上没走 NetworkStatsManager:Android 11 起它的 subscriberId 必须传 null, 拿不到 IMSI 也就没法按卡区分,还要 PACKAGE_USAGE_STATS 这种特殊授权。改成两步更简单:

  1. dumpsys netstats 的 Active interfaces 段直接给出 iface → subId 映射;
  2. 再读内核的 /sys/class/net/<iface>/statistics,按 subId 聚合。

只统计 metered=true 的接口——metered=false 的那条是 IMS 信令专用 APN,运营商不计费。 接口计数在 rmnet 重连时会归零,所以基线落库,跨重连和跨应用重启都能连续累加。

两个踩过的坑

Android 的 grep 是 toybox,不支持 {n,m} 大范围重复计数。 在设备侧写 grep -oE 'xxx.{0,300}yyy' 会直接报 invalid repetition count(s) 导致整条命令失败。 需要截断的地方一律整行取出,交给 Kotlin 侧的正则处理。

切换 4G/5G 的参数是二进制字符串cmd phone set-allowed-network-types-for-users 的参数名虽然叫 NETWORK_TYPES_BITMASK,但它既不收十进制数字、也不收 LTE|NR 名字列表—— AOSP 里的校验是 bitmaskString.matches("[01]+"),要的是 10101001110000100 这种二进制串。 传错只会得到一句 No valid NETWORK_TYPES_BITMASK

而且不能凭空构造一份制式清单下发,那会把设备原本支持的制式改没。正确做法是读出当前掩码 (dumpsys isuballowedNetworkTypesForReasons=user=NNN),只增删 NR 那一位(1 shl 19)。 -s 跟的是卡槽号不是 subId(传错会得到 N is not a valid SLOT_ID)。

别碰 Settings.Global.preferred_network_mode。这一版踩过:它在现代 Android 上已经是遗留物, 谁都能写、写了也不改变实际制式,而真正生效的只有上面那个掩码。当时读它当界面状态、 又顺手写它当「持久化偏好」,结果同一台机器上三个值指向三个答案—— preferred_network_mode 卡槽 0 段是 26(含 5G)、系统自己维护的 preferred_network_mode1 是 11(仅 4G)、 掩码里 NR 位却是 1。界面读了其中一个,于是切换「点了没反应」。 读写认准掩码这一处就不会打架,下发后再回读校验一次 NR 位,不一致就如实报错, 不要让界面显示成功而实际没动。


Web 后台的访问控制

默认关着,在手机 App 的「设置 → Web 后台访问控制」里开。开了之后打开后台要先登录, 账号密码也在那里设——Web 端只能登录,不能改自己的凭据, 不然拿到一个会话就能顺手把管理员锁在门外。

几个实现上的选择:

  • 密码只存 PBKDF2-HMAC-SHA256 派生值和 16 字节随机盐,不存明文。 迭代 12 万次:OWASP 给服务器的建议是 60 万,但这跑在一台旧手机上, 登录要等两三秒就没法用了;配合随机盐,离线爆破一个像样的密码依然不划算。
  • 会话只在内存里。进程重启后全部失效——服务被杀过一次,中间发生了什么谁也说不准, 让人重登一次的代价很小。Cookie 带 HttpOnly(脚本读不到,XSS 偷不走) 和 SameSite=Strict(挡住 CSRF)。
  • 连错 5 次锁 5 分钟。局域网的攻击面不大,但密码往往很短, 没有节流的话一个脚本几分钟就能把常见密码表跑完。
  • 不区分「用户名错」和「密码错」,那等于白送攻击者一半答案。 用户名和密码都走常量时间比较。
  • 在 App 上改凭据会清掉全部会话,连同失败计数和锁定。 密码换了而旧 Cookie 还能用,这次「改密码」就没真正生效; 而锁定不清的话,任何人故意连错 5 次就能把真正的管理员锁 5 分钟—— 防暴破反倒成了拒绝服务。

静态资源不设防:前端得先加载出来才能画登录页,而它本身不含任何数据, 真正的东西都在 /api 后面,那一层是一律过闸的。


关于频段

默认走 5 GHz(实测跑在信道 36/44、80MHz 带宽、Wi-Fi 6)。

5 GHz 更快,但穿墙差、部分老设备不支持。隔墙连不上或者对方设备连不上时, 去「设置 → 频段」改成 2.4 GHz。

改完频段要重启热点才生效,App 会问你要不要立刻重启。


关于分设备流量

总流量读的是 AP 网卡的 /sys/class/net/<iface>/statistics,稳定可靠。

分设备流量靠自建的两条 iptables 计数链(按源/目的 IP 统计)。但 Android 的 tethering 会做 eBPF/硬件 offload,被 offload 的转发流量根本不经过 netfilter,计数链会一直是 0。

App 会自动检测这种情况(总流量在涨、计数链纹丝不动),把分设备数据标记为「暂不可用」, 而不是显示一堆假的 0。

想要精确到每台设备,在「设置 → 精确分设备流量」里打开 —— 它会关掉系统的转发加速, 代价是转发全部走软件路径,高速下载时更费电、吞吐略降。默认是关的。


技术选型

单模块 Kotlin 工程,依赖压到最少:

用途 方案
UI Jetpack Compose,自己实现的 shadcn/ui 组件(见下)
图标 Lucide(SVG path 直接喂给 PathParser
二维码 zxing-core,只用编码部分
Web 服务 NanoHTTPD
存储 DataStore(配置)+ Room(设备档案、流量时序、配额)
实时推送 SSE(NanoHTTPD chunked + EventSource
图表 Recharts
root 自己实现的常驻 su shell(core/RootShell.kt

热点控制走的是 root + cmd wifi start-softap。没用 TetheringManager / WifiManager 的隐藏 API,是因为那条路需要签名级权限,第三方应用拿不到;而 cmd wifi start-softap 在 root 下走的是完整 tethering 流程(回显里能看到 TetheringRequest), DHCP 和 NAT 都由系统负责,正是我们想要的。

RootShell 保持一个长期存活的 su 子进程,命令通过 stdin 送进去、用哨兵行标记输出结束。 不是每条命令都 fork 一个 su —— 那样 Magisk 每次都要走一遍鉴权,很慢也很费电。 拿不到 root 时会指数退避(30s 起,最多 5 分钟),否则几秒一轮的状态轮询会把 Magisk 的授权框一遍遍怼到用户脸上。


关于 UI

App 和 Web 两端共用同一套 shadcn/ui 设计 token,base color 取 stone—— 需求文档给的那组颜色(#FAFAF9 / #E7E5E4 / #1C1917 / #78716C / #F5F5F4) 恰好就是 Tailwind 的 stone 色阶。

Web 端是真的 shadcn/ui(Radix + cva + Tailwind v4 + lucide-react)。

Android 端不存在 shadcn 这个库,所以在 Compose 里复刻了它的设计系统ui/components/ 下的 Button / Input / Select / Switch / Card / Badge / Dialog 全部自绘, 不使用 Material3 的默认外观(没有紫色、没有填充式输入框、没有涟漪)。 交互反馈按 shadcn 的路子走:hover:bg-primary/90 映射成按下变色, 输入框有 ring-2 ring-offset-2 的聚焦环,禁用态是整体 opacity-50


已知限制

  • 设备名不是一定能拿到。Android 的 tethering DHCP 服务跑在 NetworkStack 进程内、 不落地 lease 文件,所以名字要靠 mDNS 广播和 DHCP 抓包碰运气。 Apple 设备基本都能拿到(它们在 mDNS 上很活跃),沉默的设备会退化成 iPhone A1B2 这样的占位名——手动改个名就好。 另外 iOS 17+ 有些服务实例名是随机 UUID(故意防止设备名泄露),这类会被直接忽略。
  • 分设备流量可能不可用,原因见上。
  • 热点空闲会被系统自动关闭(实测超时 10 分钟)。App 会尝试写 settings put global soft_ap_timeout_enabled 0,但这个设置项不一定被所有 ROM 采纳; 真正兜底的是服务侧的保活轮询,检测到热点掉了就重开。
  • Web 后台的登录默认是关着的。开着的话第一次打开就要密码、而用户还没设过, 人就被自己的后台锁在外面了。没开的时候,连上热点的人都能打开它、也都能改热点配置, 所以装好之后建议去「设置 → Web 后台访问控制」开上。
  • 后台走的是 HTTP,不是 HTTPS。登录密码在局域网内是明文传输的: Wi-Fi 本身有 WPA2 加密,外人抓不到,但同一个热点下做 ARP 欺骗的人能截到。 自签证书要每台设备手动装信任链,对一个「连上就能看」的面板来说代价太大, 所以这里选了不做——别把这个热点的密码给不信任的人,那才是真正的边界。
  • 改 SSID / 密码必须重启热点,hostapd 不支持热更新。两端都会先问你再重启。
  • 双卡切换没有实测。开发设备只插了一张卡,「设为数据卡」这条路径代码写了但没能真机验证。
  • 固定域名访问(gate.lan)还没做。需要在 app 内跑 DNS 服务并用 iptables 把 53 端口 REDIRECT 过来,属于会动数据通路的改动,留到最后单独做。

项目结构

app/src/main/java/com/hotspotgate/app/
├── core/          RootShell、二维码、格式化
├── data/          数据模型、DataStore、Room 实体与 DAO
├── discovery/     mDNS 报文解析、DHCP 抓包、设备指纹与档案
├── hotspot/       热点控制、客户端发现、流量统计与时序库、配额
├── sim/           SIM 读取、分卡流量、制式与数据开关
├── service/       前台服务、通知、开机广播、保活加固
├── ui/            Compose 界面
│   ├── components/  shadcn 组件的 Compose 实现
│   ├── icons/       Lucide 图标
│   ├── screens/     概览 / 设备 / SIM / 设置
│   └── theme/       设计 token
└── web/           NanoHTTPD 服务、JSON 接口、SSE、登录与会话

web/               Web 后台前端(Vite + React + shadcn/ui)

排查

看 App 日志:

adb logcat -s HotspotGate:*

手动验证开机自启(不用真的重启手机):

adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n com.hotspotgate.app/.service.BootReceiver

看热点当前状态:

adb shell su -c 'dumpsys wifi | grep -m1 mApInterfaceName'

登不进 Web 后台:连错 5 次会锁 5 分钟,在手机上重新保存一次凭据即可立刻解锁 (改凭据会连同失败计数和锁定一起清掉)。忘了密码就在 「设置 → Web 后台访问控制」重设一个——存的是不可逆哈希,找不回来。

看后台是不是开着登录保护:

curl -s http://<网关IP>:8080/api/auth/session

切 4G/5G 不生效:真正决定制式的是这个掩码,preferred_network_mode 是不生效的遗留项:

adb shell su -c "dumpsys isub | grep -o 'allowedNetworkTypesForReasons=user=[0-9]*'"

第 19 位(1 shl 19)是 NR。578439 含 NR 就是 5G 优先,去掉那一位就是锁 4G。

About

把闲置 Android 手机变成常开的 Wi-Fi 热点网关:开机自启、掉线自愈、设备识别、流量趋势、SIM 卡管理,还带一个局域网 Web 后台

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages