mirror of
https://github.com/wweir/sower.git
synced 2026-10-19 16:14:33 +00:00
docs: trim the completed plans and record the sowerd rejected directions
admin-console.md and site-routing.md still carried the full design and implementation checklists for work that shipped and was written back to ARCHITECTURE.md. Keep only what cannot be recovered from the code -- the two frontend constraints and the site-routing trade-offs -- and point at the authoritative documents. Drop the plan-to-plan "Supersedes" reference from admin-persistence-config.md so plans stop sourcing rules from other plans. security.md gains the rejected directions those rewrites now point at: wildcard site_routes domains (rejected in validateSiteRoutes, and the SNI dispatch is an exact-match siteRouter.lookup), additional listen addresses or configurable 80/443 ports, per-site certificate binding, and domain routing on plain HTTP.
This commit is contained in:
+4
-126
@@ -1,131 +1,9 @@
|
||||
# Admin Console Plan (MVP)
|
||||
|
||||
Status: 已实现;本文为历史计划,非权威来源。权威规则见根目录 `ARCHITECTURE.md`。
|
||||
Scope: `cmd/sower`(客户端)管理控制台,前端 embed 进二进制
|
||||
Superseded by: [admin-persistence-config.md](admin-persistence-config.md)(扩展了本文的 MVP 边界:规则持久化与配置页)。
|
||||
|
||||
## 目标
|
||||
原 MVP 计划的后端设计(`RuleSet`、流量统计、admin HTTP 服务、`cmd/sower` 接线、构建与测试清单)已全部落地,并回写到 `ARCHITECTURE.md` 的 Layer Responsibilities、sower Data Flow 第 11–12 步与 Design Decisions,此处不再重复。仅保留两条仍在生效、且容易从代码复现不出来的前端约束:
|
||||
|
||||
给 `sower`(客户端)加一个本地 admin 管理页面:
|
||||
|
||||
- 执行管理规则:查看 / 添加 / 删除 block、direct、proxy 三类路由规则(运行期生效)
|
||||
- 监控流量:DNS 查询数、各入口连接数、上下行字节数、按域名聚合的流量统计
|
||||
- 前端为 Svelte 5 + Vite(由 bun 驱动),构建产物 embed 进 Go 二进制
|
||||
|
||||
## 范围与边界(MVP 明确不做)
|
||||
|
||||
- **只做 `cmd/sower`,不做 `sowerd`**。sowerd 是服务端 TLS 入口,没有规则与流量管理面。
|
||||
- ~~**规则变更仅运行期生效,重启后重置**,不改写 TOML、不持久化、不静默暗示持久化。~~ **已被 [`admin-persistence-config.md`](./admin-persistence-config.md) 取代**:规则变更改为增量持久化到 admin 状态文件,重启后重放;配置页提供全量展示与白名单字段(`log_level`、`dns.upstream`、`dns.fallback`)在线调整。
|
||||
- **监控的是代理 payload 字节与请求/连接计数**,不是网卡级网络计量。
|
||||
- 不做配置热加载、不做规则文件管理(文件仍走启动时加载路径)。
|
||||
|
||||
## 配置
|
||||
|
||||
`config/sower.go` 新增 `[admin]` 段:
|
||||
|
||||
```toml
|
||||
[admin]
|
||||
disable = true # 默认禁用
|
||||
addr = "127.0.0.1:19090" # 默认仅回环
|
||||
password = "" # 为空时启动自动生成
|
||||
```
|
||||
|
||||
校验规则:`disable=false` 时,`addr` 必须是合法 `host:port`;`password` 为空时启动自动生成一次性随机密码并打印到日志(已实现变更,见 `ARCHITECTURE.md`)。
|
||||
|
||||
## 后端设计
|
||||
|
||||
### 1. `router/ruleset.go` — 线程安全规则集
|
||||
|
||||
```go
|
||||
type RuleSet struct {
|
||||
mu sync.RWMutex
|
||||
rules []string // 全部规则(配置内联 + 文件 + 运行期添加)的原始串
|
||||
set map[string]struct{} // 去重键
|
||||
tree *suffixtree.Node // 由 rules 派生的匹配树
|
||||
}
|
||||
```
|
||||
|
||||
- `Match`(RLock)、`List`(返回拷贝)、`Count`、`Add`(O(1) 去重,追加 + 入树,不立即 GC)、`Compact`(GC)、`Remove`(从列表删除 + 从 rules 全量重建树,重建即 GC)
|
||||
- 启动批量加载沿用现有模式:逐条 `Add`,结束时一次 `Compact`,避免大规则文件 O(n²) GC
|
||||
- 删除时从**保留的完整 rules 列表**重建树,保证"配置/文件规则 + 运行期增删"内部一致
|
||||
- 迁移 `Router.BlockRule/DirectRule/ProxyRule` 为 `*RuleSet`,`dnsRuleMatch` 的 `Match(string) bool` 接口兼容,测试调用点机械替换
|
||||
|
||||
### 2. `internal/admin/stats.go` — 流量统计
|
||||
|
||||
- 聚合原子计数:`dnsQueries`、`httpConns`、`httpsConns`、`socksConns`、`bytesUp`、`bytesDown`
|
||||
- 有界 per-domain 表(上限 100000):`domain → {conns, bytesUp, bytesDown, lastSeen, bySource, byClient}`;超限时按 lastSeen 批量淘汰(域名 1024/次、客户端 64/次),用固定大小堆单次扫描选出最旧批次,避免每插入一个新域名都全表扫描
|
||||
- `WrapConn(conn) net.Conn`:**在协议解析前**包装客户端连接,先累计聚合字节(保证 HTTP 头 / TLS ClientHello 不被漏计),解析出域名后 `BindConn(conn, kind, domain)` 归属到域名并自增该域名连接数
|
||||
- 方向语义(以客户端连接为参照):从客户端读到 = 上行(bytesUp),向客户端写入 = 下行(bytesDown)
|
||||
- DNS 计数:`RecordDNS(qname, clientIP)`,由 cmd/sower 的 `dns.Handler` 装饰器调用,**不给 router 加 admin 回调字段**
|
||||
- `Snapshot()` 返回不可变快照:锁内仅复制标量与域名/客户端明细,排序与 Top-N 截断在锁外完成(Top-N 用固定大小堆,避免为只展示 100/50 项而全量排序),缩短锁持有时间以降低对流量记账的阻塞
|
||||
- 默认视图(bytes + all + 无客户端筛选)的 SSE 快照由 server 缓存一个 tick(TTL 与 5s tick 相等),多个默认流共享一次快照计算与一次 history 采样;筛选视图仍即时计算,避免无界按客户端缓存
|
||||
|
||||
### 3. `internal/admin/server.go` — HTTP 服务
|
||||
|
||||
依赖注入接口(便于测试与替换):
|
||||
|
||||
```go
|
||||
type RuleManager interface {
|
||||
RuleList(category Category) ([]string, error)
|
||||
RuleAdd(category Category, rules ...string) error
|
||||
RuleRemove(category Category, rule string) (bool, error)
|
||||
RuleCount(category Category) uint64
|
||||
}
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/session` | 密码换取会话;进程内随机 token + HttpOnly/SameSite=Strict cookie |
|
||||
| DELETE | `/api/session` | 登出,吊销 token |
|
||||
| GET | `/api/status` | version/date/uptime/规则计数/监听器状态 |
|
||||
| GET | `/api/rules?category=block\|direct\|proxy` | 列出某类规则 |
|
||||
| POST | `/api/rules` | `{"category":..., "rules":[...]}` 添加 |
|
||||
| DELETE | `/api/rules` | 同上语义删除(幂等) |
|
||||
| GET | `/api/traffic` | 聚合计数 + 按域名流量 TopN |
|
||||
| GET | `/` 及 SPA 路由 | 静态资源(embed FS),未匹配路径回退 index.html |
|
||||
|
||||
安全约束:
|
||||
|
||||
- 密码**不落 localStorage**;认证后由 HttpOnly cookie 携带
|
||||
- 会话 token:`crypto/rand` 32 字节 hex,服务端 map 持有,24h 过期,惰性清理
|
||||
- 密码比对用 SHA-256 摘要后 `subtle.ConstantTimeCompare`(长度无关恒定时间)
|
||||
- 变更类请求(非 GET/HEAD)校验 Origin 同源,不一致 403
|
||||
- JSON body 限额 64KB,`DisallowUnknownFields` 严格解码,类别白名单校验,单次规则条数与单条长度受限
|
||||
- 静态资源与 API 同 server,优雅关闭走 `http.Server.Shutdown`
|
||||
|
||||
### 4. `cmd/sower` 接线
|
||||
|
||||
- `proxy.go`:三个 handler(HTTP/HTTPS/SOCKS5)在协议解析前 `stats.WrapConn(conn)`,解析出域名后 `BindConn`
|
||||
- DNS:`dnsStatsHandler{dns.Handler, *admin.Stats}` 装饰器包 `dns.Server.Handler`
|
||||
- `startAdminListener(ctx, cfg, r, stats, errCh)`:`disable` 时直接返回;否则建 listener + `srv.Serve`,ctx 取消时 `Shutdown`
|
||||
- 版本/日期经 Options 注入
|
||||
|
||||
## 前端设计(`web/`)
|
||||
|
||||
- **UI 层用 shadcn-svelte 风格组件**:Tailwind CSS v4 + lucide-svelte,浅色主题(`main.ts` 不挂 dark 类)。组件手维护在 `src/lib/components/ui/`(button/card/input/textarea/label/badge/dropdown-menu/table/alert),原生 Svelte 5 实现,不依赖 bits-ui。不要用 `shadcn-svelte add` 覆盖这些文件(registry 产物会重新引入 bits-ui)。
|
||||
- Svelte 5(runes)+ Vite,`bun install` / `bun run build` / `bun run check`(svelte-check)
|
||||
- 视图:Login(Card + 密码 → POST /api/session)、Overview(卡片栅格)、Rules(按面包屑 category 上下文切换分类:列表 + 添加 + 删除)、Traffic(Table)
|
||||
- **高级菜单面包屑**(参照 cyberguard 的 `AppBreadcrumbHeader`):品牌 → 当前分区(DropdownMenu 下拉列出全部页面,带图标/描述/当前态)→ 动态上下文段(Rules 的 category 段可下拉切换 block/direct/proxy;Overview 的 version 段、Traffic 的 scope 段为静态段);右侧收藏星标(localStorage 持久化)+ 页面菜单 + 状态徽章 + 退出。定位/键盘导航/外点关闭由手维护的 DropdownMenu(fixed + 原生焦点/键盘)承担。
|
||||
- 有界轮询(3–5s),401 时回登录;loading/error/empty 三态齐全;不引图表库。已实现改为 SSE 实时流(`/api/stream`,会话续期/过期事件),图表用自研 `AreaChart`(`src/lib/components/AreaChart.svelte`)。
|
||||
- `traffic.domains` 为空时服务端返回 `[]` 而非 `null`(Go nil slice 会序列化成 null,前端 `null.length` 会抛错);前端同时做 `?? []` 防御
|
||||
- `web/embed.go`:`//go:embed all:dist` 导出 `fs.Sub(FS, "dist")`
|
||||
- 构建产物 `web/dist` **提交进仓库**(go:embed 要求编译期文件存在,占位符会产生"能编译但 admin 页面是坏的"的二进制)
|
||||
|
||||
## 构建
|
||||
|
||||
`Makefile`:
|
||||
|
||||
- `build: web sower sowerd`;`sower` 目标依赖 `web`(`bun install --frozen-lockfile` + `bun run build`),`sowerd` 不依赖
|
||||
- 保留独立 `web` 目标;`.gitignore` 增加 `web/node_modules`、`web/dist` 的排除处理(dist 需提交,故不 ignore)
|
||||
|
||||
## 测试清单
|
||||
|
||||
- Go:`RuleSet` 并发 Add/Remove/Match(-race);admin 认证 / 畸形 JSON / 非法 category / CRUD / 静态回退 / 关闭;stats 方向与字节核算 / 域名上限淘汰;config 校验
|
||||
- 前端:`bun run check`、`bun run build`
|
||||
- 全量:`go vet ./...`、`go test -race ./...`、`make build`
|
||||
- 端到端:启动真实 admin listener,浏览器截图桌面/移动宽度验证渲染与控件无重叠
|
||||
|
||||
## 文档收尾
|
||||
|
||||
- 定稿后回写 `ARCHITECTURE.md`(admin 边界、数据流、设计决策)与 `README.md`(admin 用法);`[admin]` 段示例以 `config/sower.toml` 为准
|
||||
- **不要用 `shadcn-svelte add` 覆盖 `web/src/lib/components/ui/`**:registry 产物会重新引入 bits-ui,而项目坚持手维护原生 Svelte 5 组件。UI 为 Tailwind v4 + lucide-svelte 浅色主题。
|
||||
- **`web/dist` 必须提交进仓库**:`go:embed all:dist` 要求编译期文件存在,用占位符会产出"能编译但 admin 页面是坏的"的二进制。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Admin 规则持久化与配置页设计
|
||||
|
||||
Status: 已实现并部署验证(2026-08-07);本文为历史计划,非权威来源。权威规则见根目录 `ARCHITECTURE.md`。
|
||||
Supersedes: `docs/plans/admin-console.md` 中的 MVP 边界「规则变更仅运行期生效,重启后重置,不改写 TOML、不持久化」
|
||||
Supersedes: 早期 admin console MVP 计划的边界「规则变更仅运行期生效,重启后重置,不改写 TOML、不持久化」
|
||||
Scope: `cmd/sower` admin 控制台
|
||||
|
||||
## 后续演进(超出本文 v1 范围,已实现)
|
||||
@@ -174,4 +174,4 @@ PATCH /api/config
|
||||
2. 规则持久化接通(RuleManager 适配层 + 启动顺序 + GC)+ 规则页保存状态/Undo/变更汇总;
|
||||
3. `slog.LevelVar` 接线 + `router.SetDNS`;
|
||||
4. `GET/PATCH /api/config` + 配置页(展示先行,编辑随后);
|
||||
5. 文档:定稿后回写 `docs/plans/admin-console.md` 的范围说明(或沉淀为主设计文档)。
|
||||
5. 文档:定稿后把规则与配置规则沉淀到主设计文档(`ARCHITECTURE.md` 设计决策)。
|
||||
|
||||
+15
-102
@@ -1,116 +1,29 @@
|
||||
# Plan: sowerd Site Routing by SNI
|
||||
|
||||
状态:已实现;本文为非权威历史计划。权威规则见根目录 `ARCHITECTURE.md`。
|
||||
状态:已实现;本文为非权威历史计划。权威规则见根目录 `ARCHITECTURE.md`,安全规则见 `docs/security.md`。
|
||||
|
||||
## 背景与现状
|
||||
|
||||
`sowerd` 在 443 上对每条连接先探测 `sower` transport,不匹配时 fallback 到单一 `fake_site`:
|
||||
- 若 `fake_site` 是本地目录:在 `127.0.0.1:80` 用 `http.FileServer` 服务,443 fallback 通过 `relay.RelayTo` 转发到 `127.0.0.1:80`。
|
||||
- 若 `fake_site` 是 `host:port`:443 fallback 直接裸 TCP relay 到该地址。
|
||||
|
||||
该 fallback 语义是"单一伪装/兜底站点",无法按访问域名路由到不同上游。
|
||||
本文只保留已定稿设计里**不容易从代码复现的取舍记录**;配置字段、校验分支与运行流程以代码和 `ARCHITECTURE.md` 为准。
|
||||
|
||||
## 目标
|
||||
|
||||
将 443 的 fallback 从单一目标扩展为按 TLS SNI 路由:
|
||||
- 配置若干 `site_routes`,每条映射一组精确域名到一个 upstream URL(`http://` 或 `https://`)。
|
||||
- 命中某 route 的连接转发到对应 upstream。
|
||||
- 未命中任何 route 的连接走配置的 `fake_site` 兜底;当 `fake_site` 是目录时,由本地 fileserver 服务。
|
||||
将 443 的 fallback 从单一 `fake_site` 扩展为按 TLS SNI 路由:若干 `site_routes` 把一组精确域名映射到 upstream URL,未命中回落 `fake_site`。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不动 `sower` 认证流量的目标路由(由 transport 帧解码决定,与域名无关)。
|
||||
- 不支持通配域名(`*.example.com`)。精确匹配即可。
|
||||
- 不在 `sower` 客户端侧实现;本功能仅限 `sowerd`。
|
||||
- HTTP(:80)请求只做重定向到 HTTPS,不参与 site routing。
|
||||
- 不支持通配域名(`*.example.com`),只做精确匹配;拒绝理由见 `docs/security.md` 的 "Rejected Directions"。
|
||||
- 不在 `sower` 客户端侧实现。
|
||||
- HTTP(:80)不参与 site routing(拒绝理由见 `docs/security.md`)。
|
||||
|
||||
## 设计
|
||||
## 关键取舍
|
||||
|
||||
### 配置
|
||||
- **精确 SNI 匹配,不做通配**:SNI 路由是精确 map 命中,通配要么让所有子域塌到同一 upstream(失去按域分流的意义),要么在边缘最热路径上引入优先级歧义。
|
||||
- **用 reverse proxy 而非裸 TCP relay**:只有交给 `http.Server` 解析 HTTP 才能拿到路径做前缀分流,并统一重写 outbound `Host` 与转发头。代价是每条连接多一层 HTTP 解析。
|
||||
- **单连接 listener 模式**:把 `rereadConn` 包装成只 `Accept` 一次的 `net.Listener` 交给 `http.Server.Serve`,让标准库处理请求生命周期,而不是自写 HTTP 解析循环。
|
||||
- **HTTP/1.1-only 是有意决策**:`NextProtos` 只留 `http/1.1`;advertise h2 后 WebSocket 需要 RFC 8441 extended CONNECT,stdlib 不支持。详细证据与 per-SNI ALPN 替代方案被拒的理由见 `docs/security.md` 的 "Rejected Directions"。
|
||||
- **未配置 `site_routes` 时行为与改造前完全一致**:`fake_site` 字段保留,语义不变(本地目录或 `host:port`)。
|
||||
|
||||
`config/sowerd.go` 新增:
|
||||
## 已推翻的实现决策
|
||||
|
||||
```go
|
||||
type SiteRoute struct {
|
||||
Domains []string // 精确域名,小写归一化
|
||||
Upstream string // 完整 URL,如 http://127.0.0.1:8080 或 https://backend.example.com
|
||||
}
|
||||
|
||||
type SowerdConfig struct {
|
||||
// ...现有字段...
|
||||
SiteRoutes []SiteRoute
|
||||
FakeSite string // 保留,作为未命中时的最终兜底
|
||||
}
|
||||
```
|
||||
|
||||
`Validate` 约束:
|
||||
- `Upstream` 必须是合法 URL,scheme 仅允许 `http`/`https`。
|
||||
- `Domains` 非空,每项小写归一化,不含通配符 `*`,不重复。
|
||||
- 同一域名不得出现在多条 route 中(重复即配置错误,启动失败)。
|
||||
- `FakeSite` 语义不变:未命中 route 时使用;仍要求是已存在目录或 `host:port`。
|
||||
|
||||
示例(TOML):
|
||||
|
||||
```toml
|
||||
fake_site = "/var/www"
|
||||
|
||||
[[site_routes]]
|
||||
domains = ["a.example.com", "b.example.com"]
|
||||
upstream = "http://127.0.0.1:8080"
|
||||
|
||||
[[site_routes]]
|
||||
domains = ["c.example.com"]
|
||||
upstream = "https://backend.example.com"
|
||||
```
|
||||
|
||||
### 匹配规则
|
||||
|
||||
- 构建 `map[string]*url.URL`:SNI → upstream。
|
||||
- `handleConn` 中两个 transport 探测失败后,取 `(*tls.Conn).ConnectionState().ServerName`,小写归一化后查 map。
|
||||
- 命中:用 `httputil.ReverseProxy` 转发到 upstream URL。
|
||||
- 未命中:走现有 `fakeSite` 兜底(本地目录时 relay 到 `127.0.0.1:80` 的 fileserver)。
|
||||
|
||||
### 证书
|
||||
|
||||
- autocert 模式(未配置 `cert.cert`):`autocert.Manager.GetCertificate` 已按 SNI 自动申请,多域名无需改动。
|
||||
- 自定义 cert 模式:单一证书只覆盖配置的 SAN,多域名路由需自行提供覆盖所有域名的多 SAN 证书;否则非证书覆盖域名的 TLS 握手会失败。文档需声明此约束。
|
||||
|
||||
### 443 fallback 流程改动
|
||||
|
||||
当前 fallback 用 `relay.RelayTo(rereadConn, fakeSite)` 裸 TCP 转发。新流程:
|
||||
|
||||
1. transport 探测失败后,`rereadConn.Stop().Reread()` 重置字节流到起点。
|
||||
2. 取 SNI。
|
||||
3. 若 SNI 命中 route:
|
||||
- 构造 `httputil.NewSingleHostReverseProxy(targetURL)`。
|
||||
- 用单连接 listener 模式(把 `rereadConn` 包装成一次性 `net.Listener`)交给 `http.Server{Handler: reverseProxy}.Serve(listener)`,让标准库解析 HTTP 请求并反向代理。
|
||||
4. 若未命中:保持现有 `relay.RelayTo(rereadConn, fakeSite)` 行为不变。
|
||||
|
||||
单连接 listener 模式:实现一个只 `Accept` 一次 `rereadConn`、之后阻塞直到连接关闭的 `net.Listener`,供 `http.Server.Serve` 消费。
|
||||
|
||||
### HTTP/2 限制
|
||||
|
||||
当前 `NextProtos = ["http/1.1", "h2"]`。fallback 的 reverse proxy 路径在 MVP 仅支持 HTTP/1.1(`http.Server` 默认不启用 h2,启用需额外 `http2.ConfigureServer`)。MVP 决策:将 `NextProtos` 改为 `["http/1.1"]`,简化 fallback 行为,避免协商到 h2 后 reverse proxy 不支持的错配。
|
||||
|
||||
影响:fallback/伪装站点仅 HTTP/1.1;`sower` transport 不走 ALPN,不受影响。
|
||||
|
||||
## 兼容性
|
||||
|
||||
- 未配置 `site_routes` 时,行为与当前完全一致(单一 `fake_site` 兜底)。
|
||||
- `fake_site` 字段保留,语义不变:可以是本地目录,也可以是 `host:port`。
|
||||
|
||||
## 实现决策
|
||||
|
||||
- reverse proxy 会把 outbound `Host` 显式改写为 upstream Host,路径和查询参数保留。
|
||||
- reverse proxy 会拒绝 HTTP upgrade 请求,避免 fallback 连接被 hijack 后生命周期不可控。
|
||||
- reverse proxy 设置客户端 header、upstream dial、TLS handshake 和 upstream response header 超时。
|
||||
|
||||
## 实现清单
|
||||
|
||||
1. `config/sowerd.go`:新增 `SiteRoute`、`SiteRoutes`,`Validate`,示例配置字符串。
|
||||
2. `config/sowerd_test.go`:覆盖 Validate 各分支。
|
||||
3. `cmd/sowerd`:新增 site router(map + 查询)与单连接 listener。
|
||||
4. `cmd/sowerd/main.go`:`handleConn` fallback 分支接入 router + reverse proxy;`NextProtos` 改为 `http/1.1`。
|
||||
5. `cmd/sowerd/main_test.go`:SNI 命中/未命中/无 routes 兼容的集成测试(用本地 dummy upstream listener)。
|
||||
6. `ARCHITECTURE.md`:定稿后回写 sowerd data flow 与 design decisions。
|
||||
7. `docs/README.md`:索引更新。
|
||||
- ~~reverse proxy 会拒绝 HTTP upgrade 请求~~ — **已推翻**:现行实现不做任何拒绝,`httputil.ReverseProxy` 在 101 时经 hijack 直通,`TestReverseProxyConnWebSocket` 与 `TestReverseProxyConnPathRoutedWebSocket` 均通过,见 `ARCHITECTURE.md`。
|
||||
- reverse proxy 会把 outbound `Host` 显式改写为 upstream Host,路径和查询参数保留;并设置客户端 header、upstream dial、TLS handshake、upstream response header 超时。
|
||||
|
||||
@@ -103,6 +103,28 @@ Decisions recorded here so they are not re-litigated without new evidence:
|
||||
(golang/go#71128). The per-SNI ALPN workaround (h1-only for WS-bearing
|
||||
routes) was declined: it makes "new WS routes must opt out of h2" a silent
|
||||
failure mode on an anti-censorship edge.
|
||||
- **Wildcard `site_routes` domains** (`*.example.com`) — rejected in
|
||||
`config/sowerd.go` (`validateSiteRoutes`) and must stay rejected. SNI
|
||||
dispatch is an exact-match map (`siteRouter.lookup`); a wildcard either
|
||||
collapses every subdomain onto one upstream — losing the per-domain
|
||||
routing it was meant to add — or needs priority/ambiguity rules on the
|
||||
edge's hottest path. `autocert.HostWhitelist` is exact-match too, so
|
||||
wildcard issuance would additionally force a hand-written `HostPolicy`.
|
||||
- **Additional listen addresses and configurable 80/443 ports** — one
|
||||
`serve_ip` and hardcoded 80/443 are deliberate: an edge listening on a
|
||||
non-standard port is itself an active-probing signal. Multiple public
|
||||
addresses are served by one instance per address, not by widening this
|
||||
listener. `serve_ip` may be a concrete address to bind a single interface.
|
||||
- **Per-site certificate binding** — the edge's certificate set is
|
||||
independent of the `site_routes` table (autocert issues for the union of
|
||||
routed domains plus `cert.domains`; static mode serves one pair for every
|
||||
SNI). Keeping it independent means a prober cannot enumerate configured
|
||||
sites from the certificate. Multi-SAN certificates already cover the
|
||||
legitimate multi-domain case.
|
||||
- **Domain routing on plain HTTP (:80)** — the :80 listener only completes
|
||||
ACME challenges and 308-redirects public traffic to HTTPS; the `fake_site`
|
||||
directory is served there on loopback only (`isLocalRemoteAddr`). Routing
|
||||
:80 by domain would hand out a cleartext bypass of the TLS edge.
|
||||
|
||||
## Residual Accepted Signals
|
||||
|
||||
|
||||
Reference in New Issue
Block a user