├── README.md ├── echoserver.go ├── relay.go └── stress_tester.go /README.md: -------------------------------------------------------------------------------- 1 | tcp-relay 2 | ========= 3 | 4 | A simple tcp relay written in Go. The [muxado](https://github.com/inconshreveable/muxado) 5 | library is used to multiplex TCP connections. Services wishing to utilize the relay must 6 | use a muxado session to connect to the relay. 7 | 8 | * `relay.go` implements the relay 9 | * `echoserver.go` implements a simple echo server that utilizes the relay 10 | 11 | Usage 12 | ----- 13 | 14 | Make sure you have Go set up properly. [Official Go setup page.](http://golang.org/doc/install) 15 | 16 | To build: 17 | 18 | ```sh 19 | $ go get github.com/inconshreveable/muxado 20 | $ go build relay.go 21 | $ go build echoserver.go 22 | ``` 23 | 24 | To run: 25 | 26 | ```sh 27 | $ ./relay 8080 & # the relay will listen for services on 8080 28 | $ ./echoserver localhost 8080 & # connect to the relay at localhost:8080 29 | localhost:8081 30 | # the relay opened 8081 for programs wishing to reach the echoserver 31 | $ telnet localhost 8081 32 | Hello, world 33 | Hello, world 34 | ``` 35 | 36 | `echoserver.go` is a clean, well commented example of how Go programs can connect 37 | and handshake with the relay. 38 | 39 | Architectural Overview 40 | ----------- 41 | 42 | ``` 43 | --------------- 44 | | HTTP Client | 45 | --------------- 46 | \ 47 | Generic TCP conn. -----> \ 48 | \ \ 49 | \ \ 50 | v ---------------- 51 | --------------- | | --------------- 52 | | Echo Client |---------------| tcp-relay |=================| HTTP Server | 53 | --------------- | | ^ --------------- 54 | ---------------- \ 55 | / \\ Multiplexed Connections 56 | / \\ / 57 | / \\ v 58 | / \\ 59 | --------------- \\ 60 | | HTTP Client | \\ 61 | --------------- --------------- 62 | | Echo Server | 63 | --------------- 64 | ``` 65 | 66 | n00bish design decisions 67 | ------------------------ 68 | 69 | * Services wishing to register with the relay are assigned a forward-facing port. 70 | If the relay is handling too many services, the service is put on hold until a 71 | port opens up. This could take a very long time. 72 | * The closing of connections isn't handled very gracefully. It was either that or spaghetti code. 73 | This means the relay probably isn't very robust, because resources aren't being freed properly. 74 | * Many programs can't use muxado. It would be nice to have a proxy adaptor/server. 75 | 76 | -------------------------------------------------------------------------------- /echoserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "github.com/inconshreveable/muxado" 9 | ) 10 | 11 | func main() { 12 | flag.Parse() 13 | host := flag.Arg(0) 14 | port := flag.Arg(1) 15 | if host == "" || port == "" { 16 | panic("usage: ./echoserver [relay host] [relay port]") 17 | } 18 | 19 | // connect to the relay 20 | sess, err := muxado.Dial("tcp", host+":"+port) 21 | if err != nil { 22 | panic(err) 23 | } 24 | defer (func() { 25 | sess.Close() 26 | fmt.Println("Disconnected...") 27 | })() 28 | 29 | fmt.Println("Connected. Waiting for handshake...") 30 | 31 | // before we can run smoothly, we must handshake with the server 32 | // the server will send our new address down the first session 33 | // that the client opens 34 | stream, err := sess.Open() 35 | if err != nil { 36 | panic(err) 37 | } 38 | buf, err := ioutil.ReadAll(stream) // the server sends the address and nothing else 39 | if err != nil { 40 | panic(err) 41 | } 42 | fmt.Println("Listening on", string(buf)) 43 | stream.Close() 44 | 45 | // use this goroutine to wait for and process clients 46 | for { 47 | stream, err := sess.Accept() 48 | if err != nil { 49 | fmt.Println("Couldn't accept client:", err) 50 | break 51 | } 52 | fmt.Println("Accepted client") 53 | 54 | go handleStream(stream) 55 | } 56 | } 57 | 58 | // annoyingly echoes to a client 59 | func handleStream(stream muxado.Stream) { 60 | defer (func() { 61 | stream.Close() 62 | fmt.Println("Closed connection to client") 63 | })() 64 | 65 | for { 66 | buf := make([]byte, 256) 67 | _, err := stream.Read(buf) 68 | if err != nil { 69 | fmt.Println("Error reading:", err.Error()) 70 | return 71 | } 72 | stream.Write(buf) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /relay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "os" 10 | "strconv" 11 | 12 | "github.com/inconshreveable/muxado" 13 | ) 14 | 15 | var info, warn *log.Logger 16 | 17 | func main() { 18 | var max_services int 19 | flag.IntVar(&max_services, "max", 50, "maximum number of services to forward on behalf of") 20 | flag.Parse() 21 | 22 | port, err := strconv.Atoi(flag.Arg(0)) 23 | if flag.NArg() != 1 || err != nil { 24 | fmt.Println("operand missing: port to listen on") 25 | os.Exit(2) 26 | } 27 | 28 | info = log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds) 29 | warn = log.New(os.Stdout, "WARN: ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) 30 | 31 | // open up the back-facing port for services to connect 32 | socket, err := muxado.Listen("tcp", ":"+strconv.Itoa(port)) 33 | if err != nil { 34 | fmt.Println("couldn't create muxado socket: ", err.Error()) 35 | return 36 | } 37 | defer socket.Close() 38 | info.Println("listening on port", port) 39 | 40 | // create our socket pool 41 | info.Println("serving for", max_services, "services") 42 | socket_pool := make(chan int, max_services) 43 | for i := 1; i <= max_services; i++ { 44 | socket_pool <- i + port 45 | } 46 | 47 | // handle new connections from services 48 | for { 49 | req, err := socket.Accept() 50 | if err != nil { 51 | warn.Println("error accepting service:", err.Error()) 52 | break 53 | } 54 | 55 | go handleSession(req, socket_pool) 56 | } 57 | 58 | log.Panic("aborting...") 59 | } 60 | 61 | func handleSession(back_conn muxado.Session, socket_pool chan int) { 62 | defer back_conn.Close() 63 | 64 | // begin handshaking 65 | // the first (and only) stream opened by the client is the handshaking stream 66 | stream, err := back_conn.Accept() 67 | if err != nil { 68 | warn.Println("can't accept handshake stream:", err) 69 | return 70 | } 71 | 72 | // pull a socket from the pool 73 | // the connection will stall here if the server is full 74 | port := <-socket_pool 75 | address := "localhost" + ":" + strconv.Itoa(port) 76 | 77 | // setup a new forward-facing port for clients to connect to the connecting server 78 | front_conn, err := net.Listen("tcp", address) 79 | if err != nil { 80 | warn.Println("can't open socket:", err.Error()) 81 | // don't put the socket back in the pool, maybe it's taken 82 | return 83 | } 84 | defer (func() { 85 | front_conn.Close() 86 | info.Println("closing port:", port) 87 | socket_pool <- port 88 | })() 89 | 90 | // send the service's forwarded address to the service 91 | byteArray := []byte(address) 92 | stream.Write(byteArray) 93 | stream.Close() 94 | 95 | info.Println("forwarding a service on:", address) 96 | 97 | // accept clients for the server 98 | for { 99 | client, err := front_conn.Accept() 100 | if err != nil { 101 | warn.Println("can't accept from:", address, ": ", err.Error()) 102 | continue 103 | } 104 | defer client.Close() 105 | 106 | info.Println("accepted client for:", address) 107 | 108 | // "finish" the connection with a muxado stream 109 | server, err := back_conn.Open() 110 | if err != nil { 111 | warn.Println("can't open multiplexed stream:", err) 112 | break 113 | } 114 | 115 | // whichever goroutine reads from a stream is expected to close it 116 | // this could probably be improved in the future 117 | 118 | // forward server data to client 119 | go (func(server muxado.Stream, client net.Conn) { 120 | defer server.Close() 121 | io.Copy(server, client) 122 | })(server, client) 123 | 124 | // forward client data to server 125 | go (func(server muxado.Stream, client net.Conn) { 126 | defer client.Close() 127 | io.Copy(client, server) 128 | })(server, client) 129 | } 130 | 131 | warn.Println("aborting service:", address) 132 | } 133 | -------------------------------------------------------------------------------- /stress_tester.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net" 7 | "time" 8 | ) 9 | 10 | var clients, packets int 11 | var host, port string 12 | 13 | func client(id int, ch chan bool) { 14 | log.Printf("Spawning client #%d", id) 15 | 16 | conn, err := net.Dial("tcp", host+":"+port) 17 | if err != nil { 18 | log.Fatal("error connecting to relay:", err) 19 | } 20 | defer conn.Close() 21 | 22 | for i := 0; i < packets; i++ { 23 | _, err = conn.Write([]byte("HEAD")) 24 | 25 | time.Sleep(time.Microsecond) 26 | } 27 | 28 | log.Println("Client", id, "shutting down") 29 | ch <- true 30 | } 31 | 32 | func main() { 33 | flag.IntVar(&clients, "clients", 5, "Number of clients to spawn") 34 | flag.IntVar(&packets, "packets", 100, "Number of packets to send per client") 35 | 36 | flag.Parse() 37 | host = flag.Arg(0) 38 | port = flag.Arg(1) 39 | if host == "" || port == "" { 40 | panic("usage: ./stress_tester [host] [port]") 41 | } 42 | 43 | log.Println("connecting to", host+":"+port) 44 | log.Println("using", clients, "clients and", packets, "per client") 45 | 46 | ch := make(chan bool) 47 | 48 | for i := 0; i < clients; i++ { 49 | go client(i, ch) 50 | 51 | time.Sleep(time.Second) 52 | } 53 | 54 | // wait for all clients to close 55 | for i := 0; i < clients; i++ { 56 | <-ch 57 | } 58 | } 59 | --------------------------------------------------------------------------------