feat: Go port of the panel in panel-go/ — drop-in replacement for server.js
CI / test (push) Canceled after 0s
CI / anytls-smoke (push) Canceled after 0s

Pure-stdlib Go implementation of the whole control plane: same
nexusgate.json schema (unknown fields preserved verbatim, explicit
null/false tri-states modeled), same HTTP API and error strings,
embedded frontend (NG_PUBLIC_DIR override), self-implemented scrypt
auth, X25519 Reality keys, panel-hosted keyStore and heartbeat
reconciliation included. Version stays 0.6.8.

Verified: Go tests 33/33 (scrypt + Reality vectors generated from
the Node implementation, subscription goldens byte-identical),
Node suite 52/52 untouched, differential E2E against the Node
panel 50/50 including two-way DB swap. See panel-go/PARITY.md
and panel-go/INSTALL-GO.md.
This commit is contained in:
aaaamoooo
2026-10-20 08:21:54 +00:00
parent abb1a4344b
commit 3951134940
37 changed files with 12183 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
package main
import (
"encoding/base64"
"os"
"reflect"
"testing"
)
func TestRealityVectors(t *testing.T) {
raw, err := os.ReadFile("testdata/reality_vectors.json")
if err != nil {
t.Fatal(err)
}
var vectors []KeyPair
if err := unmarshalJSONUseNumber(raw, &vectors); err != nil {
t.Fatal(err)
}
if len(vectors) == 0 {
t.Fatal("no vectors")
}
for _, v := range vectors {
got, err := realityPublicKeyFromPrivate(v.PrivateKey)
if err != nil {
t.Fatalf("derive from %s: %v", v.PrivateKey, err)
}
if got != v.PublicKey {
t.Errorf("private %s: got public %s, want %s", v.PrivateKey, got, v.PublicKey)
}
}
}
func TestGenerateRealityKeyPairFormat(t *testing.T) {
pair := GenerateRealityKeyPair()
for _, k := range []string{pair.PrivateKey, pair.PublicKey} {
if len(k) != 43 {
t.Errorf("key %q length %d, want 43", k, len(k))
}
raw, err := base64.RawURLEncoding.DecodeString(k)
if err != nil {
t.Errorf("key %q not base64url: %v", k, err)
}
if len(raw) != 32 {
t.Errorf("key %q decodes to %d bytes, want 32", k, len(raw))
}
}
derived, err := realityPublicKeyFromPrivate(pair.PrivateKey)
if err != nil {
t.Fatal(err)
}
if derived != pair.PublicKey {
t.Errorf("generated pair inconsistent: derived %s, public %s", derived, pair.PublicKey)
}
}
func TestGoldenResources(t *testing.T) {
raw, err := os.ReadFile("testdata/golden_resources.json")
if err != nil {
t.Fatal(err)
}
var golden map[string]any
if err := unmarshalJSONUseNumber(raw, &golden); err != nil {
t.Fatal(err)
}
exitPassword := "AwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISI="
rfNode := &Node{
ID: "nod_rf", Name: "RF-Reality", ServerID: "srv_entry",
ExitServerID: StrPtr("srv_exit"),
Protocol: "vless-reality-vision",
Topology: "forward",
ExitProtocol: StrPtr("shadowsocks-2022-aes256"),
NetworkMode: "ipv4",
RealityServerName: "www.amd.com",
RealityDestPort: 443,
Port: 23456,
ExitPort: IntPtr(23457),
Status: "active",
ResourceID: "res_rf",
ExitResourceID: "res_rfx",
WSPath: StrPtr("/rfpath"),
Reality: &RealityConfig{KeyID: "reality-nod_rf", ServerName: "www.amd.com", DestPort: 443},
ExitCredentials: &ExitCredentials{ClientID: "11111111-2222-3333-4444-0000000000ff", Password: exitPassword, SocksUser: "ng_0123456789"},
}
rfClients := []*NodeClient{
{NodeID: "nod_rf", CustomerID: "cus_1", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "unused1", ShortID: "abcd0001ef01"}},
{NodeID: "nod_rf", CustomerID: "cus_2", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000002", RelayPassword: "unused2", ShortID: "abcd0002ef01"}},
}
entryServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.10"}
exitServer := &Server{ID: "srv_exit", PublicAddress: "exit.example.com"}
exitServerLocal := &Server{ID: "srv_exit", PublicAddress: "203.0.113.20"}
ssNode := &Node{
ID: "nod_ss", Name: "SS-Direct", ServerID: "srv_entry",
Protocol: "shadowsocks-2022-aes128",
Topology: "direct",
NetworkMode: "ipv4",
Port: 23458,
Status: "active",
ResourceID: "res_ss",
WSPath: StrPtr("/sspath"),
SSServerKey: StrPtr("CwwNDg8QERITFBUWFxgZGg=="),
}
ssClients := []*NodeClient{
{NodeID: "nod_ss", CustomerID: "cus_1", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01"}},
}
ssServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.11"}
rfEntry, err := BuildNodeResource(BuildNodeResourceParams{
ResourceID: "res_rf", TagPrefix: tagForNode(rfNode), Node: rfNode,
NodeClients: rfClients, Server: entryServer, ExitServer: exitServer,
})
if err != nil {
t.Fatal(err)
}
rfExit, err := BuildNodeExitResource(BuildNodeExitResourceParams{
ResourceID: "res_rfx", TagPrefix: tagForNode(rfNode), Node: rfNode, Server: exitServerLocal,
})
if err != nil {
t.Fatal(err)
}
ssEntry, err := BuildNodeResource(BuildNodeResourceParams{
ResourceID: "res_ss", TagPrefix: tagForNode(ssNode), Node: ssNode,
NodeClients: ssClients, Server: ssServer,
})
if err != nil {
t.Fatal(err)
}
for name, got := range map[string]map[string]any{"rfEntry": rfEntry, "rfExit": rfExit, "ssEntry": ssEntry} {
rawGot, err := marshalJSON(got)
if err != nil {
t.Fatal(err)
}
var decoded any
if err := unmarshalJSONUseNumber(rawGot, &decoded); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(decoded, golden[name]) {
t.Errorf("%s mismatch:\n got: %s\nwant: %v", name, rawGot, golden[name])
}
}
}