隧道与穿透:配置受限隧道与访问路径 · 前置、依赖与输入至产品配置与操作契约
本篇以隧道两端配置为例;前置是监听端、控制通道、目标服务与回程。源码与输入与下文配置共用同一份文件,示例未执行。
隧道与穿透:配置受限隧道与访问路径
本篇以隧道两端配置为例;前置是监听端、控制通道、目标服务与回程。源码与输入与下文配置共用同一份文件,示例未执行。
前置、依赖与输入
隧道与穿透完整示例是离线证据处理器,示例未执行。需要Python3.11以上标准库,第三方依赖为零,不需要服务、管理员或网络。输入JSON只含虚构记录,程序不调用系统命令、不建立连接、不修改主机。完整文件位于examples/m5-next-tunnels,必须一起阅读README、lab.py和case.json。
完整源码与配置
下面包括完整源码与全部输入;这不是产品配置验证器,也不执行协议、密码学或系统策略。判断只依据输入声明,不能用输出证明真实环境。
"""Offline teaching model. No network, subprocesses, or system mutations."""
import json
from pathlib import Path
import sys
def require(value, schema):
if type(value) is not dict or set(value) != set(schema):
raise ValueError("unexpected or missing fields")
for key, expected in schema.items():
if type(value[key]) is not expected:
raise ValueError("wrong field type: " + key)
def decide(row):
require(row, {"name": str, "listener": str, "approved_listener": str, "destination": str, "approved_destination": str, "return_route": bool})
import ipaddress
listener = ipaddress.ip_address(row["listener"])
approved = ipaddress.ip_address(row["approved_listener"])
destination = ipaddress.ip_address(row["destination"])
target = ipaddress.ip_address(row["approved_destination"])
if listener != approved:
return "DENY_LISTENER_SCOPE"
if destination != target:
return "DENY_DESTINATION"
if not row["return_route"]:
return "CHECK_RETURN_ROUTE"
return "BOUNDARY_PLAN_ACCEPTED"
def main(args):
if len(args) != 1:
print("usage: python lab.py case.json", file=sys.stderr)
return 2
try:
source = Path(args[0]).read_text(encoding="utf-8")
data = json.loads(source)
if type(data) is not list or not data:
raise ValueError("nonempty list required")
# Validate every row before publishing any decisions.
results = [(row, decide(row)) for row in data]
for row, result in results:
print(row["name"] + ": " + result)
return 0
except (OSError, ValueError, KeyError, TypeError, OverflowError):
print("invalid input", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))[
{
"name": "loopback",
"listener": "127.0.0.1",
"approved_listener": "127.0.0.1",
"destination": "192.0.2.10",
"approved_destination": "192.0.2.10",
"return_route": true
},
{
"name": "wildcard",
"listener": "0.0.0.0",
"approved_listener": "127.0.0.1",
"destination": "192.0.2.10",
"approved_destination": "192.0.2.10",
"return_route": true
},
{
"name": "missing-return",
"listener": "127.0.0.1",
"approved_listener": "127.0.0.1",
"destination": "192.0.2.10",
"approved_destination": "192.0.2.10",
"return_route": false
}
]读者自跑、成功和失败
隧道与穿透离线推演先复制三个文件到独立目录。Windows PowerShell使用python,Linux Bash使用python3调用lab.py case.json;命令在README完整列出。正常输入输出逐行判定并以0结束;缺字段、非法类型或JSON损坏输出invalid input并以2结束。预期拒绝是业务判定,不等同输入错误,仍以0结束。
恢复、清理与边界
恢复时从仓库还原case.json,重新核对变更前后字段。程序不写结果文件,不产生系统资源,不需要关闭端口或停止服务。清理只删除读者自己复制的练习目录;不要删除仓库示例。修改输入不会改变真实授权、证书或路由。
综合练习与分开的解答
练习:先不看参考答案,解释输入中每个正常和失败判定,修改一项条件让一个判定改变,再写出该变化仍不能证明的现实条件。
参考解答
loopback监听与目标都等于批准值且声明回程存在,得到BOUNDARY_PLAN_ACCEPTED。wildcard的0.0.0.0不等于批准的127.0.0.1,优先DENY_LISTENER_SCOPE。missing-return只声明缺回程,输出CHECK_RETURN_ROUTE;不自动添加NAT或路由。
具体变体:只把 wildcard 的 listener 从 0.0.0.0 改为 127.0.0.1,其余字段不变,将由 DENY_LISTENER_SCOPE 变为 BOUNDARY_PLAN_ACCEPTED。真实监听、目标身份和返回路由没有被程序观察,不能把JSON修改当作收紧真实入口。
产品配置与操作契约
以下为与离线模型分开的完整产品配置,示例未执行。产品二进制、真实密钥与账户均需读者按前置另行提供;静态解析不等于产品验证。
# frp v0.61.1; replace documentation address and all secrets before isolated use.
bindAddr = "192.0.2.10"
bindPort = 7000
proxyBindAddr = "127.0.0.1"
allowPorts = [{ single = 6000 }]
maxPortsPerClient = 1
detailedErrorsToClient = false
auth.method = "token"
auth.token = "READER_MUST_REPLACE_WITH_SHARED_RANDOM_SECRET"
auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
transport.tls.force = true
transport.tls.certFile = "./secrets/frps.crt"
transport.tls.keyFile = "./secrets/frps.key"
log.to = "console"
log.level = "info"# frp v0.61.1; this maps existing SSH on the client to loopback on the server.
serverAddr = "192.0.2.10"
serverPort = 7000
auth.method = "token"
auth.token = "READER_MUST_REPLACE_WITH_SHARED_RANDOM_SECRET"
auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
transport.tls.enable = true
transport.tls.trustedCaFile = "./secrets/root.crt"
transport.tls.serverName = "frps.example.invalid"
log.to = "console"
log.level = "info"
[[proxies]]
name = "lab-ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6000完整FRP两端配置(未执行)
frps.toml与frpc.toml按v0.61.1固定字段编写。读者需已有该版本二进制、获批两台隔离主机及客户端本机既有SSH服务。192.0.2.10必须替换为服务端实际地址;只允许客户端到服务端7000的必要网络路径,禁止将示例文档地址部署为公网地址。
两端auth.token必须替换成同一份独立随机秘密,不能使用仓库里的明显占位值。服务端secrets/frps.crt及frps.key由读者自供,证书SAN须含frps.example.invalid(实际部署也可统一改成获批域名);客户端secrets/root.crt是对应已核验CA。不得关闭trustedCaFile或名称校验以通过连接。私钥与token文件只允许相应进程身份读取。
proxyBindAddr限制映射入口在服务端回环6000,allowPorts只允许6000,管理面板不启用。frpc的localIP属于客户端,因此连接的是客户端127.0.0.1:22。7000是控制/工作连接入口,6000是被映射入口。客户端到代理的TLS不替代被映射SSH服务的主机验证与用户授权。
读者获得部署授权后,在各自包含配置及secrets的隔离目录分别运行:
./frps -c ./frps.toml./frpc -c ./frpc.toml预期客户端注册lab-ssh,服务端仅回环6000可作为SSH入口;服务端以外主机不能直接连接6000。令牌不同预期注册拒绝,证书不可信或名称不匹配预期TLS拒绝,客户端SSH未监听则注册可能成功但业务连接失败。此处均为预期。
SSH本地转发的完整命令(未执行)
前置:独立核验并预置192.0.2.10主机公钥、提供有效student账户私钥,服务端允许到其自身127.0.0.1:6000的转发。这里直接连接FRP主机,不使用限制目的地为2222的SSH课程跳板配置。
ssh -N -T -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes -o ExitOnForwardFailure=yes -o ForwardAgent=no -i /srv/tunnel-lab/client_ed25519 -L 127.0.0.1:16000:127.0.0.1:6000 student@192.0.2.10本机16000只监听回环,转发目标127.0.0.1:6000属于SSH服务端。ExitOnForwardFailure能报告初始转发建立失败,但不保证之后FRP或目标服务始终可用。经此入口再连接SSH仍须核验最终被映射主机的身份。
恢复时撤销本次映射或还原已批准端口范围,保留原管理入口;清理只结束本次前台frpc/frps/转发会话,删除分发的实验秘密并核查相应授权已撤销。不终止同名其他进程。本项目未创建这些进程。WireGuard的独立两端模型见下节,不能与FRP端口映射混为同一机制。
继续阅读
- 知识专题
- 知识专题
- 应用实践