1 local readmeURL = "https://github.com/openwrt/packages/tree/master/net/vpn-policy-routing/files/README.md"
3 local packageName = "vpn-policy-routing"
4 local uci = require "luci.model.uci".cursor()
5 local sys = require "luci.sys"
6 local util = require "luci.util"
7 local ip = require "luci.ip"
8 local fs = require "nixio.fs"
9 local jsonc = require "luci.jsonc"
10 local http = require "luci.http"
11 local nutil = require "nixio.util"
12 local dispatcher = require "luci.dispatcher"
13 local enabledFlag = uci:get(packageName, "config", "enabled")
16 local ubusStatus = util.ubus("service", "list", { name = packageName })
17 if ubusStatus and ubusStatus[packageName] and
18 ubusStatus[packageName]["instances"] and
19 ubusStatus[packageName]["instances"]["main"] and
20 ubusStatus[packageName]["instances"]["main"]["data"] and
21 ubusStatus[packageName]["instances"]["main"]["data"]["status"] and
22 ubusStatus[packageName]["instances"]["main"]["data"]["status"][1] then
23 serviceGateways = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["gateway"]
24 serviceGateways = serviceGateways and serviceGateways:gsub('\\n', '\n')
25 serviceGateways = serviceGateways and serviceGateways:gsub('\\033%[0;32m%[\\xe2\\x9c\\x93%]\\033%[0m', '✓')
26 serviceErrors = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["error"]
27 serviceErrors = serviceErrors and serviceErrors:gsub('\\n', '\n')
28 serviceErrors = serviceErrors and serviceErrors:gsub('\\033%[0;31mERROR\\033%[0m: ', '')
29 serviceWarnings = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["warning"]
30 serviceWarnings = serviceWarnings and serviceWarnings:gsub('\\n', '\n')
31 serviceWarnings = serviceWarnings and serviceWarnings:gsub('\\033%[0;33mWARNING\\033%[0m: ', '')
32 serviceMode = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["mode"]
35 local serviceRunning, statusText = false, nil
36 local packageVersion = tostring(util.trim(sys.exec("opkg list-installed " .. packageName .. " | awk '{print $3}'"))) or ""
37 if packageVersion == "" then
38 statusText = translatef("%s is not installed or not found", packageName)
40 if sys.call("iptables -t mangle -L | grep -q VPR_PREROUTING") == 0 then
42 statusText = translate("Running")
43 if serviceMode and serviceMode == "strict" then
44 statusText = translatef("%s (strict mode)", statusText)
47 statusText = translate("Stopped")
48 if uci:get(packageName, "config", "enabled") ~= "1" then
49 statusText = translatef("%s (disabled)", statusText)
53 local t = uci:get("vpn-policy-routing", "config", "supported_interface")
56 elseif type(t) == "table" then
57 for key,value in pairs(t) do supportedIfaces = supportedIfaces and supportedIfaces .. ' ' .. value or value end
58 elseif type(t) == "string" then
62 t = uci:get("vpn-policy-routing", "config", "ignored_interface")
65 elseif type(t) == "table" then
66 for key,value in pairs(t) do ignoredIfaces = ignoredIfaces and ignoredIfaces .. ' ' .. value or value end
67 elseif type(t) == "string" then
71 local lanIPAddr = uci:get("network", "lan", "ipaddr")
72 local lanNetmask = uci:get("network", "lan", "netmask")
73 -- if multiple ip addresses on lan interface, will be returned as table of CIDR notations i.e. {"10.0.0.1/24","10.0.0.2/24"}
74 if (type(lanIPAddr) == "table") then
76 for i,line in ipairs(lanIPAddr) do
77 lanIPAddr = lanIPAddr[i]
80 lanIPAddr = lanIPAddr:match("[0-9.]+")
82 if lanIPAddr and lanNetmask then
83 laPlaceholder = ip.new(lanIPAddr .. "/" .. lanNetmask )
87 return name:sub(1,3) == "wan" or name:sub(-3) == "wan"
90 function is_supported_interface(arg)
91 local name=arg['.name']
92 local proto=arg['proto']
93 local ifname=arg['ifname']
95 if name and is_wan(name) then return true end
96 if name and supportedIfaces:match('%f[%w]' .. name .. '%f[%W]') then return true end
97 if name and not ignoredIfaces:match('%f[%w]' .. name .. '%f[%W]') then
98 if type(ifname) == "table" then
99 for key,value in pairs(ifname) do
100 if value and value:sub(1,3) == "tun" then return true end
101 if value and value:sub(1,3) == "tap" then return true end
102 if value and value:sub(1,3) == "tor" then return true end
103 if value and fs.access("/sys/devices/virtual/net/" .. value .. "/tun_flags") then return true end
105 elseif type(ifname) == "string" then
106 if ifname and ifname:sub(1,3) == "tun" then return true end
107 if ifname and ifname:sub(1,3) == "tap" then return true end
108 if ifname and ifname:sub(1,3) == "tor" then return true end
109 if ifname and fs.access("/sys/devices/virtual/net/" .. ifname .. "/tun_flags") then return true end
111 if proto and proto:sub(1,11) == "openconnect" then return true end
112 if proto and proto:sub(1,4) == "pptp" then return true end
113 if proto and proto:sub(1,4) == "l2tp" then return true end
114 if proto and proto:sub(1,9) == "wireguard" then return true end
118 m = Map("vpn-policy-routing", translate("VPN and WAN Policy-Based Routing"))
120 h = m:section(NamedSection, "config", packageName, translatef("Service Status [%s %s]", packageName, packageVersion))
121 status = h:option(DummyValue, "_dummy", translate("Service Status"))
122 status.template = "vpn-policy-routing/status"
123 status.value = statusText
124 if serviceRunning and serviceGateways and serviceGateways ~= "" then
125 gateways = h:option(DummyValue, "_dummy", translate("Service Gateways"))
126 gateways.template = packageName .. "/status-gateways"
127 gateways.value = serviceGateways
129 if serviceErrors and serviceErrors ~= "" then
130 errors = h:option(DummyValue, "_dummy", translate("Service Errors"))
131 errors.template = packageName .. "/status-textarea"
132 errors.value = serviceErrors
134 if serviceWarnings and serviceWarnings ~= "" then
135 warnings = h:option(DummyValue, "_dummy", translate("Service Warnings"))
136 warnings.template = packageName .. "/status-textarea"
137 warnings.value = serviceWarnings
139 if packageVersion ~= "" then
140 buttons = h:option(DummyValue, "_dummy")
141 buttons.template = packageName .. "/buttons"
145 config = m:section(NamedSection, "config", "vpn-policy-routing", translate("Configuration"))
146 config.override_values = true
147 config.override_depends = true
150 config:tab("basic", translate("Basic Configuration"))
152 verb = config:taboption("basic", ListValue, "verbosity", translate("Output verbosity"), translate("Controls both system log and console output verbosity."))
153 verb:value("0", translate("Suppress/No output"))
154 verb:value("1", translate("Condensed output"))
155 verb:value("2", translate("Verbose output"))
158 se = config:taboption("basic", ListValue, "strict_enforcement", translate("Strict enforcement"),
159 translatef("See the <a href=\"%s\" target=\"_blank\">README</a> for details.", readmeURL .. "#strict-enforcement"))
160 se:value("0", translate("Do not enforce policies when their gateway is down"))
161 se:value("1", translate("Strictly enforce policies when their gateway is down"))
164 dest_ipset = config:taboption("basic", ListValue, "dest_ipset", translate("The ipset option for remote policies"),
165 translatef("Please check the <a href=\"%s\" target=\"_blank\">README</a> before changing this option.", readmeURL .. "#service-configuration-settings"))
166 dest_ipset:value("", translate("Disabled"))
167 dest_ipset:value("ipset", translate("Use ipset command"))
168 dest_ipset:value("dnsmasq.ipset", translate("Use DNSMASQ ipset"))
169 dest_ipset.default = ""
170 dest_ipset.rmempty = true
172 src_ipset = config:taboption("basic", ListValue, "src_ipset", translate("The ipset option for local policies"),
173 translatef("Please check the <a href=\"%s\" target=\"_blank\">README</a> before changing this option.", readmeURL .. "#service-configuration-settings"))
174 src_ipset:value("0", translate("Disabled"))
175 src_ipset:value("1", translate("Use ipset command"))
177 ipv6 = config:taboption("basic", ListValue, "ipv6_enabled", translate("IPv6 Support"))
178 ipv6:value("0", translate("Disabled"))
179 ipv6:value("1", translate("Enabled"))
182 config:tab("advanced", translate("Advanced Configuration"),
183 translatef("%sWARNING:%s Please make sure to check the <a href=\"%s\" target=\"_blank\">README</a> before changing anything in this section! Change any of the settings below with extreme caution!%s" , "<br/> <b>", "</b>", readmeURL .. "#service-configuration-settings", "<br/><br/>"))
185 supportedIface = config:taboption("advanced", DynamicList, "supported_interface", translate("Supported Interfaces"), translate("Allows to specify the list of interface names (in lower case) to be explicitly supported by the service. Can be useful if your OpenVPN tunnels have dev option other than tun* or tap*."))
186 supportedIface.optional = false
188 ignoredIface = config:taboption("advanced", DynamicList, "ignored_interface", translate("Ignored Interfaces"), translate("Allows to specify the list of interface names (in lower case) to be ignored by the service. Can be useful if running both VPN server and VPN client on the router."))
189 ignoredIface.optional = false
191 timeout = config:taboption("advanced", Value, "boot_timeout", translate("Boot Time-out"), translate("Time (in seconds) for service to wait for WAN gateway discovery on boot."))
192 timeout.optional = false
193 timeout.rmempty = true
195 insert = config:taboption("advanced", ListValue, "iptables_rule_option", translate("IPTables rule option"), translate("Select Append for -A and Insert for -I."))
196 insert:value("append", translate("Append"))
197 insert:value("insert", translate("Insert"))
198 insert.default = "append"
200 iprule = config:taboption("advanced", ListValue, "iprule_enabled", translate("IP Rules Support"), translate("Add an ip rule, not an iptables entry for policies with just the local address. Use with caution to manipulte policies priorities."))
201 iprule:value("0", translate("Disabled"))
202 iprule:value("1", translate("Enabled"))
204 icmp = config:taboption("advanced", ListValue, "icmp_interface", translate("Default ICMP Interface"), translate("Force the ICMP protocol interface."))
205 icmp:value("", translate("No Change"))
206 icmp:value("wan", translate("WAN"))
207 uci:foreach("network", "interface", function(s)
208 local name=s['.name']
209 if is_supported_interface(s) then icmp:value(name, name:upper()) end
213 append_local = config:taboption("advanced", Value, "append_src_rules", translate("Append local IP Tables rules"), translate("Special instructions to append iptables rules for local IPs/netmasks/devices."))
214 append_local.rmempty = true
216 append_remote = config:taboption("advanced", Value, "append_dest_rules", translate("Append remote IP Tables rules"), translate("Special instructions to append iptables rules for remote IPs/netmasks."))
217 append_remote.rmempty = true
219 wantid = config:taboption("advanced", Value, "wan_tid", translate("WAN Table ID"), translate("Starting (WAN) Table ID number for tables created by the service."))
220 wantid.rmempty = true
221 wantid.placeholder = "201"
222 wantid.datatype = 'and(uinteger, min(201))'
224 wanmark = config:taboption("advanced", Value, "wan_mark", translate("WAN Table FW Mark"), translate("Starting (WAN) FW Mark for marks used by the service. High starting mark is used to avoid conflict with SQM/QoS. Change with caution together with") .. " " .. translate("Service FW Mask") .. ".")
225 wanmark.rmempty = true
226 wanmark.placeholder = "0x010000"
227 wanmark.datatype = "hex(8)"
229 fwmask = config:taboption("advanced", Value, "fw_mask", translate("Service FW Mask"), translate("FW Mask used by the service. High mask is used to avoid conflict with SQM/QoS. Change with caution together with") .. " " .. translate("WAN Table FW Mark") .. ".")
230 fwmask.rmempty = true
231 fwmask.placeholder = "0xff0000"
232 fwmask.datatype = "hex(8)"
234 config:tab("webui", translate("Web UI Configuration"))
236 webui_enable_column = config:taboption("webui", ListValue, "webui_enable_column", translate("Show Enable Column"), translate("Shows the enable checkbox column for policies, allowing you to quickly enable/disable specific policy without deleting it."))
237 webui_enable_column:value("0", translate("Disabled"))
238 webui_enable_column:value("1", translate("Enabled"))
240 webui_protocol_column = config:taboption("webui", ListValue, "webui_protocol_column", translate("Show Protocol Column"), translate("Shows the protocol column for policies, allowing you to assign a specific protocol to a policy."))
241 webui_protocol_column:value("0", translate("Disabled"))
242 webui_protocol_column:value("1", translate("Enabled"))
244 webui_supported_protocol = config:taboption("webui", DynamicList, "webui_supported_protocol", translate("Supported Protocols"), translate("Display these protocols in protocol column in Web UI."))
245 webui_supported_protocol.optional = false
247 webui_chain_column = config:taboption("webui", ListValue, "webui_chain_column", translate("Show Chain Column"), translate("Shows the chain column for policies, allowing you to assign a PREROUTING, FORWARD, INPUT or OUTPUT chain to a policy."))
248 webui_chain_column:value("0", translate("Disabled"))
249 webui_chain_column:value("1", translate("Enabled"))
251 webui_sorting = config:taboption("webui", ListValue, "webui_sorting", translate("Show Up/Down Buttons"), translate("Shows the Up/Down buttons for policies, allowing you to move a policy up or down in the list."))
252 webui_sorting:value("0", translate("Disabled"))
253 webui_sorting:value("1", translate("Enabled"))
254 webui_sorting.default = "1"
258 p = m:section(TypedSection, "policy", translate("Policies"), translate("Comment, interface and at least one other field are required. Multiple local and remote addresses/devices/domains and ports can be space separated. Placeholders below represent just the format/syntax and will not be used if fields are left blank."))
259 p.template = "cbi/tblsection"
260 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_sorting"))
261 if not enc or enc ~= 0 then
267 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_enable_column"))
268 if enc and enc ~= 0 then
269 le = p:option(Flag, "enabled", translate("Enabled"))
273 local comment = uci:get_first("vpn-policy-routing", "policy", "comment")
275 p:option(Value, "comment", translate("Comment"))
277 p:option(Value, "name", translate("Name"))
280 la = p:option(Value, "src_addr", translate("Local addresses / devices"))
281 if laPlaceholder then
282 la.placeholder = laPlaceholder
285 la.datatype = 'list(neg(or(host,network,macaddr)))'
287 lp = p:option(Value, "src_port", translate("Local ports"))
288 lp.datatype = 'list(neg(or(portrange, string)))'
289 lp.placeholder = "0-65535"
292 ra = p:option(Value, "dest_addr", translate("Remote addresses / domains"))
293 ra.datatype = 'list(neg(host))'
294 ra.placeholder = "0.0.0.0/0"
297 rp = p:option(Value, "dest_port", translate("Remote ports"))
298 rp.datatype = 'list(neg(or(portrange, string)))'
299 rp.placeholder = "0-65535"
302 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_protocol_column"))
303 if enc and enc ~= 0 then
304 proto = p:option(ListValue, "proto", translate("Protocol"))
305 proto:value("", "AUTO")
308 enc = uci:get_list("vpn-policy-routing", "config", "webui_supported_protocol")
310 for key, value in pairs(enc) do
312 proto:value(value:lower(), value:gsub(" ", "/"):upper())
315 enc = { "tcp", "udp", "tcp udp", "icmp", "all" }
316 for key,value in pairs(enc) do
317 proto:value(value:lower(), value:gsub(" ", "/"):upper())
322 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_chain_column"))
323 if enc and enc ~= 0 then
324 chain = p:option(ListValue, "chain", translate("Chain"))
325 chain:value("", "PREROUTING")
326 chain:value("FORWARD", "FORWARD")
327 chain:value("INPUT", "INPUT")
328 chain:value("OUTPUT", "OUTPUT")
333 gw = p:option(ListValue, "interface", translate("Interface"))
334 gw.datatype = "network"
336 uci:foreach("network", "interface", function(s)
337 local name=s['.name']
339 gw:value(name, name:upper())
340 if not gw.default then gw.default = name end
341 elseif is_supported_interface(s) then
342 gw:value(name, name:upper())
346 dscp = m:section(NamedSection, "config", "vpn-policy-routing", translate("DSCP Tagging"),
347 translatef("Set DSCP tags (in range between 1 and 63) for specific interfaces. See the <a href=\"%s\" target=\"_blank\">README</a> for details.", readmeURL .. "#dscp-tag-based-policies"))
348 uci:foreach("network", "interface", function(s)
349 local name=s['.name']
350 if is_supported_interface(s) then
351 local x = dscp:option(Value, name .. "_dscp", name:upper() .. " " .. translate("DSCP Tag"))
353 x.datatype = "range(1,63)"
358 inc = m:section(TypedSection, "include", translate("Custom User File Includes"),
359 translatef("Run the following user files after setting up but before restarting DNSMASQ. See the <a href=\"%s\" target=\"_blank\">README</a> for details.", readmeURL .. "#custom-user-files"))
360 inc.template = "cbi/tblsection"
365 finc = inc:option(Flag, "enabled", translate("Enabled"))
366 finc.optional = false
368 inc:option(Value, "path", translate("Path")).optional = false