init: 1.0.0
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package clash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
"github.com/perfect-panel/ppanel-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 _, v := range c.Proxies {
|
||||
p, err := c.parseProxy(v, uuid)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to parse proxy for %s: %s", v.Name, err.Error())
|
||||
continue
|
||||
}
|
||||
proxies = append(proxies, *p)
|
||||
}
|
||||
var rawConfig RawConfig
|
||||
if err := yaml.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal template: %w", err)
|
||||
}
|
||||
rawConfig.Proxies = proxies
|
||||
// generate proxy groups
|
||||
var groups []ProxyGroup
|
||||
for _, group := range c.Group {
|
||||
groups = append(groups, ProxyGroup{
|
||||
Name: group.Name,
|
||||
Type: string(group.Type),
|
||||
Proxies: group.Proxies,
|
||||
Url: group.URL,
|
||||
Interval: group.Interval,
|
||||
})
|
||||
}
|
||||
rawConfig.ProxyGroups = groups
|
||||
rawConfig.Rules = append(c.Rules, "# 最终规则", "MATCH,手动选择")
|
||||
return yaml.Marshal(&rawConfig)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package clash
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/perfect-panel/ppanel-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)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package clash
|
||||
|
||||
const DefaultTemplate = `
|
||||
mixed-port: 7890
|
||||
allow-lan: true
|
||||
bind-address: "*"
|
||||
mode: rule
|
||||
log-level: info
|
||||
external-controller: 127.0.0.1:9090
|
||||
global-client-fingerprint: chrome
|
||||
unified-delay: true
|
||||
geox-url:
|
||||
mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb"
|
||||
dns:
|
||||
enable: true
|
||||
ipv6: true
|
||||
enhanced-mode: fake-ip
|
||||
fake-ip-range: 198.18.0.1/16
|
||||
use-hosts: true
|
||||
default-nameserver:
|
||||
- 120.53.53.53
|
||||
- 1.12.12.12
|
||||
nameserver:
|
||||
- https://120.53.53.53/dns-query#skip-cert-verify=true
|
||||
- tls://1.12.12.12#skip-cert-verify=true
|
||||
proxy-server-nameserver:
|
||||
- https://120.53.53.53/dns-query#skip-cert-verify=true
|
||||
- tls://1.12.12.12#skip-cert-verify=true
|
||||
|
||||
proxies:
|
||||
|
||||
proxy-groups:
|
||||
|
||||
rules:
|
||||
`
|
||||
@@ -0,0 +1,131 @@
|
||||
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"`
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package clash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/ppanel-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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
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,
|
||||
SNI: tuic.SecurityConfig.SNI,
|
||||
SkipCertVerify: tuic.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package clash
|
||||
|
||||
import "github.com/perfect-panel/ppanel-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"
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user