A Tour of Inlets - A Tunnel Built for the Cloud
Inlets is a lightweight tunneling tool. It’s not a SaaS product, it’s a self-hosted tunneling solution. That means you have total control over it. According to the documentation of Inlets PRO, there are various use cases:
- Exposing services from a private network
- Self-hosting HTTP endpoints with Let’s Encrypt integration (so you have HTTPS)
- Connecting local Kubernetes with public IP
- … etc.
inlets-pro is the main program where the magic happened. Basically it has two
modes: TCP and HTTP. Both the server side and the client side run with the same
program, but with different subcommands. Whatever you send over the tunnel
between the server and the client gets encrypted in transit through the
built-in TLS encryption. This is done automatically. A bonus feature is that you
do not have to expose your services on the Internet, so can use Inlets like a
VPN or SSH tunnel.
As mentioned above, there must be two endpoints in a tunnel: one end is Inlets PRO server, the other end is Inlets PRO client. The node where Inlets PRO server runs is called exit-server. It means that all the responses coming from the other end of the tunnel will take the exit here. The Inlets PRO server exposes its control-plane (websocket) to other Inlets PRO clients, so that the clients can connect and establish the tunnel. You can put Inlets PRO clients on your private server or even on your local computer, then expose private servers with public endpoints through the tunnel. Then you can share the public endpoints with customers to let them access your private services without a hassle.
You can download
inlets-pro binary from GitHub release
page directly, or you can try
$ curl -sLSf https://inletsctl.inlets.dev | sudo sh $ inletsctl download --pro $ inlets-pro version _ _ _ _ (_)_ __ | | ___| |_ ___ __| | _____ __ | | '_ \| |/ _ \ __/ __| / _` |/ _ \ \ / / | | | | | | __/ |_\__ \| (_| | __/\ V / |_|_| |_|_|\___|\__|___(_)__,_|\___| \_/ PRO edition Version: 0.8.9 - 7df6fc42cfc14dd56d93c32930262202967d234b
One of my use case is to establish a secured tunnel between my homelab server and my office Windows desktop, so I can access office environment through Windows RDP from my Mac. All I have to do is opening up Microsoft Remote Desktop app, and connect to my homelab server on port 3389.
Since I prefer commands which will block the terminal to be in the background,
inlets-pro tcp server a systemd service is the way to go. Be sure to
provide the public IP address to
--auto-tls-san argument, so the tunnel server
can use it to generate the certificate. The token here is to ensure that no one
but only who knows the token can connect to the tunnel server. It will be used
on both server side and client side.
export TOKEN=$(head -c 16 /dev/urandom | shasum | cut -d '-' -f 1) inlets-pro tcp server \ --auto-tls \ --auto-tls-san="22.214.171.124" \ --token="$TOKEN" \ --generate=systemd | sudo tee -a /etc/systemd/system/inlets-pro.service sudo systemctl enable inlets-pro.service sudo systemctl start inlets-pro.service
In order to make the tunnel client outside of my homelab be able to connect to
the tunnel server on control-plane (port 8123 for example), I have to setup a
port-forwarding rule on my router/firewall. So that anyone connect to
<public-ip>:8123 will reach
On the client side, run
inlets-pro.exe tcp client along with a valid license
key, the aforementioned token, the URL of the tunnel server, and the ports that
you want to expose, i.e. port 3389 for Windows RDP service.
C:\Program Files\Inlets PRO>inlets-pro.exe tcp client --license="INLETS-PRO-LICENSE-KEY" --url="wss://126.96.36.199:8123" --token="fe6f868d72123701326e31d3179a07208cc5d80d" --ports=3389 2021/08/09 10:51:30 Starting TCP client. Version 0.8.8 - 57580545a321dc7549a26e8008999e12cb7161de 2021/08/09 10:51:31 Licensed to: <redacted> (Gumroad subscription) 2021/08/09 10:51:31 Upstream server: localhost, for ports: 3389 inlets-pro client. Copyright OpenFaaS Ltd 2021 time="2021/08/09 10:51:31" level=info msg="Connecting to proxy" url="wss://188.8.131.52:8123/connect" time="2021/08/09 10:51:31" level=info msg="Connection established.. OK."
Now that the tunnel has been established. The tunnel server listens on port 3389
for any incoming RDP connection. I can connect to
<private-ip>:3389 for my
office Windows desktop in home without exposing it to the entire world.
Here I will use “local file sharing via HTTP server” as an example.
- Local computer is the place (my M1 Mac mini) where the files we wanna share, resides in a private network
- Exit server is a machine with publicly accessible IP address, usually resides on the cloud
On the exit-server, start an Inlets PRO HTTP server. It will listens on port
8123 and 8000 by default as control port and data port respectively. One can
change the ports by providing
--port if there are port
$ inlets-pro http server \ --auto-tls \ --auto-tls-san="184.108.40.206" \ --token="fe6f868d72123701326e31d3179a07208cc5d80d" 2021/08/17 16:08:45 Starting HTTP client. Version 0.8.9 - 7df6fc42cfc14dd56d93c32930262202967d234b 2021/08/17 16:08:45 Wrote: /tmp/certs/ca.crt 2021/08/17 16:08:45 Wrote: /tmp/certs/ca.key 2021/08/17 16:08:45 Wrote: /tmp/certs/server.crt 2021/08/17 16:08:45 Wrote: /tmp/certs/server.key 2021/08/17 16:08:45 TLS: 220.127.116.11, expires in: 2491.999991 days 2021/08/17 16:08:45 Data Plane Listening on 0.0.0.0:8000 2021/08/17 16:08:45 Control Plane Listening with TLS on 0.0.0.0:8123
On my local computer, setup an Inlets PRO HTTP fileserver (something similar
python3 -m http.server but with more features such as directory browsing
toggle, basic authentication, etc.).
$ inlets-pro http fileserver \ --webroot="$HOME/Projects" \ --allow-browsing \ --token="supersecret" Starting inlets PRO fileserver. Version: 0.8.9-18-gf4fc15b - f4fc15b9604efd0b0ca3cc604c19c200ae6a1d7b 2021/08/17 16:11:13 Serving: /Users/starbops/Projects, on 127.0.0.1:8080, browsing: true, auth: true
Since the Inlets PRO HTTP fileserver binds to local interface, I’ll start the
Inlets PRO HTTP client on the same local computer and specify
as upstream. So the client will establish the tunnel to the exit-server,
and forward related HTTP requests back to the fileserver running on my local
$ inlets-pro http client \ --url="wss://18.104.22.168:8123" \ --token="fe6f868d72123701326e31d3179a07208cc5d80d" \ --upstream="localhost:8080" Starting HTTP client. Version: 0.8.9-18-gf4fc15b - f4fc15b9604efd0b0ca3cc604c19c200ae6a1d7b 2021/08/17 16:10:59 Licensed to: <redacted> (Gumroad subscription) 2021/08/17 16:10:59 Upstream: => http://localhost:8080 INFO[2021/08/17 16:10:59] Connecting to proxy url="wss://22.214.171.124:8123/connect" INFO[2021/08/17 16:10:59] Connection established client_id=5482d06cafa4404786d92eb44a916903
By the way, thanks to @alex, I have the opportunity to test on the RC version of the Darwin ARM64 build. It works on my M1 Mac mini perfectly.
$ inlets-pro version _ _ _ _ (_)_ __ | | ___| |_ ___ __| | _____ __ | | '_ \| |/ _ \ __/ __| / _` |/ _ \ \ / / | | | | | | __/ |_\__ \| (_| | __/\ V / |_|_| |_|_|\___|\__|___(_)__,_|\___| \_/ PRO edition Version: 0.8.9-18-gf4fc15b - f4fc15b9604efd0b0ca3cc604c19c200ae6a1d7b
And we are all set. Open a browser then navigate to the address of the
exit-server with the designated port (data port specified by
inlets-pro http server command). A familiar authentication dialog should pop
up. Sign in with the default username (which is
admin) and the specified
password, the directory list will show up.
As mentioned above, you can download the
inlets-pro binary through
inletsctl download --pro
inlets-pro is dedicated to tunnel establishing,
inletsctl provides a
bunch of additional features about cloud service provider integrations. With
inletsctl, one can easily provision an inlets-pro-ready cloud instance
(usually VM) as exit-server like a breeze. For example:
$ inletsctl create --provider digitalocean \ --region sgp1 \ --access-token-file do-access-token Using provider: digitalocean Requesting host: epic-feynman8 in sgp1, from digitalocean 2021/08/09 11:41:59 Provisioning host with DigitalOcean Host: 258759207, status: [1/500] Host: 258759207, status: new [2/500] Host: 258759207, status: new [3/500] Host: 258759207, status: new [4/500] Host: 258759207, status: new [5/500] Host: 258759207, status: new [6/500] Host: 258759207, status: new [7/500] Host: 258759207, status: new [8/500] Host: 258759207, status: new [9/500] Host: 258759207, status: new [10/500] Host: 258759207, status: new [11/500] Host: 258759207, status: new [12/500] Host: 258759207, status: new [13/500] Host: 258759207, status: new [14/500] Host: 258759207, status: new [15/500] Host: 258759207, status: new [16/500] Host: 258759207, status: new [17/500] Host: 258759207, status: active inlets PRO TCP (0.8.6) server summary: IP: 126.96.36.199 Auth-token: EJW4btMsNaC5CKIl9cZ6qGP3baMztheIvW8GtU1zifXkkxuBr3EwtmVI7hM1bmsK Command: # Obtain a license at https://inlets.dev # Store it at $HOME/.inlets/LICENSE or use --help for more options export LICENSE="$HOME/.inlets/LICENSE" # Give a single value or comma-separated export PORTS="8000" # Where to route traffic from the inlets server export UPSTREAM="localhost" inlets-pro tcp client --url "wss://188.8.131.52:8123" \ --token "EJW4btMsNaC5CKIl9cZ6qGP3baMztheIvW8GtU1zifXkkxuBr3EwtmVI7hM1bmsK" \ --upstream $UPSTREAM \ --ports $PORTS To delete: inletsctl delete --provider digitalocean --id "258759207"
The IP address and the token is shown in the output after finishing the
deployment of cloud instance. Use the provided information in
$ export UPSTREAM="nuclear.internal.zespre.com" $ export PORTS="3000" $ inlets-pro tcp client \ --url="wss://184.108.40.206:8123" \ --token="EJW4btMsNaC5CKIl9cZ6qGP3baMztheIvW8GtU1zifXkkxuBr3EwtmVI7hM1bmsK" \ --upstream="$UPSTREAM" \ --ports=$PORTS 2021/08/09 12:00:00 Starting TCP client. Version 0.8.8 - 57580545a321dc7549a26e8008999e12cb7161de 2021/08/09 12:00:00 Licensed to: <redacted> (Gumroad subscription) 2021/08/09 12:00:00 Upstream server: nuclear.internal.zespre.com, for ports: 3000 inlets-pro client. Copyright OpenFaaS Ltd 2021 INFO[2021/08/09 12:00:01] Connecting to proxy url="wss://220.127.116.11:8123/connect" INFO[2021/08/09 12:00:01] Connection established.. OK.
Just remember to put a valid Inlets PRO license under
you don’t need to specify the license in the command line.
Delete the cloud instance if the tunnel is no longer needed, otherwise it’ll cost your money:
inletsctl delete --provider digitalocean \ --access-token-file do-access-token \ --ip 18.104.22.168
IMO Inlets-operator is the most brilliant part over all these cloud native tunneling use cases. As an operator of Inlets PRO in Kubernetes, it monitors on Service resources (especially on LoadBalancer type of Service) and manages Tunnel resource and exit servers. This is perfect for a private/local deployment of Kubernetes cluster. Think about exposing your application running on a Raspberry Pi powered K8s in your homelab, it could be a real pain in the ass. However, with Inlets-operator, things will be different! For more details, check out Ivan Velichko’s article about Kubernetes operator pattern and Inlets-operator.
Install CRDs and Helm charts of Inlets-operator. Here I choose DigitalOcean as my cloud service provider, you can choose whatever suits you best.
git clone https://github.com/inlets/inlets-operator.git cd inlets-operator/ helm repo add inlets https://inlets.github.io/inlets-operator/ helm repo update kubectl apply -f ./artifacts/crds/ kubectl create ns inlets-operator helm upgrade inlets-operator --install inlets/inlets-operator \ --set provider=digitalocean \ --set region=sgp1 \ --set inletsProLicense="$(cat $HOME/.inlets/LICENSE)" \ --set annotatedOnly=true \ --namespace inlets-operator
If there is any problem, check the logs generated in Inlets-operator Deployment:
kubectl -n inlets-operator logs deploy/inlets-operator -f
Deploying Example Application
Now the Inlets-operator is ready, let’s deploy an example application to see how it works. Spin up a Deployment with Nginx web server:
$ cat <<EOF | kubectl apply -f - > apiVersion: apps/v1 > kind: Deployment > metadata: > name: nginx-1 > labels: > app: nginx > spec: > replicas: 1 > selector: > matchLabels: > app: nginx > template: > metadata: > labels: > app: nginx > spec: > containers: > - name: nginx > image: nginx:1.14.2 > ports: > - containerPort: 80 > EOF deployment.apps/nginx-1 created
Exposing Private Service
With Nginx running in a Pod, we still need to expose the port on which Nginx listens, so that users can access it. Here we use LoadBalancer type of Service which means Inlets-operator will provision a cloud instance with a public IP address as a load-balancer, then establish a secured tunnel between the cloud instance and the auxiliary Pod running alongside with Nginx Pod on the local K8s for us.
$ cat <<EOF | kubectl apply -f - > apiVersion: v1 > kind: Service > metadata: > name: nginx-1 > annotations: > metallb.universe.tf/address-pool: "dummy" > dev.inlets.manage: "true" > spec: > type: LoadBalancer > selector: > app: nginx > ports: > - name: http > protocol: TCP > port: 80 > targetPort: 80 > EOF service/nginx-1 created
It’s worth mentioning that since in my deployment of K8s, there is
MetalLB running as a bare-metal load-balancer,
which allocates IP addresses from a private network segment (192.168.xx.0/24)
to LoadBalancer Services. So I have to make MetalLB and Inlets-operator know
which LoadBalancer type of Service they should take care of. The solution by far
is to install Inlets-operator with
annotatedOnly=true and add two annotations
in every LoadBalancer type of Service:
metallb.universe.tf/address-pool: "dummy": “dummy” can be replaced with any other terms which isn’t a valid address pool name in your MetalLB setup. This will make MetalLB ignore the request of load-balancer IP address.
dev.inlets.manage: "true": Inlets-operator will only take action on Services with this annotated
Accessing from Outside
After the Service created, we can see that Inlets-operator is provisioning the tunnel for us. And the external IP address is still in the pending status because the cloud instance is spawning.
$ kubectl get svc,tunnel NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/nginx-1 LoadBalancer 10.97.233.179 <pending> 80:30555/TCP 6s NAME SERVICE TUNNEL HOSTSTATUS HOSTIP HOSTID tunnel.inlets.inlets.dev/nginx-1-tunnel nginx-1 provisioning 261289194
We can use
-w option to watch the progress. Soon there is a public IP address
$ kubectl get svc -w NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx-1 LoadBalancer 10.97.233.179 <pending> 80:30555/TCP 28s nginx-1 LoadBalancer 10.97.233.179 22.214.171.124 80:30555/TCP 72s nginx-1 LoadBalancer 10.97.233.179 126.96.36.199,188.8.131.52 80:30555/TCP 72s nginx-1 LoadBalancer 10.97.233.179 184.108.40.206 80:30555/TCP 72s
If you check out the Deployment and Pod lists you’ll notice that there is a new Pod created by Inlets-operator automatically. Actually it is the auxiliary Pod which runs Inlets PRO client! All the tedious works such as generating token, figuring out where the tunnel server and upstream are, are handled by Inlets-operator. It just works.
$ kubectl get deploy,po NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/nginx-1 1/1 1 1 5m35s deployment.apps/nginx-1-tunnel-client 1/1 1 1 2m26s NAME READY STATUS RESTARTS AGE pod/nginx-1-66b6c48dd5-dksx9 1/1 Running 0 5m35s pod/nginx-1-tunnel-client-d9cd96c74-ks7vg 1/1 Running 3 2m25s
Now you can access the private Nginx with the above listed public IP address from anywhere (with Internet connection)!
Inlets PRO is a swiss army knife. There are various use cases listed in the documents. It can replace SSH tunneling with so much ease. Users can expose their private services efficiently like never before. It is also possible bringing remote services to local using Inlets PRO. Unlike SaaS tunneling solutions like Ngrok, you have total control over your infrastructure without traffic throttling. And the data flow through the tunnel is secured out of the box. If you have a local K8s deployment, definitely give it a try!