Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s

This commit is contained in:
李建琦
2026-08-21 18:30:13 +08:00
commit 6d655c9903
3584 changed files with 1270640 additions and 0 deletions
@@ -0,0 +1,152 @@
package service
import (
"context"
"net"
"strings"
)
// SSRF 防护 helper
// - validateEndpoint 在 admin 提交时阻止 http/loopback/私网/云元数据 URL
// - safeDialContext 在 socket 层再次校验真实 IP,防止 DNS rebinding
//
// 已知 cloud metadata hostname 拒绝列表(小写比较)。
var monitorBlockedHostnames = map[string]struct{}{
"localhost": {},
"localhost.localdomain": {},
"metadata": {},
"metadata.google.internal": {},
"metadata.goog": {},
"instance-data": {},
"instance-data.ec2.internal": {},
}
// CIDR 列表:包含所有需要拒绝的 IPv4/IPv6 段。
// 解析时只 panic 一次(启动时确认),生产路径只做 Contains。
var monitorBlockedCIDRs = mustParseCIDRs([]string{
"127.0.0.0/8", // IPv4 loopback
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"169.254.0.0/16", // link-local(含云元数据 169.254.169.254
"100.64.0.0/10", // CGNAT
"0.0.0.0/8", // "this network"
"::1/128", // IPv6 loopback
"fc00::/7", // IPv6 ULA
"fe80::/10", // IPv6 link-local
"::/128", // IPv6 unspecified
})
// monitorDialer 共享 Dialer,与 net/http 默认值对齐。
var monitorDialer = &net.Dialer{
Timeout: monitorDialTimeout,
KeepAlive: monitorDialKeepAlive,
}
// mustParseCIDRs 在包初始化时解析 CIDR 字符串,失败 panic。
func mustParseCIDRs(cidrs []string) []*net.IPNet {
out := make([]*net.IPNet, 0, len(cidrs))
for _, c := range cidrs {
_, n, err := net.ParseCIDR(c)
if err != nil {
panic("channel_monitor_ssrf: invalid CIDR " + c + ": " + err.Error())
}
out = append(out, n)
}
return out
}
// isBlockedHostname 判断 hostname 是否命中黑名单。
func isBlockedHostname(hostname string) bool {
if hostname == "" {
return true
}
_, blocked := monitorBlockedHostnames[strings.ToLower(hostname)]
return blocked
}
// isPrivateIP 判断 IP 是否落在禁止段(loopback/RFC1918/link-local/ULA 等)。
func isPrivateIP(ip net.IP) bool {
if ip == nil {
return true
}
if ip.IsUnspecified() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast() {
return true
}
for _, n := range monitorBlockedCIDRs {
if n.Contains(ip) {
return true
}
}
return false
}
// isPrivateOrLoopbackHost 解析 hostname 的所有 A/AAAA 记录,
// 任一 IP 落在私网/loopback 段即认为不安全。
//
// hostname 是 IP 字面量时也走同一路径。
func isPrivateOrLoopbackHost(ctx context.Context, hostname string) (bool, error) {
if isBlockedHostname(hostname) {
return true, nil
}
// IP 字面量直接判断。
if ip := net.ParseIP(hostname); ip != nil {
return isPrivateIP(ip), nil
}
resolver := net.DefaultResolver
addrs, err := resolver.LookupIPAddr(ctx, hostname)
if err != nil {
return false, err
}
if len(addrs) == 0 {
return true, nil
}
for _, a := range addrs {
if isPrivateIP(a.IP) {
return true, nil
}
}
return false, nil
}
// safeDialContext 在真实 dial 前再次校验目标 IP,防止 DNS rebinding。
// 解析 hostname 后逐个 IP 尝试连接,命中私网即拒绝(即便 validateEndpoint 时返回的是公网 IP)。
func safeDialContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
// 字面量 IP 走快速路径。
if ip := net.ParseIP(host); ip != nil {
if isPrivateIP(ip) {
return nil, &net.AddrError{Err: "blocked by SSRF policy", Addr: address}
}
return monitorDialer.DialContext(ctx, network, address)
}
if isBlockedHostname(host) {
return nil, &net.AddrError{Err: "blocked by SSRF policy", Addr: address}
}
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
if len(addrs) == 0 {
return nil, &net.AddrError{Err: "no addresses for host", Addr: host}
}
var lastErr error
for _, a := range addrs {
if isPrivateIP(a.IP) {
lastErr = &net.AddrError{Err: "blocked by SSRF policy", Addr: a.IP.String()}
continue
}
conn, err := monitorDialer.DialContext(ctx, network, net.JoinHostPort(a.IP.String(), port))
if err == nil {
return conn, nil
}
lastErr = err
}
if lastErr == nil {
lastErr = &net.AddrError{Err: "no usable addresses", Addr: host}
}
return nil, lastErr
}