egress

package
v0.18.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package egress 出口围栏:LLM 可控 URL 的字面量层 SSRF 防护——阻断指向 本机/私网/保留段的 IP 字面量与 localhost 域,放行普通域名。

对标 ZCode webfetch-egress-guard.ts:

  • 只做字面量判定,不对域名做 DNS preflight——部分网络 1s 内解析不完会 误杀公网目标(解析层防护由消费方按需叠加,如自定义 DialContext 的 Control 钩子二次校验解析结果);
  • IPv4-mapped IPv6(::ffff:0:0/96)与 NAT64 well-known prefix (64:ff9b::/96)先还原内嵌 IPv4 再套同一套 IPv4 策略——这是绕过 IP 围栏的经典逃逸路径;
  • 显式排除 special-use IPv6 段(ORCHID/discard/benchmark/文档段)—— 标准库 IsPrivate 等判定不覆盖这些段。

放行面与拨号层

放行面用 Policy(**具名值**)表达:写面校验(Check / CheckHost)与拨号层守卫 (Policy.NewClient / Policy.MustClient)从**同一个值**派生——两层口径不可能 不一致(Options 的双 bool 做不到:语义只能靠调用点口头约定区分,值一传岔 就出「校验层放行、拨号层拒」的假红或反过来的安全问题)。

拨号层判的是**拨号地址的字面量**,用的是与写面同一个判定函数,不做 DNS 解析层判定(与本包「只做字面量判定」的既有口径一致)。

用法:

if err := egress.Check(u); err != nil { …拒绝抓取… }

// 同一份放行面同时管住写面与拨号层:
p := egress.PrivateNoLoopback
if err := p.Check(u); err != nil { …拒绝抓取… }
c := p.MustClient(base)

Index

Constants

This section is empty.

Variables

View Source
var (
	// PublicOnly 只放公网(目标完全由用户填的场景)——零值口径。
	PublicOnly = Policy{}
	// PrivateNoLoopback 放行私网、拒回环(内网自建服务是合法场景)。
	PrivateNoLoopback = Policy{AllowPrivate: true}
	// SelfService 私网与回环都放行(探测本机自建服务)。
	SelfService = Policy{AllowPrivate: true, AllowLoopback: true}
)

具名放行面(常用组合)。

⚠️ 这些是**包级变量**:可以读,但**不要重新赋值**——改它们等于全局改掉所有 消费方的放行面(Policy 是值类型,要变体时自己构造副本:

p := egress.PrivateNoLoopback
p.AllowLoopback = true

).

Functions

func Check

func Check(rawURL string) error

Check 校验完整 URL 的 host(解析失败 fail-closed)。

func CheckHost

func CheckHost(host string) error

CheckHost 校验 host(URL hostname 或去端口/括号后的 host 字面量)。 空值 fail-closed;非 IP 字面量(域名)放行——见包注释的 preflight 取舍。

func CheckHostWith

func CheckHostWith(host string, p Policy) error

CheckHostWith 校验 host(放行面语义见 Policy;放行位仅作用于私网与 回环/未指定两族;链路本地(含云元数据)、组播、CGNAT 与保留段一律拒绝—— 放行面只该覆盖「目标在内网是合法场景」的那部分)。

func CheckWith

func CheckWith(rawURL string, p Policy) error

CheckWith 校验完整 URL 的 host(放行面语义见 Policy;解析失败 fail-closed)。

Types

type BlockedError

type BlockedError struct {
	Host   string // 原始 host(URL Hostname 形态)
	Addr   string // 命中判定的地址(载体还原后的字面量;域名为空)
	Reason string // 命中原因(人可读)
}

BlockedError 围栏命中(结构化:调用方按需呈现原因与目标)。

func (*BlockedError) Error

func (e *BlockedError) Error() string

type Options

type Options = Policy

Options 旧名(兼容别名)。

已由 Policy 取代:Policy 是本体,Options 只是旧名——保留它是为了让既有调用点 继续编译(egress.Options{AllowPrivate: true} 与 egress.Options{} 都照旧可用), 语义与 Policy 完全一致(**同一个类型**,不是两套并行表示)。

新代码请用 Policy + 具名放行面(PublicOnly / PrivateNoLoopback / SelfService)。

type Policy added in v0.15.0

type Policy struct {
	// AllowPrivate 放行私网段(IPv4 RFC1918 / IPv6 唯一本地)。
	AllowPrivate bool
	// AllowLoopback 放行回环与未指定地址(仅 127.0.0.0/8、::1、0.0.0.0、::
	// 四个字面量)。0.0.0.0/8 余下整段是 RFC 1122 的 "this network" 保留段,
	// 与 CGNAT/benchmark 同列——不在放行面内。
	AllowLoopback bool
}

Policy 放行面(**具名值**)。

为什么要有这个类型

Options 的两个 bool 位**读不出意图**:同样的 {AllowPrivate: true, AllowLoopback: false} 在不同调用点可能表达不同场景(内网自建服务 / 回调地址 / 探测本机), 而值一旦传岔,就会出两种故障:

  • 校验层放行私网、拨号层按缺省拒 ⇒ 假红(「配置明明允许内网,调用却失败」);
  • 校验层拒、拨号层放行 ⇒ **安全问题**(写面拒了,拨号层没拒)。

Policy 把放行面收成**一个值**:写面校验(Check / CheckHost)与拨号层守卫 (NewClient / MustClient)都从同一个值派生 ⇒ **结构上没法不一致**。

与 Options 的关系

Policy 是本体,Options 是**旧名的兼容别名**(同一个类型,不是两套表示): 既有的 egress.Options{AllowPrivate: true} / egress.Options{} 调用点照旧编译, 语义逐字相同。新代码请用 Policy 与下面的具名放行面。

零值口径

零值 = 两个放行位全关 = 只放公网与域名(与 Check / CheckHost 的缺省口径一致), 即 PublicOnly。

func (Policy) Check added in v0.15.0

func (p Policy) Check(rawURL string) error

Check 校验完整 URL 的 host(放行面 = 本值;解析失败 fail-closed)。

与 CheckWith 等价——同一个值派生,语义不可能与写面分叉。

func (Policy) CheckHost added in v0.15.0

func (p Policy) CheckHost(host string) error

CheckHost 校验 host(放行面 = 本值;空值 fail-closed;非 IP 字面量放行)。

与 CheckHostWith 等价——同一个值派生,语义不可能与写面分叉。

func (Policy) MustClient added in v0.15.0

func (p Policy) MustClient(base *http.Client) *http.Client

MustClient 同 NewClient,出错即 panic(适合包级初始化等不容错的场景)。

func (Policy) NewClient added in v0.15.0

func (p Policy) NewClient(base *http.Client) (*http.Client, error)

NewClient 派生一个把**同一放行面**落到拨号层的 *http.Client。

拨号层判的是什么

拨号层用的是与写面**同一个字面量判定**(Policy.CheckHost):拨号地址的 host 是 IP 字面量时按放行面判;是域名时放行——与本包「只做字面量判定、不做 DNS preflight」的既有口径一致。

刻意**不做**「连上之后核对实际对端地址」:那会让拨号层比写面**更严** (写面放行域名、拨号层拒它的解析结果),两层口径就又不一致了—— 那正是本类型要消灭的故障形态。解析层防护仍由消费方按需叠加。

拨号层多出来的价值

  • **重定向**:http.Client 默认跟随重定向,而写面只校验了**最初那个 URL**; 真正被拨号的是重定向目标 ⇒ 由拨号层兜住。
  • **漏掉写面校验的调用方**:拨号层仍然拦住 IP 字面量目标。

边界(有意不覆盖的)

  • 走了代理(Transport.Proxy)时,拨号地址是**代理地址**而不是目标地址 ⇒ 目标侧的判定仍要靠写面 p.Check(url)。
  • 域名解析到私网(DNS 层)不在本包判定面内——与包的既有口径一致。

参数与错误

base 为 nil 时按 http.DefaultClient 取;**不修改** base,也不修改全局默认传输器 (默认传输器走 Clone)。base.Transport 不是 *http.Transport 时挂不上拨号守卫 ⇒ 返回错误(用 MustClient 则 panic)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL