Hello everyone! today I will be starting a series of articles trying to implement a bare-bones working ngrok. If you're not familiar with ngrok it's a cross-platform application that enables developers to expose a local development server to the Internet with minimal effort.
Its usage is very easy all you do is download the executable, run it specifying the port you want to expose and it returns a URL that you can access from the web.
./ngrok http 3000 # exposing localhost 3000
# repsonse
Session Status online
Account x (Plan: Free)
Update update available (version 3.3.1, Ctrl-U to update)
Version 3.1.1
Region Europe (eu)
Latency -
Web Interface http://127.0.0.1:4040
Forwarding https://de2a-154-180-33-154.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
If anyone in the world uses this URL https://de2a-154-180-33-154.ngrok-free.app
, they can access the web server running on our localhost port 3000.
Every Part will have its series of steps. In this part, we'll mainly focus on creating a CLI using Rust that I can pass a port to.
To start a new Rust project cargo new ngrnotok
(yes I named it ngrnotok ๐
)
We'll need to add a few packages in our Cargo.toml
file
[dependencies]
bytes = "1.4.0"
clap = { version = "4.3.11", features = ["derive"] }
httparse = "1.8.0"
reqwest = "0.11.18"
tokio = { version = "1.29.1", features = ["full"] }
tokio-util = "0.7.8"
The clap
package is the CLI one that will help us create the CLI with commands.
Let's get started by creating a file in src/
called cli.rs
use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
#[command(author, version)]
#[command(about = "NGR IM NOT OK")]
pub struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Http(Config),
}
#[derive(Args)]
struct Config {
#[arg(short = 'p', long = "port")]
port: Option<String>,
}
impl Cli {
pub async fn run() {
let cli = Cli::parse();
match &cli.command {
Some(Commands::Http(config)) => {
match &config.port {
Some(port) => {
println!("port provided!")
}
None => {
println!("No port provided")
}
}
}
None => {
println!("No command provided")
}
}
}
}
We do the following in the code above:
Create a struct
Cli
which has a property calledcommand
wherecommand
is an enum of different commandsWe create a struct
commands
which only have one value for the time beinghttp
The
http
command takes arguments specified in a struct calledconfig
. It only takes in the port number of the server running on our local hostWe then implement
run
as a struct method which will executeCli::parse();
and block until a command is typed inOnce a command gets typed in if it's
http
we'll check if a port is present or not and just print it.
We need to add this module created to our main.rs
and instantiate a new Cli
instance.
#main.rs
mod cli;
use commands::Cli;
async fn main() {
Cli::run();
}
To get this running all you need to do is cargo build
which builds an executable binary. Then in target/debug
execute ./ngrnotok http -p 3000
and it should print the port number.
Next, we'll move on to explaining the flow. What are we exactly planning to do? how will the data flow from one end to another? Let's take a look at the image below
Let's break the steps down:
Start ngrnotok
./ngrnotok http -p 3000
. When starting a TCP connection stream is opened between the Rust client and the Server (later part of the series)The client will then return a unique URL where I can access the project
foo-123-44f.ngrnotok.app
Then I send this URL to my friend and he types it in his browser
His request gets routed to the Server that we'll be creating in Golang.
The Server checks for the unique identifier in the URL
foo-123-44f
and then forwards the request over the TCP stream created to the Rust client (the blue data store in the image above keeps a key value for each unique id and tcp connection).The Rust client receives the required request and forwards it to the local server.
The response is returned from the local server to the Rust client
Rust client returns the response to the Go server
The Server then will forward this Response back to the client.
A brilliant way of port-forwarding without actually needing to configure any port-forwarding done by ngrok!
This will be it for this article because I want anyone reading to have a general idea before diving deep.
In the Next part we'll cover the following:
Starting our Go server
Creating a TCP connection between the rust client and Golang server
Generating a URL using
uuid
for every Rust clientDownloading and installing
dnsmasq
which is a useful tool for development environment to redirect DNS names using wildcards *.ngrnotok.app