diff --git a/baidupcs-go/Makefile b/baidupcs-go/Makefile
index 39a026db..bceedbb7 100644
--- a/baidupcs-go/Makefile
+++ b/baidupcs-go/Makefile
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=BaiduPCS-Go
-PKG_VERSION:=4.0.0
-PKG_RELEASE:=1
+PKG_VERSION:=4.0.1
+PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/qjfoidnh/BaiduPCS-Go/tar.gz/v$(PKG_VERSION)?
diff --git a/luci-proto-openvpn/Makefile b/luci-proto-openvpn/Makefile
new file mode 100644
index 00000000..32943be0
--- /dev/null
+++ b/luci-proto-openvpn/Makefile
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2026 OpenWRT
+#
+# This is free software, licensed under the Apache License, Version 2.0 .
+#
+
+include $(TOPDIR)/rules.mk
+
+LUCI_TITLE:=Support for OpenVPN
+LUCI_DEPENDS:=+openvpn
+LUCI_PKGARCH:=all
+
+PKG_LICENSE:=Apache-2.0
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/luci-proto-openvpn/htdocs/luci-static/resources/protocol/openvpn.js b/luci-proto-openvpn/htdocs/luci-static/resources/protocol/openvpn.js
new file mode 100644
index 00000000..da1b698d
--- /dev/null
+++ b/luci-proto-openvpn/htdocs/luci-static/resources/protocol/openvpn.js
@@ -0,0 +1,1882 @@
+// luci-proto-openvpn: OpenVPN protocol handler for LuCI
+'use strict';
+'require form';
+'require fs';
+'require network';
+'require rpc';
+'require uci';
+'require ui';
+
+const callGenKey = rpc.declare({
+ object: 'luci.openvpn',
+ method: 'generateKey',
+ params: { keytype: 'keytype', ifname: 'ifname', server_key: 'server_key', cl_meta: '' },
+});
+
+
+const callGetSKeys = rpc.declare({
+ object: 'luci.openvpn',
+ method: 'getSKeys',
+ params: [ 'ifname' ],
+});
+
+
+const openvpnOptions = [
+ /* Basic options (unchanged) */
+ {
+ tab: 'general',
+ type: form.Value,
+ name: 'port',
+ placeholder: '1194',
+ label: _('TCP/UDP port # for both local and remote')
+ },
+ {
+ tab: 'general',
+ type: form.Flag,
+ name: 'nobind',
+ label: _('Do not bind to local address and port'),
+ default: 0
+ },
+ // --client Options error: specify only one of --tls-server, --tls-client, or --secret
+ // --client also needs DCO(?)
+ {
+ tab: 'general',
+ type: form.Flag,
+ name: 'client',
+ label: _('Configure client mode') + '
' + _('Requires --tls-server, --tls-client, or --secret'),
+ default: 0
+ },
+ {
+ tab: 'general',
+ type: form.DynamicList,
+ name: 'remote',
+ datatype: 'or(host,tuple(host,port),tuple(host,port,string))',
+ label: _('Remote host name or IP address'),
+ placeholder: '1.2.3.4'
+ },
+ {
+ tab: 'general',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'ca',
+ label: _('Certificate authority'),
+ placeholder: '/etc/easy-rsa/keys/ca.crt'
+ },
+ {
+ tab: 'general',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'cert',
+ label: _('Local certificate'),
+ placeholder: '/etc/easy-rsa/keys/some-client.crt'
+ },
+ {
+ tab: 'general',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'key',
+ label: _('Local private key'),
+ placeholder: '/etc/easy-rsa/keys/some-client.key'
+ },
+ {
+ tab: 'general',
+ type: form.Value,
+ name: 'ifconfig',
+ datatype: 'tuple(ipaddr,ipaddr)',
+ placeholder: '10.200.200.3 10.200.200.1',
+ label: _('Set tun/tap adapter parameters')
+ },
+ {
+ tab: 'general',
+ type: form.Value,
+ name: 'ifconfig_ipv6',
+ datatype: 'or(ip6addr,tuple(ip6addr,ip6addr))',
+ placeholder: 'fd15:53b6:dead::2/64 [fd15:53b6:dead::1]',
+ label: _('Set tun/tap adapter parameters')
+ },
+ {
+ tab: 'general',
+ type: form.Value,
+ name: 'server',
+ datatype: 'tuple(ipaddr,ipaddr)',
+ placeholder: '10.200.200.0 255.255.255.0',
+ label: _('Configure server mode')
+ },
+ {
+ tab: 'general',
+ type: form.Value,
+ name: 'server_bridge',
+ datatype: 'tuple(ipaddr,ipaddr,ipaddr,ipaddr)',
+ placeholder: '192.168.1.1 255.255.255.0 192.168.1.128 192.168.1.254',
+ label: _('Configure server bridge')
+ },
+ /*
+ * {
+ * tab: 'general',
+ * type: form.ListValue,
+ * name: 'comp_lzo',
+ * lvalues: [
+ * 'yes',
+ * 'no',
+ * 'adaptive'
+ * ],
+ * label: _('Security recommendation: It is recommended to not enable compression and set this parameter to `no`'),
+ * default: 'no'
+ * },
+ */
+ {
+ tab: 'general',
+ type: form.Value,
+ name: 'keepalive',
+ placeholder: '10 60',
+ label: _('Helper directive to simplify the expression of --ping and --ping-restart in server mode configurations')
+ },
+ {
+ tab: 'general',
+ type: form.Flag,
+ name: 'client_to_client',
+ label: _('Allow client-to-client traffic'),
+ default: 0
+ },
+ // secret requires --cipher *-CBC
+ {
+ tab: 'basic',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'secret',
+ label: _('Enable Static Key encryption mode (non-TLS)') + '¹' + '
' + _('Used with auth and cipher params'),
+ placeholder: '/etc/openvpn/secret.key'
+ },
+ {
+ tab: 'basic',
+ type: form.ListValue,
+ name: 'key_direction',
+ values: [
+ 0,
+ 1
+ ],
+ label: _('The key direction for \'tls-auth\' and \'secret\' options'),
+ default: 0
+ },
+ {
+ tab: 'basic',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'pkcs12',
+ label: _('PKCS#12 file containing keys') + '²',
+ placeholder: '/etc/easy-rsa/keys/some-client.pk12'
+ },
+ {
+ tab: 'basic',
+ type: form.Value,
+ name: 'peer_fingerprint',
+ label: _('The peer key fingerprint'),
+ placeholder: 'AD:B0:95:D8:09:...'
+ },
+
+ /* Devices */
+ {
+ tab: 'devices',
+ type: form.Value,
+ name: 'dev',
+ placeholder: 'tun0',
+ label: _('tun/tap device')
+ },
+ // tun == L3 IPv4/6 tap == L2 Ethernet 802.3
+ {
+ tab: 'devices',
+ type: form.Value,
+ name: 'dev_type',
+ lvalues: [
+ 'tun',
+ 'tap'
+ ],
+ label: _('Type of used device'),
+ default: 'tun'
+ },
+ {
+ tab: 'devices',
+ type: form.Value,
+ name: 'dev_node',
+ label: _('Use tun/tap device node'),
+ placeholder: '/dev/net/tun'
+ },
+ {
+ tab: 'devices',
+ type: form.Flag,
+ name: 'vlan_tagging',
+ label: _('Use VLAN tagging'),
+ default: 0
+ },
+ {
+ tab: 'devices',
+ type: form.ListValue,
+ lvalues: [
+ 'all',
+ 'tagged',
+ 'untagged'
+ ],
+ name: 'vlan_accept',
+ label: _('Accept VLANs')
+ },
+ {
+ tab: 'devices',
+ type: form.Value,
+ datatype: 'and(uinteger,min(1),max(4094))',
+ name: 'vlan_pvid',
+ label: _('Use these PVIDs')
+ },
+
+ /* Service (init/daemon) */
+ {
+ tab: 'service',
+ type: form.Flag,
+ name: 'mlock',
+ label: _('Disable Paging'),
+ default: 0
+ },
+ {
+ tab: 'service',
+ type: form.Flag,
+ name: 'disable_occ',
+ label: _('Disable options consistency check'),
+ default: 0
+ },
+ {
+ tab: 'service',
+ type: form.DirectoryPicker,
+ name: 'cd',
+ root_directory: '/etc/openvpn',
+ label: _('Change to directory before initialization'),
+ placeholder: '/etc/openvpn'
+ },
+ {
+ tab: 'service',
+ type: form.DirectoryPicker,
+ name: 'chroot',
+ root_directory: '/var/run',
+ label: _('Chroot to directory after initialization'),
+ placeholder: '/var/run'
+ },
+ {
+ tab: 'service',
+ type: form.Flag,
+ name: 'passtos',
+ label: _('TOS passthrough (applies to IPv4 only)'),
+ default: 0
+ },
+ {
+ tab: 'service',
+ type: form.Value,
+ name: 'nice',
+ datatype: 'and(integer,min(-20),max(19))',
+ label: _('Change process priority'),
+ placeholder: 0
+ },
+ {
+ tab: 'service',
+ type: form.Flag,
+ name: 'fast_io',
+ label: _('Optimize TUN/TAP/UDP writes'),
+ default: 0
+ },
+ {
+ tab: 'service',
+ type: form.ListValue,
+ name: 'remap_usr1',
+ lvalues: [
+ 'SIGHUP',
+ 'SIGTERM'
+ ],
+ label: _('Remap SIGUSR1 signals')
+ },
+ /*
+ * {
+ * tab: 'service',
+ * type: form.Value,
+ * name: 'status',
+ * placeholder: '/var/run/openvpn.status 5',
+ * label: _('Write status to file every n seconds'),
+ * placeholder: '/var/run/openvpn.status 5'
+ * },
+ */
+ {
+ tab: 'service',
+ type: form.ListValue,
+ name: 'status_version',
+ values: [
+ 1,
+ 2
+ ],
+ label: _('Status file format version'),
+ default: 2
+ },
+ /*
+ * {
+ * tab: 'service',
+ * type: form.ListValue,
+ * name: 'compress',
+ * values: [
+ * 'frames_only',
+ * 'lzo',
+ * 'lz4',
+ * 'stub-v2'
+ * ],
+ * label: _('Security recommendation: It is recommended to not enable compression and set this parameter to `stub-v2`'),
+ * default: 'stub-v2'
+ * },
+ */
+
+ /* Scripts */
+ {
+ tab: 'scripts',
+ type: form.ListValue,
+ name: 'script_security',
+ values: [
+ _('0: Deny'),
+ _('1: OS utils'),
+ _('2: User scripts'),
+ _('3: Allow passwords in env')
+ ],
+ label: _('Policy level over usage of external programs and scripts'),
+ default: 1
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'up',
+ placeholder: '/usr/bin/ovpn-up',
+ label: _('Shell cmd to execute after tun device open')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'up_delay',
+ placeholder: '5',
+ label: _('Delay tun/tap open and up script execution')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'down',
+ placeholder: '/usr/bin/ovpn-down',
+ label: _('Shell cmd to run after tun device close')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Flag,
+ name: 'down_pre',
+ label: _('Call down cmd/script before TUN/TAP close'),
+ default: 0
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Flag,
+ name: 'up_restart',
+ label: _('Run up/down scripts for all restarts'),
+ default: 0
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'route_up',
+ placeholder: '/usr/bin/ovpn-routeup',
+ label: _('Execute shell cmd after routes are added')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'ipchange',
+ placeholder: '/usr/bin/ovpn-ipchange',
+ label: _('Execute shell command on remote IP change')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.DynamicList,
+ name: 'setenv',
+ label: _('Pass environment variables to script'),
+ placeholder: _('name value')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.DynamicList,
+ name: 'setenv_safe',
+ label: _('Pass environment variables to script prepended with OPENVPN_'),
+ placeholder: _('name value')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'tls_verify',
+ placeholder: '/usr/bin/ovpn-tlsverify',
+ label: _('Shell command to verify X509 name')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'client_connect',
+ placeholder: '/usr/bin/ovpn-clientconnect',
+ label: _('Run script cmd on client connection')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'client_crresponse',
+ placeholder: '/usr/bin/ovpn-clientcrresponse',
+ label: _('Run script cmd to validate client certificates')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'client_disconnect',
+ placeholder: '/usr/bin/ovpn-clientdisconnect',
+ label: _('Run script cmd on client disconnection')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'tls_crypt_v2_verify',
+ placeholder: '/usr/bin/ovpn-tlscryptv2verify',
+ label: _('Run script cmd for client TLS verification')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'learn_address',
+ placeholder: '/usr/bin/ovpn-learnaddress',
+ label: _('Executed in server mode whenever an IPv4 address/route or MAC address is added to OpenVPN\'s internal routing table')
+ },
+ {
+ tab: 'scripts',
+ depends: { script_security: /[1-3]/ },
+ type: form.Value,
+ name: 'auth_user_pass_verify',
+ placeholder: '/usr/bin/ovpn-userpass via-env',
+ label: _('Executed in server mode on new client connections, when the client is still untrusted')
+ },
+
+ /* Logging */
+ {
+ tab: 'logging',
+ type: form.Value,
+ name: 'echo',
+ label: _('Echo parameters to log'),
+ placeholder: _('some params echoed to log')
+ },
+ {
+ tab: 'logging',
+ type: form.Value,
+ name: 'log',
+ label: _('Write log to file'),
+ placeholder: '/var/log/openvpn.log'
+ },
+ {
+ tab: 'logging',
+ type: form.Value,
+ name: 'log_append',
+ label: _('Append log to file'),
+ placeholder: '/var/log/openvpn.log'
+ },
+ /*
+ * {
+ * tab: 'logging',
+ * type: form.Value,
+ * name: 'syslog',
+ * placeholder: 'openvpn',
+ * label: _('Syslog tag')
+ * },
+ */
+ {
+ tab: 'logging',
+ type: form.Value,
+ name: 'mute',
+ label: _('Limit repeated log messages'),
+ placeholder: 5
+ },
+ {
+ tab: 'logging',
+ type: form.Flag,
+ name: 'suppress_timestamps',
+ label: _('Don\'t log timestamps'),
+ default: 0
+ },
+ {
+ tab: 'logging',
+ type: form.Value,
+ name: 'verb',
+ datatype: 'and(uinteger,min(0),max(11))',
+ label: _('Set output verbosity'),
+ placeholder: '0-11'
+ },
+
+ /* Networking (socket, device, routing) */
+ {
+ tab: 'networking',
+ type: form.ListValue,
+ name: 'mode',
+ lvalues: [
+ 'p2p',
+ 'server'
+ ],
+ label: _('Major mode')
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'local',
+ datatype: 'or(hostname,ipaddr)',
+ label: _('Local host name or IP address'),
+ placeholder: '0.0.0.0'
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'port',
+ datatype: 'port',
+ label: _('TCP/UDP port # for both local and remote'),
+ placeholder: 1194
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'lport',
+ datatype: 'port',
+ label: _('TCP/UDP port # for local'),
+ placeholder: 1194
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'rport',
+ datatype: 'port',
+ label: _('TCP/UDP port # for remote'),
+ placeholder: 1194
+ },
+ // name: 'proto' collides with netifd 'proto' -> map to ovpnproto
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'ovpnproto',
+ lvalues: [
+ 'udp',
+ 'tcp-client',
+ 'tcp-server'
+ ],
+ label: _('Use protocol'),
+ placeholder: 'udp'
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'float',
+ label: _('Allow remote to change its IP or port'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'nobind',
+ label: _('Do not bind to local address and port'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'multihome',
+ label: _('When you have more than one IP address (e.g. multiple interfaces, or secondary IP addresses), and do not use --local to force binding to one specific address only'),
+ default: 0
+ },
+ /*
+ * {
+ * tab: 'networking',
+ * type: form.Value,
+ * name: 'ifconfig',
+ * label: _('Set tun/tap adapter parameters'),
+ * placeholder: '10.200.200.3 10.200.200.1'
+ * },
+ */
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'ifconfig_noexec',
+ label: _('Don\'t actually execute ifconfig'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'ifconfig_nowarn',
+ label: _('Don\'t warn on ifconfig inconsistencies'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.DynamicList,
+ name: 'route',
+ label: _('Add route after establishing connection'),
+ placeholder: '10.123.0.0 255.255.0.0'
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'route_gateway',
+ datatype: 'ipaddr',
+ label: _('Specify a placeholder gateway for routes'),
+ placeholder: '10.234.1.1'
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'route_delay',
+ datatype: 'uinteger',
+ label: _('Delay n seconds after connection'),
+ placeholder: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'route_noexec',
+ label: _('Don\'t add routes automatically'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'route_nopull',
+ label: _('Don\'t pull routes automatically'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'allow_recursive_routing',
+ label: _('Don\'t drop incoming tun packets with same destination as host'),
+ placeholder: 0
+ },
+ {
+ tab: 'networking',
+ type: form.ListValue,
+ name: 'mtu_disc',
+ lvalues: [
+ 'yes',
+ 'maybe',
+ 'no'
+ ],
+ label: _('Enable Path MTU discovery'),
+ default: 'no'
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'mtu_test',
+ label: _('Empirically measure MTU'),
+ default: 0
+ },
+ /*
+ * {
+ * tab: 'networking',
+ * type: form.ListValue,
+ * name: 'comp_lzo',
+ * lvalues: [
+ * 'yes',
+ * 'no',
+ * 'adaptive'
+ * ],
+ * label: _('Security recommendation: It is recommended to not enable compression and set this parameter to `no`') + '¹',
+ * default: 'no'
+ * },
+ */
+ /* {
+ * tab: 'networking',
+ * type: form.Flag,
+ * name: 'comp_noadapt',
+ * label: _('Don\'t use adaptive lzo compression') + '¹',
+ * placeholder: 0
+ * },
+ */
+ /* {
+ * tab: 'networking',
+ * type: form.Value,
+ * name: 'link_mtu',
+ * datatype: 'uinteger',
+ * label: _('Set TCP/UDP MTU') + '¹',
+ * placeholder: 1500
+ * },
+ */
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'tun_mtu',
+ datatype: 'uinteger',
+ label: _('Set tun/tap device MTU'),
+ placeholder: 1500
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'tun_mtu_extra',
+ datatype: 'uinteger',
+ label: _('Set tun/tap device overhead'),
+ placeholder: 1500
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'fragment',
+ datatype: 'uinteger',
+ label: _('Enable internal datagram fragmentation'),
+ placeholder: 1500
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'mssfix',
+ datatype: 'uinteger',
+ label: _('Set upper bound on TCP MSS'),
+ placeholder: 1450
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'sndbuf',
+ datatype: 'uinteger',
+ label: _('Set the TCP/UDP send buffer size'),
+ placeholder: 65536
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'rcvbuf',
+ datatype: 'uinteger',
+ label: _('Set the TCP/UDP receive buffer size'),
+ placeholder: 65536
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'txqueuelen',
+ datatype: 'uinteger',
+ label: _('Set tun/tap TX queue length'),
+ placeholder: 100
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'shaper',
+ datatype: 'uinteger',
+ label: _('Shaping for peer bandwidth'),
+ placeholder: 10240
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'inactive',
+ datatype: 'uinteger',
+ label: _('tun/tap inactivity timeout'),
+ placeholder: 240
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'keepalive',
+ label: _('Helper directive to simplify the expression of --ping and --ping-restart in server mode configurations'),
+ placeholder: '10 60'
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'ping',
+ datatype: 'uinteger',
+ label: _('Ping remote every n seconds over TCP/UDP port'),
+ placeholder: 30
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'ping_exit',
+ datatype: 'uinteger',
+ label: _('Remote ping timeout'),
+ placeholder: 120
+ },
+ {
+ tab: 'networking',
+ type: form.Value,
+ name: 'ping_restart',
+ datatype: 'uinteger',
+ label: _('Restart after remote ping timeout'),
+ placeholder: 60
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'ping_timer_rem',
+ label: _('Only process ping timeouts if routes exist'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'persist_tun',
+ label: _('Keep tun/tap device open on restart'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'persist_key',
+ label: _('Don\'t re-read key on restart'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'persist_local_ip',
+ label: _('Keep local IP address on restart'),
+ default: 0
+ },
+ {
+ tab: 'networking',
+ type: form.Flag,
+ name: 'persist_remote_ip',
+ label: _('Keep remote IP address on restart'),
+ default: 0
+ },
+
+ /* Management */
+ {
+ tab: 'management',
+ type: form.Value,
+ name: 'management',
+ label: _('Enable management interface on IP port'),
+ placeholder: '127.0.0.1 31194 /etc/openvpn/mngmt-pwds'
+ },
+ {
+ tab: 'management',
+ type: form.Flag,
+ name: 'management_query_passwords',
+ label: _('Query management channel for private key'),
+ default: 0
+ },
+ {
+ tab: 'management',
+ type: form.Flag,
+ name: 'management_hold',
+ label: _('Start OpenVPN in a hibernating state'),
+ default: 0
+ },
+ {
+ tab: 'management',
+ type: form.Value,
+ name: 'management_log_cache',
+ datatype: 'uinteger',
+ label: _('Number of lines for log file history'),
+ placeholder: 100
+ },
+ {
+ tab: 'management',
+ type: form.Value,
+ name: 'management_external_cert',
+ datatype: 'file',
+ label: _('Management cert'),
+ placeholder: 'certificate-hint'
+ },
+ {
+ tab: 'management',
+ type: form.Value,
+ name: 'management_external_key',
+ datatype: 'file',
+ label: _('Management key'),
+ placeholder: 'nopadding pkcs1'
+ },
+
+ /* Topology */
+ {
+ tab: 'topology',
+ type: form.ListValue,
+ name: 'topology',
+ lvalues: [
+ 'net30',
+ 'p2p',
+ 'subnet'
+ ],
+ label: _('\'net30\', \'p2p\', or \'subnet\''),
+ default: ''
+ },
+ {
+ tab: 'topology',
+ type: form.Flag,
+ name: 'disable_dco',
+ label: _('Disable Data Channel Offloading (DCO) support'),
+ default: 0
+ },
+
+ /* Crypto */
+ {
+ tab: 'cryptography',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'dh',
+ label: _('Diffie-Hellman parameters'),
+ placeholder: '/etc/easy-rsa/keys/dh1024.pem'
+ },
+ {
+ tab: 'cryptography',
+ type: form.DirectoryPicker,
+ root_directory: '/etc',
+ name: 'capath',
+ label: _('CA path') + '²'
+ },
+ {
+ tab: 'cryptography',
+ type: form.FileUpload,
+ root_directory: '/etc',
+ name: 'askpass_file',
+ label: _('Get certificate password from file before we daemonize')
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'auth',
+ label: _('HMAC authentication for packets'),
+ lvalues: [
+ 'SHA1',
+ 'SHA224',
+ 'SHA256',
+ 'SHA384',
+ 'SHA512',
+ 'SHA3-224',
+ 'SHA3-256',
+ 'SHA3-384',
+ 'SHA3-512',
+ 'none'
+ ]
+ },
+ // --data-ciphers-fallback with cipher 'AES-256-CBC' disables data channel offload.
+ // for --secret pre-shared-key mode. Ignored in >= 2.6 (TLS mode).
+ { tab: 'cryptography',
+ type: form.Value,
+ name: 'cipher',
+ label: _('Encryption cipher for packets') + '¹',
+ lvalues: [
+ 'AES-128-CBC',
+ 'AES-128-GCM',
+ 'AES-192-CBC',
+ 'AES-192-GCM',
+ 'AES-256-CBC',
+ 'AES-256-GCM',
+ 'CHACHA20-POLY1305'
+ ]
+ },
+ /*
+ * {
+ * tab: 'cryptography',
+ * type: form.Value,
+ * name: 'keysize',
+ * label: _('Size of cipher key'),
+ * placeholder: 1024
+ * },
+ */
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'engine',
+ label: _('Enable OpenSSL hardware crypto engines') + '²',
+ lvalues: [
+ 'dynamic'
+ ]
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'ecdh_curve',
+ label: _('Specify the curve to use for ECDH') + '²',
+ lvalues: [
+ 'x25519',
+ 'secp521r1',
+ 'secp384r1',
+ 'secp256r1',
+ 'secp256k1'
+ ]
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'replay_window',
+ label: _('Replay protection sliding window size'),
+ placeholder: '64 15'
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'replay_window',
+ label: _('Replay protection sliding window size'),
+ placeholder: '64 15'
+ },
+ {
+ tab: 'cryptography',
+ type: form.Flag,
+ name: 'mute_replay_warnings',
+ label: _('Silence the output of replay warnings'),
+ default: 0
+ },
+ {
+ tab: 'cryptography',
+ type: form.FileUpload,
+ root_directory: '/var/run/',
+ name: 'replay_persist',
+ label: _('Persist replay-protection state'),
+ placeholder: '/var/run/openvpn-replay-state'
+ },
+ {
+ tab: 'cryptography',
+ type: form.Flag,
+ name: 'tls_server',
+ label: _('Enable TLS and assume server role'),
+ default: 0
+ }, // depends: { tls_client: 0}
+ {
+ tab: 'cryptography',
+ type: form.Flag,
+ name: 'tls_client',
+ label: _('Enable TLS and assume client role'),
+ default: 0
+ }, // depends: { tls_server: 0}
+ /*
+ * {
+ * tab: 'cryptography',
+ * type: form.Value,
+ * name: 'key_method',
+ * label: _('Enable TLS and assume client role')
+ * },
+ */
+ {
+ tab: 'cryptography',
+ type: form.DynamicList,
+ name: 'tls_cipher',
+ label: _('TLS cipher'),
+ lvalues: [
+ 'TLS1-3-CHACHA20-POLY1305-SHA256',
+ 'TLS1-3-AES-256-GCM-SHA384',
+ 'TLS1-3-AES-128-GCM-SHA256',
+ 'TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384',
+ 'TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384',
+ 'TLS-DHE-RSA-WITH-AES-256-GCM-SHA384',
+ 'TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256',
+ 'TLS-ECDHE-RSA-WITH-CHACHA20-POLY1305-SHA256',
+ 'TLS-DHE-RSA-WITH-CHACHA20-POLY1305-SHA256',
+ 'TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256',
+ 'TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256',
+ 'TLS-DHE-RSA-WITH-AES-128-GCM-SHA256',
+ 'TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA384',
+ 'TLS-ECDHE-RSA-WITH-AES-256-CBC-SHA384',
+ 'TLS-DHE-RSA-WITH-AES-256-CBC-SHA256',
+ 'TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA256',
+ 'TLS-ECDHE-RSA-WITH-AES-128-CBC-SHA256',
+ 'TLS-DHE-RSA-WITH-AES-128-CBC-SHA256',
+ 'TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA',
+ 'TLS-ECDHE-RSA-WITH-AES-256-CBC-SHA',
+ 'TLS-DHE-RSA-WITH-AES-256-CBC-SHA',
+ 'TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA',
+ 'TLS-ECDHE-RSA-WITH-AES-128-CBC-SHA',
+ 'TLS-DHE-RSA-WITH-AES-128-CBC-SHA',
+ 'TLS-ECDHE-PSK-WITH-CHACHA20-POLY1305-SHA256',
+ 'TLS-ECDHE-PSK-WITH-AES-256-CBC-SHA384',
+ 'TLS-ECDHE-PSK-WITH-AES-256-CBC-SHA',
+ 'TLS-ECDHE-PSK-WITH-AES-128-CBC-SHA256',
+ 'TLS-ECDHE-PSK-WITH-AES-128-CBC-SHA',
+ 'TLS-PSK-WITH-CHACHA20-POLY1305-SHA256',
+ 'TLS-PSK-WITH-AES-256-GCM-SHA384',
+ 'TLS-PSK-WITH-AES-256-CBC-SHA384',
+ 'TLS-PSK-WITH-AES-256-CBC-SHA',
+ 'TLS-PSK-WITH-AES-128-GCM-SHA256',
+ 'TLS-PSK-WITH-AES-128-CBC-SHA256',
+ 'TLS-PSK-WITH-AES-128-CBC-SHA'
+ ]
+ },
+ {
+ tab: 'cryptography',
+ type: form.DynamicList,
+ name: 'tls_ciphersuites',
+ label: _('TLS 1.3 or newer cipher'),
+ lvalues: [
+ 'TLS_AES_256_GCM_SHA384',
+ 'TLS_AES_128_GCM_SHA256',
+ 'TLS_CHACHA20_POLY1305_SHA256'
+ ]
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'tls_timeout',
+ datatype: 'uinteger',
+ label: _('Retransmit timeout on TLS control channel'),
+ placeholder: 2
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'reneg_bytes',
+ datatype: 'uinteger',
+ label: _('Renegotiate data chan. key after bytes'),
+ placeholder: 1024
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'reneg_pkts',
+ datatype: 'uinteger',
+ label: _('Renegotiate data chan. key after packets'),
+ placeholder: 100
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'reneg_sec',
+ datatype: 'uinteger',
+ label: _('Renegotiate data chan. key after seconds'),
+ placeholder: 3600
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'hand_window',
+ datatype: 'uinteger',
+ label: _('Timeframe for key exchange'),
+ placeholder: 60
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'tran_window',
+ datatype: 'uinteger',
+ label: _('Key transition window'),
+ placeholder: 3600
+ },
+ {
+ tab: 'cryptography',
+ type: form.Flag,
+ name: 'single_session',
+ datatype: 'uinteger',
+ label: _('Allow only one session'),
+ default: 0
+ },
+ {
+ tab: 'cryptography',
+ type: form.Flag,
+ name: 'tls_exit',
+ datatype: 'uinteger',
+ label: _('Exit on TLS negotiation failure'),
+ default: 0
+ },
+ {
+ tab: 'cryptography',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'tls_auth', label: _('Additional authentication over TLS'),
+ placeholder: '/etc/openvpn/tlsauth.key'
+ },
+ {
+ tab: 'cryptography',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'tls_crypt',
+ label: _('Encrypt and authenticate all control channel packets with the key'),
+ placeholder: '/etc/openvpn/tlscrypt.key'
+ },
+ {
+ tab: 'cryptography',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'tls_crypt_v2',
+ label: _('Encrypt and authenticate all control channel packets with the key, version 2.'),
+ placeholder: '/etc/openvpn/servertlscryptv2.key'
+ },
+ {
+ tab: 'cryptography',
+ type: form.Flag,
+ name: 'auth_nocache',
+ label: _('Don\'t cache --askpass or --auth-user-pass passwords'),
+ default: 0
+ },
+ /*
+ * {
+ * tab: 'cryptography',
+ * type: form.Value,
+ * name: 'tls_remote',
+ * label: _('Only accept connections from given X509 name'),
+ * placeholder: 'remote_x509_name'
+ * },
+ */
+ {
+ tab: 'cryptography',
+ type: form.ListValue,
+ name: 'ns_cert_type',
+ lvalues: [
+ 'client',
+ 'server'
+ ],
+ label: _('Require explicit designation on certificate')
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'remote_cert_eku',
+ label: _('Require remote cert extended key usage on certificate'),
+ placeholder: 'oid'
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'remote_cert_ku',
+ label: _('Require explicit key usage on certificate'),
+ placeholder: 'a0'
+ },
+ {
+ tab: 'cryptography',
+ type: form.ListValue,
+ name: 'remote_cert_tls',
+ lvalues: [
+ 'client',
+ 'server'
+ ],
+ label: _('Require explicit key usage on certificate')
+ },
+ {
+ tab: 'cryptography',
+ type: form.FileUpload,
+ root_directory: '/etc/openvpn',
+ name: 'crl_verify',
+ label: _('Check peer certificate against a CRL'),
+ placeholder: '/etc/easy-rsa/keys/crl.pem'
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'tls_version_min',
+ label: _('The lowest supported TLS version'),
+ lvalues: [
+ '1.2',
+ '1.3'
+ ]
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'tls_version_max',
+ label: _('The highest supported TLS version'),
+ lvalues: [
+ '1.2',
+ '1.3'
+ ]
+ },
+ {
+ tab: 'cryptography',
+ type: form.Value,
+ name: 'tls_cert_profile',
+ label: _('TLS cet profile'),
+ lvalues: [
+ 'insecure',
+ 'legacy',
+ 'preferred',
+ 'suiteb'
+ ]
+ },
+ /*
+ * {
+ * tab: 'cryptography',
+ * type: form.Flag,
+ * name: 'ncp_disable',
+ * label: _('This completely disables cipher negotiation'),
+ * default: 0
+ * },
+ */
+ /*
+ * {
+ * tab: 'cryptography',
+ * type: form.DynamicList,
+ * name: 'ncp_ciphers',
+ * label: _('Restrict the allowed ciphers to be negotiated'),
+ * lvalues: [
+ * 'AES-256-GCM',
+ * 'AES-128-GCM'
+ * ]
+ * },
+ */
+ {
+ tab: 'cryptography',
+ type: form.DynamicList,
+ name: 'data_ciphers',
+ label: _('Restrict the allowed ciphers to be negotiated'),
+ lvalues: [
+ 'CHACHA20-POLY1305',
+ 'AES-256-GCM',
+ 'AES-128-GCM',
+ 'AES-256-CBC'
+ ]
+ },
+
+ /* Push/Client */
+ {
+ tab: 'push_opt',
+ type: form.DynamicList,
+ name: 'push',
+ label: _('Push options to peer'),
+ lvalues: ['redirect-gateway']
+ },
+ {
+ tab: 'push_opt',
+ type: form.DynamicList,
+ name: 'push_remove',
+ label: _('Remove Push options'),
+ },
+ {
+ tab: 'push_opt',
+ type: form.Flag,
+ name: 'push_reset',
+ label: _('Don\'t inherit global push options'),
+ default: 0
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Flag,
+ name: 'disable',
+ label: _('Client is disabled'),
+ default: 0
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'ifconfig_pool',
+ label: _('Set aside a pool of subnets')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'ifconfig_pool_persist',
+ label: _('Persist/unpersist ifconfig-pool')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'ifconfig_push',
+ label: _('Push an ifconfig option to remote')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'iroute',
+ label: _('Route subnet to client')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'iroute_ipv6',
+ label: _('Route v6 subnet to client')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Flag,
+ name: 'duplicate_cn',
+ label: _('Allow multiple clients with same certificate'),
+ default: 0
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.DirectoryPicker,
+ root_directory: '/etc/openvpn',
+ directory_create: true,
+ name: 'client_config_dir',
+ label: _('Directory for custom client config files')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Flag,
+ name: 'ccd_exclusive',
+ label: _('Refuse connection if no custom client config'),
+ default: 0
+ },
+ /*
+ * {
+ * tab: 'push_opt',
+ * depends: { server: "", "!reverse": true },
+ * type: form.DirectoryPicker,
+ * root_directory: '/var/run',
+ * directory_create: true,
+ * name: 'tmp_dir',
+ * label: _('Temporary directory for client-connect return file')
+ * },
+ */
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'hash_size',
+ label: _('Set size of real and virtual address hash tables')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'bcast_buffers',
+ label: _('Number of allocated broadcast buffers')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'tcp_queue_limit',
+ label: _('Maximum number of queued TCP output packets')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'max_clients',
+ label: _('Allowed maximum of connected clients')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'max_routes_per_client',
+ label: _('Allowed maximum of internal')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'connect_freq',
+ label: _('Allowed maximum of new connections')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Flag,
+ name: 'username_as_common_name',
+ label: _('Use username as common name'),
+ default: 0
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Flag,
+ name: 'pull',
+ label: _('Accept options pushed from server'),
+ default: 0
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'auth_user_pass',
+ label: _('Authenticate using username/password')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'auth_retry',
+ label: _('Handling of authentication failures')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'explicit_exit_notify',
+ label: _('Send notification to peer on disconnect')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Flag,
+ name: 'remote_random',
+ label: _('Randomly choose remote server'),
+ default: 0
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'connect_retry',
+ label: _('Connection retry interval')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'http_proxy',
+ label: _('Connect to remote host through an HTTP proxy')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Flag,
+ name: 'http_proxy_retry',
+ label: _('Retry indefinitely on HTTP proxy errors'),
+ default: 0
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'http_proxy_timeout',
+ label: _('Proxy timeout in seconds')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.DynamicList,
+ name: 'http_proxy_option',
+ label: _('Set extended HTTP proxy options')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'socks_proxy',
+ label: _('Connect through Socks5 proxy')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'socks_proxy_retry',
+ label: _('Retry indefinitely on Socks proxy errors')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'resolv_retry',
+ label: _('If hostname resolve fails, retry')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'redirect_gateway',
+ label: _('Automatically redirect default route')
+ },
+ {
+ tab: 'push_opt',
+ depends: { server: "", "!reverse": true },
+ type: form.Value,
+ name: 'verify_client_cert',
+ label: _('Specify whether the client is required to supply a valid certificate')
+ },
+
+];
+
+// Tabs for UI
+var tabs = [
+ { id: 'basic', label: _('Basic Settings') },
+ { id: 'file', label: _('Config File') },
+ { id: 'general' },
+ { id: 'cryptography', label: _('Cryptography') },
+ { id: 'devices', label: _('Devices') },
+ { id: 'expert', label: _('Expert Settings') },
+ { id: 'keygen', label: _('Keygen') },
+ { id: 'logging', label: _('Logging') },
+ { id: 'management', label: _('Management') },
+ { id: 'networking', label: _('Networking') },
+ { id: 'push_opt', label: _('Push') },
+ { id: 'scripts', label: _('Scripting') },
+ { id: 'service', label: _('Service') },
+ { id: 'topology', label: _('Topology') },
+];
+
+function renderOpenVPNOptions(s, tabId) {
+
+ openvpnOptions.filter(function(opt) { return opt.tab === tabId; }).forEach(function(opt) {
+ let o = s.taboption(tabId, opt.type, opt.name, opt.name, opt.label);
+ if (opt.values) {
+ // index based values (o.value(i, string))
+ opt.values.forEach((v, i) => o.value(i, v));
+ } else if (opt.lvalues) {
+ // literal values: the text string is the value
+ opt.lvalues.forEach((v) => o.value(v));
+ }
+ // Copy any extra properties to the widget
+ Object.keys(opt).forEach(function(key) {
+ if (!['tab', 'type', 'name', 'label', 'values', 'lvalues'].includes(key)) {
+ if (key === 'depends')
+ o.depends(opt[key])
+ else
+ o[key] = opt[key];
+ }
+ });
+ o.optional = true;
+ });
+}
+
+network.registerPatternVirtual(/^openvpn-.+$/);
+
+return network.registerProtocol('openvpn', {
+ getI18n: function() {
+ return _('OpenVPN');
+ },
+
+ getIfname: function() {
+ return this._ubus('l3_device') || this.sid;
+ },
+
+ getPackageName: function() {
+ return 'openvpn-openssl';
+ },
+
+ isFloating: function() {
+ return true;
+ },
+
+ isVirtual: function() {
+ return true;
+ },
+
+ getDevices: function() {
+ return null;
+ },
+
+ containsDevice: function(ifname) {
+ return (network.getIfnameOf(ifname) == this.getIfname());
+ },
+
+ renderFormOptions: function(s) {
+
+ let o, kt, sk, krp, krc, clm, ovconf;
+ // Add main tabs
+ tabs.forEach(function(tab) {
+ if (!s.tabs[tab.id])
+ s.tab(tab.id, tab.label);
+ });
+
+ o = s.taboption('general', form.DummyValue, '_dummy');
+ o.rawhtml = true;
+ o.default = _('Almost nothing here prevents you from selecting invalid configuration options which prevent openvpn from starting. Read the manual.') + '
' +
+ _('Options marked with ¹ are deprecated and will be removed.') /* + '
' +
+ _('Options marked with * are server only.') */ + '
' +
+ _('Options marked with ² are OpenSSL only.');
+
+ // Render options for each tab
+ tabs.forEach(function(tab) {
+ renderOpenVPNOptions(s, tab.id);
+ });
+
+ kt = s.taboption('keygen', form.ListValue, '_keytype', _('Type'));
+ kt.default = 'secret';
+ kt.value('secret');
+ kt.value('tls-crypt');
+ kt.value('tls-auth');
+ kt.value('auth-token');
+ kt.value('tls-crypt-v2-server');
+ kt.value('tls-crypt-v2-client');
+ kt.write = function(section_id, value) {};
+
+ sk = s.taboption('keygen', form.Value, '_skey', _('Server key'));
+ callGetSKeys(s.map.section).then(keys => {
+ if (keys.skeys) {
+ for (let key of keys.skeys)
+ sk.value(key);
+ }
+ });
+
+ clm = s.taboption('keygen', form.TextValue, '_cmeta', _('Client metadata'),
+ _('Freeform metadata to embed into the client key'));
+ clm.depends('_keytype', 'tls-crypt-v2-client');
+ clm.default = '';
+ clm.placeholder = '{"cn":"alice","exp":1735689600}';
+ clm.monospace = true;
+ clm.rows = 10;
+ clm.wrap = 90;
+
+ o = s.taboption('keygen', form.Button, '_keygen', _('Generate'));
+ o.onclick = L.bind(function(ev, sid) {
+ const ktype = kt.formvalue(sid);
+ const svk = sk.formvalue(sid);
+ const clmeta = clm.formvalue(sid);
+
+ callGenKey({ ifname: sid, keytype: ktype, server_key: svk ?? '', cl_meta: btoa(clmeta) }).then(result => {
+ const path_output = krp.getUIElement(sid);
+ const cont_output = krc.getUIElement(sid);
+ path_output.setValue(result.path);
+ cont_output.setValue(result.content);
+ if (ktype === 'tls-crypt-v2-server') {
+ const skv = sk.getUIElement(sid);
+ skv.setValue(result.filename);
+ }
+ });
+ }, this);
+
+ krp = s.taboption('keygen', form.TextValue, '_pathresult', _('Path'));
+ krp.default = '';
+ krp.monospace = true;
+ krp.readonly = true;
+ krp.readonly = true;
+ krp.rows = 2;
+ krp.wrap = 90;
+ krp.write = function(sid, value) { return; };
+
+ krc = s.taboption('keygen', form.TextValue, '_contentresult');
+ krc.default = '';
+ krc.monospace = true;
+ krc.readonly = true;
+ krc.rows = 10;
+ krc.wrap = 90;
+ krc.write = function(sid, value) { return; };
+
+ o = s.taboption('file', form.Button, '_clear', _('Clear'));
+ o.onclick = L.bind(function(ev, sid) {
+ const config_name = `/etc/openvpn/${sid}/${sid}_config.cfg`;
+ let v = ovconf.getUIElement(sid);
+ // remove file, clear field, mark it changed and save
+ fs.remove(config_name).catch(() => {}).then(() => {
+ try {
+ v.setValue('');
+ if (v && v.node)
+ v.node.dispatchEvent(new Event('change', { bubbles: true }));
+ } catch (e) {}
+ uci.unset(this.config, sid, 'config');
+ if (s && s.map)
+ s.map.save(null, true);
+ });
+ }, this);
+
+ ovconf = s.taboption('file', form.TextValue, 'ovpn_config', _('Raw OVPN config'));
+ ovconf.rows = 20;
+ ovconf.optional = true;
+ ovconf.placeholder = _('Drag and drop an ovpn config file here');
+
+ // Attach drag-and-drop handler for file import
+ function attachDragDrop(sid) {
+
+ try {
+ // If the map root is not yet available, retry a few times with back-off
+ ovconf._attach_attempts = ovconf._attach_attempts || {};
+ ovconf._attach_attempts[sid] = (ovconf._attach_attempts[sid] || 0) + 1;
+ if (!s || !s.map || !s.map.root) {
+ if (ovconf._attach_attempts[sid] < 10) {
+ setTimeout(() => attachDragDrop(sid), 1000);
+ }
+ return;
+ }
+
+ const w = ovconf.getUIElement(sid);
+ if (!w || !w.node || w._ovpn_drop_attached) {
+ if (ovconf._attach_attempts[sid] < 10)
+ setTimeout(() => attachDragDrop(sid), 1000);
+ return;
+ }
+ const ta = w.node.firstElementChild;
+ if (!ta) {
+ return;
+ }
+
+ ta.addEventListener('dragover', ev => {
+ ev.preventDefault();
+ ev.dataTransfer.dropEffect = 'copy';
+ ta.classList.add('cbi-dragover');
+ });
+
+ ta.addEventListener('dragleave', ev => {
+ ev.preventDefault();
+ ta.classList.remove('cbi-dragover');
+ });
+
+ ta.addEventListener('drop', ev => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ ta.classList.remove('cbi-dragover');
+
+ const files = ev.dataTransfer && (ev.dataTransfer.files || ev.dataTransfer.items);
+
+ let file = files && files[0];
+ if (files && files[0] && files[0].kind === 'file' && files[0].getAsFile)
+ file = files[0].getAsFile();
+ if (!file) {
+ // fallback: try plain text data
+ const text = ev.dataTransfer && (ev.dataTransfer.getData && (ev.dataTransfer.getData('text') || ev.dataTransfer.getData('text/plain')));
+
+ if (text) {
+ w.setValue(text);
+ w.node.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = function() {
+ const content = reader.result;
+
+ w.setValue(content);
+ w.node.dispatchEvent(new Event('change', { bubbles: true }));
+ };
+ reader.onerror = function(err) {
+ console.error('ovpn file read error', err);
+ };
+ reader.readAsText(file);
+ });
+
+ w._ovpn_drop_attached = true;
+
+ } catch (e) { }
+ }
+
+ ovconf.cfgvalue = function(sid) {
+ attachDragDrop(sid);
+ const config_name = `/etc/openvpn/${sid}/${sid}_config.cfg`;
+ return fs.read(config_name).then(readresult => {
+ attachDragDrop(sid);
+ if (readresult == null || readresult === '') {
+ uci.unset(this.config, sid, 'config');
+ return '';
+ }
+ uci.set(this.config, sid, 'config', config_name);
+ return readresult;
+ }).catch(() => {
+ attachDragDrop(sid);
+ uci.unset(this.config, sid, 'config');
+ return '';
+ });
+ };
+ ovconf.write = function(sid, value) {
+ const config_name = `/etc/openvpn/${sid}/${sid}_config.cfg`;
+ return fs.exec('/bin/mkdir', ['-p', `/etc/openvpn/${sid}/`]).then(() => {
+ return fs.write(config_name, value);
+ }).then(() => {
+ try {
+ uci.set(this.config, sid, 'config', config_name);
+ } catch (err) {}
+ });
+ };
+ ovconf.rmempty = true;
+ },
+
+ deleteConfiguration: function() {
+ uci.sections('network', 'openvpn_%s'.format(this.sid), function(s) {
+ uci.remove('network', s['.name']);
+ });
+ }
+});
diff --git a/luci-proto-openvpn/root/usr/share/rpcd/acl.d/luci-proto-openvpn.json b/luci-proto-openvpn/root/usr/share/rpcd/acl.d/luci-proto-openvpn.json
new file mode 100644
index 00000000..af4ea82b
--- /dev/null
+++ b/luci-proto-openvpn/root/usr/share/rpcd/acl.d/luci-proto-openvpn.json
@@ -0,0 +1,28 @@
+{
+ "luci-proto-openvpn": {
+ "description": "Grant access to LuCI openvpn procedures",
+ "read": {
+ "file": {
+ "/bin/mkdir -p /etc/openvpn/*": [ "exec" ],
+ "/etc/openvpn/*": [ "read" ]
+ },
+ "ubus": {
+ "luci.openvpn": [
+ "*"
+ ]
+ },
+ "uci": [ "network" ]
+ },
+ "write": {
+ "file": {
+ "/etc/openvpn/*": [ "write", "remove" ]
+ },
+ "ubus": {
+ "luci.openvpn": [
+ "*"
+ ]
+ },
+ "uci": [ "network" ]
+ }
+ }
+}
diff --git a/luci-proto-openvpn/root/usr/share/rpcd/ucode/luci.openvpn.uc b/luci-proto-openvpn/root/usr/share/rpcd/ucode/luci.openvpn.uc
new file mode 100644
index 00000000..da389170
--- /dev/null
+++ b/luci-proto-openvpn/root/usr/share/rpcd/ucode/luci.openvpn.uc
@@ -0,0 +1,104 @@
+#!/usr/bin/env ucode
+
+'use strict';
+
+import { stdin, access, chmod, dirname, basename, open, popen, glob, lsdir, readfile, readlink, error, mkdir } from 'fs';
+import { cursor } from 'uci';
+import { connect } from 'ubus';
+
+
+const openvpn_dir = '/etc/openvpn';
+
+function shellquote(s) {
+ return `'${replace(s, "'", "'\\''")}'`;
+}
+
+function command(cmd) {
+ return trim(popen(cmd)?.read?.('all'));
+}
+
+function makedirs(path) {
+ const parts = split(path, '/');
+ const grow = [];
+ for(let i = 0; i < length(parts); i++) {
+ push(grow, parts[i]);
+ mkdir(join('/', grow));
+ }
+ return join('/', grow);
+}
+
+function keyDir(ifname, kt) {
+ return `${openvpn_dir}/${ifname}/${kt}`;
+}
+
+const methods = {
+ generateKey: {
+ args: { keytype: 'keytype', ifname: 'ifname', server_key: 'server_key', cl_meta: 'c_metadata' },
+ call: function(req) {
+ const kt = req.args?.keytype;
+ const ifname = req.args?.ifname || 'unnamed';
+ const ts = time();
+
+ if (!kt) return { error: 'missing keytype' };
+
+ let dir;
+ let outfile = `${ifname}_${kt}_${ts}.key`;
+
+ let mkpath = makedirs(`${keyDir(ifname, kt)}`);
+ let path = `${mkpath}/${outfile}`;
+
+ // find openvpn binary
+ const openvpn = trim(popen('command -v openvpn 2>/dev/null')?.read?.('all'));
+ if (!length(openvpn)) return { error: 'openvpn binary not found' };
+
+ let cmd;
+ if (kt == 'tls-crypt-v2-client') {
+ // client generation needs server key
+ let server_key = req.args?.server_key;
+ if (!server_key) {
+ // try to pick latest server key for same interface
+ const serverDir = `${keyDir(ifname, 'tls-crypt-v2-server')}`;
+ const list = lsdir(serverDir);
+ if (length(list) > 0) server_key = serverDir + '/' + list[-1];
+ } else {
+ server_key = `${keyDir(ifname, 'tls-crypt-v2-server')}/${req.args?.server_key}`;
+ }
+
+ if (!server_key) return { error: 'missing server_key for tls-crypt-v2-client' };
+
+ // denote which server key this client key is derived from in the name
+ path = `${mkpath}/${ifname}_${kt}_${ts}-${req.args?.server_key}`;
+ cmd = `${openvpn} --tls-crypt-v2 ${shellquote(server_key)} --genkey tls-crypt-v2-client ${shellquote(path)} ${req.args?.cl_meta} 2>/dev/null`;
+ } else {
+ // basic genkey
+ cmd = `${openvpn} --genkey ${kt} ${shellquote(path)} 2>/dev/null`;
+ }
+
+ const out = popen(cmd)?.read?.('all') || '';
+
+ // ensure permissions
+ chmod(path, 0o600);
+
+ let content = '';
+ try { content = readfile(path); } catch (e) { /* ignore */ }
+
+ if (!length(content)) {
+ return { error: 'failed to generate key', cmd_output: out };
+ }
+
+ return { path, filename: outfile, content };
+ }
+ },
+
+ getSKeys: {
+ args: { ifname: 'ifname' },
+ call: function(req) {
+ const serverDir = `${keyDir(req.args?.ifname, 'tls-crypt-v2-server')}`;
+ const list = lsdir(serverDir);
+
+ return { skeys: list, path: serverDir };
+ }
+ }
+};
+
+return { 'luci.openvpn': methods };