init: 1.0.0
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
)
|
||||
|
||||
func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) {
|
||||
// build outbounds type is Proxy
|
||||
var proxies []Proxy
|
||||
// build outbound group
|
||||
for _, group := range adapter.Group {
|
||||
if group.Type == proxy.GroupTypeSelect {
|
||||
selector := Proxy{
|
||||
Type: Selector,
|
||||
Tag: group.Name,
|
||||
SelectorOptions: &SelectorOutboundOptions{
|
||||
OutboundOptions: OutboundOptions{
|
||||
Tag: group.Name,
|
||||
Type: Selector,
|
||||
},
|
||||
Outbounds: group.Proxies,
|
||||
Default: group.Proxies[0],
|
||||
InterruptExistConnections: false,
|
||||
},
|
||||
}
|
||||
proxies = append(proxies, selector)
|
||||
} else if group.Type == proxy.GroupTypeURLTest {
|
||||
selector := Proxy{
|
||||
Type: URLTest,
|
||||
Tag: group.Name,
|
||||
URLTestOptions: &URLTestOutboundOptions{
|
||||
OutboundOptions: OutboundOptions{
|
||||
Tag: group.Name,
|
||||
Type: URLTest,
|
||||
},
|
||||
Outbounds: group.Proxies,
|
||||
URL: group.URL,
|
||||
},
|
||||
}
|
||||
proxies = append(proxies, selector)
|
||||
} else {
|
||||
logger.Errorf("[sing-box] Unknown group type: %s, group name: %s", group.Type, group.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// build outbounds
|
||||
for _, data := range adapter.Proxies {
|
||||
p := buildProxy(data, uuid)
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
proxies = append(proxies, *p)
|
||||
}
|
||||
|
||||
// add direct outbound
|
||||
direct := Proxy{
|
||||
Type: Direct,
|
||||
Tag: "DIRECT",
|
||||
}
|
||||
// add block outbound
|
||||
block := Proxy{
|
||||
Type: Block,
|
||||
Tag: "block",
|
||||
}
|
||||
// add dns outbound
|
||||
dns := Proxy{
|
||||
Type: DNS,
|
||||
Tag: "dns-out",
|
||||
}
|
||||
proxies = append(proxies, direct, block, dns)
|
||||
|
||||
var rawConfig map[string]any
|
||||
if err := json.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawConfig["outbounds"] = proxies
|
||||
route := RouteOptions{
|
||||
Final: "手动选择",
|
||||
Rules: []Rule{
|
||||
{
|
||||
Inbound: []string{
|
||||
"tun-in",
|
||||
"mixed-in",
|
||||
},
|
||||
Action: "sniff",
|
||||
},
|
||||
{
|
||||
Type: "logical",
|
||||
Mode: "or",
|
||||
Rules: []Rule{
|
||||
{
|
||||
Port: []uint16{53},
|
||||
},
|
||||
{
|
||||
Protocol: []string{"dns"},
|
||||
},
|
||||
},
|
||||
Action: "hijack-dns",
|
||||
},
|
||||
{
|
||||
RuleSet: []string{
|
||||
"geosite-category-ads-all",
|
||||
},
|
||||
ClashMode: "rule",
|
||||
Action: "reject",
|
||||
},
|
||||
{
|
||||
ClashMode: "direct",
|
||||
Outbound: "DIRECT",
|
||||
},
|
||||
{
|
||||
ClashMode: "global",
|
||||
Outbound: "手动选择",
|
||||
},
|
||||
{
|
||||
IPIsPrivate: true,
|
||||
Outbound: "DIRECT",
|
||||
},
|
||||
{
|
||||
RuleSet: []string{
|
||||
"geosite-private",
|
||||
},
|
||||
Outbound: "DIRECT",
|
||||
},
|
||||
},
|
||||
RuleSet: []RuleSet{
|
||||
{
|
||||
Tag: "geoip-cn",
|
||||
Type: "remote",
|
||||
Format: "binary",
|
||||
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs",
|
||||
DownloadDetour: "DIRECT",
|
||||
},
|
||||
{
|
||||
Tag: "geosite-cn",
|
||||
Type: "remote",
|
||||
Format: "binary",
|
||||
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs",
|
||||
DownloadDetour: "DIRECT",
|
||||
},
|
||||
{
|
||||
Tag: "geosite-private",
|
||||
Type: "remote",
|
||||
Format: "binary",
|
||||
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs",
|
||||
DownloadDetour: "DIRECT",
|
||||
},
|
||||
{
|
||||
Tag: "geosite-category-ads-all",
|
||||
Type: "remote",
|
||||
Format: "binary",
|
||||
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs",
|
||||
DownloadDetour: "DIRECT",
|
||||
},
|
||||
{
|
||||
Tag: "geosite-geolocation-!cn",
|
||||
Type: "remote",
|
||||
Format: "binary",
|
||||
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs",
|
||||
DownloadDetour: "DIRECT",
|
||||
},
|
||||
},
|
||||
AutoDetectInterface: true,
|
||||
}
|
||||
route.Rules = append(route.Rules, adapterToSingboxRule(adapter.Rules)...)
|
||||
rawConfig["route"] = route
|
||||
return json.Marshal(rawConfig)
|
||||
}
|
||||
|
||||
func buildProxy(data proxy.Proxy, uuid string) *Proxy {
|
||||
var p *Proxy
|
||||
var err error
|
||||
switch data.Protocol {
|
||||
case VLESS:
|
||||
p, err = ParseVless(data, uuid)
|
||||
case Shadowsocks:
|
||||
p, err = ParseShadowsocks(data, uuid)
|
||||
case Trojan:
|
||||
p, err = ParseTrojan(data, uuid)
|
||||
case VMess:
|
||||
p, err = ParseVMess(data, uuid)
|
||||
|
||||
case Hysteria2:
|
||||
p, err = ParseHysteria2(data, uuid)
|
||||
|
||||
case TUIC:
|
||||
p, err = ParseTUIC(data, uuid)
|
||||
|
||||
default:
|
||||
logger.Error("Unknown protocol", logger.Field("protocol", data.Protocol), logger.Field("server", data.Name))
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error("ParseVless", logger.Field("error", err.Error()), logger.Field("server", data.Name), logger.Field("protocol", data.Protocol))
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package singbox
|
||||
|
||||
const DefaultTemplate = `
|
||||
{
|
||||
"log": {
|
||||
"level": "info",
|
||||
"timestamp": true
|
||||
},
|
||||
"experimental": {
|
||||
"clash_api": {
|
||||
"external_controller": "127.0.0.1:9090",
|
||||
"external_ui": "ui",
|
||||
"secret": "",
|
||||
"external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
|
||||
"external_ui_download_detour": "direct",
|
||||
"default_mode": "rule"
|
||||
},
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"store_fakeip": false
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"tag": "dns_proxy",
|
||||
"address": "tls://8.8.8.8",
|
||||
"detour": "手动选择"
|
||||
},
|
||||
{
|
||||
"tag": "dns_direct",
|
||||
"address": "https://223.5.5.5/dns-query",
|
||||
"detour": "DIRECT"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"outbound": "any",
|
||||
"server": "dns_direct",
|
||||
"disable_cache": true
|
||||
},
|
||||
{
|
||||
"rule_set": "geosite-cn",
|
||||
"server": "dns_direct"
|
||||
},
|
||||
{
|
||||
"clash_mode": "direct",
|
||||
"server": "dns_direct"
|
||||
},
|
||||
{
|
||||
"clash_mode": "global",
|
||||
"server": "dns_proxy"
|
||||
},
|
||||
{
|
||||
"rule_set": "geosite-geolocation-!cn",
|
||||
"server": "dns_proxy"
|
||||
}
|
||||
],
|
||||
"final": "dns_direct",
|
||||
"strategy": "ipv4_only"
|
||||
},
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"action": "sniff"
|
||||
},
|
||||
{
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "tun-in",
|
||||
"type": "tun",
|
||||
"address": [
|
||||
"172.18.0.1/30",
|
||||
"fdfe:dcba:9876::1/126"
|
||||
],
|
||||
"auto_route": true,
|
||||
"strict_route": true,
|
||||
"stack": "system",
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": true,
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 7890
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "mixed-in",
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 7890
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,76 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
type Hysteria2Obfs struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type Hysteria2OutboundOptions struct {
|
||||
ServerOptions
|
||||
ServerPorts []string `json:"server_ports,omitempty"`
|
||||
HopInterval int `json:"hop_interval,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
}
|
||||
|
||||
func ParseHysteria2(data proxy.Proxy, password string) (*Proxy, error) {
|
||||
hysteria2 := data.Option.(proxy.Hysteria2)
|
||||
|
||||
p := &Proxy{
|
||||
Tag: data.Name,
|
||||
Type: Hysteria2,
|
||||
Hysteria2Options: &Hysteria2OutboundOptions{
|
||||
ServerOptions: ServerOptions{
|
||||
Tag: data.Name,
|
||||
Type: Hysteria2,
|
||||
Server: data.Server,
|
||||
},
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
|
||||
var ports []string
|
||||
|
||||
if hysteria2.HopPorts != "" {
|
||||
ps := strings.Split(hysteria2.HopPorts, ",")
|
||||
for _, port := range ps {
|
||||
// 舍弃单个端口,只保留端口范围
|
||||
if len(strings.Split(port, "-")) > 1 {
|
||||
tmp := strings.Split(port, "-")
|
||||
ports = append(ports, strings.Join(tmp, ":"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if len(ports) > 0 {
|
||||
p.Hysteria2Options.ServerPorts = ports
|
||||
p.Hysteria2Options.HopInterval = hysteria2.HopInterval
|
||||
} else {
|
||||
p.Hysteria2Options.ServerPort = data.Port
|
||||
}
|
||||
|
||||
if hysteria2.ObfsPassword != "" {
|
||||
p.Hysteria2Options.Obfs = &Hysteria2Obfs{
|
||||
Type: "salamander",
|
||||
Password: hysteria2.ObfsPassword,
|
||||
}
|
||||
}
|
||||
var tls *OutboundTLSOptions
|
||||
if hysteria2.SecurityConfig.SNI != "" {
|
||||
tls = NewOutboundTLSOptions("tls", hysteria2.SecurityConfig)
|
||||
}
|
||||
p.Hysteria2Options.TLS = tls
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package singbox
|
||||
|
||||
type OutboundMultiplexOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
MaxConnections int `json:"max_connections,omitempty"`
|
||||
MinStreams int `json:"min_streams,omitempty"`
|
||||
MaxStreams int `json:"max_streams,omitempty"`
|
||||
Padding bool `json:"padding,omitempty"`
|
||||
Brutal *BrutalOptions `json:"brutal,omitempty"`
|
||||
}
|
||||
|
||||
type BrutalOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/rules"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Outbound string `json:"outbound,omitempty"`
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
RuleSet []string `json:"rule_set,omitempty"`
|
||||
Domain []string `json:"domain,omitempty"`
|
||||
DomainSuffix []string `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword []string `json:"domain_keyword,omitempty"`
|
||||
DomainRegex []string `json:"domain_regex,omitempty"`
|
||||
GeoIP []string `json:"geoip,omitempty"`
|
||||
IPCIDR []string `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
SourceIPCIDR []string `json:"source_ip_cidr,omitempty"`
|
||||
ProcessName []string `json:"process_name,omitempty"`
|
||||
ProcessPath []string `json:"process_path,omitempty"`
|
||||
SourcePort []uint16 `json:"source_port,omitempty"`
|
||||
Protocol []string `json:"protocol,omitempty"`
|
||||
Port []uint16 `json:"port,omitempty"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Inbound []string `json:"inbound,omitempty"`
|
||||
Rules []Rule `json:"rules,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
}
|
||||
|
||||
type RuleSet struct {
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
}
|
||||
|
||||
func adapterToSingboxRule(texts []string) []Rule {
|
||||
var rulesList []Rule
|
||||
for _, rule := range texts {
|
||||
r := rules.NewRule(rule, "")
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
rulesList = addRuleToItem(rulesList, r.Target, *r)
|
||||
}
|
||||
return rulesList
|
||||
}
|
||||
|
||||
func addRuleToItem(group []Rule, outbound string, rule rules.Rule) []Rule {
|
||||
for i := range group {
|
||||
if group[i].Outbound == outbound {
|
||||
switch rules.ParseRuleType(rule.Type) {
|
||||
case rules.Domain:
|
||||
group[i].Domain = append(group[i].Domain, rule.Payload)
|
||||
return group
|
||||
case rules.DomainSuffix:
|
||||
group[i].DomainSuffix = append(group[i].DomainSuffix, rule.Payload)
|
||||
return group
|
||||
case rules.DomainKeyword:
|
||||
group[i].DomainKeyword = append(group[i].DomainKeyword, rule.Payload)
|
||||
return group
|
||||
case rules.IPCIDR:
|
||||
group[i].IPCIDR = append(group[i].IPCIDR, rule.Payload)
|
||||
return group
|
||||
case rules.SrcIPCIDR:
|
||||
group[i].SourceIPCIDR = append(group[i].SourceIPCIDR, rule.Payload)
|
||||
return group
|
||||
case rules.SrcPort:
|
||||
port, err := strconv.ParseUint(rule.Payload, 10, 16)
|
||||
if err != nil {
|
||||
logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload)
|
||||
return group
|
||||
}
|
||||
group[i].SourcePort = append(group[i].SourcePort, uint16(port))
|
||||
return group
|
||||
case rules.GEOIP:
|
||||
group[i].GeoIP = append(group[i].GeoIP, rule.Payload)
|
||||
return group
|
||||
case rules.Process:
|
||||
group[i].ProcessName = append(group[i].ProcessName, rule.Payload)
|
||||
return group
|
||||
case rules.ProcessPath:
|
||||
group[i].ProcessPath = append(group[i].ProcessPath, rule.Payload)
|
||||
return group
|
||||
default:
|
||||
logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type)
|
||||
return group
|
||||
}
|
||||
}
|
||||
}
|
||||
newRule := Rule{
|
||||
Outbound: outbound,
|
||||
}
|
||||
|
||||
switch rules.ParseRuleType(rule.Type) {
|
||||
case rules.Domain:
|
||||
newRule.Domain = []string{rule.Payload}
|
||||
case rules.DomainSuffix:
|
||||
newRule.DomainSuffix = []string{rule.Payload}
|
||||
case rules.DomainKeyword:
|
||||
newRule.DomainKeyword = []string{rule.Payload}
|
||||
case rules.IPCIDR:
|
||||
newRule.IPCIDR = []string{rule.Payload}
|
||||
case rules.SrcIPCIDR:
|
||||
newRule.SourceIPCIDR = []string{rule.Payload}
|
||||
case rules.SrcPort:
|
||||
port, err := strconv.ParseUint(rule.Payload, 10, 16)
|
||||
if err != nil {
|
||||
logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload)
|
||||
return group
|
||||
}
|
||||
newRule.SourcePort = []uint16{uint16(port)}
|
||||
case rules.GEOIP:
|
||||
newRule.GeoIP = []string{rule.Payload}
|
||||
case rules.Process:
|
||||
newRule.ProcessName = []string{rule.Payload}
|
||||
case rules.ProcessPath:
|
||||
newRule.ProcessPath = []string{rule.Payload}
|
||||
default:
|
||||
logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type)
|
||||
return group
|
||||
}
|
||||
group = append(group, newRule)
|
||||
return group
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAdapterToSingboxRule(t *testing.T) {
|
||||
rules := []string{
|
||||
"DOMAIN,example.com,DIRECT",
|
||||
"DOMAIN-SUFFIX,google.com,智能线路",
|
||||
}
|
||||
result := adapterToSingboxRule(rules)
|
||||
fmt.Printf("TestAdapterToSingboxRule: result: %+v\n", result)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
type ShadowsocksOptions struct {
|
||||
ServerOptions
|
||||
Method string `json:"method,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Plugin string `json:"plugin,omitempty"`
|
||||
PluginOptions string `json:"plugin_opts,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
}
|
||||
|
||||
func ParseShadowsocks(data proxy.Proxy, uuid string) (*Proxy, error) {
|
||||
config := data.Option.(proxy.Shadowsocks)
|
||||
p := &Proxy{
|
||||
Tag: data.Name,
|
||||
Type: Shadowsocks,
|
||||
ShadowsocksOptions: &ShadowsocksOptions{
|
||||
ServerOptions: ServerOptions{
|
||||
Tag: data.Name,
|
||||
Type: Shadowsocks,
|
||||
Server: data.Server,
|
||||
ServerPort: data.Port,
|
||||
},
|
||||
Method: config.Method,
|
||||
Password: uuid,
|
||||
Network: "tcp",
|
||||
},
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
Trojan = "trojan"
|
||||
VLESS = "vless"
|
||||
VMess = "vmess"
|
||||
TUIC = "tuic"
|
||||
Hysteria2 = "hysteria2"
|
||||
Shadowsocks = "shadowsocks"
|
||||
Selector = "selector"
|
||||
URLTest = "urltest"
|
||||
Direct = "direct"
|
||||
Block = "block"
|
||||
DNS = "dns"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Type string `json:"type"`
|
||||
ShadowsocksOptions *ShadowsocksOptions `json:"-"`
|
||||
TUICOptions *TUICOutboundOptions `json:"-"`
|
||||
TrojanOptions *TrojanOutboundOptions `json:"-"`
|
||||
VLESSOptions *VLESSOutboundOptions `json:"-"`
|
||||
VMessOptions *VMessOutboundOptions `json:"-"`
|
||||
Hysteria2Options *Hysteria2OutboundOptions `json:"-"`
|
||||
SelectorOptions *SelectorOutboundOptions `json:"-"`
|
||||
URLTestOptions *URLTestOutboundOptions `json:"-"`
|
||||
}
|
||||
|
||||
type ServerOptions struct {
|
||||
Tag string `json:"tag"`
|
||||
Type string `json:"type"`
|
||||
Server string `json:"server"`
|
||||
ServerPort int `json:"server_port,omitempty"`
|
||||
}
|
||||
type OutboundOptions struct {
|
||||
Tag string `json:"tag"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
type SelectorOutboundOptions struct {
|
||||
OutboundOptions
|
||||
Outbounds []string `json:"outbounds"`
|
||||
Default string `json:"default,omitempty"`
|
||||
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
|
||||
}
|
||||
|
||||
type URLTestOutboundOptions struct {
|
||||
OutboundOptions
|
||||
Outbounds []string `json:"outbounds"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Interval Duration `json:"interval,omitempty"`
|
||||
Tolerance uint16 `json:"tolerance,omitempty"`
|
||||
IdleTimeout Duration `json:"idle_timeout,omitempty"`
|
||||
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
|
||||
}
|
||||
|
||||
type RouteOptions struct {
|
||||
Rules []Rule `json:"rules,omitempty"`
|
||||
Final string `json:"final,omitempty"`
|
||||
RuleSet []RuleSet `json:"rule_set,omitempty"`
|
||||
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
|
||||
}
|
||||
|
||||
func (p Proxy) MarshalJSON() ([]byte, error) {
|
||||
type Alias Proxy
|
||||
aux := struct {
|
||||
Alias
|
||||
}{
|
||||
Alias: (Alias)(p),
|
||||
}
|
||||
switch p.Type {
|
||||
case Shadowsocks:
|
||||
return json.Marshal(p.ShadowsocksOptions)
|
||||
case TUIC:
|
||||
return json.Marshal(p.TUICOptions)
|
||||
case Trojan:
|
||||
return json.Marshal(p.TrojanOptions)
|
||||
case VLESS:
|
||||
return json.Marshal(p.VLESSOptions)
|
||||
case VMess:
|
||||
return json.Marshal(p.VMessOptions)
|
||||
case Hysteria2:
|
||||
return json.Marshal(p.Hysteria2Options)
|
||||
case Selector:
|
||||
return json.Marshal(p.SelectorOptions)
|
||||
case URLTest:
|
||||
return json.Marshal(p.URLTestOptions)
|
||||
case Direct, Block, DNS:
|
||||
return json.Marshal(aux.Alias)
|
||||
default:
|
||||
return nil, fmt.Errorf("[sing-box] MarshalJSON unknown type: %s", p.Type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createSS() proxy.Proxy {
|
||||
c := proxy.Shadowsocks{
|
||||
Method: "aes-256-gcm",
|
||||
Port: 10301,
|
||||
ServerKey: "",
|
||||
}
|
||||
return proxy.Proxy{
|
||||
Name: "Shadowsocks",
|
||||
Server: "127.0.0.1",
|
||||
Port: 10301,
|
||||
Protocol: "shadowsocks",
|
||||
Option: c,
|
||||
}
|
||||
}
|
||||
|
||||
func createVLESS() proxy.Proxy {
|
||||
c := proxy.Vless{
|
||||
Port: 10301,
|
||||
Flow: "xtls-rprx-direct",
|
||||
Transport: "websocket",
|
||||
TransportConfig: proxy.TransportConfig{
|
||||
Path: "/ws",
|
||||
Host: "baidu.com",
|
||||
},
|
||||
Security: "tls",
|
||||
SecurityConfig: proxy.SecurityConfig{
|
||||
SNI: "baidu.com",
|
||||
Fingerprint: "chrome",
|
||||
AllowInsecure: true,
|
||||
},
|
||||
}
|
||||
s := proxy.Proxy{
|
||||
Name: "VLESS",
|
||||
Server: "test.xxx.com",
|
||||
Port: 10301,
|
||||
Protocol: "vless",
|
||||
Option: c,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func TestSingboxShadowsocks(t *testing.T) {
|
||||
s := createSS()
|
||||
p, err := ParseShadowsocks(s, "uuid")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := p.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NotEqual(t, 0, len(data))
|
||||
|
||||
// Output:
|
||||
// proxy: proxy: {"tag":"Shadowsocks","type":"shadowsocks","server":"127.0.0.1","server_port":10301,"method":"aes-256-gcm","password":"uuid","network":"tcp"}
|
||||
|
||||
}
|
||||
|
||||
func TestSingboxVless(t *testing.T) {
|
||||
s := createVLESS()
|
||||
p, err := ParseVless(s, "uuid")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := p.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NotEqual(t, 0, len(data))
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
type OutboundTLSOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
DisableSNI bool `json:"disable_sni,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
Insecure bool `json:"insecure,omitempty"`
|
||||
ALPN Listable[string] `json:"alpn,omitempty"`
|
||||
MinVersion string `json:"min_version,omitempty"`
|
||||
MaxVersion string `json:"max_version,omitempty"`
|
||||
CipherSuites Listable[string] `json:"cipher_suites,omitempty"`
|
||||
Certificate Listable[string] `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
||||
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
||||
Reality *OutboundRealityOptions `json:"reality,omitempty"`
|
||||
}
|
||||
|
||||
func NewOutboundTLSOptions(security string, cfg proxy.SecurityConfig) *OutboundTLSOptions {
|
||||
var tls = &OutboundTLSOptions{}
|
||||
switch security {
|
||||
case "none":
|
||||
return nil
|
||||
case "tls":
|
||||
tls.Enabled = true
|
||||
if cfg.SNI != "" {
|
||||
tls.ServerName = cfg.SNI
|
||||
} else {
|
||||
tls.DisableSNI = true
|
||||
}
|
||||
tls.Insecure = cfg.AllowInsecure
|
||||
if cfg.Fingerprint != "" {
|
||||
tls.UTLS = &OutboundUTLSOptions{
|
||||
Enabled: true,
|
||||
Fingerprint: cfg.Fingerprint,
|
||||
}
|
||||
}
|
||||
case "reality":
|
||||
tls.Enabled = true
|
||||
if cfg.SNI != "" {
|
||||
tls.ServerName = cfg.SNI
|
||||
} else {
|
||||
tls.DisableSNI = true
|
||||
}
|
||||
tls.Insecure = cfg.AllowInsecure
|
||||
if cfg.Fingerprint != "" {
|
||||
tls.UTLS = &OutboundUTLSOptions{
|
||||
Enabled: true,
|
||||
Fingerprint: cfg.Fingerprint,
|
||||
}
|
||||
}
|
||||
tls.Reality = &OutboundRealityOptions{
|
||||
Enabled: true,
|
||||
PublicKey: cfg.RealityPublicKey,
|
||||
ShortID: cfg.RealityShortId,
|
||||
}
|
||||
}
|
||||
return tls
|
||||
}
|
||||
|
||||
type OutboundECHOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"`
|
||||
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"`
|
||||
Config Listable[string] `json:"config,omitempty"`
|
||||
ConfigPath string `json:"config_path,omitempty"`
|
||||
}
|
||||
|
||||
type OutboundRealityOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
ShortID string `json:"short_id,omitempty"`
|
||||
}
|
||||
|
||||
type OutboundUTLSOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
}
|
||||
type Listable[T any] []T
|
||||
|
||||
type OutboundTLSOptionsContainer struct {
|
||||
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package singbox
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func mergeOptions(target map[string]any, options any) error {
|
||||
optionsJSON, err := json.Marshal(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(optionsJSON, &target)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
type TrojanOutboundOptions struct {
|
||||
ServerOptions
|
||||
Password string `json:"password"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
}
|
||||
|
||||
func ParseTrojan(data proxy.Proxy, uuid string) (*Proxy, error) {
|
||||
trojan := data.Option.(proxy.Trojan)
|
||||
p := &Proxy{
|
||||
Tag: data.Name,
|
||||
Type: Trojan,
|
||||
TrojanOptions: &TrojanOutboundOptions{
|
||||
ServerOptions: ServerOptions{
|
||||
Tag: data.Name,
|
||||
Type: Trojan,
|
||||
Server: data.Server,
|
||||
ServerPort: data.Port,
|
||||
},
|
||||
Password: uuid,
|
||||
},
|
||||
}
|
||||
// Transport options
|
||||
transport := NewV2RayTransportOptions(trojan.Transport, trojan.TransportConfig)
|
||||
|
||||
p.TrojanOptions.Transport = transport
|
||||
// Security options
|
||||
p.TrojanOptions.TLS = NewOutboundTLSOptions(trojan.Security, trojan.SecurityConfig)
|
||||
return p, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
type TUICOutboundOptions struct {
|
||||
ServerOptions
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
CongestionControl string `json:"congestion_control,omitempty"`
|
||||
UDPRelayMode string `json:"udp_relay_mode,omitempty"`
|
||||
UDPOverStream bool `json:"udp_over_stream,omitempty"`
|
||||
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
|
||||
Heartbeat string `json:"heartbeat,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
func ParseTUIC(data proxy.Proxy, uuid string) (*Proxy, error) {
|
||||
tuic := data.Option.(proxy.Tuic)
|
||||
p := &Proxy{
|
||||
Tag: data.Name,
|
||||
Type: TUIC,
|
||||
TUICOptions: &TUICOutboundOptions{
|
||||
ServerOptions: ServerOptions{
|
||||
Tag: data.Name,
|
||||
Type: TUIC,
|
||||
Server: data.Server,
|
||||
ServerPort: data.Port,
|
||||
},
|
||||
UUID: uuid,
|
||||
Password: uuid,
|
||||
CongestionControl: "bbr",
|
||||
},
|
||||
}
|
||||
// Security options
|
||||
p.TUICOptions.TLS = NewOutboundTLSOptions("tls", tuic.SecurityConfig)
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
type V2RayTransportOptions struct {
|
||||
Type string `json:"type"`
|
||||
HTTPOptions V2RayHTTPOptions `json:"-"`
|
||||
WebsocketOptions V2RayWebsocketOptions `json:"-"`
|
||||
QUICOptions V2RayQUICOptions `json:"-"`
|
||||
GRPCOptions V2RayGRPCOptions `json:"-"`
|
||||
HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"`
|
||||
}
|
||||
|
||||
func (v V2RayTransportOptions) MarshalJSON() ([]byte, error) {
|
||||
var v2rayTransportOptions any
|
||||
data := map[string]any{
|
||||
"type": v.Type,
|
||||
}
|
||||
switch v.Type {
|
||||
case "http":
|
||||
v2rayTransportOptions = v.HTTPOptions
|
||||
case "ws":
|
||||
v2rayTransportOptions = v.WebsocketOptions
|
||||
case "quic":
|
||||
v2rayTransportOptions = v.QUICOptions
|
||||
case "grpc":
|
||||
v2rayTransportOptions = v.GRPCOptions
|
||||
case "httpupgrade":
|
||||
v2rayTransportOptions = v.HTTPUpgradeOptions
|
||||
}
|
||||
if err := mergeOptions(data, v2rayTransportOptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func NewV2RayTransportOptions(network string, transport proxy.TransportConfig) *V2RayTransportOptions {
|
||||
var t *V2RayTransportOptions = nil
|
||||
switch network {
|
||||
case "websocket":
|
||||
t = &V2RayTransportOptions{
|
||||
Type: "ws",
|
||||
WebsocketOptions: V2RayWebsocketOptions{
|
||||
Path: transport.Path,
|
||||
Headers: map[string]Listable[string]{
|
||||
"Host": []string{transport.Host},
|
||||
},
|
||||
MaxEarlyData: 2048,
|
||||
EarlyDataHeaderName: "Sec-WebSocket-Protocol",
|
||||
},
|
||||
}
|
||||
case "httpupgrade":
|
||||
t = &V2RayTransportOptions{
|
||||
Type: "httpupgrade",
|
||||
HTTPOptions: V2RayHTTPOptions{
|
||||
Path: transport.Path,
|
||||
Host: []string{transport.Host},
|
||||
Headers: map[string]Listable[string]{
|
||||
"Host": []string{transport.Host},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
case "grpc":
|
||||
t = &V2RayTransportOptions{
|
||||
Type: "grpc",
|
||||
GRPCOptions: V2RayGRPCOptions{
|
||||
ServiceName: transport.ServiceName,
|
||||
},
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
type V2RayHTTPOptions struct {
|
||||
Host Listable[string] `json:"host,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Headers HTTPHeader `json:"headers,omitempty"`
|
||||
IdleTimeout Duration `json:"idle_timeout,omitempty"`
|
||||
PingTimeout Duration `json:"ping_timeout,omitempty"`
|
||||
}
|
||||
|
||||
type V2RayWebsocketOptions struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
Headers HTTPHeader `json:"headers,omitempty"`
|
||||
MaxEarlyData uint32 `json:"max_early_data,omitempty"`
|
||||
EarlyDataHeaderName string `json:"early_data_header_name,omitempty"`
|
||||
}
|
||||
|
||||
type V2RayQUICOptions struct{}
|
||||
|
||||
type V2RayGRPCOptions struct {
|
||||
ServiceName string `json:"service_name,omitempty"`
|
||||
IdleTimeout string `json:"idle_timeout,omitempty"`
|
||||
PingTimeout string `json:"ping_timeout,omitempty"`
|
||||
PermitWithoutStream bool `json:"permit_without_stream,omitempty"`
|
||||
ForceLite bool `json:"-"` // for test
|
||||
}
|
||||
|
||||
type V2RayHTTPUpgradeOptions struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Headers HTTPHeader `json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPHeader map[string]Listable[string]
|
||||
|
||||
type Duration time.Duration
|
||||
@@ -0,0 +1,44 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
type VLESSOutboundOptions struct {
|
||||
ServerOptions
|
||||
OutboundTLSOptionsContainer
|
||||
UUID string `json:"uuid"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
PacketEncoding *string `json:"packet_encoding,omitempty"`
|
||||
}
|
||||
|
||||
func ParseVless(data proxy.Proxy, uuid string) (*Proxy, error) {
|
||||
vless := data.Option.(proxy.Vless)
|
||||
packetEncoding := "xudp"
|
||||
p := &Proxy{
|
||||
Tag: data.Name,
|
||||
Type: VLESS,
|
||||
VLESSOptions: &VLESSOutboundOptions{
|
||||
ServerOptions: ServerOptions{
|
||||
Tag: data.Name,
|
||||
Type: VLESS,
|
||||
Server: data.Server,
|
||||
ServerPort: data.Port,
|
||||
},
|
||||
UUID: uuid,
|
||||
Flow: vless.Flow,
|
||||
PacketEncoding: &packetEncoding,
|
||||
},
|
||||
}
|
||||
// Transport options
|
||||
transport := NewV2RayTransportOptions(vless.Transport, vless.TransportConfig)
|
||||
p.VLESSOptions.Transport = transport
|
||||
|
||||
// Security options
|
||||
p.VLESSOptions.TLS = NewOutboundTLSOptions(vless.Security, vless.SecurityConfig)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package singbox
|
||||
|
||||
import (
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
type VMessOutboundOptions struct {
|
||||
ServerOptions
|
||||
UUID string `json:"uuid"`
|
||||
Security string `json:"security"`
|
||||
AlterId int `json:"alter_id,omitempty"`
|
||||
GlobalPadding bool `json:"global_padding,omitempty"`
|
||||
AuthenticatedLength bool `json:"authenticated_length,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
PacketEncoding string `json:"packet_encoding,omitempty"`
|
||||
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
|
||||
Transport *V2RayTransportOptions `json:"transport,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
func ParseVMess(data proxy.Proxy, uuid string) (*Proxy, error) {
|
||||
vmess := data.Option.(proxy.Vmess)
|
||||
p := &Proxy{
|
||||
Type: VMess,
|
||||
VMessOptions: &VMessOutboundOptions{
|
||||
ServerOptions: ServerOptions{
|
||||
Tag: data.Name,
|
||||
Type: VMess,
|
||||
Server: data.Server,
|
||||
ServerPort: data.Port,
|
||||
},
|
||||
UUID: uuid,
|
||||
Security: "auto",
|
||||
AlterId: 0,
|
||||
},
|
||||
}
|
||||
// Transport options
|
||||
p.VMessOptions.Transport = NewV2RayTransportOptions(vmess.Transport, vmess.TransportConfig)
|
||||
// Security options
|
||||
p.VMessOptions.TLS = NewOutboundTLSOptions(vmess.Security, vmess.SecurityConfig)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
Reference in New Issue
Block a user