From e4c38b29fdd6cd5edc6805988b1252cb9c7bffe8 Mon Sep 17 00:00:00 2001 From: lwch Date: Tue, 18 Jan 2022 11:32:30 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=96=B9=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 20d78d3..2ffdbbd 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/lwch/runtime v0.0.0-20190520054850-8c97e19e0c6d github.com/lwch/yaml v0.0.0-20211206085137-772f80e85a26 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect - golang.org/x/sys v0.0.0-20220111092808-5a964db01320 + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 golang.org/x/text v0.3.7 golang.org/x/tools v0.1.8 // indirect google.golang.org/protobuf v1.27.1 diff --git a/go.sum b/go.sum index d2e64ea..335abe1 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe h1:W8vbETX/n8S6EmY0Pu4Ix7Vvp golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From 8a60567799225a6cdbbf8f28ea517cdf1ba34c23 Mon Sep 17 00:00:00 2001 From: lwch Date: Fri, 21 Jan 2022 18:45:39 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E6=B1=A0=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/client/app/app.go | 76 ++++----- code/client/app/callback.go | 73 +++++++++ code/client/app/connect.go | 76 --------- code/client/conn/conn.go | 167 ++++++++++++++++++++ code/client/conn/send.go | 18 +++ code/client/{pool => conn}/send_conn.go | 47 +++--- code/client/{pool => conn}/send_shell.go | 12 +- code/client/{pool => conn}/send_vnc.go | 37 ++--- code/client/dashboard/dashboard.go | 8 +- code/client/dashboard/h_info.go | 2 +- code/client/global/conf.go | 5 - code/client/pool/conn.go | 190 ----------------------- code/client/pool/pool.go | 120 -------------- code/client/pool/send.go | 45 ------ code/client/rule/bench/bench.go | 8 +- code/client/rule/bench/h_http.go | 6 +- code/client/rule/mgr.go | 4 +- code/client/rule/shell/h_new.go | 17 +- code/client/rule/shell/h_resize.go | 4 +- code/client/rule/shell/h_ws.go | 4 +- code/client/rule/shell/link.go | 25 ++- code/client/rule/shell/shell.go | 38 +++-- code/client/rule/vnc/events.go | 18 +-- code/client/rule/vnc/h_clipboard.go | 18 +-- code/client/rule/vnc/h_ctrl.go | 7 +- code/client/rule/vnc/h_new.go | 17 +- code/client/rule/vnc/h_ws.go | 13 +- code/client/rule/vnc/link.go | 32 ++-- code/client/rule/vnc/vnc.go | 40 ++--- code/network/msg.pb.go | 143 ++++++++--------- code/network/msg.proto | 2 - code/server/handler/client.go | 32 ++-- code/server/handler/clients.go | 50 ++---- code/server/handler/handler.go | 143 +++++++---------- conf/common.yaml | 1 - 35 files changed, 600 insertions(+), 898 deletions(-) create mode 100644 code/client/app/callback.go delete mode 100644 code/client/app/connect.go create mode 100644 code/client/conn/conn.go create mode 100644 code/client/conn/send.go rename code/client/{pool => conn}/send_conn.go (83%) rename code/client/{pool => conn}/send_shell.go (70%) rename code/client/{pool => conn}/send_vnc.go (73%) delete mode 100644 code/client/pool/conn.go delete mode 100644 code/client/pool/pool.go delete mode 100644 code/client/pool/send.go diff --git a/code/client/app/app.go b/code/client/app/app.go index 640f291..d8c9eeb 100644 --- a/code/client/app/app.go +++ b/code/client/app/app.go @@ -2,11 +2,10 @@ package app import ( rt "runtime" - "time" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/client/dashboard" "github.com/jkstack/natpass/code/client/global" - "github.com/jkstack/natpass/code/client/pool" "github.com/jkstack/natpass/code/client/rule" "github.com/jkstack/natpass/code/client/rule/bench" "github.com/jkstack/natpass/code/client/rule/shell" @@ -22,6 +21,7 @@ type App struct { confDir string cfg *global.Configure version string + conn *conn.Conn } // New create application @@ -52,67 +52,53 @@ func (a *App) run() { logging.SetSizeRotate(a.cfg.LogDir, "np-cli", int(a.cfg.LogSize.Bytes()), a.cfg.LogRotate, stdout) defer logging.Flush() - pl := pool.New(a.cfg) + a.conn = conn.New(a.cfg) mgr := rule.New() for _, t := range a.cfg.Rules { switch t.Type { case "shell": - sh := shell.New(t) + sh := shell.New(t, a.cfg.ReadTimeout, a.cfg.WriteTimeout) mgr.Add(sh) - go sh.Handle(pl) + go sh.Handle(a.conn) case "vnc": - v := vnc.New(t) + v := vnc.New(t, a.cfg.ReadTimeout, a.cfg.WriteTimeout) mgr.Add(v) - go v.Handle(pl) + go v.Handle(a.conn) case "bench": b := bench.New(t) mgr.Add(b) - go b.Handle(pl) + go b.Handle(a.conn) } } - for i := 0; i < a.cfg.Links-pl.Size(); i++ { - go func() { - for { - conn := pl.Get() - if conn == nil { - time.Sleep(time.Second) - continue + go func() { + for { + msg := <-a.conn.ChanUnknown() + var linkID string + switch msg.GetXType() { + case network.Msg_connect_req: + switch msg.GetCreq().GetXType() { + case network.ConnectRequest_shell: + a.shellCreate(mgr, a.conn, msg) + case network.ConnectRequest_vnc: + a.vncCreate(a.confDir, mgr, a.conn, msg) + case network.ConnectRequest_bench: + a.benchCreate(a.confDir, mgr, a.conn, msg) } - for { - msg := <-conn.ChanUnknown() - if msg == nil { - break - } - var linkID string - switch msg.GetXType() { - case network.Msg_connect_req: - switch msg.GetCreq().GetXType() { - case network.ConnectRequest_shell: - a.shellCreate(mgr, conn, msg) - case network.ConnectRequest_vnc: - a.vncCreate(a.confDir, mgr, conn, msg) - case network.ConnectRequest_bench: - a.benchCreate(a.confDir, mgr, conn, msg) - } - default: - linkID = msg.GetLinkId() - } - if len(linkID) > 0 { - logging.Error("link of %s on connection %d not found, type=%s", - linkID, conn.Idx, msg.GetXType().String()) - continue - } - } - logging.Info("connection %s-%d exited", a.cfg.ID, conn.Idx) - time.Sleep(time.Second) + default: + linkID = msg.GetLinkId() } - }() - } + if len(linkID) > 0 { + logging.Error("link of %s not found, type=%s", + linkID, msg.GetXType().String()) + continue + } + } + }() if a.cfg.DashboardEnabled { - db := dashboard.New(a.cfg, pl, mgr, a.version) + db := dashboard.New(a.cfg, a.conn, mgr, a.version) runtime.Assert(db.ListenAndServe(a.cfg.DashboardListen, a.cfg.DashboardPort)) } else { select {} diff --git a/code/client/app/callback.go b/code/client/app/callback.go new file mode 100644 index 0000000..c560681 --- /dev/null +++ b/code/client/app/callback.go @@ -0,0 +1,73 @@ +package app + +import ( + "github.com/jkstack/natpass/code/client/conn" + "github.com/jkstack/natpass/code/client/global" + "github.com/jkstack/natpass/code/client/rule" + "github.com/jkstack/natpass/code/client/rule/shell" + "github.com/jkstack/natpass/code/client/rule/vnc" + "github.com/jkstack/natpass/code/network" + "github.com/lwch/logging" +) + +func (a *App) shellCreate(mgr *rule.Mgr, conn *conn.Conn, msg *network.Msg) { + create := msg.GetCreq() + tn := mgr.Get(create.GetName(), msg.GetFrom()) + if tn == nil { + tn = shell.New(global.Rule{ + Name: create.GetName(), + Target: msg.GetFrom(), + Type: "shell", + Exec: create.GetCshell().GetExec(), + Env: create.GetCshell().GetEnv(), + }, a.cfg.ReadTimeout, a.cfg.WriteTimeout) + mgr.Add(tn) + } + lk := tn.NewLink(msg.GetLinkId(), msg.GetFrom(), nil, conn).(*shell.Link) + logging.Info("create link %s for shell rule [%s] from %s to %s", + msg.GetLinkId(), create.GetName(), + msg.GetFrom(), a.cfg.ID) + err := lk.Exec() + if err != nil { + logging.Error("create shell failed: %v", err) + conn.SendConnectError(msg.GetFrom(), msg.GetLinkId(), err.Error()) + return + } + conn.SendConnectOK(msg.GetFrom(), msg.GetLinkId()) + lk.Forward() +} + +func (a *App) vncCreate(confDir string, mgr *rule.Mgr, conn *conn.Conn, msg *network.Msg) { + create := msg.GetCreq() + tn := mgr.Get(create.GetName(), msg.GetFrom()) + if tn == nil { + tn = vnc.New(global.Rule{ + Name: create.GetName(), + Target: msg.GetFrom(), + Type: "vnc", + Fps: create.GetCvnc().GetFps(), + }, a.cfg.ReadTimeout, a.cfg.WriteTimeout) + mgr.Add(tn) + } + lk := tn.NewLink(msg.GetLinkId(), msg.GetFrom(), nil, conn).(*vnc.Link) + logging.Info("create link %s for vnc rule [%s] from %s to %s", + msg.GetLinkId(), create.GetName(), + msg.GetFrom(), a.cfg.ID) + lk.SetQuality(create.GetCvnc().GetQuality()) + err := lk.Fork(confDir) + if err != nil { + logging.Error("create vnc failed: %v", err) + conn.SendConnectError(msg.GetFrom(), msg.GetLinkId(), err.Error()) + return + } + conn.SendConnectOK(msg.GetFrom(), msg.GetLinkId()) + lk.Forward() +} + +func (a *App) benchCreate(confDir string, mgr *rule.Mgr, conn *conn.Conn, msg *network.Msg) { + create := msg.GetCreq() + logging.Info("create link %s for bench rule [%s] from %s to %s", + msg.GetLinkId(), create.GetName(), + msg.GetFrom(), a.cfg.ID) + conn.SendConnectOK(msg.GetFrom(), msg.GetLinkId()) +} diff --git a/code/client/app/connect.go b/code/client/app/connect.go deleted file mode 100644 index f2250d9..0000000 --- a/code/client/app/connect.go +++ /dev/null @@ -1,76 +0,0 @@ -package app - -import ( - "github.com/jkstack/natpass/code/client/global" - "github.com/jkstack/natpass/code/client/pool" - "github.com/jkstack/natpass/code/client/rule" - "github.com/jkstack/natpass/code/client/rule/shell" - "github.com/jkstack/natpass/code/client/rule/vnc" - "github.com/jkstack/natpass/code/network" - "github.com/lwch/logging" -) - -func (a *App) shellCreate(mgr *rule.Mgr, conn *pool.Conn, msg *network.Msg) { - create := msg.GetCreq() - tn := mgr.Get(create.GetName(), msg.GetFrom()) - if tn == nil { - tn = shell.New(global.Rule{ - Name: create.GetName(), - Target: msg.GetFrom(), - Type: "shell", - Exec: create.GetCshell().GetExec(), - Env: create.GetCshell().GetEnv(), - }) - mgr.Add(tn) - } - lk := tn.NewLink(msg.GetLinkId(), msg.GetFrom(), msg.GetFromIdx(), nil, conn).(*shell.Link) - logging.Info("create link %s for shell rule [%s] from %s-%d to %s-%d", - msg.GetLinkId(), create.GetName(), - msg.GetFrom(), msg.GetFromIdx(), - a.cfg.ID, conn.GetIdx()) - err := lk.Exec() - if err != nil { - logging.Error("create shell failed: %v", err) - conn.SendConnectError(msg.GetFrom(), msg.GetFromIdx(), msg.GetLinkId(), err.Error()) - return - } - conn.SendConnectOK(msg.GetFrom(), msg.GetFromIdx(), msg.GetLinkId()) - lk.Forward() -} - -func (a *App) vncCreate(confDir string, mgr *rule.Mgr, conn *pool.Conn, msg *network.Msg) { - create := msg.GetCreq() - tn := mgr.Get(create.GetName(), msg.GetFrom()) - if tn == nil { - tn = vnc.New(global.Rule{ - Name: create.GetName(), - Target: msg.GetFrom(), - Type: "vnc", - Fps: create.GetCvnc().GetFps(), - }) - mgr.Add(tn) - } - lk := tn.NewLink(msg.GetLinkId(), msg.GetFrom(), msg.GetFromIdx(), nil, conn).(*vnc.Link) - logging.Info("create link %s for vnc rule [%s] from %s-%d to %s-%d", - msg.GetLinkId(), create.GetName(), - msg.GetFrom(), msg.GetFromIdx(), - a.cfg.ID, conn.GetIdx()) - lk.SetQuality(create.GetCvnc().GetQuality()) - err := lk.Fork(confDir) - if err != nil { - logging.Error("create vnc failed: %v", err) - conn.SendConnectError(msg.GetFrom(), msg.GetFromIdx(), msg.GetLinkId(), err.Error()) - return - } - conn.SendConnectOK(msg.GetFrom(), msg.GetFromIdx(), msg.GetLinkId()) - lk.Forward() -} - -func (a *App) benchCreate(confDir string, mgr *rule.Mgr, conn *pool.Conn, msg *network.Msg) { - create := msg.GetCreq() - logging.Info("create link %s for bench rule [%s] from %s-%d to %s-%d", - msg.GetLinkId(), create.GetName(), - msg.GetFrom(), msg.GetFromIdx(), - a.cfg.ID, conn.GetIdx()) - conn.SendConnectOK(msg.GetFrom(), msg.GetFromIdx(), msg.GetLinkId()) -} diff --git a/code/client/conn/conn.go b/code/client/conn/conn.go new file mode 100644 index 0000000..2443d74 --- /dev/null +++ b/code/client/conn/conn.go @@ -0,0 +1,167 @@ +package conn + +import ( + "crypto/tls" + "net" + "strings" + "sync" + "time" + + "github.com/jkstack/natpass/code/client/global" + "github.com/jkstack/natpass/code/network" + "github.com/jkstack/natpass/code/utils" + "github.com/lwch/logging" + "github.com/lwch/runtime" +) + +// Conn connection +type Conn struct { + sync.RWMutex + cfg *global.Configure + conn *network.Conn + read map[string]chan *network.Msg // link id => channel + unknownRead chan *network.Msg // read message without link + write chan *network.Msg +} + +// New new connection +func New(cfg *global.Configure) *Conn { + conn := &Conn{ + cfg: cfg, + read: make(map[string]chan *network.Msg), + unknownRead: make(chan *network.Msg, 1024), + write: make(chan *network.Msg, 1024), + } + conn.conn = conn.connect() + go conn.loopRead() + go conn.loopWrite() + go conn.keepalive() + return conn +} + +func (conn *Conn) connect() *network.Conn { + defer func() { + if err := recover(); err != nil { + logging.Error("connect error: %v", err) + } + }() + var dial net.Conn + var err error + if conn.cfg.UseSSL { + dial, err = tls.Dial("tcp", conn.cfg.Server, nil) + } else { + dial, err = net.Dial("tcp", conn.cfg.Server) + } + runtime.Assert(err) + cn := network.NewConn(dial) + err = writeHandshake(cn, conn.cfg) + runtime.Assert(err) + logging.Info("%s connected", conn.cfg.Server) + return cn +} + +func writeHandshake(conn *network.Conn, cfg *global.Configure) error { + var msg network.Msg + msg.XType = network.Msg_handshake + msg.From = cfg.ID + msg.To = "server" + msg.Payload = &network.Msg_Hsp{ + Hsp: &network.HandshakePayload{ + Enc: cfg.Enc[:], + }, + } + return conn.WriteMessage(&msg, 5*time.Second) +} + +func (conn *Conn) loopRead() { + defer utils.Recover("loopRead") + var timeout int + for { + msg, _, err := conn.conn.ReadMessage(conn.cfg.ReadTimeout) + if err != nil { + if strings.Contains(err.Error(), "i/o timeout") { + timeout++ + if timeout >= 60 { + logging.Error("too many timeout times") + conn.conn = conn.connect() + continue + } + continue + } + logging.Error("read message: %v", err) + conn.conn = conn.connect() + continue + } + timeout = 0 + if msg.GetXType() == network.Msg_keepalive { + continue + } + logging.Debug("read message %s(%s) from %s", + msg.GetXType().String(), msg.GetLinkId(), msg.GetFrom()) + linkID := msg.GetLinkId() + conn.RLock() + ch := conn.read[linkID] + conn.RUnlock() + if ch == nil { + ch = conn.unknownRead + } + select { + case ch <- msg: + case <-time.After(conn.cfg.ReadTimeout): + logging.Error("drop message: %s", msg.GetXType().String()) + } + } +} + +func (conn *Conn) loopWrite() { + defer utils.Recover("loopWrite") + for { + msg := <-conn.write + msg.From = conn.cfg.ID + err := conn.conn.WriteMessage(msg, conn.cfg.WriteTimeout) + if err != nil { + logging.Error("write message error on %s: %v", + conn.cfg.ID, err) + conn.conn = conn.connect() + continue + } + } +} + +func (conn *Conn) keepalive() { + defer utils.Recover("keepalive") + for { + time.Sleep(10 * time.Second) + conn.SendKeepalive() + } +} + +// AddLink attach read message +func (conn *Conn) AddLink(id string) { + logging.Info("add link %s", id) + conn.Lock() + if _, ok := conn.read[id]; !ok { + conn.read[id] = make(chan *network.Msg, 10) + } + conn.Unlock() +} + +// Reset reset message next read +func (conn *Conn) Reset(id string, msg *network.Msg) { + conn.RLock() + ch := conn.read[id] + conn.RUnlock() + ch <- msg +} + +// ChanRead get read channel from link id +func (conn *Conn) ChanRead(id string) <-chan *network.Msg { + conn.RLock() + defer conn.RUnlock() + return conn.read[id] +} + +// ChanUnknown get channel of unknown link id +func (conn *Conn) ChanUnknown() <-chan *network.Msg { + return conn.unknownRead +} diff --git a/code/client/conn/send.go b/code/client/conn/send.go new file mode 100644 index 0000000..25eb198 --- /dev/null +++ b/code/client/conn/send.go @@ -0,0 +1,18 @@ +package conn + +import ( + "time" + + "github.com/jkstack/natpass/code/network" +) + +// SendKeepalive send keepalive message +func (conn *Conn) SendKeepalive() { + var msg network.Msg + msg.To = "server" + msg.XType = network.Msg_keepalive + select { + case conn.write <- &msg: + case <-time.After(conn.cfg.WriteTimeout): + } +} diff --git a/code/client/pool/send_conn.go b/code/client/conn/send_conn.go similarity index 83% rename from code/client/pool/send_conn.go rename to code/client/conn/send_conn.go index 7a59426..3523fdc 100644 --- a/code/client/pool/send_conn.go +++ b/code/client/conn/send_conn.go @@ -1,4 +1,4 @@ -package pool +package conn import ( "time" @@ -56,7 +56,7 @@ func (conn *Conn) SendConnectReq(id string, cfg global.Rule) { } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } @@ -87,15 +87,29 @@ func (conn *Conn) SendConnectVnc(id string, cfg global.Rule, quality uint64, sho } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): + } +} + +// SendDisconnect send disconnect message +func (conn *Conn) SendDisconnect(to string, id string) uint64 { + var msg network.Msg + msg.To = to + msg.XType = network.Msg_disconnect + msg.LinkId = id + select { + case conn.write <- &msg: + data, _ := proto.Marshal(&msg) + return uint64(len(data)) + case <-time.After(conn.cfg.WriteTimeout): + return 0 } } // SendConnectError send connect error response message -func (conn *Conn) SendConnectError(to string, toIdx uint32, id, info string) { +func (conn *Conn) SendConnectError(to string, id, info string) { var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_connect_rep msg.LinkId = id msg.Payload = &network.Msg_Crep{ @@ -106,15 +120,14 @@ func (conn *Conn) SendConnectError(to string, toIdx uint32, id, info string) { } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } // SendConnectOK send connect success response message -func (conn *Conn) SendConnectOK(to string, toIdx uint32, id string) { +func (conn *Conn) SendConnectOK(to string, id string) { var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_connect_rep msg.LinkId = id msg.Payload = &network.Msg_Crep{ @@ -124,22 +137,6 @@ func (conn *Conn) SendConnectOK(to string, toIdx uint32, id string) { } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): - } -} - -// SendDisconnect send disconnect message -func (conn *Conn) SendDisconnect(to string, toIdx uint32, id string) uint64 { - var msg network.Msg - msg.To = to - msg.ToIdx = toIdx - msg.XType = network.Msg_disconnect - msg.LinkId = id - select { - case conn.write <- &msg: - data, _ := proto.Marshal(&msg) - return uint64(len(data)) - case <-time.After(conn.parent.cfg.WriteTimeout): - return 0 + case <-time.After(conn.cfg.WriteTimeout): } } diff --git a/code/client/pool/send_shell.go b/code/client/conn/send_shell.go similarity index 70% rename from code/client/pool/send_shell.go rename to code/client/conn/send_shell.go index a7214a5..f5b197a 100644 --- a/code/client/pool/send_shell.go +++ b/code/client/conn/send_shell.go @@ -1,4 +1,4 @@ -package pool +package conn import ( "time" @@ -8,7 +8,7 @@ import ( ) // SendShellData send shell data -func (conn *Conn) SendShellData(to string, toIdx uint32, id string, data []byte) uint64 { +func (conn *Conn) SendShellData(to string, id string, data []byte) uint64 { dup := func(data []byte) []byte { ret := make([]byte, len(data)) copy(ret, data) @@ -16,7 +16,6 @@ func (conn *Conn) SendShellData(to string, toIdx uint32, id string, data []byte) } var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_shell_data msg.LinkId = id msg.Payload = &network.Msg_Sdata{ @@ -28,16 +27,15 @@ func (conn *Conn) SendShellData(to string, toIdx uint32, id string, data []byte) case conn.write <- &msg: data, _ := proto.Marshal(&msg) return uint64(len(data)) - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): return 0 } } // SendShellResize send shell resize -func (conn *Conn) SendShellResize(to string, toIdx uint32, id string, rows, cols uint32) { +func (conn *Conn) SendShellResize(to string, id string, rows, cols uint32) { var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_shell_resize msg.LinkId = id msg.Payload = &network.Msg_Sresize{ @@ -48,6 +46,6 @@ func (conn *Conn) SendShellResize(to string, toIdx uint32, id string, rows, cols } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } diff --git a/code/client/pool/send_vnc.go b/code/client/conn/send_vnc.go similarity index 73% rename from code/client/pool/send_vnc.go rename to code/client/conn/send_vnc.go index 2eb5393..549d6fe 100644 --- a/code/client/pool/send_vnc.go +++ b/code/client/conn/send_vnc.go @@ -1,4 +1,4 @@ -package pool +package conn import ( "image" @@ -8,7 +8,7 @@ import ( ) // SendVNCImage send vnc image data -func (conn *Conn) SendVNCImage(to string, toIdx uint32, id string, screen, rect image.Rectangle, +func (conn *Conn) SendVNCImage(to string, id string, screen, rect image.Rectangle, encode network.VncImageEncoding, data []byte) { dup := func(data []byte) []byte { ret := make([]byte, len(data)) @@ -17,7 +17,6 @@ func (conn *Conn) SendVNCImage(to string, toIdx uint32, id string, screen, rect } var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_vnc_image msg.LinkId = id msg.Payload = &network.Msg_Vimg{ @@ -36,15 +35,14 @@ func (conn *Conn) SendVNCImage(to string, toIdx uint32, id string, screen, rect } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } // SendVNCCtrl send vnc config -func (conn *Conn) SendVNCCtrl(to string, toIdx uint32, id string, quality uint64, showCursor bool) { +func (conn *Conn) SendVNCCtrl(to string, id string, quality uint64, showCursor bool) { var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_vnc_ctrl msg.LinkId = id msg.Payload = &network.Msg_Vctrl{ @@ -55,12 +53,12 @@ func (conn *Conn) SendVNCCtrl(to string, toIdx uint32, id string, quality uint64 } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } // SendVNCMouse send vnc mouse event -func (conn *Conn) SendVNCMouse(to string, toIdx uint32, id string, +func (conn *Conn) SendVNCMouse(to string, id string, button, status string, x, y int) { t := network.VncStatus_unset_st switch status { @@ -80,7 +78,6 @@ func (conn *Conn) SendVNCMouse(to string, toIdx uint32, id string, } var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_vnc_mouse msg.LinkId = id msg.Payload = &network.Msg_Vmouse{ @@ -93,12 +90,12 @@ func (conn *Conn) SendVNCMouse(to string, toIdx uint32, id string, } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } // SendVNCKeyboard send vnc keyboard event -func (conn *Conn) SendVNCKeyboard(to string, toIdx uint32, id string, +func (conn *Conn) SendVNCKeyboard(to string, id string, status, key string) { t := network.VncStatus_unset_st switch status { @@ -109,7 +106,6 @@ func (conn *Conn) SendVNCKeyboard(to string, toIdx uint32, id string, } var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_vnc_keyboard msg.LinkId = id msg.Payload = &network.Msg_Vkbd{ @@ -120,28 +116,26 @@ func (conn *Conn) SendVNCKeyboard(to string, toIdx uint32, id string, } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } // SendVNCCADEvent send vnc keyboard event -func (conn *Conn) SendVNCCADEvent(to string, toIdx uint32, id string) { +func (conn *Conn) SendVNCCADEvent(to string, id string) { var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_vnc_cad msg.LinkId = id select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } // SendVNCScroll send vnc scroll event -func (conn *Conn) SendVNCScroll(to string, toIdx uint32, id string, x, y int32) { +func (conn *Conn) SendVNCScroll(to string, id string, x, y int32) { var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_vnc_scroll msg.LinkId = id msg.Payload = &network.Msg_Vscroll{ @@ -152,15 +146,14 @@ func (conn *Conn) SendVNCScroll(to string, toIdx uint32, id string, x, y int32) } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } // SendVNCClipboardData send vnc clipboard data -func (conn *Conn) SendVNCClipboardData(to string, toIdx uint32, id string, set bool, data string) { +func (conn *Conn) SendVNCClipboardData(to string, id string, set bool, data string) { var msg network.Msg msg.To = to - msg.ToIdx = toIdx msg.XType = network.Msg_vnc_clipboard msg.LinkId = id msg.Payload = &network.Msg_Vclipboard{ @@ -174,6 +167,6 @@ func (conn *Conn) SendVNCClipboardData(to string, toIdx uint32, id string, set b } select { case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): + case <-time.After(conn.cfg.WriteTimeout): } } diff --git a/code/client/dashboard/dashboard.go b/code/client/dashboard/dashboard.go index 1dd7190..18bf6a9 100644 --- a/code/client/dashboard/dashboard.go +++ b/code/client/dashboard/dashboard.go @@ -5,24 +5,24 @@ import ( "net/http" "net/http/pprof" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/client/global" - "github.com/jkstack/natpass/code/client/pool" "github.com/jkstack/natpass/code/client/rule" ) // Dashboard dashboard object type Dashboard struct { cfg *global.Configure - pl *pool.Pool + conn *conn.Conn mgr *rule.Mgr Version string } // New create dashboard object -func New(cfg *global.Configure, pl *pool.Pool, mgr *rule.Mgr, version string) *Dashboard { +func New(cfg *global.Configure, conn *conn.Conn, mgr *rule.Mgr, version string) *Dashboard { return &Dashboard{ cfg: cfg, - pl: pl, + conn: conn, mgr: mgr, Version: version, } diff --git a/code/client/dashboard/h_info.go b/code/client/dashboard/h_info.go index 7b092f2..2813e16 100644 --- a/code/client/dashboard/h_info.go +++ b/code/client/dashboard/h_info.go @@ -16,7 +16,7 @@ func (db *Dashboard) Info(w http.ResponseWriter, r *http.Request) { Session int `json:"sessions"` } ret.Rules = len(db.cfg.Rules) - ret.PhysicalLinks = db.pl.Size() + ret.PhysicalLinks = 0 // TODO db.mgr.Range(func(t rule.Rule) { n := len(t.GetLinks()) ret.VirtualLinks += n diff --git a/code/client/global/conf.go b/code/client/global/conf.go index 5b3144e..a0dbc2b 100644 --- a/code/client/global/conf.go +++ b/code/client/global/conf.go @@ -52,7 +52,6 @@ func LoadConf(dir string) *Configure { Secret string `yaml:"secret"` SSL bool `yaml:"ssl"` Link struct { - Connections int `yaml:"connections"` ReadTimeout time.Duration `yaml:"read_timeout"` WriteTimeout time.Duration `yaml:"write_timeout"` } `yaml:"link"` @@ -77,9 +76,6 @@ func LoadConf(dir string) *Configure { } cfg.Rules[i] = t } - if cfg.Link.Connections <= 0 { - cfg.Link.Connections = 3 - } if cfg.Link.ReadTimeout <= 0 { cfg.Link.ReadTimeout = 5 * time.Second } @@ -96,7 +92,6 @@ func LoadConf(dir string) *Configure { Server: cfg.Server, UseSSL: cfg.SSL, Enc: md5.Sum([]byte(cfg.Secret)), - Links: cfg.Link.Connections, ReadTimeout: cfg.Link.ReadTimeout, WriteTimeout: cfg.Link.WriteTimeout, LogDir: cfg.Log.Dir, diff --git a/code/client/pool/conn.go b/code/client/pool/conn.go deleted file mode 100644 index 945fe8e..0000000 --- a/code/client/pool/conn.go +++ /dev/null @@ -1,190 +0,0 @@ -package pool - -import ( - "context" - "strings" - "sync" - "time" - - "github.com/jkstack/natpass/code/network" - "github.com/jkstack/natpass/code/utils" - "github.com/lwch/logging" -) - -// Conn pool connection -type Conn struct { - sync.RWMutex - Idx uint32 - ReadTimeout time.Duration - WriteTimeout time.Duration - parent *Pool - conn *network.Conn - read map[string]chan *network.Msg // link id => channel - unknownRead chan *network.Msg // read message without link - write chan *network.Msg // link id => channel -} - -func newConn(parent *Pool, conn *network.Conn, idx uint32) *Conn { - ret := &Conn{ - Idx: idx, - ReadTimeout: parent.cfg.ReadTimeout, - WriteTimeout: parent.cfg.WriteTimeout, - parent: parent, - conn: conn, - read: make(map[string]chan *network.Msg), - unknownRead: make(chan *network.Msg), - write: make(chan *network.Msg), - } - logging.Info("new connection: %s-%d", ret.parent.cfg.ID, ret.Idx) - ctx, cancel := context.WithCancel(context.Background()) - go ret.loopRead(cancel) - go ret.loopWrite(cancel) - go ret.keepalive(ctx) - return ret -} - -func (conn *Conn) hasLink(id string) bool { - conn.RLock() - defer conn.RUnlock() - _, ok := conn.read[id] - return ok -} - -// AddLink attach read message -func (conn *Conn) AddLink(id string) { - logging.Info("add link %s from %d", id, conn.Idx) - conn.Lock() - if _, ok := conn.read[id]; !ok { - conn.read[id] = make(chan *network.Msg, 10) - } - conn.Unlock() -} - -// RemoveLink detach read message -func (conn *Conn) RemoveLink(id string) { - logging.Info("remove link %s from %d", id, conn.Idx) - conn.Lock() - ch := conn.read[id] - if ch != nil { - close(ch) - } - delete(conn.read, id) - conn.Unlock() -} - -// Close close connection -func (conn *Conn) Close() { - conn.conn.Close() - conn.Lock() - for id, ch := range conn.read { - close(ch) - delete(conn.read, id) - } - conn.Unlock() - if conn.unknownRead != nil { - close(conn.unknownRead) - conn.unknownRead = nil - } - if conn.write != nil { - close(conn.write) - conn.write = nil - } - conn.parent.onClose(conn.Idx) - logging.Error("connection %s-%d closed", conn.parent.cfg.ID, conn.Idx) -} - -func (conn *Conn) loopRead(cancel context.CancelFunc) { - defer utils.Recover("loopRead") - defer conn.Close() - defer cancel() - var timeout int - for { - msg, _, err := conn.conn.ReadMessage(conn.parent.cfg.ReadTimeout) - if err != nil { - if strings.Contains(err.Error(), "i/o timeout") { - timeout++ - if timeout >= 60 { - logging.Error("too many timeout times") - return - } - continue - } - logging.Error("read message: %v", err) - return - } - timeout = 0 - if msg.GetXType() == network.Msg_keepalive { - continue - } - logging.Debug("read message %s(%s) from %s-%d", - msg.GetXType().String(), msg.GetLinkId(), msg.GetFrom(), msg.GetFromIdx()) - linkID := msg.GetLinkId() - conn.RLock() - ch := conn.read[linkID] - conn.RUnlock() - if ch == nil { - ch = conn.unknownRead - } - select { - case ch <- msg: - case <-time.After(conn.WriteTimeout): - } - } -} - -func (conn *Conn) loopWrite(cancel context.CancelFunc) { - defer utils.Recover("loopWrite") - defer conn.Close() - defer cancel() - for { - msg := <-conn.write - if msg == nil { - return - } - msg.From = conn.parent.cfg.ID - msg.FromIdx = conn.Idx - err := conn.conn.WriteMessage(msg, conn.parent.cfg.WriteTimeout) - if err != nil { - logging.Error("write message error on %s-%d: %v", - conn.parent.cfg.ID, conn.Idx, err) - return - } - } -} - -// ChanRead get read channel from link id -func (conn *Conn) ChanRead(id string) <-chan *network.Msg { - conn.RLock() - defer conn.RUnlock() - return conn.read[id] -} - -// Reset reset message next read -func (conn *Conn) Reset(id string, msg *network.Msg) { - conn.RLock() - ch := conn.read[id] - conn.RUnlock() - ch <- msg -} - -// ChanUnknown get channel of unknown link id -func (conn *Conn) ChanUnknown() <-chan *network.Msg { - return conn.unknownRead -} - -func (conn *Conn) keepalive(ctx context.Context) { - defer utils.Recover("keepalive") - for { - select { - case <-ctx.Done(): - return - case <-time.After(10 * time.Second): - conn.SendKeepalive() - } - } -} - -// GetIdx get connection index -func (conn *Conn) GetIdx() uint32 { - return conn.Idx -} diff --git a/code/client/pool/pool.go b/code/client/pool/pool.go deleted file mode 100644 index cd1c9bb..0000000 --- a/code/client/pool/pool.go +++ /dev/null @@ -1,120 +0,0 @@ -package pool - -import ( - "crypto/tls" - "net" - "sync" - "sync/atomic" - "time" - - "github.com/jkstack/natpass/code/client/global" - "github.com/jkstack/natpass/code/network" - "github.com/lwch/logging" - "github.com/lwch/runtime" -) - -// Pool connection pool -type Pool struct { - sync.RWMutex - cfg *global.Configure - conns map[uint32]*Conn - count int - idx uint32 -} - -// New create connection pool -func New(cfg *global.Configure) *Pool { - return &Pool{ - cfg: cfg, - conns: make(map[uint32]*Conn, cfg.Links), - count: cfg.Links, - idx: 0, - } -} - -func (p *Pool) getConns() []*Conn { - ret := make([]*Conn, 0, len(p.conns)) - p.RLock() - for _, conn := range p.conns { - ret = append(ret, conn) - } - p.RUnlock() - return ret -} - -// Get get connection -func (p *Pool) Get(id ...string) *Conn { - conns := p.getConns() - if len(id) > 0 { - for _, conn := range conns { - if conn.hasLink(id[0]) { - return conn - } - } - } - if len(conns) >= p.count { - p.Lock() - conn := conns[int(p.idx)%len(conns)] - p.idx++ - p.Unlock() - return conn - } - - idx := atomic.AddUint32(&p.idx, 1) - conn := p.connect(idx) - if conn == nil { - return nil - } - c := newConn(p, conn, idx) - - p.Lock() - p.conns[c.Idx] = c - p.Unlock() - return c -} - -func (p *Pool) connect(idx uint32) *network.Conn { - defer func() { - if err := recover(); err != nil { - logging.Error("connect error: %v", err) - } - }() - var conn net.Conn - var err error - if p.cfg.UseSSL { - conn, err = tls.Dial("tcp", p.cfg.Server, nil) - } else { - conn, err = net.Dial("tcp", p.cfg.Server) - } - runtime.Assert(err) - c := network.NewConn(conn) - err = p.writeHandshake(c, p.cfg, idx) - runtime.Assert(err) - logging.Info("%s connected", p.cfg.Server) - return c -} - -func (p *Pool) writeHandshake(conn *network.Conn, cfg *global.Configure, idx uint32) error { - var msg network.Msg - msg.XType = network.Msg_handshake - msg.From = p.cfg.ID - msg.FromIdx = idx - msg.To = "server" - msg.Payload = &network.Msg_Hsp{ - Hsp: &network.HandshakePayload{ - Enc: cfg.Enc[:], - }, - } - return conn.WriteMessage(&msg, 5*time.Second) -} - -func (p *Pool) onClose(idx uint32) { - p.Lock() - delete(p.conns, idx) - p.Unlock() -} - -// Size get pool size -func (p *Pool) Size() int { - return len(p.conns) -} diff --git a/code/client/pool/send.go b/code/client/pool/send.go deleted file mode 100644 index 77d658a..0000000 --- a/code/client/pool/send.go +++ /dev/null @@ -1,45 +0,0 @@ -package pool - -import ( - "time" - - "github.com/jkstack/natpass/code/network" - "google.golang.org/protobuf/proto" -) - -// SendData send forward data -func (conn *Conn) SendData(to string, toIdx uint32, id string, data []byte) uint64 { - dup := func(data []byte) []byte { - ret := make([]byte, len(data)) - copy(ret, data) - return ret - } - var msg network.Msg - msg.To = to - msg.ToIdx = toIdx - msg.XType = network.Msg_forward - msg.LinkId = id - msg.Payload = &network.Msg_XData{ - XData: &network.Data{ - Data: dup(data), - }, - } - select { - case conn.write <- &msg: - data, _ := proto.Marshal(&msg) - return uint64(len(data)) - case <-time.After(conn.parent.cfg.WriteTimeout): - return 0 - } -} - -// SendKeepalive send keepalive message -func (conn *Conn) SendKeepalive() { - var msg network.Msg - msg.To = "server" - msg.XType = network.Msg_keepalive - select { - case conn.write <- &msg: - case <-time.After(conn.parent.cfg.WriteTimeout): - } -} diff --git a/code/client/rule/bench/bench.go b/code/client/rule/bench/bench.go index 8ecf1c9..06ece41 100644 --- a/code/client/rule/bench/bench.go +++ b/code/client/rule/bench/bench.go @@ -5,8 +5,8 @@ import ( "net" "net/http" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/client/global" - "github.com/jkstack/natpass/code/client/pool" "github.com/jkstack/natpass/code/client/rule" "github.com/lwch/logging" "github.com/lwch/runtime" @@ -47,7 +47,7 @@ func New(cfg global.Rule) *Bench { } // NewLink new link -func (bench *Bench) NewLink(id, remote string, remoteIdx uint32, localConn net.Conn, remoteConn *pool.Conn) rule.Link { +func (bench *Bench) NewLink(id, remote string, localConn net.Conn, remoteConn *conn.Conn) rule.Link { return &Link{id: id} } @@ -82,7 +82,7 @@ func (bench *Bench) GetPort() uint16 { } // Handle handle shell -func (bench *Bench) Handle(pl *pool.Pool) { +func (bench *Bench) Handle(conn *conn.Conn) { defer func() { if err := recover(); err != nil { logging.Error("close shell: %s, err=%v", bench.Name, err) @@ -90,7 +90,7 @@ func (bench *Bench) Handle(pl *pool.Pool) { }() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - bench.http(pl, w, r) + bench.http(conn, w, r) }) svr := &http.Server{ Addr: fmt.Sprintf("%s:%d", bench.cfg.LocalAddr, bench.cfg.LocalPort), diff --git a/code/client/rule/bench/h_http.go b/code/client/rule/bench/h_http.go index ebd0d61..78d7abc 100644 --- a/code/client/rule/bench/h_http.go +++ b/code/client/rule/bench/h_http.go @@ -4,12 +4,12 @@ import ( "fmt" "net/http" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" "github.com/lwch/logging" "github.com/lwch/runtime" ) -func (bench *Bench) http(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (bench *Bench) http(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { id, err := runtime.UUID(16, "0123456789abcdef") if err != nil { logging.Error("failed to generate link_id for bench: %s, err=%v", @@ -17,11 +17,9 @@ func (bench *Bench) http(pool *pool.Pool, w http.ResponseWriter, r *http.Request http.Error(w, err.Error(), http.StatusInternalServerError) return } - conn := pool.Get(id) conn.AddLink(id) conn.SendConnectReq(id, bench.cfg) ch := conn.ChanRead(id) <-ch - conn.RemoveLink(id) fmt.Fprint(w, id) } diff --git a/code/client/rule/mgr.go b/code/client/rule/mgr.go index 42a1bd6..d76088d 100644 --- a/code/client/rule/mgr.go +++ b/code/client/rule/mgr.go @@ -4,7 +4,7 @@ import ( "net" "sync" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" ) // Link link interface @@ -18,7 +18,7 @@ type Link interface { // Rule rule interface type Rule interface { - NewLink(id, remote string, remoteIdx uint32, localConn net.Conn, remoteConn *pool.Conn) Link + NewLink(id, remote string, localConn net.Conn, remoteConn *conn.Conn) Link GetName() string GetRemote() string GetPort() uint16 diff --git a/code/client/rule/shell/h_new.go b/code/client/rule/shell/h_new.go index e3150b6..dd15df7 100644 --- a/code/client/rule/shell/h_new.go +++ b/code/client/rule/shell/h_new.go @@ -5,14 +5,14 @@ import ( "net/http" "time" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/network" "github.com/lwch/logging" "github.com/lwch/runtime" ) // New new shell -func (shell *Shell) New(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (shell *Shell) New(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { id, err := runtime.UUID(16, "0123456789abcdef") if err != nil { logging.Error("failed to generate link_id for shell: %s, err=%v", @@ -20,11 +20,10 @@ func (shell *Shell) New(pool *pool.Pool, w http.ResponseWriter, r *http.Request) http.Error(w, err.Error(), http.StatusInternalServerError) return } - conn := pool.Get(id) - link := shell.NewLink(id, shell.cfg.Target, 0, nil, conn).(*Link) + link := shell.NewLink(id, shell.cfg.Target, nil, conn).(*Link) conn.SendConnectReq(id, shell.cfg) ch := conn.ChanRead(id) - timeout := time.After(conn.ReadTimeout) + timeout := time.After(shell.readTimeout) var repMsg *network.Msg for { var msg *network.Msg @@ -35,10 +34,9 @@ func (shell *Shell) New(pool *pool.Pool, w http.ResponseWriter, r *http.Request) http.Error(w, "timeout", http.StatusBadGateway) return } - link.SetTargetIdx(msg.GetFromIdx()) if msg.GetXType() != network.Msg_connect_rep { conn.Reset(id, msg) - time.Sleep(conn.ReadTimeout / 10) + time.Sleep(shell.readTimeout / 10) continue } rep := msg.GetCrep() @@ -51,9 +49,8 @@ func (shell *Shell) New(pool *pool.Pool, w http.ResponseWriter, r *http.Request) repMsg = msg break } - logging.Info("create link %s for shell rule [%s] from %s-%d to %s-%d", + logging.Info("create link %s for shell rule [%s] from %s to %s", link.GetID(), shell.cfg.Name, - repMsg.GetTo(), repMsg.GetToIdx(), - repMsg.GetFrom(), repMsg.GetFromIdx()) + repMsg.GetTo(), repMsg.GetFrom()) fmt.Fprint(w, id) } diff --git a/code/client/rule/shell/h_resize.go b/code/client/rule/shell/h_resize.go index 8798777..414a63c 100644 --- a/code/client/rule/shell/h_resize.go +++ b/code/client/rule/shell/h_resize.go @@ -5,11 +5,11 @@ import ( "net/http" "strconv" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" ) // Resize resize terminal -func (shell *Shell) Resize(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (shell *Shell) Resize(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { id := r.FormValue("id") rows := r.FormValue("rows") cols := r.FormValue("cols") diff --git a/code/client/rule/shell/h_ws.go b/code/client/rule/shell/h_ws.go index ac1d933..8a86d03 100644 --- a/code/client/rule/shell/h_ws.go +++ b/code/client/rule/shell/h_ws.go @@ -6,7 +6,7 @@ import ( "sync" "github.com/gorilla/websocket" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/network" "github.com/jkstack/natpass/code/utils" "github.com/lwch/logging" @@ -16,7 +16,7 @@ import ( var upgrader = websocket.Upgrader{} // WS websocket for forward data -func (shell *Shell) WS(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (shell *Shell) WS(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { id := strings.TrimPrefix(r.URL.Path, "/ws/") local, err := upgrader.Upgrade(w, r, nil) diff --git a/code/client/rule/shell/link.go b/code/client/rule/shell/link.go index c5d9d0a..b7938c5 100644 --- a/code/client/rule/shell/link.go +++ b/code/client/rule/shell/link.go @@ -4,7 +4,7 @@ import ( "io" "os" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/network" "github.com/jkstack/natpass/code/utils" "github.com/lwch/logging" @@ -14,11 +14,10 @@ import ( // Link shell link type Link struct { - parent *Shell - id string // link id - target string // target id - targetIdx uint32 // target idx - remote *pool.Conn + parent *Shell + id string // link id + target string // target id + remote *conn.Conn // in remote pid int stdin io.WriteCloser @@ -45,11 +44,6 @@ func (link *Link) GetPackets() (uint64, uint64) { return link.recvPacket, link.sendPacket } -// SetTargetIdx set link remote index -func (link *Link) SetTargetIdx(idx uint32) { - link.targetIdx = idx -} - // Close close link func (link *Link) Close() { link.onClose() @@ -57,7 +51,7 @@ func (link *Link) Close() { if err == nil { p.Kill() } - link.remote.SendDisconnect(link.target, link.targetIdx, link.id) + link.remote.SendDisconnect(link.target, link.id) link.parent.remove(link.id) } @@ -79,7 +73,6 @@ func (link *Link) remoteRead() { data, _ := proto.Marshal(msg) link.recvBytes += uint64(len(data)) link.recvPacket++ - link.targetIdx = msg.GetFromIdx() switch msg.GetXType() { case network.Msg_shell_resize: size := msg.GetSresize() @@ -125,7 +118,7 @@ func (link *Link) localRead() { } logging.Debug("link %s on shell %s read from local %d bytes", link.id, link.parent.Name, n) - send := link.remote.SendShellData(link.target, link.targetIdx, link.id, data) + send := link.remote.SendShellData(link.target, link.id, data) link.sendBytes += send link.sendPacket++ } @@ -133,12 +126,12 @@ func (link *Link) localRead() { // SendData send data func (link *Link) SendData(data []byte) { - send := link.remote.SendShellData(link.target, link.targetIdx, link.id, data) + send := link.remote.SendShellData(link.target, link.id, data) link.sendBytes += send link.sendPacket++ } // SendResize send resize message func (link *Link) SendResize(rows, cols uint32) { - link.remote.SendShellResize(link.target, link.targetIdx, link.id, rows, cols) + link.remote.SendShellResize(link.target, link.id, rows, cols) } diff --git a/code/client/rule/shell/shell.go b/code/client/rule/shell/shell.go index df20d48..544f63e 100644 --- a/code/client/rule/shell/shell.go +++ b/code/client/rule/shell/shell.go @@ -5,9 +5,10 @@ import ( "net" "net/http" "sync" + "time" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/client/global" - "github.com/jkstack/natpass/code/client/pool" "github.com/jkstack/natpass/code/client/rule" "github.com/lwch/logging" "github.com/lwch/runtime" @@ -16,29 +17,32 @@ import ( // Shell shell handler type Shell struct { sync.RWMutex - Name string - cfg global.Rule - links map[string]*Link + Name string + cfg global.Rule + links map[string]*Link + readTimeout time.Duration + writeTimeout time.Duration } // New new shell -func New(cfg global.Rule) *Shell { +func New(cfg global.Rule, readTimeout, writeTimeout time.Duration) *Shell { return &Shell{ - Name: cfg.Name, - cfg: cfg, - links: make(map[string]*Link), + Name: cfg.Name, + cfg: cfg, + links: make(map[string]*Link), + readTimeout: readTimeout, + writeTimeout: writeTimeout, } } // NewLink new link -func (shell *Shell) NewLink(id, remote string, remoteIdx uint32, localConn net.Conn, remoteConn *pool.Conn) rule.Link { +func (shell *Shell) NewLink(id, remote string, localConn net.Conn, remoteConn *conn.Conn) rule.Link { remoteConn.AddLink(id) link := &Link{ - parent: shell, - id: id, - target: remote, - targetIdx: remoteIdx, - remote: remoteConn, + parent: shell, + id: id, + target: remote, + remote: remoteConn, } shell.Lock() shell.links[link.id] = link @@ -83,15 +87,15 @@ func (shell *Shell) GetPort() uint16 { } // Handle handle shell -func (shell *Shell) Handle(pl *pool.Pool) { +func (shell *Shell) Handle(c *conn.Conn) { defer func() { if err := recover(); err != nil { logging.Error("close shell: %s, err=%v", shell.Name, err) } }() - pf := func(cb func(*pool.Pool, http.ResponseWriter, *http.Request)) http.HandlerFunc { + pf := func(cb func(*conn.Conn, http.ResponseWriter, *http.Request)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - cb(pl, w, r) + cb(c, w, r) } } mux := http.NewServeMux() diff --git a/code/client/rule/vnc/events.go b/code/client/rule/vnc/events.go index 7831bd3..c29cf18 100644 --- a/code/client/rule/vnc/events.go +++ b/code/client/rule/vnc/events.go @@ -3,11 +3,11 @@ package vnc import ( "encoding/json" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" "github.com/lwch/logging" ) -func (v *VNC) mouseEvent(remote *pool.Conn, data []byte) { +func (v *VNC) mouseEvent(remote *conn.Conn, data []byte) { var payload struct { Payload struct { Button string `json:"button"` @@ -21,11 +21,11 @@ func (v *VNC) mouseEvent(remote *pool.Conn, data []byte) { logging.Error("unmarshal: %v", err) return } - remote.SendVNCMouse(v.link.target, v.link.targetIdx, v.link.id, + remote.SendVNCMouse(v.link.target, v.link.id, payload.Payload.Button, payload.Payload.Status, payload.Payload.X, payload.Payload.Y) } -func (v *VNC) keyboardEvent(remote *pool.Conn, data []byte) { +func (v *VNC) keyboardEvent(remote *conn.Conn, data []byte) { var payload struct { Payload struct { Status string `json:"status"` @@ -37,15 +37,15 @@ func (v *VNC) keyboardEvent(remote *pool.Conn, data []byte) { logging.Error("unmarshal: %v", err) return } - remote.SendVNCKeyboard(v.link.target, v.link.targetIdx, v.link.id, + remote.SendVNCKeyboard(v.link.target, v.link.id, payload.Payload.Status, payload.Payload.Key) } -func (v *VNC) cadEvent(remote *pool.Conn) { - remote.SendVNCCADEvent(v.link.target, v.link.targetIdx, v.link.id) +func (v *VNC) cadEvent(remote *conn.Conn) { + remote.SendVNCCADEvent(v.link.target, v.link.id) } -func (v *VNC) scrollEvent(remote *pool.Conn, data []byte) { +func (v *VNC) scrollEvent(remote *conn.Conn, data []byte) { var payload struct { Payload struct { X int32 `json:"x"` @@ -57,6 +57,6 @@ func (v *VNC) scrollEvent(remote *pool.Conn, data []byte) { logging.Error("unmarshal: %v", err) return } - remote.SendVNCScroll(v.link.target, v.link.targetIdx, v.link.id, + remote.SendVNCScroll(v.link.target, v.link.id, payload.Payload.X, payload.Payload.Y) } diff --git a/code/client/rule/vnc/h_clipboard.go b/code/client/rule/vnc/h_clipboard.go index 0a31872..f963cd5 100644 --- a/code/client/rule/vnc/h_clipboard.go +++ b/code/client/rule/vnc/h_clipboard.go @@ -4,36 +4,34 @@ import ( "fmt" "net/http" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" ) // Clipboard get/set clipboard -func (v *VNC) Clipboard(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (v *VNC) Clipboard(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - v.getClipboard(pool, w, r) + v.getClipboard(conn, w, r) return } - v.setClipboard(pool, w, r) + v.setClipboard(conn, w, r) } -func (v *VNC) getClipboard(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (v *VNC) getClipboard(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { if v.link == nil { http.NotFound(w, r) return } - conn := pool.Get(v.link.id) - conn.SendVNCClipboardData(v.link.target, v.link.targetIdx, v.link.id, false, "") + conn.SendVNCClipboardData(v.link.target, v.link.id, false, "") data := <-v.chClipboard fmt.Fprint(w, data.GetData()) } -func (v *VNC) setClipboard(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (v *VNC) setClipboard(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { data := r.FormValue("data") if v.link == nil { http.NotFound(w, r) return } - conn := pool.Get(v.link.id) - conn.SendVNCClipboardData(v.link.target, v.link.targetIdx, v.link.id, true, data) + conn.SendVNCClipboardData(v.link.target, v.link.id, true, data) fmt.Fprint(w, "ok") } diff --git a/code/client/rule/vnc/h_ctrl.go b/code/client/rule/vnc/h_ctrl.go index 4cb1569..155f813 100644 --- a/code/client/rule/vnc/h_ctrl.go +++ b/code/client/rule/vnc/h_ctrl.go @@ -5,11 +5,11 @@ import ( "net/http" "strconv" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" ) // Ctrl change vnc rule config -func (v *VNC) Ctrl(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (v *VNC) Ctrl(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { q := r.FormValue("quality") s := r.FormValue("show_cursor") quality, err := strconv.ParseUint(q, 10, 32) @@ -24,7 +24,6 @@ func (v *VNC) Ctrl(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } - conn := pool.Get(v.link.id) - conn.SendVNCCtrl(v.link.target, v.link.targetIdx, v.link.id, quality, showCursor) + conn.SendVNCCtrl(v.link.target, v.link.id, quality, showCursor) fmt.Fprint(w, "ok") } diff --git a/code/client/rule/vnc/h_new.go b/code/client/rule/vnc/h_new.go index 71af36a..8bdc488 100644 --- a/code/client/rule/vnc/h_new.go +++ b/code/client/rule/vnc/h_new.go @@ -6,14 +6,14 @@ import ( "strconv" "time" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/network" "github.com/lwch/logging" "github.com/lwch/runtime" ) // New new vnc -func (v *VNC) New(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (v *VNC) New(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { if v.link != nil { v.link.close() } @@ -34,17 +34,13 @@ func (v *VNC) New(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - conn := pool.Get(id) if v.link != nil { - old := pool.Get(v.link.id) - if old != nil { - old.SendDisconnect(v.link.target, v.link.targetIdx, v.link.id) - } + conn.SendDisconnect(v.link.target, v.link.id) } conn.SendConnectVnc(id, v.cfg, quality, showCursor) - v.link = v.NewLink(id, v.cfg.Target, 0, nil, conn).(*Link) + v.link = v.NewLink(id, v.cfg.Target, nil, conn).(*Link) ch := conn.ChanRead(id) - timeout := time.After(conn.ReadTimeout) + timeout := time.After(v.readTimeout) for { var msg *network.Msg select { @@ -54,10 +50,9 @@ func (v *VNC) New(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { http.Error(w, "timeout", http.StatusBadGateway) return } - v.link.SetTargetIdx(msg.GetFromIdx()) if msg.GetXType() != network.Msg_connect_rep { conn.Reset(id, msg) - time.Sleep(conn.ReadTimeout / 10) + time.Sleep(v.readTimeout / 10) continue } rep := msg.GetCrep() diff --git a/code/client/rule/vnc/h_ws.go b/code/client/rule/vnc/h_ws.go index 9a6cff5..67cfad6 100644 --- a/code/client/rule/vnc/h_ws.go +++ b/code/client/rule/vnc/h_ws.go @@ -15,7 +15,7 @@ import ( "sync" "github.com/gorilla/websocket" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/network" "github.com/jkstack/natpass/code/utils" "github.com/lwch/logging" @@ -25,13 +25,8 @@ import ( var upgrader = websocket.Upgrader{} // WS websocket handler -func (v *VNC) WS(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { +func (v *VNC) WS(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { id := strings.TrimPrefix(r.URL.Path, "/ws/") - conn := pool.Get(id) - if conn == nil { - http.NotFound(w, r) - return - } local, err := upgrader.Upgrade(w, r, nil) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -39,7 +34,7 @@ func (v *VNC) WS(pool *pool.Pool, w http.ResponseWriter, r *http.Request) { } defer local.Close() ch := conn.ChanRead(id) - defer conn.SendDisconnect(v.link.target, v.link.targetIdx, v.link.id) + defer conn.SendDisconnect(v.link.target, v.link.id) ctx, cancel := context.WithCancel(context.Background()) defer cancel() var wg sync.WaitGroup @@ -80,7 +75,7 @@ func (v *VNC) remoteRead(ctx context.Context, ch <-chan *network.Msg, local *web } } -func (v *VNC) localRead(ctx context.Context, local *websocket.Conn, remote *pool.Conn) { +func (v *VNC) localRead(ctx context.Context, local *websocket.Conn, remote *conn.Conn) { defer utils.Recover("localRead") for { select { diff --git a/code/client/rule/vnc/link.go b/code/client/rule/vnc/link.go index b49c813..3cbd843 100644 --- a/code/client/rule/vnc/link.go +++ b/code/client/rule/vnc/link.go @@ -6,7 +6,7 @@ import ( "image/jpeg" "time" - "github.com/jkstack/natpass/code/client/pool" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/client/rule/vnc/process" "github.com/jkstack/natpass/code/network" "github.com/jkstack/natpass/code/utils" @@ -20,11 +20,10 @@ const ( // Link vnc link type Link struct { - parent *VNC - id string // link id - target string // target id - targetIdx uint32 // target idx - remote *pool.Conn + parent *VNC + id string // link id + target string // target id + remote *conn.Conn // vnc ps *process.Process quality uint32 @@ -54,11 +53,6 @@ func (link *Link) GetPackets() (uint64, uint64) { return link.recvPacket, link.sendPacket } -// SetTargetIdx set link remote index -func (link *Link) SetTargetIdx(idx uint32) { - link.targetIdx = idx -} - // SetQuality transfer quality func (link *Link) SetQuality(q uint32) { link.quality = q @@ -114,7 +108,7 @@ func (link *Link) remoteRead() { link.ps.SetClipboard(msg.GetVclipboard()) } else { data := link.ps.GetClipboard() - link.remote.SendVNCClipboardData(link.target, link.targetIdx, link.id, true, data) + link.remote.SendVNCClipboardData(link.target, link.id, true, data) } case network.Msg_disconnect: logging.Info("link %s disconnected", link.id) @@ -162,7 +156,7 @@ func (link *Link) close() { if link.ps != nil { link.ps.Close() } - link.remote.SendDisconnect(link.target, link.targetIdx, link.id) + link.remote.SendDisconnect(link.target, link.id) } func cut(src *image.RGBA, rect image.Rectangle) *image.RGBA { @@ -195,17 +189,17 @@ func (link *Link) sendAll(img *image.RGBA) { rect := image.Rect(x, y, x+width, y+height) next := cut(img, rect) if link.quality == 100 { - link.remote.SendVNCImage(link.target, link.targetIdx, link.id, + link.remote.SendVNCImage(link.target, link.id, screen, rect, network.VncImage_raw, next.Pix) continue } buf.Reset() err := jpeg.Encode(&buf, next, &jpeg.Options{Quality: int(link.quality)}) if err == nil { - link.remote.SendVNCImage(link.target, link.targetIdx, link.id, + link.remote.SendVNCImage(link.target, link.id, screen, rect, network.VncImage_jpeg, buf.Bytes()) } else { - link.remote.SendVNCImage(link.target, link.targetIdx, link.id, + link.remote.SendVNCImage(link.target, link.id, screen, rect, network.VncImage_raw, next.Pix) } } @@ -219,17 +213,17 @@ func (link *Link) sendDiff(img *image.RGBA) { for _, block := range blocks { next := cut(img, block) if link.quality == 100 { - link.remote.SendVNCImage(link.target, link.targetIdx, link.id, + link.remote.SendVNCImage(link.target, link.id, screen, block, network.VncImage_raw, next.Pix) continue } buf.Reset() err := jpeg.Encode(&buf, next, &jpeg.Options{Quality: int(link.quality)}) if err == nil { - link.remote.SendVNCImage(link.target, link.targetIdx, link.id, + link.remote.SendVNCImage(link.target, link.id, screen, block, network.VncImage_jpeg, buf.Bytes()) } else { - link.remote.SendVNCImage(link.target, link.targetIdx, link.id, + link.remote.SendVNCImage(link.target, link.id, screen, block, network.VncImage_raw, next.Pix) } } diff --git a/code/client/rule/vnc/vnc.go b/code/client/rule/vnc/vnc.go index 1d8fa76..377917b 100644 --- a/code/client/rule/vnc/vnc.go +++ b/code/client/rule/vnc/vnc.go @@ -5,9 +5,10 @@ import ( "net" "net/http" "sync" + "time" + "github.com/jkstack/natpass/code/client/conn" "github.com/jkstack/natpass/code/client/global" - "github.com/jkstack/natpass/code/client/pool" "github.com/jkstack/natpass/code/client/rule" "github.com/jkstack/natpass/code/network" "github.com/lwch/logging" @@ -17,30 +18,33 @@ import ( // VNC vnc handler type VNC struct { sync.RWMutex - Name string - cfg global.Rule - link *Link - chClipboard chan *network.VncClipboard + Name string + cfg global.Rule + link *Link + readTimeout time.Duration + writeTimeout time.Duration + chClipboard chan *network.VncClipboard } // New new vnc -func New(cfg global.Rule) *VNC { +func New(cfg global.Rule, readTimeout, writeTimeout time.Duration) *VNC { return &VNC{ - Name: cfg.Name, - cfg: cfg, - chClipboard: make(chan *network.VncClipboard), + Name: cfg.Name, + cfg: cfg, + readTimeout: readTimeout, + writeTimeout: writeTimeout, + chClipboard: make(chan *network.VncClipboard), } } // NewLink new link -func (v *VNC) NewLink(id, remote string, remoteIdx uint32, localConn net.Conn, remoteConn *pool.Conn) rule.Link { +func (v *VNC) NewLink(id, remote string, localConn net.Conn, remoteConn *conn.Conn) rule.Link { remoteConn.AddLink(id) link := &Link{ - parent: v, - id: id, - target: remote, - targetIdx: remoteIdx, - remote: remoteConn, + parent: v, + id: id, + target: remote, + remote: remoteConn, } if v.link != nil { v.link.close() @@ -83,15 +87,15 @@ func (v *VNC) GetPort() uint16 { } // Handle handle shell -func (v *VNC) Handle(pl *pool.Pool) { +func (v *VNC) Handle(c *conn.Conn) { defer func() { if err := recover(); err != nil { logging.Error("close shell: %s, err=%v", v.Name, err) } }() - pf := func(cb func(*pool.Pool, http.ResponseWriter, *http.Request)) http.HandlerFunc { + pf := func(cb func(*conn.Conn, http.ResponseWriter, *http.Request)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - cb(pl, w, r) + cb(c, w, r) } } mux := http.NewServeMux() diff --git a/code/network/msg.pb.go b/code/network/msg.pb.go index 66055a7..fdad4c3 100644 --- a/code/network/msg.pb.go +++ b/code/network/msg.pb.go @@ -162,12 +162,10 @@ type Msg struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - XType MsgType `protobuf:"varint,1,opt,name=_type,json=Type,proto3,enum=network.MsgType" json:"_type,omitempty"` - From string `protobuf:"bytes,2,opt,name=from,proto3" json:"from,omitempty"` - FromIdx uint32 `protobuf:"varint,3,opt,name=from_idx,json=fromIdx,proto3" json:"from_idx,omitempty"` - To string `protobuf:"bytes,4,opt,name=to,proto3" json:"to,omitempty"` - ToIdx uint32 `protobuf:"varint,5,opt,name=to_idx,json=toIdx,proto3" json:"to_idx,omitempty"` - LinkId string `protobuf:"bytes,6,opt,name=link_id,json=linkId,proto3" json:"link_id,omitempty"` + XType MsgType `protobuf:"varint,1,opt,name=_type,json=Type,proto3,enum=network.MsgType" json:"_type,omitempty"` + From string `protobuf:"bytes,2,opt,name=from,proto3" json:"from,omitempty"` + To string `protobuf:"bytes,4,opt,name=to,proto3" json:"to,omitempty"` + LinkId string `protobuf:"bytes,6,opt,name=link_id,json=linkId,proto3" json:"link_id,omitempty"` // Types that are assignable to Payload: // *Msg_Hsp // *Msg_Creq @@ -230,13 +228,6 @@ func (x *Msg) GetFrom() string { return "" } -func (x *Msg) GetFromIdx() uint32 { - if x != nil { - return x.FromIdx - } - return 0 -} - func (x *Msg) GetTo() string { if x != nil { return x.To @@ -244,13 +235,6 @@ func (x *Msg) GetTo() string { return "" } -func (x *Msg) GetToIdx() uint32 { - if x != nil { - return x.ToIdx - } - return 0 -} - func (x *Msg) GetLinkId() string { if x != nil { return x.LinkId @@ -437,70 +421,67 @@ var file_msg_proto_rawDesc = []byte{ 0x09, 0x76, 0x6e, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x25, 0x0a, 0x11, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x65, 0x6e, - 0x63, 0x22, 0xdf, 0x07, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x26, 0x0a, 0x05, 0x5f, 0x74, 0x79, + 0x63, 0x22, 0xad, 0x07, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x26, 0x0a, 0x05, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x6d, 0x73, 0x67, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x69, 0x64, - 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x66, 0x72, 0x6f, 0x6d, 0x49, 0x64, 0x78, - 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, - 0x12, 0x15, 0x0a, 0x06, 0x74, 0x6f, 0x5f, 0x69, 0x64, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x05, 0x74, 0x6f, 0x49, 0x64, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, - 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x69, 0x6e, 0x6b, 0x49, 0x64, - 0x12, 0x2e, 0x0a, 0x03, 0x68, 0x73, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, - 0x65, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x00, 0x52, 0x03, 0x68, 0x73, 0x70, - 0x12, 0x2e, 0x0a, 0x04, 0x63, 0x72, 0x65, 0x71, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x63, 0x72, 0x65, 0x71, - 0x12, 0x2f, 0x0a, 0x04, 0x63, 0x72, 0x65, 0x70, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x04, 0x63, 0x72, 0x65, - 0x70, 0x12, 0x24, 0x0a, 0x05, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0d, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x48, - 0x00, 0x52, 0x04, 0x44, 0x61, 0x74, 0x61, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x72, 0x65, 0x73, 0x69, - 0x7a, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x2e, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x48, - 0x00, 0x52, 0x07, 0x73, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x73, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x2e, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00, - 0x52, 0x05, 0x73, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x63, 0x74, 0x72, 0x6c, - 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x48, 0x00, 0x52, 0x05, - 0x76, 0x63, 0x74, 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x04, 0x76, 0x69, 0x6d, 0x67, 0x18, 0x1f, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, - 0x63, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x04, 0x76, 0x69, 0x6d, 0x67, 0x12, - 0x2c, 0x0a, 0x06, 0x76, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x18, 0x20, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x12, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x6d, 0x6f, - 0x75, 0x73, 0x65, 0x48, 0x00, 0x52, 0x06, 0x76, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x12, 0x2b, 0x0a, - 0x04, 0x76, 0x6b, 0x62, 0x64, 0x18, 0x21, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x62, 0x6f, 0x61, - 0x72, 0x64, 0x48, 0x00, 0x52, 0x04, 0x76, 0x6b, 0x62, 0x64, 0x12, 0x2f, 0x0a, 0x07, 0x76, 0x73, - 0x63, 0x72, 0x6f, 0x6c, 0x6c, 0x18, 0x22, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x73, 0x63, 0x72, 0x6f, 0x6c, 0x6c, - 0x48, 0x00, 0x52, 0x07, 0x76, 0x73, 0x63, 0x72, 0x6f, 0x6c, 0x6c, 0x12, 0x38, 0x0a, 0x0a, 0x76, - 0x63, 0x6c, 0x69, 0x70, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x18, 0x23, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x63, 0x6c, - 0x69, 0x70, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x48, 0x00, 0x52, 0x0a, 0x76, 0x63, 0x6c, 0x69, 0x70, - 0x62, 0x6f, 0x61, 0x72, 0x64, 0x22, 0x80, 0x02, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0b, - 0x0a, 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x68, - 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x6b, 0x65, - 0x65, 0x70, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x72, 0x65, 0x70, 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, 0x64, - 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x66, - 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x73, 0x68, 0x65, 0x6c, - 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x10, 0x0a, 0x12, 0x0e, 0x0a, 0x0a, 0x73, 0x68, - 0x65, 0x6c, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x10, 0x0b, 0x12, 0x0c, 0x0a, 0x08, 0x76, 0x6e, - 0x63, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x10, 0x14, 0x12, 0x0d, 0x0a, 0x09, 0x76, 0x6e, 0x63, 0x5f, - 0x69, 0x6d, 0x61, 0x67, 0x65, 0x10, 0x15, 0x12, 0x0d, 0x0a, 0x09, 0x76, 0x6e, 0x63, 0x5f, 0x6d, - 0x6f, 0x75, 0x73, 0x65, 0x10, 0x16, 0x12, 0x10, 0x0a, 0x0c, 0x76, 0x6e, 0x63, 0x5f, 0x6b, 0x65, - 0x79, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x10, 0x17, 0x12, 0x0b, 0x0a, 0x07, 0x76, 0x6e, 0x63, 0x5f, - 0x63, 0x61, 0x64, 0x10, 0x18, 0x12, 0x0e, 0x0a, 0x0a, 0x76, 0x6e, 0x63, 0x5f, 0x73, 0x63, 0x72, - 0x6f, 0x6c, 0x6c, 0x10, 0x19, 0x12, 0x11, 0x0a, 0x0d, 0x76, 0x6e, 0x63, 0x5f, 0x63, 0x6c, 0x69, - 0x70, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x10, 0x1a, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, - 0x6f, 0x61, 0x64, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x2f, 0x3b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x17, 0x0a, 0x07, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x69, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x69, 0x6e, 0x6b, 0x49, 0x64, 0x12, 0x2e, + 0x0a, 0x03, 0x68, 0x73, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x00, 0x52, 0x03, 0x68, 0x73, 0x70, 0x12, 0x2e, + 0x0a, 0x04, 0x63, 0x72, 0x65, 0x71, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x63, 0x72, 0x65, 0x71, 0x12, 0x2f, + 0x0a, 0x04, 0x63, 0x72, 0x65, 0x70, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x04, 0x63, 0x72, 0x65, 0x70, 0x12, + 0x24, 0x0a, 0x05, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, + 0x04, 0x44, 0x61, 0x74, 0x61, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x2e, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x48, 0x00, 0x52, + 0x07, 0x73, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x73, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x2e, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x05, + 0x73, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x63, 0x74, 0x72, 0x6c, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, + 0x6e, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x48, 0x00, 0x52, 0x05, 0x76, 0x63, + 0x74, 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x04, 0x76, 0x69, 0x6d, 0x67, 0x18, 0x1f, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x12, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, + 0x69, 0x6d, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x04, 0x76, 0x69, 0x6d, 0x67, 0x12, 0x2c, 0x0a, + 0x06, 0x76, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x18, 0x20, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x6d, 0x6f, 0x75, 0x73, + 0x65, 0x48, 0x00, 0x52, 0x06, 0x76, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x76, + 0x6b, 0x62, 0x64, 0x18, 0x21, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x62, 0x6f, 0x61, 0x72, 0x64, + 0x48, 0x00, 0x52, 0x04, 0x76, 0x6b, 0x62, 0x64, 0x12, 0x2f, 0x0a, 0x07, 0x76, 0x73, 0x63, 0x72, + 0x6f, 0x6c, 0x6c, 0x18, 0x22, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x73, 0x63, 0x72, 0x6f, 0x6c, 0x6c, 0x48, 0x00, + 0x52, 0x07, 0x76, 0x73, 0x63, 0x72, 0x6f, 0x6c, 0x6c, 0x12, 0x38, 0x0a, 0x0a, 0x76, 0x63, 0x6c, + 0x69, 0x70, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x18, 0x23, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x76, 0x6e, 0x63, 0x5f, 0x63, 0x6c, 0x69, 0x70, + 0x62, 0x6f, 0x61, 0x72, 0x64, 0x48, 0x00, 0x52, 0x0a, 0x76, 0x63, 0x6c, 0x69, 0x70, 0x62, 0x6f, + 0x61, 0x72, 0x64, 0x22, 0x80, 0x02, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, + 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x68, 0x61, 0x6e, + 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, + 0x61, 0x6c, 0x69, 0x76, 0x65, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x5f, 0x72, 0x65, 0x70, 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, 0x64, 0x69, 0x73, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x66, 0x6f, 0x72, + 0x77, 0x61, 0x72, 0x64, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x5f, + 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x10, 0x0a, 0x12, 0x0e, 0x0a, 0x0a, 0x73, 0x68, 0x65, 0x6c, + 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x10, 0x0b, 0x12, 0x0c, 0x0a, 0x08, 0x76, 0x6e, 0x63, 0x5f, + 0x63, 0x74, 0x72, 0x6c, 0x10, 0x14, 0x12, 0x0d, 0x0a, 0x09, 0x76, 0x6e, 0x63, 0x5f, 0x69, 0x6d, + 0x61, 0x67, 0x65, 0x10, 0x15, 0x12, 0x0d, 0x0a, 0x09, 0x76, 0x6e, 0x63, 0x5f, 0x6d, 0x6f, 0x75, + 0x73, 0x65, 0x10, 0x16, 0x12, 0x10, 0x0a, 0x0c, 0x76, 0x6e, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x62, + 0x6f, 0x61, 0x72, 0x64, 0x10, 0x17, 0x12, 0x0b, 0x0a, 0x07, 0x76, 0x6e, 0x63, 0x5f, 0x63, 0x61, + 0x64, 0x10, 0x18, 0x12, 0x0e, 0x0a, 0x0a, 0x76, 0x6e, 0x63, 0x5f, 0x73, 0x63, 0x72, 0x6f, 0x6c, + 0x6c, 0x10, 0x19, 0x12, 0x11, 0x0a, 0x0d, 0x76, 0x6e, 0x63, 0x5f, 0x63, 0x6c, 0x69, 0x70, 0x62, + 0x6f, 0x61, 0x72, 0x64, 0x10, 0x1a, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x2f, 0x3b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/code/network/msg.proto b/code/network/msg.proto index 53f3395..62107e9 100644 --- a/code/network/msg.proto +++ b/code/network/msg.proto @@ -35,9 +35,7 @@ message msg { } type _type = 1; string from = 2; - uint32 from_idx = 3; string to = 4; - uint32 to_idx = 5; string link_id = 6; oneof payload { handshake_payload hsp = 10; diff --git a/code/server/handler/client.go b/code/server/handler/client.go index f9ded98..4997260 100644 --- a/code/server/handler/client.go +++ b/code/server/handler/client.go @@ -11,15 +11,27 @@ import ( type client struct { sync.RWMutex + id string parent *clients - idx uint32 conn *network.Conn updated time.Time links map[string]struct{} // link id => struct{} } +func (c *client) close() { + for _, link := range c.getLinks() { + c.parent.parent.closeLink(link) + c.Lock() + delete(c.links, link) + c.Unlock() + } + c.conn.Close() + c.parent.remove(c.id) + logging.Info("client %s connection closed", c.id) +} + func (c *client) run() { - defer c.parent.parent.closeClient(c) + defer c.close() for { if time.Since(c.updated).Seconds() > 600 { links := make([]string, 0, len(c.links)) @@ -28,7 +40,7 @@ func (c *client) run() { links = append(links, id) } c.RUnlock() - logging.Info("%s-%d is not keepalived, links: %v", c.parent.id, c.idx, links) + logging.Info("%s is not keepalived, links: %v", c.id, links) return } msg, size, err := c.conn.ReadMessage(c.parent.parent.cfg.ReadTimeout) @@ -36,7 +48,7 @@ func (c *client) run() { if strings.Contains(err.Error(), "i/o timeout") { continue } - logging.Error("read message from %s-%d: %v", c.parent.id, c.idx, err) + logging.Error("read message from %s: %v", c.id, err) return } c.updated = time.Now() @@ -70,11 +82,10 @@ func (c *client) getLinks() []string { return ret } -func (c *client) closeLink(id string) { +func (c *client) sendClose(id string) { var msg network.Msg msg.From = "server" - msg.To = c.parent.id - msg.ToIdx = c.idx + msg.To = c.id msg.XType = network.Msg_disconnect msg.LinkId = id c.conn.WriteMessage(&msg, c.parent.parent.cfg.WriteTimeout) @@ -83,15 +94,10 @@ func (c *client) closeLink(id string) { c.Unlock() } -func (c *client) is(id string, idx uint32) bool { - return c.parent.id == id && c.idx == idx -} - func (c *client) keepalive() { var msg network.Msg msg.From = "server" - msg.To = c.parent.id - msg.ToIdx = c.idx + msg.To = c.id msg.XType = network.Msg_keepalive for { time.Sleep(10 * time.Second) diff --git a/code/server/handler/clients.go b/code/server/handler/clients.go index 8273f71..21b91ff 100644 --- a/code/server/handler/clients.go +++ b/code/server/handler/clients.go @@ -2,65 +2,49 @@ package handler import ( "sync" - "sync/atomic" "time" "github.com/jkstack/natpass/code/network" - "github.com/lwch/logging" ) type clients struct { sync.RWMutex parent *Handler - id string - data map[uint32]*client // idx => client - idx uint32 + data map[string]*client // id => client } -func newClients(parent *Handler, id string) *clients { - logging.Info("new clients: %s", id) +func newClients(parent *Handler) *clients { return &clients{ parent: parent, - id: id, - data: make(map[uint32]*client), + data: make(map[string]*client), } } -func (cs *clients) new(idx uint32, conn *network.Conn) *client { - logging.Info("new client: %s-%d", cs.id, idx) +func (cs *clients) new(id string, conn *network.Conn) *client { cli := &client{ + id: id, parent: cs, - idx: idx, conn: conn, updated: time.Now(), links: make(map[string]struct{}), } cs.Lock() - cs.data[idx] = cli + if c, ok := cs.data[id]; ok { + c.close() + } + cs.data[id] = cli cs.Unlock() return cli } -func (cs *clients) next() *client { - list := make([]*client, 0, len(cs.data)) - cs.RLock() - for _, cli := range cs.data { - list = append(list, cli) - } - cs.RUnlock() - if len(list) > 0 { - idx := atomic.AddUint32(&cs.idx, 1) - cli := list[int(idx)%len(list)] - return cli - } - return nil +func (cs *clients) remove(id string) { + cs.Lock() + delete(cs.data, id) + cs.Unlock() } -func (cs *clients) close(idx uint32) { - cs.Lock() - delete(cs.data, idx) - cs.Unlock() - if len(cs.data) == 0 { - cs.parent.removeClients(cs.id) - } +func (cs *clients) lookup(id string) *client { + cs.RLock() + defer cs.RUnlock() + return cs.data[id] } diff --git a/code/server/handler/handler.go b/code/server/handler/handler.go index 971b256..ed25ed6 100644 --- a/code/server/handler/handler.go +++ b/code/server/handler/handler.go @@ -22,7 +22,7 @@ func (link *link) close() { if cli == nil { return } - cli.closeLink(link.id) + cli.sendClose(link.id) } close(link.endPoints[0]) close(link.endPoints[1]) @@ -30,20 +30,20 @@ func (link *link) close() { // Handler handler type Handler struct { - cfg *global.Configure - lockClients sync.RWMutex - clients map[string]*clients // client id => client - lockLinks sync.RWMutex - links map[string]link // link id => endpoints + cfg *global.Configure + clis *clients + lockLinks sync.RWMutex + links map[string]link // link id => endpoints } // New create handler func New(cfg *global.Configure) *Handler { - return &Handler{ - cfg: cfg, - clients: make(map[string]*clients), - links: make(map[string]link), + h := &Handler{ + cfg: cfg, + links: make(map[string]link), } + h.clis = newClients(h) + return h } // Handle main loop @@ -59,7 +59,7 @@ func (h *Handler) Handle(conn net.Conn) { }() var err error for i := 0; i < 10; i++ { - id, idx, err = h.readHandshake(c) + id, err = h.readHandshake(c) if err != nil { if err == errInvalidHandshake { logging.Error("invalid handshake from %s", c.RemoteAddr().String()) @@ -75,83 +75,61 @@ func (h *Handler) Handle(conn net.Conn) { } logging.Info("%s-%d connected", id, idx) - clients := h.tryGetClients(id) - cli := clients.new(idx, c) + cli := h.clis.new(id, c) - defer h.closeClient(cli) + defer cli.close() go cli.keepalive() cli.run() } -func (h *Handler) tryGetClients(id string) *clients { - h.lockClients.Lock() - defer h.lockClients.Unlock() - clients := h.clients[id] - if clients != nil { - return clients - } - clients = newClients(h, id) - h.clients[id] = clients - return clients -} - // readHandshake read handshake message and compare secret encoded from md5 -func (h *Handler) readHandshake(c *network.Conn) (string, uint32, error) { +func (h *Handler) readHandshake(c *network.Conn) (string, error) { msg, _, err := c.ReadMessage(5 * time.Second) if err != nil { - return "", 0, err + return "", err } if msg.GetXType() != network.Msg_handshake { - return "", 0, errNotHandshake + return "", errNotHandshake } n := bytes.Compare(msg.GetHsp().GetEnc(), h.cfg.Enc[:]) if n != 0 { - return "", 0, errInvalidHandshake + return "", errInvalidHandshake } - return msg.GetFrom(), msg.GetFromIdx(), nil + return msg.GetFrom(), nil } -func (h *Handler) getClient(linkID, to string, toIdx uint32) *client { +func (h *Handler) getClient(linkID, to string) *client { h.lockLinks.RLock() link := h.links[linkID] h.lockLinks.RUnlock() - if link.endPoints[0] != nil && link.endPoints[0].is(to, toIdx) { + if link.endPoints[0] != nil && link.endPoints[0].id == to { return link.endPoints[0] } - if link.endPoints[1] != nil && link.endPoints[1].is(to, toIdx) { + if link.endPoints[1] != nil && link.endPoints[1].id == to { return link.endPoints[1] } - h.lockClients.RLock() - clients := h.clients[to] - h.lockClients.RUnlock() - - if clients == nil { - return nil - } - return clients.next() + return h.clis.lookup(to) } func (h *Handler) onMessage(from *client, conn *network.Conn, msg *network.Msg, size uint16) { to := msg.GetTo() - toIdx := msg.GetToIdx() if msg.GetXType() == network.Msg_keepalive { return } - cli := h.getClient(msg.GetLinkId(), to, toIdx) + cli := h.getClient(msg.GetLinkId(), to) if cli == nil { - logging.Error("client %s-%d not found", to, toIdx) + logging.Error("client %s not found", to) return } h.msgHook(msg, from, cli, size) err := cli.writeMessage(msg) if err != nil { - logging.Error("write message %s from %s-%d to %s-%d: %v", + logging.Error("write message %s from %s to %s: %v", msg.GetXType().String(), - msg.GetFrom(), msg.GetFromIdx(), - msg.GetTo(), msg.GetToIdx(), + msg.GetFrom(), msg.GetTo(), err) } } @@ -171,8 +149,8 @@ func (h *Handler) addLink(name, id string, t network.ConnectRequestType, from, t h.lockLinks.Lock() h.links[id] = link h.lockLinks.Unlock() - logging.Info("add link %s name %s from %s-%d to %s-%d", - id, name, from.parent.id, from.idx, to.parent.id, to.idx) + logging.Info("add link %s name %s from %s to %s", + id, name, from.id, to.id) } func (h *Handler) removeLink(id string, from, to *client) { @@ -185,17 +163,17 @@ func (h *Handler) removeLink(id string, from, to *client) { h.lockLinks.Lock() delete(h.links, id) h.lockLinks.Unlock() - logging.Info("remove link %s from %s-%d to %s-%d", - id, from.parent.id, from.idx, to.parent.id, to.idx) + logging.Info("remove link %s from %s to %s", + id, from.id, to.id) } func (h *Handler) responseLink(id string, ok bool, msg string, from, to *client) { if ok { - logging.Info("link %s from %s-%d to %s-%d connect successed", - id, from.parent.id, from.idx, to.parent.id, to.idx) + logging.Info("link %s from %s to %s connect successed", + id, from.id, to.id) } else { - logging.Info("link %s from %s-%d to %s-%d connect failed, %s", - id, from.parent.id, from.idx, to.parent.id, to.idx, msg) + logging.Info("link %s from %s to %s connect failed, %s", + id, from.id, to.id, msg) // TODO: remove link? } } @@ -216,48 +194,31 @@ func (h *Handler) msgHook(msg *network.Msg, from, to *client, size uint16) { // forward data case network.Msg_forward: data := msg.GetXData() - logging.Debug("link %s forward %d bytes from %s-%d to %s-%d", - msg.GetLinkId(), len(data.GetData()), from.parent.id, from.idx, to.parent.id, to.idx) + logging.Debug("link %s forward %d bytes from %s to %s", + msg.GetLinkId(), len(data.GetData()), from.id, to.id) case network.Msg_shell_data: data := msg.GetSdata() - logging.Debug("shell %s forward %d bytes from %s-%d to %s-%d", - msg.GetLinkId(), len(data.GetData()), from.parent.id, from.idx, to.parent.id, to.idx) + logging.Debug("shell %s forward %d bytes from %s to %s", + msg.GetLinkId(), len(data.GetData()), from.id, to.id) // shell case network.Msg_shell_resize: data := msg.GetSresize() - logging.Info("shell %s from %s-%d to %s-%d resize to (%d,%d)", - msg.GetLinkId(), from.parent.id, from.idx, to.parent.id, to.idx, + logging.Info("shell %s from %s to %s resize to (%d,%d)", + msg.GetLinkId(), from.id, to.id, data.GetRows(), data.GetCols()) } - msg.From = from.parent.id - msg.FromIdx = from.idx - msg.To = to.parent.id - msg.ToIdx = to.idx - logging.Debug("forward %d bytes on link %s from %s-%d to %s-%d", size, msg.GetLinkId(), - from.parent.id, from.idx, to.parent.id, to.idx) + msg.From = from.id + msg.To = to.id + logging.Debug("forward %d bytes on link %s from %s to %s", size, msg.GetLinkId(), + from.id, to.id) } -func (h *Handler) closeClient(cli *client) { - links := cli.getLinks() - for _, t := range links { - h.lockLinks.RLock() - link := h.links[t] - h.lockLinks.RUnlock() - link.close() - h.lockLinks.Lock() - delete(h.links, t) - h.lockLinks.Unlock() - } - h.lockClients.RLock() - clients := h.clients[cli.parent.id] - h.lockClients.RUnlock() - if clients != nil { - clients.close(cli.idx) - } -} - -func (h *Handler) removeClients(id string) { - h.lockClients.Lock() - delete(h.clients, id) - h.lockClients.Unlock() +func (h *Handler) closeLink(id string) { + h.lockLinks.RLock() + link := h.links[id] + h.lockLinks.RUnlock() + link.close() + h.lockLinks.Lock() + delete(h.links, id) + h.lockLinks.Unlock() } diff --git a/conf/common.yaml b/conf/common.yaml index 064d65f..5349cb7 100644 --- a/conf/common.yaml +++ b/conf/common.yaml @@ -1,6 +1,5 @@ secret: 0123456789 # 预共享密钥,否则握手失败 link: - connections: 3 # 连接数,仅client有效 read_timeout: 1s # 读取数据包超时时间 write_timeout: 1s # 发送数据包超时时间 log: From 569d5a3dde132b3247c2adb8183c273dfc3729af Mon Sep 17 00:00:00 2001 From: lwch Date: Mon, 24 Jan 2022 10:29:52 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/client/dashboard/h_info.go | 11 +++++------ html/dashboard/js/index.js | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/code/client/dashboard/h_info.go b/code/client/dashboard/h_info.go index 2813e16..c4c6bd1 100644 --- a/code/client/dashboard/h_info.go +++ b/code/client/dashboard/h_info.go @@ -10,17 +10,16 @@ import ( // Info information data func (db *Dashboard) Info(w http.ResponseWriter, r *http.Request) { var ret struct { - Rules int `json:"rules"` - PhysicalLinks int `json:"physical_links"` - VirtualLinks int `json:"virtual_links"` - Session int `json:"sessions"` + Rules int `json:"rules"` + VirtualLinks int `json:"virtual_links"` + Session int `json:"sessions"` } ret.Rules = len(db.cfg.Rules) - ret.PhysicalLinks = 0 // TODO db.mgr.Range(func(t rule.Rule) { n := len(t.GetLinks()) ret.VirtualLinks += n - if t.GetTypeName() == "shell" { + if t.GetTypeName() == "shell" || + t.GetTypeName() == "vnc" { ret.Session += n } }) diff --git a/html/dashboard/js/index.js b/html/dashboard/js/index.js index c15111b..d5211aa 100644 --- a/html/dashboard/js/index.js +++ b/html/dashboard/js/index.js @@ -12,7 +12,6 @@ var page = { $.get('/api/info', function(ret) { $('#cards').empty(); page.add_card('规则总数', ret.rules); - page.add_card('物理连接数', ret.physical_links); page.add_card('虚拟连接数', ret.virtual_links); page.add_card('终端会话', ret.sessions); }); From c1bc5089b9c98fe5e8fc5818ec973b20e7668e63 Mon Sep 17 00:00:00 2001 From: lwch Date: Mon, 24 Jan 2022 10:48:02 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/server/handler/handler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/server/handler/handler.go b/code/server/handler/handler.go index ed25ed6..e0e5719 100644 --- a/code/server/handler/handler.go +++ b/code/server/handler/handler.go @@ -50,7 +50,6 @@ func New(cfg *global.Configure) *Handler { func (h *Handler) Handle(conn net.Conn) { c := network.NewConn(conn) var id string - var idx uint32 defer func() { if len(id) > 0 { logging.Info("%s disconnected", id) @@ -73,7 +72,7 @@ func (h *Handler) Handle(conn net.Conn) { if err != nil { return } - logging.Info("%s-%d connected", id, idx) + logging.Info("%s connected", id) cli := h.clis.new(id, c) From 16a5c03ebc6b84bec1f98fa1f292b2610efa5cef Mon Sep 17 00:00:00 2001 From: lwch Date: Mon, 24 Jan 2022 14:47:28 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 +++++- docs/desc.md | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d8f7e..8ee6b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,4 +95,8 @@ 1. 升级go版本到1.17.6 2. 简化打包docker镜像 -3. 优化性能 \ No newline at end of file +3. 优化性能 + +# v0.8.2 + +1. 去除连接池的支持 \ No newline at end of file diff --git a/docs/desc.md b/docs/desc.md index 3c29aca..f965cb5 100644 --- a/docs/desc.md +++ b/docs/desc.md @@ -1,6 +1,6 @@ # 实现原理 -基于tls链接,protobuf进行数据传输,下面举例远程连接服务器集群内的某台主机 +支持tls链接,protobuf进行数据传输,下面举例远程连接服务器集群内的某台主机 ![shell](imgs/example.jpg) From b09c7a0719342a27b8d7c3677b8f657246747596 Mon Sep 17 00:00:00 2001 From: lwch Date: Mon, 24 Jan 2022 14:47:46 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E6=89=93=E5=8C=85=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build | 2 +- build_all | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build b/build index 895032d..7861ab8 100755 --- a/build +++ b/build @@ -1,6 +1,6 @@ #!/bin/sh -VERSION=0.8.1 +VERSION=0.8.2 DOCKER_BUILDKIT=1 docker build -t natpass -f contrib/build/Dockerfile . docker run --rm \ diff --git a/build_all b/build_all index a9699b9..e99a3d2 100755 --- a/build_all +++ b/build_all @@ -1,6 +1,6 @@ #!/bin/sh -VERSION=0.8.1 +VERSION=0.8.2 DOCKER_BUILDKIT=1 docker build -t natpass -f contrib/build/Dockerfile . docker run --rm \ From 6cf13a38c9daf9a90d68b8c1d5cf38c3a2aec830 Mon Sep 17 00:00:00 2001 From: lwch Date: Mon, 24 Jan 2022 15:06:07 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++++++++ docs/imgs/bench.png | Bin 0 -> 32916 bytes 2 files changed, 9 insertions(+) create mode 100644 docs/imgs/bench.png diff --git a/README.md b/README.md index 523e62b..40d91dc 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,15 @@ windows读取剪贴板内容 ![vnc-clipboard](docs/imgs/vnc_clipboard.png) +## 性能 + +在vmware环境下创建4C2G(AMD Ryzen 7 4800U with Radeon Graphics)测试环境,并进行all in one部署server、remote端和local端,使用bench规则进行压测,结果如下: + +![bench](docs/imgs/bench.png) + +1. 压测结果仅包含local端发起连接到remote端收到连接并返回成功的整个过程 +2. 实验结果表明,在4C2G环境下可达到上万+的qps,且p99和p100均在60ms以下 + ## TODO 1. ~~支持include的yaml配置文件~~ diff --git a/docs/imgs/bench.png b/docs/imgs/bench.png new file mode 100644 index 0000000000000000000000000000000000000000..cef93407664a3c4f5477409e929f876b496d3eb5 GIT binary patch literal 32916 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

}+Fi+n zONdNAPFV}`_&Fu7%nC>I^IF(=wri^5aaw;L6j9%BFme{?Jm(Sho9l+t1Xrb{6s9%4 z7|t|#-R;@H7kMvGt3epBqLb8qM|+K&Xi+4Jq9(kQR-y?vXOi0vo)oOwXhMAH-W^B+ zE9xG1A1I)okgSZrE^@bW?6~M3<;%w@DrM4V1R-$V;8$_8=RV;}8{+*(`KOSsa$w>Vu>`D4B+nimg=UiCu7a{6 z1zq7vM%hI=^c3%K^68J5Zre!FZ)I=eLa~qj;Qd)bS&n|L50}ysUum51l@@J`;G=x4 zyR2oJ(fa0B<|=Rz0tJ=x&Ge5z%~Ks=O1-qx!(|d}Pi8~xX+N?`^~Q5l?0^if*;DLg z9Ao9Y1vhJ%A6c{3DGQACHJ@$s;HP#M!uU8+ z9+jeEx4TKJbcV<3GKet9WP7#yZ9M!srt02=jzEtdfLL-UaT@;d?^XCMoHttejsgqn z=14IdbE_c@&K<;NwFXIg7e<;Cw+eTVuCp0qx36O&TTK?h;Z0k(*RB254f{aj7oT#H z`SNPTqk99#{*{-#P#QPI(eOx>>%m9F*eqwx5o*ry6H;JrM<1O;@71`NX?{mr*PrDa zks-_z*ToS)3(^f?NNIyh=3D;xjOA7=6~-$n#fik1Q5Sy!j)g! zq4NQ2y}xO@KYrD@-S<=A=yn>(iL%bP594XYuhOy#RhXIJos1v6sA>~EE)Bo2K+6h* zPs@Hc7M*1BQ4`2nI2<>%ijU`25N4JIqw}T>tlD+vfCcp~0PX zUz5qy(`|{oPGp=GO`1u+X#@!5N3KN=;w=zTZ+F%M;gCw<$++!x3LVI$hjc&Wy*x84 z`0w2D8+SmD1zimv+bm7VO+8eeNi@1AkE)#SIP+fB#k=HuD7+!=aq`yWt18>2v)^Xl zU?@5}#>uKFt{q0>YE~7TASqhrMqAcz`rEBy&Hk5F>8_*hiVGproY({U+kSMsD5}kT zzgV(rq(wSSE=#e}c)sj*U}y6CPaTypHxP~~-mC6bV(I}Vx&rUlHb$1hpuj7fgp(;$ zSH*s(ye!E(CF=0% zb45Q3XIUBKH3I9W^cl7&DkR8183$bn=GHS&OwxGed97~OcE1a0oBoz{6DeCU{H7G? ziO9G7dwNGW@D;zz&h-q}5*UQQEyBBG(6mU~eOZ=x{M`Bn%7Y}w$#|0O zCnQ1&T>Je4<)SkaQbzH<*;SJ`ey(gf(ZiQ|d1oZ(*h1w5DZ7uGN`0Uh#A9;H4ssSg zsg6&~_?xiC7Z(_Jxob`&a>ktwzXx0`T*0=p61zB%{*HzCrr?e-mc+l>}!d7oypzmYvuiMtf7Pv;9>E|(j7DhPfljagNik-m5C`N>3D@QzO7vr{HN z8!}r#8#MM|M&NL%rk#ezHfKx?qu<$bqk4RXg)>{`Rrg=Qzuy4|(+Uq%D`1y@V1$~` zWYBd??OT6BtRCUc6sCCVLLvIHSf}Xl#eM&Z_|nH9;RZjMD!IvI!I1^G;i_Ai-m@Ok zZL^b}3)F}7f=~5{s8}UG`N;;W(yREz))}Xs16Tii{s*lz2yi^8`p`sPEm1-TZagdP)iRV9jsvI<_=E4-^Wp_+fl zan!Y$Cr@WaHQLabmZXB75Rc!4YR6^f{6#vvy|U#6SQbiS+{PGr1>{jK@??#R94%LF z5S;yMpGv17P)QRfSctKTBq}RplRe6zHjTC5=lEm^P)1Bmm=zneCS1dMh&`v&AQjS{ zGqv49UP!DRtIp+wIcO4M_X}A6pu8P(hcyh1V9{GWQKvq=RHWLnVI)4APm_FmK7>w)Txvu9v*A|b} z<@a<6&70nol3OZ*NpEEc&_dJD}>ptiEl>gaY)H^n+HI1=3P5KckOyL+H6R zsZ5OECbdDR|KU97#i=n?n)lAK94>gfQ;pYi;1u*kFZ-K>w*K8-2_cWfKa0g?+PO8t zPasl0Jr~5zqf}A{G6>z1(xpRd8 zkFE9<9_$|XG9G(m(a5{WYVz1d7#sLxES9j|q(P%Qw7J&)erkWIcOkF!qAYm(SiOdXj``Zo1f?t&)+=bLV-z`C{*m%7JFUcsG!{6; zn_c!u5F-?<<8PN_Qx+4S zN|-yX#sPj*`&OfNKlrg+U|w`NkLUHsyVhl1_xo>ax3sPU_`>0IQj9vcf}cS070abD zk?HSF&YBZmpP6)$TTEnn77cxRaHhW$c`m#QWMap%v z*_HA)PuqN)J$K<>33c|l0~Hdo9#-m{_7DzyaYHsbv8~+3iR>JZJj)ltpq2xe?*uit zQx7gbb<{8EW%zjzSH*Pn~0iL=5?FT$j&ccKjE_L`HVwu6t2IZ1&R zGQ)4Hkt76=ho^$V1j}PwB>+_qd8jc%VxJkST-tt>MH1^8AD|eBe?-d(_E3g^lr-7e zYN+#i6JdCGbXL!wyqLJ%iiyME^p}?n+SL>ERS#+Mb744)sZ=>PcSlNh(=4{5g{PS`yP8lgr;k_eqh`@ZHeSI-AX}q~X6mGcUu!)=+cH1v z9=aGJQ~8yc!Gb$Qq6D-Rh0WCYN{dMzWglR?=M&h1xqk?3te?F71_s&j%@FI5?OMf7 zftqA7Tt~I`F(;?lC?m2boI5C|kMXatkHNk!=dOXfnrh^r)bAlyvqqcS1$}C-dUXG9 z_5Z_yCe|B%&3vC2FbxQ76l^kjoa+5R|Cxj6noi=S>W3 z{t}e38~b%v!qp}wIbAjnbEl(~J&Vd%jr-t_;x#wpY48S@>ctDk(ZyuV`^fv$Xa6d` zoZYnBcAD{ngku7UKf@Q?S;j0rW)J+qY^pC7BL5H6whQcq{*se;O9@scETWbcr{(*{ zx=`dk&Eh@U_ z@#oHFOq>$h(~s$Z04#MC{LM?pZ;ccBDukz z=JXskHlD+i5A2#*Izl?eejT0=`8&FSw_p753)KV-5mcdSe9vCsQ||C5c42%w=^$J4 zj#XFu1wCPe9kkH&&JrmXT&iI6JVTs}X2cQvK%jk$$+ar(DW_J+tA7}ND?;k{$@Tlj% z>HSe<&s?rX3w^8A?}|GWwVl;-U@fFS*|s420mbnGV{UW44_TTd2#e+H6V7}cs~rqS zp8@!k+@eYWDbx}^W!BQvm9!L*O*?h{wluK^D9eMxLbyvUTC$#`r4NPQn`#L3+&*wL zQZ*vo(eqQ(fOz*T?e&p)* z4f!3XsoeF|vis~3o@D?d<8ksc`)tJUp|4roPr$JE3|$kEe)*rkW8Cclcjc3Tj3RAb zeU&?i>V`yQRYU>}qw0R;zKJ~mB>!(0wI((APtiXy*Pk(eWHw_6-gJU^<5l=H`3W6} zNj2{ciIGi3pz7uF53-B0H_MkKRm`u(r|Mu`2deRTU<-;b}wV~NB5I-VfDFSc$D&%f2ob~@& z!uQ+Nl?`KJz4Kn9?;zDJe_9+^9o^jZxR+4yag3x7$B~rEFVQZJ!qBtyqiP3f*J9|@fr43F%O}^-%CbKxBiaM+#qKBb zo@qNv?ArWv(#ifdt$vogbA2h7afsV-`Hr%$v*CHm-=zVl9RnZnNaFlF8#Gcl}CaAtQztb2QnE)rsy7d~^S|cKCAaP&_`NYE{ZY>sXG^F{0pUGP5NXPURv|fnMu}?2_V4+P%PS?FDL1aONhksg|4=Ui#Rb~0+ zR4>zJ5}?L+l5KBOLGyiTEp6cstz$aOI}KSS!KyVn66dDB?7s2=jEHzIv8=Z z>kSCWaNx&4+1&f>80J98a_f46)3^g0l$JcBO*oX*QH_j+WTt(%={PwHV{U)&PTxCQ z>!%op{h^pJ+>kRG?C0J5j|o5M@kmE@3K%&yoU%r!Ts7hE56ugkyTkJJu&M zKQ12#meC%2T8lVTGF468KkImS*26x4N0u+}0k=vzTAt&p*JuMR^?XlEHzT^UQqh;ccW46&&S!v z|LcFZ!EY=0U;k&|QMh}wv-}`{?QRta=!YNq^;9L2#HvBu;J$_#wTd=37e}mqF8F99 zAD@*DJC>t?|2S-|>ig_ZZt3Awv7mt)XZILB^S`3(^WTF~gelUd^{t`WLf{44B5c>fm!EZ6(_fWX} z7Q(?&bp=z6Ebj|Rv$v+O+dm4fUNp%Y`M-L5�!hE?hW>0!j&r3QCh>!GeHNq$6;S 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 M52N2pb?yKBA0u0uQUCw| literal 0 HcmV?d00001