refactor(adapter): delete old adapter

This commit is contained in:
Chang lue Tsen 2025-09-29 11:22:36 -04:00
parent 766e210f06
commit 3fb23e3106
66 changed files with 0 additions and 4509 deletions

View File

@ -1,102 +0,0 @@
package adapter
import (
"embed"
"github.com/perfect-panel/server/internal/model/server"
"github.com/perfect-panel/server/pkg/adapter/clash"
"github.com/perfect-panel/server/pkg/adapter/general"
"github.com/perfect-panel/server/pkg/adapter/loon"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/adapter/quantumultx"
"github.com/perfect-panel/server/pkg/adapter/shadowrocket"
"github.com/perfect-panel/server/pkg/adapter/singbox"
"github.com/perfect-panel/server/pkg/adapter/surfboard"
"github.com/perfect-panel/server/pkg/adapter/surge"
"github.com/perfect-panel/server/pkg/adapter/v2rayn"
)
//go:embed template/*
var TemplateFS embed.FS
var (
AutoSelect = "Auto - UrlTest"
)
type Config struct {
Nodes []*server.Server
Rules []*server.RuleGroup
Tags map[string][]*server.Server
}
type Adapter struct {
proxy.Adapter
}
func NewAdapter(cfg *Config) *Adapter {
// 转换服务器列表
proxies, nodes, tags := adapterProxies(cfg.Nodes)
// 转换规则组
g, r, d := adapterRules(cfg.Rules)
if d == "" {
d = AutoSelect
}
// 生成默认代理组
proxyGroup := append(generateDefaultGroup(), g...)
// 合并代理组
proxyGroup = SortGroups(proxyGroup, nodes, tags, d)
return &Adapter{
Adapter: proxy.Adapter{
Proxies: proxies,
Group: proxyGroup,
Rules: r,
Nodes: nodes,
Default: d,
TemplateFS: &TemplateFS,
},
}
}
// BuildClash generates a Clash configuration for the given UUID.
func (m *Adapter) BuildClash(uuid string) ([]byte, error) {
client := clash.NewClash(m.Adapter)
return client.Build(uuid)
}
// BuildGeneral generates a general configuration for the given UUID.
func (m *Adapter) BuildGeneral(uuid string) []byte {
return general.GenerateBase64General(m.Proxies, uuid)
}
// BuildLoon generates a Loon configuration for the given UUID.
func (m *Adapter) BuildLoon(uuid string) []byte {
return loon.BuildLoon(m.Proxies, uuid)
}
// BuildQuantumultX generates a Quantumult X configuration for the given UUID.
func (m *Adapter) BuildQuantumultX(uuid string) string {
return quantumultx.BuildQuantumultX(m.Proxies, uuid)
}
// BuildSingbox generates a Singbox configuration for the given UUID.
func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) {
return singbox.BuildSingbox(m.Adapter, uuid)
}
func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte {
return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo)
}
// BuildSurfboard generates a Surfboard configuration for the given site name and user info.
func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte {
return surfboard.BuildSurfboard(m.Adapter, siteName, user)
}
// BuildV2rayN generates a V2rayN configuration for the given UUID.
func (m *Adapter) BuildV2rayN(uuid string) []byte {
return v2rayn.NewV2rayN(m.Adapter).Build(uuid)
}
// BuildSurge generates a Surge configuration for the given UUID and site name.
func (m *Adapter) BuildSurge(siteName string, user surge.UserInfo) []byte {
return surge.NewSurge(m.Adapter).Build(siteName, user)
}

View File

@ -1,138 +0,0 @@
package adapter
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/perfect-panel/server/internal/model/server"
"github.com/perfect-panel/server/pkg/adapter/surfboard"
)
func createTestServer() []*server.Server {
c := server.Shadowsocks{
Method: "aes-256-gcm",
Port: 10301,
ServerKey: "",
}
data, _ := json.Marshal(c)
relays := creatRelayNode()
relay, _ := json.Marshal(relays)
enable := true
// 创建一个测试用的服务器列表
return []*server.Server{
{
Id: 1,
Name: "Test Server 1",
Tags: "",
Country: "CN",
City: "",
Latitude: "",
Longitude: "",
ServerAddr: "test1.example.com",
RelayMode: "random",
RelayNode: string(relay),
SpeedLimit: 0,
TrafficRatio: 0,
GroupId: 0,
Protocol: "shadowsocks",
Config: string(data),
Enable: &enable,
Sort: 0,
},
}
}
func creatRelayNode() []*server.NodeRelay {
var nodes []*server.NodeRelay
for i := 0; i < 10; i++ {
port := 10301 + i
c := server.NodeRelay{
Host: fmt.Sprintf("192.168.1.%d", i),
Port: port,
Prefix: fmt.Sprintf("relay-%d", i),
}
nodes = append(nodes, &c)
}
return nodes
}
func TestNewAdapter(t *testing.T) {
nodes := createTestServer()
rules := []*server.RuleGroup{
{
Name: "Test Rule Group 1",
Tags: "",
Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1",
},
}
adapter := NewAdapter(nodes, rules)
bytes, err := adapter.BuildClash("some-uuid")
if err != nil {
t.Errorf("Failed to build adapter: %v", err)
return
}
t.Logf("Adapter built successfully: %s", string(bytes))
}
func TestAdapter_BuildSingbox(t *testing.T) {
nodes := createTestServer()
rules := []*server.RuleGroup{
{
Name: "Test Rule Group 1",
Tags: "",
Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1",
},
}
adapter := NewAdapter(nodes, rules)
bytes, err := adapter.BuildSingbox("some-uuid")
if err != nil {
t.Errorf("Failed to build adapter: %v", err)
return
}
var pretty map[string]interface{}
_ = json.Unmarshal(bytes, &pretty)
if pretty == nil {
t.Errorf("Failed to parse Singbox config")
return
}
prettyStr, err := json.MarshalIndent(pretty, "", " ")
if err != nil {
t.Errorf("Failed to format Singbox config: %v", err)
return
}
t.Logf("Adapter built successfully: \n %s", string(prettyStr))
}
func TestAdapter_BuildSurfboard(t *testing.T) {
nodes := createTestServer()
rules := []*server.RuleGroup{
{
Name: "Test Rule Group 1",
Tags: "",
Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1",
},
}
adapter := NewAdapter(nodes, rules)
user := surfboard.UserInfo{
UUID: "some-uuid",
Upload: 200,
Download: 13012,
TotalTraffic: 1024000,
ExpiredDate: time.Now().Add(24 * time.Hour),
SubscribeURL: "",
}
bytes := adapter.BuildSurfboard("test-site", user)
if bytes == nil {
t.Errorf("Failed to build adapter")
return
}
t.Logf("Adapter built successfully: %s", string(bytes))
}

View File

@ -1,95 +0,0 @@
package clash
import (
"bytes"
"fmt"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/logger"
"gopkg.in/yaml.v3"
)
type Clash struct {
proxy.Adapter
}
func NewClash(adapter proxy.Adapter) *Clash {
return &Clash{
Adapter: adapter,
}
}
func (c *Clash) Build(uuid string) ([]byte, error) {
var proxies []Proxy
for _, proxied := range c.Adapter.Proxies {
p, err := c.parseProxy(proxied, uuid)
if err != nil {
logger.Errorw("Failed to parse proxy", logger.Field("error", err), logger.Field("proxy", p.Name))
continue
}
proxies = append(proxies, *p)
}
var groups []ProxyGroup
for _, group := range c.Adapter.Group {
groups = append(groups, ProxyGroup{
Name: group.Name,
Type: string(group.Type),
Proxies: group.Proxies,
Url: group.URL,
Interval: group.Interval,
})
}
var rules = append(c.Rules, fmt.Sprintf("MATCH,%s", c.Default))
tmplBytes, err := c.TemplateFS.ReadFile("template/clash.tpl")
if err != nil {
logger.Errorw("Failed to read template file", logger.Field("error", err))
return nil, fmt.Errorf("failed to read template file: %w", err)
}
tpl, err := template.New("clash.yaml").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{
"toYaml": func(v interface{}) string {
out, err := yaml.Marshal(v)
if err != nil {
return fmt.Sprintf("# YAML encode error: %v", err.Error())
}
return string(out)
},
}).Parse(string(tmplBytes))
if err != nil {
logger.Errorw("[Clash] Failed to parse template", logger.Field("error", err))
return nil, fmt.Errorf("failed to parse template: %w", err)
}
var buf bytes.Buffer
err = tpl.Execute(&buf, map[string]interface{}{
"Proxies": proxies,
"ProxyGroups": groups,
"Rules": rules,
})
if err != nil {
logger.Errorw("[Clash] Failed to execute template", logger.Field("error", err))
return nil, fmt.Errorf("failed to execute template: %w", err)
}
return buf.Bytes(), nil
}
func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) {
parseFuncs := map[string]func(proxy.Proxy, string) (*Proxy, error){
"shadowsocks": parseShadowsocks,
"trojan": parseTrojan,
"vless": parseVless,
"vmess": parseVmess,
"hysteria2": parseHysteria2,
"tuic": parseTuic,
"anytls": parseAnyTLS,
}
if parseFunc, exists := parseFuncs[p.Protocol]; exists {
return parseFunc(p, uuid)
}
logger.Errorw("Unknown protocol", logger.Field("protocol", p.Protocol), logger.Field("server", p.Name))
return nil, fmt.Errorf("unknown protocol: %s", p.Protocol)
}

View File

@ -1,41 +0,0 @@
package clash
import (
"testing"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/stretchr/testify/assert"
)
func TestClash_Build(t *testing.T) {
adapter := proxy.Adapter{
Proxies: []proxy.Proxy{
{
Name: "test-proxy",
Protocol: "shadowsocks",
Server: "1.2.3.4",
Port: 8388,
Option: proxy.Shadowsocks{
Method: "aes-256-gcm",
},
},
},
Group: []proxy.Group{
{
Name: "test-group",
Type: "select",
Proxies: []string{"test-proxy"},
},
},
Rules: []string{
"DOMAIN-SUFFIX,example.com,DIRECT",
"GEOIP,CN,DIRECT",
"MATCH,DIRECT",
},
}
clash := NewClash(adapter)
result, err := clash.Build("test-uuid")
assert.NoError(t, err)
assert.NotNil(t, result)
}

View File

@ -1,54 +0,0 @@
package clash
const DefaultTemplate = `
mode: rule
ipv6: true
allow-lan: true
bind-address: "*"
mixed-port: 7890
log-level: error
unified-delay: true
tcp-concurrent: true
external-controller: 0.0.0.0:9090
tun:
enable: true
stack: system
auto-route: true
dns:
enable: true
cache-algorithm: arc
listen: 0.0.0.0:1053
ipv6: true
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- "*.lan"
- "lens.l.google.com"
- "*.srv.nintendo.net"
- "*.stun.playstation.net"
- "xbox.*.*.microsoft.com"
- "*.xboxlive.com"
- "*.msftncsi.com"
- "*.msftconnecttest.com"
default-nameserver:
- 119.29.29.29
- 223.5.5.5
nameserver:
- system
- 119.29.29.29
- 223.5.5.5
fallback:
- 8.8.8.8
- 1.1.1.1
fallback-filter:
geoip: true
geoip-code: CN
proxies:
proxy-groups:
rules:
`

View File

@ -1,131 +0,0 @@
package clash
type RawConfig struct {
Port int `yaml:"port" json:"port"`
SocksPort int `yaml:"socks-port" json:"socks-port"`
RedirPort int `yaml:"redir-port" json:"redir-port"`
TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"`
MixedPort int `yaml:"mixed-port" json:"mixed-port"`
AllowLan bool `yaml:"allow-lan" json:"allow-lan"`
Mode string `yaml:"mode" json:"mode"`
LogLevel string `yaml:"log-level" json:"log-level"`
ExternalController string `yaml:"external-controller" json:"external-controller"`
Secret string `yaml:"secret" json:"secret"`
Proxies []Proxy `yaml:"proxies" json:"proxies"`
ProxyGroups []ProxyGroup `yaml:"proxy-groups" json:"proxy-groups"`
Rules []string `yaml:"rules" json:"rule"`
}
type Proxy struct {
// 基础数据
Name string `yaml:"name"`
Type string `yaml:"type"`
Server string `yaml:"server"`
Port int `yaml:"port,omitempty"`
// Shadowsocks
Password string `yaml:"password,omitempty"`
Cipher string `yaml:"cipher,omitempty"`
UDP bool `yaml:"udp,omitempty"`
Plugin string `yaml:"plugin,omitempty"`
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
// Vmess
UUID string `yaml:"uuid,omitempty"`
AlterID *int `yaml:"alterId,omitempty"`
Network string `yaml:"network,omitempty"`
TLS bool `yaml:"tls,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ServerName string `yaml:"servername,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
GlobalPadding bool `yaml:"global-padding,omitempty"`
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
// Vless
Flow string `yaml:"flow,omitempty"`
WSPath string `yaml:"ws-path,omitempty"`
WSHeaders map[string]string `yaml:"ws-headers,omitempty"`
// Trojan
SNI string `yaml:"sni,omitempty"`
SSOpts TrojanSSOption `yaml:"ss-opts,omitempty"`
// Hysteria2
Ports string `yaml:"ports,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsPassword string `yaml:"obfs-password,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
CWND int `yaml:"cwnd,omitempty"`
UdpMTU int `yaml:"udp-mtu,omitempty"`
// Tuic
Token string `yaml:"token,omitempty"`
Ip string `yaml:"ip,omitempty"`
HeartbeatInterval int `yaml:"heartbeat-interval,omitempty"`
ReduceRtt bool `yaml:"reduce-rtt,omitempty"`
RequestTimeout int `yaml:"request-timeout,omitempty"`
UdpRelayMode string `yaml:"udp-relay-mode,omitempty"`
CongestionController string `yaml:"congestion-controller,omitempty"`
DisableSni bool `yaml:"disable-sni,omitempty"`
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
MaxOpenStreams int `yaml:"max-open-streams,omitempty"`
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
ReceiveWindow int `yaml:"recv-window,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `yaml:"max-datagram-frame-size,omitempty"`
UDPOverStream bool `yaml:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `yaml:"udp-over-stream-version,omitempty"`
}
type ProxyGroup struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Proxies []string `yaml:"proxies"`
Url string `yaml:"url,omitempty"`
Interval int `yaml:"interval,omitempty"`
}
type TrojanSSOption struct {
Enabled bool `yaml:"enabled,omitempty"`
Method string `yaml:"method,omitempty"`
Password string `yaml:"password,omitempty"`
}
type RealityOptions struct {
PublicKey string `yaml:"public-key"`
ShortID string `yaml:"short-id"`
}
type HTTPOptions struct {
Method string `yaml:"method,omitempty"`
Path []string `yaml:"path,omitempty"`
Headers map[string][]string `yaml:"headers,omitempty"`
}
type HTTP2Options struct {
Host []string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
}
type WSOptions struct {
Path string `yaml:"path,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
V2rayHttpUpgrade bool `yaml:"v2ray-http-upgrade,omitempty"`
V2rayHttpUpgradeFastOpen bool `yaml:"v2ray-http-upgrade-fast-open,omitempty"`
}

View File

@ -1,207 +0,0 @@
package clash
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func parseShadowsocks(s proxy.Proxy, uuid string) (*Proxy, error) {
config, ok := s.Option.(proxy.Shadowsocks)
if !ok {
return nil, fmt.Errorf("invalid type for Shadowsocks")
}
p := &Proxy{
Name: s.Name,
Type: "ss",
Server: s.Server,
Port: s.Port,
Cipher: config.Method,
Password: uuid,
UDP: true,
}
if strings.Contains(p.Cipher, "2022") {
serverKey, userKey := proxy.GenerateShadowsocks2022Password(config, uuid)
p.Password = fmt.Sprintf("%s:%s", serverKey, userKey)
}
return p, nil
}
func parseTrojan(data proxy.Proxy, password string) (*Proxy, error) {
trojan, ok := data.Option.(proxy.Trojan)
if !ok {
return nil, fmt.Errorf("invalid type for Trojan")
}
p := &Proxy{
Name: data.Name,
Type: "trojan",
Server: data.Server,
Port: data.Port,
Password: password,
SNI: trojan.SecurityConfig.SNI,
SkipCertVerify: trojan.SecurityConfig.AllowInsecure,
}
setTransportOptions(p, trojan.Transport, trojan.TransportConfig)
return p, nil
}
func parseVless(data proxy.Proxy, uuid string) (*Proxy, error) {
vless, ok := data.Option.(proxy.Vless)
if !ok {
return nil, fmt.Errorf("invalid type for Vless")
}
p := &Proxy{
Name: data.Name,
Type: "vless",
Server: data.Server,
Port: data.Port,
UUID: uuid,
Flow: vless.Flow,
UDP: true,
}
setSecurityOptions(p, vless.Security, vless.SecurityConfig)
clashTransport(p, vless.Transport, vless.TransportConfig)
return p, nil
}
func parseVmess(data proxy.Proxy, uuid string) (*Proxy, error) {
vmess, ok := data.Option.(proxy.Vmess)
if !ok {
return nil, fmt.Errorf("invalid type for Vmess")
}
alterID := 0
p := &Proxy{
Name: data.Name,
Type: "vmess",
Server: data.Server,
Port: data.Port,
UUID: uuid,
AlterID: &alterID,
Cipher: "auto",
}
setSecurityOptions(p, vmess.Security, vmess.SecurityConfig)
clashTransport(p, vmess.Transport, vmess.TransportConfig)
return p, nil
}
func parseHysteria2(data proxy.Proxy, uuid string) (*Proxy, error) {
hysteria2, ok := data.Option.(proxy.Hysteria2)
if !ok {
return nil, fmt.Errorf("invalid type for Hysteria2")
}
p := &Proxy{
Name: data.Name,
Type: "hysteria2",
Server: data.Server,
Port: data.Port,
Ports: hysteria2.HopPorts,
Password: uuid,
HeartbeatInterval: hysteria2.HopInterval,
SkipCertVerify: hysteria2.SecurityConfig.AllowInsecure,
SNI: hysteria2.SecurityConfig.SNI,
}
if hysteria2.ObfsPassword != "" {
p.Obfs = "salamander"
p.ObfsPassword = hysteria2.ObfsPassword
}
return p, nil
}
func parseTuic(data proxy.Proxy, uuid string) (*Proxy, error) {
tuic, ok := data.Option.(proxy.Tuic)
if !ok {
return nil, fmt.Errorf("invalid type for Tuic")
}
p := &Proxy{
Name: data.Name,
Type: "tuic",
Server: data.Server,
Port: data.Port,
UUID: uuid,
Password: uuid,
ALPN: []string{"h3"},
DisableSni: tuic.DisableSNI,
ReduceRtt: tuic.ReduceRtt,
CongestionController: tuic.CongestionController,
UdpRelayMode: tuic.UDPRelayMode,
SNI: tuic.SecurityConfig.SNI,
SkipCertVerify: tuic.SecurityConfig.AllowInsecure,
}
return p, nil
}
func parseAnyTLS(data proxy.Proxy, uuid string) (*Proxy, error) {
anyTLS, ok := data.Option.(proxy.AnyTLS)
if !ok {
return nil, fmt.Errorf("invalid type for AnyTLS")
}
p := &Proxy{
Name: data.Name,
Type: "anytls",
Server: data.Server,
Port: data.Port,
Password: uuid,
UDP: true,
ALPN: []string{
"h2",
"http/1.1",
},
}
if anyTLS.SecurityConfig.SNI != "" {
p.SNI = anyTLS.SecurityConfig.SNI
}
if anyTLS.SecurityConfig.AllowInsecure {
p.SkipCertVerify = anyTLS.SecurityConfig.AllowInsecure
}
return p, nil
}
func setSecurityOptions(p *Proxy, security string, config proxy.SecurityConfig) {
switch security {
case "tls":
p.TLS = true
p.ServerName = config.SNI
p.ClientFingerprint = config.Fingerprint
p.SkipCertVerify = config.AllowInsecure
case "reality":
p.TLS = true
p.ServerName = config.SNI
p.ClientFingerprint = config.Fingerprint
p.RealityOpts = RealityOptions{
PublicKey: config.RealityPublicKey,
ShortID: config.RealityShortId,
}
p.SkipCertVerify = config.AllowInsecure
default:
p.TLS = false
}
}
func setTransportOptions(p *Proxy, transport string, config proxy.TransportConfig) {
switch transport {
case "websocket":
p.Network = "ws"
p.WSOpts = WSOptions{
Path: config.Path,
Headers: map[string]string{
"Host": config.Host,
},
}
case "grpc":
p.Network = "grpc"
p.GrpcOpts = GrpcOptions{
GrpcServiceName: config.ServiceName,
}
default:
p.Network = "tcp"
}
}

View File

@ -1,35 +0,0 @@
package clash
import (
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func clashTransport(c *Proxy, transportType string, transportConfig proxy.TransportConfig) {
switch transportType {
case "websocket", "httpupgrade":
if transportType == "websocket" {
c.Network = "ws"
} else {
c.Network = transportType
}
c.WSOpts = WSOptions{
Path: transportConfig.Path,
Headers: map[string]string{},
}
if transportConfig.Host != "" {
c.WSOpts.Headers["host"] = transportConfig.Host
}
if transportType == "httpupgrade" {
c.WSOpts.V2rayHttpUpgrade = true
}
case "grpc":
c.Network = "grpc"
c.GrpcOpts = GrpcOptions{
GrpcServiceName: transportConfig.ServiceName,
}
case "tcp":
c.Network = "tcp"
}
}

View File

@ -1,278 +0,0 @@
package general
import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
type v2rayShareLink struct {
Ps string `json:"ps"`
Add string `json:"add"`
Port string `json:"port"`
ID string `json:"id"`
Aid string `json:"aid"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
SNI string `json:"sni"`
Path string `json:"path"`
TLS string `json:"tls"`
Flow string `json:"flow,omitempty"`
Alpn string `json:"alpn,omitempty"`
AllowInsecure bool `json:"allowInsecure"`
Fingerprint string `json:"fp,omitempty"`
PublicKey string `json:"pbk,omitempty"`
ShortId string `json:"sid,omitempty"`
SpiderX string `json:"spx,omitempty"`
V string `json:"v"`
}
// GenerateBase64General will output node URLs split by '\n' and then encode into base64
func GenerateBase64General(data []proxy.Proxy, uuid string) []byte {
var links []string
for _, v := range data {
p := buildProxy(v, uuid)
if p == "" {
continue
}
links = append(links, p)
}
var rsp []byte
rsp = base64.RawStdEncoding.AppendEncode(rsp, []byte(strings.Join(links, "\r\n")))
return rsp
}
func buildProxy(data proxy.Proxy, uuid string) string {
switch data.Protocol {
case "shadowsocks":
return ShadowsocksUri(data, uuid)
case "vmess":
return VmessUri(data, uuid)
case "vless":
return VlessUri(data, uuid)
case "trojan":
return TrojanUri(data, uuid)
case "hysteria2":
return Hysteria2Uri(data, uuid)
case "tuic":
return TuicUri(data, uuid)
default:
return ""
}
}
func ShadowsocksUri(data proxy.Proxy, uuid string) string {
ss, ok := data.Option.(proxy.Shadowsocks)
if !ok {
return ""
}
password := uuid
// SIP022 AEAD-2022 Ciphers
if strings.Contains(ss.Method, "2022") {
serverKey, userKey := proxy.GenerateShadowsocks2022Password(ss, uuid)
password = fmt.Sprintf("%s:%s", serverKey, userKey)
}
u := &url.URL{
Scheme: "ss",
User: url.User(strings.TrimSuffix(base64.URLEncoding.EncodeToString([]byte(ss.Method+":"+password)), "=")),
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)),
Fragment: data.Name,
}
return u.String()
}
func VmessUri(data proxy.Proxy, uuid string) string {
vmess := data.Option.(proxy.Vmess)
transport := vmess.TransportConfig
securityConfig := vmess.SecurityConfig
var s = v2rayShareLink{
V: "2",
Add: data.Server,
Port: fmt.Sprint(data.Port),
ID: uuid,
Aid: "0",
Ps: data.Name,
Net: "tcp",
}
switch vmess.Transport {
case "websocket":
s.Net = "ws"
s.Path = transport.Path
s.Host = transport.Host
case "grpc":
s.Net = "grpc"
s.Path = transport.ServiceName
case "httpupgrade":
s.Net = "http"
s.Path = transport.Path
s.Host = transport.Host
}
if vmess.Security == "tls" {
s.TLS = "tls"
s.SNI = securityConfig.SNI
s.AllowInsecure = securityConfig.AllowInsecure
s.Fingerprint = securityConfig.Fingerprint
}
b, _ := json.Marshal(s)
return "vmess://" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(b), "=")
}
func VlessUri(data proxy.Proxy, uuid string) string {
vless := data.Option.(proxy.Vless)
transportConfig := vless.TransportConfig
securityConfig := vless.SecurityConfig
var query = make(url.Values)
setQuery(&query, "flow", vless.Flow)
setQuery(&query, "security", vless.Security)
setQuery(&query, "encryption", "none")
switch vless.Transport {
case "websocket":
setQuery(&query, "type", "ws")
setQuery(&query, "host", transportConfig.Host)
setQuery(&query, "path", transportConfig.Path)
case "http2", "httpupgrade":
setQuery(&query, "type", "http")
setQuery(&query, "path", transportConfig.Path)
setQuery(&query, "host", transportConfig.Host)
case "grpc":
setQuery(&query, "type", "grpc")
setQuery(&query, "serviceName", transportConfig.ServiceName)
}
if vless.Security == "tls" {
setQuery(&query, "sni", securityConfig.SNI)
setQuery(&query, "fp", securityConfig.Fingerprint)
} else if vless.Security == "reality" {
setQuery(&query, "pbk", securityConfig.RealityPublicKey)
setQuery(&query, "sid", securityConfig.RealityShortId)
setQuery(&query, "sni", securityConfig.SNI)
setQuery(&query, "fp", securityConfig.Fingerprint)
setQuery(&query, "servername", securityConfig.SNI)
setQuery(&query, "spx", "/")
}
u := url.URL{
Scheme: "vless",
User: url.User(uuid),
Host: net.JoinHostPort(data.Server, fmt.Sprint(data.Port)),
RawQuery: query.Encode(),
Fragment: data.Name,
}
return u.String()
}
func TrojanUri(data proxy.Proxy, uuid string) string {
trojan := data.Option.(proxy.Trojan)
transportConfig := trojan.TransportConfig
securityConfig := trojan.SecurityConfig
var query = make(url.Values)
setQuery(&query, "security", trojan.Security)
switch trojan.Transport {
case "websocket":
setQuery(&query, "type", "ws")
setQuery(&query, "path", transportConfig.Path)
setQuery(&query, "host", transportConfig.Host)
case "grpc":
setQuery(&query, "type", "grpc")
setQuery(&query, "serviceName", transportConfig.ServiceName)
default:
setQuery(&query, "type", "tcp")
setQuery(&query, "path", transportConfig.Path)
setQuery(&query, "host", transportConfig.Host)
}
if securityConfig.AllowInsecure {
setQuery(&query, "allowInsecure", "1")
}
u := &url.URL{
Scheme: "trojan",
User: url.User(uuid),
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)),
RawQuery: query.Encode(),
Fragment: data.Name,
}
return u.String()
}
func Hysteria2Uri(data proxy.Proxy, uuid string) string {
hysteria2 := data.Option.(proxy.Hysteria2)
var query = make(url.Values)
setQuery(&query, "sni", hysteria2.SecurityConfig.SNI)
if hysteria2.SecurityConfig.AllowInsecure {
setQuery(&query, "insecure", "1")
}
if hp := strings.TrimSpace(hysteria2.HopPorts); hp != "" {
setQuery(&query, "mport", hp)
}
if hysteria2.ObfsPassword != "" {
setQuery(&query, "obfs", "salamander")
setQuery(&query, "obfs-password", hysteria2.ObfsPassword)
}
u := &url.URL{
Scheme: "hysteria2",
User: url.User(uuid),
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)),
RawQuery: query.Encode(),
Fragment: data.Name,
}
return u.String()
}
func TuicUri(data proxy.Proxy, uuid string) string {
tuic := data.Option.(proxy.Tuic)
var query = make(url.Values)
setQuery(&query, "congestion_control", tuic.CongestionController)
setQuery(&query, "udp_relay_mode", tuic.UDPRelayMode)
if tuic.SecurityConfig.SNI != "" {
setQuery(&query, "sni", tuic.SecurityConfig.SNI)
} else {
setQuery(&query, "disable_sni", "1")
}
if tuic.SecurityConfig.AllowInsecure {
setQuery(&query, "allow_insecure", "1")
}
u := &url.URL{
Scheme: "tuic",
User: url.User(uuid + ":" + uuid),
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)),
RawQuery: query.Encode(),
Fragment: data.Name,
}
return u.String()
}
func setQuery(q *url.Values, k, v string) {
if v != "" {
q.Set(k, v)
}
}

View File

@ -1,26 +0,0 @@
package general
import (
"testing"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func createServer() proxy.Proxy {
return proxy.Proxy{
Name: "Meta",
Server: "127.0.0.1",
Port: 13092,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Method: "aes-256-gcm",
ServerKey: "",
},
}
}
func TestGenerateBase64General(t *testing.T) {
s := createServer()
p := buildProxy(s, "935b33c7-e128-49f2-816b-71070469cac2")
t.Log(p)
}

View File

@ -1,61 +0,0 @@
package loon
import (
"bytes"
"embed"
"strings"
"text/template"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/logger"
)
//go:embed *.tpl
var configFiles embed.FS
func BuildLoon(servers []proxy.Proxy, uuid string) []byte {
uri := ""
nodes := make([]string, 0)
for _, s := range servers {
switch s.Protocol {
case "vmess":
nodes = append(nodes, s.Name)
uri += buildVMess(s, uuid)
case "shadowsocks":
nodes = append(nodes, s.Name)
uri += buildShadowsocks(s, uuid)
case "trojan":
nodes = append(nodes, s.Name)
uri += buildTrojan(s, uuid)
case "vless":
nodes = append(nodes, s.Name)
uri += buildVless(s, uuid)
case "hysteria2":
nodes = append(nodes, s.Name)
uri += buildHysteria2(s, uuid)
default:
continue
}
}
file, err := configFiles.ReadFile("default.tpl")
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
// replace template
tpl, err := template.New("default").Parse(string(file))
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
var buf bytes.Buffer
if err = tpl.Execute(&buf, map[string]interface{}{
"Proxies": uri,
"Nodes": strings.Join(nodes, ","),
}); err != nil {
logger.Errorf("Execute Loon template error: %v", err.Error())
return nil
}
return buf.Bytes()
}

View File

@ -1,58 +0,0 @@
[General]
ipv6-vif = auto
ip-mode = dual
skip-proxy = 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,127.0.0.0/8,localhost,*.local
bypass-tun = 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,127.0.0.0/8,localhost,*.local
dns-server = system,119.29.29.29,223.5.5.5
hijack-dns = 8.8.8.8:53,8.8.4.4:53,1.1.1.1:53,1.0.0.1:53
allow-wifi-access = true
wifi-access-http-port = 6888
wifi-access-socks5-port = 6889
proxy-test-url = http://bing.com/generate_204
internet-test-url = http://wifi.vivo.com.cn/generate_204
test-timeout = 5
interface-mode = auto
[Proxy]
{{.Proxies}}
[Proxy Group]
🚀 Proxy = select,🌏 Auto,{{.Nodes}}
🌏 Auto = fallback,{{.Nodes}},interval = 600,max-timeout = 3000
🍎 Apple = select,🚀 Proxy,🎯 Direct,{{.Nodes}}
🔍 Google = select,🚀 Proxy,{{.Nodes}}
🪟 Microsoft = select,🚀 Proxy,🎯 Direct,{{.Nodes}}
📠 X = select,🚀 Proxy,{{.Nodes}}
🤖 AI = select,🚀 Proxy,🎯 Direct,{{.Nodes}}
📟 Telegram = select,🚀 Proxy,{{.Nodes}}
📺 YouTube = select,🚀 Proxy,{{.Nodes}}
🇨🇳 China = select,🎯 Direct,🚀 Proxy,{{.Nodes}}
🐠 Final = select,🚀 Proxy,🎯 Direct,{{.Nodes}}
🎯 Direct = select,DIRECT
[Remote Rule]
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Apple/Apple.list, policy=🍎 Apple, tag=Apple, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Apple/Apple_Domain.list, policy=🍎 Apple, tag=Apple_Domain, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Google/Google.list, policy=🔍 Google, tag=Google, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Microsoft/Microsoft.list, policy=🪟 Microsoft, tag=Microsoft, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Twitter/Twitter.list, policy=📠 X, tag=X, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/OpenAI/OpenAI.list, policy=🤖 AI, tag=OpenAI, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Telegram/Telegram.list, policy=📟 Telegram, tag=Telegram, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/YouTube/YouTube.list, policy=📺 YouTube, tag=YouTube, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/YouTubeMusic/YouTubeMusic.list, policy=📺 YouTube, tag=YouTubeMusic, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Global/Global.list, policy=🚀 Proxy, tag=Global, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Global/Global_Domain.list, policy=🚀 Proxy, tag=Global_Domain, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/ChinaMax/ChinaMax.list, policy=🇨🇳 China, tag=ChinaMax, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/ChinaMax/ChinaMax_Domain.list, policy=🇨🇳 China, tag=ChinaMax_Domain, enabled=true
https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Loon/Lan/Lan.list, policy=🎯 Direct, tag=LAN, enabled=true
[Rule]
GEOIP,CN,🇨🇳 China
FINAL,🐠 Final
[Rewrite]
# Redirect Google Service
^https?:\/\/(www.)?g\.cn 302 https://www.google.com
^https?:\/\/(www.)?google\.cn 302 https://www.google.com
# Redirect Githubusercontent
^https://.*\.githubusercontent\.com\/ header-replace Accept-Language en-us

View File

@ -1,34 +0,0 @@
package loon
import (
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildHysteria2(data proxy.Proxy, password string) string {
hysteria2 := data.Option.(proxy.Hysteria2)
configs := []string{
fmt.Sprintf("%s=Hysteria2", data.Name),
data.Server,
strconv.Itoa(data.Port),
password,
"udp=true",
}
if hysteria2.ObfsPassword != "" {
configs = append(configs, "obfs=salamander", fmt.Sprintf("salamander-password=%s", hysteria2.ObfsPassword))
}
if hysteria2.SecurityConfig.SNI != "" {
configs = append(configs, fmt.Sprintf("sni=%s", hysteria2.SecurityConfig.SNI))
if hysteria2.SecurityConfig.AllowInsecure {
configs = append(configs, "skip-cert-verify=true")
} else {
configs = append(configs, "skip-cert-verify=false")
}
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}

View File

@ -1,29 +0,0 @@
package loon
import (
"testing"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func createSS() proxy.Proxy {
return proxy.Proxy{
Name: "Shadowsocks",
Server: "127.0.0.1",
Port: 10301,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Method: "aes-256-gcm",
ServerKey: "",
},
}
}
func TestBuildSS(t *testing.T) {
s := createSS()
password := "f0d0237d-193a-4cf5-99dd-b02207beaea6"
uri := buildShadowsocks(s, password)
t.Log(uri)
}

View File

@ -1,34 +0,0 @@
package loon
import (
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildShadowsocks(data proxy.Proxy, password string) string {
shadowsocks := data.Option.(proxy.Shadowsocks)
// If the method is 2022-blake3-chacha20-poly1305, it means that the server is a relay server
if shadowsocks.Method == "2022-blake3-chacha20-poly1305" {
return ""
}
if strings.Contains(shadowsocks.Method, "2022") {
serverKey, userKey := proxy.GenerateShadowsocks2022Password(shadowsocks, password)
password = fmt.Sprintf("%s:%s", serverKey, userKey)
}
configs := []string{
fmt.Sprintf("%s=Shadowsocks", data.Name),
data.Server,
strconv.Itoa(data.Port),
shadowsocks.Method,
password,
"fast-open=false",
"udp=true",
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}

View File

@ -1,43 +0,0 @@
package loon
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildTrojan(data proxy.Proxy, password string) string {
trojan := data.Option.(proxy.Trojan)
configs := []string{
fmt.Sprintf("%s=trojan", data.Name),
data.Server,
fmt.Sprintf("%d", data.Port),
password,
"fast-open=false",
"udp=true",
}
if trojan.SecurityConfig.SNI != "" {
configs = append(configs, fmt.Sprintf("sni=%s", trojan.SecurityConfig.SNI))
}
if trojan.SecurityConfig.AllowInsecure {
configs = append(configs, "skip-cert-verify=true")
} else {
configs = append(configs, "skip-cert-verify=false")
}
if trojan.Transport == "websocket" {
configs = append(configs, "transport=ws")
if trojan.TransportConfig.Path != "" {
configs = append(configs, fmt.Sprintf("path=%s", trojan.TransportConfig.Path))
}
if trojan.TransportConfig.Host != "" {
configs = append(configs, fmt.Sprintf("host=%s", trojan.TransportConfig.Host))
}
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}

View File

@ -1,62 +0,0 @@
package loon
import (
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/logger"
)
func buildVless(data proxy.Proxy, password string) string {
vless := data.Option.(proxy.Vless)
// If flow is not empty, it means that the server is a relay server
if vless.Flow != "" {
return ""
}
configs := []string{
fmt.Sprintf("%s=vless", data.Name),
data.Server,
strconv.Itoa(data.Port),
"auto",
password,
"fast-open=false",
"udp=true",
"alterId=0",
}
switch vless.Transport {
case "tcp":
configs = append(configs, "transport=tcp")
case "websocket":
configs = append(configs, "transport=ws")
if vless.TransportConfig.Path != "" {
configs = append(configs, fmt.Sprintf("path=%s", vless.TransportConfig.Path))
}
if vless.TransportConfig.Host != "" {
configs = append(configs, fmt.Sprintf("host=%s", vless.TransportConfig.Host))
}
default:
logger.Info("Loon Unknown transport type: ", logger.Field("transport", vless.Transport))
return ""
}
if vless.Security == "tls" {
configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vless.SecurityConfig.SNI))
if vless.SecurityConfig.AllowInsecure {
configs = append(configs, "skip-cert-verify=true")
} else {
configs = append(configs, "skip-cert-verify=false")
}
} else if vless.Security == "reality" {
// Loon does not support reality security
logger.Info("Loon Unknown security type: ", logger.Field("security", vless.Security))
return ""
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}

View File

@ -1,53 +0,0 @@
package loon
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/logger"
)
func buildVMess(data proxy.Proxy, password string) string {
vmess := data.Option.(proxy.Vmess)
configs := []string{
fmt.Sprintf("%s=vmess", data.Name),
data.Server,
fmt.Sprintf("%d", data.Port),
"auto",
password,
"fast-open=false",
"udp=true",
"alterId=0",
}
switch vmess.Transport {
case "tcp":
configs = append(configs, "transport=tcp")
case "websocket":
configs = append(configs, "transport=ws")
if vmess.TransportConfig.Path != "" {
configs = append(configs, fmt.Sprintf("path=%s", vmess.TransportConfig.Path))
}
if vmess.TransportConfig.Host != "" {
configs = append(configs, fmt.Sprintf("host=%s", vmess.TransportConfig.Host))
}
default:
logger.Info("Loon Unknown transport type: ", logger.Field("transport", vmess.Transport))
return ""
}
if vmess.Security == "tls" {
configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vmess.SecurityConfig.SNI))
if vmess.SecurityConfig.AllowInsecure {
configs = append(configs, "skip-cert-verify=true")
} else {
configs = append(configs, "skip-cert-verify=false")
}
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}

View File

@ -1,137 +0,0 @@
package proxy
import "embed"
// Adapter represents a proxy adapter
type Adapter struct {
Proxies []Proxy
Group []Group
Rules []string // rule
Nodes []string // all node
Default string // Default Node
TemplateFS *embed.FS // Template file system
}
// Proxy represents a proxy server
type Proxy struct {
Name string // Name of the proxy
Server string // Server address of the proxy
Port int // Port of the proxy server
Protocol string // Protocol type (e.g., shadowsocks, vless, vmess, trojan, hysteria2, tuic, anytls)
Country string // Country of the proxy
Tags []string // Tags for the proxy
Option any // Additional options for the proxy configuration
}
// Group represents a group of proxies
type Group struct {
Name string
Type GroupType
Proxies []string
URL string
Interval int
Reject bool // Reject group
Direct bool // Direct group
Tags []string // Tags for the group
Default bool // Default group
}
type GroupType string
const (
GroupTypeSelect GroupType = "select"
GroupTypeURLTest GroupType = "url-test"
GroupTypeFallback GroupType = "fallback"
)
// Shadowsocks represents a Shadowsocks proxy configuration
type Shadowsocks struct {
Port int `json:"port"`
Method string `json:"method"`
ServerKey string `json:"server_key"`
}
// Vless represents a Vless proxy configuration
type Vless struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// Vmess represents a Vmess proxy configuration
type Vmess struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// Trojan represents a Trojan proxy configuration
type Trojan struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// Hysteria2 represents a Hysteria2 proxy configuration
type Hysteria2 struct {
Port int `json:"port"`
HopPorts string `json:"hop_ports"`
HopInterval int `json:"hop_interval"`
ObfsPassword string `json:"obfs_password"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// Tuic represents a Tuic proxy configuration
type Tuic struct {
Port int `json:"port"`
DisableSNI bool `json:"disable_sni"`
ReduceRtt bool `json:"reduce_rtt"`
UDPRelayMode string `json:"udp_relay_mode"`
CongestionController string `json:"congestion_controller"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// AnyTLS represents an AnyTLS proxy configuration
type AnyTLS struct {
Port int `json:"port"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// TransportConfig represents the transport configuration for a proxy
type TransportConfig struct {
Path string `json:"path,omitempty"` // ws/httpupgrade
Host string `json:"host,omitempty"`
ServiceName string `json:"service_name"` // grpc
DisableSNI bool `json:"disable_sni"` // Disable SNI for the transport(tuic)
ReduceRtt bool `json:"reduce_rtt"` // Reduce RTT for the transport(tuic)
UDPRelayMode string `json:"udp_relay_mode"` // UDP relay mode for the transport(tuic)
CongestionController string `json:"congestion_controller"` // Congestion controller for the transport(tuic)
}
// SecurityConfig represents the security configuration for a proxy
type SecurityConfig struct {
SNI string `json:"sni"`
AllowInsecure bool `json:"allow_insecure"`
Fingerprint string `json:"fingerprint"`
RealityServerAddr string `json:"reality_server_addr"`
RealityServerPort int `json:"reality_server_port"`
RealityPrivateKey string `json:"reality_private_key"`
RealityPublicKey string `json:"reality_public_key"`
RealityShortId string `json:"reality_short_id"`
}
// Relay represents a relay configuration
type Relay struct {
RelayHost string
DispatchMode string
Prefix string
}

View File

@ -1,15 +0,0 @@
package proxy
import (
"encoding/base64"
"github.com/perfect-panel/server/pkg/uuidx"
)
func GenerateShadowsocks2022Password(ss Shadowsocks, password string) (string, string) {
if ss.Method == "2022-blake3-aes-128-gcm" {
password = uuidx.UUIDToBase64(password, 16)
} else {
password = uuidx.UUIDToBase64(password, 32)
}
return base64.StdEncoding.EncodeToString([]byte(ss.ServerKey)), password
}

View File

@ -1,22 +0,0 @@
package quantumultx
import (
"encoding/base64"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func BuildQuantumultX(servers []proxy.Proxy, uuid string) string {
var uri string
for _, s := range servers {
switch s.Protocol {
case "vmess":
uri += buildVmess(s, uuid)
case "shadowsocks":
uri += buildShadowsocks(s, uuid)
case "trojan":
uri += buildTrojan(s, uuid)
}
}
return base64.StdEncoding.EncodeToString([]byte(uri))
}

View File

@ -1,94 +0,0 @@
package quantumultx
import (
"testing"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func createVMess() proxy.Proxy {
return proxy.Proxy{
Name: "Vmess",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "vmess",
Option: proxy.Vmess{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "test.xx.com",
},
Security: "none",
},
}
}
func createSS() proxy.Proxy {
return proxy.Proxy{
Name: "Shadowsocks",
Server: "test.xxxx.com",
Port: 10301,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Port: 10301,
Method: "aes-256-gcm",
ServerKey: "123456",
},
}
}
func createTrojan() proxy.Proxy {
return proxy.Proxy{
Name: "Trojan",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "trojan",
Option: proxy.Trojan{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "baidu.com",
},
SecurityConfig: proxy.SecurityConfig{
SNI: "baidu.com",
AllowInsecure: true,
},
},
}
}
func TestVmess(t *testing.T) {
s := createVMess()
vmess := buildVmess(s, "uuid")
t.Log(vmess)
// output:
// vmess=127.0.0.1:13002,method=chacha20-poly1305,password=uuid,fast-open=true,udp-relay=true,tag=Vmess,tls-verification=true,obfs-uri=/ws,obfs-host=baidu.com
}
func TestShadowsocks(t *testing.T) {
s := createSS()
shadowsocks := buildShadowsocks(s, "uuid")
t.Log(shadowsocks)
// output:
// shadowsocks=127.0.0.1:10301,method=aes-256-gcm,password=uuid,fast-open=true,udp-relay=true,tag=Shadowsocks
}
func TestTrojan(t *testing.T) {
s := createTrojan()
trojan := buildTrojan(s, "password")
t.Log(trojan)
// output:
// trojan=192.168.0.1:13002,password=password,fast-open=true,udp-relay=true,tag=Trojan,obfs=wss,obfs-uri=ws,obfs-host=baidu.com
}
func TestBuildQuantumultX(t *testing.T) {
var servers []proxy.Proxy
uri := BuildQuantumultX(servers, "uuid")
t.Log(uri)
// output:
// c2hhZG93c29ja3M9MTI3LjAuMC4xOjEwMzAxLG1ldGhvZD1hZXMtMjU2LWdjbSxwYXNzd29yZD11dWlkLGZhc3Qtb3Blbj10cnVlLHVkcC1yZWxheT10cnVlLHRhZz1TaGFkb3dzb2Nrcw0KdHJvamFuPTE5Mi4xNjguMC4xOjEzMDAyLHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVRyb2phbixvYmZzPXdzcyxvYmZzLXVyaT13cyxvYmZzLWhvc3Q9YmFpZHUuY29tDQp2bWVzcz0xMjcuMC4wLjE6MTMwMDIsbWV0aG9kPWNoYWNoYTIwLXBvbHkxMzA1LHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVZtZXNzLHRscy12ZXJpZmljYXRpb249dHJ1ZSxvYmZzLXVyaT0vd3Msb2Jmcy1ob3N0PWJhaWR1LmNvbQ0K
}

View File

@ -1,30 +0,0 @@
package quantumultx
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildShadowsocks(data proxy.Proxy, uuid string) string {
ss := data.Option.(proxy.Shadowsocks)
addr := fmt.Sprintf("%s:%d", data.Server, data.Port)
password := uuid
if strings.Contains(ss.Method, "2022") {
serverKey, userKey := proxy.GenerateShadowsocks2022Password(ss, uuid)
password = fmt.Sprintf("%s:%s", serverKey, userKey)
}
config := []string{
addr,
fmt.Sprintf("method=%s", ss.Method),
fmt.Sprintf("password=%s", password),
"fast-open=true",
"udp-relay=true",
fmt.Sprintf("tag=%s", data.Name),
}
return strings.Join(config, ",") + "\r\n"
}

View File

@ -1,39 +0,0 @@
package quantumultx
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
// 生成 Trojan 配置
func buildTrojan(data proxy.Proxy, password string) string {
trojan := data.Option.(proxy.Trojan)
addr := fmt.Sprintf("trojan=%s:%d", data.Server, data.Port)
config := []string{
addr,
fmt.Sprintf("password=%s", password),
"fast-open=true",
"udp-relay=true",
fmt.Sprintf("tag=%s", data.Name),
}
if trojan.Transport == "websocket" {
config = append(config, "obfs=wss")
if trojan.TransportConfig.Path != "" {
config = append(config, fmt.Sprintf("obfs-uri=%s", trojan.TransportConfig.Path))
}
if trojan.TransportConfig.Host != "" {
config = append(config, fmt.Sprintf("obfs-host=%s", trojan.TransportConfig.Host))
}
} else {
config = append(config, "over-tls=true")
if trojan.SecurityConfig.SNI != "" {
config = append(config, fmt.Sprintf("tls-host=%s", trojan.SecurityConfig.SNI))
}
}
return strings.Join(config, ",") + "\r\n"
}

View File

@ -1,45 +0,0 @@
package quantumultx
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildVmess(data proxy.Proxy, uuid string) string {
vmess := data.Option.(proxy.Vmess)
addr := fmt.Sprintf("vmess=%s:%d", data.Server, data.Port)
var host string
uriConfig := []string{
addr,
"method=chacha20-poly1305",
fmt.Sprintf("password=%s", uuid),
"fast-open=true",
"udp-relay=true",
fmt.Sprintf("tag=%s", data.Name),
}
if vmess.Security == "tls" {
if vmess.Transport == "tcp" {
uriConfig = append(uriConfig, "obfs=over-tls")
}
if vmess.SecurityConfig.AllowInsecure {
uriConfig = append(uriConfig, "tls-verification=true")
} else {
uriConfig = append(uriConfig, "tls-verification=false")
}
if vmess.SecurityConfig.SNI != "" {
host = vmess.SecurityConfig.SNI
}
}
if vmess.Transport == "websocket" {
uriConfig = append(uriConfig, fmt.Sprintf("obfs-uri=%s", vmess.TransportConfig.Path))
host = vmess.TransportConfig.Host
}
if host != "" {
uriConfig = append(uriConfig, fmt.Sprintf("obfs-host=%s", host))
}
return strings.Join(uriConfig, ",") + "\r\n"
}

View File

@ -1,48 +0,0 @@
package shadowrocket
import (
"fmt"
"time"
"github.com/perfect-panel/server/pkg/adapter/general"
"encoding/base64"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/traffic"
)
type UserInfo struct {
Upload int64
Download int64
TotalTraffic int64
ExpiredDate time.Time
}
func BuildShadowrocket(servers []proxy.Proxy, uuid string, userinfo UserInfo) []byte {
upload := traffic.AutoConvert(userinfo.Upload, false)
download := traffic.AutoConvert(userinfo.Download, false)
total := traffic.AutoConvert(userinfo.TotalTraffic, false)
expiredAt := userinfo.ExpiredDate.Format("2006-01-02 15:04:05")
uri := fmt.Sprintf("STATUS=🚀↑:%s,↓:%s,TOT:%s💡Expires:%s\r\n", upload, download, total, expiredAt)
for _, s := range servers {
switch s.Protocol {
case "vmess":
uri += buildVmess(s, uuid)
case "shadowsocks":
uri += general.ShadowsocksUri(s, uuid) + "\r\n"
case "trojan":
uri += general.TrojanUri(s, uuid) + "\r\n"
case "vless":
uri += general.VlessUri(s, uuid) + "\r\n"
case "hysteria2":
uri += general.Hysteria2Uri(s, uuid) + "\r\n"
case "tuic":
uri += general.TuicUri(s, uuid) + "\r\n"
default:
continue
}
}
return []byte(base64.StdEncoding.EncodeToString([]byte(uri)))
}

View File

@ -1,76 +0,0 @@
package shadowrocket
import (
"testing"
"time"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func createVMess() proxy.Proxy {
return proxy.Proxy{
Name: "Vmess",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "vmess",
Option: proxy.Vmess{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "test.xx.com",
},
Security: "none",
},
}
}
func createSS() proxy.Proxy {
return proxy.Proxy{
Name: "Shadowsocks",
Server: "test.xxxx.com",
Port: 10301,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Port: 10301,
Method: "aes-256-gcm",
ServerKey: "123456",
},
}
}
func createTrojan() proxy.Proxy {
return proxy.Proxy{
Name: "Trojan",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "trojan",
Option: proxy.Trojan{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "baidu.com",
},
SecurityConfig: proxy.SecurityConfig{
SNI: "baidu.com",
AllowInsecure: true,
},
},
}
}
func TestBuildShadowrocket(t *testing.T) {
s := []proxy.Proxy{
createVMess(),
createSS(),
createTrojan(),
}
uri := BuildShadowrocket(s, "uuid", UserInfo{
Upload: 1024,
Download: 1024,
TotalTraffic: 2048,
ExpiredDate: time.Now().AddDate(0, 0, 1),
})
t.Log(string(uri))
}

View File

@ -1,57 +0,0 @@
package shadowrocket
import (
"fmt"
"strings"
"encoding/base64"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildVmess(data proxy.Proxy, uuid string) string {
vmess := data.Option.(proxy.Vmess)
userinfo := fmt.Sprintf("auto:%s@%s:%d", uuid, data.Server, data.Port)
// 准备 config使用默认值
config := map[string]interface{}{
"tfo": 1,
"remark": data.Name,
"alterId": 0,
}
// tls 配置
if vmess.Security == "tls" {
config["tls"] = 1
if vmess.SecurityConfig.AllowInsecure {
config["allowInsecure"] = 1
}
if vmess.SecurityConfig.SNI != "" {
config["peer"] = vmess.SecurityConfig.SNI
}
}
// transport 配置
switch vmess.Transport {
case "websocket":
config["obfs"] = "websocket"
if vmess.TransportConfig.Path != "" {
config["path"] = vmess.TransportConfig.Path
}
if vmess.TransportConfig.Host != "" {
config["obfsParam"] = vmess.TransportConfig.Host
}
case "grpc":
config["obfs"] = "grpc"
if vmess.TransportConfig.ServiceName != "" {
config["path"] = vmess.TransportConfig.ServiceName
}
}
query := make([]string, 0)
for k, v := range config {
query = append(query, fmt.Sprintf("%s=%v", k, v))
}
queryStr := strings.Join(query, "&")
uri := fmt.Sprintf("vmess://%s?%s\r\n", base64.StdEncoding.EncodeToString([]byte(userinfo)), queryStr)
return uri
}

View File

@ -1,42 +0,0 @@
package singbox
import "github.com/perfect-panel/server/pkg/adapter/proxy"
type AnyTLSOutboundOptions struct {
ServerOptions
OutboundTLSOptionsContainer
Password string `json:"password,omitempty"`
}
func ParseAnyTLS(data proxy.Proxy, password string) (*Proxy, error) {
anyTLS := data.Option.(proxy.AnyTLS)
config := &AnyTLSOutboundOptions{
ServerOptions: ServerOptions{
Tag: data.Name,
Type: AnyTLS,
Server: data.Server,
ServerPort: data.Port,
},
OutboundTLSOptionsContainer: OutboundTLSOptionsContainer{
TLS: &OutboundTLSOptions{
Enabled: true,
ALPN: []string{"h2", "http/1.1"},
Insecure: anyTLS.SecurityConfig.AllowInsecure,
},
},
Password: password,
}
if anyTLS.SecurityConfig.SNI != "" {
config.OutboundTLSOptionsContainer.TLS.ServerName = anyTLS.SecurityConfig.SNI
}
p := &Proxy{
Tag: data.Name,
Type: AnyTLS,
AnyTLSOptions: config,
}
return p, nil
}

View File

@ -1,201 +0,0 @@
package singbox
import (
"encoding/json"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/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: adapter.Default,
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: adapter.Default,
},
{
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
}

View File

@ -1,100 +0,0 @@
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
}
]
}
`

View File

@ -1,76 +0,0 @@
package singbox
import (
"strings"
"github.com/perfect-panel/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
}

View File

@ -1,17 +0,0 @@
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"`
}

View File

@ -1,130 +0,0 @@
package singbox
import (
"strconv"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/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
}

View File

@ -1,15 +0,0 @@
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)
}

View File

@ -1,45 +0,0 @@
package singbox
import (
"fmt"
"strings"
"github.com/perfect-panel/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) {
ss := data.Option.(proxy.Shadowsocks)
password := uuid
// SIP022 AEAD-2022 Ciphers
if strings.Contains(ss.Method, "2022") {
serverKey, userKey := proxy.GenerateShadowsocks2022Password(ss, uuid)
password = fmt.Sprintf("%s:%s", serverKey, userKey)
}
p := &Proxy{
Tag: data.Name,
Type: Shadowsocks,
ShadowsocksOptions: &ShadowsocksOptions{
ServerOptions: ServerOptions{
Tag: data.Name,
Type: Shadowsocks,
Server: data.Server,
ServerPort: data.Port,
},
Method: ss.Method,
Password: password,
Network: "tcp",
},
}
return p, nil
}

View File

@ -1,102 +0,0 @@
package singbox
import (
"encoding/json"
"fmt"
)
const (
Trojan = "trojan"
VLESS = "vless"
VMess = "vmess"
TUIC = "tuic"
Hysteria2 = "hysteria2"
AnyTLS = "anytls"
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:"-"`
AnyTLSOptions *AnyTLSOutboundOptions `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 AnyTLS:
return json.Marshal(p.AnyTLSOptions)
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)
}
}

View File

@ -1,80 +0,0 @@
package singbox
import (
"testing"
"github.com/perfect-panel/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))
}

View File

@ -1,87 +0,0 @@
package singbox
import (
"github.com/perfect-panel/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"`
}

View File

@ -1,11 +0,0 @@
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)
}

View File

@ -1,39 +0,0 @@
package singbox
import (
"github.com/perfect-panel/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
}

View File

@ -1,42 +0,0 @@
package singbox
import (
"github.com/perfect-panel/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: tuic.CongestionController,
UDPRelayMode: tuic.UDPRelayMode,
ZeroRTTHandshake: tuic.ReduceRtt,
},
}
// Security options
p.TUICOptions.TLS = NewOutboundTLSOptions("tls", tuic.SecurityConfig)
return p, nil
}

View File

@ -1,114 +0,0 @@
package singbox
import (
"encoding/json"
"time"
"github.com/perfect-panel/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

View File

@ -1,44 +0,0 @@
package singbox
import (
"github.com/perfect-panel/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
}

View File

@ -1,43 +0,0 @@
package singbox
import (
"github.com/perfect-panel/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
}

View File

@ -1,80 +0,0 @@
package surfboard
import (
"bytes"
"embed"
"fmt"
"strings"
"text/template"
"time"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/traffic"
)
//go:embed *.tpl
var configFiles embed.FS
var shadowsocksSupportMethod = []string{"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"}
func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byte {
var proxies, proxyGroup string
var removed []string
var ps []string
for _, p := range servers.Proxies {
switch p.Protocol {
case "shadowsocks":
proxies += buildShadowsocks(p, user.UUID)
case "trojan":
proxies += buildTrojan(p, user.UUID)
case "vmess":
proxies += buildVMess(p, user.UUID)
default:
removed = append(removed, p.Name)
}
ps = append(ps, p.Name)
}
file, err := configFiles.ReadFile("default.tpl")
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
// replace template
tpl, err := template.New("default").Parse(string(file))
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
var buf bytes.Buffer
var expiredAt string
if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) {
expiredAt = "长期有效"
} else {
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
}
ps = tool.RemoveStringElement(ps, removed...)
proxyGroup = strings.Join(ps, ",")
// convert traffic
upload := traffic.AutoConvert(user.Upload, false)
download := traffic.AutoConvert(user.Download, false)
total := traffic.AutoConvert(user.TotalTraffic, false)
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
// query Host
if err = tpl.Execute(&buf, map[string]interface{}{
"Proxies": proxies,
"ProxyGroup": proxyGroup,
"SubscribeURL": user.SubscribeURL,
"SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量%s\\n剩余流量: %s\\n套餐流量%s\\n到期时间%s", siteName, upload, download, unusedTraffic, total, expiredAt),
}); err != nil {
logger.Errorf("build Surge config error: %v", err.Error())
return nil
}
return buf.Bytes()
}

View File

@ -1,24 +0,0 @@
package surfboard
import (
"testing"
"time"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/uuidx"
)
func TestBuildSurfboard(t *testing.T) {
siteName := "test"
user := UserInfo{
UUID: uuidx.NewUUID().String(),
Upload: 0,
Download: 0,
TotalTraffic: 0,
ExpiredDate: time.Now().AddDate(0, 1, 1),
SubscribeURL: "https://test.com",
}
conf := BuildSurfboard(proxy.Adapter{}, siteName, user)
t.Log(string(conf))
}

View File

@ -1,62 +0,0 @@
#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true
[General]
dns-server = system, 119.29.29.29, 223.5.5.5
skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local
always-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com
proxy-test-url = http://www.gstatic.com/generate_204
internet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204
test-timeout = 5
http-listen = 0.0.0.0:6088
socks5-listen = 0.0.0.0:6089
[Panel]
SubscribeInfo = {{.SubscribeInfo}}, style=info
[Proxy]
{{.Proxies}}
[Proxy Group]
🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes
🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes
🐠 Final = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes
🌏 Auto = fallback, include-other-group=🇺🇳 Nodes, url=http://www.gstatic.com/generate_204, interval=600, timeout=5
🎯 Direct = select, DIRECT, hidden=1
🇺🇳 Nodes = select, {{.ProxyGroup}}, hidden=1
[Rule]
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct
GEOIP, CN, 🇨🇳 China
FINAL, 🐠 Final, dns-failed
[URL Rewrite]
^https?:\/\/(www.)?g\.cn https://www.google.com 302
^https?:\/\/(www.)?google\.cn https://www.google.com 302

View File

@ -1,12 +0,0 @@
package surfboard
import "time"
type UserInfo struct {
UUID string
Upload int64
Download int64
TotalTraffic int64
ExpiredDate time.Time
SubscribeURL string
}

View File

@ -1,28 +0,0 @@
package surfboard
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildShadowsocks(data proxy.Proxy, uuid string) string {
ss, ok := data.Option.(proxy.Shadowsocks)
if !ok {
return ""
}
// Not supporting SIP022 AEAD-2022 Ciphers
if strings.Contains(ss.Method, "2022") {
return ""
}
addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port)
config := []string{
addr,
fmt.Sprintf("encrypt-method=%s", ss.Method),
fmt.Sprintf("password=%s", uuid),
"tfo=true",
"udp-relay=true",
}
return strings.Join(config, ",") + "\r\n"
}

View File

@ -1,28 +0,0 @@
package surfboard
import (
"testing"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func createSS() proxy.Proxy {
return proxy.Proxy{
Name: "Shadowsocks",
Server: "test.xxxx.com",
Port: 10301,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Port: 10301,
Method: "aes-256-gcm",
ServerKey: "123456",
},
}
}
func TestShadowsocks(t *testing.T) {
node := createSS()
uuid := "123456"
shadowsocks := buildShadowsocks(node, uuid)
t.Log(shadowsocks)
}

View File

@ -1,41 +0,0 @@
package surfboard
import (
"strconv"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildTrojan(data proxy.Proxy, uuid string) string {
// $config = [
// "{$server['name']}=trojan",
// "{$server['host']}",
// "{$server['port']}",
// "password={$password}",
// $protocol_settings['server_name'] ? "sni={$protocol_settings['server_name']}" : "",
// 'tfo=true',
// 'udp-relay=true'
//];
trojan, ok := data.Option.(proxy.Trojan)
if !ok {
return ""
}
config := []string{
data.Name + "=trojan",
data.Server,
strconv.Itoa(data.Port),
"password=" + uuid,
"tfo=true",
"udp-relay=true",
}
if trojan.SecurityConfig.SNI != "" {
config = append(config, "sni="+trojan.SecurityConfig.SNI)
}
if trojan.SecurityConfig.AllowInsecure {
config = append(config, "skip-cert-verify=true")
} else {
config = append(config, "skip-cert-verify=false")
}
return strings.Join(config, ",") + "\r\n"
}

View File

@ -1,36 +0,0 @@
package surfboard
import (
"testing"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func createTrojan() proxy.Proxy {
return proxy.Proxy{
Name: "Trojan",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "trojan",
Option: proxy.Trojan{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "baidu.com",
},
SecurityConfig: proxy.SecurityConfig{
SNI: "baidu.com",
AllowInsecure: true,
},
},
}
}
func TestTrojan(t *testing.T) {
node := createTrojan()
uuid := "123456"
trojan := buildTrojan(node, uuid)
t.Log(trojan)
}

View File

@ -1,45 +0,0 @@
package surfboard
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildVMess(data proxy.Proxy, uuid string) string {
vmess, ok := data.Option.(proxy.Vmess)
if !ok {
return ""
}
addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port)
uriConfig := []string{
addr,
fmt.Sprintf("username=%s", uuid),
"vmess-aead=true",
"tfo=true",
"udp-relay=true",
}
if vmess.Security == "tls" {
uriConfig = append(uriConfig, "tls=true")
if vmess.SecurityConfig.AllowInsecure {
uriConfig = append(uriConfig, "skip-cert-verify=true")
} else {
uriConfig = append(uriConfig, "skip-cert-verify=false")
}
if vmess.SecurityConfig.SNI != "" {
uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI))
}
}
if vmess.Transport == "websocket" {
uriConfig = append(uriConfig, "ws=true")
if vmess.TransportConfig.Path != "" {
uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path))
}
if vmess.TransportConfig.Host != "" {
uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host))
}
}
return strings.Join(uriConfig, ",") + "\r\n"
}

View File

@ -1,33 +0,0 @@
package surfboard
import (
"testing"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func createVMess() proxy.Proxy {
return proxy.Proxy{
Name: "Vmess",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "vmess",
Option: proxy.Vmess{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "test.xx.com",
},
Security: "none",
},
}
}
func TestVMess(t *testing.T) {
node := createVMess()
uuid := "123456"
p := buildVMess(node, uuid)
t.Log(p)
}

View File

@ -1,79 +0,0 @@
#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true
[General]
loglevel = notify
external-controller-access = purinio@0.0.0.0:6170
exclude-simple-hostnames = true
show-error-page-for-reject = true
udp-priority = true
udp-policy-not-supported-behaviour = reject
ipv6 = true
ipv6-vif = auto
proxy-test-url = http://www.gstatic.com/generate_204
internet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204
test-timeout = 5
dns-server = system, 119.29.29.29, 223.5.5.5
hijack-dns = 8.8.8.8:53, 8.8.4.4:53, 1.1.1.1:53, 1.0.0.1:53
skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local
always-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com
# > Surge Mac Parameters
http-listen = 0.0.0.0:6088
socks5-listen = 0.0.0.0:6089
# > Surge iOS Parameters
allow-wifi-access = true
allow-hotspot-access = true
wifi-access-http-port = 6088
wifi-access-socks5-port = 6089
[Panel]
SubscribeInfo = {{.SubscribeInfo}}, style=info
[Proxy]
{{.Proxies}}
[Proxy Group]
🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes
🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes
🐠 Final = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes
🌏 Auto = smart, include-other-group=🇺🇳 Nodes
🎯 Direct = select, DIRECT, hidden=1
🇺🇳 Nodes = select, {{.ProxyGroup}}, hidden=1
[Rule]
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China
RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct
GEOIP, CN, 🇨🇳 China
FINAL, 🐠 Final, dns-failed
[URL Rewrite]
^https?:\/\/(www.)?g\.cn https://www.google.com 302
^https?:\/\/(www.)?google\.cn https://www.google.com 302

View File

@ -1,43 +0,0 @@
package surge
import (
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildHysteria2(data proxy.Proxy, uuid string) string {
hysteria2, ok := data.Option.(proxy.Hysteria2)
if !ok {
return ""
}
var port int
if hysteria2.HopPorts != "" {
ports := strings.Split(hysteria2.HopPorts, ",")
p := ports[0]
if len(strings.Split(p, "-")) > 1 {
p = strings.Split(p, "-")[0]
}
port, _ = strconv.Atoi(p)
} else {
port = data.Port
}
config := []string{
fmt.Sprintf("%s=hysteria2,%s,%d", data.Name, data.Server, port),
"password=" + uuid,
"udp-relay=true",
}
if hysteria2.SecurityConfig.SNI != "" {
config = append(config, "sni="+hysteria2.SecurityConfig.SNI)
}
if hysteria2.SecurityConfig.AllowInsecure {
config = append(config, "skip-cert-verify=true")
} else {
config = append(config, "skip-cert-verify=false")
}
return strings.Join(config, ",") + "\r\n"
}

View File

@ -1,70 +0,0 @@
package surge
import (
"testing"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func TestBuildHysteria2(t *testing.T) {
tests := []struct {
name string
data proxy.Proxy
uuid string
expected string
}{
{
name: "Valid Hysteria2 with HopPorts",
data: proxy.Proxy{
Name: "test",
Server: "server.com",
Port: 443,
Option: proxy.Hysteria2{
HopPorts: "1000-2000",
SecurityConfig: proxy.SecurityConfig{
SNI: "example.com",
AllowInsecure: true,
},
},
},
uuid: "test-uuid",
expected: "test=hysteria2,server.com,1000,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=true\r\n",
},
{
name: "Valid Hysteria2 without HopPorts",
data: proxy.Proxy{
Name: "test",
Server: "server.com",
Port: 443,
Option: proxy.Hysteria2{
SecurityConfig: proxy.SecurityConfig{
SNI: "example.com",
AllowInsecure: false,
},
},
},
uuid: "test-uuid",
expected: "test=hysteria2,server.com,443,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=false\r\n",
},
{
name: "Invalid Hysteria2 Option",
data: proxy.Proxy{
Name: "test",
Server: "server.com",
Port: 443,
Option: nil,
},
uuid: "test-uuid",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildHysteria2(tt.data, tt.uuid)
if result != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, result)
}
})
}
}

View File

@ -1,32 +0,0 @@
package surge
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildShadowsocks(data proxy.Proxy, uuid string) string {
ss, ok := data.Option.(proxy.Shadowsocks)
if !ok {
return ""
}
password := uuid
// SIP022 AEAD-2022 Ciphers
if strings.Contains(ss.Method, "2022") {
serverKey, userKey := proxy.GenerateShadowsocks2022Password(ss, uuid)
password = fmt.Sprintf("%s:%s", serverKey, userKey)
}
addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port)
config := []string{
addr,
fmt.Sprintf("encrypt-method=%s", ss.Method),
fmt.Sprintf("password=%s", password),
"tfo=true",
"udp-relay=true",
}
return strings.Join(config, ",") + "\r\n"
}

View File

@ -1,101 +0,0 @@
package surge
import (
"bytes"
"embed"
"fmt"
"strings"
"text/template"
"time"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/traffic"
)
//go:embed *.tpl
var configFiles embed.FS
type UserInfo struct {
UUID string
Upload int64
Download int64
TotalTraffic int64
ExpiredDate time.Time
SubscribeURL string
}
type Surge struct {
Adapter proxy.Adapter
UUID string
User UserInfo
}
func NewSurge(adapter proxy.Adapter) *Surge {
return &Surge{
Adapter: adapter,
}
}
func (m *Surge) Build(siteName string, user UserInfo) []byte {
var proxies, proxyGroup string
var removed []string
var ps []string
for _, p := range m.Adapter.Proxies {
switch p.Protocol {
case "shadowsocks":
proxies += buildShadowsocks(p, user.UUID)
case "trojan":
proxies += buildTrojan(p, user.UUID)
case "hysteria2":
proxies += buildHysteria2(p, user.UUID)
case "vmess":
proxies += buildVMess(p, user.UUID)
default:
removed = append(removed, p.Name)
}
ps = append(ps, p.Name)
}
file, err := configFiles.ReadFile("default.tpl")
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
// replace template
tpl, err := template.New("default").Parse(string(file))
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
var buf bytes.Buffer
var expiredAt string
if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) {
expiredAt = "长期有效"
} else {
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
}
ps = tool.RemoveStringElement(ps, removed...)
proxyGroup = strings.Join(ps, ",")
// convert traffic
upload := traffic.AutoConvert(user.Upload, false)
download := traffic.AutoConvert(user.Download, false)
total := traffic.AutoConvert(user.TotalTraffic, false)
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
// query Host
if err := tpl.Execute(&buf, map[string]interface{}{
"Proxies": proxies,
"ProxyGroup": proxyGroup,
"SubscribeURL": user.SubscribeURL,
"SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量%s\\n剩余流量: %s\\n套餐流量%s\\n到期时间%s", siteName, upload, download, unusedTraffic, total, expiredAt),
}); err != nil {
logger.Errorf("build Surge config error: %v", err.Error())
return nil
}
return buf.Bytes()
}

View File

@ -1,97 +0,0 @@
package surge
import (
"strings"
"testing"
"time"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func TestSurgeBuild(t *testing.T) {
adapter := proxy.Adapter{
Proxies: []proxy.Proxy{
{
Name: "test-shadowsocks",
Protocol: "shadowsocks",
Server: "1.2.3.4",
Port: 8388,
Option: proxy.Shadowsocks{
Method: "aes-256-gcm",
},
},
{
Name: "test-trojan",
Protocol: "trojan",
Server: "5.6.7.8",
Port: 443,
Option: proxy.Trojan{
SecurityConfig: proxy.SecurityConfig{
SNI: "example.com",
AllowInsecure: true,
},
},
},
{
Name: "test-hysteria",
Protocol: "hysteria2",
Server: "1.1.1.1",
Port: 443,
Option: proxy.Hysteria2{
HopPorts: "8080-8090",
HopInterval: 320,
SecurityConfig: proxy.SecurityConfig{
SNI: "example.com",
AllowInsecure: true,
},
},
},
},
Group: []proxy.Group{
{
Name: "test-group",
Type: proxy.GroupTypeSelect,
Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"},
},
{
Name: "手动选择",
Type: proxy.GroupTypeSelect,
Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"},
},
},
Rules: []string{
"DOMAIN-SUFFIX,example.com,DIRECT",
},
}
user := UserInfo{
UUID: "test-uuid",
Upload: 1024,
Download: 2048,
TotalTraffic: 4096,
ExpiredDate: time.Now().Add(24 * time.Hour),
SubscribeURL: "http://example.com/subscribe",
}
surge := NewSurge(adapter)
config := surge.Build("test-uuid", "TestSite", user)
if config == nil {
t.Fatal("Expected non-nil config")
}
configStr := string(config)
t.Logf("configStr: %v", configStr)
if !strings.Contains(configStr, "test-shadowsocks=ss") {
t.Errorf("Expected config to contain test-shadowsocks proxy")
}
if !strings.Contains(configStr, "test-trojan=trojan") {
t.Errorf("Expected config to contain test-trojan proxy")
}
if !strings.Contains(configStr, "test-group = select") {
t.Errorf("Expected config to contain test-group proxy group")
}
if !strings.Contains(configStr, "DOMAIN-SUFFIX,example.com,DIRECT") {
t.Errorf("Expected config to contain rule for example.com")
}
}

View File

@ -1,32 +0,0 @@
package surge
import (
"strconv"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildTrojan(data proxy.Proxy, uuid string) string {
trojan, ok := data.Option.(proxy.Trojan)
if !ok {
return ""
}
config := []string{
data.Name + "=trojan",
data.Server,
strconv.Itoa(data.Port),
"password=" + uuid,
"tfo=true",
"udp-relay=true",
}
if trojan.SecurityConfig.SNI != "" {
config = append(config, "sni="+trojan.SecurityConfig.SNI)
}
if trojan.SecurityConfig.AllowInsecure {
config = append(config, "skip-cert-verify=true")
} else {
config = append(config, "skip-cert-verify=false")
}
return strings.Join(config, ",") + "\r\n"
}

View File

@ -1,44 +0,0 @@
package surge
import (
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
func buildVMess(data proxy.Proxy, uuid string) string {
vmess, ok := data.Option.(proxy.Vmess)
if !ok {
return ""
}
addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port)
uriConfig := []string{
addr,
fmt.Sprintf("username=%s", uuid),
"vmess-aead=true",
"tfo=true",
"udp-relay=true",
}
if vmess.Security == "tls" {
uriConfig = append(uriConfig, "tls=true")
if vmess.SecurityConfig.AllowInsecure {
uriConfig = append(uriConfig, "skip-cert-verify=true")
} else {
uriConfig = append(uriConfig, "skip-cert-verify=false")
}
if vmess.SecurityConfig.SNI != "" {
uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI))
}
}
if vmess.Transport == "websocket" {
uriConfig = append(uriConfig, "ws=true")
if vmess.TransportConfig.Path != "" {
uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path))
}
if vmess.TransportConfig.Host != "" {
uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host))
}
}
return strings.Join(uriConfig, ",") + "\r\n"
}

View File

@ -1,51 +0,0 @@
mode: rule
ipv6: true
allow-lan: true
bind-address: "*"
mixed-port: 7890
log-level: error
unified-delay: true
tcp-concurrent: true
external-controller: 0.0.0.0:9090
tun:
enable: true
stack: system
auto-route: true
dns:
enable: true
cache-algorithm: arc
listen: 0.0.0.0:1053
ipv6: true
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- "*.lan"
- "lens.l.google.com"
- "*.srv.nintendo.net"
- "*.stun.playstation.net"
- "xbox.*.*.microsoft.com"
- "*.xboxlive.com"
- "*.msftncsi.com"
- "*.msftconnecttest.com"
default-nameserver:
- 119.29.29.29
- 223.5.5.5
nameserver:
- system
- 119.29.29.29
- 223.5.5.5
fallback:
- 8.8.8.8
- 1.1.1.1
fallback-filter:
geoip: true
geoip-code: CN
proxies:
{{.Proxies | toYaml | indent 2}}
proxy-groups:
{{.ProxyGroups | toYaml | indent 2}}
rules:
{{.Rules | toYaml | indent 2}}

View File

@ -1,299 +0,0 @@
package adapter
import (
"encoding/json"
"log"
"strings"
"github.com/perfect-panel/server/internal/model/server"
"github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/random"
"github.com/perfect-panel/server/pkg/tool"
)
// addNode creates a new proxy node based on the provided server data and host/port.
func addNode(data *server.Server, host string, port int) *proxy.Proxy {
var option any
tags := strings.Split(data.Tags, ",")
if len(tags) > 0 {
tags = tool.RemoveDuplicateElements(tags...)
}
node := proxy.Proxy{
Name: data.Name,
Server: host,
Port: port,
Country: data.Country,
Protocol: data.Protocol,
Tags: tags,
}
switch data.Protocol {
case "shadowsocks":
var ss proxy.Shadowsocks
if err := json.Unmarshal([]byte(data.Config), &ss); err != nil {
return nil
}
if port == 0 {
node.Port = ss.Port
}
option = ss
case "vless":
var vless proxy.Vless
if err := json.Unmarshal([]byte(data.Config), &vless); err != nil {
return nil
}
if port == 0 {
node.Port = vless.Port
}
option = vless
case "vmess":
var vmess proxy.Vmess
if err := json.Unmarshal([]byte(data.Config), &vmess); err != nil {
return nil
}
if port == 0 {
node.Port = vmess.Port
}
option = vmess
case "trojan":
var trojan proxy.Trojan
if err := json.Unmarshal([]byte(data.Config), &trojan); err != nil {
return nil
}
if port == 0 {
node.Port = trojan.Port
}
option = trojan
case "hysteria2":
var hysteria2 proxy.Hysteria2
if err := json.Unmarshal([]byte(data.Config), &hysteria2); err != nil {
return nil
}
if port == 0 {
node.Port = hysteria2.Port
}
option = hysteria2
case "tuic":
var tuic proxy.Tuic
if err := json.Unmarshal([]byte(data.Config), &tuic); err != nil {
return nil
}
if port == 0 {
node.Port = tuic.Port
}
option = tuic
default:
return nil
}
node.Option = option
return &node
}
func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string, defaultGroup string) {
for _, group := range groups {
if group.Default {
log.Printf("[Debug] 规则组 %s 是默认组", group.Name)
defaultGroup = group.Name
}
switch group.Type {
case server.RuleGroupTypeReject:
proxyGroup = append(proxyGroup, proxy.Group{
Name: group.Name,
Type: proxy.GroupTypeSelect,
Proxies: []string{"REJECT", "DIRECT", AutoSelect},
Reject: true,
})
case server.RuleGroupTypeDirect:
proxyGroup = append(proxyGroup, proxy.Group{
Name: group.Name,
Type: proxy.GroupTypeSelect,
Proxies: []string{"DIRECT", AutoSelect},
Direct: true,
})
default:
proxyGroup = append(proxyGroup, proxy.Group{
Name: group.Name,
Type: proxy.GroupTypeSelect,
Proxies: []string{},
Tags: RemoveEmptyString(strings.Split(group.Tags, ",")),
Default: group.Default,
})
}
rules = append(rules, strings.Split(group.Rules, "\n")...)
}
log.Printf("[Dapter] 生成规则组: %d", len(proxyGroup))
return proxyGroup, tool.RemoveDuplicateElements(rules...), defaultGroup
}
// generateDefaultGroup generates a default proxy group with auto-selection and manual selection options.
func generateDefaultGroup() (proxyGroup []proxy.Group) {
proxyGroup = append(proxyGroup, proxy.Group{
Name: AutoSelect,
Type: proxy.GroupTypeURLTest,
Proxies: make([]string, 0),
URL: "https://www.gstatic.com/generate_204",
Interval: 300,
})
return proxyGroup
}
func adapterProxies(servers []*server.Server) ([]proxy.Proxy, []string, map[string][]string) {
var proxies []proxy.Proxy
var tags = make(map[string][]string)
for _, node := range servers {
switch node.RelayMode {
case server.RelayModeAll:
var relays []server.NodeRelay
if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil {
logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode))
continue
}
for _, relay := range relays {
n := addNode(node, relay.Host, relay.Port)
if n == nil {
continue
}
if relay.Prefix != "" {
n.Name = relay.Prefix + n.Name
}
if node.Tags != "" {
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
for _, tag := range t {
if tag != "" {
if _, ok := tags[tag]; !ok {
tags[tag] = []string{}
}
tags[tag] = append(tags[tag], n.Name)
}
}
}
proxies = append(proxies, *n)
}
case server.RelayModeRandom:
var relays []server.NodeRelay
if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil {
logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode))
continue
}
randNum := random.RandomInRange(0, len(relays)-1)
relay := relays[randNum]
n := addNode(node, relay.Host, relay.Port)
if n == nil {
continue
}
if relay.Prefix != "" {
n.Name = relay.Prefix + node.Name
}
if node.Tags != "" {
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
for _, tag := range t {
if tag != "" {
if _, ok := tags[tag]; !ok {
tags[tag] = []string{}
}
tags[tag] = append(tags[tag], n.Name)
}
}
}
proxies = append(proxies, *n)
default:
logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode))
n := addNode(node, node.ServerAddr, 0)
if n != nil {
if node.Tags != "" {
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
for _, tag := range t {
if tag != "" {
if _, ok := tags[tag]; !ok {
tags[tag] = []string{}
}
tags[tag] = append(tags[tag], n.Name)
}
}
}
proxies = append(proxies, *n)
}
}
}
var nodes []string
for _, p := range proxies {
nodes = append(nodes, p.Name)
}
return proxies, tool.RemoveDuplicateElements(nodes...), tags
}
// RemoveEmptyString 切片去除空值
func RemoveEmptyString(arr []string) []string {
var result []string
for _, str := range arr {
if str != "" {
result = append(result, str)
}
}
return result
}
// SortGroups sorts the provided slice of proxy groups by their names.
func SortGroups(groups []proxy.Group, nodes []string, tags map[string][]string, defaultName string) []proxy.Group {
var sortedGroups []proxy.Group
var defaultGroup, autoSelectGroup proxy.Group
// 在所有分组找到默认分组并将他放到第一个
for _, group := range groups {
if group.Name == "" || group.Name == "DIRECT" || group.Name == "REJECT" {
continue
}
// 如果是默认分组
if group.Default {
group.Proxies = append([]string{AutoSelect}, nodes...)
group.Proxies = append(group.Proxies, "DIRECT")
defaultGroup = group
continue
}
if group.Reject || group.Direct {
if defaultName != AutoSelect {
group.Proxies = append(group.Proxies, defaultName)
}
sortedGroups = append(sortedGroups, group)
continue
}
if group.Name == AutoSelect {
group.Proxies = nodes
autoSelectGroup = group
continue
}
// Tags 分组
if len(group.Tags) > 0 {
var proxies []string
for _, tag := range group.Tags {
if node, ok := tags[tag]; ok {
proxies = append(proxies, node...)
}
}
group.Proxies = append(tool.RemoveDuplicateElements(proxies...), AutoSelect, "DIRECT")
sortedGroups = append(sortedGroups, group)
continue
}
group.Proxies = append([]string{AutoSelect}, nodes...)
group.Proxies = append(group.Proxies, "DIRECT")
group.Proxies = tool.RemoveElementBySlice(group.Proxies, group.Name)
sortedGroups = append(sortedGroups, group)
}
if defaultGroup.Name != "" {
sortedGroups = append([]proxy.Group{defaultGroup}, sortedGroups...)
}
if autoSelectGroup.Name != "" && autoSelectGroup.Name != defaultGroup.Name {
sortedGroups = append(sortedGroups, autoSelectGroup)
}
return sortedGroups
}

View File

@ -1,40 +0,0 @@
package v2rayn
import (
"github.com/perfect-panel/server/pkg/adapter/general"
"github.com/perfect-panel/server/pkg/adapter/proxy"
)
type v2rayShareLink struct {
Ps string `json:"ps"`
Add string `json:"add"`
Port string `json:"port"`
ID string `json:"id"`
Aid string `json:"aid"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
SNI string `json:"sni"`
Path string `json:"path"`
TLS string `json:"tls"`
Flow string `json:"flow,omitempty"`
Alpn string `json:"alpn,omitempty"`
AllowInsecure bool `json:"allowInsecure,omitempty"`
Fingerprint string `json:"fp,omitempty"`
PublicKey string `json:"pbk,omitempty"`
ShortId string `json:"sid,omitempty"`
SpiderX string `json:"spx,omitempty"`
V string `json:"v"`
}
type V2rayN struct {
proxy.Adapter
}
func NewV2rayN(adapter proxy.Adapter) *V2rayN {
return &V2rayN{
Adapter: adapter,
}
}
func (m *V2rayN) Build(uuid string) []byte {
return general.GenerateBase64General(m.Adapter.Proxies, uuid)
}