Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down
25 changes: 15 additions & 10 deletions backend/xray/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{}
Expand Down
2 changes: 1 addition & 1 deletion backend/xray/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
}
},
{
"tag": "Shadowsocks 2024",
"tag": "Shadowsocks 2022",
"port": 1234,
"protocol": "shadowsocks",
"settings": {
Expand Down
91 changes: 51 additions & 40 deletions backend/xray/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
23 changes: 17 additions & 6 deletions backend/xray/xray.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions backend/xray/xray_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions common/helper.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package common

import (
"crypto/sha256"
"encoding/base64"
"io"
"net/http"
"strings"

"google.golang.org/protobuf/proto"
)
Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions controller/rest/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func (s *Service) checkSessionIDMiddleware(next http.Handler) http.Handler {
return
}

s.NewRequest()
next.ServeHTTP(w, r)
})
}
Expand Down
14 changes: 10 additions & 4 deletions controller/rpc/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rpc
import (
"context"
"errors"
"log"
"net"

"github.com/m03ed/gozargah-node/backend"
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading