refactor(adapter): delete old adapter
This commit is contained in:
parent
766e210f06
commit
3fb23e3106
@ -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)
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
}
|
||||
@ -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:
|
||||
`
|
||||
@ -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"`
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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)))
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
@ -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
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -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
|
||||
@ -1,12 +0,0 @@
|
||||
package surfboard
|
||||
|
||||
import "time"
|
||||
|
||||
type UserInfo struct {
|
||||
UUID string
|
||||
Upload int64
|
||||
Download int64
|
||||
TotalTraffic int64
|
||||
ExpiredDate time.Time
|
||||
SubscribeURL string
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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}}
|
||||
@ -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
|
||||
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user