Verify SIP-only notifications against native Asterisk on test host

This commit is contained in:
2026-10-03 13:10:30 +08:00
parent a52fe5a741
commit 36195075af
8 changed files with 49 additions and 11 deletions
+1 -1
View File
@@ -80,7 +80,7 @@
- P01–P08 及 K01–K16 已完成**项目内隔离 Mock** 核验;本轮把分散的人类可读契约、文档与第三方对接合为唯一当前规范,不重审已确认规则。若未来另获启动开发/审查子 Agent 授权,必须按使用者指定的 `gpt-5.6-luna`、`max` 思考和 `fast: true` 逐项核验并显式配置,不静默换模型、降档或关闭 fast。
- 唯一现行 SaaS↔Dispatcher 业务规范是 [`docs/thirds/saas-dispatcher.md`](docs/thirds/saas-dispatcher.md);当前项目内 Schema、拓扑、正反例及来源/hash 在 [`contracts/local/`](contracts/local/);内部 Agent RPC 在 [`proto/agent/agent.proto`](proto/agent/agent.proto)。本地验收与外部缺口见 [`docs/evidence/saas-dispatcher-p08-acceptance.md`](docs/evidence/saas-dispatcher-p08-acceptance.md)。Markdown 不代替机器合同或外部签收,也不另外维护一份平行字段定义。
- 已由当前合同来源清单固定哈希的历史提案与计划保留**原字节**于 [`docs/archive/sources/`](docs/archive/sources/README.md),使用者原有未提交的两份旧对接文档也按原字节归档;旧上游 v1 在 [`docs/archive/upstream/`](docs/archive/upstream/README.md) 可离线校验,但不嵌入运行合同。旧 F/W 工作包、旧 MQ-only 合同及归档不作为当前运行入口。固定 MQ `v1`、HTTP `/internal/v1/dispatcher/...` 和业务 revision 是现行通信规则,不是自有实现代次;不得为历史路径新建兼容或回退。
- 当前业务范围仍仅**单节点、单 Dispatcher、单 Agent、单 Cell、单租户和隔离 Mock**。根命令只接受显式 `agent`/`dispatcher`;mixed/real 启动即拒绝。另有严格隔离的 `--mode sip-only`:只允许 Dispatcher 读完整 SIP、持久接纳归属 `sip.config`、经已激活双向 TLS Agent 会话将完整快照应用到原生 Asterisk,并从运行态核对版本;不发现任务、不启动业务呼叫、不开放准入、不处理其他业务控制。测试机已仅凭使用者批准写入三条免鉴权、无需 REGISTER 的 UDP 历史登记线路并核对运行态,无拨号;这不证明供应商线路可用或生产签收。没有真实 SaaS、management、OSS、AI 供应商或生产签收;生产发布包仍为 `production_approval=false`。任何本机 Mock 或 SIP-only 核验均不授权真实呼叫。
- 当前业务范围仍仅**单节点、单 Dispatcher、单 Agent、单 Cell、单租户和隔离 Mock**。根命令只接受显式 `agent`/`dispatcher`;mixed/real 启动即拒绝。另有严格隔离的 `--mode sip-only`:只允许 Dispatcher 读完整 SIP、持久接纳归属 `sip.config`、经已激活双向 TLS Agent 会话将完整快照应用到原生 Asterisk,并从运行态核对版本;不发现任务、不启动业务呼叫、不开放准入、不处理其他业务控制。测试机已凭使用者批准,将三条历史登记地址以**测试快照显式声明的** UDP/IP 鉴权、无需 REGISTER 配置写入并核对 Asterisk 运行态;供应商尚未确认这些实际线路是否满足上述参数,无拨号,不证明线路可用或生产签收。没有真实 SaaS、management、OSS、AI 供应商或生产签收;生产发布包仍为 `production_approval=false`。任何本机 Mock 或 SIP-only 核验均不授权真实呼叫。
- 开发按 TDD 分批,小步提交;不得覆盖使用者现存修改/未跟踪文件,不自动清理、迁移或覆盖任何现存 SQLite、spool、outbox 和 Agent 恢复文件。旧 `.executions` 及恢复根目录中旧 `.uploads`、`.upload-locks`、逐执行 `state.json` 的发现须只读失败关闭,现存未交付事实由使用者确认处置。真实云账号、EIP、线路、拨号、生产部署和共享数据操作分别需要明确授权。
## SaaS、Dispatcher 与 Agent 的现行边界
+13 -5
View File
@@ -1,6 +1,6 @@
# Nonproduction native Asterisk user service — 2026-10-03
This is **host-only evidence**, not SIP trunk, outbound-call, SaaS, or production acceptance.
This is **nonproduction host and isolated-Mock channel evidence**, not carrier trunk, outbound-call, external SaaS, or production acceptance.
The test host's address and raw logs are omitted from committed evidence.
- Fresh Debian 13 amd64 host, root directory mode `0755 root:root` (read-only inspection).
@@ -14,12 +14,20 @@ The test host's address and raw logs are omitted from committed evidence.
## Authorized native SIP load — 2026-10-03
- After confirming **zero active calls**, a fixed UDP transport and Agent-owned include were installed in the `rogee` user config. Restarting the user service took time; an early CLI query found it unready, so no loaded revision was claimed then. Once ready, `go-sip-udp` was observed on `0.0.0.0:5060`.
- The project's native renderer and loader applied a complete approved, registration-free, IP-auth three-trunk snapshot and inspected live PJSIP endpoint, AOR/contact, PCMA and transport state. **Three endpoints loaded, revision 9 recorded, zero active calls.** The initial interrupted observation recorded no revision; recovery required both the exact approved file and matching Asterisk runtime state. No dial, REGISTER, provider authorization, media, or active-call reload was attempted.
- This is a direct native-loader probe, **not yet** proof of Dispatcher→Agent RPC, RabbitMQ `sip.config`, or external SaaS. Only counts, status and revision are retained here; service addresses, caller identities, keys and raw host logs are omitted.
- The project's native renderer and loader applied a complete user-authorized **test** snapshot that explicitly declares UDP/IP authentication and no REGISTER for three historical trunk addresses and inspected live PJSIP endpoint, AOR/contact, PCMA and transport state. **Three endpoints loaded, revision 9 recorded, zero active calls.** The initial interrupted observation recorded no revision; recovery required both the exact approved file and matching Asterisk runtime state. No dial, REGISTER, provider authorization, media, or active-call reload was attempted. The carrier has **not** confirmed that these historical lines actually use the asserted authentication, transport, or registration settings.
- This initial direct native-loader probe **alone** did not prove Dispatcher→Agent RPC or RabbitMQ `sip.config`; the later channel test below does. Only counts, status and revision are retained here; service addresses, caller identities, keys and raw host logs are omitted.
## Isolated HTTPS / RabbitMQ / mTLS → native Asterisk — 2026-10-03
- Test Agent/Dispatcher binary SHA-256 matched locally and on the host: `6e5b7b9b3de0cdd5aeb944d8df842269b4748dd6af4763396b95ea6288f1166d`. A private, self-signed test CA and isolated HTTPS config source supplied the approved complete SIP snapshot; an isolated RabbitMQ container had the SaaS-owned topic exchanges and Dispatcher control queue provisioned **before** the SIP-only Dispatcher started. Only its own assigned control route was consumed. A pinned SSH tunnel carried the Dispatcher's mTLS gRPC session to the SIP-only Agent on the Debian host. No external SaaS or carrier participated.
- Startup read revision **10** and verified **3** native Asterisk endpoints; the Dispatcher checkpoint became `(applied=10, pending=0, call_admission=0)`.
- Two persistent `sip.config` notifications changed only the approved middle trunk's enabled state. Revision **11** loaded **2** native endpoints; revision **12** restored all **3**. Each time, the Dispatcher checkpoint reached the matching applied revision, `pending=0`, and `call_admission=0`; Agent's persisted revision, live endpoint count, and Asterisk's **zero active calls** were independently read. The final observed state is **revision 12, three endpoints**, not an authorization to call.
- A replay of the already applied revision 12 returned to `(12,0,0)` with an empty control queue and unchanged managed-config and verified-state file modification times; no second native write/reload was needed. RabbitMQ publisher confirms only broker acceptance; the separate Agent/Asterisk readback and Dispatcher checkpoint established this isolated application's handling. Neither the publisher confirm nor the local Mock response proves external SaaS receipt or production readiness. A transient user-service startup delay was treated as unready rather than inventing a loaded revision. With lingering disabled, this test does **not** verify service availability after logout or reboot.
- Restricted test certificates, environment files, raw logs and snapshots remain outside the repository; committed evidence contains no credential, service IP, caller identity, audio, or transcript. The test made **no call**, registration, media capture, or active-call reload.
## Still required before a real call or production acceptance
1. Prove the isolated SIP-only Dispatcher→Agent RPC and durable `sip.config` notification path end to end against real Asterisk. Verify endpoint updates while a call is active separately; the business call runtime remains Mock-only and is not authorized to dial. Unsupported authentication/REGISTER and transport changes must continue to fail closed.
2. Provide approved real line configuration and arrange each whitelist trial (trunk, original number, time, attempt count). The fixed Asia/Shanghai 09:00–20:00 gate and per-number daily cap remain mandatory.
1. Verify the effect of endpoint updates **during an active authorized call** separately; SIP-only does not enable business dialing. Unsupported authentication/REGISTER and transport changes must continue to fail closed, and provider-side authentication, registration, routing and capacity remain unverified.
2. Obtain carrier-confirmed authentication, transport and registration requirements for each real line, provide approved real line configuration, and arrange each whitelist trial (trunk, original number, time, attempt count). The fixed Asia/Shanghai 09:00–20:00 gate and per-number daily cap remain mandatory.
3. Establish RabbitMQ/OSS/AI real integrations and nonproduction call-evidence capture. `deploys/test/nonprod-call-evidence.sh` requires root or the required capture capabilities; this host's `rogee` currently has no sudo. If tcpdump, logger, or ARI/PJSIP state is unavailable, do not dial.
4. Enable `rogee` user lingering and verify reboot-persistent `enabled+active` before claiming production readiness. Complete the host/network/dependency diagnostics and external signoffs separately; local `make check` cannot replace them.
+8 -3
View File
@@ -53,8 +53,6 @@ func LoadAgentEnvironment(mode string) (AgentEnvironment, error) {
{"DISPATCHER_ID", &settings.DispatcherID},
{"AGENT_GRPC_LISTEN", &settings.Listen}, {"AGENT_SESSION_PATH", &settings.SessionPath},
{"AGENT_RECOVERY_ROOT", &settings.RecoveryRoot},
{"DISPATCHER_GRPC_ENDPOINT", &settings.DispatcherEndpoint},
{"DISPATCHER_GRPC_SERVER_NAME", &settings.DispatcherServerName},
{"MTLS_CA_FILE", &settings.CAFile}, {"MTLS_CERT_FILE", &settings.CertFile},
{"MTLS_KEY_FILE", &settings.KeyFile},
} {
@@ -68,7 +66,14 @@ func LoadAgentEnvironment(mode string) (AgentEnvironment, error) {
name string
value *string
}
var err error
if mode == "mock" {
if settings.DispatcherEndpoint, err = get("DISPATCHER_GRPC_ENDPOINT"); err != nil {
return AgentEnvironment{}, err
}
if settings.DispatcherServerName, err = get("DISPATCHER_GRPC_SERVER_NAME"); err != nil {
return AgentEnvironment{}, err
}
modeFields = []struct {
name string
value *string
@@ -92,7 +97,7 @@ func LoadAgentEnvironment(mode string) (AgentEnvironment, error) {
if !localGRPCAddress(settings.Listen, true) {
return AgentEnvironment{}, errors.New("AGENT_GRPC_LISTEN must be an isolated local Mock address")
}
if !localGRPCAddress(settings.DispatcherEndpoint, false) {
if mode == "mock" && !localGRPCAddress(settings.DispatcherEndpoint, false) {
return AgentEnvironment{}, errors.New("DISPATCHER_GRPC_ENDPOINT must be an isolated local Mock address")
}
rawFingerprints, err := get("MTLS_PEER_CERT_FINGERPRINTS")
+2
View File
@@ -44,6 +44,8 @@ func TestLoadSIPOnlyAgentEnvironmentDoesNotRequireMockCalls(t *testing.T) {
setAgentEnvironment(t)
t.Setenv("AGENT_MOCK_SCENARIO_FILE", "")
t.Setenv("AGENT_MOCK_APPLIED_SIP_FILE", "")
t.Setenv("DISPATCHER_GRPC_ENDPOINT", "")
t.Setenv("DISPATCHER_GRPC_SERVER_NAME", "")
for name, value := range map[string]string{
"ASTERISK_CONFIG_DIR": "/tmp/asterisk-config",
"ASTERISK_BIN": "/tmp/asterisk",
+7 -1
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/contract"
@@ -43,7 +44,11 @@ func (s SIPOnly) HandleNotification(_ context.Context, route string, body []byte
if notice.DispatcherID != s.DispatcherID || notice.EventType != "sip.config" || notice.Payload.Revision <= 0 {
return errors.New("SIP-only cannot consume another Dispatcher or business control")
}
return s.Store.NoteSIPChange(s.DispatcherID, notice.Payload.Revision)
if err := s.Store.NoteSIPChange(s.DispatcherID, notice.Payload.Revision); err != nil {
return err
}
slog.Info("SIP-only notification durably fenced", "dispatcher_id", s.DispatcherID, "revision", notice.Payload.Revision)
return nil
}
// Sync fetches only the complete SIP snapshot; it never reads task, quota or
@@ -79,5 +84,6 @@ func (s SIPOnly) Sync(ctx context.Context) error {
if err := s.Store.MarkSIPOnlyVerified(s.DispatcherID, sip.Revision); err != nil {
return err
}
slog.Info("SIP-only native revision verified without call admission", "dispatcher_id", s.DispatcherID, "revision", sip.Revision)
return nil
}
+1
View File
@@ -66,5 +66,6 @@ func (s *Server) ApplySIP(ctx context.Context, req *agentpb.ApplySIPRequest) (*a
return nil, status.Error(codes.Unavailable, "native Asterisk SIP loaded-revision mismatch")
}
}
slog.Info("native Asterisk SIP revision confirmed", "dispatcher_id", dispatcherID, "revision", approved.Revision, "trunk_count", len(loaded))
return &agentpb.ApplySIPResponse{TrunkRevision: loaded}, nil
}
+16
View File
@@ -48,6 +48,22 @@ func TestApplySIPOnlyAllowsActivatedSIPService(t *testing.T) {
if attempts != 1 {
t.Fatalf("apply attempts = %d", attempts)
}
for name, changed := range map[string][]byte{
"unsupported digest": []byte(strings.Replace(string(body), `"auth_mode":"ip"`, `"auth_mode":"digest"`, 1)),
"unsupported registration": []byte(strings.Replace(string(body), `"registration_required":false`, `"registration_required":true`, 1)),
"unsupported transport": []byte(strings.Replace(string(body), `"transport":"udp"`, `"transport":"tcp"`, 1)),
"wrong dispatcher": []byte(strings.Replace(string(body), id, "66b3c533-11ec-4afb-b6bd-d2941fa7513d", 1)),
} {
t.Run(name, func(t *testing.T) {
if _, err := server.ApplySIP(context.Background(), &agentpb.ApplySIPRequest{Meta: req.Meta, ApprovedSnapshotJson: changed}); status.Code(err) != codes.InvalidArgument || attempts != 1 {
t.Fatalf("invalid SIP reached native loader: attempts=%d err=%v", attempts, err)
}
})
}
server.applySIP = func(context.Context, []byte) (map[string]int64, error) { return map[string]int64{"trunk-mock": 7}, nil }
if _, err := server.ApplySIP(context.Background(), req); status.Code(err) != codes.Unavailable {
t.Fatalf("stale native load was reported as success: %v", err)
}
server.mode = "mock"
if _, err := server.ApplySIP(context.Background(), req); status.Code(err) != codes.FailedPrecondition || attempts != 1 {
t.Fatalf("mock boundary allowed real SIP apply: attempts=%d err=%v", attempts, err)
+1 -1
View File
@@ -30,4 +30,4 @@ go build -trimpath -buildvcs=false -o "$tmp/sip-go-agent" ./cmd/sip-go-agent
bash scripts/check-current-mq-mock.sh
echo "Current single-node Mock checks passed; see docs/evidence/saas-dispatcher-implementation.md. External SaaS, OSS, AI, Asterisk, SIP, non-production host diagnostics and production acceptance remain unverified."
echo "Current single-node Mock checks passed; see docs/evidence/saas-dispatcher-implementation.md. Separately documented nonproduction SIP-only host checks are not run here; external SaaS, carrier, OSS, AI, active-call reload, host diagnostics and production acceptance remain unverified."