157 lines
3.4 KiB
Go
157 lines
3.4 KiB
Go
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
)
|
|
|
|
// GenerateSingBoxConfig generates a full JSON config for sing-box (v1.14+).
|
|
// sing-box acts as frontend: creates a TUN interface,
|
|
// captures system traffic and routes it to xray via SOCKS5.
|
|
func GenerateSingBoxConfig(serverAddress string, socksPort int, tunName string) (string, error) {
|
|
if socksPort == 0 {
|
|
socksPort = DefaultSocksPort
|
|
}
|
|
if tunName == "" {
|
|
tunName = "kettuTun"
|
|
}
|
|
|
|
config := map[string]any{
|
|
"log": buildSingBoxLog(),
|
|
"dns": buildSingBoxDns(),
|
|
"inbounds": []any{buildTunInbound(tunName)},
|
|
"outbounds": buildSingBoxOutbounds(socksPort),
|
|
"route": buildSingBoxRoute(serverAddress),
|
|
}
|
|
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal sing-box config: %w", err)
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func buildSingBoxLog() map[string]any {
|
|
return map[string]any{
|
|
"level": "info",
|
|
"timestamp": true,
|
|
}
|
|
}
|
|
|
|
func buildSingBoxDns() map[string]any {
|
|
return map[string]any{
|
|
"servers": []any{
|
|
map[string]any{
|
|
"type": "https",
|
|
"tag": "remote-dns",
|
|
"server": "1.1.1.1",
|
|
"server_port": 443,
|
|
"path": "/dns-query",
|
|
"detour": "proxy-out",
|
|
},
|
|
map[string]any{
|
|
"type": "https",
|
|
"tag": "local-dns",
|
|
"server": "8.8.8.8",
|
|
"server_port": 443,
|
|
"path": "/dns-query",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func buildTunInbound(tunName string) map[string]any {
|
|
tun := map[string]any{
|
|
"type": "tun",
|
|
"tag": "tun-in",
|
|
"address": []string{
|
|
"172.18.0.1/30",
|
|
"fdfe:dcba:9876::1/126",
|
|
},
|
|
"mtu": 1400,
|
|
"auto_route": true,
|
|
"strict_route": false,
|
|
"stack": "gvisor",
|
|
}
|
|
|
|
if tunName != "" && tunName != "kettuTun" {
|
|
tun["interface_name"] = tunName
|
|
}
|
|
|
|
return tun
|
|
}
|
|
|
|
func buildSingBoxOutbounds(socksPort int) []any {
|
|
return []any{
|
|
map[string]any{
|
|
"type": "socks",
|
|
"tag": "proxy-out",
|
|
"server": "127.0.0.1",
|
|
"server_port": socksPort,
|
|
"version": "5",
|
|
"udp_over_tcp": false,
|
|
},
|
|
map[string]any{
|
|
"type": "direct",
|
|
"tag": "direct-out",
|
|
},
|
|
}
|
|
}
|
|
|
|
func buildSingBoxRoute(serverAddress string) map[string]any {
|
|
rules := []any{
|
|
// Anti routing loop: bypass VPN core processes
|
|
map[string]any{
|
|
"process_name": []string{"xray.exe", "sing-box.exe", "kettuRay.exe"},
|
|
"action": "route",
|
|
"outbound": "direct-out",
|
|
},
|
|
}
|
|
|
|
// Anti routing loop: bypass remote VPN server
|
|
bypassRule := map[string]any{
|
|
"action": "route",
|
|
"outbound": "direct-out",
|
|
}
|
|
|
|
if ip := net.ParseIP(serverAddress); ip != nil {
|
|
prefix := "32"
|
|
if ip.To4() == nil {
|
|
prefix = "128"
|
|
}
|
|
bypassRule["ip_cidr"] = []string{serverAddress + "/" + prefix}
|
|
} else {
|
|
bypassRule["domain"] = []string{serverAddress}
|
|
}
|
|
rules = append(rules, bypassRule)
|
|
|
|
// Sniff rule
|
|
rules = append(rules, map[string]any{
|
|
"action": "sniff",
|
|
"timeout": "300ms",
|
|
})
|
|
|
|
// DNS hijack
|
|
rules = append(rules, map[string]any{
|
|
"protocol": "dns",
|
|
"action": "hijack-dns",
|
|
})
|
|
|
|
// Private IPs go direct
|
|
rules = append(rules, map[string]any{
|
|
"ip_is_private": true,
|
|
"action": "route",
|
|
"outbound": "direct-out",
|
|
})
|
|
|
|
return map[string]any{
|
|
"rules": rules,
|
|
"default_domain_resolver": map[string]any{
|
|
"server": "local-dns",
|
|
},
|
|
"final": "proxy-out",
|
|
"auto_detect_interface": true,
|
|
}
|
|
}
|