diff --git a/luci-proto-openvpnc/Makefile b/luci-proto-openvpnc/Makefile new file mode 100644 index 00000000..cdede378 --- /dev/null +++ b/luci-proto-openvpnc/Makefile @@ -0,0 +1,24 @@ +# +# Copyright (C) 2026 +# +# This is free software, licensed under the Apache License, Version 2.0. +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=Support for OpenVPN Client +LUCI_DESCRIPTION:=This package provides support for OpenVPN client configuration in LuCI. +LUCI_DEPENDS:=+openvpn +kmod-tun +firewall +LUCI_PKGARCH:=all +PKG_VERSION:=1 +PKG_RELEASE:=1 + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +mkdir -p "$${IPKG_INSTROOT}/etc/openvpn/openvpnc" +exit 0 +endef + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-proto-openvpnc/htdocs/luci-static/resources/protocol/openvpnc.js b/luci-proto-openvpnc/htdocs/luci-static/resources/protocol/openvpnc.js new file mode 100644 index 00000000..6a8fefe2 --- /dev/null +++ b/luci-proto-openvpnc/htdocs/luci-static/resources/protocol/openvpnc.js @@ -0,0 +1,157 @@ +'use strict'; +'require form'; +'require network'; +'require uci'; +'require ui'; + +var profileDirectory = '/etc/openvpn/openvpnc'; + +network.registerPatternVirtual(/^vpn-.+$/); + +function getProfilePath(section, section_id) { + return section.formvalue(section_id, 'ovpn_file') || + uci.get('network', section_id, 'ovpn_file') || + '%s/default-%s.ovpn'.format(profileDirectory, section_id); +} + +function getAuthPath(section, section_id) { + var profile = getProfilePath(section, section_id); + + if (profile != null && profile !== '') + return profile.replace(/\.ovpn$/, '.auth'); + + return '%s/default-%s.auth'.format(profileDirectory, section_id); +} + +return network.registerProtocol('openvpnc', { + getI18n: function() { + return _('OpenVPN Client'); + }, + + getIfname: function() { + return this._ubus('l3_device') || 'vpn-%s'.format(this.sid); + }, + + getOpkgPackage: function() { + return 'openvpn'; + }, + + isFloating: function() { + return true; + }, + + isVirtual: function() { + return true; + }, + + getDevices: function() { + return null; + }, + + containsDevice: function(ifname) { + return (network.getIfnameOf(ifname) == this.getIfname()); + }, + + renderFormOptions: function(s) { + var o; + + o = s.taboption('general', form.FileUpload, 'ovpn_file', _('OpenVPN configuration'), + _('Upload or choose the client profile stored under %s. Only .ovpn files are accepted.').format(profileDirectory)); + o.root_directory = profileDirectory; + o.enable_remove = false; + o.rmempty = false; + o.renderWidget = function(section_id, option_index, cfgvalue) { + var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, { + id: this.cbid(section_id), + name: this.cbid(section_id), + show_hidden: this.show_hidden, + enable_upload: this.enable_upload, + enable_remove: this.enable_remove, + root_directory: this.root_directory, + disabled: (this.readonly != null) ? this.readonly : this.map.readonly + }); + + browserEl.renderListing = function(container, path, list) { + return ui.FileUpload.prototype.renderListing.apply(this, [ + container, + path, + list.filter(function(entry) { + return entry.type == 'directory' || + (entry.type == 'file' && entry.name.match(/\.ovpn$/)); + }) + ]); + }; + + return browserEl.render(); + }; + o.validate = function(section_id, value) { + if (value == null || value === '') + return _('Please upload or choose an .ovpn file'); + + if (value.indexOf(profileDirectory + '/') !== 0) + return _('Selected file must be stored in %s').format(profileDirectory); + + if (!value.match(/\.ovpn$/)) + return _('Selected file must use the .ovpn extension'); + + return true; + }; + + o = s.taboption('general', form.Value, 'username', _('Username'), + _('If the selected profile uses auth-user-pass, a matching .auth file will be generated automatically from these credentials.')); + o.rmempty = true; + + o = s.taboption('general', form.Value, 'password', _('Password')); + o.password = true; + o.rmempty = true; + + o = s.taboption('advanced', form.Flag, 'custom_dns_enable', _('Allow custom DNS servers'), + _('When enabled, the DNS servers entered below will be added to this interface even if the OpenVPN server does not push any DNS settings.')); + o.rmempty = false; + o.default = o.disabled; + + o = s.taboption('advanced', form.Value, 'custom_dns', _('Custom DNS servers'), + _('Enter one or more DNS server addresses separated by spaces or commas, for example: 1.1.1.1,8.8.8.8')); + o.placeholder = '1.1.1.1 8.8.8.8'; + o.depends('custom_dns_enable', '1'); + o.rmempty = true; + + o = s.taboption('advanced', form.Flag, 'extra_routes_enable', _('Allow extra route networks'), + _('When enabled, the custom route networks entered below will be appended after the route networks pushed by the OpenVPN server.')); + o.rmempty = false; + o.default = o.disabled; + + o = s.taboption('advanced', form.Value, 'extra_routes', _('Extra route networks'), + _('Enter one or more IPv4 CIDR networks separated by spaces or commas, for example: 10.0.0.0/24,172.16.10.0/24')); + o.placeholder = '10.0.0.0/24 172.16.10.0/24'; + o.depends('extra_routes_enable', '1'); + o.rmempty = true; + + o = s.taboption('advanced', form.Flag, 'domain_dns_enable', _('Resolve specific domains via custom DNS'), + _('When enabled, dnsmasq will forward the domains entered below to the custom DNS servers through the generated ovpnc.conf file in its active conf-dir directory.')); + o.rmempty = false; + o.default = o.disabled; + o.depends('custom_dns_enable', '1'); + + o = s.taboption('advanced', form.Value, 'dns_domains', _('Domains resolved by custom DNS'), + _('Enter one or more domains separated by spaces or commas, for example: corp.example.com')); + o.placeholder = 'corp.example.com'; + o.depends('domain_dns_enable', '1'); + o.rmempty = true; + + o = s.taboption('advanced', form.DummyValue, '_auth_file', _('Authentication file')); + o.cfgvalue = function(section_id) { + return getAuthPath(this.section, section_id); + }; + + o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU')); + o.placeholder = '1500'; + o.datatype = 'max(9200)'; + o.rmempty = true; + + o = s.taboption('advanced', form.DummyValue, '_profile_path', _('Stored profile path')); + o.cfgvalue = function(section_id) { + return getProfilePath(this.section, section_id); + }; + } +}); diff --git a/luci-proto-openvpnc/po/zh-cn b/luci-proto-openvpnc/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-proto-openvpnc/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-proto-openvpnc/po/zh_Hans/openvpnc.po b/luci-proto-openvpnc/po/zh_Hans/openvpnc.po new file mode 100644 index 00000000..648f0515 --- /dev/null +++ b/luci-proto-openvpnc/po/zh_Hans/openvpnc.po @@ -0,0 +1,105 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:22 +msgid "OpenVPN Client" +msgstr "OpenVPN 客户端" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:49 +msgid "OpenVPN configuration" +msgstr "OpenVPN 配置文件" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:50 +msgid "Upload or choose the client profile stored under %s. Only .ovpn files are accepted." +msgstr "上传或选择保存在 %s 下的客户端配置文件,仅接受 .ovpn 文件。" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:80 +msgid "Please upload or choose an .ovpn file" +msgstr "请上传或选择一个 .ovpn 文件" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:83 +msgid "Selected file must be stored in %s" +msgstr "所选文件必须保存在 %s 下" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:86 +msgid "Selected file must use the .ovpn extension" +msgstr "所选文件必须使用 .ovpn 扩展名" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:91 +msgid "Username" +msgstr "用户名" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:92 +msgid "If the selected profile uses auth-user-pass, a matching .auth file will be generated automatically from these credentials." +msgstr "如果所选配置使用 auth-user-pass,将根据这里填写的凭据自动生成对应的 .auth 文件。" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:96 +msgid "Password" +msgstr "密码" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:100 +msgid "Allow custom DNS servers" +msgstr "启用自定义 DNS 服务器" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:101 +msgid "When enabled, the DNS servers entered below will be added to this interface even if the OpenVPN server does not push any DNS settings." +msgstr "启用后,即使 OpenVPN 服务端没有下发 DNS 设置,下方填写的 DNS 服务器也会被加入到此接口中。" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:106 +msgid "Custom DNS servers" +msgstr "自定义 DNS 服务器" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:107 +msgid "Enter one or more DNS server addresses separated by spaces or commas, for example: 1.1.1.1,8.8.8.8" +msgstr "输入一个或多个 DNS 服务器地址,使用空格或逗号分隔,例如:1.1.1.1,8.8.8.8" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:112 +msgid "Allow extra route networks" +msgstr "启用附加路由网段" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:113 +msgid "When enabled, the custom route networks entered below will be appended after the route networks pushed by the OpenVPN server." +msgstr "启用后,下方填写的自定义路由网段会追加到 OpenVPN 服务端下发的路由网段之后。" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:118 +msgid "Extra route networks" +msgstr "附加路由网段" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:119 +msgid "Enter one or more IPv4 CIDR networks separated by spaces or commas, for example: 10.0.0.0/24,172.16.10.0/24" +msgstr "输入一个或多个 IPv4 CIDR 网段,使用空格或逗号分隔,例如:10.0.0.0/24,172.16.10.0/24" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:124 +msgid "Resolve specific domains via custom DNS" +msgstr "使用自定义 DNS 解析特定域名" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:125 +msgid "When enabled, dnsmasq will forward the domains entered below to the custom DNS servers through the generated ovpnc.conf file in its active conf-dir directory." +msgstr "启用后,dnsmasq 会在当前生效的 conf-dir 目录中生成 ovpnc.conf,并将下方填写的域名转发到自定义 DNS 服务器。" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:131 +msgid "Domains resolved by custom DNS" +msgstr "由自定义 DNS 解析的域名" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:132 +msgid "Enter one or more domains separated by spaces or commas, for example: corp.example.com" +msgstr "输入一个或多个域名,使用空格或逗号分隔,例如:corp.example.com" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:137 +msgid "Authentication file" +msgstr "认证文件路径" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:142 +msgid "Override MTU" +msgstr "自定义 MTU" + +#: htdocs/luci-static/resources/protocol/openvpnc.js:147 +msgid "Stored profile path" +msgstr "配置文件保存路径" diff --git a/luci-proto-openvpnc/root/etc/hotplug.d/iface/95-openvpnc-wan b/luci-proto-openvpnc/root/etc/hotplug.d/iface/95-openvpnc-wan new file mode 100644 index 00000000..1090b179 --- /dev/null +++ b/luci-proto-openvpnc/root/etc/hotplug.d/iface/95-openvpnc-wan @@ -0,0 +1,86 @@ +#!/bin/sh + +[ "$ACTION" = "ifup" ] || exit 0 +[ "$INTERFACE" = "wan" ] || exit 0 +[ -x /sbin/ifup ] || exit 0 +[ -x /sbin/ifdown ] || exit 0 + +. /lib/functions.sh + +openvpnc_find_stale_pids() { + local section="$1" + local runtime_profile="/var/etc/openvpnc-$section.ovpn" + local status_file="/var/run/openvpnc-$section.status" + local proc pid cmdline + + for proc in /proc/[0-9]*; do + [ -r "$proc/cmdline" ] || continue + + pid="${proc##*/}" + cmdline="$(tr '\000' ' ' < "$proc/cmdline" 2>/dev/null)" + + case "$cmdline" in + *"/usr/sbin/openvpn "*"$runtime_profile"*|*"/usr/sbin/openvpn "*"$status_file"*) + echo "$pid" + ;; + esac + done +} + +openvpnc_kill_stale_processes() { + local section="$1" + local pid pids="" + + for pid in $(openvpnc_find_stale_pids "$section"); do + pids="$pids $pid" + done + + pids="${pids# }" + [ -n "$pids" ] || return 0 + + logger -t openvpnc-hotplug "terminating stale openvpnc process(es) for $section: $pids" + kill $pids >/dev/null 2>&1 + sleep 2 + + for pid in $pids; do + [ -d "/proc/$pid" ] || continue + kill -9 "$pid" >/dev/null 2>&1 + done +} + +openvpnc_restart_interface() { + local section="$1" + local proto auto + + config_get proto "$section" proto + [ "$proto" = "openvpnc" ] || return 0 + + config_get auto "$section" auto 1 + [ "$auto" = "0" ] && return 0 + + logger -t openvpnc-hotplug "restarting interface $section after wan ifup" + /sbin/ifdown "$section" >/dev/null 2>&1 + openvpnc_kill_stale_processes "$section" + /sbin/ifup "$section" >/dev/null 2>&1 +} + +LOCK_FILE="/var/lock/openvpnc-wan-ifup.lock" +mkdir -p "${LOCK_FILE%/*}" + +if [ -s "$LOCK_FILE" ]; then + read -r pid < "$LOCK_FILE" + if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then + exit 0 + fi + + rm -f "$LOCK_FILE" +fi + +( + trap 'rm -f "$LOCK_FILE"' EXIT + echo $$ > "$LOCK_FILE" + logger -t openvpnc-hotplug "scheduled openvpnc restart 10 seconds after wan ifup" + sleep 10 + config_load network + config_foreach openvpnc_restart_interface interface +) >/dev/null 2>&1 & diff --git a/luci-proto-openvpnc/root/lib/netifd/openvpnc-down b/luci-proto-openvpnc/root/lib/netifd/openvpnc-down new file mode 100755 index 00000000..3bf0d867 --- /dev/null +++ b/luci-proto-openvpnc/root/lib/netifd/openvpnc-down @@ -0,0 +1,9 @@ +#!/bin/sh + +. /lib/netifd/netifd-proto.sh + +[ -n "$OPENVPNC_ID" ] || exit 0 +[ -n "$dev" ] || exit 0 + +proto_init_update "$dev" 0 +proto_send_update "$OPENVPNC_ID" diff --git a/luci-proto-openvpnc/root/lib/netifd/openvpnc-up b/luci-proto-openvpnc/root/lib/netifd/openvpnc-up new file mode 100755 index 00000000..3987b342 --- /dev/null +++ b/luci-proto-openvpnc/root/lib/netifd/openvpnc-up @@ -0,0 +1,189 @@ +#!/bin/sh + +. /lib/netifd/netifd-proto.sh + +[ -n "$OPENVPNC_ID" ] || exit 0 +[ -n "$dev" ] || exit 0 + +openvpnc_prefix() { + case "$1" in + 255.255.255.255) echo 32 ;; + 255.255.255.254) echo 31 ;; + 255.255.255.252) echo 30 ;; + 255.255.255.248) echo 29 ;; + 255.255.255.240) echo 28 ;; + 255.255.255.224) echo 27 ;; + 255.255.255.192) echo 26 ;; + 255.255.255.128) echo 25 ;; + 255.255.255.0) echo 24 ;; + 255.255.254.0) echo 23 ;; + 255.255.252.0) echo 22 ;; + 255.255.248.0) echo 21 ;; + 255.255.240.0) echo 20 ;; + 255.255.224.0) echo 19 ;; + 255.255.192.0) echo 18 ;; + 255.255.128.0) echo 17 ;; + 255.255.0.0) echo 16 ;; + 255.254.0.0) echo 15 ;; + 255.252.0.0) echo 14 ;; + 255.248.0.0) echo 13 ;; + 255.240.0.0) echo 12 ;; + 255.224.0.0) echo 11 ;; + 255.192.0.0) echo 10 ;; + 255.128.0.0) echo 9 ;; + 255.0.0.0) echo 8 ;; + 254.0.0.0) echo 7 ;; + 252.0.0.0) echo 6 ;; + 248.0.0.0) echo 5 ;; + 240.0.0.0) echo 4 ;; + 224.0.0.0) echo 3 ;; + 192.0.0.0) echo 2 ;; + 128.0.0.0) echo 1 ;; + 0.0.0.0) echo 0 ;; + *) echo "" ;; + esac +} + +openvpnc_getvar() { + local name="$1" + eval "printf '%s' \"\${$name}\"" +} + +openvpnc_ucivalue() { + uci -q get "network.${OPENVPNC_ID}.$1" +} + +openvpnc_enabled() { + case "$1" in + 1|on|true|yes|enabled) return 0 ;; + *) return 1 ;; + esac +} + +openvpnc_words() { + printf '%s' "$1" | tr ',\n\t' ' ' +} + +openvpnc_cidr_split() { + local cidr="$1" + local address prefix + + case "$cidr" in + */*) + address="${cidr%/*}" + prefix="${cidr#*/}" + case "$prefix" in + ''|*[!0-9]*) return 1 ;; + *) ;; + esac + [ "$prefix" -ge 0 ] && [ "$prefix" -le 32 ] || return 1 + printf '%s %s\n' "$address" "$prefix" + return 0 + ;; + *) + return 1 + ;; + esac +} + +openvpnc_add_dns_data() { + local index=1 + local option + local value + local rest + + while :; do + option="$(openvpnc_getvar "foreign_option_$index")" + [ -n "$option" ] || break + + case "$option" in + "dhcp-option DNS "*) + value="${option#dhcp-option DNS }" + [ -n "$value" ] && proto_add_dns_server "$value" + ;; + "dhcp-option DOMAIN "*) + value="${option#dhcp-option DOMAIN }" + [ -n "$value" ] && proto_add_dns_search "$value" + ;; + "dhcp-option DOMAIN-SEARCH "*) + rest="${option#dhcp-option DOMAIN-SEARCH }" + for value in $rest; do + [ -n "$value" ] && proto_add_dns_search "$value" + done + ;; + esac + + index=$((index + 1)) + done +} + +openvpnc_add_custom_dns_data() { + local enabled dns_list dns + + enabled="$(openvpnc_ucivalue custom_dns_enable)" + openvpnc_enabled "$enabled" || return 0 + + dns_list="$(openvpnc_ucivalue custom_dns)" + for dns in $(openvpnc_words "$dns_list"); do + [ -n "$dns" ] && proto_add_dns_server "$dns" + done +} + +openvpnc_add_route_data() { + local source_ip="$1" + local fallback_gw="$2" + local index=1 + local network + local netmask + local gateway + local prefix + + while :; do + network="$(openvpnc_getvar "route_network_$index")" + [ -n "$network" ] || break + + netmask="$(openvpnc_getvar "route_netmask_$index")" + gateway="$(openvpnc_getvar "route_gateway_$index")" + [ -n "$gateway" ] || gateway="$fallback_gw" + + prefix="$(openvpnc_prefix "$netmask")" + [ -n "$prefix" ] || prefix=32 + + proto_add_ipv4_route "$network" "$prefix" "$gateway" "$source_ip" + index=$((index + 1)) + done +} + +openvpnc_add_custom_route_data() { + local source_ip="$1" + local fallback_gw="$2" + local enabled route_list route_entry parsed network prefix + + enabled="$(openvpnc_ucivalue extra_routes_enable)" + openvpnc_enabled "$enabled" || return 0 + + route_list="$(openvpnc_ucivalue extra_routes)" + for route_entry in $(openvpnc_words "$route_list"); do + parsed="$(openvpnc_cidr_split "$route_entry")" || continue + network="${parsed% *}" + prefix="${parsed#* }" + [ -n "$network" ] && [ -n "$prefix" ] && proto_add_ipv4_route "$network" "$prefix" "$fallback_gw" "$source_ip" + done +} + +local_ip="${ifconfig_local:-$4}" +remote_ip="${ifconfig_remote:-$5}" +prefix="$(openvpnc_prefix "$ifconfig_netmask")" + +[ -n "$local_ip" ] || local_ip="$route_vpn_gateway" +[ -n "$prefix" ] || [ -n "$remote_ip" ] && prefix=30 +[ -n "$prefix" ] || prefix=32 + +proto_init_update "$dev" 1 1 +proto_set_keep 1 +[ -n "$local_ip" ] && proto_add_ipv4_address "$local_ip" "$prefix" "" "$remote_ip" +openvpnc_add_dns_data +openvpnc_add_custom_dns_data +openvpnc_add_route_data "$local_ip" "$route_vpn_gateway" +openvpnc_add_custom_route_data "$local_ip" "$route_vpn_gateway" +proto_send_update "$OPENVPNC_ID" diff --git a/luci-proto-openvpnc/root/lib/netifd/proto/openvpnc.sh b/luci-proto-openvpnc/root/lib/netifd/proto/openvpnc.sh new file mode 100755 index 00000000..7a346422 --- /dev/null +++ b/luci-proto-openvpnc/root/lib/netifd/proto/openvpnc.sh @@ -0,0 +1,442 @@ +#!/bin/sh + +[ -n "$INCLUDE_ONLY" ] || { + . /lib/functions.sh + . ../netifd-proto.sh + init_proto "$@" +} + +openvpnc_get_first_value() { + local file="$1" + local key="$2" + + awk -v key="$key" ' + /^[[:space:]]*[#;]/ { next } + $1 == key { + print $2 + exit + } + ' "$file" +} + +openvpnc_is_ipv4() { + local value="$1" + local octet + + case "$value" in + ''|*[!0-9.]*|*.*.*.*.*) + return 1 + ;; + esac + + local old_ifs="$IFS" + IFS='.' + set -- $value + IFS="$old_ifs" + [ "$#" -eq 4 ] || return 1 + + for octet in "$@"; do + [ -n "$octet" ] || return 1 + [ "$octet" -ge 0 ] 2>/dev/null || return 1 + [ "$octet" -le 255 ] || return 1 + done + + return 0 +} + +openvpnc_get_upstream_dns_servers() { + local resolv_file + + for resolv_file in /tmp/resolv.conf.d/resolv.conf.auto /tmp/resolv.conf.auto; do + [ -r "$resolv_file" ] || continue + awk '/^nameserver / { print $2 }' "$resolv_file" + done | awk '!seen[$0]++' +} + +openvpnc_resolve_host() { + local host="$1" + local ip + local server + + openvpnc_is_ipv4 "$host" && { + echo "$host" + return 0 + } + + if command -v nslookup >/dev/null 2>&1; then + for server in $(openvpnc_get_upstream_dns_servers); do + openvpnc_is_ipv4 "$server" || continue + ip="$({ nslookup "$host" "$server" 2>/dev/null || true; } | awk '/^Address [0-9]+: / { print $3; exit }')" + openvpnc_is_ipv4 "$ip" || continue + echo "$ip" + return 0 + done + fi + + for ip in $(resolveip -t 5 "$host" 2>/dev/null); do + openvpnc_is_ipv4 "$ip" || continue + echo "$ip" + return 0 + done + + return 1 +} + +openvpnc_get_dev_type() { + local file="$1" + local dev_type + local dev + + dev_type="$(openvpnc_get_first_value "$file" "dev-type")" + [ -n "$dev_type" ] && { + echo "$dev_type" + return + } + + dev="$(openvpnc_get_first_value "$file" "dev")" + case "$dev" in + tap*) echo "tap" ;; + *) echo "tun" ;; + esac + } + +openvpnc_get_auth_file() { + local file="$1" + local mode + local sidecar + + mode="$(awk ' + /^[[:space:]]*[#;]/ { next } + $1 == "auth-user-pass" { + if (NF >= 2) { + print "configured" + } else { + print "required" + } + exit + } + ' "$file")" + + case "$mode" in + "") + return 1 + ;; + configured) + return 2 + ;; + required) + sidecar="${file%.ovpn}.auth" + [ -f "$sidecar" ] || return 3 + echo "$sidecar" + return 0 + ;; + esac + + return 1 +} + +openvpnc_add_host_dependencies() { + local config="$1" + local file="$2" + local host + local ip + + for host in $(awk ' + /^[[:space:]]*[#;]/ { next } + $1 == "remote" && NF >= 2 { print $2 } + ' "$file" | awk '!seen[$0]++'); do + [ -n "$host" ] || continue + ip="$(openvpnc_resolve_host "$host")" || continue + proto_add_host_dependency "$config" "$ip" + done +} + +openvpnc_prepare_profile() { + local config="$1" + local file="$2" + local tmp_file="/var/etc/openvpnc-$config.ovpn" + local host + local ip + local rewrites='' + local changed=0 + + for host in $(awk ' + /^[[:space:]]*[#;]/ { next } + $1 == "remote" && NF >= 2 { print $2 } + ' "$file" | awk '!seen[$0]++'); do + openvpnc_is_ipv4 "$host" && continue + ip="$(openvpnc_resolve_host "$host")" || continue + rewrites="${rewrites}${host}=${ip}|" + changed=1 + logger -t openvpnc "resolved remote host $host to $ip for $config" + done + + if [ "$changed" != "1" ]; then + echo "$file" + return 0 + fi + + awk -v rewrites="$rewrites" ' + BEGIN { + n = split(rewrites, items, /\|/) + for (i = 1; i <= n; i++) { + if (items[i] == "") + continue + split(items[i], pair, /=/) + map[pair[1]] = pair[2] + } + } + /^[[:space:]]*[#;]/ { + print + next + } + $1 == "remote" && NF >= 2 && ($2 in map) { + $2 = map[$2] + } + { + print + } + ' "$file" > "$tmp_file" || return 1 + + echo "$tmp_file" +} + +openvpnc_write_auth_file() { + local file="$1" + local username="$2" + local password="$3" + + mkdir -p "${file%/*}" || return 1 + umask 077 + { + printf '%s\n' "$username" + printf '%s\n' "$password" + } > "$file" +} + +openvpnc_zone_name() { + printf 'ovpnc' +} + +openvpnc_zone_section() { + openvpnc_zone_name "$1" +} + +openvpnc_forwarding_section() { + printf '%s_fwd' "$(openvpnc_zone_name "$1")" +} + +openvpnc_firewall_reload() { + /etc/init.d/firewall enabled >/dev/null 2>&1 || return 0 + /etc/init.d/firewall reload >/dev/null 2>&1 +} + +openvpnc_sync_dnsmasq() { + [ -x /usr/bin/openvpnc-dnsmasq-sync ] || return 0 + /usr/bin/openvpnc-dnsmasq-sync >/dev/null 2>&1 +} + +openvpnc_ensure_firewall() { + local config="$1" + local zone_section forwarding_section zone_name changed=0 + local legacy_zone_section legacy_forwarding_section + + zone_section="$(openvpnc_zone_section "$config")" + forwarding_section="$(openvpnc_forwarding_section "$config")" + zone_name="$zone_section" + legacy_zone_section="$(printf 'openvpnc_%s' "$config" | tr -c 'A-Za-z0-9_' '_')" + legacy_forwarding_section="$(printf 'openvpnc_%s_lan_fwd' "$config" | tr -c 'A-Za-z0-9_' '_')" + + for old_section in "$legacy_zone_section" "$legacy_forwarding_section" ovpn_ ovpn__fwd ovpnc_lan_fwd_; do + if uci -q get firewall.$old_section >/dev/null; then + uci -q delete firewall.$old_section + changed=1 + fi + done + + if [ "$(uci -q get firewall.$zone_section)" != "zone" ]; then + uci -q set firewall.$zone_section=zone + changed=1 + fi + + if [ "$(uci -q get firewall.$zone_section.name)" != "$zone_name" ]; then + uci -q set firewall.$zone_section.name="$zone_name" + changed=1 + fi + + if [ "$(uci -q get firewall.$zone_section.input)" != "REJECT" ]; then + uci -q set firewall.$zone_section.input='REJECT' + changed=1 + fi + + if [ "$(uci -q get firewall.$zone_section.output)" != "ACCEPT" ]; then + uci -q set firewall.$zone_section.output='ACCEPT' + changed=1 + fi + + if [ "$(uci -q get firewall.$zone_section.forward)" != "REJECT" ]; then + uci -q set firewall.$zone_section.forward='REJECT' + changed=1 + fi + + if [ "$(uci -q get firewall.$zone_section.masq)" != "1" ]; then + uci -q set firewall.$zone_section.masq='1' + changed=1 + fi + + if [ "$(uci -q get firewall.$zone_section.mtu_fix)" != "1" ]; then + uci -q set firewall.$zone_section.mtu_fix='1' + changed=1 + fi + + if [ "$(uci -q get firewall.$zone_section.family)" != "ipv4" ]; then + uci -q set firewall.$zone_section.family='ipv4' + changed=1 + fi + + if [ "$(uci -q get firewall.$zone_section.network)" != "$config" ]; then + uci -q delete firewall.$zone_section.network + uci add_list firewall.$zone_section.network="$config" + changed=1 + fi + + if [ "$(uci -q get firewall.$forwarding_section)" != "forwarding" ]; then + uci -q set firewall.$forwarding_section=forwarding + changed=1 + fi + + if [ "$(uci -q get firewall.$forwarding_section.src)" != "lan" ]; then + uci -q set firewall.$forwarding_section.src='lan' + changed=1 + fi + + if [ "$(uci -q get firewall.$forwarding_section.dest)" != "$zone_name" ]; then + uci -q set firewall.$forwarding_section.dest="$zone_name" + changed=1 + fi + + if [ "$changed" = "1" ]; then + uci commit firewall + logger -t openvpnc "updated firewall zone $zone_name for interface $config" + openvpnc_firewall_reload + fi +} + +proto_openvpnc_init_config() { + proto_config_add_string "ovpn_file" + proto_config_add_string "username" + proto_config_add_string "password" + proto_config_add_int "mtu" + available=1 + no_device=1 +} + +proto_openvpnc_setup() { + local config="$1" + local ovpn_file username password mtu + local ifname dev_type auth_file auth_state auth_required auth_inline ovpn_run_file + + json_get_vars ovpn_file username password mtu + + [ -n "$username" ] || username="$(uci -q get network.$config.username)" + [ -n "$password" ] || password="$(uci -q get network.$config.password)" + + ifname="vpn-$config" + mkdir -p /etc/openvpn/openvpnc + + [ -n "$ovpn_file" ] && [ -f "$ovpn_file" ] || { + logger -t openvpnc "missing profile for $config" + proto_setup_failed "$config" + proto_block_restart "$config" + return 1 + } + + openvpnc_add_host_dependencies "$config" "$ovpn_file" + + mkdir -p /var/run /var/etc + ovpn_run_file="$(openvpnc_prepare_profile "$config" "$ovpn_file")" || { + logger -t openvpnc "failed to prepare runtime profile for $config" + proto_setup_failed "$config" + proto_block_restart "$config" + return 1 + } + + dev_type="$(openvpnc_get_dev_type "$ovpn_file")" + auth_file="$(openvpnc_get_auth_file "$ovpn_file")" + auth_state="$?" + auth_required=0 + auth_inline=0 + + case "$auth_state" in + 0|3) + auth_required=1 + auth_file="${ovpn_file%.ovpn}.auth" + ;; + 2) + auth_required=1 + auth_inline=1 + ;; + esac + + if [ -n "$username$password" ]; then + [ -n "$username" ] && [ -n "$password" ] || { + logger -t openvpnc "username and password must both be set for $config" + proto_setup_failed "$config" + proto_block_restart "$config" + return 1 + } + + auth_file="${ovpn_file%.ovpn}.auth" + openvpnc_write_auth_file "$auth_file" "$username" "$password" || { + logger -t openvpnc "failed to write auth file $auth_file for $config" + proto_setup_failed "$config" + proto_block_restart "$config" + return 1 + } + auth_required=1 + auth_state=0 + fi + + if [ "$auth_state" = "3" ]; then + logger -t openvpnc "profile $ovpn_file requires auth-user-pass; set username and password in UCI or create ${ovpn_file%.ovpn}.auth with username on first line and password on second line" + proto_setup_failed "$config" + proto_block_restart "$config" + return 1 + fi + + openvpnc_ensure_firewall "$config" + openvpnc_sync_dnsmasq + + set -- /usr/sbin/openvpn \ + --syslog "openvpnc($config)" \ + --status "/var/run/openvpnc-$config.status" \ + --cd "${ovpn_file%/*}" \ + --config "$ovpn_run_file" \ + --dev "$ifname" \ + --dev-type "$dev_type" \ + --script-security 2 \ + --up /lib/netifd/openvpnc-up \ + --down-pre \ + --down /lib/netifd/openvpnc-down \ + --setenv OPENVPNC_ID "$config" + + [ -n "$mtu" ] && set -- "$@" --tun-mtu "$mtu" + if [ "$auth_required" = "1" ] && [ "$auth_inline" = "0" ]; then + set -- "$@" --auth-user-pass "$auth_file" + fi + + proto_run_command "$config" "$@" +} + +proto_openvpnc_teardown() { + local config="$1" + + rm -f "/var/run/openvpnc-$config.status" + rm -f "/var/etc/openvpnc-$config.ovpn" + openvpnc_sync_dnsmasq + proto_kill_command "$config" +} + +[ -n "$INCLUDE_ONLY" ] || { + add_protocol openvpnc +} diff --git a/luci-proto-openvpnc/root/lib/upgrade/keep.d/luci-proto-openvpnc b/luci-proto-openvpnc/root/lib/upgrade/keep.d/luci-proto-openvpnc new file mode 100644 index 00000000..df79d6d6 --- /dev/null +++ b/luci-proto-openvpnc/root/lib/upgrade/keep.d/luci-proto-openvpnc @@ -0,0 +1 @@ +/etc/openvpn/openvpnc/ diff --git a/luci-proto-openvpnc/root/usr/bin/openvpnc-dnsmasq-sync b/luci-proto-openvpnc/root/usr/bin/openvpnc-dnsmasq-sync new file mode 100755 index 00000000..3ddab4e7 --- /dev/null +++ b/luci-proto-openvpnc/root/usr/bin/openvpnc-dnsmasq-sync @@ -0,0 +1,116 @@ +#!/bin/sh + +. /lib/functions.sh + +OPENVPNC_DNSMASQ_DIRS= + +openvpnc_append_dnsmasq_dir() { + local section="$1" + local dnsmasq_conf="/tmp/etc/dnsmasq.conf.$section" + local dnsmasq_dir + + [ -f "$dnsmasq_conf" ] || return 0 + + dnsmasq_dir="$(awk -F '=' '/^conf-dir=/ {print $2; exit}' "$dnsmasq_conf")" + dnsmasq_dir="${dnsmasq_dir%%,*}" + dnsmasq_dir="${dnsmasq_dir%/}" + [ -n "$dnsmasq_dir" ] || return 0 + + case " $OPENVPNC_DNSMASQ_DIRS " in + *" $dnsmasq_dir "*) return 0 ;; + esac + + append OPENVPNC_DNSMASQ_DIRS "$dnsmasq_dir" +} + +openvpnc_get_dnsmasq_dirs() { + local default_dir="/tmp/dnsmasq.d" + + if [ -f /etc/openwrt_release ]; then + OPENVPNC_DNSMASQ_DIRS= + config_load dhcp + config_foreach openvpnc_append_dnsmasq_dir dnsmasq + fi + + [ -n "$OPENVPNC_DNSMASQ_DIRS" ] || OPENVPNC_DNSMASQ_DIRS="$default_dir" + printf '%s\n' $OPENVPNC_DNSMASQ_DIRS +} + +openvpnc_prepare_dnsmasq_file() { + OPENVPNC_DNSMASQ_DIR="$1" + OPENVPNC_DNSMASQ_FILE="$OPENVPNC_DNSMASQ_DIR/ovpnc.conf" + OPENVPNC_DNSMASQ_TMP="$OPENVPNC_DNSMASQ_FILE.tmp" + + mkdir -p "$OPENVPNC_DNSMASQ_DIR" || return 1 + : > "$OPENVPNC_DNSMASQ_TMP" || return 1 + config_foreach openvpnc_append_dnsmasq_section interface + + if [ -s "$OPENVPNC_DNSMASQ_TMP" ]; then + mv "$OPENVPNC_DNSMASQ_TMP" "$OPENVPNC_DNSMASQ_FILE" + else + rm -f "$OPENVPNC_DNSMASQ_TMP" "$OPENVPNC_DNSMASQ_FILE" + fi +} + +openvpnc_enabled() { + case "$1" in + 1|on|true|yes|enabled) return 0 ;; + *) return 1 ;; + esac +} + +openvpnc_words() { + printf '%s' "$1" | tr ',\n\t' ' ' +} + +openvpnc_clean_domain() { + local domain="$1" + + domain="${domain#/}" + domain="${domain#/}" + domain="${domain#.}" + domain="${domain%.}" + domain="${domain%/}" + printf '%s' "$domain" +} + +openvpnc_append_dnsmasq_section() { + local section="$1" + local proto enabled dns_servers domains dns_server domain + + config_get proto "$section" proto + [ "$proto" = "openvpnc" ] || return 0 + + config_get enabled "$section" custom_dns_enable + openvpnc_enabled "$enabled" || return 0 + + config_get dns_servers "$section" custom_dns + [ -n "$dns_servers" ] || return 0 + + config_get enabled "$section" domain_dns_enable + openvpnc_enabled "$enabled" || return 0 + + config_get domains "$section" dns_domains + [ -n "$domains" ] || return 0 + + for domain in $(openvpnc_words "$domains"); do + domain="$(openvpnc_clean_domain "$domain")" + [ -n "$domain" ] || continue + echo "rebind-domain-ok=/$domain/" >> "$OPENVPNC_DNSMASQ_TMP" + + for dns_server in $(openvpnc_words "$dns_servers"); do + [ -n "$dns_server" ] || continue + echo "server=/$domain/$dns_server" >> "$OPENVPNC_DNSMASQ_TMP" + done + done +} + +config_load network + +for OPENVPNC_DNSMASQ_DIR in $(openvpnc_get_dnsmasq_dirs); do + openvpnc_prepare_dnsmasq_file "$OPENVPNC_DNSMASQ_DIR" +done + +if /etc/init.d/dnsmasq enabled >/dev/null 2>&1; then + /etc/init.d/dnsmasq restart >/dev/null 2>&1 +fi diff --git a/luci-proto-openvpnc/root/usr/share/rpcd/acl.d/luci-openvpnc.json b/luci-proto-openvpnc/root/usr/share/rpcd/acl.d/luci-openvpnc.json new file mode 100644 index 00000000..e396c733 --- /dev/null +++ b/luci-proto-openvpnc/root/usr/share/rpcd/acl.d/luci-openvpnc.json @@ -0,0 +1,22 @@ +{ + "luci-proto-openvpnc": { + "description": "Grant access to LuCI OpenVPN Client procedures", + "read": { + "file": { + "/etc/openvpn/openvpnc": [ "list" ], + "/etc/openvpn/openvpnc/*.ovpn": [ "read" ] + }, + "ubus": { + "file": [ "list", "stat" ] + }, + "uci": [ "network" ] + }, + "write": { + "cgi-io": [ "upload" ], + "file": { + "/etc/openvpn/openvpnc/*.ovpn": [ "write" ] + }, + "uci": [ "network" ] + } + } +}