Developer-focused toolkit that extends Microsoft Dev Tunnels with local network access helpers.
It packages three services:
| Service | Image | Purpose |
|---|---|---|
| DevTunnel | devtunnel-toolkit |
Hosts or connects Microsoft Dev Tunnels |
| Squid | devtunnel-toolkit-squid |
HTTP/HTTPS proxy for local network access |
| OpenVPN | devtunnel-toolkit-openvpn |
Privileged TCP VPN server for routed local network access |
Dev Tunnels is great for exposing a local development service. This toolkit adds two common development workflows:
- Use a proxy through the tunnel to reach internal HTTP/HTTPS/SSH-over-CONNECT endpoints.
- Use a VPN through the tunnel to route development traffic to the network where the tunnel host is running.
The default proxy port is 3140, matching the Squid configuration used by the
rhel_squid_proxy_install Ansible role.
Login once:
make loginOr login with GitHub:
make login-githubConfirm the container volume has a cached login:
make statusStart the toolkit attached to the logs:
make upWhen running attached, stopping the command with Ctrl+C also stops the Compose services. To start in the background, use:
make up-dBy default, the tunnel hosts:
| Port | Service |
|---|---|
3140 |
Squid proxy |
53194 |
OpenVPN TCP server |
Use a persistent tunnel ID when desired:
TUNNEL_ID=my-dev-gateway make upAllow anonymous access only when you understand the exposure:
ALLOW_ANONYMOUS=true TUNNEL_ID=my-dev-gateway make upLogin once with Microsoft/Entra ID:
docker compose run --rm devtunnel login microsoftOr login with GitHub:
docker compose run --rm devtunnel login githubConfirm the login before starting the long-running services:
docker compose run --rm devtunnel auth-checkStart everything in the background:
docker compose up -dOr with Make:
make up-dThis starts:
| Service | Local endpoint |
|---|---|
| Squid proxy | 127.0.0.1:3140 |
| OpenVPN server | 127.0.0.1:53194/tcp |
| DevTunnel host | ports 3140,53194 |
Follow the tunnel logs:
docker compose logs -f devtunnelUse a persistent tunnel ID:
TUNNEL_ID=my-dev-gateway docker compose up -dExpose only the proxy:
PORTS=3140 docker compose up -d squid devtunnelExpose only the VPN:
PORTS=53194 docker compose up -d openvpn devtunnelGenerate an OpenVPN client profile:
docker compose run --rm openvpn client > devtunnel-toolkit.ovpnStop the toolkit:
docker compose downDelete containers, networks, and toolkit volumes:
docker compose down --volumes --remove-orphansThis removes the cached DevTunnel login and OpenVPN PKI/client certificates.
After this command, run login again and regenerate any .ovpn files.
Equivalent Make targets:
make reset
make recreatemake reset removes containers, networks, and volumes. make recreate removes
and recreates containers while keeping volumes, so the cached DevTunnel login
and OpenVPN certificates are preserved. For a full wipe, run make reset, then
login again and start with make up.
Dev Tunnels requires login to create and host tunnels. The CLI supports Microsoft/Entra ID and GitHub accounts; after login, Microsoft documents that the token is cached in the system secure key chain for several days in the Dev tunnels CLI reference.
This project mounts /home/devtunnel as a named Docker volume so normal
docker compose stop, docker compose restart, docker compose down, and
make up runs can reuse the cached login. Commands that remove volumes, such as
make reset or docker compose down --volumes, remove that cache.
make up runs an auth check before starting the tunnel host. If it says
Not logged in, run:
make login-github
make status
make updocker compose up also starts devtunnel-renew, a lightweight sidecar that
shares the DevTunnel home volume. For GitHub logins, it reads the cached access
token expiration and runs a DevTunnel management check shortly after expiration
so the CLI can refresh and rotate the cached GitHub token pair before an idle
host has to reconnect with old credentials. It does not make credentials
permanent; if GitHub or Microsoft revoke a token, or if the refresh token
expires, run the login flow again.
Run one renew probe manually with:
make renewFor unattended runs, provide an access token instead of a cached interactive login:
TUNNEL_ID=my-dev-gateway \
DEVTUNNEL_ACCESS_TOKEN=... \
docker compose up -dThe token is passed to devtunnel host/connect --access-token. Treat it as a
secret; environment variables can be visible through Docker inspection and shell
history.
On the client machine, run the DevTunnel client:
docker run --rm -it \
-v devtunnel-home:/home/devtunnel \
ghcr.io/matheuskshn/devtunnel-toolkit:edge login microsoft
docker run --rm -it \
-v devtunnel-home:/home/devtunnel \
ghcr.io/matheuskshn/devtunnel-toolkit:edge connect my-dev-gatewayAfter connect, the tunnel ports are available on the client as local ports.
Configure a terminal or tool to use:
export http_proxy=http://127.0.0.1:3140
export https_proxy=http://127.0.0.1:3140
export no_proxy=localhost,127.0.0.1Then use internal HTTP/HTTPS endpoints normally.
The Squid defaults are based on the Ansible role:
| Setting | Default |
|---|---|
| Listen address | 127.0.0.1 |
| HTTP port | 3140 |
| SSL ports | 443 563 22 |
| Safe ports | 80 21 22 443 70 210 1025-65535 280 488 591 777 |
Generate a client profile on the server side:
make ovpn-client > devtunnel-toolkit.ovpnThe generated profile defaults to:
remote 127.0.0.1 53194
proto tcp-client
That means the OpenVPN client connects to the local forwarded port created by
devtunnel connect.
| Variable | Default | Description |
|---|---|---|
PORTS |
3140,53194 |
Comma-separated ports hosted by Dev Tunnels |
TUNNEL_ID |
empty | Existing tunnel ID to host or connect |
DEVTUNNEL_ACCESS_TOKEN |
empty | Optional token passed as --access-token to host and connect |
ALLOW_ANONYMOUS |
false |
Adds --allow-anonymous when true |
PROTOCOL |
empty | Optional http, https, or auto |
EXPIRATION |
empty | Optional tunnel expiration, such as 2h or 7d |
VERBOSE |
false |
Adds --verbose when true |
LOGIN_PROVIDER |
microsoft |
Provider used by bare login |
DEVTUNNEL_RENEW_AFTER_EXPIRATION_SECONDS |
60 |
Seconds after cached GitHub access-token expiration before devtunnel-renew probes the tunnel |
DEVTUNNEL_RENEW_FALLBACK_INTERVAL_SECONDS |
29700 |
Fallback renew interval when GitHub token expiration cannot be read |
DEVTUNNEL_RENEW_RETRY_SECONDS |
300 |
Retry delay after a failed renew probe |
DEVTUNNEL_RENEW_MIN_SLEEP_SECONDS |
30 |
Minimum sleep when the next renew time is very close |
DEVTUNNEL_DNS_PRIMARY |
1.1.1.1 |
First DNS server used by the DevTunnel container |
DEVTUNNEL_DNS_SECONDARY |
8.8.8.8 |
Second DNS server used by the DevTunnel container |
DEVTUNNEL_DNS_FALLBACK |
127.0.0.1 |
Fallback to the host resolver when external DNS is unavailable |
The DevTunnel container uses external DNS by default because some local resolvers do not resolve Microsoft Dev Tunnels service domains. To disable the external DNS override and use only the system resolver, set:
COMPOSE_FILE=compose.yml:compose.system-dns.ymlThe host CA bundle is mounted read-only into the DevTunnel, Squid, and OpenVPN containers so internal TLS endpoints can use locally trusted corporate CAs.
| Variable | Default | Description |
|---|---|---|
LOCAL_CA_BUNDLE |
/etc/ssl/certs/ca-certificates.crt |
Host CA bundle path; common on Debian/Ubuntu hosts |
CONTAINER_CA_BUNDLE |
/etc/ssl/certs/ca-certificates.crt |
CA bundle path inside the Debian-based toolkit images |
On RHEL, Rocky, Fedora, and similar hosts, set:
LOCAL_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt| Variable | Default | Description |
|---|---|---|
SQUID_HTTP_PORT |
3140 |
Squid listen port inside the container |
SQUID_LISTEN_ADDRESS |
127.0.0.1 |
Squid listen address inside the container |
SQUID_VISIBLE_HOSTNAME |
devtunnel-toolkit-squid |
Squid visible hostname |
SQUID_SSL_PORTS |
443 563 22 |
Ports allowed for CONNECT |
SQUID_SAFE_PORTS |
80 21 22 443 70 210 1025-65535 280 488 591 777 |
Safe destination ports |
SQUID_EXTRA_CONFIG |
empty | Extra raw Squid configuration lines |
| Variable | Default | Description |
|---|---|---|
OVPN_PORT |
53194 |
OpenVPN listen port inside the container |
OVPN_LISTEN_ADDRESS |
127.0.0.1 |
OpenVPN listen address |
OVPN_PROTO |
tcp |
tcp or udp; TCP is recommended for Dev Tunnels |
OVPN_NETWORK |
10.8.0.0 |
VPN subnet network |
OVPN_NETMASK |
255.255.255.0 |
VPN subnet mask |
OVPN_CIDR |
10.8.0.0/24 |
VPN subnet CIDR used for NAT |
OVPN_CLIENT_NAME |
devtunnel-toolkit |
Default client certificate/profile name |
OVPN_REMOTE_HOST |
127.0.0.1 |
Remote host written to generated client profiles |
OVPN_REMOTE_PORT |
53194 |
Remote port written to generated client profiles |
OVPN_PUSH_ROUTES |
RFC1918 routes | Comma-separated routes pushed to clients |
OVPN_DNS |
empty | Comma-separated DNS servers pushed to clients |
OVPN_REDIRECT_GATEWAY |
false |
Push default route when true |
OVPN_EXTRA_CONFIG |
empty | Extra raw OpenVPN server configuration |
docker pull ghcr.io/matheuskshn/devtunnel-toolkit:edge
docker pull ghcr.io/matheuskshn/devtunnel-toolkit-squid:edge
docker pull ghcr.io/matheuskshn/devtunnel-toolkit-openvpn:edgeDocker Hub publishing uses the same image names under matheuskshn/ when the
repository secrets are configured.
Images follow Git tags.
| Git event | Published tags |
|---|---|
| Pull request | Build only, no push |
Push to main |
edge, main |
Tag v1.2.3 |
v1.2.3, 1.2.3, 1.2, 1, latest |
Release a new version:
git tag v1.0.0
git push origin v1.0.0- Squid and OpenVPN are bound to
127.0.0.1on the host; Dev Tunnels exposes those local ports. ALLOW_ANONYMOUS=truecan expose your proxy/VPN to anyone with the tunnel URL or connection details.- OpenVPN runs as a privileged container because it needs
/dev/net/tun, IP forwarding, and NAT rules. - Treat generated
.ovpnfiles, tunnel URLs, and access tokens as secrets. - Review pushed VPN routes before starting the service in sensitive networks.
make build
make help
docker compose config- Microsoft Dev Tunnels quickstart: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started
- Microsoft Dev Tunnels CLI reference: https://learn.microsoft.com/azure/developer/dev-tunnels/cli-commands
- Squid project: https://www.squid-cache.org/
- OpenVPN project: https://openvpn.net/