Skip to content

Commit 276512f

Browse files
committed
By EbraSha
1 parent fc9c0a5 commit 276512f

File tree

8 files changed

+750
-323
lines changed

8 files changed

+750
-323
lines changed

Critical configuration files/users.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"role": "admin",
66
"blocked_domains": [],
77
"blocked_ips": [],
8-
"log": "no"
8+
"log": "no",
9+
"max_sessions": 1,
10+
"session_ttl_seconds": 300
911
},
1012
{
1113
"username": "user1",
@@ -24,7 +26,9 @@
2426
"10.0.0.*",
2527
"172.16.*.*"
2628
],
27-
"log": "yes"
29+
"log": "yes",
30+
"max_sessions": 2,
31+
"session_ttl_seconds": 300
2832
},
2933
{
3034
"username": "user2",
@@ -40,6 +44,8 @@
4044
"192.168.10.1",
4145
"10.10.10.10"
4246
],
43-
"log": "yes"
47+
"log": "yes",
48+
"max_sessions": 5,
49+
"session_ttl_seconds": 300
4450
}
4551
]

README.fa.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
- **مدیریت تنظیمات**: تنظیمات سرور مبتنی بر JSON
4646
- **سیستم ثبت**: ثبت جامع اتصالات و حملات
4747

48+
## 🧾 قابلیت های اکانتینگ
49+
- **کنترل داخلی سشن‌ها**: مدیر می‌تواند حداکثر تعداد سشن‌های فعال همزمان برای هر حساب را تنظیم کند تا میزان اتصال کاربران کنترل شود.
50+
- **انقضای خودکار سشن‌ها**: برای هر سشن مدت زمان اعتبار (TTL) تعریف می‌شود تا پس از پایان آن، سشن منقضی شده و منابع آزاد شوند.
51+
- **مدیریت پویا در زمان محدودیت**: وقتی تعداد سشن‌ها به حد مجاز برسد، اتصال‌های جدید می‌توانند رد شوند یا در صف انتظار قرار گیرند (قابل تنظیم توسط مدیر).
52+
- **نظارت لحظه‌ای بر سشن‌ها**: سیستم وضعیت سشن‌های فعال را به‌صورت زنده ثبت کرده و برای حسابرسی و تحلیل ذخیره می‌کند.
53+
54+
55+
4856
## 📋 نیازمندی‌ها
4957

5058
- Go 1.19 یا بالاتر
@@ -92,7 +100,9 @@
92100
"role": "admin",
93101
"blocked_domains": [],
94102
"blocked_ips": [],
95-
"log": "no"
103+
"log": "no",
104+
"max_sessions": 1,
105+
"session_ttl_seconds": 300
96106
},
97107
{
98108
"username": "user1",
@@ -111,7 +121,9 @@
111121
"10.0.0.*",
112122
"172.16.*.*"
113123
],
114-
"log": "yes"
124+
"log": "yes",
125+
"max_sessions": 2,
126+
"session_ttl_seconds": 300
115127
},
116128
{
117129
"username": "user2",
@@ -127,7 +139,9 @@
127139
"192.168.10.1",
128140
"10.10.10.10"
129141
],
130-
"log": "yes"
142+
"log": "yes",
143+
"max_sessions": 5,
144+
"session_ttl_seconds": 300
131145
}
132146
]
133147

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ A high-performance SSH-based tunneling server designed for secure internet acces
4141
- **Configuration Management**: JSON-based server configuration
4242
- **Logging System**: Comprehensive logging of connections and attacks
4343

44+
### 🧾 Accounting Features
45+
- **Built-in Session Control**: Administrators can define how many concurrent sessions each account can open at the same time.
46+
- **Automatic Session Expiration**: Each session has a defined Time To Live (TTL). Expired sessions are automatically terminated to free resources.
47+
- **Dynamic Connection Handling**: When the session limit is reached, new connections can be rejected or queued — fully configurable.
48+
- **Real-time Session Monitoring**: Tracks and logs all active sessions in real time for auditing and analytics.
49+
50+
4451
## 📋 Requirements
4552

4653
- Go 1.19 or higher
@@ -89,7 +96,9 @@ A high-performance SSH-based tunneling server designed for secure internet acces
8996
"role": "admin",
9097
"blocked_domains": [],
9198
"blocked_ips": [],
92-
"log": "no"
99+
"log": "no",
100+
"max_sessions": 1,
101+
"session_ttl_seconds": 300
93102
},
94103
{
95104
"username": "user1",
@@ -108,7 +117,9 @@ A high-performance SSH-based tunneling server designed for secure internet acces
108117
"10.0.0.*",
109118
"172.16.*.*"
110119
],
111-
"log": "yes"
120+
"log": "yes",
121+
"max_sessions": 2,
122+
"session_ttl_seconds": 300
112123
},
113124
{
114125
"username": "user2",
@@ -124,7 +135,9 @@ A high-performance SSH-based tunneling server designed for secure internet acces
124135
"192.168.10.1",
125136
"10.10.10.10"
126137
],
127-
"log": "yes"
138+
"log": "yes",
139+
"max_sessions": 5,
140+
"session_ttl_seconds": 300
128141
}
129142
]
130143

go.mod

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ go 1.24
44

55
require (
66
github.com/UserExistsError/conpty v0.1.4
7+
github.com/creack/pty v1.1.24
8+
go.etcd.io/bbolt v1.3.10
79
golang.org/x/crypto v0.39.0
8-
)
9-
10-
require (
11-
github.com/creack/pty v1.1.24 // indirect
12-
golang.org/x/sys v0.33.0 // indirect
13-
golang.org/x/term v0.32.0 // indirect
10+
golang.org/x/sys v0.33.0
1411
)

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@ github.com/UserExistsError/conpty v0.1.4 h1:+3FhJhiqhyEJa+K5qaK3/w6w+sN3Nh9O9VbJ
22
github.com/UserExistsError/conpty v0.1.4/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I=
33
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
44
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
5+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
10+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
11+
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
12+
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
513
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
614
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
15+
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
16+
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
717
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
818
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
919
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
1020
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
1121
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
22+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
23+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import (
3636
var trafficMap sync.Map // key: username, value: *TrafficStats
3737
var activeConnections sync.Map // key: connection ID, value: connection info
3838

39+
// Session metadata storage: sessionID -> connection metadata for session management
40+
var sessionMetadata sync.Map // key: sessionID, value: sessionInfo (username, ip, etc.)
41+
3942
// SetExecutableDir changes current working directory to exe dir and returns it.
4043
func SetExecutableDir() (string, error) {
4144
exePath, err := os.Executable()
@@ -94,12 +97,14 @@ type TrafficStats struct {
9497

9598
// User structure with role-based access control, domain/IP blocking, and logging
9699
type User struct {
97-
Username string `json:"username"`
98-
Password string `json:"password"`
99-
Role string `json:"role"` // "user" or "admin"
100-
BlockedDomains []string `json:"blocked_domains"` // List of blocked domains/IPs with wildcard support
101-
BlockedIPs []string `json:"blocked_ips"` // List of blocked IPs with wildcard support
102-
Log string `json:"log"` // "yes" or "no" - enable/disable user access logging
100+
Username string `json:"username"`
101+
Password string `json:"password"`
102+
Role string `json:"role"` // "user" or "admin"
103+
BlockedDomains []string `json:"blocked_domains"` // List of blocked domains/IPs with wildcard support
104+
BlockedIPs []string `json:"blocked_ips"` // List of blocked IPs with wildcard support
105+
Log string `json:"log"` // "yes" or "no" - enable/disable user access logging
106+
MaxSessions int `json:"max_sessions"` // Maximum number of concurrent sessions (default: 2)
107+
SessionTTLSeconds int `json:"session_ttl_seconds"` // Session TTL in seconds (default: 300)
103108
}
104109

105110
var users map[string]User
@@ -139,6 +144,13 @@ func loadUsers(path string) {
139144
// New format: array of User objects
140145
users = make(map[string]User)
141146
for _, user := range userList {
147+
// Set default values if not specified
148+
if user.MaxSessions <= 0 {
149+
user.MaxSessions = 2 // Default max sessions
150+
}
151+
if user.SessionTTLSeconds <= 0 {
152+
user.SessionTTLSeconds = 300 // Default TTL: 5 minutes (300 seconds)
153+
}
142154
users[user.Username] = user
143155
}
144156
log.Printf("✅ Loaded %d users with role-based access control", len(users))
@@ -155,9 +167,11 @@ func loadUsers(path string) {
155167
users = make(map[string]User)
156168
for username, password := range oldUserPass {
157169
users[username] = User{
158-
Username: username,
159-
Password: password,
160-
Role: "user", // Default role for backward compatibility
170+
Username: username,
171+
Password: password,
172+
Role: "user", // Default role for backward compatibility
173+
MaxSessions: 2, // Default max sessions
174+
SessionTTLSeconds: 300, // Default TTL: 5 minutes
161175
}
162176
}
163177
log.Printf("✅ Loaded %d users from legacy format (all set to 'user' role)", len(users))
@@ -371,8 +385,33 @@ func createSSHConfig() *ssh.ServerConfig {
371385

372386
if user, ok := users[c.User()]; ok && user.Password == string(pass) {
373387
delete(failedAttempts, ip) // reset count
374-
log.Printf("✅ User %s (%s) authenticated from %s", c.User(), user.Role, ip)
375-
return nil, nil
388+
389+
// Get session manager
390+
sm := GetSessionManager()
391+
392+
// Get client version (before handshake, we don't have full metadata yet)
393+
// We'll use IP and username for now, and update with client version in handleConnection
394+
clientVersion := "SSH-2.0-Unknown" // Will be updated in handleConnection
395+
396+
// Create session
397+
sessionID, err := sm.CreateSession(c.User(), ip, clientVersion)
398+
if err != nil {
399+
log.Printf("⚠️ Failed to create session for %s: %v", c.User(), err)
400+
return nil, fmt.Errorf("session creation failed")
401+
}
402+
403+
// Store sessionID in memory for later use in handleConnection
404+
sessionMetadata.Store(c.User()+"|"+ip, sessionID)
405+
406+
log.Printf("✅ User %s (%s) authenticated from %s [Session: %s]", c.User(), user.Role, ip, sessionID[:16]+"...")
407+
408+
// Return sessionID in permissions for later retrieval
409+
perms := &ssh.Permissions{
410+
Extensions: map[string]string{
411+
"session_id": sessionID,
412+
},
413+
}
414+
return perms, nil
376415
}
377416

378417
logInvalidLogin(c.User(), string(pass), ip, clientPort, serverPort)
@@ -482,7 +521,7 @@ func handleSession(channel ssh.Channel, requests <-chan *ssh.Request, username s
482521
██████╔╝███████╗██║░░██║░░╚██╔╝░░███████╗██║░░██║
483522
╚═════╝░╚══════╝╚═╝░░╚═╝░░░╚═╝░░░╚══════╝╚═╝░░╚═╝
484523
485-
🛡️ Welcome to Abdal 4iProto Server ver 5.10
524+
🛡️ Welcome to Abdal 4iProto Server ver 6.2
486525
🧠 Developed by: Ebrahim Shafiei (EbraSha)
487526
✉️ Prof.Shafiei@Gmail.com
488527
@@ -515,14 +554,89 @@ func handleConnection(conn net.Conn, config *ssh.ServerConfig) {
515554
}
516555
defer sshConn.Close()
517556

557+
// Get session manager
558+
sm := GetSessionManager()
559+
560+
// Extract sessionID from permissions (set during PasswordCallback)
561+
username := sshConn.User()
562+
userIP, _, _ := net.SplitHostPort(sshConn.RemoteAddr().String())
563+
clientVersionBytes := sshConn.ClientVersion()
564+
clientVersion := string(clientVersionBytes)
565+
566+
var sessionID string
567+
// Try to get sessionID from permissions first
568+
perms := sshConn.Permissions
569+
if perms != nil && perms.Extensions != nil {
570+
if sid, ok := perms.Extensions["session_id"]; ok {
571+
sessionID = sid
572+
}
573+
}
574+
575+
// If not found in permissions, try to get from memory (fallback)
576+
if sessionID == "" {
577+
if sid, ok := sessionMetadata.Load(username + "|" + userIP); ok {
578+
sessionID = sid.(string)
579+
}
580+
}
581+
582+
// Validate session if sessionID exists
583+
if sessionID != "" {
584+
// Check if session is valid
585+
if !sm.IsSessionValid(sessionID) {
586+
log.Printf("🔒 Invalid or expired session for user %s from %s, closing connection", username, userIP)
587+
return // Connection will be closed by defer
588+
}
589+
590+
// Register connection with session manager
591+
sm.RegisterConnection(sessionID, sshConn)
592+
593+
// Update client version (was unknown during PasswordCallback)
594+
if err := sm.UpdateSessionClientVersion(sessionID, clientVersion); err != nil {
595+
log.Printf("⚠️ Failed to update session client version: %v", err)
596+
}
597+
598+
// Update last seen
599+
if err := sm.UpdateSessionLastSeen(sessionID); err != nil {
600+
log.Printf("⚠️ Failed to update session last seen: %v", err)
601+
}
602+
603+
// Unregister on connection close
604+
defer func() {
605+
sm.UnregisterConnection(sessionID)
606+
sm.CloseSession(sessionID)
607+
}()
608+
609+
log.Printf("🔐 Session validated: %s for user %s from %s", sessionID[:16]+"...", username, userIP)
610+
} else {
611+
log.Printf("⚠️ No sessionID found for user %s from %s, connection may not be tracked", username, userIP)
612+
}
613+
518614
// Track active connection
519615
connID := fmt.Sprintf("%s-%d", sshConn.RemoteAddr().String(), time.Now().UnixNano())
520616
activeConnections.Store(connID, sshConn.RemoteAddr().String())
521617
defer activeConnections.Delete(connID)
522618

523-
log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
619+
log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), clientVersion)
524620
go ssh.DiscardRequests(reqs)
525621

622+
// Start periodic session last seen update if session exists
623+
if sessionID != "" {
624+
go func() {
625+
ticker := time.NewTicker(30 * time.Second) // Update every 30 seconds
626+
defer ticker.Stop()
627+
for range ticker.C {
628+
if !sm.IsSessionValid(sessionID) {
629+
log.Printf("🔒 Session expired for user %s, stopping updates", username)
630+
return
631+
}
632+
if err := sm.UpdateSessionLastSeen(sessionID); err != nil {
633+
log.Printf("⚠️ Failed to update session last seen: %v", err)
634+
return
635+
}
636+
}
637+
}()
638+
}
639+
526640
for newChannel := range chans {
527641

528642
// Accessing username and IP address
@@ -962,6 +1076,10 @@ func startServer() {
9621076

9631077
// Load existing traffic files to preserve traffic statistics across restarts
9641078
loadExistingTrafficFiles()
1079+
1080+
// Initialize session manager and start cleanup routine
1081+
sm := GetSessionManager()
1082+
sm.StartCleanupRoutine()
9651083

9661084
for _, port := range serverConfig.Ports {
9671085
go func(p int) {

0 commit comments

Comments
 (0)