init: 1.0.0

This commit is contained in:
Chang lue Tsen
2025-04-25 12:08:29 +09:00
commit 8addcc584b
1031 changed files with 76472 additions and 0 deletions
+201
View File
@@ -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
}
+100
View File
@@ -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
}
]
}
`
+76
View File
@@ -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
}
+17
View File
@@ -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"`
}
+130
View File
@@ -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
}
+15
View File
@@ -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)
}
+34
View File
@@ -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
}
+98
View File
@@ -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)
}
}
+80
View File
@@ -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))
}
+87
View File
@@ -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"`
}
+11
View File
@@ -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)
}
+39
View File
@@ -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
}
+40
View File
@@ -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
}
+114
View File
@@ -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
+44
View File
@@ -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
}
+43
View File
@@ -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
}