OpenVPN sidecar for DataGate Monitor. In the monorepo this is the openvpn/ submodule (DataGateMonitor). Standalone clone: DataGateOpenVpnManager.
Links
| Resource | Link |
|---|---|
| datagateapp.com | |
| datagateapp.com/download | |
| @datagateapp |
This project builds a Docker image containing:
- A .NET 9 application: DataGateOpenVpnManager
- A fully functional OpenVPN server
- Integrated Easy-RSA certificate management
It uses a custom entrypoint to initialize PKI, generate default configuration, and run both OpenVPN and the .NET app in the same container.
git clone https://github.com/IMKolganov/DataGateOpenVpnManager.git
cd DataGateOpenVpnManagerdocker build --build-arg TARGETARCH=$(uname -m) -t datagate-openvpn .You must pass TARGETARCH. Common values:
x64oramd64arm64
Optional:
docker build --build-arg TARGETARCH=amd64 --build-arg BUILD_CONFIGURATION=Debug -t datagate-openvpn .docker run \
--cap-add=NET_ADMIN \
-e PORT=1194 \
-e PROTO=udp \
-e DATA_DIR=/mnt \
-v /path/to/data:/mnt \
-p 1194:1194/udp \
datagate-openvpnEnvironment variables:
| Variable | Description | Default |
|---|---|---|
PORT |
OpenVPN port | 1194 |
PROTO |
Protocol (udp or tcp) |
udp |
DATA_DIR |
Data directory for config/logs/pki | /mnt |
DNS1, DNS2 |
Pushed DNS servers | 8.8.8.8, 8.8.4.4 |
CIPHER |
Data-channel cipher; rewritten into issued .ovpn and /api/info |
AES-128-GCM if DCO, else AES-256-CBC |
DATA_CIPHERS |
OpenVPN data-ciphers list; rewritten into issued .ovpn / /api/info |
GCM/ChaCha list if DCO, else unset |
DCO |
Enable OpenVPN DCO (true/1/yes); exposed on /api/info |
false |
AUTH |
HMAC digest (auth); server.conf + issued .ovpn / /api/info |
SHA256 |
TLS_VERSION_MIN |
tls-version-min on server and issued .ovpn / /api/info |
1.2 |
CLIENT_VERB |
Suggested client .ovpn verb (not server log verb) / /api/info |
3 |
MSSFIX |
Optional push "mssfix N" to clients; exposed on /api/info (not duplicated in .ovpn) |
(unset) |
TUN_DEV |
Fixed tun device name (stable ufw/NAT) | _(unset β dev tun) |
TUN_IF |
Extra FORWARD by iface (optional) | (unset) |
WAN_IF |
WAN iface for MASQUERADE | eth0 (auto if missing) |
VPN_SUBNET, VPN_NETMASK |
VPN subnet config | 10.51.28.0/24 |
OpenVpnManagement__Port |
OpenVPN management interface port | 5092 |
OpenVpnProxy__ByteDebug |
Compare proxy vs management bytes (WSS debug) | false |
OpenVpnProxy__ByteDebugIntervalSeconds |
Periodic byte comparison while connected (0 = on disconnect only) |
0 |
OpenVpnProxy__CloseZombieAfterMissingSeconds |
Close WSS when OpenVPN peer missing from management (0 = off) |
0 |
OpenVpnProxy__ZombieCheckIntervalSeconds |
How often to check for zombie proxy sessions | 30 |
OpenVpnProxy__TlsLogEnrichmentEnabled |
Tag tls-crypt errors as external probe vs app client (for Wazuh) | true |
PROXY_BYTE_DEBUG |
Legacy alias for OpenVpnProxy__ByteDebug (1 / true) |
(unset) |
PiHole__Enabled |
Collect VPN DNS queries from Pi-hole API | false |
PiHole__BaseUrl |
Pi-hole FTL API base URL (same network namespace) | http://127.0.0.1:8080 |
PiHole__AppPassword |
Pi-hole application password for /api/auth |
(empty) |
PiHole__PollIntervalSeconds |
How often to poll Pi-hole query log | 60 |
PiHole__BatchSize |
Max queries per poll | 200 |
PIHOLE_ENABLED |
Legacy env alias for PiHole__Enabled |
(unset) |
PIHOLE_BASE_URL |
Legacy env alias for PiHole__BaseUrl |
(unset) |
PIHOLE_APP_PASSWORD |
Legacy env alias for PiHole__AppPassword |
(unset) |
PIHOLE_POLL_INTERVAL_SEC |
Legacy env alias for PiHole__PollIntervalSeconds |
(unset) |
Pi-hole config priority (highest wins): PIHOLE_* / PiHole__* env vars β dashboard Save & apply ($DATA_DIR/pihole-runtime-config.json) β appsettings defaults. Env overrides only the fields that are set.
Do not rely on tunN names in UFW β they change on every OpenVPN recreate. Use:
- Image β₯ 1.2.5.94 with
TUN_DEV=ovpn-udp(unique per stack) β entrypoint writesdev ovpn-udp/dev-type tun, deletes orphans that still hold this subnetβs.1, and installs FORWARD/NAT by CIDR. - Host UFW once per subnet (survives reboot and tun renames):
sudo chmod +x scripts/host-ufw-vpn-subnet.sh
# UDP pool β internet + DNS via Pi-hole on TCP tun (10.51.30.1):
sudo ./scripts/host-ufw-vpn-subnet.sh 10.51.32.0/24 10.51.30.1- Compose env for UDP WSS:
TUN_DEV: ovpn-udp
VPN_SUBNET: "10.51.32.0"
DNS1: "10.51.30.1"
DNS2: "10.51.30.1"
DCO: "true"
CIPHER: "AES-128-GCM"Keep DNS push on Pi-hole (10.51.30.1 or this stackβs own .1), not public 1.1.1.1.
Same host usually has one Pi-hole in network_mode: "container:openvpn-tcp-wss" (host netns). OpenVPN TCP uses dev tun unless TUN_DEV is set β after reboot/recreate the kernel picks tun0/tun1, while UFW still allows 53 on the old name. Symptom: dashboard Pi-hole pipeline green (HTTP :8080), VPN connected, no internet (block-outside-dns + DNS timeout). UDP often still works if its iface name is stable (TUN_DEV=ovpn-udp) or UFW allows that subnet β Pi-hole .1.
Do this:
- Pin TCP:
TUN_DEV: tun-tcpandTUN_IF: tun-tcp(do not turn DCO off if it already worked). - Prefer UFW by destination, not iface name (survives
tun0βtun-tcp):
sudo ufw allow in on tun-tcp proto udp to any port 53 comment 'Pi-hole DNS tun-tcp'
sudo ufw allow in on tun-tcp proto tcp to any port 53 comment 'Pi-hole DNS tun-tcp'
# even better, already used on some hosts:
sudo ufw allow from 10.51.15.0/24 to 10.51.15.1 port 53
sudo ufw allow from 10.51.16.0/24 to 10.51.16.1 port 53- Pi-hole FTL v6:
dns.interfaceis one name.BIND+tun-tcp,ovpn-udp,br-β¦(comma list) or a customentrypointwrappingstart.shcan leave:53open with no answers (Recv-Qgrowing, nolistening on β¦ port 53in FTL log). Working pattern:
# pi-hole compose β no custom entrypoint
FTLCONF_dns_listeningMode: 'BIND'
FTLCONF_dns_interface: 'tun-tcp' # TCP .1 onlyExtra listen IPs (UDP .1, docker-bridge for Xray) via /etc/dnsmasq.d/ after FTL is healthy:
printf '%s\n' 'listen-address=10.51.16.1' 'listen-address=172.20.0.1' \
> ~/pi-hole/etc-dnsmasq.d/99-extra-listen.conf
docker exec datagate-pihole pihole-FTL --config misc.etc_dnsmasq_d true
docker restart datagate-pihole
# log must show: listening on 10.51.16.1 and 172.20.0.1network_mode: container:openvpn-tcp-wssstores the container ID. Recreate OpenVPN β Pi-hole staysExited(No such container: <old id>). Recreate Pi-hole after TCP is Up. Optional systemd oneshot (After=docker.service) that waits foropenvpn-tcp-wss+openvpn-udp-wssthendocker compose up -d --force-recreatein~/pi-hole.- Host reboot does not fix a wedged FTL. Check
docker logs datagate-pihole | grep 'listening on'. Dashboard step 4 (API:8080) β DNS:53.
Compose env for TCP WSS:
TUN_DEV: tun-tcp
TUN_IF: tun-tcp
VPN_SUBNET: "10.51.15.0" # example
DNS1: "10.51.15.1"
DNS2: "10.51.15.1"| Path | Purpose |
|---|---|
/etc/openvpn |
Final OpenVPN config/cert destination |
/usr/share/easy-rsa |
EasyRSA installation |
$DATA_DIR (default /mnt) |
Mounted volume for logs, config, PKI |
- Copies and initializes Easy-RSA in
$DATA_DIR/easy-rsa - Generates CA, server cert/key, and
ta.keyif missing - Creates
server.confif missing - Configures
iptablesto allow VPN traffic - Runs OpenVPN and .NET app in parallel
Log files in $DATA_DIR:
openvpn.logopenvpn-status.logpihole-query-cursor.txtβ last Pi-hole query log cursor (pre-created by entrypoint, mode644)pihole-runtime-config.jsonβ Pi-hole collector config from dashboard Save & apply (survives container restart; mode600, re-applied on each container start)
Raw tls-crypt unwrapping failed lines from internet scanners are filtered out of Docker stdout; the .NET app re-emits them with an origin tag:
| Log prefix | Meaning | Suggested Wazuh level |
|---|---|---|
[OpenVpnTlsExternalProbe] |
Direct probe to OpenVPN port (not our WSS app) | ignore / low |
[OpenVpnTlsAppClient] |
WSS proxy path (127.0.0.1:localPort) with clientRef / User-Agent |
alert |
[OpenVpnTlsLocalUnknown] |
Loopback without matched proxy session | investigate |
Wazuh rule 100913 should match [OpenVpnTlsAppClient] instead of the raw TLS Error: tls-crypt string.
- Requires
--cap-add=NET_ADMINto allow iptables setup - Container exposes OpenVPN on configured port/protocol
- Designed to be used with external volume (
-v) for persistence
services:
openvpn_udp:
image: imkolganov/datagate-monitor-openvpn:latest
pull_policy: always
container_name: openvpn_udp
restart: unless-stopped
networks:
- backend_network
cap_add:
- NET_ADMIN
devices:
- "/dev/net/tun:/dev/net/tun"
volumes:
- openvpn_data_udp:/openvpn-udp
ports:
- "1194:1194/udp"
- "5010:5010"
environment:
EASY_RSA_PATH: "/openvpn-udp/easy-rsa"
DATA_DIR: /openvpn-udp
PORT: "1194"
API_PORT: 5010
PROTO: udp
# OPENVPN_WSS_UDP_PROXY: rust # optional: native UDP WSS datapath (default: dotnet)
OpenVpnManagement__Port: "5092"
OpenVpnManagement__Host: "localhost"
BACKEND__BASEURL: "http://backend:5581/"Created by Ivan Kolganov based on Kyle Mannaβs OpenVPN concepts.
| Contact | Link |
|---|---|
| IMKolganov/DataGateOpenVpnManager | |
| linkedin.com/in/imkolganov | |
| @KolganovIvan | |
| buymeacoffee.com/imkolganov |
MIT License .