Last active
May 5, 2025 22:55
-
-
Save philtrade/88bf4168b33b35b04667c5d56bfbfd10 to your computer and use it in GitHub Desktop.
Set up VPN server on openWRT with openvpn, openssl, and easyrsa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This script is adapted/tweaked from the openWRT wiki page on creating VPN server. | |
# VPN client can access outside world as if the traffic originates from the openWRT router. | |
# | |
# Prerequisites | |
# 1. opkg update && opkg install openvpn-openssl openvpn-easy-rsa | |
# 2. Get a public DDNS domain name or a static IP for the vpn server, put it into ddns_name="" near the bottom of the script. | |
# 3. Customize parameters, server/client service name, subnet, server port, output dir etc in the same bottom section. | |
# | |
# USAGE: | |
# 1. sh ./ovpn_owrt.sh <pki directory> [optional dh.pem file] | |
# 2. run /etc/openvpn/stop && sleep 10 && /etc/openvpn/start | |
# 3. install or import /tmp/test_ovpnout/<client_name>.ovpn into the device's OVPN client software. | |
# | |
# The pki directory is where certificates are created and managed, often people would use /etc/easyrsa/pki. | |
# Once generated, this script will *not* overwrite existing files if re-run with the same parameters. | |
# Therefore, to generate more client keys, change the client_name, and invoke with the same pki-dir value should do. | |
# | |
# User can supply a dh.pem generated on another faster machine, it may taka minutes to generate on the router. | |
# To generate a dh.pem: "openssl dhparam -dsaparam -out dh.pem 4096" | |
# | |
# OUTPUT: /etc/openvpn/server_<port>.conf, with keys/certs embedded. | |
# /tmp/test_ovpn/client_<port>.ovpn, can be imported/installed on the vpn client device. | |
# | |
fw_subnet_snat() { # SNAT outbound VPN client traffic to look like from this router (src_ip). | |
local subnet="$1" # subnet whose traffic are to be SNAT'ed | |
local src_ip="$2" # external IP of the router | |
local output="$3" # output firewall rules file | |
# Create an iptable routing rule in a file, then tell firewall to include it. | |
cat << EOF > $output | |
iptables -I INPUT -i tun+ -j ACCEPT | |
iptables -I FORWARD -i tun+ -j ACCEPT | |
iptables -I OUTPUT -o tun+ -j ACCEPT | |
iptables -I FORWARD -o tun+ -j ACCEPT | |
iptables -t nat -A POSTROUTING -s ${subnet}/24 -j SNAT --to-source ${src_ip} | |
EOF | |
} | |
run_easyrsa() { | |
if test -z $EASYRSA_PKI; then | |
echo "MUST PROVIDE output PKI directory." | |
exit 1 | |
fi | |
local dh_file=$1 | |
if ! test -e $EASYRSA_PKI; then | |
echo "==> Initializing new easy-rsa PKI directory $EASYRSA_PKI for certificates." | |
easyrsa init-pki | |
else | |
echo "==> Using existing easy-rsa PKI directory $EASYRSA_PKI." | |
fi | |
[ ! -e ${EASYRSA_PKI}/dh.pem ] && { | |
if ! test -z $dh_file && test -e $dh_file; then | |
echo "==> Using user-supplied DH.pem file: $dh_file." | |
cp $dh_file ${EASYRSA_PKI}/dh.pem | |
else # "openssl dhparam -dsaparam" is much faster than "easyrsa gen-dh" | |
echo "==> Building DH.pem ..." | |
openssl dhparam -dsaparam -out ${EASYRSA_PKI}/dh.pem $EASYRSA_KEY_SIZE | |
fi | |
} | |
[ ! -e ${EASYRSA_PKI}/ca.crt ] && echo "==> Building ca.crt" && easyrsa build-ca nopass # default no passphrase | |
[ ! -e ${EASYRSA_PKI}/tc.pem ] && echo "==> Building tc.pem" && openvpn --genkey --secret ${EASYRSA_PKI}/tc.pem | |
} | |
gen_vpn_confs() { | |
local OVPN_DIR="$1" | |
local OVPN_PKI="$2" | |
local OVPN_PORT="$3" | |
local OVPN_PROTO="$4" | |
local OVPN_SUBNET="$5" | |
local OVPN_SERV="$6" | |
local OVPN_DOMAIN="" # or on openWRT: "$(uci get dhcp.@dnsmasq[0].domain)" | |
local OVPN_POOL="${OVPN_SUBNET} 255.255.255.0" | |
local OVPN_DNS="${OVPN_POOL%.* *}.1" | |
OVPN_DH="$(cat ${OVPN_PKI}/dh.pem)" | |
OVPN_TC="$(sed -e "/^#/d;/^\w/N;s/\n//" ${OVPN_PKI}/tc.pem)" | |
OVPN_CA="$(openssl x509 -in ${OVPN_PKI}/ca.crt)" | |
NL=$'\n' | |
umask go= | |
# issued=$(ls ${OVPN_PKI}/issued | sed -e "s/\.\w*$//") | |
for OVPN_ID in $server_name $client_name; do | |
echo "Processing ${OVPN_ID}.crt..." | |
OVPN_KEY="$(cat ${OVPN_PKI}/private/${OVPN_ID}.key)" | |
OVPN_CERT="$(openssl x509 -in ${OVPN_PKI}/issued/${OVPN_ID}.crt)" | |
OVPN_CERT_EXT="$(openssl x509 -in ${OVPN_PKI}/issued/${OVPN_ID}.crt -purpose)" | |
OVPN_CONF_SERVER="\ | |
user nobody | |
group nogroup | |
dev tun | |
port ${OVPN_PORT} | |
proto ${OVPN_PROTO} | |
server ${OVPN_POOL} | |
topology subnet | |
keepalive 10 120 | |
persist-tun | |
persist-key | |
# To let the vpn client see other VPN clients in the private LAN, uncomment | |
; client-to-client | |
# Reuse virtual IP when client reconnect or VPN server is restarted | |
ifconfig-pool-persist ipp.txt | |
push \"dhcp-option DNS ${OVPN_DNS}\" | |
# To allow nslookup of hostname in the private LAN domain, set OVPN_DOMAIN above and uncomment below | |
; push \"dhcp-option DOMAIN ${OVPN_DOMAIN}\" | |
push \"persist-tun\" | |
push \"persist-key\" | |
push \"redirect-gateway def1 bypass-dhcp\" | |
<dh>${NL}${OVPN_DH}${NL}</dh>" | |
OVPN_CONF_CLIENT="\ | |
dev tun | |
nobind | |
client | |
remote ${OVPN_SERV} ${OVPN_PORT} ${OVPN_PROTO} | |
auth-nocache | |
remote-cert-tls server" | |
OVPN_CONF_COMMON="\ | |
<tls-crypt>${NL}${OVPN_TC}${NL}</tls-crypt> | |
<key>${NL}${OVPN_KEY}${NL}</key> | |
<cert>${NL}${OVPN_CERT}${NL}</cert> | |
<ca>${NL}${OVPN_CA}${NL}</ca>" | |
case ${OVPN_CERT_EXT} in | |
(*"SSL server : Yes"*) | |
cat << EOF > ${OVPN_DIR}/${OVPN_ID}.conf | |
${OVPN_CONF_SERVER} | |
${OVPN_CONF_COMMON} | |
EOF | |
echo "Generated server config file: ${OVPN_DIR}/${OVPN_ID}.conf" | |
;; | |
(*"SSL client : Yes"*) | |
cat << EOF > ${OVPN_DIR}/${OVPN_ID}.ovpn | |
${OVPN_CONF_CLIENT} | |
${OVPN_CONF_COMMON} | |
EOF | |
echo "Generated client .ovpn file: ${OVPN_DIR}/${OVPN_ID}.ovpn" | |
;; | |
esac | |
done | |
} | |
uci_include_firewall() { # Update the services (firewall rules and vpn server) | |
local fw_file="$1" | |
local fw_name="$2" | |
uci -q delete firewall.${fw_name} | |
uci set firewall.${fw_name}="include" | |
uci set firewall.${fw_name}.path=$(readlink -f $fw_file) | |
uci commit firewall | |
/etc/init.d/firewall restart 2>/dev/null || echo "firewall restart problem, re-run \"/etc/init.d/firewall restart\" from command line for more details." | |
} | |
uci_include_ovpn_server() { | |
local config_file="$1" | |
local server_name="$2" | |
uci -q delete openvpn.${server_name} | |
uci set openvpn.${server_name}=openvpn | |
uci set openvpn.${server_name}.config=$(readlink -f ${config_file}) | |
uci set openvpn.${server_name}.enabled="1" | |
uci commit openvpn | |
echo "==> Added ${config_file} to /etc/config/openvpn and is enabled:" | |
uci show openvpn.${server_name} | |
} | |
check_and_init() { | |
[ -z $pki_dir ] && echo "Must provide existing or a new target PKI directory" && exit 1 | |
[ -z $ddns_name ] && { | |
echo "Please provide a DDNS domain or public IP for the VPN server." | |
exit 1 | |
} | |
[ -z $openWRT_ip ] && { | |
. /lib/functions/network.sh; network_find_wan NET_IF; network_get_ipaddr openWRT_ip "${NET_IF}" | |
echo "Router IP not provided. Using what network_get_ipaddr suggests: $openWRT_ip" | |
} | |
[ ! -e $ovpn_output ] && mkdir $ovpn_output | |
export EASYRSA_PKI=$pki_dir | |
export EASYRSA_REQ_CN="ovpnca" | |
export EASYRSA_BATCH="1" | |
export EASYRSA_KEY_SIZE=4096 | |
} | |
make_vpn() { | |
# Step 1: Generate crypto files, VPN server 'conf' file, and VPN client '.ovpn' files | |
run_easyrsa $DHF # Generate CA cert, DH params, TSL pks pem | |
[ ! -e ${EASYRSA_PKI}/issued/${server_name}.crt ] && { easyrsa build-server-full $server_name $server_opts; } || { | |
echo "Server certificate already exists, not re-generating: ${EASYRSA_PKI}/issued/${server_name}.crt" | |
} | |
[ ! -e ${EASYRSA_PKI}/issued/${client_name}.crt ] && { easyrsa build-client-full $client_name $client_opts; } || { | |
echo "Client certificate already exists, not re-generating: ${EASYRSA_PKI}/issued/${client_name}.crt" | |
} | |
# Step 2: Generate VPN server config file using the crypto files from step 2 | |
gen_vpn_confs $ovpn_output $pki_dir $port $proto $subnet $ddns_name | |
server_conf_dst=/etc/openvpn/$(basename $server_conf_file) | |
[ ! -e $server_conf_dst ] && { | |
mv $server_conf_file /etc/openvpn/ | |
uci_include_ovpn_server /etc/openvpn/$(basename $server_conf_file) $server_name | |
} || echo "==> WARNING: $server_conf_dst already, not overwriting." | |
# Step 3: Configure routes - allow incoming WAN, then SNAT on outbound traffic | |
ovpnrule="ovpn_${port}_rule" | |
uci -q delete firewall.$ovpnrule | |
uci set firewall.${ovpnrule}="rule" | |
uci set firewall.${ovpnrule}.name="Allow-OpenVPN-${port}" | |
uci set firewall.${ovpnrule}.src="wan" | |
uci set firewall.${ovpnrule}.dest_port=$port | |
uci set firewall.${ovpnrule}.proto=$proto | |
uci set firewall.${ovpnrule}.target="ACCEPT" | |
fw_subnet_snat $subnet $openWRT_ip $opvn_fwrules_output | |
mv $opvn_fwrules_output /etc/openvpn/ | |
uci_include_firewall /etc/openvpn/$(basename $opvn_fwrules_output) "ovpn_${port}" | |
echo "${NL}==> VPN Client .ovpn, scp and install onto your VPN client device:" | |
ls -l ${ovpn_output}/${client_name}.ovpn | |
echo "${NL}==> To reload or restart the VPN service:${NL} | |
/etc/init.d/openvpn reload or restart" | |
} | |
# Customizable Parameters | |
subnet="10.4.0.0" | |
port="1194" # Or any port | |
proto="udp" | |
ddns_name="" # Point this to the DDNS domain reachable on the Internet | |
openWRT_ip="" # Change to the openWRT router public LAN IP, e.g. 192.168.1.5 | |
server_name="server_${port}"; server_opts="nopass" # s/nopass//, if need to passphrase-protect it. | |
client_name="client_${port}"; client_opts="nopass" # default no passphrase | |
# Step 0: Various vpn n config file parameters | |
ovpn_output="/tmp/test_ovpnout" | |
server_conf_file=$ovpn_output/${server_name}.conf | |
opvn_fwrules_output="${ovpn_output}/firewall.ovpn_${port}" | |
pki_dir=$1 # Required argument, the RSA PKI directory. Existing one will be reused; otherwise generate anew | |
DHF=$2 # Optional DH parameters .pem file | |
check_and_init | |
make_vpn |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment