A tunneling application for exposing local servers behind NATs and firewalls to the internet. In the demo below, the mgrok server (top-left) and client (top-right) are running concurrently. A local web server on port 8080 (bottom-left) hosts a simple HTML page; then, from the bottom-right tab I 'curl' the mgrok server's public port 8000. The request is forwarded over the tunnel to the client, which fetches the page from localhost:8080 and sends it back through the server to 'curl'.
- Basic TCP tunnel ✅
- TCP tunnel with
smux
+ multiple TCP proxies ✅ - YAML config ✅
- TLS support ✅
- Simple Auth ✅
-
Install Go:
brew install go go version
You should see something like
go version go1.24.3 darwin/arm64
-
Set up GOPATH (add to your ~/.zshrc or ~/.bash_profile):
export GOPATH=$HOME/go export PATH=$PATH:$GOPATH/bin
-
Clone this repository and install dependencies:
git clone https://github.com/markCwatson/mgrok.git cd mgrok go mod tidy
Go stores all dependencies in a central cache, typically at:
$GOPATH/pkg/mod/
(usually ~/go/pkg/mod/
on macOS).
You can build the project using the provided script:
./scripts/build.sh
This will create the following architecture-specific binaries in the build
directory:
mgrok-server
: The server componentmgrok-client
: The client component
TLS must be setup and configured (the following instructions were tested on Mac).
-
Install mkcert - We'll use
mkcert
to handle the TLS certificate.brew install mkcert nss
-
Install a local Certificate Authority - Bootstrap a local CA and add it to your trust store.
mkcert -install
-
Generate a cert for localhost -Generate the
.pem
files inmgrok/certs/
then return to the root of this repo.cd certs mkcert localhost 127.0.0.1 ::1 cd -
-
Configure mgrk server - Update the
configs/server.yaml
file with your files/paths.enable_tls: true tls_cert_file: ~/repos/mgrok/certs/localhost+2.pem tls_key_file: ~/repos/mgrok/certs/localhost+2-key.pem bind_addr: 127.0.0.1 bind_port: 9000 auth_token: your-secret-token-here
-
Start a local service - For testing, run the web server (configured for https) in the
web/
directory.python web/server.py
-
Configure your proxies - The
configs/client.yaml
file defines which local services to expose. Configure it for the web proxy.server: localhost:9000 token: your-secret-token-here proxies: web: type: tcp local_port: 8080 remote_port: 8000
-
Start your server and client:
./build/mgrok-server ./build/mgrok-client
-
Verify proxy registration - The client will register all proxies defined in the config. You should see
Registered proxy web: tcp port 8080 -> 8000
-
Test the tunnel - Connect to the exposed port on your mgrok server using TLS.
curl https://localhost:8000
You should see the text html page returned. This test shows:
- A user connects to the exposed server port
- Server creates a data stream to client
- Client identifies which proxy was requested and connects to the corresponding local service
- Data is copied bidirectionally through the multiplexed tunnel
- TLS support
Note: you can disable TLS by setting enable_tls: false
in configs/server.yaml
(the client will fallback to TCP if the TLS handshake fails).
mgrok uses a simple token-based authentication to secure connections between the client and server:
-
Token Configuration:
- Server: Set the
auth_token
field inconfigs/server.yaml
- Client: Set the
token
field inconfigs/client.yaml
- Both tokens must match exactly for authentication to succeed
- Server: Set the
-
How it works:
- During handshake, the client sends its token to the server
- The server validates this token against its configured auth_token
- If they match, the connection is authenticated and allowed to proceed
- If they don't match, the server rejects the connection
-
Security:
- Keep your token values private and secure
- Use a strong, unique token (UUID or random string)
- The token is kept private and not logged in plaintext
Example tokens in config files:
# Server (configs/server.yaml)
auth_token: 0196e9bd-dab3-7d51-a89c-4fcc68e3a811
# Client (configs/client.yaml)
token: 0196e9bd-dab3-7d51-a89c-4fcc68e3a811
-
Public server: Listens on a well‑known TCP port (e.g. :9000) for control tunnels from clients. For every service the client wants to expose, it also opens a public listener (TCP or UDP) on demand and forwards traffic through the tunnel. Go primitives/libs:
net.Listen
,net.ListenPacket
; optional TLS (crypto/tls
). -
Client (behind NAT): Reads a config file; dials the server with TLS; authenticates; registers one or more proxies (
ssh
,web
,udp‑game
, …); keeps the control connection alive; for each incoming stream/packet from the server, opens/uses a local socket and pipes bytes both directions. Go primitives/libs:net.Dial
, goroutines,io.Copy
; YAML/INI parser. -
Multiplexing layer: Allows many logical streams over one physical TCP/TLS connection so you don't need 1 × TCP socket per proxied connection. Go primitives/libs:
smux
(GitHub) oryamux
(GitHub) (both production‑grade). -
Reliable‑UDP option (future): If you want "UDP but reliable, congestion‑controlled" (like frp's
kcp
mode) you can swap the physical link with kcp‑go. Go primitives/libs:kcp-go
(GitHub).
To read more, see this doc on tunneling in mgrok. Here is the summary from that doc:
- Control channel (TCP) carries JSON-framed control messages (NewProxy, StartWorkConn, UDPPacket, Ping, …) multiplexed via a yamux-style transporter.
- "NewProxy" handshake tells the server which proxy (TCP/UDP/etc.) to open and returns the remoteAddr to listen on.
- TCP proxy: the server listens on a TCP port and for each incoming connection grabs a workConn to the client; the client connects that workConn to the local service and shuttles bytes.
- UDP proxy: the server binds a UDP socket and sends/receives each datagram as a base64-encoded msg.UDPPacket over the workConn; on the client side the packet is unwrapped and forwarded to the local UDP service (and vice versa).
<Register> : msgType=0x01 | uint8 proxyType | uint16 remotePort | uint16 localPort | N bytes name
<NewStream> : msgType=0x02 | uint32 streamID
<Data> : msgType=0x03 | uint32 streamID | uint16 length | …bytes…
<Close> : msgType=0x04 | uint32 streamID
<Heartbeat> : msgType=0x05