feat(simnet): add SimNet protocol end-to-end support
- model: SimNet protocol fields + NormalizeSimnet; type+port protocol uniqueness - server config: OmnXT runtime config delivery via compatible() simnet case - credentials: derive per-user psk/key_id from subscription (pkg/simnet), no new table - subscription: adapter buildOmnxtSimnetConfigs + base64 buildOmnxtProtocolLinks + OmnXT SimNet application (migration 02161) - UA gating: hide experimental protocols from non first-party clients (download + JSON node-list) - admin: normalize simnet on create/update and on GET responses - tests: 21 simnet unit tests; full suite green
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/simnet"
|
||||
)
|
||||
|
||||
// buildOmnxtSimnetConfigs is a subscription template function (registered in
|
||||
// Client.Build) that produces the per-user OmnXT SimNet JSON config array.
|
||||
//
|
||||
// It mirrors the Pro reference (NPanel-backend
|
||||
// internal/biz/public/subscription/template.go buildOmnxtSimnetConfigs):
|
||||
// - per-user simnet_psk / simnet_key_id are DERIVED from the user's
|
||||
// subscription (uuid + user_subscribe.id), never stored.
|
||||
// - the server PSK (key_id=0) is passed through as simnet_server_psk so the
|
||||
// client SDK can derive AF path/magic with the same key material.
|
||||
//
|
||||
// Template usage: {{ buildOmnxtSimnetConfigs .Proxies .UserInfo .Params | toPrettyJson }}
|
||||
func buildOmnxtSimnetConfigs(proxies []map[string]interface{}, userInfo User, params map[string]string) []map[string]interface{} {
|
||||
result := make([]map[string]interface{}, 0)
|
||||
|
||||
proxyMode := strings.TrimSpace(params["proxy_mode"])
|
||||
if proxyMode == "" {
|
||||
proxyMode = "global"
|
||||
}
|
||||
|
||||
dnsServers := []string{"1.1.1.1"}
|
||||
if raw := strings.TrimSpace(params["dns_servers"]); raw != "" {
|
||||
parts := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ',' || r == '\n' || r == '\r'
|
||||
})
|
||||
parsed := make([]string, 0, len(parts))
|
||||
for _, item := range parts {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
parsed = append(parsed, item)
|
||||
}
|
||||
}
|
||||
if len(parsed) > 0 {
|
||||
dnsServers = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Per-user credentials derived from the subscription record (see pkg/simnet).
|
||||
userKeyID := simnet.DeriveKeyID(userInfo.SubscribeID)
|
||||
userPSK := simnet.DeriveUserPSK(userInfo.Password)
|
||||
|
||||
for _, proxy := range proxies {
|
||||
if smString(proxy["Type"]) != "simnet" {
|
||||
continue
|
||||
}
|
||||
|
||||
afEnabled := smBool(proxy["SimnetAfEnabled"])
|
||||
item := map[string]interface{}{
|
||||
"tag": smString(proxy["Name"]),
|
||||
"server_addr": smString(proxy["Server"]),
|
||||
"server_port": smInt(proxy["Port"]),
|
||||
"protocol": "simnet",
|
||||
"sni": smString(proxy["SNI"]),
|
||||
"allow_insecure": smBool(proxy["AllowInsecure"]),
|
||||
"simnet_psk": userPSK,
|
||||
"simnet_key_id": userKeyID,
|
||||
// Server PSK is required for AF path/magic/content-type derivation.
|
||||
"simnet_server_psk": smStringOrNil(proxy["SimnetPsk"]),
|
||||
"simnet_server_key_id": smInt(proxy["SimnetKeyID"]),
|
||||
"simnet_ticket_id": smStringOrNil(proxy["SimnetTicketID"]),
|
||||
"simnet_path": smDefaultString(smString(proxy["SimnetPath"]), "/simnet/session"),
|
||||
"simnet_carrier": smDefaultString(smString(proxy["SimnetCarrier"]), "h2"),
|
||||
"simnet_af_enabled": afEnabled,
|
||||
"simnet_client_max_concurrent_streams": smDefaultInt(smInt(proxy["SimnetClientMaxConcurrentStreams"]), 32),
|
||||
"simnet_client_max_streams_per_session": smDefaultInt(smInt(proxy["SimnetClientMaxStreamsPerSession"]), 512),
|
||||
"simnet_client_session_idle_timeout_secs": smDefaultInt(smInt(proxy["SimnetClientSessionIdleTimeoutSecs"]), 90),
|
||||
"simnet_client_max_udp_sessions": smDefaultInt(smInt(proxy["SimnetClientMaxUDPSessions"]), 64),
|
||||
"proxy_mode": proxyMode,
|
||||
"dns_servers": dnsServers,
|
||||
}
|
||||
if afEnabled {
|
||||
item["simnet_af_path_mode"] = smDefaultString(smString(proxy["SimnetAfPathMode"]), "api")
|
||||
item["simnet_af_path_prefix"] = smStringOrNil(proxy["SimnetAfPathPrefix"])
|
||||
item["simnet_af_path_suffix"] = smStringOrNil(proxy["SimnetAfPathSuffix"])
|
||||
item["simnet_af_magic_mode"] = smDefaultString(smString(proxy["SimnetAfMagicMode"]), "derived")
|
||||
item["simnet_af_response_jitter_ms"] = smDefaultInt(smInt(proxy["SimnetAfResponseJitterMs"]), 50)
|
||||
item["simnet_af_handshake_polymorphism"] = smBool(proxy["SimnetAfHandshakePolymorphism"])
|
||||
item["simnet_af_settings_jitter"] = smBool(proxy["SimnetAfSettingsJitter"])
|
||||
item["simnet_af_fake_header_injection"] = smBool(proxy["SimnetAfFakeHeaderInjection"])
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func smString(v interface{}) string {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func smStringOrNil(v interface{}) interface{} {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func smBool(v interface{}) bool {
|
||||
b, ok := v.(bool)
|
||||
return ok && b
|
||||
}
|
||||
|
||||
func smInt(v interface{}) int {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n
|
||||
case int8:
|
||||
return int(n)
|
||||
case int16:
|
||||
return int(n)
|
||||
case int32:
|
||||
return int(n)
|
||||
case int64:
|
||||
return int(n)
|
||||
case uint:
|
||||
return int(n)
|
||||
case uint8:
|
||||
return int(n)
|
||||
case uint16:
|
||||
return int(n)
|
||||
case uint32:
|
||||
return int(n)
|
||||
case uint64:
|
||||
return int(n)
|
||||
case float32:
|
||||
return int(n)
|
||||
case float64:
|
||||
return int(n)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func smDefaultString(s, def string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return def
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func smDefaultInt(i, def int) int {
|
||||
if i == 0 {
|
||||
return def
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// buildOmnxtProtocolLinks wraps each simnet config into a base64 "simnet://"
|
||||
// link, matching the Pro reference's final delivery format (migration 02140,
|
||||
// template.go buildOmnxtProtocolLinks). Template usage:
|
||||
//
|
||||
// {{- range $link := buildOmnxtProtocolLinks .Proxies .UserInfo .Params }}{{ $link }}
|
||||
// {{- end }}
|
||||
func buildOmnxtProtocolLinks(proxies []map[string]interface{}, userInfo User, params map[string]string) []string {
|
||||
configs := buildOmnxtSimnetConfigs(proxies, userInfo, params)
|
||||
result := make([]string, 0, len(configs))
|
||||
|
||||
for _, item := range configs {
|
||||
serverAddr := smString(item["server_addr"])
|
||||
serverPort := smInt(item["server_port"])
|
||||
tag := smString(item["tag"])
|
||||
if serverAddr == "" || serverPort == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
afEnabled := smBool(item["simnet_af_enabled"])
|
||||
payload := map[string]interface{}{
|
||||
"protocol": "simnet",
|
||||
"server_addr": serverAddr,
|
||||
"server_port": serverPort,
|
||||
"sni": smString(item["sni"]),
|
||||
"simnet_psk": smString(item["simnet_psk"]),
|
||||
"simnet_key_id": smInt(item["simnet_key_id"]),
|
||||
"simnet_server_psk": item["simnet_server_psk"],
|
||||
"simnet_server_key_id": smInt(item["simnet_server_key_id"]),
|
||||
"simnet_ticket_id": item["simnet_ticket_id"],
|
||||
"simnet_path": item["simnet_path"],
|
||||
"simnet_carrier": smString(item["simnet_carrier"]),
|
||||
"simnet_af_enabled": afEnabled,
|
||||
"simnet_client_max_concurrent_streams": smInt(item["simnet_client_max_concurrent_streams"]),
|
||||
"simnet_client_max_streams_per_session": smInt(item["simnet_client_max_streams_per_session"]),
|
||||
"simnet_client_session_idle_timeout_secs": smInt(item["simnet_client_session_idle_timeout_secs"]),
|
||||
"simnet_client_max_udp_sessions": smInt(item["simnet_client_max_udp_sessions"]),
|
||||
"proxy_mode": item["proxy_mode"],
|
||||
"dns_servers": item["dns_servers"],
|
||||
}
|
||||
if afEnabled {
|
||||
payload["simnet_af_path_mode"] = smString(item["simnet_af_path_mode"])
|
||||
payload["simnet_af_path_prefix"] = item["simnet_af_path_prefix"]
|
||||
payload["simnet_af_path_suffix"] = item["simnet_af_path_suffix"]
|
||||
payload["simnet_af_magic_mode"] = smString(item["simnet_af_magic_mode"])
|
||||
payload["simnet_af_response_jitter_ms"] = smInt(item["simnet_af_response_jitter_ms"])
|
||||
payload["simnet_af_handshake_polymorphism"] = smBool(item["simnet_af_handshake_polymorphism"])
|
||||
payload["simnet_af_settings_jitter"] = smBool(item["simnet_af_settings_jitter"])
|
||||
payload["simnet_af_fake_header_injection"] = smBool(item["simnet_af_fake_header_injection"])
|
||||
}
|
||||
|
||||
encoded := encodeProtocolPayload(payload)
|
||||
if encoded == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, "simnet://"+encoded+"#"+url.QueryEscape(tag))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// encodeProtocolPayload url-encodes a payload map and base64-encodes it,
|
||||
// matching the reference encodeProtocolPayload.
|
||||
func encodeProtocolPayload(payload map[string]interface{}) string {
|
||||
values := url.Values{}
|
||||
for key, value := range payload {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
continue
|
||||
case string:
|
||||
if strings.TrimSpace(v) != "" {
|
||||
values.Set(key, v)
|
||||
}
|
||||
case bool:
|
||||
if v {
|
||||
values.Set(key, "1")
|
||||
}
|
||||
case int:
|
||||
if v != 0 {
|
||||
values.Set(key, strconv.Itoa(v))
|
||||
}
|
||||
case int32:
|
||||
if v != 0 {
|
||||
values.Set(key, strconv.FormatInt(int64(v), 10))
|
||||
}
|
||||
case int64:
|
||||
if v != 0 {
|
||||
values.Set(key, strconv.FormatInt(v, 10))
|
||||
}
|
||||
case []string:
|
||||
if len(v) > 0 {
|
||||
values.Set(key, strings.Join(v, ","))
|
||||
}
|
||||
case []interface{}:
|
||||
items := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s := smString(item); s != "" {
|
||||
items = append(items, s)
|
||||
}
|
||||
}
|
||||
if len(items) > 0 {
|
||||
values.Set(key, strings.Join(items, ","))
|
||||
}
|
||||
default:
|
||||
if s := smString(v); s != "" {
|
||||
values.Set(key, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString([]byte(values.Encode()))
|
||||
}
|
||||
Reference in New Issue
Block a user