WebVPN
一个以 SQLite 为默认数据库的 Python 登录反向代理,用于从公网访问本机能够访问的内网 Web 服务。SQLite 模式无第三方依赖,MySQL 模式需要 PyMySQL。
文档导航:
部署时推荐只上传 webvpn-ubuntu-deploy.zip,不要上传整个开发目录。服务器只需要 Python 程序、deploy/ 模板和可选文档;本机配置、数据库和明文初始密码文件不应上传。
快速开始
需要 Python 3.10 或更高版本。首次运行生成随机密码、会话密钥和配置文件:
cd C:\Users\dearsky\Desktop\webvpn
python server.py init --username admin --target router=http://192.168.1.1
python server.py serve
默认监听 127.0.0.1:8080。打开 http://127.0.0.1:8080 登录。
也可以直接运行 .\start.ps1。首次运行时它会自动生成配置并显示随机密码。
用户、用户组、固定目标、临时目标、页面权限和匿名路由都保存在数据库中。默认数据库位置与 config.json 相同目录的 webvpn.sqlite3。
使用管理程序添加固定目标:
python manage.py targets add --name "路由器" --url http://192.168.1.1 --group "家庭网络" --order 10
python manage.py targets add --name "NAS" --url https://192.168.1.10:5001 --group "家庭网络" --order 20
python manage.py targets list
name 是登录后显示的友好标签。目标 key、协议、真实域名、IP 和端口不会出现在公开路由中。
管理员登录后,首页右上角有“用户管理”入口,可以:
- 添加普通用户或管理员。
- 修改用户密码和管理员权限,并设置是否允许用户自行修改密码。
- 删除用户;密码修改或账号删除后,该用户已有的登录会话立即失效。
- 最后一个管理员不能降级或删除,当前登录账号不能删除自己。
用户管理只对管理员可见,并带有 CSRF 校验。用户和目标数据直接写入数据库,其他服务进程在下次请求时即可看到变更,无需重启。
普通用户和管理员都可以从首页的“账户设置”查看自己的角色与用户组。默认可以输入当前密码后修改新密码;管理员可在“用户管理”中关闭某个账号的自助改密权限。自助改密成功后,其他设备上的旧会话失效,当前浏览器自动获得新会话。管理员还可从“用户组”页面创建用户组、编辑说明并勾选成员。
管理员首页右上角还有“应用管理”入口,可以直接在浏览器中:
- 添加固定应用名称和目标 URL。
- 修改固定应用名称、URL 和访问范围。
- 切换“仅管理员访问”。
- 将一个应用同时分配到多个页面分组,并设置组内顺序。
- 为应用内不同路径配置“仅允许”或“不允许”规则。
- 将页面规则应用于全部用户、指定用户或指定用户组。
- 删除固定应用及其匿名路由。
- 打开应用自动生成的匿名入口。
普通用户看不到应用管理入口,直接访问 /__webvpn/targets 会返回 403。固定应用通过管理页修改后会立即写入数据库,无需修改 config.json。
路径规则支持 /admin/*、/api/private/* 等通配形式。拒绝规则优先;只要路径存在允许规则,未被允许规则覆盖的用户就不能访问。规则对所有已登录用户生效,管理员账号也受页面规则限制。
管理员可在“页面分组”页面创建任意名称的分组,并独立调整分组顺序。首页按分组顺序显示;同一页面可出现在多个分组,组内按页面顺序排列;未加入任何分组的页面固定显示在最下方。点击目标应用会在新标签页打开;用户管理、用户组、页面分组、页面权限和系统设置仍在当前标签页打开。
管理员右上角的“设置”页面可以直接修改并立即应用:
- 公开域名和 Host 校验列表。
- 管理员可打开的主机白名单、CIDR、私有网络和链路本地策略。
- 登录会话时长、匿名路由有效期和 Cookie Secure 策略。
- 目标连接超时、读取超时、WebSocket 空闲超时、TLS 证书校验和访问日志。
数据库地址、监听地址、监听端口、TLS 证书路径和会话密钥只读展示,避免网页误操作导致服务失联。这些参数仍通过配置文件修改并重启服务。
也可以完全使用 Python 管理程序:
python manage.py users list
python manage.py users add alice --role user
python manage.py users passwd alice
python manage.py users role alice admin
python manage.py users reset alice deny
python manage.py users reset alice allow
python manage.py users delete alice
python manage.py groups add operators --member alice
python manage.py groups update operators --member alice --member bob
python manage.py groups list
python manage.py groups delete operators
python manage.py targets list
python manage.py targets access router admin
python manage.py targets access router all
python manage.py targets delete router
密码通过隐藏输入读取。数据库迁移和状态检查:
python manage.py db info
python manage.py db import-config
python manage.py db migrate --target-url "mysql://webvpn:密码@127.0.0.1/webvpn"
旧版本的 config.json 中如果仍有 users、targets 或 dynamic_targets,首次启动会自动导入数据库。导入完成后数据库是唯一数据源,后续不再读取这些旧字段。
数据库通过 config.json 配置:
"database": {
"url": "sqlite:///webvpn.sqlite3"
}
相对 SQLite 路径以配置文件目录为基准。也可以使用环境变量 WEBVPN_DATABASE_URL 覆盖。迁移到 MySQL 时,需要先安装 PyMySQL,并在 MySQL 中创建空数据库:
python -m pip install PyMySQL
python manage.py db migrate --target-url "mysql://webvpn:密码@127.0.0.1/webvpn"
程序使用相同的数据表和 SQL 语义处理 SQLite/MySQL。匿名路由属于登录会话,不会随数据库迁移;迁移后重新登录并打开应用会生成新的路由。迁移目标必须为空,避免覆盖已有数据。
两台服务器自动同步
两台服务器可以使用各自独立的 SQLite,并通过节点间双向同步实现同一域名下的无感切换。两台节点必须有:
- 相同的
session.secret。 - 相同的
sync.secret。 - 不同的
sync.node_id。 - 指向对方的
sync.peers。 - 相同的公开域名和用户授权策略。
局域网节点示例:
"sync": {
"enabled": true,
"node_id": "lan",
"secret": "两台服务器共享的至少32字符同步密钥",
"peers": [
{
"name": "wan",
"url": "https://webvpn.dearsky.top",
"connect_addr": "公网服务器IP:443"
}
],
"interval_seconds": 30,
"request_timeout": 10,
"verify_tls": true,
"clock_skew_seconds": 300,
"max_sync_bytes": 67108864,
"settings_updated_at": ""
}
connect_addr 会直接连接指定服务器,但保留相同域名的 TLS SNI 和 Host 校验,适合两台服务器共用域名但通过不同公网/内网地址互联的情况。
同步内容包括:
- 用户、密码哈希、管理员角色和
auth_version。 - 固定应用和管理员临时应用。
- 页面分组、分组顺序和页面的多分组归属。
- 当前有效会话路由。
- 删除墓碑,避免已删除用户或应用在另一节点重新出现。
- 管理员设置页中的共享运行设置。
本机专属的监听地址、端口、数据库 URL、TLS 证书、node_id 和 peer 列表不会互相覆盖。
同步频率、TLS、连接地址和节点 ID 在 sync 配置中设置。打开应用前会尽量先把新会话路由推送到对端;如果对端暂时离线,本地仍可访问,恢复后下一次同步会补齐。
同步快照包含密码哈希和会话密钥。节点同步必须使用受信任的 HTTPS 通道;只有完全隔离的实验网络才可以临时使用 HTTP。
访问格式
所有应用都通过服务端生成的不透明路由访问:
https://webvpn.dearsky.top/<随机路由>/<应用路径>
- 随机路由至少包含 20 个 URL 安全随机字符。
- 每个用户、每个登录会话分别获得独立路由;同一应用的链接不能跨用户或跨会话使用。
- 路由默认 60 分钟后过期。过期链接返回 410,用户需要回到首页重新打开应用,届时生成新的随机入口。
- 协议、域名、IP、端口只保存在数据库和内存中,不会出现在公开 URL、页面目标卡片或运行时脚本参数中。
- HTTP 和 HTTPS 目标统一使用
/<随机路由>/...,不再使用/http/<域名>或/https/<域名>,因此协议和主机都不会暴露。 - 旧的
/别名/...、/域名/...、/http/域名/...和/https/域名/...访问方式已经禁用。 - 会话路由记录保存在
webvpn_routes,包含所属用户、会话 ID 和过期时间。退出登录会立即删除当前会话的全部路由。 - 应用请求
/api、/static/...等绝对路径时,服务器会通过会话级webvpn_routeCookie 和同源 Referer 识别业务;Cookie 只保存随机路由,不包含真实目标。 - 上游服务设置的 Cookie 会按当前业务随机路由命名空间化,再以原名称和值转发回对应上游。浏览器中的业务 Cookie 使用
Path=/,因此绝对路径请求也能携带;WebVPN 不会把其他业务的 Cookie 发给当前服务器。 - 页面运行时脚本会把
document.cookie的读取和写入映射到当前业务命名空间。不同业务即使使用相同 Cookie 名称也不会相互覆盖。 localStorage、sessionStorage、IndexedDB 和 Cache Storage 同样按“用户 + 固定应用”生成匿名命名空间。应用仍使用原始键名和数据库名,不会看到真实域名或目标 key。
普通用户登录后只能看到和使用数据库中的固定应用。普通用户不能使用“打开主机”,也不能访问管理员创建的非固定应用。
管理员的“打开主机”表单使用 POST 提交。服务器会把目标保存为仅管理员可访问的临时应用,并返回新的随机路由;真实主机不会进入重定向地址或浏览器 URL。
如需把固定目标限制为仅管理员可访问:
python manage.py targets access router admin
HTTPS 与域名
推荐让 Caddy、Nginx 或 Cloudflare Tunnel 负责公网 HTTPS,再把请求转发到 127.0.0.1:8080。仓库中的 Caddyfile 可用于:
caddy run --config Caddyfile
Caddy 会自动申请 webvpn.dearsky.top 的 TLS 证书,并反向代理到 Python 服务。若已有 Nginx,需要保留原始 Host,并设置 X-Forwarded-Proto、X-Forwarded-Host。
也可以直接在 config.json 配置 server.tls_cert 和 server.tls_key,让 Python 自己监听 HTTPS。若直接公网监听,建议把 server.host 改为 0.0.0.0,并开放对应端口。
Ubuntu + Apache2 的 systemd 服务、虚拟主机和一键安装说明见 deploy/README.md。
安全模型
- 未登录请求不会到达目标服务;登录会话使用 HMAC-SHA256 签名,Cookie 为
HttpOnly、SameSite=Lax。 - 登录失败按来源 IP 限速,密码使用 PBKDF2-SHA256(60 万轮)保存。
- 只有
server.trusted_proxies中的反向代理地址可以提供转发头;直接公网监听时不会信任客户端伪造的来源或协议。 - Apache 模板会设置受信的
X-Forwarded-Proto和X-Forwarded-Host,并清除客户端提供的X-Forwarded-For后重新写入真实来源。 - 普通用户只能访问固定的、非
admin_only目标;只有管理员可以创建临时应用,临时应用也只对管理员开放。 - 管理员打开未知目标时默认只允许私有、回环或内网地址,拒绝公网地址、组播和链路本地地址(包括常见云元数据地址)。
- DNS 会在验证后固定到已检查的 IP,HTTPS 仍使用原域名进行 SNI 和证书校验,降低 DNS 重绑定风险。
- 支持
allowed_hosts精确主机、*.example.lan通配符和 CIDR。只在确有需要时放宽。 - 代理会过滤 WebVPN 会话及业务路由 Cookie,移除上游
Domain属性,对业务 Cookie 做随机路由命名空间隔离,并重写跳转地址、HTML、JavaScript、CSS、CSP 和常见响应头中的真实来源。 - 上游身份相关响应默认标记为
private,移除public、s-maxage、CDN-Cache-Control和Surrogate-Control,并发送Vary: Cookie,避免共享缓存混用不同用户或业务的内容。 - 固定目标和管理员临时应用全部保存在
webvpn_targets,匿名路由保存在webvpn_routes。 - 可以通过
session.route_ttl_minutes调整路由有效期,范围为 1 分钟到 7 天。 - 页面会注入轻量运行时补丁,并通过路由 Cookie 处理 HTML、CSS、JavaScript 和表单发出的根路径请求。多个应用同时打开时优先用 Referer 区分;禁止 Referer 的客户端只能回退到最近访问的应用。依赖跨域 iframe、WebAssembly 特殊路径的站点仍可能需要目标站点自身的反向代理基础路径配置。
- 支持普通 HTTP 流、SSE 长连接、文件上传和 WebSocket 隧道。
- HTTP 服务使用多线程模型,每个连接独立处理,SSE、WebSocket 或慢速上游不会阻塞其他用户请求。
运维
# 启动服务
python server.py serve
# 临时覆盖监听地址
python server.py serve --host 0.0.0.0 --port 8080
# 健康检查
Invoke-RestMethod http://127.0.0.1:8080/__webvpn/healthz
生产环境建议使用 Windows 服务、计划任务或进程管理器维持 Python 服务,并让 Caddy 负责自动 HTTPS。通过管理页或 manage.py 修改用户、密码或目标后不需要重启服务。
如果目标采用自签名证书,可以设置 proxy.verify_tls 为 false 或通过 proxy.ca_file 指定受信任 CA。除非目标不在你的控制之下,不建议关闭证书校验。