diff --git a/Makefile b/Makefile index 9573b90..d4ccc01 100644 --- a/Makefile +++ b/Makefile @@ -38,13 +38,20 @@ generate_grpc_code: --go-grpc_opt=paths=source_relative \ common/service.proto +CN ?= localhost +SAN ?= DNS:localhost,IP:127.0.0.1 + generate_server_cert: openssl req -x509 -newkey rsa:4096 -keyout ./certs/ssl_key.pem \ - -out ./certs/ssl_cert.pem -days 36500 -nodes -subj "/CN=Gozargah" + -out ./certs/ssl_cert.pem -days 36500 -nodes \ + -subj "/CN=$(CN)" \ + -addext "subjectAltName = $(SAN)" generate_client_cert: openssl req -x509 -newkey rsa:4096 -keyout ./certs/ssl_client_key.pem \ - -out ./certs/ssl_client_cert.pem -days 36500 -nodes -subj "/CN=Gozargah" + -out ./certs/ssl_client_cert.pem -days 36500 -nodes \ + -subj "/CN=$(CN)" \ + -addext "subjectAltName = $(SAN)" UNAME_S := $(shell uname -s) UNAME_M := $(shell uname -m) diff --git a/README.md b/README.md index 0288f4f..c6e5573 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,23 @@ We plan to expand supported cores after the testing stage, allowing you to use a | `DEBUG` | Debug mode for development; prints core logs in the node server (default: `False`). | | `GENERATED_CONFIG_PATH` | Path to the generated config by the node (default: `/var/lib/gozargah-node/generated`). | +## SSL Configuration + +### SSL Certificates +You can use SSL certificates issued by `Let's Encrypt` or other certificate authorities. +Make sure to set both `SSL_CERT_FILE` and `SSL_KEY_FILE` environment variables. + +### mTLS +If you don't have access to a real domain or tools like `ACME`, you can use `mTLS` to connect to a node. +Just replace the `CN` and `subjectAltName` values with your server information: + +```shell +openssl req -x509 -newkey rsa:4096 -keyout /var/lib/gozargah-node/certs/ssl_key.pem \ + -out /var/lib/gozargah-node/certs/ssl_cert.pem -days 36500 -nodes \ + -subj "/CN={replace with your server IP or domain}" \ + -addext "subjectAltName = {replace with alternative names you need}" +``` + ## API Gozargah Node supports two types of connection protocols: **gRPC** and **REST API**. diff --git a/backend/xray/config.go b/backend/xray/config.go index f8b7617..2ea1aca 100644 --- a/backend/xray/config.go +++ b/backend/xray/config.go @@ -61,7 +61,7 @@ func (i *Inbound) syncUsers(users []*common.User) { switch i.Protocol { case Vmess: - var clients []*api.VmessAccount + clients := []*api.VmessAccount{} for _, user := range users { if user.GetProxies().GetVmess() == nil { continue @@ -77,7 +77,7 @@ func (i *Inbound) syncUsers(users []*common.User) { i.Settings["clients"] = clients case Vless: - var clients []*api.VlessAccount + clients := []*api.VlessAccount{} for _, user := range users { if user.GetProxies().GetVless() == nil { continue @@ -86,14 +86,15 @@ func (i *Inbound) syncUsers(users []*common.User) { if err != nil { log.Println("error for user", user.GetEmail(), ":", err) } - if newAccount, active := isActiveInbound(i, user.GetInbounds(), api.ProxySettings{Vless: account}); active { - clients = append(clients, newAccount.(*api.VlessAccount)) + if slices.Contains(user.Inbounds, i.Tag) { + newAccount := checkVless(i, *account) + clients = append(clients, &newAccount) } } i.Settings["clients"] = clients case Trojan: - var clients []*api.TrojanAccount + clients := []*api.TrojanAccount{} for _, user := range users { if user.GetProxies().GetTrojan() == nil { continue @@ -106,14 +107,16 @@ func (i *Inbound) syncUsers(users []*common.User) { case Shadowsocks: method, methodOk := i.Settings["method"].(string) - if methodOk && strings.HasPrefix("2022-blake3", method) { - var clients []*api.ShadowsocksAccount + if methodOk && strings.HasPrefix(method, "2022-blake3") { + clients := []*api.ShadowsocksAccount{} for _, user := range users { if user.GetProxies().GetShadowsocks() == nil { continue } if slices.Contains(user.Inbounds, i.Tag) { - clients = append(clients, api.NewShadowsocksAccount(user)) + account := api.NewShadowsocksAccount(user) + newAccount := checkShadowsocks2022(method, *account) + clients = append(clients, &newAccount) } } i.Settings["clients"] = clients @@ -212,7 +215,9 @@ func (i *Inbound) updateUser(account api.Account) { } } - i.Settings["clients"] = append(clients, account.(*api.ShadowsocksAccount)) + method := i.Settings["method"].(string) + newAccount := checkShadowsocks2022(method, *account.(*api.ShadowsocksAccount)) + i.Settings["clients"] = append(clients, &newAccount) default: return @@ -268,7 +273,7 @@ func (i *Inbound) removeUser(email string) { case Shadowsocks: method, methodOk := i.Settings["method"].(string) - if methodOk && strings.HasPrefix("2022-blake3", method) { + if methodOk && strings.HasPrefix(method, "2022-blake3") { clients, ok := i.Settings["clients"].([]*api.ShadowsocksAccount) if !ok { clients = []*api.ShadowsocksAccount{} diff --git a/backend/xray/config.json b/backend/xray/config.json index 4efb9a8..b758d53 100644 --- a/backend/xray/config.json +++ b/backend/xray/config.json @@ -15,7 +15,7 @@ } }, { - "tag": "Shadowsocks 2024", + "tag": "Shadowsocks 2022", "port": 1234, "protocol": "shadowsocks", "settings": { diff --git a/backend/xray/user.go b/backend/xray/user.go index 5f7df87..51c614f 100644 --- a/backend/xray/user.go +++ b/backend/xray/user.go @@ -41,52 +41,61 @@ func setupUserAccount(user *common.User) (api.ProxySettings, error) { return settings, nil } -func isActiveInbound(inbound *Inbound, inbounds []string, settings api.ProxySettings) (api.Account, bool) { - if slices.Contains(inbounds, inbound.Tag) { - switch inbound.Protocol { - case Vless: - if settings.Vless == nil { - return nil, false - } +func checkVless(inbound *Inbound, account api.VlessAccount) api.VlessAccount { + if account.Flow != "" { - account := *settings.Vless - if settings.Vless.Flow != "" { + networkType, ok := inbound.StreamSettings["network"] + if !ok || !(networkType == "tcp" || networkType == "raw" || networkType == "kcp") { + account.Flow = "" + return account + } - networkType, ok := inbound.StreamSettings["network"] - if !ok || !(networkType == "tcp" || networkType == "raw" || networkType == "kcp") { - account.Flow = "" - return &account, true - } + securityType, ok := inbound.StreamSettings["security"] + if !ok || !(securityType == "tls" || securityType == "reality") { + account.Flow = "" + return account + } - securityType, ok := inbound.StreamSettings["security"] - if !ok || !(securityType == "tls" || securityType == "reality") { - account.Flow = "" - return &account, true - } + rawMap, ok := inbound.StreamSettings["rawSettings"].(map[string]interface{}) + if !ok { + rawMap, ok = inbound.StreamSettings["tcpSettings"].(map[string]interface{}) + if !ok { + return account + } + } - rawMap, ok := inbound.StreamSettings["rawSettings"].(map[string]interface{}) - if !ok { - rawMap, ok = inbound.StreamSettings["tcpSettings"].(map[string]interface{}) - if !ok { - return &account, true - } - } + headerMap, ok := rawMap["header"].(map[string]interface{}) + if !ok { + return account + } - headerMap, ok := rawMap["header"].(map[string]interface{}) - if !ok { - return &account, true - } + headerType, ok := headerMap["Type"].(string) + if !ok { + return account + } - headerType, ok := headerMap["Type"].(string) - if !ok { - return &account, true - } + if headerType == "http" { + account.Flow = "" + return account + } + } + return account +} - if headerType == "http" { - account.Flow = "" - return &account, true - } +func checkShadowsocks2022(method string, account api.ShadowsocksAccount) api.ShadowsocksAccount { + account.Password = common.EnsureBase64Password(account.Password, method) + + return account +} + +func isActiveInbound(inbound *Inbound, inbounds []string, settings api.ProxySettings) (api.Account, bool) { + if slices.Contains(inbounds, inbound.Tag) { + switch inbound.Protocol { + case Vless: + if settings.Vless == nil { + return nil, false } + account := checkVless(inbound, *settings.Vless) return &account, true case Vmess: @@ -103,11 +112,13 @@ func isActiveInbound(inbound *Inbound, inbounds []string, settings api.ProxySett case Shadowsocks: method, ok := inbound.Settings["method"].(string) - if ok && strings.HasPrefix("2022-blake3", method) { + if ok && strings.HasPrefix(method, "2022-blake3") { if settings.Shadowsocks2022 == nil { return nil, false } - return settings.Shadowsocks2022, true + account := checkShadowsocks2022(method, *settings.Shadowsocks2022) + + return &account, true } if settings.Shadowsocks == nil { return nil, false diff --git a/backend/xray/xray.go b/backend/xray/xray.go index 6bbe547..39f63b3 100644 --- a/backend/xray/xray.go +++ b/backend/xray/xray.go @@ -247,28 +247,39 @@ func (x *Xray) GenerateConfigFile() error { func (x *Xray) checkXrayStatus() error { core := x.getCore() - logChan := core.GetLogs() version := core.GetVersion() ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() + // Precompile regex for better performance + logRegex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[([^]]+)\] (.+)$`) + Loop: for { select { case lastLog := <-logChan: if strings.Contains(lastLog, "Xray "+version+" started") { break Loop + } + + // Check for failure patterns + matches := logRegex.FindStringSubmatch(lastLog) + if len(matches) > 3 { + // Check both error level and message content + if matches[2] == "Error" || strings.Contains(matches[3], "Failed to start") { + return fmt.Errorf("failed to start xray: %s", matches[3]) + } } else { - regex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[([^]]+)\] (.+)$`) - matches := regex.FindStringSubmatch(lastLog) - if len(matches) > 3 && matches[2] == "Error" { - return errors.New("Failed to start xray: " + matches[3]) + // Fallback check if log format doesn't match + if strings.Contains(lastLog, "Failed to start") { + return fmt.Errorf("failed to start xray: %s", lastLog) } } + case <-ctx.Done(): - return errors.New("failed to start xray: context done") + return errors.New("failed to start xray: context timeout") } } return nil diff --git a/backend/xray/xray_test.go b/backend/xray/xray_test.go index 778e584..9f64631 100644 --- a/backend/xray/xray_test.go +++ b/backend/xray/xray_test.go @@ -39,6 +39,7 @@ func TestXrayBackend(t *testing.T) { user := &common.User{ Email: "test_user@example.com", Inbounds: []string{ + "Shadowsocks 2022", "VMESS TCP NOTLS", "VLESS TCP REALITY", "TROJAN TCP NOTLS", diff --git a/common/helper.go b/common/helper.go index 4bce609..83d3a1f 100644 --- a/common/helper.go +++ b/common/helper.go @@ -1,8 +1,11 @@ package common import ( + "crypto/sha256" + "encoding/base64" "io" "net/http" + "strings" "google.golang.org/protobuf/proto" ) @@ -30,3 +33,31 @@ func SendProtoResponse(w http.ResponseWriter, data proto.Message) { return } } + +func EnsureBase64Password(password string, method string) string { + // First check if it's already a valid base64 string + decodedBytes, err := base64.StdEncoding.DecodeString(password) + if err == nil { + // It's already base64, now check if length is appropriate + if (strings.Contains(method, "aes-128-gcm") && len(decodedBytes) == 16) || + ((strings.Contains(method, "aes-256-gcm") || strings.Contains(method, "chacha20-poly1305")) && len(decodedBytes) == 32) { + // Already correct length + return password + } + } + + // Hash the password to get a consistent byte array + hasher := sha256.New() + hasher.Write([]byte(password)) + hashBytes := hasher.Sum(nil) + + // Resize based on method + var keyBytes []byte + if strings.Contains(method, "aes-128-gcm") { + keyBytes = hashBytes[:16] // First 16 bytes for AES-128 + } else { + keyBytes = hashBytes[:32] // First 32 bytes for AES-256 or ChaCha20 + } + + return base64.StdEncoding.EncodeToString(keyBytes) +} diff --git a/config/config.go b/config/config.go index 9c73461..8dd8b76 100644 --- a/config/config.go +++ b/config/config.go @@ -22,7 +22,7 @@ func init() { SslKeyFile = GetEnv("SSL_KEY_FILE", "/var/lib/gozargah-node/certs/ssl_key.pem") SslClientCertFile = GetEnv("SSL_CLIENT_CERT_FILE", "/var/lib/gozargah-node/certs/ssl_client_cert.pem") GeneratedConfigPath = GetEnv("GENERATED_CONFIG_PATH", "/var/lib/gozargah-node/generated/") - ServiceProtocol = GetEnv("SERVICE_PROTOCOL", "rest") + ServiceProtocol = GetEnv("SERVICE_PROTOCOL", "grpc") MaxLogPerRequest = GetEnvAsInt("MAX_LOG_PER_REQUEST", 1000) Debug = GetEnvAsBool("DEBUG", false) nodeHostStr := GetEnv("NODE_HOST", "0.0.0.0") diff --git a/controller/rest/middleware.go b/controller/rest/middleware.go index 766dd3a..4a8a72f 100644 --- a/controller/rest/middleware.go +++ b/controller/rest/middleware.go @@ -55,6 +55,7 @@ func (s *Service) checkSessionIDMiddleware(next http.Handler) http.Handler { return } + s.NewRequest() next.ServeHTTP(w, r) }) } diff --git a/controller/rpc/base.go b/controller/rpc/base.go index 46388f7..2867fb0 100644 --- a/controller/rpc/base.go +++ b/controller/rpc/base.go @@ -3,6 +3,7 @@ package rpc import ( "context" "errors" + "log" "net" "github.com/m03ed/gozargah-node/backend" @@ -17,10 +18,6 @@ func (s *Service) Start(ctx context.Context, detail *common.Backend) (*common.Ba return nil, err } - if err = s.StartBackend(ctx, detail.GetType()); err != nil { - return nil, err - } - clientIP := "" if p, ok := peer.FromContext(ctx); ok { // Extract IP address from peer address @@ -38,6 +35,15 @@ func (s *Service) Start(ctx context.Context, detail *common.Backend) (*common.Ba } } + if s.GetBackend() != nil { + log.Println("New connection from ", clientIP, " core control access was taken away from previous client.") + s.Disconnect() + } + + if err = s.StartBackend(ctx, detail.GetType()); err != nil { + return nil, err + } + s.Connect(clientIP, detail.GetKeepAlive()) return s.BaseInfoResponse(true, ""), nil diff --git a/controller/rpc/middleware.go b/controller/rpc/middleware.go index 3f47212..19465b1 100644 --- a/controller/rpc/middleware.go +++ b/controller/rpc/middleware.go @@ -187,33 +187,35 @@ func LoggingStreamInterceptor() grpc.StreamServerInterceptor { } var backendMethods = map[string]bool{ - "/service.NodeService/GetOutboundsStats": true, - "/service.NodeService/GetOutboundStats": true, - "/service.NodeService/GetInboundsStats": true, - "/service.NodeService/GetInboundStats": true, - "/service.NodeService/GetUsersStats": true, - "/service.NodeService/GetUserStats": true, - "/service.NodeService/GetUserOnlineStats": true, - "/service.NodeService/GetBackendStats": true, - "/service.NodeService/SyncUser": true, - "/service.NodeService/SyncUsers": true, + "/service.NodeService/GetOutboundsStats": true, + "/service.NodeService/GetOutboundStats": true, + "/service.NodeService/GetInboundsStats": true, + "/service.NodeService/GetInboundStats": true, + "/service.NodeService/GetUsersStats": true, + "/service.NodeService/GetUserStats": true, + "/service.NodeService/GetUserOnlineStats": true, + "/service.NodeService/GetUserOnlineIpListStats": true, + "/service.NodeService/GetBackendStats": true, + "/service.NodeService/SyncUser": true, + "/service.NodeService/SyncUsers": true, } var sessionIDMethods = map[string]bool{ - "/service.NodeService/Stop": true, - "/service.NodeService/GetBaseInfo": true, - "/service.NodeService/GetLogs": true, - "/service.NodeService/GetSystemStats": true, - "/service.NodeService/GetOutboundsStats": true, - "/service.NodeService/GetOutboundStats": true, - "/service.NodeService/GetInboundsStats": true, - "/service.NodeService/GetInboundStats": true, - "/service.NodeService/GetUsersStats": true, - "/service.NodeService/GetUserStats": true, - "/service.NodeService/GetUserOnlineStats": true, - "/service.NodeService/GetBackendStats": true, - "/service.NodeService/SyncUser": true, - "/service.NodeService/SyncUsers": true, + "/service.NodeService/Stop": true, + "/service.NodeService/GetBaseInfo": true, + "/service.NodeService/GetLogs": true, + "/service.NodeService/GetSystemStats": true, + "/service.NodeService/GetOutboundsStats": true, + "/service.NodeService/GetOutboundStats": true, + "/service.NodeService/GetInboundsStats": true, + "/service.NodeService/GetInboundStats": true, + "/service.NodeService/GetUsersStats": true, + "/service.NodeService/GetUserStats": true, + "/service.NodeService/GetUserOnlineStats": true, + "/service.NodeService/GetUserOnlineIpListStats": true, + "/service.NodeService/GetBackendStats": true, + "/service.NodeService/SyncUser": true, + "/service.NodeService/SyncUsers": true, } func ConditionalMiddleware(s *Service) grpc.UnaryServerInterceptor { diff --git a/controller/rpc/user.go b/controller/rpc/user.go index 2f7185e..47ae26e 100644 --- a/controller/rpc/user.go +++ b/controller/rpc/user.go @@ -3,6 +3,7 @@ package rpc import ( "context" "errors" + "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/tools/tls.go b/tools/tls.go index 2cee541..9f22925 100644 --- a/tools/tls.go +++ b/tools/tls.go @@ -28,7 +28,6 @@ func LoadTLSCredentials(cert, key, poolCert string, isClient bool) (*tls.Config, } if isClient { config.RootCAs = certPool - config.InsecureSkipVerify = true } else { config.ClientAuth = tls.RequireAndVerifyClientCert config.ClientCAs = certPool