add config watch

This commit is contained in:
yuzuki999
2022-10-10 11:31:15 +08:00
parent 231678cc8d
commit baab76d667
32 changed files with 320 additions and 247 deletions

259
node/controller/inbound.go Normal file
View File

@@ -0,0 +1,259 @@
// Package node the InbounderConfig used by add inbound
package controller
import (
"encoding/json"
"fmt"
"github.com/Yuzuki616/V2bX/api/panel"
"github.com/Yuzuki616/V2bX/conf"
"github.com/Yuzuki616/V2bX/node/controller/legoCmd"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/uuid"
"github.com/xtls/xray-core/core"
coreConf "github.com/xtls/xray-core/infra/conf"
)
// buildInbound build Inbound config for different protocol
func buildInbound(config *conf.ControllerConfig, nodeInfo *panel.NodeInfo, tag string) (*core.InboundHandlerConfig, error) {
var proxySetting interface{}
if nodeInfo.NodeType == "V2ray" {
defer func() {
//Clear v2ray config
nodeInfo.V2ray = nil
}()
if nodeInfo.EnableVless {
//Set vless
nodeInfo.V2ray.Inbounds[0].Protocol = "vless"
if config.EnableFallback {
// Set fallback
fallbackConfigs, err := buildVlessFallbacks(config.FallBackConfigs)
if err == nil {
proxySetting = &coreConf.VLessInboundConfig{
Decryption: "none",
Fallbacks: fallbackConfigs,
}
} else {
return nil, err
}
} else {
proxySetting = &coreConf.VLessInboundConfig{
Decryption: "none",
}
}
} else {
// Set vmess
nodeInfo.V2ray.Inbounds[0].Protocol = "vmess"
proxySetting = &coreConf.VMessInboundConfig{}
}
} else if nodeInfo.NodeType == "Trojan" {
defer func() {
//clear trojan and v2ray config
nodeInfo.V2ray = nil
nodeInfo.Trojan = nil
}()
nodeInfo.V2ray = &panel.V2rayConfig{}
nodeInfo.V2ray.Inbounds = make([]coreConf.InboundDetourConfig, 1)
nodeInfo.V2ray.Inbounds[0].Protocol = "trojan"
if config.EnableFallback {
// Set fallback
fallbackConfigs, err := buildTrojanFallbacks(config.FallBackConfigs)
if err == nil {
proxySetting = &coreConf.TrojanServerConfig{
Fallbacks: fallbackConfigs,
}
} else {
return nil, err
}
} else {
proxySetting = &coreConf.TrojanServerConfig{}
}
nodeInfo.V2ray.Inbounds[0].PortList = &coreConf.PortList{
Range: []coreConf.PortRange{{From: uint32(nodeInfo.Trojan.LocalPort), To: uint32(nodeInfo.Trojan.LocalPort)}},
}
t := coreConf.TransportProtocol(nodeInfo.Trojan.TransportProtocol)
nodeInfo.V2ray.Inbounds[0].StreamSetting = &coreConf.StreamConfig{Network: &t}
} else if nodeInfo.NodeType == "Shadowsocks" {
defer func() {
//Clear v2ray config
nodeInfo.V2ray = nil
}()
nodeInfo.V2ray = &panel.V2rayConfig{}
nodeInfo.V2ray.Inbounds = []coreConf.InboundDetourConfig{{Protocol: "shadowsocks"}}
proxySetting = &coreConf.ShadowsocksServerConfig{}
randomPasswd := uuid.New()
defaultSSuser := &coreConf.ShadowsocksUserConfig{
Cipher: "aes-128-gcm",
Password: randomPasswd.String(),
}
proxySetting, _ := proxySetting.(*coreConf.ShadowsocksServerConfig)
proxySetting.Users = append(proxySetting.Users, defaultSSuser)
proxySetting.NetworkList = &coreConf.NetworkList{"tcp", "udp"}
proxySetting.IVCheck = true
if config.DisableIVCheck {
proxySetting.IVCheck = false
}
nodeInfo.V2ray.Inbounds[0].PortList = &coreConf.PortList{
Range: []coreConf.PortRange{{From: uint32(nodeInfo.SS.Port), To: uint32(nodeInfo.SS.Port)}},
}
t := coreConf.TransportProtocol(nodeInfo.SS.TransportProtocol)
nodeInfo.V2ray.Inbounds[0].StreamSetting = &coreConf.StreamConfig{Network: &t}
} else {
return nil, fmt.Errorf("unsupported node type: %s, Only support: V2ray, Trojan, Shadowsocks", nodeInfo.NodeType)
}
// Build Listen IP address
ipAddress := net.ParseAddress(config.ListenIP)
nodeInfo.V2ray.Inbounds[0].ListenOn = &coreConf.Address{Address: ipAddress}
// SniffingConfig
sniffingConfig := &coreConf.SniffingConfig{
Enabled: true,
DestOverride: &coreConf.StringList{"http", "tls"},
}
if config.DisableSniffing {
sniffingConfig.Enabled = false
}
nodeInfo.V2ray.Inbounds[0].SniffingConfig = sniffingConfig
var setting json.RawMessage
// Build Protocol and Protocol setting
setting, err := json.Marshal(proxySetting)
if err != nil {
return nil, fmt.Errorf("marshal proxy %s config fialed: %s", nodeInfo.NodeType, err)
}
if *nodeInfo.V2ray.Inbounds[0].StreamSetting.Network == "tcp" {
if nodeInfo.V2ray.Inbounds[0].StreamSetting.TCPSettings != nil {
nodeInfo.V2ray.Inbounds[0].StreamSetting.TCPSettings.AcceptProxyProtocol = config.EnableProxyProtocol
} else {
tcpSetting := &coreConf.TCPConfig{
AcceptProxyProtocol: config.EnableProxyProtocol,
} //Enable proxy protocol
nodeInfo.V2ray.Inbounds[0].StreamSetting.TCPSettings = tcpSetting
}
} else if *nodeInfo.V2ray.Inbounds[0].StreamSetting.Network == "ws" {
nodeInfo.V2ray.Inbounds[0].StreamSetting.WSSettings = &coreConf.WebSocketConfig{
AcceptProxyProtocol: config.EnableProxyProtocol} //Enable proxy protocol
}
// Build TLS and XTLS settings
if nodeInfo.EnableTls && config.CertConfig.CertMode != "none" {
nodeInfo.V2ray.Inbounds[0].StreamSetting.Security = nodeInfo.TLSType
certFile, keyFile, err := getCertFile(config.CertConfig)
if err != nil {
return nil, err
}
if nodeInfo.TLSType == "tls" {
tlsSettings := &coreConf.TLSConfig{
RejectUnknownSNI: config.CertConfig.RejectUnknownSni,
}
tlsSettings.Certs = append(tlsSettings.Certs, &coreConf.TLSCertConfig{CertFile: certFile, KeyFile: keyFile, OcspStapling: 3600})
nodeInfo.V2ray.Inbounds[0].StreamSetting.TLSSettings = tlsSettings
} else if nodeInfo.TLSType == "xtls" {
xtlsSettings := &coreConf.XTLSConfig{
RejectUnknownSNI: config.CertConfig.RejectUnknownSni,
}
xtlsSettings.Certs = append(xtlsSettings.Certs, &coreConf.XTLSCertConfig{
CertFile: certFile,
KeyFile: keyFile,
OcspStapling: 3600})
nodeInfo.V2ray.Inbounds[0].StreamSetting.XTLSSettings = xtlsSettings
}
}
// Support ProxyProtocol for any transport protocol
if *nodeInfo.V2ray.Inbounds[0].StreamSetting.Network != "tcp" &&
*nodeInfo.V2ray.Inbounds[0].StreamSetting.Network != "ws" &&
config.EnableProxyProtocol {
sockoptConfig := &coreConf.SocketConfig{
AcceptProxyProtocol: config.EnableProxyProtocol,
} //Enable proxy protocol
nodeInfo.V2ray.Inbounds[0].StreamSetting.SocketSettings = sockoptConfig
}
nodeInfo.V2ray.Inbounds[0].Settings = &setting
nodeInfo.V2ray.Inbounds[0].Tag = tag
return nodeInfo.V2ray.Inbounds[0].Build()
}
func getCertFile(certConfig *conf.CertConfig) (certFile string, keyFile string, err error) {
if certConfig.CertMode == "file" {
if certConfig.CertFile == "" || certConfig.KeyFile == "" {
return "", "", fmt.Errorf("cert file path or key file path not exist")
}
return certConfig.CertFile, certConfig.KeyFile, nil
} else if certConfig.CertMode == "dns" {
lego, err := legoCmd.New()
if err != nil {
return "", "", err
}
certPath, keyPath, err := lego.DNSCert(certConfig.CertDomain, certConfig.Email, certConfig.Provider, certConfig.DNSEnv)
if err != nil {
return "", "", err
}
return certPath, keyPath, err
} else if certConfig.CertMode == "http" {
lego, err := legoCmd.New()
if err != nil {
return "", "", err
}
certPath, keyPath, err := lego.HTTPCert(certConfig.CertDomain, certConfig.Email)
if err != nil {
return "", "", err
}
return certPath, keyPath, err
}
return "", "", fmt.Errorf("unsupported certmode: %s", certConfig.CertMode)
}
func buildVlessFallbacks(fallbackConfigs []*conf.FallBackConfig) ([]*coreConf.VLessInboundFallback, error) {
if fallbackConfigs == nil {
return nil, fmt.Errorf("you must provide FallBackConfigs")
}
vlessFallBacks := make([]*coreConf.VLessInboundFallback, len(fallbackConfigs))
for i, c := range fallbackConfigs {
if c.Dest == "" {
return nil, fmt.Errorf("dest is required for fallback fialed")
}
var dest json.RawMessage
dest, err := json.Marshal(c.Dest)
if err != nil {
return nil, fmt.Errorf("marshal dest %s config fialed: %s", dest, err)
}
vlessFallBacks[i] = &coreConf.VLessInboundFallback{
Name: c.SNI,
Alpn: c.Alpn,
Path: c.Path,
Dest: dest,
Xver: c.ProxyProtocolVer,
}
}
return vlessFallBacks, nil
}
func buildTrojanFallbacks(fallbackConfigs []*conf.FallBackConfig) ([]*coreConf.TrojanInboundFallback, error) {
if fallbackConfigs == nil {
return nil, fmt.Errorf("you must provide FallBackConfigs")
}
trojanFallBacks := make([]*coreConf.TrojanInboundFallback, len(fallbackConfigs))
for i, c := range fallbackConfigs {
if c.Dest == "" {
return nil, fmt.Errorf("dest is required for fallback fialed")
}
var dest json.RawMessage
dest, err := json.Marshal(c.Dest)
if err != nil {
return nil, fmt.Errorf("marshal dest %s config fialed: %s", dest, err)
}
trojanFallBacks[i] = &coreConf.TrojanInboundFallback{
Name: c.SNI,
Alpn: c.Alpn,
Path: c.Path,
Dest: dest,
Xver: c.ProxyProtocolVer,
}
}
return trojanFallBacks, nil
}

View File

@@ -0,0 +1,99 @@
package controller_test
import (
"github.com/Yuzuki616/V2bX/api/panel"
. "github.com/Yuzuki616/V2bX/node/controller"
"testing"
)
func TestBuildV2ray(t *testing.T) {
nodeInfo := &panel.NodeInfo{
NodeType: "V2ray",
NodeID: 1,
Port: 1145,
SpeedLimit: 0,
AlterID: 2,
TransportProtocol: "ws",
Host: "test.test.tk",
Path: "v2ray",
EnableTLS: false,
TLSType: "tls",
}
certConfig := &CertConfig{
CertMode: "http",
CertDomain: "test.test.tk",
Provider: "alidns",
Email: "test@gmail.com",
}
config := &Config{
CertConfig: certConfig,
}
_, err := buildInbound(config, nodeInfo)
if err != nil {
t.Error(err)
}
}
func TestBuildTrojan(t *testing.T) {
nodeInfo := &panel.NodeInfo{
NodeType: "Trojan",
NodeID: 1,
Port: 1145,
SpeedLimit: 0,
AlterID: 2,
TransportProtocol: "tcp",
Host: "trojan.test.tk",
Path: "v2ray",
EnableTLS: false,
TLSType: "tls",
}
DNSEnv := make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certConfig := &CertConfig{
CertMode: "dns",
CertDomain: "trojan.test.tk",
Provider: "alidns",
Email: "test@gmail.com",
DNSEnv: DNSEnv,
}
config := &Config{
CertConfig: certConfig,
}
_, err := buildInbound(config, nodeInfo)
if err != nil {
t.Error(err)
}
}
func TestBuildSS(t *testing.T) {
nodeInfo := &panel.NodeInfo{
NodeType: "Shadowsocks",
NodeID: 1,
Port: 1145,
SpeedLimit: 0,
AlterID: 2,
TransportProtocol: "tcp",
Host: "test.test.tk",
Path: "v2ray",
EnableTLS: false,
TLSType: "tls",
}
DNSEnv := make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certConfig := &CertConfig{
CertMode: "dns",
CertDomain: "trojan.test.tk",
Provider: "alidns",
Email: "test@me.com",
DNSEnv: DNSEnv,
}
config := &Config{
CertConfig: certConfig,
}
_, err := buildInbound(config, nodeInfo)
if err != nil {
t.Error(err)
}
}

View File

@@ -0,0 +1,33 @@
package cmd
import (
"crypto"
"github.com/go-acme/lego/v4/registration"
)
// Account represents a users local saved credentials.
type Account struct {
Email string `json:"email"`
Registration *registration.Resource `json:"registration"`
key crypto.PrivateKey
}
/** Implementation of the registration.User interface **/
// GetEmail returns the email address for the account.
func (a *Account) GetEmail() string {
return a.Email
}
// GetPrivateKey returns the private RSA account key.
func (a *Account) GetPrivateKey() crypto.PrivateKey {
return a.key
}
// GetRegistration returns the server registration.
func (a *Account) GetRegistration() *registration.Resource {
return a.Registration
}
/** End **/

View File

@@ -0,0 +1,242 @@
package cmd
import (
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/log"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
const (
baseAccountsRootFolderName = "accounts"
baseKeysFolderName = "keys"
accountFileName = "account.json"
)
// AccountsStorage A storage for account data.
//
// rootPath:
//
// ./.lego/accounts/
// │ └── root accounts directory
// └── "path" option
//
// rootUserPath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
//
// keysPath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/
// │ │ │ │ └── root keys directory
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
//
// accountFilePath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json
// │ │ │ │ └── account file
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
type AccountsStorage struct {
userID string
rootPath string
rootUserPath string
keysPath string
accountFilePath string
ctx *cli.Context
}
// NewAccountsStorage Creates a new AccountsStorage.
func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
// TODO: move to account struct? Currently MUST pass email.
email := getEmail(ctx)
serverURL, err := url.Parse(ctx.GlobalString("server"))
if err != nil {
log.Panic(err)
}
rootPath := filepath.Join(ctx.GlobalString("path"), baseAccountsRootFolderName)
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
accountsPath := filepath.Join(rootPath, serverPath)
rootUserPath := filepath.Join(accountsPath, email)
return &AccountsStorage{
userID: email,
rootPath: rootPath,
rootUserPath: rootUserPath,
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
accountFilePath: filepath.Join(rootUserPath, accountFileName),
ctx: ctx,
}
}
func (s *AccountsStorage) ExistsAccountFilePath() bool {
accountFile := filepath.Join(s.rootUserPath, accountFileName)
if _, err := os.Stat(accountFile); os.IsNotExist(err) {
return false
} else if err != nil {
log.Panic(err)
}
return true
}
func (s *AccountsStorage) GetRootPath() string {
return s.rootPath
}
func (s *AccountsStorage) GetRootUserPath() string {
return s.rootUserPath
}
func (s *AccountsStorage) GetUserID() string {
return s.userID
}
func (s *AccountsStorage) Save(account *Account) error {
jsonBytes, err := json.MarshalIndent(account, "", "\t")
if err != nil {
return err
}
return ioutil.WriteFile(s.accountFilePath, jsonBytes, filePerm)
}
func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {
fileBytes, err := ioutil.ReadFile(s.accountFilePath)
if err != nil {
log.Panicf("Could not load file for account %s: %v", s.userID, err)
}
var account Account
err = json.Unmarshal(fileBytes, &account)
if err != nil {
log.Panicf("Could not parse file for account %s: %v", s.userID, err)
}
account.key = privateKey
if account.Registration == nil || account.Registration.Body.Status == "" {
reg, err := tryRecoverRegistration(s.ctx, privateKey)
if err != nil {
log.Panicf("Could not load account for %s. Registration is nil: %#v", s.userID, err)
}
account.Registration = reg
err = s.Save(&account)
if err != nil {
log.Panicf("Could not save account for %s. Registration is nil: %#v", s.userID, err)
}
}
return &account
}
func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey {
accKeyPath := filepath.Join(s.keysPath, s.userID+".key")
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType)
s.createKeysFolder()
privateKey, err := generatePrivateKey(accKeyPath, keyType)
if err != nil {
log.Panicf("Could not generate RSA private account key for account %s: %v", s.userID, err)
}
log.Printf("Saved key to %s", accKeyPath)
return privateKey
}
privateKey, err := loadPrivateKey(accKeyPath)
if err != nil {
log.Panicf("Could not load RSA private key from file %s: %v", accKeyPath, err)
}
return privateKey
}
func (s *AccountsStorage) createKeysFolder() {
if err := createNonExistingFolder(s.keysPath); err != nil {
log.Panicf("Could not check/create directory for account %s: %v", s.userID, err)
}
}
func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) {
privateKey, err := certcrypto.GeneratePrivateKey(keyType)
if err != nil {
return nil, err
}
certOut, err := os.Create(file)
if err != nil {
return nil, err
}
defer certOut.Close()
pemKey := certcrypto.PEMBlock(privateKey)
err = pem.Encode(certOut, pemKey)
if err != nil {
return nil, err
}
return privateKey, nil
}
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode(keyBytes)
switch keyBlock.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(keyBlock.Bytes)
}
return nil, errors.New("unknown private key type")
}
func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {
// couldn't load account but got a key. Try to look the account up.
config := lego.NewConfig(&Account{key: privateKey})
config.CADirURL = ctx.GlobalString("server")
config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version)
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
reg, err := client.Registration.ResolveAccountByKey()
if err != nil {
return nil, err
}
return reg, nil
}

View File

@@ -0,0 +1,204 @@
package cmd
import (
"bytes"
"crypto/x509"
"encoding/json"
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/log"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/urfave/cli"
"golang.org/x/net/idna"
)
const (
baseCertificatesFolderName = "certificates"
baseArchivesFolderName = "archives"
)
// CertificatesStorage a certificates storage.
//
// rootPath:
//
// ./.lego/certificates/
// │ └── root certificates directory
// └── "path" option
//
// archivePath:
//
// ./.lego/archives/
// │ └── archived certificates directory
// └── "path" option
type CertificatesStorage struct {
rootPath string
archivePath string
pem bool
filename string // Deprecated
}
// NewCertificatesStorage create a new certificates storage.
func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage {
return &CertificatesStorage{
rootPath: filepath.Join(ctx.GlobalString("path"), baseCertificatesFolderName),
archivePath: filepath.Join(ctx.GlobalString("path"), baseArchivesFolderName),
pem: ctx.GlobalBool("pem"),
filename: ctx.GlobalString("filename"),
}
}
func (s *CertificatesStorage) CreateRootFolder() {
err := createNonExistingFolder(s.rootPath)
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
}
func (s *CertificatesStorage) CreateArchiveFolder() {
err := createNonExistingFolder(s.archivePath)
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
}
func (s *CertificatesStorage) GetRootPath() string {
return s.rootPath
}
func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
domain := certRes.Domain
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
err := s.WriteFile(domain, ".crt", certRes.Certificate)
if err != nil {
log.Panicf("Unable to save Certificate for domain %s\n\t%v", domain, err)
}
if certRes.IssuerCertificate != nil {
err = s.WriteFile(domain, ".issuer.crt", certRes.IssuerCertificate)
if err != nil {
log.Panicf("Unable to save IssuerCertificate for domain %s\n\t%v", domain, err)
}
}
if certRes.PrivateKey != nil {
// if we were given a CSR, we don't know the private key
err = s.WriteFile(domain, ".key", certRes.PrivateKey)
if err != nil {
log.Panicf("Unable to save PrivateKey for domain %s\n\t%v", domain, err)
}
if s.pem {
err = s.WriteFile(domain, ".pem", bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))
if err != nil {
log.Panicf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", domain, err)
}
}
} else if s.pem {
// we don't have the private key; can't write the .pem file
log.Panicf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", domain, err)
}
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
if err != nil {
log.Panicf("Unable to marshal CertResource for domain %s\n\t%v", domain, err)
}
err = s.WriteFile(domain, ".json", jsonBytes)
if err != nil {
log.Panicf("Unable to save CertResource for domain %s\n\t%v", domain, err)
}
}
func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource {
raw, err := s.ReadFile(domain, ".json")
if err != nil {
log.Panicf("Error while loading the meta data for domain %s\n\t%v", domain, err)
}
var resource certificate.Resource
if err = json.Unmarshal(raw, &resource); err != nil {
log.Panicf("Error while marshaling the meta data for domain %s\n\t%v", domain, err)
}
return resource
}
func (s *CertificatesStorage) ExistsFile(domain, extension string) bool {
filePath := s.GetFileName(domain, extension)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
} else if err != nil {
log.Panic(err)
}
return true
}
func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) {
return ioutil.ReadFile(s.GetFileName(domain, extension))
}
func (s *CertificatesStorage) GetFileName(domain, extension string) string {
filename := sanitizedDomain(domain) + extension
return filepath.Join(s.rootPath, filename)
}
func (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) {
content, err := s.ReadFile(domain, extension)
if err != nil {
return nil, err
}
// The input may be a bundle or a single certificate.
return certcrypto.ParsePEMBundle(content)
}
func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error {
var baseFileName string
if s.filename != "" {
baseFileName = s.filename
} else {
baseFileName = sanitizedDomain(domain)
}
filePath := filepath.Join(s.rootPath, baseFileName+extension)
return ioutil.WriteFile(filePath, data, filePerm)
}
func (s *CertificatesStorage) MoveToArchive(domain string) error {
matches, err := filepath.Glob(filepath.Join(s.rootPath, sanitizedDomain(domain)+".*"))
if err != nil {
return err
}
for _, oldFile := range matches {
date := strconv.FormatInt(time.Now().Unix(), 10)
filename := date + "." + filepath.Base(oldFile)
newFile := filepath.Join(s.archivePath, filename)
err = os.Rename(oldFile, newFile)
if err != nil {
return err
}
}
return nil
}
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
func sanitizedDomain(domain string) string {
safe, err := idna.ToASCII(strings.ReplaceAll(domain, "*", "_"))
if err != nil {
log.Panic(err)
}
return safe
}

View File

@@ -0,0 +1,14 @@
package cmd
import "github.com/urfave/cli"
// CreateCommands Creates all CLI commands.
func CreateCommands() []cli.Command {
return []cli.Command{
createRun(),
createRevoke(),
createRenew(),
createDNSHelp(),
createList(),
}
}

View File

@@ -0,0 +1,23 @@
package cmd
import (
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/log"
"github.com/urfave/cli"
)
func Before(ctx *cli.Context) error {
if ctx.GlobalString("path") == "" {
log.Panic("Could not determine current working directory. Please pass --path.")
}
err := createNonExistingFolder(ctx.GlobalString("path"))
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
if ctx.GlobalString("server") == "" {
log.Panic("Could not determine current working server. Please pass --server.")
}
return nil
}

View File

@@ -0,0 +1,73 @@
package cmd
import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/urfave/cli"
)
func createDNSHelp() cli.Command {
return cli.Command{
Name: "dnshelp",
Usage: "Shows additional help for the '--dns' global option",
Action: dnsHelp,
Flags: []cli.Flag{
cli.StringFlag{
Name: "code, c",
Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()),
},
},
}
}
func dnsHelp(ctx *cli.Context) error {
code := ctx.String("code")
if code == "" {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
ew := &errWriter{w: w}
ew.writeln(`Credentials for DNS providers must be passed through environment variables.`)
ew.writeln()
ew.writeln(`To display the documentation for a DNS providers:`)
ew.writeln()
ew.writeln("\t$ lego dnshelp -c code")
ew.writeln()
ew.writeln("All DNS codes:")
ew.writef("\t%s\n", allDNSCodes())
ew.writeln()
ew.writeln("More information: https://go-acme.github.io/lego/dns")
if ew.err != nil {
return ew.err
}
return w.Flush()
}
return displayDNSHelp(strings.ToLower(code))
}
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) writeln(a ...interface{}) {
if ew.err != nil {
return
}
_, ew.err = fmt.Fprintln(ew.w, a...)
}
func (ew *errWriter) writef(format string, a ...interface{}) {
if ew.err != nil {
return
}
_, ew.err = fmt.Fprintf(ew.w, format, a...)
}

View File

@@ -0,0 +1,136 @@
package cmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"path/filepath"
"strings"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/urfave/cli"
)
func createList() cli.Command {
return cli.Command{
Name: "list",
Usage: "Display certificates and accounts information.",
Action: list,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "accounts, a",
Usage: "Display accounts.",
},
cli.BoolFlag{
Name: "names, n",
Usage: "Display certificate common names only.",
},
},
}
}
func list(ctx *cli.Context) error {
if ctx.Bool("accounts") && !ctx.Bool("names") {
if err := listAccount(ctx); err != nil {
return err
}
}
return listCertificates(ctx)
}
func listCertificates(ctx *cli.Context) error {
certsStorage := NewCertificatesStorage(ctx)
matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt"))
if err != nil {
return err
}
names := ctx.Bool("names")
if len(matches) == 0 {
if !names {
fmt.Println("No certificates found.")
}
return nil
}
if !names {
fmt.Println("Found the following certs:")
}
for _, filename := range matches {
if strings.HasSuffix(filename, ".issuer.crt") {
continue
}
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
pCert, err := certcrypto.ParsePEMCertificate(data)
if err != nil {
return err
}
if names {
fmt.Println(pCert.Subject.CommonName)
} else {
fmt.Println(" Certificate Name:", pCert.Subject.CommonName)
fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", "))
fmt.Println(" Expiry Date:", pCert.NotAfter)
fmt.Println(" Certificate Path:", filename)
fmt.Println()
}
}
return nil
}
func listAccount(ctx *cli.Context) error {
// fake email, needed by NewAccountsStorage
if err := ctx.GlobalSet("email", "unknown"); err != nil {
return err
}
accountsStorage := NewAccountsStorage(ctx)
matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json"))
if err != nil {
return err
}
if len(matches) == 0 {
fmt.Println("No accounts found.")
return nil
}
fmt.Println("Found the following accounts:")
for _, filename := range matches {
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
var account Account
err = json.Unmarshal(data, &account)
if err != nil {
return err
}
uri, err := url.Parse(account.Registration.URI)
if err != nil {
return err
}
fmt.Println(" Email:", account.Email)
fmt.Println(" Server:", uri.Host)
fmt.Println(" Path:", filepath.Dir(filename))
fmt.Println()
}
return nil
}

View File

@@ -0,0 +1,225 @@
package cmd
import (
"crypto"
"crypto/x509"
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/log"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/urfave/cli"
)
const (
renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
renewEnvCertDomain = "LEGO_CERT_DOMAIN"
renewEnvCertPath = "LEGO_CERT_PATH"
renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
)
func createRenew() cli.Command {
return cli.Command{
Name: "renew",
Usage: "Renew a certificate",
Action: renew,
Before: func(ctx *cli.Context) error {
// we require either domains or csr, but not both
hasDomains := len(ctx.GlobalStringSlice("domains")) > 0
hasCsr := len(ctx.GlobalString("csr")) > 0
if hasDomains && hasCsr {
log.Panic("Please specify either --domains/-d or --csr/-c, but not both")
}
if !hasDomains && !hasCsr {
log.Panic("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
}
return nil
},
Flags: []cli.Flag{
cli.IntFlag{
Name: "days",
Value: 30,
Usage: "The number of days left on a certificate to renew it.",
},
cli.BoolFlag{
Name: "reuse-key",
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
},
cli.BoolFlag{
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
cli.BoolFlag{
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
},
cli.StringFlag{
Name: "renew-hook",
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
},
cli.StringFlag{
Name: "preferred-chain",
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.",
},
},
}
}
func renew(ctx *cli.Context) error {
account, client := setup(ctx, NewAccountsStorage(ctx))
setupChallenges(ctx, client)
if account.Registration == nil {
log.Panicf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
}
certsStorage := NewCertificatesStorage(ctx)
bundle := !ctx.Bool("no-bundle")
meta := map[string]string{renewEnvAccountEmail: account.Email}
// CSR
if ctx.GlobalIsSet("csr") {
return renewForCSR(ctx, client, certsStorage, bundle, meta)
}
// Domains
return renewForDomains(ctx, client, certsStorage, bundle, meta)
}
func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
domains := ctx.GlobalStringSlice("domains")
domain := domains[0]
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, ".crt")
if err != nil {
log.Panicf("Error while loading the certificate for domain %s\n\t%v", domain, err)
}
cert := certificates[0]
if !needRenewal(cert, domain, ctx.Int("days")) {
return nil
}
// This is just meant to be informal for the user.
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
certDomains := certcrypto.ExtractDomains(cert)
var privateKey crypto.PrivateKey
if ctx.Bool("reuse-key") {
keyBytes, errR := certsStorage.ReadFile(domain, ".key")
if errR != nil {
log.Panicf("Error while loading the private key for domain %s\n\t%v", domain, errR)
}
privateKey, errR = certcrypto.ParsePEMPrivateKey(keyBytes)
if errR != nil {
return errR
}
}
request := certificate.ObtainRequest{
Domains: merge(certDomains, domains),
Bundle: bundle,
PrivateKey: privateKey,
MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
}
certRes, err := client.Certificate.Obtain(request)
if err != nil {
log.Panic(err)
}
certsStorage.SaveResource(certRes)
meta[renewEnvCertDomain] = domain
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
return launchHook(ctx.String("renew-hook"), meta)
}
func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
csr, err := readCSRFile(ctx.GlobalString("csr"))
if err != nil {
log.Panic(err)
}
domain := csr.Subject.CommonName
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, ".crt")
if err != nil {
log.Panicf("Error while loading the certificate for domain %s\n\t%v", domain, err)
}
cert := certificates[0]
if !needRenewal(cert, domain, ctx.Int("days")) {
return nil
}
// This is just meant to be informal for the user.
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
certRes, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
CSR: csr,
Bundle: bundle,
PreferredChain: ctx.String("preferred-chain"),
})
if err != nil {
log.Panic(err)
}
certsStorage.SaveResource(certRes)
meta[renewEnvCertDomain] = domain
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
return launchHook(ctx.String("renew-hook"), meta)
}
func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool {
if x509Cert.IsCA {
log.Panicf("[%s] Certificate bundle starts with a CA certificate", domain)
}
if days >= 0 {
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
if notAfter > days {
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
domain, notAfter, days)
return false
}
}
return true
}
func merge(prevDomains, nextDomains []string) []string {
for _, next := range nextDomains {
var found bool
for _, prev := range prevDomains {
if prev == next {
found = true
break
}
}
if !found {
prevDomains = append(prevDomains, next)
}
}
return prevDomains
}

View File

@@ -0,0 +1,118 @@
package cmd
import (
"crypto/x509"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_merge(t *testing.T) {
testCases := []struct {
desc string
prevDomains []string
nextDomains []string
expected []string
}{
{
desc: "all empty",
prevDomains: []string{},
nextDomains: []string{},
expected: []string{},
},
{
desc: "next empty",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{},
expected: []string{"a", "b", "c"},
},
{
desc: "prev empty",
prevDomains: []string{},
nextDomains: []string{"a", "b", "c"},
expected: []string{"a", "b", "c"},
},
{
desc: "merge append",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{"a", "c", "d"},
expected: []string{"a", "b", "c", "d"},
},
{
desc: "merge same",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{"a", "b", "c"},
expected: []string{"a", "b", "c"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := merge(test.prevDomains, test.nextDomains)
assert.Equal(t, test.expected, actual)
})
}
}
func Test_needRenewal(t *testing.T) {
testCases := []struct {
desc string
x509Cert *x509.Certificate
days int
expected bool
}{
{
desc: "30 days, NotAfter now",
x509Cert: &x509.Certificate{
NotAfter: time.Now(),
},
days: 30,
expected: true,
},
{
desc: "30 days, NotAfter 31 days",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(31*24*time.Hour + 1*time.Second),
},
days: 30,
expected: false,
},
{
desc: "30 days, NotAfter 30 days",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: 30,
expected: true,
},
{
desc: "0 days, NotAfter 30 days: only the day of the expiration",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: 0,
expected: false,
},
{
desc: "-1 days, NotAfter 30 days: always renew",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: -1,
expected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
actual := needRenewal(test.x509Cert, "foo.com", test.days)
assert.Equal(t, test.expected, actual)
})
}
}

View File

@@ -0,0 +1,62 @@
package cmd
import (
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/log"
"github.com/urfave/cli"
)
func createRevoke() cli.Command {
return cli.Command{
Name: "revoke",
Usage: "Revoke a certificate",
Action: revoke,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "keep, k",
Usage: "Keep the certificates after the revocation instead of archiving them.",
},
},
}
}
func revoke(ctx *cli.Context) error {
acc, client := setup(ctx, NewAccountsStorage(ctx))
if acc.Registration == nil {
log.Panicf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
}
certsStorage := NewCertificatesStorage(ctx)
certsStorage.CreateRootFolder()
for _, domain := range ctx.GlobalStringSlice("domains") {
log.Printf("Trying to revoke certificate for domain %s", domain)
certBytes, err := certsStorage.ReadFile(domain, ".crt")
if err != nil {
log.Panicf("Error while revoking the certificate for domain %s\n\t%v", domain, err)
}
err = client.Certificate.Revoke(certBytes)
if err != nil {
log.Panicf("Error while revoking the certificate for domain %s\n\t%v", domain, err)
}
log.Println("Certificate was revoked.")
if ctx.Bool("keep") {
return nil
}
certsStorage.CreateArchiveFolder()
err = certsStorage.MoveToArchive(domain)
if err != nil {
return err
}
log.Println("Certificate was archived for domain:", domain)
}
return nil
}

View File

@@ -0,0 +1,186 @@
package cmd
import (
"bufio"
"fmt"
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/log"
"os"
"strings"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
func createRun() cli.Command {
return cli.Command{
Name: "run",
Usage: "Register an account, then create and install a certificate",
Before: func(ctx *cli.Context) error {
// we require either domains or csr, but not both
hasDomains := len(ctx.GlobalStringSlice("domains")) > 0
hasCsr := len(ctx.GlobalString("csr")) > 0
if hasDomains && hasCsr {
log.Panic("Please specify either --domains/-d or --csr/-c, but not both")
}
if !hasDomains && !hasCsr {
log.Panic("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
}
return nil
},
Action: run,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
cli.BoolFlag{
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
},
cli.StringFlag{
Name: "run-hook",
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
},
cli.StringFlag{
Name: "preferred-chain",
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.",
},
},
}
}
const rootPathWarningMessage = `!!!! HEADS UP !!!!
Your account credentials have been saved in your Let's Encrypt
configuration directory at "%s".
You should make a secure backup of this folder now. This
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.
`
func run(ctx *cli.Context) error {
accountsStorage := NewAccountsStorage(ctx)
account, client := setup(ctx, accountsStorage)
setupChallenges(ctx, client)
if account.Registration == nil {
reg, err := register(ctx, client)
if err != nil {
log.Panicf("Could not complete registration\n\t%v", err)
}
account.Registration = reg
if err = accountsStorage.Save(account); err != nil {
log.Panic(err)
}
fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath())
}
certsStorage := NewCertificatesStorage(ctx)
certsStorage.CreateRootFolder()
cert, err := obtainCertificate(ctx, client)
if err != nil {
// Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error.
// Due to us not returning partial certificate we can just exit here instead of at the end.
log.Panicf("Could not obtain certificates:\n\t%v", err)
}
certsStorage.SaveResource(cert)
meta := map[string]string{
renewEnvAccountEmail: account.Email,
renewEnvCertDomain: cert.Domain,
renewEnvCertPath: certsStorage.GetFileName(cert.Domain, ".crt"),
renewEnvCertKeyPath: certsStorage.GetFileName(cert.Domain, ".key"),
}
return launchHook(ctx.String("run-hook"), meta)
}
func handleTOS(ctx *cli.Context, client *lego.Client) bool {
// Check for a global accept override
if ctx.GlobalBool("accept-tos") {
return true
}
reader := bufio.NewReader(os.Stdin)
log.Printf("Please review the TOS at %s", client.GetToSURL())
for {
fmt.Println("Do you accept the TOS? Y/n")
text, err := reader.ReadString('\n')
if err != nil {
log.Panicf("Could not read from console: %v", err)
}
text = strings.Trim(text, "\r\n")
switch text {
case "", "y", "Y":
return true
case "n", "N":
return false
default:
fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.")
}
}
}
func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, error) {
accepted := handleTOS(ctx, client)
if !accepted {
log.Panic("You did not accept the TOS. Unable to proceed.")
}
if ctx.GlobalBool("eab") {
kid := ctx.GlobalString("kid")
hmacEncoded := ctx.GlobalString("hmac")
if kid == "" || hmacEncoded == "" {
log.Panicf("Requires arguments --kid and --hmac.")
}
return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: accepted,
Kid: kid,
HmacEncoded: hmacEncoded,
})
}
return client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
}
func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) {
bundle := !ctx.Bool("no-bundle")
domains := ctx.GlobalStringSlice("domains")
if len(domains) > 0 {
// obtain a certificate, generating a new private key
request := certificate.ObtainRequest{
Domains: domains,
Bundle: bundle,
MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
}
return client.Certificate.Obtain(request)
}
// read the CSR
csr, err := readCSRFile(ctx.GlobalString("csr"))
if err != nil {
return nil, err
}
// obtain a certificate for this CSR
return client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
CSR: csr,
Bundle: bundle,
PreferredChain: ctx.String("preferred-chain"),
})
}

View File

@@ -0,0 +1,120 @@
package cmd
import (
"github.com/go-acme/lego/v4/lego"
"github.com/urfave/cli"
)
func CreateFlags(defaultPath string) []cli.Flag {
return []cli.Flag{
cli.StringSliceFlag{
Name: "domains, d",
Usage: "Add a domain to the process. Can be specified multiple times.",
},
cli.StringFlag{
Name: "server, s",
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
Value: lego.LEDirectoryProduction,
},
cli.BoolFlag{
Name: "accept-tos, a",
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
},
cli.StringFlag{
Name: "email, m",
Usage: "Email used for registration and recovery contact.",
},
cli.StringFlag{
Name: "csr, c",
Usage: "Certificate signing request filename, if an external CSR is to be used.",
},
cli.BoolFlag{
Name: "eab",
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
},
cli.StringFlag{
Name: "kid",
Usage: "Key identifier from External CA. Used for External Account Binding.",
},
cli.StringFlag{
Name: "hmac",
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
},
cli.StringFlag{
Name: "key-type, k",
Value: "ec256",
Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384.",
},
cli.StringFlag{
Name: "filename",
Usage: "(deprecated) Filename of the generated certificate.",
},
cli.StringFlag{
Name: "path",
EnvVar: "LEGO_PATH",
Usage: "Directory to use for storing the data.",
Value: defaultPath,
},
cli.BoolFlag{
Name: "http",
Usage: "Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges.",
},
cli.StringFlag{
Name: "http.port",
Usage: "Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port.",
Value: ":80",
},
cli.StringFlag{
Name: "http.proxy-header",
Usage: "Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy.",
Value: "Host",
},
cli.StringFlag{
Name: "http.webroot",
Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge",
},
cli.StringSliceFlag{
Name: "http.memcached-host",
Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.",
},
cli.BoolFlag{
Name: "tls",
Usage: "Use the TLS challenge to solve challenges. Can be mixed with other types of challenges.",
},
cli.StringFlag{
Name: "tls.port",
Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port.",
Value: ":443",
},
cli.StringFlag{
Name: "dns",
Usage: "Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.",
},
cli.BoolFlag{
Name: "dns.disable-cp",
Usage: "By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers.",
},
cli.StringSliceFlag{
Name: "dns.resolvers",
Usage: "Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.",
},
cli.IntFlag{
Name: "http-timeout",
Usage: "Set the HTTP timeout value to a specific value in seconds.",
},
cli.IntFlag{
Name: "dns-timeout",
Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries.",
Value: 10,
},
cli.BoolFlag{
Name: "pem",
Usage: "Generate a .pem file by concatenating the .key and .crt files together.",
},
cli.IntFlag{
Name: "cert.timeout",
Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.",
Value: 30,
},
}
}

View File

@@ -0,0 +1,47 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
func launchHook(hook string, meta map[string]string) error {
if hook == "" {
return nil
}
ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
parts := strings.Fields(hook)
cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...)
output, err := cmdCtx.CombinedOutput()
if len(output) > 0 {
fmt.Println(string(output))
}
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
return errors.New("hook timed out")
}
return err
}
func metaToEnv(meta map[string]string) []string {
var envs []string
for k, v := range meta {
envs = append(envs, k+"="+v)
}
return envs
}

View File

@@ -0,0 +1,129 @@
package cmd
import (
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/log"
"io/ioutil"
"os"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
const filePerm os.FileMode = 0o600
func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) {
keyType := getKeyType(ctx)
privateKey := accountsStorage.GetPrivateKey(keyType)
var account *Account
if accountsStorage.ExistsAccountFilePath() {
account = accountsStorage.LoadAccount(privateKey)
} else {
account = &Account{Email: accountsStorage.GetUserID(), key: privateKey}
}
client := newClient(ctx, account, keyType)
return account, client
}
func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client {
config := lego.NewConfig(acc)
config.CADirURL = ctx.GlobalString("server")
config.Certificate = lego.CertificateConfig{
KeyType: keyType,
Timeout: time.Duration(ctx.GlobalInt("cert.timeout")) * time.Second,
}
config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version)
if ctx.GlobalIsSet("http-timeout") {
config.HTTPClient.Timeout = time.Duration(ctx.GlobalInt("http-timeout")) * time.Second
}
client, err := lego.NewClient(config)
if err != nil {
log.Panicf("Could not create client: %v", err)
}
if client.GetExternalAccountRequired() && !ctx.GlobalIsSet("eab") {
log.Panic("Server requires External Account Binding. Use --eab with --kid and --hmac.")
}
return client
}
// getKeyType the type from which private keys should be generated.
func getKeyType(ctx *cli.Context) certcrypto.KeyType {
keyType := ctx.GlobalString("key-type")
switch strings.ToUpper(keyType) {
case "RSA2048":
return certcrypto.RSA2048
case "RSA4096":
return certcrypto.RSA4096
case "RSA8192":
return certcrypto.RSA8192
case "EC256":
return certcrypto.EC256
case "EC384":
return certcrypto.EC384
}
log.Panicf("Unsupported KeyType: %s", keyType)
return ""
}
func getEmail(ctx *cli.Context) string {
email := ctx.GlobalString("email")
if email == "" {
log.Panic("You have to pass an account (email address) to the program using --email or -m")
}
return email
}
func createNonExistingFolder(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, 0o700)
} else if err != nil {
return err
}
return nil
}
func readCSRFile(filename string) (*x509.CertificateRequest, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
raw := bytes
// see if we can find a PEM-encoded CSR
var p *pem.Block
rest := bytes
for {
// decode a PEM block
p, rest = pem.Decode(rest)
// did we fail?
if p == nil {
break
}
// did we get a CSR?
if p.Type == "CERTIFICATE REQUEST" {
raw = p.Bytes
}
}
// no PEM-encoded CSR
// assume we were given a DER-encoded ASN.1 CSR
// (if this assumption is wrong, parsing these bytes will fail)
return x509.ParseCertificateRequest(raw)
}

View File

@@ -0,0 +1,126 @@
package cmd
import (
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/log"
"net"
"strings"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/providers/dns"
"github.com/go-acme/lego/v4/providers/http/memcached"
"github.com/go-acme/lego/v4/providers/http/webroot"
"github.com/urfave/cli"
)
func setupChallenges(ctx *cli.Context, client *lego.Client) {
if !ctx.GlobalBool("http") && !ctx.GlobalBool("tls") && !ctx.GlobalIsSet("dns") {
log.Panic("No challenge selected. You must specify at least one challenge: `--http`, `--tls`, `--dns`.")
}
if ctx.GlobalBool("http") {
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx))
if err != nil {
log.Panic(err)
}
}
if ctx.GlobalBool("tls") {
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx))
if err != nil {
log.Panic(err)
}
}
if ctx.GlobalIsSet("dns") {
setupDNS(ctx, client)
}
}
func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
switch {
case ctx.GlobalIsSet("http.webroot"):
ps, err := webroot.NewHTTPProvider(ctx.GlobalString("http.webroot"))
if err != nil {
log.Panic(err)
}
return ps
case ctx.GlobalIsSet("http.memcached-host"):
ps, err := memcached.NewMemcachedProvider(ctx.GlobalStringSlice("http.memcached-host"))
if err != nil {
log.Panic(err)
}
return ps
case ctx.GlobalIsSet("http.port"):
iface := ctx.GlobalString("http.port")
if !strings.Contains(iface, ":") {
log.Panicf("The --http switch only accepts interface:port or :port for its argument.")
}
host, port, err := net.SplitHostPort(iface)
if err != nil {
log.Panic(err)
}
srv := http01.NewProviderServer(host, port)
if header := ctx.GlobalString("http.proxy-header"); header != "" {
srv.SetProxyHeader(header)
}
return srv
case ctx.GlobalBool("http"):
srv := http01.NewProviderServer("", "")
if header := ctx.GlobalString("http.proxy-header"); header != "" {
srv.SetProxyHeader(header)
}
return srv
default:
log.Panic("Invalid HTTP challenge options.")
return nil
}
}
func setupTLSProvider(ctx *cli.Context) challenge.Provider {
switch {
case ctx.GlobalIsSet("tls.port"):
iface := ctx.GlobalString("tls.port")
if !strings.Contains(iface, ":") {
log.Panicf("The --tls switch only accepts interface:port or :port for its argument.")
}
host, port, err := net.SplitHostPort(iface)
if err != nil {
log.Panic(err)
}
return tlsalpn01.NewProviderServer(host, port)
case ctx.GlobalBool("tls"):
return tlsalpn01.NewProviderServer("", "")
default:
log.Panic("Invalid HTTP challenge options.")
return nil
}
}
func setupDNS(ctx *cli.Context, client *lego.Client) {
provider, err := dns.NewDNSChallengeProviderByName(ctx.GlobalString("dns"))
if err != nil {
log.Panic(err)
}
servers := ctx.GlobalStringSlice("dns.resolvers")
err = client.Challenge.SetDNS01Provider(provider,
dns01.CondOption(len(servers) > 0,
dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.GlobalStringSlice("dns.resolvers")))),
dns01.CondOption(ctx.GlobalBool("dns.disable-cp"),
dns01.DisableCompletePropagationRequirement()),
dns01.CondOption(ctx.GlobalIsSet("dns-timeout"),
dns01.AddDNSTimeout(time.Duration(ctx.GlobalInt("dns-timeout"))*time.Second)),
)
if err != nil {
log.Panic(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
// Package legoCmd Let's Encrypt client to go!
// CLI application for generating Let's Encrypt certificates using the ACME package.
package legoCmd
import (
"errors"
"fmt"
"github.com/Yuzuki616/V2bX/node/controller/legoCmd/cmd"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/urfave/cli"
)
var version = "dev"
var defaultPath string
type LegoCMD struct {
cmdClient *cli.App
}
func New() (*LegoCMD, error) {
app := cli.NewApp()
app.Name = "lego"
app.HelpName = "lego"
app.Usage = "Let's Encrypt client written in Go"
app.EnableBashCompletion = true
app.Version = version
cli.VersionPrinter = func(c *cli.Context) {
fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH)
}
// Set default pathTemp to configPath/cert
var pathTemp = ""
configPath := os.Getenv("XRAY_LOCATION_CONFIG")
if configPath != "" {
pathTemp = configPath
} else if cwd, err := os.Getwd(); err == nil {
pathTemp = cwd
} else {
pathTemp = "."
}
defaultPath = filepath.Join(pathTemp, "cert")
app.Flags = cmd.CreateFlags(defaultPath)
app.Before = cmd.Before
app.Commands = cmd.CreateCommands()
lego := &LegoCMD{
cmdClient: app,
}
return lego, nil
}
// DNSCert cert a domain using DNS API
func (l *LegoCMD) DNSCert(domain, email, provider string, DNSEnv map[string]string) (CertPath string, KeyPath string, err error) {
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknow panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
// Set Env for DNS configuration
for key, value := range DNSEnv {
os.Setenv(key, value)
}
// First check if the certificate exists
CertPath, KeyPath, err = checkCertfile(domain)
if err == nil {
return CertPath, KeyPath, err
}
argstring := fmt.Sprintf("lego -a -d %s -m %s --dns %s run", domain, email, provider)
err = l.cmdClient.Run(strings.Split(argstring, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
// HTTPCert cert a domain using http methods
func (l *LegoCMD) HTTPCert(domain, email string) (CertPath string, KeyPath string, err error) {
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknow panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
// First check if the certificate exists
CertPath, KeyPath, err = checkCertfile(domain)
if err == nil {
return CertPath, KeyPath, err
}
argString := fmt.Sprintf("lego -a -d %s -m %s --http run", domain, email)
err = l.cmdClient.Run(strings.Split(argString, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
// RenewCert renew a domain cert
func (l *LegoCMD) RenewCert(domain, email, certMode, provider string, DNSEnv map[string]string) (CertPath string, KeyPath string, err error) {
var argstring string
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknown panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
if certMode == "http" {
argstring = fmt.Sprintf("lego -a -d %s -m %s --http renew --days 30", domain, email)
} else if certMode == "dns" {
// Set Env for DNS configuration
for key, value := range DNSEnv {
os.Setenv(key, value)
}
argstring = fmt.Sprintf("lego -a -d %s -m %s --dns %s renew --days 30", domain, email, provider)
} else {
return "", "", fmt.Errorf("unsupport cert mode: %s", certMode)
}
err = l.cmdClient.Run(strings.Split(argstring, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
func checkCertfile(domain string) (string, string, error) {
keyPath := path.Join(defaultPath, "certificates", fmt.Sprintf("%s.key", domain))
certPath := path.Join(defaultPath, "certificates", fmt.Sprintf("%s.crt", domain))
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
return "", "", fmt.Errorf("cert key failed: %s", domain)
}
if _, err := os.Stat(certPath); os.IsNotExist(err) {
return "", "", fmt.Errorf("cert cert failed: %s", domain)
}
absKeyPath, _ := filepath.Abs(keyPath)
absCertPath, _ := filepath.Abs(certPath)
return absCertPath, absKeyPath, nil
}

View File

@@ -0,0 +1,81 @@
package legoCmd_test
import (
"github.com/Yuzuki616/V2bX/node/controller/legoCmd"
"testing"
)
func TestLegoClient(t *testing.T) {
_, err := legoCmd.New()
if err != nil {
t.Error(err)
}
}
func TestLegoDNSCert(t *testing.T) {
lego, err := legoCmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
provider string = "alidns"
DNSEnv map[string]string
)
DNSEnv = make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certPath, keyPath, err := lego.DNSCert(domain, email, provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}
func TestLegoHTTPCert(t *testing.T) {
lego, err := legoCmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
)
certPath, keyPath, err := lego.HTTPCert(domain, email)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}
func TestLegoRenewCert(t *testing.T) {
lego, err := legoCmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
provider string = "alidns"
DNSEnv map[string]string
)
DNSEnv = make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certPath, keyPath, err := lego.RenewCert(domain, email, "dns", provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
certPath, keyPath, err = lego.RenewCert(domain, email, "http", provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}

View File

@@ -0,0 +1,59 @@
package log
import (
"log"
"os"
)
// Logger is an optional custom logger.
var Logger StdLogger = log.New(os.Stdout, "", log.LstdFlags)
// StdLogger interface for Standard Logger.
type StdLogger interface {
Panic(args ...interface{})
Fatalln(args ...interface{})
Panicf(format string, args ...interface{})
Print(args ...interface{})
Println(args ...interface{})
Printf(format string, args ...interface{})
}
// Panic writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Panic(args ...interface{}) {
Logger.Panic(args...)
}
// Panicf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Panicf(format string, args ...interface{}) {
Logger.Panicf(format, args...)
}
// Print writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Print(args ...interface{}) {
Logger.Print(args...)
}
// Println writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Println(args ...interface{}) {
Logger.Println(args...)
}
// Printf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Printf(format string, args ...interface{}) {
Logger.Printf(format, args...)
}
// Warnf writes a log entry.
func Warnf(format string, args ...interface{}) {
Printf("[WARN] "+format, args...)
}
// Infof writes a log entry.
func Infof(format string, args ...interface{}) {
Printf("[INFO] "+format, args...)
}

160
node/controller/node.go Normal file
View File

@@ -0,0 +1,160 @@
package controller
import (
"errors"
"fmt"
"github.com/Yuzuki616/V2bX/api/panel"
"github.com/Yuzuki616/V2bX/conf"
"github.com/Yuzuki616/V2bX/core"
"github.com/xtls/xray-core/common/task"
"log"
"time"
)
type Node struct {
server *core.Core
config *conf.ControllerConfig
clientInfo panel.ClientInfo
apiClient panel.Panel
nodeInfo *panel.NodeInfo
Tag string
userList []panel.UserInfo
nodeInfoMonitorPeriodic *task.Periodic
userReportPeriodic *task.Periodic
onlineIpReportPeriodic *task.Periodic
DynamicSpeedLimitPeriodic *task.Periodic
}
// New return a Node service with default parameters.
func New(server *core.Core, api panel.Panel, config *conf.ControllerConfig) *Node {
controller := &Node{
server: server,
config: config,
apiClient: api,
}
return controller
}
// Start implement the Start() function of the service interface
func (c *Node) Start() error {
c.clientInfo = c.apiClient.Describe()
// First fetch Node Info
newNodeInfo, err := c.apiClient.GetNodeInfo()
if err != nil {
return fmt.Errorf("get node info failed: %s", err)
}
c.nodeInfo = newNodeInfo
c.Tag = c.buildNodeTag()
// Add new tag
err = c.addNewTag(newNodeInfo)
if err != nil {
return fmt.Errorf("add new tag failed: %s", err)
}
// Update user
c.userList, err = c.apiClient.GetUserList()
if err != nil {
return fmt.Errorf("get user list failed: %s", err)
}
if len(c.userList) == 0 {
return errors.New("add users failed: not have any user")
}
err = c.addNewUser(c.userList, newNodeInfo)
if err != nil {
return err
}
if err := c.server.AddInboundLimiter(c.Tag, c.nodeInfo); err != nil {
return fmt.Errorf("add inbound limiter failed: %s", err)
}
// Add Rule Manager
if !c.config.DisableGetRule {
if ruleList, err := c.apiClient.GetNodeRule(); err != nil {
log.Printf("Get rule list filed: %s", err)
} else if ruleList != nil {
if err := c.server.UpdateRule(c.Tag, ruleList); err != nil {
log.Printf("Update rule filed: %s", err)
}
}
}
// fetch node info task
c.nodeInfoMonitorPeriodic = &task.Periodic{
Interval: time.Duration(c.config.UpdatePeriodic) * time.Second,
Execute: c.nodeInfoMonitor,
}
// fetch user list task
c.userReportPeriodic = &task.Periodic{
Interval: time.Duration(c.config.UpdatePeriodic) * time.Second,
Execute: c.reportUserTraffic,
}
log.Printf("[%s: %d] Start monitor node status", c.nodeInfo.NodeType, c.nodeInfo.NodeId)
// delay to start nodeInfoMonitor
go func() {
time.Sleep(time.Duration(c.config.UpdatePeriodic) * time.Second)
_ = c.nodeInfoMonitorPeriodic.Start()
}()
log.Printf("[%s: %d] Start report node status", c.nodeInfo.NodeType, c.nodeInfo.NodeId)
// delay to start userReport
go func() {
time.Sleep(time.Duration(c.config.UpdatePeriodic) * time.Second)
_ = c.userReportPeriodic.Start()
}()
if c.config.EnableIpRecorder {
// report and fetch online ip list task
c.onlineIpReportPeriodic = &task.Periodic{
Interval: time.Duration(c.config.IpRecorderConfig.Periodic) * time.Second,
Execute: c.reportOnlineIp,
}
go func() {
time.Sleep(time.Duration(c.config.IpRecorderConfig.Periodic) * time.Second)
_ = c.onlineIpReportPeriodic.Start()
}()
log.Printf("[%s: %d] Start report online ip", c.nodeInfo.NodeType, c.nodeInfo.NodeId)
}
if c.config.EnableDynamicSpeedLimit {
// Check dynamic speed limit task
c.DynamicSpeedLimitPeriodic = &task.Periodic{
Interval: time.Duration(c.config.DynamicSpeedLimitConfig.Periodic) * time.Second,
Execute: c.dynamicSpeedLimit,
}
go func() {
time.Sleep(time.Duration(c.config.DynamicSpeedLimitConfig.Periodic) * time.Second)
_ = c.DynamicSpeedLimitPeriodic.Start()
}()
log.Printf("[%s: %d] Start dynamic speed limit", c.nodeInfo.NodeType, c.nodeInfo.NodeId)
}
return nil
}
// Close implement the Close() function of the service interface
func (c *Node) Close() error {
if c.nodeInfoMonitorPeriodic != nil {
err := c.nodeInfoMonitorPeriodic.Close()
if err != nil {
log.Panicf("node info periodic close failed: %s", err)
}
}
if c.nodeInfoMonitorPeriodic != nil {
err := c.userReportPeriodic.Close()
if err != nil {
log.Panicf("user report periodic close failed: %s", err)
}
}
if c.onlineIpReportPeriodic != nil {
err := c.onlineIpReportPeriodic.Close()
if err != nil {
log.Panicf("online ip report periodic close failed: %s", err)
}
}
if c.DynamicSpeedLimitPeriodic != nil {
err := c.DynamicSpeedLimitPeriodic.Close()
if err != nil {
log.Panicf("dynamic speed limit periodic close failed: %s", err)
}
}
return nil
}
func (c *Node) buildNodeTag() string {
return fmt.Sprintf("%s_%s_%d", c.nodeInfo.NodeType, c.config.ListenIP, c.nodeInfo.NodeId)
}

View File

@@ -0,0 +1,80 @@
package controller_test
import (
"fmt"
"github.com/Yuzuki616/V2bX/api/panel"
"github.com/Yuzuki616/V2bX/conf"
"github.com/Yuzuki616/V2bX/core"
_ "github.com/Yuzuki616/V2bX/core/distro/all"
. "github.com/Yuzuki616/V2bX/node/controller"
xCore "github.com/xtls/xray-core/core"
coreConf "github.com/xtls/xray-core/infra/conf"
"os"
"os/signal"
"runtime"
"syscall"
"testing"
)
func TestController(t *testing.T) {
serverConfig := &coreConf.Config{
Stats: &coreConf.StatsConfig{},
LogConfig: &coreConf.LogConfig{LogLevel: "debug"},
}
policyConfig := &coreConf.PolicyConfig{}
policyConfig.Levels = map[uint32]*coreConf.Policy{0: &coreConf.Policy{
StatsUserUplink: true,
StatsUserDownlink: true,
}}
serverConfig.Policy = policyConfig
config, _ := serverConfig.Build()
// config := &core.Config{
// App: []*serial.TypedMessage{
// serial.ToTypedMessage(&dispatcher.Config{}),
// serial.ToTypedMessage(&proxyman.InboundConfig{}),
// serial.ToTypedMessage(&proxyman.OutboundConfig{}),
// serial.ToTypedMessage(&stats.Config{}),
// }}
server, err := xCore.New(config)
defer server.Close()
if err != nil {
t.Errorf("failed to create instance: %s", err)
}
if err = server.Start(); err != nil {
t.Errorf("Failed to start instance: %s", err)
}
certConfig := &conf.CertConfig{
CertMode: "http",
CertDomain: "test.ss.tk",
Provider: "alidns",
Email: "ss@ss.com",
}
controlerconfig := &conf.ControllerConfig{
UpdatePeriodic: 5,
CertConfig: certConfig,
}
apiConfig := &conf.ApiConfig{
APIHost: "http://127.0.0.1:667",
Key: "123",
NodeID: 41,
NodeType: "V2ray",
}
apiclient := panel.New(apiConfig)
c := &core.Core{Server: server}
c.Start()
node := New(c, apiclient, controlerconfig)
fmt.Println("Sleep 1s")
err = node.Start()
if err != nil {
t.Error(err)
}
//Explicitly triggering GC to remove garbage from config loading.
runtime.GC()
{
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM)
<-osSignals
}
}

View File

@@ -0,0 +1,45 @@
package controller
import (
"encoding/json"
"fmt"
"github.com/Yuzuki616/V2bX/api/panel"
conf2 "github.com/Yuzuki616/V2bX/conf"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf"
)
// buildOutbound build freedom outbund config for addoutbound
func buildOutbound(config *conf2.ControllerConfig, nodeInfo *panel.NodeInfo, tag string) (*core.OutboundHandlerConfig, error) {
outboundDetourConfig := &conf.OutboundDetourConfig{}
outboundDetourConfig.Protocol = "freedom"
outboundDetourConfig.Tag = tag
// Build Send IP address
if config.SendIP != "" {
ipAddress := net.ParseAddress(config.SendIP)
outboundDetourConfig.SendThrough = &conf.Address{Address: ipAddress}
}
// Freedom Protocol setting
var domainStrategy = "Asis"
if config.EnableDNS {
if config.DNSType != "" {
domainStrategy = config.DNSType
} else {
domainStrategy = "UseIP"
}
}
proxySetting := &conf.FreedomConfig{
DomainStrategy: domainStrategy,
}
var setting json.RawMessage
setting, err := json.Marshal(proxySetting)
if err != nil {
return nil, fmt.Errorf("marshal proxy %s config fialed: %s", nodeInfo.NodeType, err)
}
outboundDetourConfig.Settings = &setting
return outboundDetourConfig.Build()
}

304
node/controller/task.go Normal file
View File

@@ -0,0 +1,304 @@
package controller
import (
"fmt"
"github.com/Yuzuki616/V2bX/api/panel"
"github.com/Yuzuki616/V2bX/core/app/dispatcher"
"github.com/Yuzuki616/V2bX/node/controller/legoCmd"
"github.com/go-resty/resty/v2"
"github.com/goccy/go-json"
"github.com/xtls/xray-core/common/protocol"
"log"
"reflect"
"runtime"
"time"
)
func (c *Node) nodeInfoMonitor() (err error) {
// First fetch Node Info
newNodeInfo, err := c.apiClient.GetNodeInfo()
if err != nil {
log.Print(err)
return nil
}
var nodeInfoChanged = false
// If nodeInfo changed
if newNodeInfo != nil {
if c.nodeInfo.SS == nil || !reflect.DeepEqual(c.nodeInfo.SS, newNodeInfo.SS) {
// Remove old tag
oldTag := c.Tag
err := c.removeOldTag(oldTag)
if err != nil {
log.Print(err)
return nil
}
// Add new tag
c.nodeInfo = newNodeInfo
c.Tag = c.buildNodeTag()
err = c.addNewTag(newNodeInfo)
if err != nil {
log.Print(err)
return nil
}
nodeInfoChanged = true
// Remove Old limiter
if err = c.server.DeleteInboundLimiter(oldTag); err != nil {
log.Print(err)
return nil
}
}
}
// Check Rule
if !c.config.DisableGetRule {
if ruleList, err := c.apiClient.GetNodeRule(); err != nil {
log.Printf("Get rule list filed: %s", err)
} else if ruleList != nil {
if err := c.server.UpdateRule(c.Tag, ruleList); err != nil {
log.Print(err)
}
}
}
// Check Cert
if c.nodeInfo.EnableTls && c.config.CertConfig.CertMode != "none" &&
(c.config.CertConfig.CertMode == "dns" || c.config.CertConfig.CertMode == "http") {
lego, err := legoCmd.New()
if err != nil {
log.Print(err)
}
// Core-core supports the OcspStapling certification hot renew
_, _, err = lego.RenewCert(c.config.CertConfig.CertDomain, c.config.CertConfig.Email,
c.config.CertConfig.CertMode, c.config.CertConfig.Provider, c.config.CertConfig.DNSEnv)
if err != nil {
log.Print(err)
}
}
// Update User
newUserInfo, err := c.apiClient.GetUserList()
if err != nil {
log.Print(err)
return nil
}
if nodeInfoChanged {
c.userList = newUserInfo
newUserInfo = nil
err = c.addNewUser(c.userList, newNodeInfo)
if err != nil {
log.Print(err)
return nil
}
newNodeInfo = nil
// Add Limiter
if err := c.server.AddInboundLimiter(c.Tag, c.nodeInfo); err != nil {
log.Print(err)
return nil
}
runtime.GC()
} else {
deleted, added := compareUserList(c.userList, newUserInfo)
if len(deleted) > 0 {
deletedEmail := make([]string, len(deleted))
for i := range deleted {
deletedEmail[i] = fmt.Sprintf("%s|%s|%d", c.Tag,
(deleted)[i].GetUserEmail(),
(deleted)[i].UID)
}
err := c.server.RemoveUsers(deletedEmail, c.Tag)
if err != nil {
log.Print(err)
}
}
if len(added) > 0 {
err = c.addNewUser(added, c.nodeInfo)
if err != nil {
log.Print(err)
}
}
if len(added) > 0 || len(deleted) > 0 {
defer runtime.GC()
// Update Limiter
if err := c.server.UpdateInboundLimiter(c.Tag, deleted); err != nil {
log.Print(err)
}
}
log.Printf("[%s: %d] %d user deleted, %d user added", c.nodeInfo.NodeType, c.nodeInfo.NodeId,
len(deleted), len(added))
c.userList = newUserInfo
newUserInfo = nil
}
return nil
}
func (c *Node) removeOldTag(oldTag string) (err error) {
err = c.server.RemoveInbound(oldTag)
if err != nil {
return err
}
err = c.server.RemoveOutbound(oldTag)
if err != nil {
return err
}
return nil
}
func (c *Node) addNewTag(newNodeInfo *panel.NodeInfo) (err error) {
inboundConfig, err := buildInbound(c.config, newNodeInfo, c.Tag)
if err != nil {
return err
}
err = c.server.AddInbound(inboundConfig)
if err != nil {
return err
}
outBoundConfig, err := buildOutbound(c.config, newNodeInfo, c.Tag)
if err != nil {
return err
}
err = c.server.AddOutbound(outBoundConfig)
if err != nil {
return err
}
return nil
}
func (c *Node) addNewUser(userInfo []panel.UserInfo, nodeInfo *panel.NodeInfo) (err error) {
users := make([]*protocol.User, 0)
if nodeInfo.NodeType == "V2ray" {
if nodeInfo.EnableVless {
users = c.buildVlessUsers(userInfo)
} else {
users = c.buildVmessUsers(userInfo)
}
} else if nodeInfo.NodeType == "Trojan" {
users = c.buildTrojanUsers(userInfo)
} else if nodeInfo.NodeType == "Shadowsocks" {
users = c.buildSSUsers(userInfo, getCipherFromString(nodeInfo.SS.CypherMethod))
} else {
return fmt.Errorf("unsupported node type: %s", nodeInfo.NodeType)
}
err = c.server.AddUsers(users, c.Tag)
if err != nil {
return err
}
log.Printf("[%s: %d] Added %d new users", c.nodeInfo.NodeType, c.nodeInfo.NodeId, len(userInfo))
return nil
}
func compareUserList(old, new []panel.UserInfo) (deleted, added []panel.UserInfo) {
tmp := map[string]struct{}{}
tmp2 := map[string]struct{}{}
for i := range old {
tmp[(old)[i].GetUserEmail()] = struct{}{}
}
l := len(tmp)
for i := range new {
e := (new)[i].GetUserEmail()
tmp[e] = struct{}{}
tmp2[e] = struct{}{}
if l != len(tmp) {
added = append(added, (new)[i])
l++
}
}
tmp = nil
l = len(tmp2)
for i := range old {
tmp2[(old)[i].GetUserEmail()] = struct{}{}
if l != len(tmp2) {
deleted = append(deleted, (old)[i])
l++
}
}
return deleted, added
}
func (c *Node) reportUserTraffic() (err error) {
// Get User traffic
userTraffic := make([]panel.UserTraffic, 0)
for i := range c.userList {
up, down := c.server.GetUserTraffic(c.buildUserTag(&(c.userList)[i]), true)
if up > 0 || down > 0 {
if c.config.EnableDynamicSpeedLimit {
c.userList[i].Traffic += up + down
}
userTraffic = append(userTraffic, panel.UserTraffic{
UID: (c.userList)[i].UID,
Upload: up,
Download: down})
}
}
if len(userTraffic) > 0 && !c.config.DisableUploadTraffic {
err = c.apiClient.ReportUserTraffic(userTraffic)
if err != nil {
log.Printf("Report user traffic faild: %s", err)
} else {
log.Printf("[%s: %d] Report %d online users", c.nodeInfo.NodeType, c.nodeInfo.NodeId, len(userTraffic))
}
}
userTraffic = nil
if !c.config.EnableIpRecorder {
c.server.ClearOnlineIp(c.Tag)
}
runtime.GC()
return nil
}
func (c *Node) reportOnlineIp() (err error) {
onlineIp, err := c.server.ListOnlineIp(c.Tag)
if err != nil {
log.Print(err)
return nil
}
rsp, err := resty.New().SetTimeout(time.Duration(c.config.IpRecorderConfig.Timeout) * time.Second).
R().
SetBody(onlineIp).
Post(c.config.IpRecorderConfig.Url +
"/api/v1/SyncOnlineIp?token=" +
c.config.IpRecorderConfig.Token)
if err != nil {
log.Print(err)
c.server.ClearOnlineIp(c.Tag)
return nil
}
log.Printf("[Node: %d] Report %d online ip", c.nodeInfo.NodeId, len(onlineIp))
if rsp.StatusCode() == 200 {
onlineIp = []dispatcher.UserIpList{}
err := json.Unmarshal(rsp.Body(), &onlineIp)
if err != nil {
log.Print(err)
c.server.ClearOnlineIp(c.Tag)
return nil
}
if c.config.IpRecorderConfig.EnableIpSync {
c.server.UpdateOnlineIp(c.Tag, onlineIp)
log.Printf("[Node: %d] Updated %d online ip", c.nodeInfo.NodeId, len(onlineIp))
}
} else {
c.server.ClearOnlineIp(c.Tag)
}
return nil
}
func (c *Node) dynamicSpeedLimit() error {
if c.config.EnableDynamicSpeedLimit {
for i := range c.userList {
up, down := c.server.GetUserTraffic(c.buildUserTag(&(c.userList)[i]), false)
if c.userList[i].Traffic+down+up/1024/1024 > c.config.DynamicSpeedLimitConfig.Traffic {
err := c.server.AddUserSpeedLimit(c.Tag,
&c.userList[i],
c.config.DynamicSpeedLimitConfig.SpeedLimit,
time.Now().Add(time.Second*time.Duration(c.config.DynamicSpeedLimitConfig.ExpireTime)).Unix())
if err != nil {
log.Print(err)
}
}
c.userList[i].Traffic = 0
}
}
return nil
}

114
node/controller/user.go Normal file
View File

@@ -0,0 +1,114 @@
package controller
import (
"fmt"
"github.com/Yuzuki616/V2bX/api/panel"
"strings"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/shadowsocks"
"github.com/xtls/xray-core/proxy/trojan"
"github.com/xtls/xray-core/proxy/vless"
)
func (c *Node) buildVmessUsers(userInfo []panel.UserInfo) (users []*protocol.User) {
users = make([]*protocol.User, len(userInfo))
for i, user := range userInfo {
users[i] = c.buildVmessUser(&user, 0)
}
return users
}
func (c *Node) buildVmessUser(userInfo *panel.UserInfo, serverAlterID uint16) (user *protocol.User) {
vmessAccount := &conf.VMessAccount{
ID: userInfo.V2rayUser.Uuid,
AlterIds: serverAlterID,
Security: "auto",
}
return &protocol.User{
Level: 0,
Email: c.buildUserTag(userInfo), // Uid: InboundTag|email|uid
Account: serial.ToTypedMessage(vmessAccount.Build()),
}
}
func (c *Node) buildVlessUsers(userInfo []panel.UserInfo) (users []*protocol.User) {
users = make([]*protocol.User, len(userInfo))
for i := range userInfo {
users[i] = c.buildVlessUser(&(userInfo)[i])
}
return users
}
func (c *Node) buildVlessUser(userInfo *panel.UserInfo) (user *protocol.User) {
vlessAccount := &vless.Account{
Id: userInfo.V2rayUser.Uuid,
Flow: "xtls-rprx-direct",
}
return &protocol.User{
Level: 0,
Email: c.buildUserTag(userInfo),
Account: serial.ToTypedMessage(vlessAccount),
}
}
func (c *Node) buildTrojanUsers(userInfo []panel.UserInfo) (users []*protocol.User) {
users = make([]*protocol.User, len(userInfo))
for i := range userInfo {
users[i] = c.buildTrojanUser(&(userInfo)[i])
}
return users
}
func (c *Node) buildTrojanUser(userInfo *panel.UserInfo) (user *protocol.User) {
trojanAccount := &trojan.Account{
Password: userInfo.TrojanUser.Password,
Flow: "xtls-rprx-direct",
}
return &protocol.User{
Level: 0,
Email: c.buildUserTag(userInfo),
Account: serial.ToTypedMessage(trojanAccount),
}
}
func getCipherFromString(c string) shadowsocks.CipherType {
switch strings.ToLower(c) {
case "aes-128-gcm", "aead_aes_128_gcm":
return shadowsocks.CipherType_AES_128_GCM
case "aes-256-gcm", "aead_aes_256_gcm":
return shadowsocks.CipherType_AES_256_GCM
case "chacha20-poly1305", "aead_chacha20_poly1305", "chacha20-ietf-poly1305":
return shadowsocks.CipherType_CHACHA20_POLY1305
case "none", "plain":
return shadowsocks.CipherType_NONE
default:
return shadowsocks.CipherType_UNKNOWN
}
}
func (c *Node) buildSSUsers(userInfo []panel.UserInfo, cypher shadowsocks.CipherType) (users []*protocol.User) {
users = make([]*protocol.User, len(userInfo))
for i := range userInfo {
c.buildSSUser(&(userInfo)[i], cypher)
}
return users
}
func (c *Node) buildSSUser(userInfo *panel.UserInfo, cypher shadowsocks.CipherType) (user *protocol.User) {
ssAccount := &shadowsocks.Account{
Password: userInfo.Secret,
CipherType: cypher,
}
return &protocol.User{
Level: 0,
Email: c.buildUserTag(userInfo),
Account: serial.ToTypedMessage(ssAccount),
}
}
func (c *Node) buildUserTag(user *panel.UserInfo) string {
return fmt.Sprintf("%s|%s|%d", c.Tag, user.GetUserEmail(), user.UID)
}