Files

265 lines
6.6 KiB
Go

package proxy
import (
"ant-chrome/backend/internal/apppath"
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
const xrayTLSInsecurePinKey = "_antInsecureSkipVerify"
var xrayTLSPinCache sync.Map
func (m *XrayManager) buildRuntimeConfig(key string, outbound map[string]interface{}, port int, dnsServers string) (string, error) {
return m.buildRuntimeConfigWithRoute(
key,
[]interface{}{outbound},
[]interface{}{
map[string]interface{}{
"type": "field",
"inboundTag": []string{"socks-in"},
"outboundTag": "proxy-out",
},
},
port,
dnsServers,
)
}
func (m *XrayManager) buildRuntimeConfigWithRoute(key string, outbounds []interface{}, rules []interface{}, port int, dnsServers string) (string, error) {
baseDir := m.resolveWorkdir(key)
if err := os.MkdirAll(baseDir, 0o755); err != nil {
return "", err
}
outbounds = sanitizeXrayOutbounds(outbounds)
outbounds = pinXrayInsecureTLSOutbounds(outbounds)
cfgPath := filepath.Join(baseDir, "xray-config.json")
cfg := map[string]interface{}{
"log": map[string]interface{}{
"loglevel": "warning",
"error": filepath.Join(baseDir, "xray-error.log"),
},
"inbounds": []interface{}{
map[string]interface{}{
"tag": "socks-in",
"port": port,
"listen": "127.0.0.1",
"protocol": "socks",
"settings": map[string]interface{}{
"udp": true,
},
"sniffing": xrayBrowserSniffingConfig(),
},
},
"outbounds": append(outbounds,
map[string]interface{}{
"protocol": "direct",
"tag": "direct",
},
map[string]interface{}{
"protocol": "blackhole",
"tag": "block",
},
),
"routing": map[string]interface{}{
"rules": rules,
},
}
if dnsCfg := parseDnsConfig(dnsServers); dnsCfg != nil {
cfg["dns"] = dnsCfg
} else {
cfg["dns"] = defaultXrayDNSConfig()
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return "", err
}
if err := os.WriteFile(cfgPath, data, 0o644); err != nil {
return "", err
}
return cfgPath, nil
}
func defaultXrayDNSConfig() map[string]interface{} {
return map[string]interface{}{
"servers": []interface{}{"223.5.5.5", "119.29.29.29"},
}
}
func sanitizeXrayOutbounds(outbounds []interface{}) []interface{} {
for _, outbound := range outbounds {
removeXrayDeprecatedFields(outbound)
}
return outbounds
}
func pinXrayInsecureTLSOutbounds(outbounds []interface{}) []interface{} {
for _, outbound := range outbounds {
pinXrayInsecureTLSOutbound(outbound)
}
return outbounds
}
func pinXrayInsecureTLSOutbound(outbound interface{}) {
item, ok := outbound.(map[string]interface{})
if !ok {
return
}
stream, _ := item["streamSettings"].(map[string]interface{})
tlsSettings, _ := stream["tlsSettings"].(map[string]interface{})
if tlsSettings == nil || !truthy(tlsSettings[xrayTLSInsecurePinKey]) {
return
}
delete(tlsSettings, xrayTLSInsecurePinKey)
serverName := getStringAny(tlsSettings["serverName"])
host, port := firstXrayOutboundEndpoint(item)
if host == "" || port == "" {
return
}
if serverName == "" {
serverName = host
}
if fingerprints := fetchTLSPeerCertPins(host, port, serverName); len(fingerprints) > 0 {
tlsSettings["pinnedPeerCertSha256"] = strings.Join(fingerprints, ",")
}
}
func firstXrayOutboundEndpoint(outbound map[string]interface{}) (string, string) {
protocol := strings.ToLower(getStringAny(outbound["protocol"]))
settings, _ := outbound["settings"].(map[string]interface{})
if settings == nil {
return "", ""
}
switch protocol {
case "trojan":
servers, _ := settings["servers"].([]interface{})
if len(servers) == 0 {
return "", ""
}
server, _ := servers[0].(map[string]interface{})
if server == nil {
return "", ""
}
return getStringAny(server["address"]), getPortString(server["port"])
case "vless", "vmess":
vnext, _ := settings["vnext"].([]interface{})
if len(vnext) == 0 {
return "", ""
}
server, _ := vnext[0].(map[string]interface{})
if server == nil {
return "", ""
}
return getStringAny(server["address"]), getPortString(server["port"])
}
return "", ""
}
func fetchTLSPeerCertPins(host string, port string, serverName string) []string {
cacheKey := strings.ToLower(strings.TrimSpace(host)) + ":" + strings.TrimSpace(port) + "|" + strings.ToLower(strings.TrimSpace(serverName))
if cached, ok := xrayTLSPinCache.Load(cacheKey); ok {
if pins, ok := cached.([]string); ok && len(pins) > 0 {
return append([]string(nil), pins...)
}
}
fingerprint, err := fetchTLSPeerCertPin(host, port, serverName)
if err != nil || fingerprint == "" {
return nil
}
fingerprints := []string{fingerprint}
if len(fingerprints) > 0 {
xrayTLSPinCache.Store(cacheKey, append([]string(nil), fingerprints...))
}
return fingerprints
}
func fetchTLSPeerCertPin(host string, port string, serverName string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 1200*time.Millisecond)
defer cancel()
dialer := &tls.Dialer{
NetDialer: &net.Dialer{Timeout: time.Second},
Config: &tls.Config{ServerName: serverName, InsecureSkipVerify: true},
}
conn, err := dialer.DialContext(ctx, "tcp", host+":"+port)
if err != nil {
return "", err
}
defer conn.Close()
tlsConn, ok := conn.(*tls.Conn)
if !ok {
return "", nil
}
state := tlsConn.ConnectionState()
if len(state.PeerCertificates) == 0 {
return "", nil
}
sum := sha256.Sum256(state.PeerCertificates[0].Raw)
return hex.EncodeToString(sum[:]), nil
}
func getPortString(value interface{}) string {
switch v := value.(type) {
case int:
return strconv.Itoa(v)
case int64:
return strconv.FormatInt(v, 10)
case float64:
return strconv.Itoa(int(v))
case string:
return strings.TrimSpace(v)
default:
return ""
}
}
func getStringAny(value interface{}) string {
s, _ := value.(string)
return strings.TrimSpace(s)
}
func truthy(value interface{}) bool {
switch v := value.(type) {
case bool:
return v
case string:
return strings.EqualFold(v, "true") || v == "1"
default:
return false
}
}
func removeXrayDeprecatedFields(value interface{}) {
switch item := value.(type) {
case map[string]interface{}:
delete(item, "allowInsecure")
for _, nested := range item {
removeXrayDeprecatedFields(nested)
}
case []interface{}:
for _, nested := range item {
removeXrayDeprecatedFields(nested)
}
}
}
func (m *XrayManager) resolveWorkdir(key string) string {
root := strings.TrimSpace(m.Config.Browser.UserDataRoot)
if root == "" {
root = "data"
}
if !filepath.IsAbs(root) {
root = apppath.Resolve(m.AppRoot, root)
}
return filepath.Join(root, "_xray", key)
}