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 字面量条目(如 "127.0.0.1")不受此围栏管:写进名单本身就是
// 调用方对这个地址的显式意图。
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。
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,也不给任意域名当解析器用);
- DNS 围栏(guardx#96):域名 host 经 Resolver(nil 时 net.DefaultResolver)解析,**全部解析结果**逐个过与 egress 同一套 IP 围栏(egress.Policy 零值:私网/回环/链路本地(含云元数据)/CGNAT/ 组播/保留段一律拒绝,只放公网);解析失败、解析结果为空、解析出 判定不了的地址都 fail-closed。exotic IPv4 词法形态(127.1 / 0x7f000001——inet_aton 语义会被 cgo resolver 解析成回环)在词法层 直接拒绝;
- IP 字面量 host(netip 可解析的):名单判定即显式意图,返回该地址 本身,不触发解析。
返回值与 TOCTOU ¶
返回的地址集合是**已全部过检**的:调用方(本包代理的两条转发路径)拨号 时钉住其中之一(字面量直连,不再二次解析),才能闭合「检查后换解析」 的 TOCTOU——egress#60 修复说明明确了「返回全部已检地址供拨号钉住是闭合 check/use 间 TOCTOU 的前提」,CONNECT 隧道(长连接,TOCTOU 窗口更大) 尤其需要钉住。SkipDNSCheck 为 true 时返回 (nil, nil):无已检地址,调用方 按主机名拨号。
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 —— 声称禁网就是让档位表说谎。