01 architecting-sea-payment-and-offline-licensing-pipelines (EN+ZH) - rewrite toyyibPay callback: hash = MD5(secretKey+status+order_id+refno+"ok") with hash_equals, transBegin + lockForUpdateByBillCode (no TOCTOU), integer-cents money comparison, fulfilment queued after commit so a gateway outage cannot roll back a paid order - abstract hardware fingerprint to multi-dimensional immutable registers; add k-of-n (4 choose 3) threshold matching section - drop unmeasured metrics (99.8% / 1.8s / 70%); fix duplicated 离线离线 typo 03 traefik-yaegi-plugin-package-name-trap (EN+ZH) - title: drop second-person "Your" - fix YAIEGI -> YAEGI code comment typo - add Go-spec root cause (hyphen is subtraction, go.mod decoupling vs Yaegi) - add .traefik.yml manifest contract paragraph - sanitize YAML thresholds as illustrative; add "where Yaegi stops being the right tool" boundary section - de-second-person the closing paragraph Consultant review pack v1.1 shipped separately (docx 05 = response ledger).
This commit is contained in:
1 parent
c0ea7ea066
commit
a7077a3a1d
4 files changed
+209
-46
No files matched your search
@@ -13,8 +13,8 @@ summary:
|
||||
audience: "FinTech Engineers, SaaS Architects & Technical Founders"
|
||||
painPoint: "Delivering commercial desktop software in Southeast Asia fails when using Western payment rails (Stripe/PayPal) due to low regional credit card penetration and rampant license piracy."
|
||||
impact:
|
||||
- metric: "99.8% Local Conversion"
|
||||
detail: "Integrated toyyibPay for direct FPX online banking and DuitNow QR settlements, bypassing regional credit card checkout drop-offs."
|
||||
- metric: "Card-Free Checkout Path"
|
||||
detail: "Integrated toyyibPay so FPX online banking and DuitNow QR settle without a credit card, removing the regional card-checkout drop-off instead of optimising around it."
|
||||
- metric: "Zero Key Collisions"
|
||||
detail: "Bound licenses to cryptographically signed machine fingerprints via Keygate and Ed25519, preventing multi-machine key sharing while supporting offline validation."
|
||||
- metric: "100% Idempotent Delivery"
|
||||
@@ -34,7 +34,7 @@ Asian markets (specifically Malaysia, Singapore, and Indonesia), standard Wester
|
||||
architectures fall short.
|
||||
|
||||
Dropping standard Stripe Elements or PayPal onto a regional checkout page triggers two immediate barriers:
|
||||
1. **Low Credit Card Penetration**: Across local businesses and independent operators in Malaysia, over **70% of digital transactions occur via FPX (Financial Process Exchange) direct online banking or DuitNow QR rails**, not Visa or Mastercard.
|
||||
1. **Low Credit Card Penetration**: A large share of Malaysian online retail settles over **FPX (Financial Process Exchange) direct online banking or DuitNow QR rails** rather than Visa or Mastercard. A checkout that only renders a card form does not fail slowly — it fails at the first screen.
|
||||
2. **Offline Software Piracy**: Desktop executables distributed as raw binaries suffer immediate redistribution unless tied to cryptographically verified, machine-specific hardware fingerprints.
|
||||
|
||||
To resolve these barriers for an enterprise desktop media-transcoding product, I architected
|
||||
@@ -76,37 +76,99 @@ any database state mutation:
|
||||
```php
|
||||
public function callback(): ResponseInterface
|
||||
{
|
||||
$refNo = $this->request->getPost('refno');
|
||||
$status = $this->request->getPost('status');
|
||||
$billId = $this->request->getPost('billcode');
|
||||
$amount = $this->request->getPost('amount');
|
||||
$refNo = (string) $this->request->getPost('refno');
|
||||
$status = (string) $this->request->getPost('status'); // 1=success 2=pending 3=fail
|
||||
$billId = (string) $this->request->getPost('billcode');
|
||||
$orderId = (string) $this->request->getPost('order_id');
|
||||
$amount = (string) $this->request->getPost('amount');
|
||||
$hash = (string) $this->request->getPost('hash');
|
||||
|
||||
// 1. Idempotency Check: prevent duplicate processing on webhook replays
|
||||
$order = $this->orderModel->findByBillCode($billId);
|
||||
if (!$order || $order['status'] === 'PAID') {
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
// 1. Gateway checksum. toyyibPay ships no signature header: the callback carries
|
||||
// hash = MD5(userSecretKey + status + order_id + refno + "ok").
|
||||
$expected = md5($this->gateway->secretKey() . $status . $orderId . $refNo . 'ok');
|
||||
if (!hash_equals($expected, $hash)) {
|
||||
$this->logger->warning('toyyibpay.callback.bad_hash', ['bill' => $billId]);
|
||||
return $this->response->setStatusCode(401)->setBody('INVALID_HASH');
|
||||
}
|
||||
|
||||
// 2. Strict Status and Amount Validation
|
||||
if ($status === '1' && (float)$amount === (float)$order['expected_amount']) {
|
||||
$this->fulfillmentService->fulfillOrder($order['id']);
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
// Non-success statuses never mutate an order: 2=pending, 3=fail.
|
||||
if ($status !== '1') {
|
||||
return $this->response->setStatusCode(200)->setBody('IGNORED');
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode(400)->setBody('INVALID_PAYMENT');
|
||||
// 2. One transaction, one row-level exclusive lock: the read and the state
|
||||
// transition are the same critical section, so there is no TOCTOU window.
|
||||
$this->db->transBegin();
|
||||
try {
|
||||
$order = $this->orderModel->lockForUpdateByBillCode($billId); // SELECT ... FOR UPDATE
|
||||
if (!$order) {
|
||||
$this->db->transRollback();
|
||||
return $this->response->setStatusCode(404)->setBody('UNKNOWN_BILL');
|
||||
}
|
||||
if ($order['status'] === 'FULFILLED') {
|
||||
$this->db->transCommit(); // replay storm: absorb it, do not re-issue
|
||||
return $this->response->setStatusCode(200)->setBody('OK_IDEMPOTENT');
|
||||
}
|
||||
|
||||
// 3. Money is never compared as a float. Both sides are integer cents.
|
||||
if ($this->toCents($amount) !== (int) $order['expected_amount_cents']) {
|
||||
$this->db->transRollback();
|
||||
$this->logger->error('toyyibpay.callback.amount_mismatch', [
|
||||
'bill' => $billId, 'got' => $amount,
|
||||
'expected_cents' => $order['expected_amount_cents'],
|
||||
]);
|
||||
return $this->response->setStatusCode(409)->setBody('AMOUNT_MISMATCH');
|
||||
}
|
||||
|
||||
$this->orderModel->updateStatus($order['id'], 'PROCESSING');
|
||||
$this->db->transCommit();
|
||||
} catch (Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// 4. Fulfilment is queued after commit, never inside the lock: a Keygate or SMTP
|
||||
// outage must not hold the row lock, and must never roll back a paid order.
|
||||
// That is what makes [PROCESSING] → [RETRY_QUEUE] a safe transition.
|
||||
$this->fulfillmentQueue->push($order['id']);
|
||||
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
}
|
||||
|
||||
/** "12.40" -> 1240, integer arithmetic only; no float ever touches the money path. */
|
||||
private function toCents(string $amount): int
|
||||
{
|
||||
[$ringgit, $sen] = array_pad(explode('.', trim($amount), 2), 2, '0');
|
||||
return ((int) $ringgit) * 100 + (int) str_pad(substr($sen, 0, 2), 2, '0');
|
||||
}
|
||||
```
|
||||
|
||||
A checksum only proves the callback came from the gateway, not that it arrived. The
|
||||
`[PROCESSING]` state therefore also has an **active reconciliation worker**: any order sitting
|
||||
in `PROCESSING` past the settlement window triggers a server-to-server
|
||||
`getBillTransactions` pull against toyyibPay and settles from the authoritative record. Passive
|
||||
webhook and active reconciliation are two independent paths into the same idempotent state
|
||||
transition, which is what makes a lost callback survivable.
|
||||
|
||||
### 2. Hardware-bound licensing via Keygate (Asymmetric Verification)
|
||||
Once payment is locked, the fulfillment daemon does not generate a generic text key. It calls
|
||||
the licensing authority API to mint an activation entitlement locked to the buyer's machine
|
||||
architecture:
|
||||
|
||||
* Each license key encodes an entitlement tier (`standard`, `pro`, `enterprise`), expiration boundary, and feature flags.
|
||||
* Upon first launch, the desktop client hashes CPU ID, Motherboard UUID, and OS installation GUID into an immutable 256-bit hardware signature.
|
||||
* Keygate signs the entitlement payload combining the machine hash using an **Ed25519 private key**.
|
||||
* Upon first launch, the desktop client reads a set of **multi-dimensional immutable hardware registers** — SMBIOS board identifiers, processor architectural signatures, and storage-controller UUIDs — and derives a per-component digest with a server-issued cryptographic salt. The exact selector list and salt derivation are proprietary and deliberately omitted here; publishing them would hand an attacker a precise list of which OS calls to hook.
|
||||
* Keygate signs the entitlement payload combining those per-component digests using an **Ed25519 private key**.
|
||||
* The desktop client embeds only the corresponding **Ed25519 public key**. On every boot, it independently verifies the signature offline against local hardware registers. Reverse engineers cannot spoof validity by patching plain strings, and the client functions flawlessly in air-gapped environments without phoning home.
|
||||
|
||||
**Component-wise digests exist so the licence can survive legitimate hardware changes.** Concatenating
|
||||
every register into one string and hashing it once is the classic mistake: a single RAM upgrade or an
|
||||
OS reinstall changes one input, avalanches the whole digest, and locks a paying customer out. Instead
|
||||
the signed payload carries each component digest as its own field, and the client applies **k-of-n
|
||||
threshold matching** — the machine is accepted when at least three of four components match *and* at
|
||||
least one of the two chassis-anchored components (board or CPU) is among them. Swapping a disk or
|
||||
reinstalling the OS is a legitimate upgrade; transplanting the licence onto a different machine
|
||||
changes the board and CPU together and fails the check.
|
||||
|
||||
---
|
||||
|
||||
## Direct downloads with CDN failover
|
||||
@@ -168,6 +230,6 @@ Deploying commercial software in emerging markets requires rethinking standard S
|
||||
## The result
|
||||
|
||||
The automated billing and licensing architecture delivered:
|
||||
* **Zero manual intervention**: Hundreds of commercial transactions processed end-to-end, from FPX bank debit to instant license key dispatch within **1.8 seconds** of webhook receipt.
|
||||
* **Zero manual intervention**: Transactions run end-to-end from FPX bank debit to license-key dispatch with no operator in the loop; the fulfilment step is measured in seconds of webhook receipt, and a gateway outage defers it to the retry queue instead of dropping it.
|
||||
* **Eliminated key sharing**: Zero pirated key leaks across customer organizations thanks to hardware-bound Keygate hashing.
|
||||
* **Total visibility**: Instant Telegram operations telemetry ensuring immediate notice of any gateway or SMTP delivery anomalies.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
ogImage: "/og/traefik-yaegi-plugin-package-name-trap.jpg"
|
||||
banner: "/banners/traefik-yaegi-plugin-package-name-trap.jpg"
|
||||
title: "Why Your Traefik Yaegi Plugin Fails on Boot: The Module Hyphen to Underscore Trap"
|
||||
title: "Why Traefik Yaegi Plugins Fail on Boot: The Module Hyphen-to-Underscore Trap"
|
||||
description: "Why Traefik crashes on boot with 'CreateConfig failed with Yaegi' when loading custom Go plugins: diagnosing the hidden package naming convention enforced by the Yaegi dynamic interpreter."
|
||||
pubDate: 2026-10-27
|
||||
category: notes
|
||||
@@ -83,10 +83,18 @@ Instead, Traefik's dynamic plugin loader derives the expected package symbol ide
|
||||
|
||||
$$\text{Module Path}: \texttt{github.com/hoelee/traefik-botfilter} \longrightarrow \text{Package}: \texttt{traefik\_botfilter}$$
|
||||
|
||||
There is a language-level reason this trap exists at all. A hyphen is the subtraction operator in
|
||||
Go, so `package traefik-botfilter` is not merely unusual — it is ungrammatical and can never be
|
||||
compiled. The standard toolchain therefore relies on `go.mod` to decouple the hyphenated *directory*
|
||||
and module path from the identifier declared inside the files. Yaegi works from source text, not from
|
||||
a resolved module graph: it has to synthesise an evaluation context and then reflect for
|
||||
`CreateConfig` inside a namespace it computed itself. Nothing in that path consults `go.mod`, so the
|
||||
declared identifier has to agree with the loader's computed one.
|
||||
|
||||
Because my Go source files started with:
|
||||
```go
|
||||
// botfilter.go
|
||||
package botfilter // <-- YAIEGI COULD NOT FIND THE EXPORTED SYMBOLS
|
||||
package botfilter // <-- YAEGI COULD NOT FIND THE EXPORTED SYMBOLS
|
||||
```
|
||||
|
||||
Yaegi inspected the AST looking for `traefik_botfilter.CreateConfig`. It found only `botfilter.CreateConfig`.
|
||||
@@ -124,10 +132,17 @@ plugins-local/
|
||||
└── ...
|
||||
```
|
||||
|
||||
The `.traefik.yml` manifest in that directory is part of the same contract: its `import` field names
|
||||
the module path whose final segment the loader will sanitise, and `displayName` / `type` must match
|
||||
what the dynamic configuration references. A mismatch between the manifest's import path and the
|
||||
`package` clause of the source files produces exactly the same opaque `symbol not found` failure as
|
||||
the hyphen trap, which is why both have to be verified together rather than fixed one at a time.
|
||||
|
||||
<details class="tech-deep-dive">
|
||||
<summary>🔍 View corrected dynamic Traefik configuration (YAML excerpt, 29 lines)</summary>
|
||||
|
||||
```yaml
|
||||
# Illustrative sanitized configuration — the live thresholds differ.
|
||||
http:
|
||||
middlewares:
|
||||
botfilter:
|
||||
@@ -138,7 +153,7 @@ http:
|
||||
requireAccept: true
|
||||
browserValidation: true
|
||||
temporaryBanMinutes: 15
|
||||
scoreThreshold: 100
|
||||
scoreThreshold: 120
|
||||
blockedPaths:
|
||||
- /.env
|
||||
- /.git
|
||||
@@ -173,6 +188,19 @@ cleanly in **0.42 seconds**:
|
||||
2026-10-23T04:48:44Z INF Plugin loaded successfully module=github.com/hoelee/traefik-botfilter
|
||||
```
|
||||
|
||||
When developing custom Traefik plugins, remember: **the Go compiler's acceptance is only half the battle.**
|
||||
Matching the package declaration to the sanitized final segment of the Go module path is mandatory,
|
||||
or Yaegi will silently refuse to start the edge proxy.
|
||||
In custom Traefik plugin engineering, **passing the standard Go compiler is only half the battle.**
|
||||
The package declaration has to match the sanitised final segment of the module path, and the
|
||||
`.traefik.yml` import has to point at the same module — otherwise Yaegi refuses to start the edge
|
||||
proxy and the error text names a symbol that, by every local test, exists.
|
||||
|
||||
## Where a Yaegi plugin stops being the right tool
|
||||
|
||||
An interpreted in-process plugin is the cheapest correct answer for an edge node in the low
|
||||
thousands of requests per second: no Redis, no sidecar, no separate process to supervise, and the
|
||||
interceptor sits in the same memory as the router. The cost is paid at two points. Interpretation
|
||||
runs slower than compiled code, and every plugin allocation lands in the Traefik process's own heap,
|
||||
so GC pressure scales with traffic rather than with rule complexity. Beyond roughly tens of
|
||||
thousands of requests per second, or under a volumetric flood, the ordering has to invert: drop
|
||||
packets in front of the proxy (edge WAF at the CDN, or CrowdSec with nftables/eBPF on the host) and
|
||||
let the Go plugin handle the semantic cases — header shape, request pacing, per-IP scoring — that a
|
||||
packet filter cannot see.
|
||||
@@ -13,10 +13,10 @@ summary:
|
||||
audience: "金融科技工程师、SaaS 架构师与技术业务负责人"
|
||||
painPoint: "在东南亚地区销售桌面客户端软件时,直接采用西方主流支付(Stripe/PayPal)会因极低的信用卡普及率导致弃单率高企,且原始二进制极易遭遇跨机器盗版传播。"
|
||||
impact:
|
||||
- metric: "99.8% 本地支付成功率"
|
||||
detail: "原生对接 toyyibPay 聚合网关,覆盖马来西亚 FPX 本地在线网银与 DuitNow QR 直付通道,消解信用卡结算门槛。"
|
||||
- metric: "零信用卡收银通道"
|
||||
detail: "原生对接 toyyibPay 聚合网关,覆盖马来西亚 FPX 本地在线网银与 DuitNow QR 直付通道,从根上移除信用卡结算门槛,而不是在卡片表单上做优化。"
|
||||
- metric: "机器码硬件级防盗"
|
||||
detail: "通过 Keygate 将软件授权与 CPU/主板硬件指纹强绑定并签名,彻底杜绝一码多用,同时支持内网离线离线验活。"
|
||||
detail: "通过 Keygate 将软件授权与 CPU/主板硬件指纹强绑定并签名,彻底杜绝一码多用,同时支持内网纯离线验活。"
|
||||
- metric: "100% 幂等状态机履约"
|
||||
detail: "设计带分布式锁的 Webhook 消费引擎,确保在银行网络波动与重试风暴下绝不发生重复发码与状态覆盖。"
|
||||
stack: ["toyyibPay API", "Keygate 授权服务", "PHP/CodeIgniter", "PostgreSQL", "Telegram 自动化"]
|
||||
@@ -32,7 +32,7 @@ dynamicCta: true
|
||||
在面向东南亚区域(尤其是马来西亚、新加坡和印尼)的企业客户与独立专业人员交付桌面客户端工具时,直接套用欧美的常规 SaaS 支付架构往往会在上线第一天遭遇滑铁卢。
|
||||
|
||||
如果在结账页面上直接套用常规的 Stripe Elements 或 PayPal,系统会立即撞上两堵现实高墙:
|
||||
1. **信用卡渗透率低下**:在马来西亚本地企业主与从业者中,超过 **70% 的线上交易严重依赖 FPX(Financial Process Exchange)本地在线网银直连或 DuitNow QR 扫码支付**,绝大多数人不习惯或根本没有可用于扣费的企业信用卡。
|
||||
1. **信用卡渗透率低下**:在马来西亚本地企业主与从业者中,相当大比例的马来西亚线上零售结算走的是 **FPX(Financial Process Exchange)本地在线网银直连或 DuitNow QR 扫码**,而非 Visa / Mastercard。只渲染信用卡表单的结账页不会慢慢变差——它在第一屏就直接失效。
|
||||
2. **离线桌面软件的盗版风险**:如果是纯离线运行的高性能工具(例如批量视频转码器),二进制包一旦交付,如果缺乏基于物理硬件的授权签名机制,极易在团队内部被无限制复制扩散。
|
||||
|
||||
为了解决一款商业级媒体转码桌面软件的商业变现与履约闭环,我主导设计并落地了一套全自动化的计费与授权分发架构。该架构深度整合了本地 FPX 网银网关(`toyyibPay`)、多通道自动熔断降级、机器硬件绑定的离线授权系统(`Keygate`),以及联动 Telegram 与事务邮件的全闭环即时交付通知。
|
||||
@@ -68,35 +68,91 @@ dynamicCta: true
|
||||
```php
|
||||
public function callback(): ResponseInterface
|
||||
{
|
||||
$refNo = $this->request->getPost('refno');
|
||||
$status = $this->request->getPost('status');
|
||||
$billId = $this->request->getPost('billcode');
|
||||
$amount = $this->request->getPost('amount');
|
||||
$refNo = (string) $this->request->getPost('refno');
|
||||
$status = (string) $this->request->getPost('status'); // 1=成功 2=处理中 3=失败
|
||||
$billId = (string) $this->request->getPost('billcode');
|
||||
$orderId = (string) $this->request->getPost('order_id');
|
||||
$amount = (string) $this->request->getPost('amount');
|
||||
$hash = (string) $this->request->getPost('hash');
|
||||
|
||||
// 1. 幂等拦截:如果订单已处于履约完成状态,直接返回 200 终止重试风暴
|
||||
$order = $this->orderModel->findByBillCode($billId);
|
||||
if (!$order || $order['status'] === 'PAID') {
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
// 1. 网关校验和。toyyibPay 不提供签名头,回调里携带的是
|
||||
// hash = MD5(userSecretKey + status + order_id + refno + "ok")
|
||||
$expected = md5($this->gateway->secretKey() . $status . $orderId . $refNo . 'ok');
|
||||
if (!hash_equals($expected, $hash)) {
|
||||
$this->logger->warning('toyyibpay.callback.bad_hash', ['bill' => $billId]);
|
||||
return $this->response->setStatusCode(401)->setBody('INVALID_HASH');
|
||||
}
|
||||
|
||||
// 2. 严格核验状态码与应收金额(防止中间人篡改金额参数)
|
||||
if ($status === '1' && (float)$amount === (float)$order['expected_amount']) {
|
||||
$this->fulfillmentService->fulfillOrder($order['id']);
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
// 非成功状态一律不改动订单:2=处理中,3=失败
|
||||
if ($status !== '1') {
|
||||
return $this->response->setStatusCode(200)->setBody('IGNORED');
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode(400)->setBody('INVALID_PAYMENT');
|
||||
// 2. 一个事务、一把行级排他锁:读取与状态扭转属于同一个临界区,
|
||||
// 因此不存在 TOCTOU 竞态窗口。
|
||||
$this->db->transBegin();
|
||||
try {
|
||||
$order = $this->orderModel->lockForUpdateByBillCode($billId); // SELECT ... FOR UPDATE
|
||||
if (!$order) {
|
||||
$this->db->transRollback();
|
||||
return $this->response->setStatusCode(404)->setBody('UNKNOWN_BILL');
|
||||
}
|
||||
if ($order['status'] === 'FULFILLED') {
|
||||
$this->db->transCommit(); // 重放风暴:吸收掉,绝不二次发码
|
||||
return $this->response->setStatusCode(200)->setBody('OK_IDEMPOTENT');
|
||||
}
|
||||
|
||||
// 3. 金额永不使用浮点比对,双方统一为整型分(cents)
|
||||
if ($this->toCents($amount) !== (int) $order['expected_amount_cents']) {
|
||||
$this->db->transRollback();
|
||||
$this->logger->error('toyyibpay.callback.amount_mismatch', [
|
||||
'bill' => $billId, 'got' => $amount,
|
||||
'expected_cents' => $order['expected_amount_cents'],
|
||||
]);
|
||||
return $this->response->setStatusCode(409)->setBody('AMOUNT_MISMATCH');
|
||||
}
|
||||
|
||||
$this->orderModel->updateStatus($order['id'], 'PROCESSING');
|
||||
$this->db->transCommit();
|
||||
} catch (Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// 4. 履约在事务提交之后入队,绝不在锁内执行:Keygate 或 SMTP 故障既不能占住行锁,
|
||||
// 也绝不能让一笔已付款订单被回滚。这才让 [PROCESSING] → [RETRY_QUEUE] 成为安全状态。
|
||||
$this->fulfillmentQueue->push($order['id']);
|
||||
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
}
|
||||
|
||||
/** "12.40" -> 1240,纯整数运算,浮点永不进入金额链路 */
|
||||
private function toCents(string $amount): int
|
||||
{
|
||||
[$ringgit, $sen] = array_pad(explode('.', trim($amount), 2), 2, '0');
|
||||
return ((int) $ringgit) * 100 + (int) str_pad(substr($sen, 0, 2), 2, '0');
|
||||
}
|
||||
```
|
||||
|
||||
校验和只能证明回调来自网关,不能证明回调确实到达。因此 `[PROCESSING]` 状态额外挂了一个
|
||||
**主动对账 Worker**:任何停留在 `PROCESSING` 超过结算窗口的订单,都会以服务端身份调用
|
||||
`getBillTransactions` 向 toyyibPay 反查权威账单记录并据此结算。被动 Webhook 与主动对账
|
||||
是两条独立汇入同一个幂等状态转移的路径——这才是「回调丢失」不至于变成「客户付了钱没拿到码」的原因。
|
||||
|
||||
### 2. 基于 Keygate 的硬件指纹绑定与非对称离线验活
|
||||
订单扣款成功后,系统不会生成毫无约束的通用明文激活码,而是即时调用授权中心 API,为买家签发绑定其特定计算机硬件特征的专属 License:
|
||||
|
||||
* 授权码内部编码了功能等级(`standard` / `pro` / `enterprise`)、过期时间戳与特征权限集。
|
||||
* 桌面端首次运行时,自动化采集 CPU 序列号、主板 UUID 与操作系统 GUID 进行单向加盐哈希,生成 256 位不可伪造的硬件指纹。
|
||||
* Keygate 服务端使用 **Ed25519 私钥** 对机器指纹与授权元数据执行数字签名。
|
||||
* 桌面端首次运行时,读取一组**多维不可变硬件寄存器**——SMBIOS 主板标识、处理器架构签名与存储控制器 UUID——并以服务端下发的盐值派生逐组件摘要。具体取哪些寄存器、盐值如何派生属于私有实现,本文有意不公开:公开它等于直接告诉逆向者该 Hook 哪几个系统调用。
|
||||
* Keygate 服务端使用 **Ed25519 私钥** 对逐组件摘要与授权元数据执行数字签名。
|
||||
* 桌面客户端仅内置对应的 **Ed25519 公钥**。此后每次冷启动均在本地直接进行离线数学验签,即使客户将工作站置于完全隔离的物理内网机房,软件依然能零请求离线验证,逆向者也无法通过篡改本地内存字符串实现绕过。
|
||||
|
||||
**逐组件摘要的存在,是为了让授权能扛住合法的硬件变更。** 把所有寄存器拼成一个字符串单次哈希
|
||||
是经典错误:加一条内存或重装系统就改变一个输入,整个摘要雪崩式失效,把付了钱的客户锁在门外。
|
||||
正确做法是让签名负载分别携带各组件摘要,客户端执行 **k-of-n 阈值匹配**——四项中至少命中三项,
|
||||
且两项机身锚定组件(主板或 CPU)至少命中其一,即判定为合法硬件微调并放行。换硬盘、重装系统
|
||||
属于正常升级;把授权移植到另一台机器会同时改变主板与 CPU,直接卡在阈值之外。
|
||||
|
||||
---
|
||||
|
||||
## 一键极速直下与 CDN 动态容灾
|
||||
@@ -156,6 +212,6 @@ def notify_fulfillment(order_id, customer_email, bill_code, license_key, downloa
|
||||
## 交付结果
|
||||
|
||||
该全自动计费与分发流水线投产后:
|
||||
* **全流程零人工干预**:处理数百笔商业交易,从用户网银扣款成功到系统完成机器授权签发并通过邮件送达,平均仅耗时 **1.8 秒**。
|
||||
* **全流程零人工干预**:交易从网银扣款成功到授权签发并邮件送达全流程无人值守;履约以「秒」为单位计,而网关故障只会把订单推进重试队列,不会把它丢掉。
|
||||
* **杜绝私下窜码扩散**:依托硬件级 Keygate 指纹绑定,彻底终结了盗版秘钥在多台设备上的随意复制。
|
||||
* **高可用可观测性**:全链路运行日志与 Telegram 即时通知紧密联动,任何网关故障或 SMTP 队列堆积均能在 30 秒内被快速发现并处理。
|
||||
@@ -70,6 +70,12 @@ Traefik 会要求 Yaegi 在插件包中寻找并导出符合中间件契约的
|
||||
|
||||
在标准的 Go 语言工程中,模块路径与目录名经常带有连字符(如 `module github.com/hoelee/traefik-botfilter`),而源码文件首行通常声明不带前缀的合法标识符(如 `package botfilter`)。官方 Go 编译器(`gc`)通过模块导入路径可以完美绑定解析。
|
||||
|
||||
这里还有一层语言层面的根因:在 Go 中连字符 `-` 是减法运算符,因此 `package traefik-botfilter`
|
||||
不是「不常见」,而是不合语法、永远无法编译。标准工具链正是靠 `go.mod` 把带连字符的目录名/模块路径
|
||||
与文件内声明的标识符解耦的。Yaegi 不解析模块图,它从源码文本出发,先合成求值上下文,再在自己算出的
|
||||
命名空间里反射查找 `CreateConfig`——这条路径根本不读 `go.mod`,所以声明的包名必须与加载器算出的那个
|
||||
完全一致。
|
||||
|
||||
**但是,Yaegi 动态解释器并不走标准 Go 模块语义解析流程。**
|
||||
|
||||
Traefik 的动态插件加载器会直接从静态配置中 **`moduleName` 的最后一个路径段** 提取包标识符,并且**自动将所有的连字符(`-`)强制转换为下划线(`_`)**:
|
||||
@@ -162,4 +168,15 @@ http:
|
||||
2026-10-23T04:48:44Z INF Plugin loaded successfully module=github.com/hoelee/traefik-botfilter
|
||||
```
|
||||
|
||||
对于开发 Traefik 动态插件的工程师而言:**Go 官方编译器的通过并不代表动态运行时的认可。** 必须确保包名与模块路径最后一段的下划线转换规则完全一致,才能避免网关在生产启动时发生静默瘫痪。
|
||||
在 Traefik 动态插件工程里,**Go 官方编译器的通过只算赢了一半。** 包声明必须与模块路径清洗后的
|
||||
末段一致,`.traefik.yml` 的 import 又必须指向同一个模块——否则 Yaegi 会拒绝拉起网关,而报错里点名的
|
||||
那个符号,在每一轮本地测试里都明明存在。
|
||||
|
||||
## Yaegi 插件在什么情况下不再是正解
|
||||
|
||||
在每秒数千请求量级的边缘节点上,进程内解释型插件是最便宜的正解:不需要 Redis、不需要边车进程、
|
||||
不需要额外守护,拦截器与路由共享同一块内存。代价在两个地方兑现——解释执行比编译码慢,且插件的
|
||||
每一次分配都落在 Traefik 进程自己的堆上,于是 GC 压力随流量上升,而不是随规则复杂度上升。当单机
|
||||
吞吐进入数万 QPS 量级,或面对洪峰型扫描时,顺序必须反过来:先在代理之前丢包(CDN 侧边缘 WAF,或宿主机
|
||||
上的 CrowdSec 配合 nftables / eBPF),再让 Go 插件处理包过滤看不见的语义问题——头部形态、请求节流、
|
||||
分 IP 评分。
|
||||
Reference in new issue
Block a user