diff --git a/common/net/tls.go b/common/net/tls.go new file mode 100644 index 00000000..65391666 --- /dev/null +++ b/common/net/tls.go @@ -0,0 +1,18 @@ +package net + +import ( + "crypto/tls" + "fmt" +) +func ParseCert(certificate,privateKey string) (tls.Certificate, error) { + cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) + if painTextErr == nil { + return cert, nil + } + + cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey) + if loadErr != nil { + return tls.Certificate{}, fmt.Errorf("parse certificate failed,maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) + } + return cert, nil +} \ No newline at end of file diff --git a/config/config.go b/config/config.go index 39b75e54..1d80a3a5 100644 --- a/config/config.go +++ b/config/config.go @@ -77,12 +77,10 @@ type Inbound struct { // Controller config type Controller struct { - ExternalController string `json:"-"` - ExternalUI string `json:"-"` - Secret string `json:"-"` - TLSPort int `json:"-"` - Cert string `json:"-"` - PrivateKey string `json:"-"` + ExternalController string `json:"-"` + ExternalControllerTLS string `json:"-"` + ExternalUI string `json:"-"` + Secret string `json:"-"` } // DNS config @@ -130,6 +128,11 @@ type TuicServer struct { MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` } +type TLS struct { + Certificate string `yaml:"certificate"` + PrivateKey string `yaml:"private-key"` +} + func (t TuicServer) String() string { b, _ := json.Marshal(t) return string(b) @@ -230,12 +233,10 @@ type Sniffer struct { ParsePureIp bool } + // Experimental config type Experimental struct { Fingerprints []string `yaml:"fingerprints"` - TLSPort int `yaml:"tls-port,omitempty"` - Cert string `yaml:"cert,omitempty"` - PrivateKey string `yaml:"private-key,omitempty"` } // Config is clash config manager @@ -254,6 +255,7 @@ type Config struct { RuleProviders map[string]providerTypes.RuleProvider Tunnels []Tunnel Sniffer *Sniffer + TLS *TLS } type RawDNS struct { @@ -381,31 +383,32 @@ func (t *Tunnel) UnmarshalYAML(unmarshal func(any) error) error { } type RawConfig struct { - Port int `yaml:"port"` - SocksPort int `yaml:"socks-port"` - RedirPort int `yaml:"redir-port"` - TProxyPort int `yaml:"tproxy-port"` - MixedPort int `yaml:"mixed-port"` - ShadowSocksConfig string `yaml:"ss-config"` - VmessConfig string `yaml:"vmess-config"` - InboundTfo bool `yaml:"inbound-tfo"` - Authentication []string `yaml:"authentication"` - AllowLan bool `yaml:"allow-lan"` - BindAddress string `yaml:"bind-address"` - Mode T.TunnelMode `yaml:"mode"` - UnifiedDelay bool `yaml:"unified-delay"` - LogLevel log.LogLevel `yaml:"log-level"` - IPv6 bool `yaml:"ipv6"` - ExternalController string `yaml:"external-controller"` - ExternalUI string `yaml:"external-ui"` - Secret string `yaml:"secret"` - Interface string `yaml:"interface-name"` - RoutingMark int `yaml:"routing-mark"` - Tunnels []Tunnel `yaml:"tunnels"` - GeodataMode bool `yaml:"geodata-mode"` - GeodataLoader string `yaml:"geodata-loader"` - TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` - EnableProcess bool `yaml:"enable-process" json:"enable-process"` + Port int `yaml:"port"` + SocksPort int `yaml:"socks-port"` + RedirPort int `yaml:"redir-port"` + TProxyPort int `yaml:"tproxy-port"` + MixedPort int `yaml:"mixed-port"` + ShadowSocksConfig string `yaml:"ss-config"` + VmessConfig string `yaml:"vmess-config"` + InboundTfo bool `yaml:"inbound-tfo"` + Authentication []string `yaml:"authentication"` + AllowLan bool `yaml:"allow-lan"` + BindAddress string `yaml:"bind-address"` + Mode T.TunnelMode `yaml:"mode"` + UnifiedDelay bool `yaml:"unified-delay"` + LogLevel log.LogLevel `yaml:"log-level"` + IPv6 bool `yaml:"ipv6"` + ExternalController string `yaml:"external-controller"` + ExternalControllerTLS string `yaml:"external-controller-tls"` + ExternalUI string `yaml:"external-ui"` + Secret string `yaml:"secret"` + Interface string `yaml:"interface-name"` + RoutingMark int `yaml:"routing-mark"` + Tunnels []Tunnel `yaml:"tunnels"` + GeodataMode bool `yaml:"geodata-mode"` + GeodataLoader string `yaml:"geodata-loader"` + TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` + EnableProcess bool `yaml:"enable-process" json:"enable-process"` Sniffer RawSniffer `yaml:"sniffer"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` @@ -423,6 +426,7 @@ type RawConfig struct { ProxyGroup []map[string]any `yaml:"proxy-groups"` Rule []string `yaml:"rules"` SubRules map[string][]string `yaml:"sub-rules"` + RawTLS TLS `yaml:"tls"` } type RawGeoXUrl struct { @@ -572,6 +576,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config.Experimental = &rawCfg.Experimental config.Profile = &rawCfg.Profile config.IPTables = &rawCfg.IPTables + config.TLS=&rawCfg.RawTLS general, err := parseGeneral(rawCfg) if err != nil { @@ -640,6 +645,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { return nil, err } + elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm @@ -652,7 +658,6 @@ func parseGeneral(cfg *RawConfig) (*General, error) { // checkout externalUI exist if externalUI != "" { externalUI = C.Path.Resolve(externalUI) - if _, err := os.Stat(externalUI); os.IsNotExist(err) { return nil, fmt.Errorf("external-ui: %s not exist", externalUI) } @@ -675,9 +680,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { ExternalController: cfg.ExternalController, ExternalUI: cfg.ExternalUI, Secret: cfg.Secret, - TLSPort: cfg.Experimental.TLSPort, - Cert: cfg.Experimental.Cert, - PrivateKey: cfg.Experimental.PrivateKey, + ExternalControllerTLS: cfg.ExternalControllerTLS, }, UnifiedDelay: cfg.UnifiedDelay, Mode: cfg.Mode, diff --git a/docs/config.yaml b/docs/config.yaml index 78e9f025..e4bb5427 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -16,7 +16,7 @@ log-level: debug # 日志等级 silent/error/warning/info/debug ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录 external-controller: 0.0.0.0:9093 # RESTful API 监听地址 - +external-controller-tls: 0.0.0.0:9443 # RESTful API HTTPS 监听地址,需要配置 tls 部分配置文件 # secret: "123456" # `Authorization: Bearer ${secret}` # tcp-concurrent: true # TCP并发连接所有IP, 将使用最快握手的TCP @@ -51,29 +51,29 @@ tun: - 0.0.0.0/1 - 128.0.0.0/1 inet6_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 - - '::/1' - - '8000::/1' + - "::/1" + - "8000::/1" # endpoint_independent_nat: false # 启用独立于端点的 NAT # include_uid: # UID 规则仅在 Linux 下被支持,并且需要 auto_route - # - 0 + # - 0 # include_uid_range: # 限制被路由的的用户范围 - # - 1000-99999 + # - 1000-99999 # exclude_uid: # 排除路由的的用户 - #- 1000 + #- 1000 # exclude_uid_range: # 排除路由的的用户范围 - # - 1000-99999 - + # - 1000-99999 + # Android 用户和应用规则仅在 Android 下被支持 # 并且需要 auto_route - + # include_android_user: # 限制被路由的 Android 用户 - # - 0 - # - 10 + # - 0 + # - 10 # include_package: # 限制被路由的 Android 应用包名 - # - com.android.chrome + # - com.android.chrome # exclude_package: # 排除被路由的 Android 应用包名 - # - com.android.captiveportallogin - + # - com.android.captiveportallogin + #ebpf配置 ebpf: auto-redir: # redirect 模式,仅支持 TCP @@ -219,7 +219,8 @@ proxies: server: server port: 443 cipher: chacha20-ietf-poly1305 - password: "password" + password: + "password" # udp: true # udp-over-tcp: false # ip-version: ipv4 # 设置节点使用 IP 版本,可选:dual,ipv4,ipv6,ipv4-prefer,ipv6-prefer。默认使用 dual @@ -449,7 +450,7 @@ proxies: path: "/" headers: Host: example.com - + #hysteria - name: "hysteria" type: hysteria @@ -468,7 +469,7 @@ proxies: # recv_window_conn: 12582912 # 将会在未来某个时候删除 # recv-window-conn: 12582912 # recv_window: 52428800 # 将会在未来某个时候删除 - # recv-window: 52428800 + # recv-window: 52428800 # ca: "./my.ca" # ca_str: "xyz" # 将会在未来某个时候删除 # ca-str: "xyz" @@ -657,3 +658,7 @@ sub-rules: - IP-CIDR,1.1.1.1/32,REJECT - IP-CIDR,8.8.8.8/32,ss1 - DOMAIN,dns.alidns.com,REJECT + +tls: + certificate: string # 证书 PEM 格式,或者 证书的路径 + private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 diff --git a/hub/hub.go b/hub/hub.go index 216a0c68..ee18e70a 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -1,8 +1,6 @@ package hub import ( - "errors" - "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/hub/executor" "github.com/Dreamacro/clash/hub/route" @@ -44,12 +42,8 @@ func Parse(options ...Option) error { } if cfg.General.ExternalController != "" { - if cfg.General.TLSPort != 0 && (len(cfg.General.PrivateKey) == 0 || len(cfg.General.Cert) == 0) { - return errors.New("Must be provided certificates and keys, for tls controller") - } - - go route.Start(cfg.General.ExternalController, cfg.General.Secret, cfg.General.TLSPort, - cfg.General.Cert, cfg.General.PrivateKey) + go route.Start(cfg.General.ExternalController,cfg.General.ExternalControllerTLS, + cfg.General.Secret,cfg.TLS.Certificate,cfg.TLS.PrivateKey) } executor.ApplyConfig(cfg, true) diff --git a/hub/route/server.go b/hub/route/server.go index 2b449c2d..00a33a08 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -4,15 +4,14 @@ import ( "bytes" "crypto/tls" "encoding/json" - "net" "net/http" - "strconv" "strings" "time" "github.com/Dreamacro/clash/adapter/inbound" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/log" + CN "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/tunnel/statistic" "github.com/go-chi/chi/v5" @@ -43,7 +42,8 @@ func SetUIPath(path string) { uiPath = C.Path.Resolve(path) } -func Start(addr string, secret string, tlsPort int, cert string, privateKey string) { +func Start(addr string, tlsAddr string, secret string, + certificat, privateKey string) { if serverAddr != "" { return } @@ -58,7 +58,6 @@ func Start(addr string, secret string, tlsPort int, cert string, privateKey stri AllowedHeaders: []string{"Content-Type", "Authorization"}, MaxAge: 300, }) - r.Use() r.Use(corsM.Handler) r.Group(func(r chi.Router) { r.Use(authentication) @@ -85,31 +84,31 @@ func Start(addr string, secret string, tlsPort int, cert string, privateKey stri }) }) } - if tlsPort >0 { + + if len(tlsAddr) > 0 { go func() { - if host, _, err := net.SplitHostPort(addr); err != nil { - log.Errorln("External controller tls serve error,%s", err) - } else { - l, err := inbound.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(tlsPort))) - if err != nil { - log.Errorln("External controller tls listen error: %s", err) - return - } - serverAddr = l.Addr().String() - log.Infoln("RESTful API tls listening at: %s", serverAddr) - certificate, err := tls.X509KeyPair([]byte(cert), []byte(privateKey)) - if err != nil { - log.Errorln("External controller tls sevre error,%s", err) - } - tlsServe := &http.Server{ - Handler: r, - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{certificate}, - }, - } - if err = tlsServe.ServeTLS(l, "", ""); err != nil { - log.Errorln("External controller tls serve error: %s", err) - } + c, err := CN.ParseCert(certificat, privateKey) + if err != nil { + log.Errorln("External controller tls listen error: %s", err) + return + } + + l, err := inbound.Listen("tcp", tlsAddr) + if err != nil { + log.Errorln("External controller tls listen error: %s", err) + return + } + + serverAddr = l.Addr().String() + log.Infoln("RESTful API tls listening at: %s", serverAddr) + tlsServe := &http.Server{ + Handler: r, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{c}, + }, + } + if err = tlsServe.ServeTLS(l, "", ""); err != nil { + log.Errorln("External controller tls serve error: %s", err) } }() } @@ -126,7 +125,6 @@ func Start(addr string, secret string, tlsPort int, cert string, privateKey stri log.Errorln("External controller serve error: %s", err) } - } func authentication(next http.Handler) http.Handler {