diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a929be1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package client", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/tunnelclient", + "args":["--target-session", "HNCZhU8G86HSfTZv", "--local-port", "2222"] + }, + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/tunnelserver/main.go" + } + + ] +} \ No newline at end of file diff --git a/client/dataconn.go b/client/dataconn.go new file mode 100644 index 0000000..538b83f --- /dev/null +++ b/client/dataconn.go @@ -0,0 +1,169 @@ +package client + +import ( + "bytes" + "encoding/binary" + "io" + "log" + "net" + "strconv" + + "git.pyer.club/kingecg/goemitter" + "git.pyer.club/kingecg/gotunnelserver/util" + ws "github.com/gorilla/websocket" +) + +type DataEndPoint struct { + Host string + Port int + + cmdSession *CommandClient + dataSession string + wsConn *ws.Conn + conns map[int32]*DataConn +} + +func (d *DataEndPoint) Close() { + d.wsConn.Close() +} + +func (d *DataEndPoint) onDataConnClose(conn *DataConn) { + delete(d.conns, conn.id) +} + +func (d *DataEndPoint) Connect() { + conn, _, err := ws.DefaultDialer.Dial(d.cmdSession.Address+"/ws/pipe/"+d.dataSession, map[string][]string{ + "Authorization": {d.cmdSession.makeAuthHeader()}, + "Session": {d.cmdSession.SessionId}, + }) + if err != nil { + panic(err) + } + d.wsConn = conn + for { + // d.wsConn.SetReadDeadline(time.Now().Add(time.Minute * 5)) + _, data, err := d.wsConn.ReadMessage() + if err != nil { + break + } + var packet Packet + err = packet.BinaryUnmarshaler(data) + if err != nil { + continue + } + conn, ok := d.conns[packet.id] + if !ok { + tcpconn, err := net.Dial("tcp", d.Host+":"+strconv.Itoa(d.Port)) + if err != nil { + log.Println(err) + continue + } + conn = NewDataConnection(packet.id, &tcpconn, d) + conn.Start() + conn.Once("close", func(args ...interface{}) { + d.onDataConnClose(conn) + }) + } + + conn.Write(packet) + } +} + +func (d *DataEndPoint) Listen() { + // listen and accept connection at port + listener, err := net.Listen("tcp", d.Host+":"+strconv.Itoa(d.Port)) + if err != nil { + log.Println(err, d) + panic(err) + } + for { + conn, err := listener.Accept() + if err != nil { + panic(err) + } + dconn := NewDataConnection(0, &conn, d) + d.conns[dconn.id] = dconn + dconn.Start() + dconn.Once("close", func(args ...interface{}) { + d.onDataConnClose(dconn) + }) + } +} + +func (d *DataEndPoint) Write(p []byte) (n int, err error) { + return len(p), d.wsConn.WriteMessage(ws.BinaryMessage, p) +} + +func NewDataEndPoint(cmdSession *CommandClient, dataSession string) *DataEndPoint { + return &DataEndPoint{ + cmdSession: cmdSession, + dataSession: dataSession, + conns: make(map[int32]*DataConn), + } +} + +type DataConn struct { + id int32 + conn *net.Conn + out io.Writer + *goemitter.EventEmitter +} + +type Packet struct { + id int32 + data []byte +} + +func (p *Packet) MarshalBinary() ([]byte, error) { + buf := new(bytes.Buffer) + binary.Write(buf, binary.LittleEndian, p) + return buf.Bytes(), nil +} + +func (p *Packet) BinaryUnmarshaler(data []byte) error { + buf := bytes.NewReader(data) + return binary.Read(buf, binary.LittleEndian, p) +} +func (d *DataConn) Close() { + (*d.conn).Close() +} + +func (d *DataConn) Write(p Packet) (n int, err error) { + return (*d.conn).Write(p.data) +} + +func (d *DataConn) Start() { + go func() { + for { + buf := make([]byte, 1024) + n, err := (*d.conn).Read(buf) + if err != nil { + // panic(err) + break + } + packet := Packet{ + id: d.id, + data: buf[:n], + } + data, err := packet.MarshalBinary() + if err != nil { + continue + } + d.out.Write(data) + } + d.Emit("close") + }() + +} +func NewDataConnection(id int32, con *net.Conn, writer io.Writer) *DataConn { + cid := id + if cid == 0 { + cid = util.GenRandomInt() + } + return &DataConn{ + id: cid, + conn: con, + out: writer, + EventEmitter: goemitter.NewEmitter(), + } +} diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..a899025 --- /dev/null +++ b/client/main.go @@ -0,0 +1,100 @@ +package client + +import ( + "encoding/base64" + "encoding/json" + "log" + "time" + + "git.pyer.club/kingecg/goemitter" + "git.pyer.club/kingecg/gotunnelserver/server" + "git.pyer.club/kingecg/gotunnelserver/util" + ws "github.com/gorilla/websocket" +) + +type ClientConfig struct { + Username string + Salt string + Address string +} + +type CommandClient struct { + *ws.Conn + *goemitter.EventEmitter + *ClientConfig + path string + SessionId string +} + +func (c *CommandClient) Close() { + if c.Conn != nil { + c.Conn.Close() + c.Conn = nil + } +} + +func (c *CommandClient) Send(cmd *server.Command) (err error) { + return c.WriteJSON(cmd) +} +func (c *CommandClient) Start() { + defer c.Close() + // cpath:="/client" + // if c.IsAgent { + // cpath = "/agent" + // } + authHeader := c.makeAuthHeader() + conn, _, err := ws.DefaultDialer.Dial(c.Address+c.path, map[string][]string{ + "Authorization": {authHeader}, + }) + if err != nil { + panic(err) + } + c.Conn = conn + c.On(util.CmdTypeMap[util.NewSession], func(args ...interface{}) { + playload, _ := args[0].(map[string]string) + sessionId := playload["sessionId"] + log.Println("new session:", sessionId) + c.SessionId = sessionId + }) + for { + var cmd server.Command + err := c.ReadJSON(&cmd) + if err != nil { + return + } + eventLabel := util.CmdTypeMap[cmd.Type] + log.Println("cmd:", cmd.Type, eventLabel) + c.Emit(eventLabel, cmd.Payload) + } +} + +func (c *CommandClient) makeAuthHeader() string { + a := &util.AuthEntity{ + Entity: util.Entity{ + Username: c.Username, + }, + Time: time.Now().Unix(), + } + a.Authtoken = util.GenAuthToken(a, c.Salt) + str, err := json.Marshal(a) + if err != nil { + panic(err) + } + return base64.StdEncoding.EncodeToString(str) +} + +func NewClient(conf *ClientConfig) *CommandClient { + return &CommandClient{ + ClientConfig: conf, + EventEmitter: goemitter.NewEmitter(), + path: "/ws/client", + } +} + +func NewAgent(conf *ClientConfig) *CommandClient { + return &CommandClient{ + ClientConfig: conf, + EventEmitter: goemitter.NewEmitter(), + path: "/ws/agent", + } +} diff --git a/client/main_test.go b/client/main_test.go new file mode 100644 index 0000000..e2932d9 --- /dev/null +++ b/client/main_test.go @@ -0,0 +1,15 @@ +package client + +import ( + "testing" +) + +func TestClient(t *testing.T) { + clientConfig := &ClientConfig{ + Address: "ws://localhost:8080", + Salt: "", + Username: "test", + } + client := NewClient(clientConfig) + client.Start() +} diff --git a/go.mod b/go.mod index f3213d0..b05e4a3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.pyer.club/kingecg/gotunnelserver go 1.23.1 require ( + git.pyer.club/kingecg/command v0.0.0-20241115071005-9f26d1cf2992 // indirect git.pyer.club/kingecg/goemitter v0.0.0-20240919084107-533c3d1be082 // indirect git.pyer.club/kingecg/gologger v1.0.5 // indirect github.com/gorilla/websocket v1.5.3 // indirect diff --git a/go.sum b/go.sum index 68276cd..261e36e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +git.pyer.club/kingecg/command v0.0.0-20241024110012-525a2ad9662d h1:CIcY8L5FcGy1uO+GvdMeidjdfYSxwTEEEvt3WXBOQAU= +git.pyer.club/kingecg/command v0.0.0-20241024110012-525a2ad9662d/go.mod h1:lnSzW19xOIlUwlewxHH0R4SIDO+a4++USjmMjo/jZB0= +git.pyer.club/kingecg/command v0.0.0-20241115051719-6fd5111300dc h1:PVvfibld+xRlWIbBlprF0AaIuTpF5QeY3CqthCQP8lg= +git.pyer.club/kingecg/command v0.0.0-20241115051719-6fd5111300dc/go.mod h1:7j+/UU5URp7UkTbNtYDvZY3cFywuTRys1TmP3HbNX3A= +git.pyer.club/kingecg/command v0.0.0-20241115071005-9f26d1cf2992 h1:QurLvSlNSU2TjTVe9h9+N9QVZEvSXuz9POebExqhfOo= +git.pyer.club/kingecg/command v0.0.0-20241115071005-9f26d1cf2992/go.mod h1:7j+/UU5URp7UkTbNtYDvZY3cFywuTRys1TmP3HbNX3A= git.pyer.club/kingecg/goemitter v0.0.0-20240919084107-533c3d1be082 h1:U7Jbet3zObT2qPJ2g408Z9OUvR6phQyHOoHeidM5zUg= git.pyer.club/kingecg/goemitter v0.0.0-20240919084107-533c3d1be082/go.mod h1:2jbknDqoWH41M3MQ9pQZDKBiNtDmNgPcM3XfkE9YkbQ= git.pyer.club/kingecg/gologger v1.0.5 h1:L/N/bleGHhEiaBYBf9U1z2ni0HfhaU71pk8ik/D11oo= diff --git a/server/handleAgent.go b/server/handleAgent.go index 4c0b8f6..c9f0e1e 100644 --- a/server/handleAgent.go +++ b/server/handleAgent.go @@ -3,7 +3,6 @@ package server import ( "net/http" - "git.pyer.club/kingecg/gotunnelserver/util" "github.com/gorilla/websocket" ) @@ -11,11 +10,6 @@ func (s *Server) HandleAgent(c *websocket.Conn, r *http.Request) { agentSession := NewSession(c) agentSession.Start() s.agentSession[agentSession.Id] = agentSession - command := &Command{ - Type: util.NewSession, - Payload: map[string]string{ - "id": agentSession.Id, - }, - } + command := NcmdSession(agentSession.Id) agentSession.Send(command) } diff --git a/server/handleClient.go b/server/handleClient.go index 5be212e..24f0be5 100644 --- a/server/handleClient.go +++ b/server/handleClient.go @@ -25,7 +25,9 @@ func (s *Server) HandleClient(conn *websocket.Conn, r *http.Request) { clientSession.Send(NewErrorResponse("target session not found", command)) return } - command := NcmdConnectionInited(clientSession.Id) + spipe := NewPipe(clientSession.Id, targetSessionId) + s.pipes[spipe.Id] = spipe + command := NcmdConnectionInited(spipe.Id) for k, v := range orgPlayload { command.Payload[k] = v diff --git a/server/handlePipe.go b/server/handlePipe.go index 9c8cf4e..de8be09 100644 --- a/server/handlePipe.go +++ b/server/handlePipe.go @@ -3,6 +3,7 @@ package server import ( "net/http" + "git.pyer.club/kingecg/gotunnelserver/util" "github.com/gorilla/websocket" ) @@ -23,6 +24,14 @@ func (p *Pipe) Start() { p.src.Close() p.dst.Close() } +func NewPipe(src, dst string) *Pipe { + return &Pipe{ + Id: util.GenRandomstring(16), + Src: src, + Dst: dst, + } + +} func (p *Pipe) forward(src, dst *websocket.Conn) { for { @@ -75,7 +84,7 @@ func (s *Server) HandlePipe(conn *websocket.Conn, r *http.Request) { } if pipe.src != nil && pipe.dst != nil { - pipe.Start() + go pipe.Start() clientCmdSession := s.findSession(pipe.Src) clientCmdSession.Send(NcmdConnectionReady(pipe.Id)) // info src endpoint ready and can setup proxy listener } diff --git a/server/main.go b/server/main.go index 4ff4a49..262f663 100644 --- a/server/main.go +++ b/server/main.go @@ -110,7 +110,7 @@ func (s *Server) registHandler() { s.mux = http.NewServeMux() } s.mux.HandleFunc("/hello", s.HandleHello) - s.mux.HandleFunc("/ws", s.HandleWs) + s.mux.HandleFunc("/ws/", s.HandleWs) } func (s *Server) Start() { addr := s.Config.Host + ":" + s.Config.Port diff --git a/server/session.go b/server/session.go index 219cc38..e9c529d 100644 --- a/server/session.go +++ b/server/session.go @@ -92,7 +92,7 @@ func NewErrorResponse(err string, cmd *Command) *Command { } func NcmdConnectionInited(sessionId string) *Command { - return NewCommand(util.ConnectionReady, map[string]string{"sessionId": sessionId}) + return NewCommand(util.ConnectInited, map[string]string{"sessionId": sessionId}) } func NcmdConnectionReady(sessionId string) *Command { diff --git a/tunnelagent/main.go b/tunnelagent/main.go new file mode 100644 index 0000000..db3c2a4 --- /dev/null +++ b/tunnelagent/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "log" + "os" + "strconv" + + "git.pyer.club/kingecg/command" + "git.pyer.club/kingecg/gotunnelserver/client" + "git.pyer.club/kingecg/gotunnelserver/util" +) + +type TunnelAgent struct { + Username string `flag_default:"tcclient" flag_usage:"username for tunnel server"` + Salt string `flag_default:"" flag_usage:"salt for tunnel server"` + Address string `flag_default:"ws://127.0.0.1:8080" flag_usage:"address for tunnel server"` +} + +var ( + cmdClient *client.CommandClient + dataEp *client.DataEndPoint +) + +func main() { + cargs := new(TunnelAgent) + v, _ := command.NewFVSet(cargs) + err := command.Parse(os.Args[1:], v) + if err != nil { + os.Exit(0) + return + } + clientConfig := &client.ClientConfig{ + Username: cargs.Username, + Salt: cargs.Salt, + Address: cargs.Address, + } + cmdClient = client.NewAgent(clientConfig) + cmdClient.On(util.CmdTypeMap[util.ConnectInited], func(args ...interface{}) { + playload, _ := args[0].(map[string]string) + sessionId := playload["sessionId"] + dataEp = client.NewDataEndPoint(cmdClient, sessionId) + dataEp.Host = playload["host"] + dataEp.Port, _ = strconv.Atoi(playload["port"]) + log.Println("connect inited:", sessionId, dataEp.Host, dataEp.Port) + go dataEp.Connect() + }) + cmdClient.On(util.CmdTypeMap[util.ErrorCmd], func(args ...interface{}) { + playload, _ := args[0].(map[string]string) + log.Println("error:", playload["error"]) + os.Exit(1) + }) + defer func() { + if r := recover(); r != nil { + log.Println(r) + return + } + }() + cmdClient.Start() +} diff --git a/tunnelclient/main.go b/tunnelclient/main.go new file mode 100644 index 0000000..379efc1 --- /dev/null +++ b/tunnelclient/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "log" + "os" + "strconv" + + "git.pyer.club/kingecg/command" + "git.pyer.club/kingecg/gotunnelserver/client" + "git.pyer.club/kingecg/gotunnelserver/server" + "git.pyer.club/kingecg/gotunnelserver/util" +) + +type TunnelClient struct { + TargetSession string `flag_default:"" flag_usage:"target session id"` + Username string `flag_default:"tcclient" flag_usage:"username for tunnel server"` + Salt string `flag_default:"" flag_usage:"salt for tunnel server"` + Address string `flag_default:"ws://127.0.0.1:8080" flag_usage:"address for tunnel server"` + Host string `flag_default:"127.0.0.1" flag_usage:"host to proxy to"` + Port int `flag_default:"22" flag_usage:"port to proxy to"` + LocalPort int `flag_default:"0" flag_usage:"local port to accept connection from"` + LocalHost string `flag_default:"127.0.0.1" flag_usage:"local host to accept connection from"` +} + +var ( + cmdClient *client.CommandClient + dataEp *client.DataEndPoint +) + +func main() { + cargs := new(TunnelClient) + v, _ := command.NewFVSet(cargs) + err := command.Parse(os.Args[1:], v) + if err != nil { + os.Exit(0) + return + } + + if cargs.TargetSession == "" { + log.Fatal("Must set target session to connect to") + return + } + + clientConfig := &client.ClientConfig{ + Username: cargs.Username, + Salt: cargs.Salt, + Address: cargs.Address, + } + cmdClient = client.NewClient(clientConfig) + cmdClient.Once(util.CmdTypeMap[util.ConnectInited], func(args ...interface{}) { + playload, _ := args[0].(map[string]string) + sessionId := playload["sessionId"] + dataEp = client.NewDataEndPoint(cmdClient, sessionId) + dataEp.Host = cargs.LocalHost + dataEp.Port = cargs.LocalPort + go dataEp.Connect() + }) + cmdClient.On(util.CmdTypeMap[util.NewSession], func(args ...interface{}) { + cmdClient.Send(&server.Command{ + Type: util.NewConnection, + Payload: map[string]string{ + "target": cargs.TargetSession, + "host": cargs.Host, + "port": strconv.Itoa(cargs.Port), + }, + }) + }) + + cmdClient.Once(util.CmdTypeMap[util.ConnectionReady], func(args ...interface{}) { + go dataEp.Listen() + log.Println("connection ready") + }) + + cmdClient.On(util.CmdTypeMap[util.ErrorCmd], func(args ...interface{}) { + playload, _ := args[0].(map[string]string) + log.Println("error:", playload["error"]) + os.Exit(1) + }) + + defer func() { + if r := recover(); r != nil { + log.Println(r) + return + } + }() + cmdClient.Start() +} diff --git a/tunnelserver/__debug_bin2913043442 b/tunnelserver/__debug_bin2913043442 new file mode 100755 index 0000000..eb894c4 Binary files /dev/null and b/tunnelserver/__debug_bin2913043442 differ diff --git a/util/const.go b/util/const.go index b6ba165..01b8078 100644 --- a/util/const.go +++ b/util/const.go @@ -7,3 +7,13 @@ const ( ConnectionReady ErrorCmd ) + +var ( + CmdTypeMap = map[int]string{ + NewSession: "new_session", + NewConnection: "new_connection", + ConnectInited: "connect_inited", + ConnectionReady: "connection_ready", + ErrorCmd: "error", + } +) diff --git a/util/entity.go b/util/entity.go index 17b1cc6..a96491b 100644 --- a/util/entity.go +++ b/util/entity.go @@ -57,3 +57,8 @@ func GenRandomstring(n int) string { } return string(b) } + +func GenRandomInt() int32 { + // rand.Seed(time.Now().UnixNano()) + return int32(rand.Intn(65534) + 1) +}