// Package simnet holds shared, stateless helpers for the simnet protocol. // // The per-user simnet credentials are DERIVED from the existing user_subscribe // record (id + uuid), not stored in a dedicated table. This mirrors the Pro // reference (NPanel-backend internal/data/delivery_node.go deriveSimnetUserPSK // and internal/biz/public/subscription/template.go key_id derivation), and is // required for wire compatibility with OmnXT / SlagClient: // // - key_id MUST equal the user_subscribe.id (the node's StaticKeyResolver / // assembler identifies the user by this id in the handshake frame header). // - psk is the user's subscription UUID normalized to lowercase 32-hex. package simnet import ( "encoding/hex" "strings" ) // serverKeyID is the reserved key id for the server-side PSK (AF derivation). const serverKeyID = 0 // DeriveKeyID maps a user_subscribe.id to the simnet key_id. // It matches the reference: subscribeID % (2^31-1), avoiding 0 which collides // with the server key id. func DeriveKeyID(subscribeID int64) int { keyID := int(subscribeID % (1<<31 - 1)) if keyID == serverKeyID { keyID = 1 } return keyID } // DeriveUserPSK normalizes a subscription UUID/password into the per-user PSK. // - canonical UUID -> lowercase hex without dashes (32 chars) // - already 32-char ASCII hex -> lowercased // - otherwise -> hex-encoded bytes of the raw value func DeriveUserPSK(value string) string { trimmed := strings.TrimSpace(value) if isCanonicalUUID(trimmed) { return strings.ToLower(strings.ReplaceAll(trimmed, "-", "")) } if len(trimmed) == 32 && isASCIIHex(trimmed) { return strings.ToLower(trimmed) } return hex.EncodeToString([]byte(trimmed)) } func isCanonicalUUID(value string) bool { if len(value) != 36 { return false } for idx, ch := range value { switch idx { case 8, 13, 18, 23: if ch != '-' { return false } default: if !isASCIIHexRune(ch) { return false } } } return true } func isASCIIHex(value string) bool { if value == "" { return false } for _, ch := range value { if !isASCIIHexRune(ch) { return false } } return true } func isASCIIHexRune(ch rune) bool { return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F') } // officialClientKeywords are User-Agent substrings of first-party clients/SDKs // allowed to receive experimental protocols. Mirrors the Pro reference // (internal/biz/public/subscribe/subscribe.go officialClientKeywords). var officialClientKeywords = []string{"omnxt", "slag/", "slaglab"} // experimentalProtocols must be hidden from non-official clients. var experimentalProtocols = map[string]struct{}{ "simnet": {}, } // ClientSupportsExperimental reports whether the client UA is a first-party // client permitted to receive experimental protocols (simnet). func ClientSupportsExperimental(userAgent string) bool { ua := strings.ToLower(userAgent) for _, kw := range officialClientKeywords { if strings.Contains(ua, kw) { return true } } return false } // IsExperimentalProtocol reports whether a protocol type is experimental // (hidden from non-official clients). func IsExperimentalProtocol(protocol string) bool { _, ok := experimentalProtocols[strings.ToLower(strings.TrimSpace(protocol))] return ok }