第一章:低代码平台安全沙箱的演进与Go语言选型
安全沙箱是低代码平台实现租户隔离、资源约束与代码执行防护的核心基础设施。早期沙箱多依赖操作系统级隔离(如 cgroups + namespace)或 JVM 安全管理器,但存在启动开销大、语言绑定强、细粒度控制难等问题。随着平台对实时性、多租户并发执行及动态脚本支持的需求增长,轻量、可控、可嵌入的运行时沙箱成为关键演进方向——WebAssembly(Wasm)因其内存线性化、指令集精简、跨语言编译友好等特性,逐渐成为主流选择。
沙箱能力演进的关键阶段
- 第一代:基于 Docker 容器的粗粒度隔离 → 启动延迟高(>500ms),难以支撑毫秒级函数触发
- 第二代:JVM SecurityManager + 自定义 ClassLoader → 仅支持 Java 生态,权限模型僵化,易被反射绕过
- 第三代:Wasm 运行时(如 Wasmtime、Wasmer)+ 策略驱动的系统调用拦截 → 内存零拷贝、冷启动
Go语言在沙箱宿主层的不可替代性
Go 的静态链接、无虚拟机依赖、原生 goroutine 调度与精细的 runtime.LockOSThread 控制能力,使其成为构建高并发沙箱宿主服务的理想语言。尤其在实现 Wasm 主机函数(host functions)时,Go 可直接暴露类型安全的 Go 函数供 Wasm 模块调用,避免 C FFI 的内存生命周期风险:
// 注册受控的 HTTP 客户端主机函数(仅允许白名单域名)
func registerHTTPHostFuncs(store *wasmedge.Store) {
// 创建带超时与域名校验的 http.Client
client := &http.Client{Timeout: 5 * time.Second}
// 将 Go 函数注册为 Wasm 可调用的 host function
hostFunc := wasmedge.NewFunction(
wasmedge.NewFuncType(
[]wasmedge.ValType{wasmedge.ValTypeI32, wasmedge.ValTypeI32}, // url_ptr, url_len
[]wasmedge.ValType{wasmedge.ValTypeI32}, // status_code
),
func(_ context.Context, _ *wasmedge.CallingFrame, args []uint64) ([]uint64, error) {
urlPtr, urlLen := uint32(args[0]), uint32(args[1])
// 从 Wasm 线性内存安全读取 URL 字符串(需校验边界)
urlBytes, err := readStringFromMemory(frame.GetMemory(0), urlPtr, urlLen)
if err != nil || !isDomainWhitelisted(string(urlBytes)) {
return []uint64{0}, errors.New("domain not allowed")
}
resp, _ := client.Get(string(urlBytes))
return []uint64{uint64(resp.StatusCode)}, nil
},
)
store.AddHostFunction("http_get", hostFunc)
}
| 对比维度 | Node.js(V8 Isolates) | Go+Wasmtime | Rust+Wasmtime |
|---|---|---|---|
| 宿主内存安全性 | 依赖 V8 GC,存在引用泄漏风险 | 静态内存布局,RAII 显式管理 | 同样高,但生态工具链对低代码平台集成支持较弱 |
| 开发效率 | 高(JS 生态丰富) | 中(标准库完备,文档清晰) | 中低(学习曲线陡峭) |
| 运维可观测性 | Prometheus metrics 需额外封装 | 原生支持 pprof + trace | 类似 Go,但调试工具链成熟度稍逊 |
Go 不仅提供生产就绪的并发模型与部署体验,其 plugin 包(虽有限制)与 embed 特性也便于实现沙箱策略热更新与内置函数模块化加载。
第二章:Go沙箱核心机制设计与实现
2.1 基于seccomp-bpf的syscall细粒度拦截与策略建模
seccomp-bpf 将传统 seccomp 的黑白名单模式升级为可编程的 BPF 过滤器,使 syscall 拦截具备上下文感知能力。
策略建模核心维度
- 调用号(
arch+nr) - 参数值(如
args[0]指向的文件路径或 flag) - 返回值约束(仅在
SECCOMP_RET_TRACE或用户态代理场景下生效)
典型过滤器片段
// 拦截 openat() 且 pathname 含 "/etc/shadow"
SEC("filter")
int block_shadow_access(struct seccomp_data *ctx) {
if (ctx->nr == __NR_openat) {
// 注意:BPF 无法直接读取用户内存,需配合 user-space 辅助或使用 BPF_PROG_TYPE_SECCOMP + arg inspection(内核 5.11+)
return SECCOMP_RET_ERRNO | (EACCES << 16);
}
return SECCOMP_RET_ALLOW;
}
该程序在内核态执行,ctx->nr 是系统调用号,SECCOMP_RET_ERRNO 触发 -EACCES 错误返回;位移 << 16 是 errno 编码规范。
支持的返回动作对比
| 动作类型 | 语义说明 |
|---|---|
SECCOMP_RET_ALLOW |
放行调用 |
SECCOMP_RET_ERRNO |
返回指定 errno(不进入内核路径) |
SECCOMP_RET_TRACE |
转交 ptrace 处理(需 CAP_SYS_ADMIN) |
graph TD A[应用发起 syscall] –> B{seccomp-bpf 过滤器加载?} B –>|是| C[执行 BPF 程序] B –>|否| D[直通内核 syscall 处理] C –> E[根据返回码决定:放行/报错/追踪]
2.2 内存配额控制:cgroup v2 memory controller集成与OOM防护实践
cgroup v2 统一资源管理模型下,memory controller 提供精细化内存限额与压力反馈机制。
启用与挂载
# 挂载统一层级并启用 memory controller
mount -t cgroup2 none /sys/fs/cgroup
echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control
cgroup.subtree_control 中显式启用 +memory 是前提,否则子 cgroup 无法设置 memory.max。
配额设置与 OOM 策略
| 参数 | 说明 | 示例 |
|---|---|---|
memory.max |
硬性内存上限(字节或后缀) | 512M |
memory.low |
保障性最小内存(受压时优先保留) | 128M |
memory.oom.group |
控制 OOM kill 粒度(0=进程级,1=整个 cgroup) | 1 |
OOM 防护流程
graph TD
A[内存分配请求] --> B{超出 memory.max?}
B -->|是| C[触发 memory.pressure]
C --> D[内核尝试回收/swap]
D --> E{仍超限且 memory.oom.group=1}
E -->|是| F[整组进程被 OOM killer 终止]
关键实践:将 memory.oom.group=1 与 memory.low 结合,可避免单个“内存泄漏”进程拖垮同组服务。
2.3 CPU时间片限制:runtime.LockOSThread + cgroup v2 cpu.max协同调度
Go 程序需绑定 OS 线程并受内核级 CPU 配额约束时,runtime.LockOSThread() 与 cgroup v2 的 cpu.max 形成两级协同调度机制。
绑定线程与配额生效路径
func main() {
runtime.LockOSThread() // 将当前 goroutine 与底层 OS 线程永久绑定
defer runtime.UnlockOSThread()
for range time.Tick(10 * time.Millisecond) {
// 执行 CPU 密集型任务(如信号处理、实时音频)
}
}
LockOSThread确保 goroutine 不被 Go 调度器迁移,使 cgroup 的cpu.max配额精准作用于该线程。若未绑定,Go 可能将任务切至其他线程,绕过配额限制。
cgroup v2 配额配置示例
| 文件路径 | 写入值 | 含义 |
|---|---|---|
cpu.max |
50000 100000 |
每 100ms 最多运行 50ms |
协同调度流程
graph TD
A[Go 程序调用 LockOSThread] --> B[OS 线程固定]
B --> C[cgroup v2 cpu.max 生效]
C --> D[内核 scheduler 强制限频]
D --> E[避免抢占式调度抖动]
2.4 Go原生goroutine级资源隔离:pprof+trace驱动的执行时长熔断机制
Go 的 runtime/trace 与 net/http/pprof 并非仅用于事后分析——它们可实时注入执行上下文,构建轻量级 goroutine 级熔断器。
熔断触发核心逻辑
func withTimeout(ctx context.Context, maxNs int64, fn func()) {
start := time.Now().UnixNano()
defer func() {
dur := time.Now().UnixNano() - start
if dur > maxNs {
trace.Log(ctx, "goroutine", fmt.Sprintf("exceeded_%dns", maxNs))
runtime.GoSched() // 主动让出,降低抢占风险
}
}()
fn()
}
该函数在函数退出时动态采集耗时,结合
trace.Log将超时事件写入 trace buffer;runtime.GoSched()避免单个 goroutine 长期 monopolize P,实现软隔离。
pprof 与 trace 协同路径
| 组件 | 作用 | 实时性 |
|---|---|---|
/debug/pprof/goroutine?debug=2 |
获取活跃 goroutine 栈快照 | 秒级 |
runtime/trace.Start() |
连续采样调度、阻塞、GC 事件 | 微秒级纳秒精度 |
执行流控制示意
graph TD
A[goroutine 启动] --> B{trace.Start?}
B -->|Yes| C[注入 trace.WithRegion]
B -->|No| D[启用 pprof.Labels 记录耗时标签]
C --> E[定时检查 trace.Event duration]
D --> E
E --> F[超阈值 → runtime.GoSched + 拒绝后续子任务]
2.5 沙箱进程生命周期管理:exec.CommandContext + signal.Notify的可靠启停模型
沙箱进程需响应上下文取消与系统信号,实现零残留退出。
核心组合优势
exec.CommandContext自动绑定ctx.Done(),超时或取消时向子进程发送SIGKILLsignal.Notify捕获os.Interrupt、syscall.SIGTERM,触发优雅关闭流程
启停控制流
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sandbox-bin", "-mode=restricted")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
cancel() // 触发 CommandContext 取消
}()
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
if err := cmd.Wait(); err != nil {
log.Printf("process exited: %v", err)
}
逻辑分析:
cmd.Start()启动后,cmd.Wait()阻塞等待退出;cancel()调用不仅终止ctx,还会由exec包自动向子进程发送SIGKILL(若未响应SIGTERM)。signal.Notify确保外部中断可被感知,避免Wait()永久阻塞。
关键参数对照表
| 参数 | 作用 | 推荐值 |
|---|---|---|
context.WithTimeout |
设置最大生命周期 | 30s(防挂起) |
signal.Notify 通道缓冲区 |
避免信号丢失 | 1(单次信号足够) |
cmd.Wait() 调用时机 |
必须在 Start() 后 |
不可省略,否则无退出状态 |
graph TD
A[启动沙箱] --> B{监听 SIGTERM/INT}
B --> C[收到信号 → cancel ctx]
C --> D[exec.CommandContext 发送 SIGTERM]
D --> E[进程 graceful shutdown]
E --> F[Wait 返回退出状态]
第三章:低代码DSL执行环境的安全加固实践
3.1 表达式求值沙箱:govaluate定制化AST遍历与危险函数白名单校验
核心设计思路
基于 govaluate 的 AST 遍历能力,构建轻量级表达式沙箱:先解析表达式为抽象语法树,再深度遍历节点,拦截未授权函数调用。
白名单校验机制
仅允许以下安全函数(其余一律拒绝):
| 函数名 | 参数类型 | 说明 |
|---|---|---|
abs |
float64 |
绝对值 |
round |
float64, int |
四舍五入到指定位数 |
len |
string/[]interface{} |
长度计算 |
定制化遍历示例
// 自定义AST访问器,拦截非白名单函数调用
type SafeEvaluator struct {
whitelist map[string]bool
}
func (s *SafeEvaluator) Visit(node interface{}) interface{} {
if fn, ok := node.(*govaluate.FunctionNode); ok {
if !s.whitelist[fn.Name] {
panic(fmt.Sprintf("forbidden function: %s", fn.Name)) // 拦截并报错
}
}
return node
}
该访问器在 govaluate.EvaluableExpression.Walk() 中注入,于遍历阶段实时校验每个函数节点;fn.Name 是函数标识符,s.whitelist 为预置的 map[string]bool 结构,时间复杂度 O(1)。
执行流程示意
graph TD
A[输入表达式] --> B[Parse → AST]
B --> C[Walk with SafeEvaluator]
C --> D{函数节点?}
D -->|是| E[查白名单]
D -->|否| F[放行]
E -->|允许| F
E -->|禁止| G[Panic阻断]
3.2 模板渲染隔离:html/template与text/template的上下文逃逸防御与自动转义强化
Go 标准库通过双模板引擎实现语义化上下文隔离:html/template 针对 HTML 输出执行上下文感知的自动转义,而 text/template 仅做纯文本替换,无转义逻辑。
转义行为对比
| 上下文 | html/template 行为 |
text/template 行为 |
|---|---|---|
| HTML 元素内容 | 自动转义 <, >, &, ", ' |
原样输出 |
<script> 内 |
进入 JS 字符串上下文,额外转义 / |
无特殊处理 |
href="..." 属性 |
进入 URL 上下文,编码特殊字符 | 不识别属性语义 |
// 安全渲染:html/template 根据插入位置动态选择转义策略
t := template.Must(template.New("").Parse(`<a href="{{.URL}}">{{.Name}}</a>`))
t.Execute(w, map[string]string{
"URL": "javascript:alert('xss')", // → 被编码为 javascript%3Aalert%28%27xss%27%29
"Name": "<b>Hi</b>", // → 被转义为 <b>Hi</b>
})
该代码中,.URL 在 href 属性内触发 URL 上下文转义,.Name 在 HTML 文本节点中触发 HTML 实体转义——二者均由模板编译时静态分析 DOM 位置决定,无需开发者手动调用 template.HTMLEscapeString。
graph TD
A[模板解析] --> B{插值位置分析}
B -->|HTML 文本节点| C[HTML 转义]
B -->|href/src 属性| D[URL 编码]
B -->|<script> 内| E[JavaScript 字符串转义]
B -->|style 属性| F[CSS 字符串转义]
3.3 外部调用网关:HTTP Client沙箱封装与DNS/连接池/超时策略的统一管控
为保障多业务线对外服务调用的稳定性与可观测性,我们构建了轻量级 HTTP Client 沙箱——以 HttpClientSandbox 为核心,统一封装 DNS 解析、连接池生命周期与分级超时策略。
统一配置驱动的策略中心
- DNS 缓存 TTL 统一设为 30s(规避运营商劫持+降低解析抖动)
- 连接池采用
PoolingHttpClientConnectionManager,最大总连接数 200,单路由 50 - 超时分三级:连接建立 ≤ 1s、TLS 握手 ≤ 2s、读写 ≤ 5s(可按目标域名动态覆盖)
核心沙箱初始化示例
HttpClientSandbox sandbox = HttpClientSandbox.builder()
.dnsResolver(new CachingDnsResolver(30, TimeUnit.SECONDS)) // 启用带TTL的本地DNS缓存
.connectionPool(new PoolingHttpClientConnectionManager(200, 50))
.timeoutPolicy(TimeoutPolicy.of(1_000, 2_000, 5_000)) // ms: connect, handshake, socket
.build();
该构造器强制注入策略组件,禁止运行时修改;CachingDnsResolver 替换默认 JDK resolver,避免 InetAddress.getByName() 的无限阻塞风险;TimeoutPolicy 将 SSL 握手超时显式分离,精准定位 TLS 层瓶颈。
策略生效拓扑
graph TD
A[业务请求] --> B[HttpClientSandbox]
B --> C[策略路由匹配]
C --> D[DNS 缓存查询]
C --> E[连接池租借]
C --> F[分级超时计时器]
D & E & F --> G[执行 HTTP 请求]
第四章:生产级沙箱可观测性与攻防对抗体系
4.1 syscall审计日志:eBPF tracepoint注入与go runtime事件关联分析
核心机制:双源事件对齐
通过 tracepoint/syscalls/sys_enter_* 捕获系统调用入口,同时利用 uprobe 钩住 runtime.syscall 和 runtime.entersyscall,实现内核态与 Go 协程调度态的毫秒级时间戳对齐。
关键代码片段(eBPF 端)
// bpf_prog.c:关联 syscall 与 goroutine ID
SEC("tracepoint/syscalls/sys_enter_read")
int trace_read(struct trace_event_raw_sys_enter *ctx) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 tgid = (u32)(pid_tgid >> 32);
u32 goid = get_goroutine_id(tgid); // 自定义辅助函数,查 map[pid → goid]
bpf_map_update_elem(&syscall_events, &tgid, &(struct event){.goid = goid, .ts = bpf_ktime_get_ns()}, BPF_ANY);
return 0;
}
逻辑说明:
bpf_get_current_pid_tgid()提取线程粒度标识;get_goroutine_id()依赖预先注入的runtime.g地址映射;syscall_events是BPF_MAP_TYPE_HASH,用于后续用户态聚合。参数BPF_ANY允许覆盖旧事件,适应高并发短生命周期 goroutine 场景。
关联维度对照表
| 维度 | syscall tracepoint | Go runtime uprobe |
|---|---|---|
| 时间精度 | 纳秒级(ktime) | 微秒级(nanotime) |
| 上下文标识 | pid/tgid |
goid + m.id |
| 触发时机 | 进入内核前 | 进入 syscalls 前 |
数据同步机制
graph TD
A[sys_enter_read] --> B{写入 syscall_events map}
C[uprobe: entersyscall] --> D{写入 goroutine_state map}
B --> E[userspace: ringbuf 合并]
D --> E
E --> F[按 goid+ts 区间匹配]
4.2 内存/CPU异常检测:expvar+prometheus指标暴露与阈值告警联动
Go 应用可通过 expvar 暴露运行时指标,再由 Prometheus 抓取并触发告警:
import _ "expvar" // 自动注册 /debug/vars HTTP handler
该导入启用标准内存与 goroutine 统计(如 memstats.Alloc, goroutines),无需额外初始化。
指标采集配置
Prometheus 需在 scrape_configs 中添加:
| job_name | metrics_path | static_configs |
|---|---|---|
| go-app | /debug/vars | targets: [‘localhost:8080’] |
告警规则示例
- alert: HighMemoryUsage
expr: go_memstats_alloc_bytes > 500 * 1024 * 1024
for: 2m
labels: {severity: warning}
联动逻辑流程
graph TD
A[Go runtime] -->|expvar HTTP| B[/debug/vars]
B --> C[Prometheus scrape]
C --> D[PromQL评估]
D -->|阈值触发| E[Alertmanager]
4.3 沙箱逃逸模拟测试:ptrace注入、/proc/self/mem绕过、unshare命名空间提权验证
沙箱逃逸测试聚焦于现代容器隔离机制的薄弱环节,重点验证三类典型绕过路径。
ptrace注入:劫持目标进程执行流
// attach并写入shellcode到目标进程内存
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); // 读取原指令
ptrace(PTRACE_POKETEXT, pid, addr, shellcode); // 注入
ptrace(PTRACE_DETACH, pid, NULL, NULL);
PTRACE_ATTACH 获取调试权限;PTRACE_POKETEXT 需目标进程处于 TASK_TRACED 状态,依赖内核未禁用 ptrace_scope=0。
/proc/self/mem 写入绕过
需 CAP_SYS_PTRACE 或 ptrace 权限,普通容器默认被 seccomp-bpf 过滤 openat(AT_FDCWD, "/proc/.../mem", O_RDWR) 系统调用。
unshare 命名空间提权验证
| 调用方式 | 是否突破userns | 关键条件 |
|---|---|---|
unshare(CLONE_NEWUSER) |
否(仅新建) | 需配合 setgroups(0) + map_ids |
unshare(CLONE_NEWNET) |
是(若已获userns root) | 依赖 CAP_NET_ADMIN 映射 |
graph TD
A[启动受限容器] --> B{尝试ptrace注入}
B -->|失败| C[/proc/self/mem 写入]
B -->|成功| D[执行shellcode]
C -->|seccomp拦截| E[切换userns+netns]
E --> F[绑定宿主机端口]
4.4 安全策略热更新:etcd驱动的动态seccomp profile加载与运行时策略灰度发布
传统 seccomp 配置需重启容器生效,而本方案通过 etcd 作为统一策略存储中心,实现 profile 的毫秒级热加载。
数据同步机制
etcd watch 事件触发本地缓存刷新,并校验 profile SHA256 签名确保完整性:
# 监听 /seccomp/profiles/ 下所有变更
ETCDCTL_API=3 etcdctl --endpoints=localhost:2379 \
watch --prefix "/seccomp/profiles/" \
--changes-only
该命令持续监听键前缀变更;
--changes-only过滤空事件,降低 CPU 开销;实际集成中由 Go clientv3 Watcher 封装为回调函数,自动触发libseccomp-bpf规则重编译。
灰度发布流程
支持按 namespace、label 或流量比例分阶段推送:
| 灰度维度 | 示例值 | 生效方式 |
|---|---|---|
| 标签匹配 | env=staging |
Kubernetes PodSelector |
| 流量权重 | 15% |
eBPF tc classifier |
graph TD
A[etcd 写入新 profile] --> B{Watch 事件到达}
B --> C[签名验证 & 编译为 bpf bytecode]
C --> D[按灰度规则选择目标 Pod]
D --> E[通过 /proc/<pid>/status 注入]
第五章:未来方向:WASI兼容性、Rust+Go混合沙箱与零信任执行模型
WASI兼容性:从实验走向生产级隔离
WASI(WebAssembly System Interface)正快速突破浏览器边界,成为云原生函数即服务(FaaS)的核心运行时契约。Cloudflare Workers 已全面支持 WASI preview1,并在 2024 年 Q2 实现了对 wasi-http 和 wasi-crypto 的稳定 ABI 兼容;阿里云函数计算 FC 新增的 wasm32-wasi 运行时实测冷启动延迟压降至 8.3ms(对比传统容器 320ms)。关键突破在于 wasi-threads 的内核级调度适配——Linux 6.8 内核通过 clone3() + pidfd_getfd() 实现线程生命周期与 WASM 模块内存页的强绑定,避免跨模块内存越界访问。
Rust+Go混合沙箱:语言协同的可信边界设计
某金融风控平台采用 Rust 编写核心策略引擎(内存安全、零成本抽象),而用 Go 实现高并发 HTTP 网关层(goroutine 调度优势)。二者通过 WASM 模块桥接:Rust 编译为 wasm32-wasi,Go 通过 wasmedge-go SDK 加载并调用其 validate_transaction() 导出函数。沙箱隔离策略如下表所示:
| 组件 | 内存限制 | 系统调用白名单 | 网络能力 |
|---|---|---|---|
| Rust策略模块 | 16MB | args_get, clock_time_get |
禁用 |
| Go网关层 | 512MB | socket_accept, poll_oneoff |
全开 |
该架构在日均 2.7 亿笔交易压测中,策略模块崩溃率趋近于 0,且 Go 层可动态热加载新策略 WASM 模块(平均耗时 42ms)。
零信任执行模型:运行时凭证链与细粒度策略引擎
零信任不再仅限于网络层,已下沉至进程级执行上下文。CNCF Sandbox 项目 KubeWarden v1.8 引入 WASM 策略插件机制,每个策略模块需携带由硬件安全模块(HSM)签发的 attestation 证书。执行流程如下:
flowchart LR
A[用户请求] --> B{准入控制器}
B --> C[提取Pod签名凭证]
C --> D[调用TPM2.0验证WASM模块完整性]
D --> E[查询OPA策略引擎:是否允许读取/etc/secrets]
E --> F[策略决策:allow/deny]
F --> G[注入运行时seccomp profile]
某政务云平台部署该模型后,在 Kubernetes Pod 启动阶段强制执行策略:若 WASM 模块未通过 Intel TDX 远程证明,则拒绝挂载 /proc/sys/net/ipv4/ip_forward。实际拦截了 3 类违规镜像,包括一个伪装成日志收集器但尝试启用 IP 转发的恶意模块。
构建可验证的供应链信任锚点
Rust crate wasi-cap-std 与 Go 库 golang.org/x/wasi 均已接入 Sigstore Fulcio 证书颁发体系。开发者提交的 WASM 模块自动触发 Cosign 签名流水线,生成的 .sig 文件嵌入 OCI 镜像 manifest 中。当 Kubernetes 调度器拉取镜像时,通过 notaryproject.dev/v1 协议实时校验签名链——从开发者私钥 → CI/CD 系统证书 → Fulcio 根 CA。某省级医保系统上线该机制后,第三方 SaaS 插件上线周期从 5 天缩短至 47 分钟,且所有策略变更均可追溯至 Git 提交哈希与 CI 流水线 ID。
