Expose local services behind NATs and firewalls to the internet over TLS encrypted TCP and/or UDP tunnels! mgrok uses a TLS-encrypted TCP connection for the tunnel itself, and then forwards either TCP or UDP traffic through that secure tunnel to your local services. mgrok is inspired by ngrok and frp.
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 ✅
- UDP tunnel (experimental) ✅
-
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).
To test UDP forwarding you can expose a local UDP echo server. First add a UDP
proxy to configs/client.yaml
:
proxies:
echo:
type: udp
local_port: 9001 # local UDP service
remote_port: 7000 # exposed on the server
Start a simple echo service on the client machine. Using netcat
, I could only
process a single datagram even with -k
, so using a small Python script is a
reliable way to keep the UDP service running (it will echo it back over tunnel):
python3 - <<'EOF'
import socket
s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("localhost", 9001))
while True:
d, a = s.recvfrom(4096)
s.sendto(d, a)
EOF
Run the mgrok server and client as shown above. Then send a datagram to the server's exposed port and you should see it echoed back:
echo "hello" | nc -u -w1 localhost 7000
This confirms that UDP packets are transported through the tunnel.
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