From 76095bde0fd09af73f01bb153b4beb78c334052d Mon Sep 17 00:00:00 2001 From: Qian Wang Date: Sun, 4 Aug 2019 21:10:59 +0100 Subject: [PATCH] Add user bypass feature --- README.md | 4 + cmd/ck-server/ck-server.go | 11 +- example_config/ckserver.json | 3 + internal/server/activeuser.go | 30 ++--- internal/server/activeuser_test.go | 118 ++++++++++++++++++++ internal/server/state.go | 27 ++++- internal/server/usermanager/localmanager.go | 4 + internal/server/userpanel.go | 35 ++++++ internal/server/userpanel_test.go | 69 ++++++++++++ 9 files changed, 281 insertions(+), 20 deletions(-) create mode 100644 internal/server/activeuser_test.go create mode 100644 internal/server/userpanel_test.go diff --git a/README.md b/README.md index 894f175..1e4878c 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ Then run `make client` or `make server`. Output binary will be in `build` folder 6. [Configure the proxy program.](https://github.com/cbeuw/Cloak/wiki/Underlying-proxy-configuration-guides) Run `sudo ck-server -c `. ck-server needs root privilege because it binds to a low numbered port (443). Alternatively you can follow https://superuser.com/a/892391 to avoid granting ck-server root privilege unnecessarily. #### To add users +##### Unrestricted users +Run `ck-server -u` and add the UID into the `BypassUID` field in `ckserver.json` + +##### Users subject to bandwidth and credit controls 1. On your client, run `ck-client -s -l -a -c ` to enter admin mode 2. Visit https://cbeuw.github.io/Cloak-panel (Note: this is a static site, there is no backend and all data entered into this site are processed between your browser and the Cloak API endpoint you specified. Alternatively you can download the repo at https://github.com/cbeuw/Cloak-panel and host it on your own web server). 3. Type in 127.0.0.1: as the API Base, and click `List`. diff --git a/cmd/ck-server/ck-server.go b/cmd/ck-server/ck-server.go index b18bb77..0a18f71 100644 --- a/cmd/ck-server/ck-server.go +++ b/cmd/ck-server/ck-server.go @@ -116,7 +116,12 @@ func dispatchConnection(conn net.Conn, sta *server.State) { } } - user, err := sta.Panel.GetUser(UID) + var user *server.ActiveUser + if sta.IsBypass(UID) { + user, err = sta.Panel.GetBypassUser(UID) + } else { + user, err = sta.Panel.GetUser(UID) + } if err != nil { log.WithFields(log.Fields{ "UID": b64(UID), @@ -129,7 +134,7 @@ func dispatchConnection(conn net.Conn, sta *server.State) { sesh, existing, err := user.GetSession(sessionID, obfuscator, util.ReadTLS) if err != nil { - user.DelSession(sessionID) + user.DeleteSession(sessionID, "") log.Error(err) return } @@ -165,7 +170,7 @@ func dispatchConnection(conn net.Conn, sta *server.State) { "sessionID": sessionID, "reason": sesh.TerminalMsg(), }).Info("Session closed") - user.DelSession(sessionID) + user.DeleteSession(sessionID, "") return } else { continue diff --git a/example_config/ckserver.json b/example_config/ckserver.json index 9eeac85..e6523b7 100644 --- a/example_config/ckserver.json +++ b/example_config/ckserver.json @@ -4,6 +4,9 @@ "openvpn": "127.0.0.1:8389", "tor": "127.0.0.1:9001" }, + "BypassUID": [ + "1rmq6Ag1jZJCImLBIL5wzQ==" + ], "RedirAddr": "204.79.197.200:443", "PrivateKey": "EN5aPEpNBO+vw+BtFQY2OnK9bQU7rvEj5qmnmgwEtUc=", "AdminUID": "5nneblJy6lniPJfr81LuYQ==", diff --git a/internal/server/activeuser.go b/internal/server/activeuser.go index 605d4fd..b8de9a7 100644 --- a/internal/server/activeuser.go +++ b/internal/server/activeuser.go @@ -14,18 +14,22 @@ type ActiveUser struct { valve *mux.Valve + bypass bool + sessionsM sync.RWMutex sessions map[uint32]*mux.Session } -func (u *ActiveUser) DelSession(sessionID uint32) { +func (u *ActiveUser) DeleteSession(sessionID uint32, reason string) { u.sessionsM.Lock() - delete(u.sessions, sessionID) + sesh, existing := u.sessions[sessionID] + if existing { + delete(u.sessions, sessionID) + sesh.SetTerminalMsg(reason) + sesh.Close() + } if len(u.sessions) == 0 { - u.panel.updateUsageQueueForOne(u) - u.panel.activeUsersM.Lock() - delete(u.panel.activeUsers, u.arrUID) - u.panel.activeUsersM.Unlock() + u.panel.DeleteActiveUser(u) } u.sessionsM.Unlock() } @@ -36,9 +40,11 @@ func (u *ActiveUser) GetSession(sessionID uint32, obfuscator *mux.Obfuscator, un if sesh = u.sessions[sessionID]; sesh != nil { return sesh, true, nil } else { - err := u.panel.Manager.AuthoriseNewSession(u.arrUID[:], len(u.sessions)) - if err != nil { - return nil, false, err + if !u.bypass { + err := u.panel.Manager.AuthoriseNewSession(u.arrUID[:], len(u.sessions)) + if err != nil { + return nil, false, err + } } sesh = mux.MakeSession(sessionID, u.valve, obfuscator, unitReader) u.sessions[sessionID] = sesh @@ -52,12 +58,10 @@ func (u *ActiveUser) Terminate(reason string) { if reason != "" { sesh.SetTerminalMsg(reason) } - go sesh.Close() + sesh.Close() } u.sessionsM.Unlock() - u.panel.activeUsersM.Lock() - delete(u.panel.activeUsers, u.arrUID) - u.panel.activeUsersM.Unlock() + u.panel.DeleteActiveUser(u) } func (u *ActiveUser) NumSession() int { diff --git a/internal/server/activeuser_test.go b/internal/server/activeuser_test.go new file mode 100644 index 0000000..1373a01 --- /dev/null +++ b/internal/server/activeuser_test.go @@ -0,0 +1,118 @@ +package server + +import ( + "encoding/base64" + mux "github.com/cbeuw/Cloak/internal/multiplex" + "github.com/cbeuw/Cloak/internal/server/usermanager" + "os" + "testing" +) + +func TestActiveUser_Bypass(t *testing.T) { + manager, err := usermanager.MakeLocalManager(MOCK_DB_NAME) + if err != nil { + t.Error("failed to make local manager", err) + } + panel := MakeUserPanel(manager) + UID, _ := base64.StdEncoding.DecodeString("u97xvcc5YoQA8obCyt9q/w==") + user, _ := panel.GetBypassUser(UID) + obfuscator := &mux.Obfuscator{ + nil, + nil, + nil, + } + var sesh0 *mux.Session + var existing bool + var sesh1 *mux.Session + t.Run("get first session", func(t *testing.T) { + sesh0, existing, err = user.GetSession(0, obfuscator, nil) + if err != nil { + t.Error(err) + } + if existing { + t.Error("first session returned as existing") + } + if sesh0 == nil { + t.Error("no session returned") + } + }) + t.Run("get first session again", func(t *testing.T) { + seshx, existing, err := user.GetSession(0, obfuscator, nil) + if err != nil { + t.Error(err) + } + if !existing { + t.Error("first session get again returned as not existing") + } + if seshx == nil { + t.Error("no session returned") + } + if seshx != sesh0 { + t.Error("returned a different instance") + } + }) + t.Run("get second session", func(t *testing.T) { + sesh1, existing, err = user.GetSession(1, obfuscator, nil) + if err != nil { + t.Error(err) + } + if existing { + t.Error("second session returned as existing") + } + if sesh0 == nil { + t.Error("no session returned") + } + }) + t.Run("number of sessions", func(t *testing.T) { + if user.NumSession() != 2 { + t.Error("number of session is not 2") + } + }) + t.Run("delete a session", func(t *testing.T) { + user.DeleteSession(0, "") + if user.NumSession() != 1 { + t.Error("number of session is not 1 after deleting one") + } + if !sesh0.IsClosed() { + t.Error("session not closed after deletion") + } + }) + t.Run("terminating user", func(t *testing.T) { + user.Terminate("") + if panel.isActive(user.arrUID[:]) { + t.Error("user is still active after termination") + } + if !sesh1.IsClosed() { + t.Error("session not closed after user termination") + } + }) + t.Run("get session again after termination", func(t *testing.T) { + seshx, existing, err := user.GetSession(0, obfuscator, nil) + if err != nil { + t.Error(err) + } + if existing { + t.Error("session returned as existing") + } + if seshx == nil { + t.Error("no session returned") + } + if seshx == sesh0 || seshx == sesh1 { + t.Error("get session after termination returned the same instance") + } + }) + t.Run("delete last session", func(t *testing.T) { + user.DeleteSession(0, "") + if panel.isActive(user.arrUID[:]) { + t.Error("user still active after last session deleted") + } + }) + err = manager.Close() + if err != nil { + t.Error("failed to close localmanager", err) + } + err = os.Remove(MOCK_DB_NAME) + if err != nil { + t.Error("failed to delete mockdb", err) + } +} diff --git a/internal/server/state.go b/internal/server/state.go index cd50e11..6f80a44 100644 --- a/internal/server/state.go +++ b/internal/server/state.go @@ -15,6 +15,7 @@ import ( type rawConfig struct { ProxyBook map[string]string + BypassUID [][]byte RedirAddr string PrivateKey string AdminUID string @@ -31,7 +32,9 @@ type State struct { Now func() time.Time AdminUID []byte - staticPv crypto.PrivateKey + + BypassUID map[[16]byte]struct{} + staticPv crypto.PrivateKey RedirAddr string @@ -44,9 +47,10 @@ type State struct { func InitState(bindHost, bindPort string, nowFunc func() time.Time) (*State, error) { ret := &State{ - BindHost: bindHost, - BindPort: bindPort, - Now: nowFunc, + BindHost: bindHost, + BindPort: bindPort, + Now: nowFunc, + BypassUID: make(map[[16]byte]struct{}), } ret.usedRandom = make(map[[32]byte]int64) go ret.UsedRandomCleaner() @@ -99,9 +103,24 @@ func (sta *State) ParseConfig(conf string) (err error) { return errors.New("Failed to decode AdminUID: " + err.Error()) } sta.AdminUID = adminUID + + var arrUID [16]byte + for _, UID := range preParse.BypassUID { + copy(arrUID[:], UID) + sta.BypassUID[arrUID] = struct{}{} + } + copy(arrUID[:], adminUID) + sta.BypassUID[arrUID] = struct{}{} return nil } +func (sta *State) IsBypass(UID []byte) bool { + var arrUID [16]byte + copy(arrUID[:], UID) + _, exist := sta.BypassUID[arrUID] + return exist +} + // This is the accepting window of the encrypted timestamp from client // we reject the client if the timestamp is outside of this window. // This is for replay prevention so that we don't have to save unlimited amount of diff --git a/internal/server/usermanager/localmanager.go b/internal/server/usermanager/localmanager.go index e4ccdb0..99ba3a1 100644 --- a/internal/server/usermanager/localmanager.go +++ b/internal/server/usermanager/localmanager.go @@ -195,3 +195,7 @@ func (manager *localManager) UploadStatus(uploads []StatusUpdate) ([]StatusRespo }) return responses, err } + +func (manager *localManager) Close() error { + return manager.db.Close() +} diff --git a/internal/server/userpanel.go b/internal/server/userpanel.go index 28df720..73987cf 100644 --- a/internal/server/userpanel.go +++ b/internal/server/userpanel.go @@ -29,6 +29,26 @@ func MakeUserPanel(manager usermanager.UserManager) *userPanel { return ret } +func (panel *userPanel) GetBypassUser(UID []byte) (*ActiveUser, error) { + panel.activeUsersM.Lock() + var arrUID [16]byte + copy(arrUID[:], UID) + if user, ok := panel.activeUsers[arrUID]; ok { + panel.activeUsersM.Unlock() + return user, nil + } + user := &ActiveUser{ + panel: panel, + valve: mux.UNLIMITED_VALVE, + sessions: make(map[uint32]*mux.Session), + bypass: true, + } + copy(user.arrUID[:], UID) + panel.activeUsers[user.arrUID] = user + panel.activeUsersM.Unlock() + return user, nil +} + func (panel *userPanel) GetUser(UID []byte) (*ActiveUser, error) { panel.activeUsersM.Lock() var arrUID [16]byte @@ -49,12 +69,20 @@ func (panel *userPanel) GetUser(UID []byte) (*ActiveUser, error) { valve: valve, sessions: make(map[uint32]*mux.Session), } + copy(user.arrUID[:], UID) panel.activeUsers[user.arrUID] = user panel.activeUsersM.Unlock() return user, nil } +func (panel *userPanel) DeleteActiveUser(user *ActiveUser) { + panel.updateUsageQueueForOne(user) + panel.activeUsersM.Lock() + delete(panel.activeUsers, user.arrUID) + panel.activeUsersM.Unlock() +} + func (panel *userPanel) isActive(UID []byte) bool { var arrUID [16]byte copy(arrUID[:], UID) @@ -73,6 +101,10 @@ func (panel *userPanel) updateUsageQueue() { panel.activeUsersM.Lock() panel.usageUpdateQueueM.Lock() for _, user := range panel.activeUsers { + if user.bypass { + continue + } + upIncured, downIncured := user.valve.Nullify() if usage, ok := panel.usageUpdateQueue[user.arrUID]; ok { atomic.AddInt64(usage.up, upIncured) @@ -89,6 +121,9 @@ func (panel *userPanel) updateUsageQueue() { func (panel *userPanel) updateUsageQueueForOne(user *ActiveUser) { // used when one particular user deactivates + if user.bypass { + return + } upIncured, downIncured := user.valve.Nullify() panel.usageUpdateQueueM.Lock() if usage, ok := panel.usageUpdateQueue[user.arrUID]; ok { diff --git a/internal/server/userpanel_test.go b/internal/server/userpanel_test.go new file mode 100644 index 0000000..0d3ea65 --- /dev/null +++ b/internal/server/userpanel_test.go @@ -0,0 +1,69 @@ +package server + +import ( + "encoding/base64" + "github.com/cbeuw/Cloak/internal/server/usermanager" + "os" + "testing" +) + +const MOCK_DB_NAME = "userpanel_test_mock_database.db" + +func TestUserPanel_BypassUser(t *testing.T) { + manager, err := usermanager.MakeLocalManager(MOCK_DB_NAME) + if err != nil { + t.Error("failed to make local manager", err) + } + panel := MakeUserPanel(manager) + UID, _ := base64.StdEncoding.DecodeString("u97xvcc5YoQA8obCyt9q/w==") + user, _ := panel.GetBypassUser(UID) + user.valve.AddRx(10) + user.valve.AddTx(10) + t.Run("isActive", func(t *testing.T) { + a := panel.isActive(UID) + if !a { + t.Error("isActive returned ", a) + } + }) + t.Run("updateUsageQueue", func(t *testing.T) { + panel.updateUsageQueue() + if user.valve.GetRx() != 10 || user.valve.GetTx() != 10 { + t.Error("user rx or tx info altered") + } + if _, inQ := panel.usageUpdateQueue[user.arrUID]; inQ { + t.Error("user in update queue") + } + }) + t.Run("updateUsageQueueForOne", func(t *testing.T) { + panel.updateUsageQueueForOne(user) + if user.valve.GetRx() != 10 || user.valve.GetTx() != 10 { + t.Error("user rx or tx info altered") + } + if _, inQ := panel.usageUpdateQueue[user.arrUID]; inQ { + t.Error("user in update queue") + } + }) + t.Run("commitUpdate", func(t *testing.T) { + err := panel.commitUpdate() + if err != nil { + t.Error("commit returned", err) + } + }) + t.Run("DeleteActiveUser", func(t *testing.T) { + panel.DeleteActiveUser(user) + if panel.isActive(user.arrUID[:]) { + t.Error("user still active after deletion", err) + } + }) + t.Run("Repeated delete", func(t *testing.T) { + panel.DeleteActiveUser(user) + }) + err = manager.Close() + if err != nil { + t.Error("failed to close localmanager", err) + } + err = os.Remove(MOCK_DB_NAME) + if err != nil { + t.Error("failed to delete mockdb", err) + } +}