634b5a7bd0
- 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
274 lines
9.0 KiB
Go
274 lines
9.0 KiB
Go
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()))
|
|
}
|