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.
144 lines
4.6 KiB
Go
144 lines
4.6 KiB
Go
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])
|
|
}
|
|
}
|
|
}
|