Help us shape the future of Go! Take the 2026 Go Developer Survey

egress

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 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