Documentation
¶
Overview ¶
Package egressproxy 进程级出口强制:一个只监听回环的本地 HTTP 代理 (含 `CONNECT` 隧道),作为「声明了受限出口」的**兑现件**。
它补的是哪个洞 ¶
子进程(以及宿主起的 curl / git / 各种 native 工具)的出站流量在编排进程的 任何 Go 层守卫之外:`HTTP_PROXY` 只是一组环境变量,子进程可以忽略它。 于是「声明了受限出口」与「实际能出网」之间没有任何东西——直到有一个**真的 在中间拦着**的代理,并且子进程只能经它出去。
两层缺一不可:
- 代理(本包):真的拦、真的拒、默认拒绝;
- 环境下发(agent-harness 的 `egress.Spec.Env`):把子进程指过来。
只有其中一层都等于没有:代理没人用是装饰,env 没人拦是空话。
边界 ¶
- **只监听回环**(`127.0.0.1:0`),不对外提供服务;
- 只做**目标准入**(主机 + 端口 + 只读方法),不看内容——`CONNECT` 之后是 TLS,看不到任何字节,这一点在 `Limited` 的文档里写清,不假装能看见;
- 不认识任何业务概念:白/黑名单由调用方给,本包只判。
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BlockedError ¶
BlockedError 准入拒绝(结构化:调用方按需呈现原因与目标)。
func (*BlockedError) Error ¶
func (e *BlockedError) Error() string
type Policy ¶
type Policy struct {
// AllowHosts 允许的目标主机:精确主机名,或通配后缀 "*.example.com"。
//
// 通配语义**只匹配子域**:`*.example.com` 命中 `a.example.com` 与
// `a.b.example.com`,**不**命中 `example.com`(要它请显式写一条),
// 也**不**命中 `evilexample.com`(后缀按标签边界比,不做裸后缀比较)。
// 单独一个 "*" 表示放行全部主机(此时 `DenyHosts` 仍然优先)——
// 这是「只做黑名单」的唯一写法,必须显式写出来。
//
// ⚠️ 对**域名**条目(guardx#96):命中名单只是第一道闸——本放行面
// 缺省会解析 DNS,并把**全部解析结果**逐个过 IP 围栏(私网/链路本地/
// 云元数据等一律拒绝,解析失败/空结果 fail-closed)。组织 DNS(或被
// 劫持的内网 DNS)把名单里的域名解析到内网的借道形态由此被挡住。
//
// ⚠️ 对 **IP 字面量**条目(guardx#119):逐字写进名单(如 "127.0.0.1")
// 才是调用方对这个地址的**显式意图**,返回自身供拨号钉住、不走解析;
// 通配命中("*" 或 "*.x")只说明「没写任何地址/没写这个地址」,**不构成**
// 显式意图——此时字面量照样过 IP 围栏(黑名单模式下 169.254.169.254 /
// 127.0.0.1 一类不放行)。同一个地址换个写法结论相反的形态不被允许
// (#96 修复说明:同仓两个包对「域名→IP」这一步的治理必须同一套)。
AllowHosts []string
// DenyHosts 拒绝的目标主机(语义同 AllowHosts,**优先于** AllowHosts)。
DenyHosts []string
// AllowPorts 允许的目标端口。空 ⇒ 只允许 443 与 80。
//
// 为什么空缺省不是「全部端口」:代理最常见的失手是「主机名单对了,
// 但放行了 22 或 3306」——端口是攻击面的一部分,必须显式开。
AllowPorts []int
// Limited 只读模式:**明文**请求只允许 GET / HEAD。
//
// ⚠️ 对 `CONNECT` 不生效且无法生效:CONNECT 之后是 TLS(或任意协议),
// 方法不可见。所以 Limited 对 HTTPS 只保证「目标主机在名单内」,
// 不保证「只读」——这是能力的真实边界,写在这里而不是留给使用者猜。
Limited bool
// Resolver 域名解析器(guardx#96 DNS 围栏)。nil = net.DefaultResolver。
// 测试与特殊网络环境(内部 DNS、hosts 覆盖)从这里注入。
Resolver Resolver
// SkipDNSCheck 显式关闭 DNS 围栏(guardx#96):只按主机名判定,
// 不解析、返回零地址(调用方按主机名拨号)。
//
// ⚠️ 这是对「解析到内网的域名照转」**解防**的显式开关——只该在宿主
// 明确接受「域名=零 DNS 防护」的轻量档使用(与 egress 包对裸 CheckHost
// 的警示同一口径)。缺省必须是解析 + fail-closed。
//
// 豁免范围(guardx#119 收敛):跳过的只是**域名解析**——IP 字面量与
// exotic IPv4 词法形态(0177.0.0.1 一类)的**字面量层判定不豁免**
// (前者照常过 IP 围栏并返回已检地址,后者照常拒绝):那些判定不是
// 解析判定,地址写法本身的危险不随「不解析」消失。
SkipDNSCheck bool
}
Policy 代理的放行面。
**默认拒绝**:`AllowHosts` 为空 ⇒ 任何目标都不放行(不是「不限」)。 这条缺省与 permgate / sandbox 的口径一致——沉默按最弱处理, 「没配置」绝不能被读成「放行」。
func (Policy) Permit ¶
Permit 判一个目标是否放行。拒绝一律返回 *BlockedError。
⚠️ 域名目标(guardx#96):命中名单后还会**解析 DNS** 并把全部解析结果 逐个过 IP 围栏(缺省 fail-closed)——只判主机名字符串的旧形态正是 egress#60 同款洞在代理层的复现。需要已检地址做拨号钉住的调用方请用 PermitResolving;本方法是其丢弃地址的便捷形态。
func (Policy) PermitResolving ¶ added in v0.23.3
PermitResolving 判一个目标是否放行,并返回**已过检的地址集合**。 拒绝一律返回 *BlockedError。
判定顺序 ¶
- 名单判定(与既有口径一致):空主机 / 命中拒绝名单 / 放行名单为空 / 不在放行名单内 / 端口未放行——先于一切 DNS 查询,未过的目标不触发 解析(fail-fast,也不给任意域名当解析器用);
- IP 字面量 host(netip 可解析的):**逐字**命中名单才是显式意图, 返回该地址本身,不触发解析(guardx#119);通配命中("*" / "*.x") 不构成显式意图——字面量照样过 IP 围栏(字面量无解析步骤,本步与 SkipDNSCheck 无关:那个开关关的是「域名解析」,不是「地址本身的 IP 判定」);
- DNS 围栏(guardx#96):域名 host 经 Resolver(nil 时 net.DefaultResolver)解析,**全部解析结果**逐个过与 egress 同一套 IP 围栏(egress.Policy 零值:私网/回环/链路本地(含云元数据)/CGNAT/ 组播/保留段一律拒绝,只放公网);解析失败、解析结果为空、解析出 判定不了的地址都 fail-closed。exotic IPv4 词法形态(127.1 / 0x7f000001——inet_aton 语义会被 cgo resolver 解析成回环)在词法层 直接拒绝——这一条**不受 SkipDNSCheck 豁免**(它不是解析判定, 是字面量词法判定;SkipDNSCheck=true 时同样执行)。
返回值与 TOCTOU ¶
返回的地址集合是**已全部过检**的:调用方(本包代理的两条转发路径)拨号 时钉住其中之一(字面量直连,不再二次解析),才能闭合「检查后换解析」 的 TOCTOU——egress#60 修复说明明确了「返回全部已检地址供拨号钉住是闭合 check/use 间 TOCTOU 的前提」,CONNECT 隧道(长连接,TOCTOU 窗口更大) 尤其需要钉住。SkipDNSCheck 为 true 时返回 (nil, nil):无已检地址,调用方 按主机名拨号(IP 字面量除外——见第 2 步,字面量照常返回已检地址)。
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy 本地出口代理(HTTP 转发 + CONNECT 隧道)。
零值不可用:必须 `New` → `Start`。`Close` 幂等。
func (*Proxy) Egress ¶
func (p *Proxy) Egress() sandbox.EgressMode
Egress 实现 sandbox.EgressEnforcer:本代理能兑现的是 `proxy` 档。
**不声称 `denied`**:代理挡得住「经过它的流量」,挡不住绕开代理的原始 socket —— 声称禁网就是让档位表说谎。