├── .gitignore ├── go.mod ├── proto └── tunnel.proto ├── Makefile ├── client ├── main.go └── tunnel.go ├── server ├── main.go ├── handler.go └── tunnel.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *.exe 3 | *.pb.go -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/0jk6/tunnel 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | google.golang.org/grpc v1.69.2 7 | google.golang.org/protobuf v1.36.1 8 | ) 9 | 10 | require ( 11 | golang.org/x/net v0.30.0 // indirect 12 | golang.org/x/sys v0.26.0 // indirect 13 | golang.org/x/text v0.19.0 // indirect 14 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /proto/tunnel.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tunnel; 4 | 5 | option go_package = "github.com/0jk6/tunnel/proto"; 6 | 7 | message TunnelMessage { 8 | string subdomain = 1; 9 | string method = 2; 10 | string path = 3; 11 | bytes body = 4; 12 | map headers = 5; 13 | } 14 | 15 | service TunnelService { 16 | rpc Connect(stream TunnelMessage) returns (stream TunnelMessage); 17 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build proto 2 | 3 | proto: 4 | protoc -Iproto --go_out=. --go_opt=module=github.com/0jk6/tunnel --go-grpc_out=. --go-grpc_opt=module=github.com/0jk6/tunnel proto/tunnel.proto 5 | 6 | build: 7 | go build -o bin/server server/*.go 8 | go build -o bin/client client/*.go 9 | 10 | run-server: 11 | go build -o bin/server server/*.go 12 | ./bin/server 13 | 14 | run-client: 15 | go build -o bin/client client/*.go 16 | ./bin/client -------------------------------------------------------------------------------- /client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/credentials/insecure" 9 | 10 | pb "github.com/0jk6/tunnel/proto" 11 | ) 12 | 13 | func main() { 14 | 15 | if len(os.Args) < 3 { 16 | log.Fatalf("Usage: %s ", os.Args[0]) 17 | } 18 | 19 | port := os.Args[1] 20 | subdomain := os.Args[2] 21 | 22 | conn, err := grpc.NewClient("localhost:12000", grpc.WithTransportCredentials(insecure.NewCredentials())) 23 | 24 | if err != nil { 25 | log.Fatalf("Failed to connect: %v", err) 26 | } 27 | defer conn.Close() 28 | 29 | client := pb.NewTunnelServiceClient(conn) 30 | 31 | handleStream(client, port, subdomain) 32 | } 33 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "sync" 7 | 8 | pb "github.com/0jk6/tunnel/proto" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | var addr string = "0.0.0.0:12000" 13 | 14 | type Server struct { 15 | pb.TunnelServiceServer 16 | streams map[string]pb.TunnelService_ConnectServer 17 | mu sync.Mutex 18 | } 19 | 20 | func main() { 21 | lis, err := net.Listen("tcp", addr) 22 | 23 | if err != nil { 24 | log.Printf("Failed to listen: %v", err) 25 | } 26 | 27 | s := grpc.NewServer() 28 | server := Server{streams: make(map[string]pb.TunnelService_ConnectServer)} 29 | pb.RegisterTunnelServiceServer(s, &server) 30 | 31 | //start the http server 32 | // log.Printf("HTTP server listening at 0.0.0.0:8080") 33 | // http.HandleFunc("/", server.ClientHandler) 34 | // go http.ListenAndServe(":8080", nil) 35 | go startTCPServer(&server) 36 | 37 | //start the grpc server 38 | log.Printf("gRPC server listening at %s", addr) 39 | if err := s.Serve(lis); err != nil { 40 | log.Printf("Failed to serve: %v", err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tunnel 2 | 3 | An attempt to build a clone of Ngrok. While it may be a bit buggy, it works and demonstrates the basic functionality of tunneling HTTP requests. 4 | 5 | --- 6 | 7 | ## Features 8 | - **gRPC Bi-Directional Streams**: Enables seamless communication between the client and server. 9 | - **Subdomain Mapping**: Access your HTTP server on a custom subdomain like `subdomain.localhost:8080`. 10 | - **Work in Progress**: The code is a mess. 11 | 12 | --- 13 | 14 | ### Prerequisites 15 | Ensure you have the following installed: 16 | - [Go](https://golang.org/) 17 | - [Make](https://www.gnu.org/software/make/) 18 | - [Protocol Buffers Compiler (`protoc`)](https://grpc.io/docs/protoc-installation/) 19 | 20 | ### Building 21 | 1. Generate gRPC structures: 22 | ```bash 23 | make proto 24 | ``` 25 | 2. Install dependencies 26 | ```bash 27 | go mod tidy 28 | ``` 29 | 3. Build the binaries: 30 | ```bash 31 | make build 32 | ``` 33 | 34 | ### Running 35 | 1. Start the **server**: 36 | ```bash 37 | ./bin/server 38 | ``` 39 | 2. Start the **client** with the desired port and subdomain: 40 | ```bash 41 | ./bin/client 42 | ``` 43 | 44 | Example: 45 | ```bash 46 | ./bin/client 3000 mysubdomain 47 | ``` 48 | 49 | 3. Access your HTTP server on: 50 | ``` 51 | mysubdomain.localhost:8080 52 | ``` 53 | 54 | --- 55 | 56 | ## How It Works 57 | - The **client** and **server** communicate using gRPC Bi-Directional streams, facilitating real-time tunneling of requests. 58 | 59 | --- -------------------------------------------------------------------------------- /server/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | pb "github.com/0jk6/tunnel/proto" 10 | ) 11 | 12 | func (s *Server) ClientHandler(w http.ResponseWriter, r *http.Request) { 13 | subdomain := strings.Split(r.Host, ".")[0] 14 | 15 | s.mu.Lock() 16 | stream, ok := s.streams[subdomain] 17 | s.mu.Unlock() 18 | 19 | if !ok { 20 | http.Error(w, "stream not found", http.StatusNotFound) 21 | return 22 | } 23 | 24 | var body []byte 25 | if r.Body != nil { 26 | defer r.Body.Close() 27 | var err error 28 | body, err = io.ReadAll(r.Body) 29 | if err != nil { 30 | http.Error(w, "failed to read request body", http.StatusInternalServerError) 31 | log.Printf("Error while reading body: %v", err) 32 | return 33 | } 34 | } 35 | 36 | // Copy headers from the incoming request 37 | headers := make(map[string]string) 38 | for key, values := range r.Header { 39 | if len(values) > 0 { 40 | headers[key] = values[0] 41 | } 42 | } 43 | 44 | //send the incoming data to the grpc client 45 | err := stream.Send(&pb.TunnelMessage{ 46 | Subdomain: subdomain, 47 | Body: body, 48 | Path: r.URL.Path, 49 | Headers: headers, 50 | Method: r.Method, 51 | }) 52 | 53 | if err != nil { 54 | http.Error(w, "failed to send data to gRPC client", http.StatusInternalServerError) 55 | log.Printf("Error while sending data to the gRPC client: %v", err) 56 | return 57 | } 58 | 59 | //receive the response back from the grpc client 60 | res, err := stream.Recv() 61 | if err != nil { 62 | if err == io.EOF { 63 | return 64 | } 65 | http.Error(w, "failed to receive data from gRPC client", http.StatusInternalServerError) 66 | log.Printf("Error while receiving data from the client: %v", err) 67 | delete(s.streams, subdomain) 68 | return 69 | } 70 | 71 | //send the received response back to the user over http 72 | if res != nil { 73 | // Set response headers based on the gRPC response 74 | for key, value := range res.Headers { 75 | w.Header().Set(key, value) 76 | } 77 | 78 | w.WriteHeader(http.StatusOK) 79 | _, _ = w.Write(res.Body) 80 | } else { 81 | http.Error(w, "empty response from gRPC client", http.StatusInternalServerError) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /client/tunnel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | 11 | pb "github.com/0jk6/tunnel/proto" 12 | ) 13 | 14 | //Todo: implement tcp requests as well 15 | 16 | func handleStream(client pb.TunnelServiceClient, port, subdomain string) { 17 | stream, err := client.Connect(context.Background()) 18 | if err != nil { 19 | log.Fatalf("Failed to create stream: %v", err) 20 | } 21 | 22 | //send the first request to register the client 23 | err = stream.Send(&pb.TunnelMessage{ 24 | Subdomain: subdomain, 25 | }) 26 | if err != nil { 27 | log.Fatalf("Failed to send: %v", err) 28 | } 29 | 30 | log.Println("Connected to the gRPC tunnel server") 31 | 32 | //channel to wait for the goroutine 33 | waitc := make(chan struct{}) 34 | 35 | //receive the response from the server, process it and send back the response 36 | go func() { 37 | for { 38 | res, err := stream.Recv() 39 | if err == io.EOF { 40 | break 41 | } 42 | if err != nil { 43 | log.Printf("Failed to receive: %v", err) 44 | break 45 | } 46 | 47 | // Create a new HTTP request 48 | url := fmt.Sprintf("http://localhost:%s%s", port, res.Path) 49 | log.Printf("%s %s", res.Method, res.Path) 50 | req, err := http.NewRequest(res.Method, url, bytes.NewReader(res.Body)) 51 | if err != nil { 52 | log.Printf("Failed to create request: %v", err) 53 | // break 54 | } 55 | 56 | // Set headers from the gRPC response 57 | for key, value := range res.Headers { 58 | req.Header.Set(key, value) 59 | } 60 | 61 | // Make the HTTP request 62 | resp, err := http.DefaultClient.Do(req) 63 | if err != nil { 64 | log.Printf("Failed to make request: %v", err) 65 | continue 66 | } 67 | 68 | body, err := io.ReadAll(resp.Body) 69 | if err != nil { 70 | log.Printf("Failed to read response body: %v", err) 71 | // break 72 | } 73 | resp.Body.Close() 74 | 75 | // log.Println(string(body)) 76 | 77 | // Based on the server's response, send back a response 78 | err = stream.Send(&pb.TunnelMessage{ 79 | Subdomain: subdomain, 80 | Body: body, 81 | }) 82 | 83 | if err != nil { 84 | log.Printf("Failed to send response to the gRPC server: %v", err) 85 | // break 86 | } 87 | 88 | } 89 | close(waitc) 90 | }() 91 | 92 | <-waitc 93 | 94 | //close the stream 95 | log.Println("Closing stream") 96 | 97 | err = stream.CloseSend() 98 | if err != nil { 99 | log.Fatalf("Failed to close stream: %v", err) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 2 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 3 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 4 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 5 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 6 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 7 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 8 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 12 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 13 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 14 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 15 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= 16 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= 17 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= 18 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 19 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 20 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 21 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 22 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 23 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 24 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 25 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 26 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 27 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= 28 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= 29 | google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= 30 | google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 31 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 32 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 33 | -------------------------------------------------------------------------------- /server/tunnel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "log" 7 | "net" 8 | "net/http" 9 | "strconv" 10 | 11 | pb "github.com/0jk6/tunnel/proto" 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | func (s *Server) Connect(stream grpc.BidiStreamingServer[pb.TunnelMessage, pb.TunnelMessage]) error { 16 | //receive the first request and store the stream in the map 17 | req, err := stream.Recv() 18 | if err != nil { 19 | return err 20 | } 21 | 22 | subdomain := req.Subdomain 23 | if subdomain == "" { 24 | return nil 25 | } 26 | 27 | //register the client 28 | s.mu.Lock() 29 | s.streams[subdomain] = stream 30 | s.mu.Unlock() 31 | 32 | log.Println("streams", s.streams) 33 | log.Println("-----------------") 34 | 35 | //wait until the client disconnects, all the bidirectional stream will happen in the handler.go file 36 | <-stream.Context().Done() 37 | 38 | //delete the stream from the map when the client disconnects 39 | s.mu.Lock() 40 | delete(s.streams, subdomain) 41 | s.mu.Unlock() 42 | 43 | return nil 44 | } 45 | 46 | //tcp server implementation 47 | 48 | func startTCPServer(server *Server) { 49 | listener, err := net.Listen("tcp", ":8080") 50 | if err != nil { 51 | log.Fatalf("Failed to start TCP server: %v", err) 52 | } 53 | 54 | defer listener.Close() 55 | 56 | log.Printf("TCP server listening at 0.0.0.0:8080") 57 | 58 | // Accept incoming connections 59 | for { 60 | conn, err := listener.Accept() 61 | if err != nil { 62 | log.Printf("Failed to accept connection: %v", err) 63 | continue 64 | } 65 | go handleTCPConnection(conn, server) 66 | } 67 | 68 | //handle connection 69 | } 70 | 71 | func handleTCPConnection(conn net.Conn, server *Server) { 72 | defer conn.Close() 73 | 74 | //determine if the request is an HTTP request or a raw TCP request 75 | reader := bufio.NewReader(conn) 76 | peek, err := reader.Peek(8) //peek the first 8 bytes to determine if it is an HTTP request 77 | if err != nil { 78 | log.Printf("Failed to peek: %v", err) 79 | return 80 | } 81 | 82 | if bytes.HasPrefix(peek, []byte("GET ")) || bytes.HasPrefix(peek, []byte("POST")) || bytes.HasPrefix(peek, []byte("PUT")) || bytes.HasPrefix(peek, []byte("DELETE")) || bytes.HasPrefix(peek, []byte("PATCH")) || bytes.HasPrefix(peek, []byte("OPTIONS")) { 83 | //HTTP request 84 | req, err := http.ReadRequest(reader) 85 | if err != nil { 86 | log.Printf("Failed to read request: %v", err) 87 | return 88 | } 89 | 90 | tcpResponseWriter := &TCPResponseWriter{conn: conn, header: http.Header{}} 91 | server.ClientHandler(tcpResponseWriter, req) 92 | } else { 93 | //raw TCP request, send this tcp data directly to client over grpc 94 | //todo: edit client to make tcp requests 95 | log.Println("Raw TCP request") 96 | } 97 | 98 | } 99 | 100 | // following struct should implement the http.ResponseWriter interface 101 | // and we can pass this to the http handler that we have in the handler.go file 102 | type TCPResponseWriter struct { 103 | conn net.Conn 104 | header http.Header 105 | status int 106 | } 107 | 108 | // WriteHeader sends an HTTP response header with the provided status code 109 | func (t *TCPResponseWriter) WriteHeader(statusCode int) { 110 | t.status = statusCode 111 | statusLine := "HTTP/1.1 " + strconv.Itoa(statusCode) + " " + http.StatusText(statusCode) + "\r\n" 112 | t.conn.Write([]byte(statusLine)) 113 | for key, values := range t.header { 114 | for _, value := range values { 115 | t.conn.Write([]byte(key + ": " + value + "\r\n")) 116 | } 117 | } 118 | t.conn.Write([]byte("\r\n")) 119 | } 120 | 121 | func (t *TCPResponseWriter) Write(b []byte) (int, error) { 122 | return t.conn.Write(b) 123 | } 124 | 125 | // Header returns the header map that will be sent by WriteHeader 126 | func (t *TCPResponseWriter) Header() http.Header { 127 | return t.header 128 | } 129 | --------------------------------------------------------------------------------