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 function getPackageVersion()
17 local opkgFile = "/usr/lib/opkg/status"
20 for line in io.lines(opkgFile) do
22 return line:match('[%d%.$-]+') or ""
23 elseif line:find("Package: " .. packageName:gsub("%-", "%%%-")) then
30 local ubusStatus = util.ubus("service", "list", { name = packageName })
31 if ubusStatus and ubusStatus[packageName] and
32 ubusStatus[packageName]["instances"] and
33 ubusStatus[packageName]["instances"]["main"] and
34 ubusStatus[packageName]["instances"]["main"]["data"] and
35 ubusStatus[packageName]["instances"]["main"]["data"]["status"] and
36 ubusStatus[packageName]["instances"]["main"]["data"]["status"][1] then
37 serviceGateways = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["gateway"]
38 serviceGateways = serviceGateways and serviceGateways:gsub('\\n', '\n')
39 serviceGateways = serviceGateways and serviceGateways:gsub('\\033%[0;32m%[\\xe2\\x9c\\x93%]\\033%[0m', '✓')
40 serviceErrors = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["error"]
41 serviceErrors = serviceErrors and serviceErrors:gsub('\\n', '\n')
42 serviceErrors = serviceErrors and serviceErrors:gsub('\\033%[0;31mERROR\\033%[0m: ', '')
43 serviceWarnings = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["warning"]
44 serviceWarnings = serviceWarnings and serviceWarnings:gsub('\\n', '\n')
45 serviceWarnings = serviceWarnings and serviceWarnings:gsub('\\033%[0;33mWARNING\\033%[0m: ', '')
46 serviceMode = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["mode"]
49 local serviceRunning, statusText = false, nil
50 local packageVersion = getPackageVersion()
51 if packageVersion == "" then
52 statusText = translatef("%s is not installed or not found", packageName)
54 if sys.call("iptables -t mangle -L | grep -q VPR_PREROUTING") == 0 then
56 statusText = translate("Running")
57 if serviceMode and serviceMode == "strict" then
58 statusText = translatef("%s (strict mode)", statusText)
61 statusText = translate("Stopped")
62 if uci:get(packageName, "config", "enabled") ~= "1" then
63 statusText = translatef("%s (disabled)", statusText)
67 local t = uci:get("vpn-policy-routing", "config", "supported_interface")
70 elseif type(t) == "table" then
71 for key,value in pairs(t) do supportedIfaces = supportedIfaces and supportedIfaces .. ' ' .. value or value end
72 elseif type(t) == "string" then
76 t = uci:get("vpn-policy-routing", "config", "ignored_interface")
79 elseif type(t) == "table" then
80 for key,value in pairs(t) do ignoredIfaces = ignoredIfaces and ignoredIfaces .. ' ' .. value or value end
81 elseif type(t) == "string" then
85 local lanIPAddr = uci:get("network", "lan", "ipaddr")
86 local lanNetmask = uci:get("network", "lan", "netmask")
87 -- 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"}
88 if (type(lanIPAddr) == "table") then
90 for i,line in ipairs(lanIPAddr) do
91 lanIPAddr = lanIPAddr[i]
94 lanIPAddr = lanIPAddr:match("[0-9.]+")
96 if lanIPAddr and lanNetmask then
97 laPlaceholder = ip.new(lanIPAddr .. "/" .. lanNetmask )
100 function is_wan(name)
101 return name:sub(1,3) == "wan" or name:sub(-3) == "wan"
104 function is_supported_interface(arg)
105 local name=arg['.name']
106 local proto=arg['proto']
107 local ifname=arg['ifname']
109 if name and is_wan(name) then return true end
110 if name and supportedIfaces:match('%f[%w]' .. name .. '%f[%W]') then return true end
111 if name and not ignoredIfaces:match('%f[%w]' .. name .. '%f[%W]') then
112 if type(ifname) == "table" then
113 for key,value in pairs(ifname) do
114 if value and value:sub(1,3) == "tun" then return true end
115 if value and value:sub(1,3) == "tap" then return true end
116 if value and value:sub(1,3) == "tor" then return true end
117 if value and fs.access("/sys/devices/virtual/net/" .. value .. "/tun_flags") then return true end
119 elseif type(ifname) == "string" then
120 if ifname and ifname:sub(1,3) == "tun" then return true end
121 if ifname and ifname:sub(1,3) == "tap" then return true end
122 if ifname and ifname:sub(1,3) == "tor" then return true end
123 if ifname and fs.access("/sys/devices/virtual/net/" .. ifname .. "/tun_flags") then return true end
125 if proto and proto:sub(1,11) == "openconnect" then return true end
126 if proto and proto:sub(1,4) == "pptp" then return true end
127 if proto and proto:sub(1,4) == "l2tp" then return true end
128 if proto and proto:sub(1,9) == "wireguard" then return true end
132 m = Map("vpn-policy-routing", translate("VPN and WAN Policy-Based Routing"))
134 h = m:section(NamedSection, "config", packageName, translatef("Service Status [%s %s]", packageName, packageVersion))
135 status = h:option(DummyValue, "_dummy", translate("Service Status"))
136 status.template = "vpn-policy-routing/status"
137 status.value = statusText
138 if serviceRunning and serviceGateways and serviceGateways ~= "" then
139 gateways = h:option(DummyValue, "_dummy", translate("Service Gateways"))
140 gateways.template = packageName .. "/status-gateways"
141 gateways.value = serviceGateways
143 if serviceErrors and serviceErrors ~= "" then
144 errors = h:option(DummyValue, "_dummy", translate("Service Errors"))
145 errors.template = packageName .. "/status-textarea"
146 errors.value = serviceErrors
148 if serviceWarnings and serviceWarnings ~= "" then
149 warnings = h:option(DummyValue, "_dummy", translate("Service Warnings"))
150 warnings.template = packageName .. "/status-textarea"
151 warnings.value = serviceWarnings
153 if packageVersion ~= "" then
154 buttons = h:option(DummyValue, "_dummy")
155 buttons.template = packageName .. "/buttons"
159 config = m:section(NamedSection, "config", "vpn-policy-routing", translate("Configuration"))
160 config.override_values = true
161 config.override_depends = true
164 config:tab("basic", translate("Basic Configuration"))
166 verb = config:taboption("basic", ListValue, "verbosity", translate("Output verbosity"), translate("Controls both system log and console output verbosity."))
167 verb:value("0", translate("Suppress/No output"))
168 verb:value("1", translate("Condensed output"))
169 verb:value("2", translate("Verbose output"))
172 se = config:taboption("basic", ListValue, "strict_enforcement", translate("Strict enforcement"),
173 translatef("See the %sREADME%s for details.", "<a href=\"" .. readmeURL .. "#strict-enforcement" .. "\" target=\"_blank\">", "</a>"))
174 se:value("0", translate("Do not enforce policies when their gateway is down"))
175 se:value("1", translate("Strictly enforce policies when their gateway is down"))
178 dest_ipset = config:taboption("basic", ListValue, "dest_ipset", translate("The ipset option for remote policies"),
179 translatef("Please check the %sREADME%s before changing this option.", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>"))
180 dest_ipset:value("", translate("Disabled"))
181 dest_ipset:value("ipset", translate("Use ipset command"))
182 dest_ipset:value("dnsmasq.ipset", translate("Use DNSMASQ ipset"))
183 dest_ipset.default = ""
184 dest_ipset.rmempty = true
186 src_ipset = config:taboption("basic", ListValue, "src_ipset", translate("The ipset option for local policies"),
187 translatef("Please check the %sREADME%s before changing this option.", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>"))
188 src_ipset:value("0", translate("Disabled"))
189 src_ipset:value("1", translate("Use ipset command"))
191 ipv6 = config:taboption("basic", ListValue, "ipv6_enabled", translate("IPv6 Support"))
192 ipv6:value("0", translate("Disabled"))
193 ipv6:value("1", translate("Enabled"))
196 config:tab("advanced", translate("Advanced Configuration"),
197 translatef("%sWARNING:%s Please make sure to check the %sREADME%s before changing anything in this section! Change any of the settings below with extreme caution!%s" , "<br/> <b>", "</b>", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>", "<br/><br/>"))
199 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*."))
200 supportedIface.optional = false
202 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."))
203 ignoredIface.optional = false
205 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."))
206 timeout.optional = false
207 timeout.rmempty = true
209 insert = config:taboption("advanced", ListValue, "iptables_rule_option", translate("IPTables rule option"), translate("Select Append for -A and Insert for -I."))
210 insert:value("append", translate("Append"))
211 insert:value("insert", translate("Insert"))
212 insert.default = "append"
214 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."))
215 iprule:value("0", translate("Disabled"))
216 iprule:value("1", translate("Enabled"))
218 icmp = config:taboption("advanced", ListValue, "icmp_interface", translate("Default ICMP Interface"), translate("Force the ICMP protocol interface."))
219 icmp:value("", translate("No Change"))
220 icmp:value("wan", translate("WAN"))
221 uci:foreach("network", "interface", function(s)
222 local name=s['.name']
223 if is_supported_interface(s) then icmp:value(name, name:upper()) end
227 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."))
228 append_local.rmempty = true
230 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."))
231 append_remote.rmempty = true
233 wantid = config:taboption("advanced", Value, "wan_tid", translate("WAN Table ID"), translate("Starting (WAN) Table ID number for tables created by the service."))
234 wantid.rmempty = true
235 wantid.placeholder = "201"
236 wantid.datatype = 'and(uinteger, min(201))'
238 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") .. ".")
239 wanmark.rmempty = true
240 wanmark.placeholder = "0x010000"
241 wanmark.datatype = "hex(8)"
243 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") .. ".")
244 fwmask.rmempty = true
245 fwmask.placeholder = "0xff0000"
246 fwmask.datatype = "hex(8)"
248 config:tab("webui", translate("Web UI Configuration"))
250 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."))
251 webui_enable_column:value("0", translate("Disabled"))
252 webui_enable_column:value("1", translate("Enabled"))
254 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."))
255 webui_protocol_column:value("0", translate("Disabled"))
256 webui_protocol_column:value("1", translate("Enabled"))
258 webui_supported_protocol = config:taboption("webui", DynamicList, "webui_supported_protocol", translate("Supported Protocols"), translate("Display these protocols in protocol column in Web UI."))
259 webui_supported_protocol.optional = false
261 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."))
262 webui_chain_column:value("0", translate("Disabled"))
263 webui_chain_column:value("1", translate("Enabled"))
265 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."))
266 webui_sorting:value("0", translate("Disabled"))
267 webui_sorting:value("1", translate("Enabled"))
268 webui_sorting.default = "1"
272 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."))
273 p.template = "cbi/tblsection"
274 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_sorting"))
275 if not enc or enc ~= 0 then
281 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_enable_column"))
282 if enc and enc ~= 0 then
283 le = p:option(Flag, "enabled", translate("Enabled"))
287 local comment = uci:get_first("vpn-policy-routing", "policy", "comment")
289 p:option(Value, "comment", translate("Comment"))
291 p:option(Value, "name", translate("Name"))
294 la = p:option(Value, "src_addr", translate("Local addresses / devices"))
295 if laPlaceholder then
296 la.placeholder = laPlaceholder
299 la.datatype = 'list(neg(or(host,network,macaddr,string)))'
301 lp = p:option(Value, "src_port", translate("Local ports"))
302 lp.datatype = 'list(neg(or(portrange, string)))'
303 lp.placeholder = "0-65535"
306 ra = p:option(Value, "dest_addr", translate("Remote addresses / domains"))
307 ra.datatype = 'list(neg(host))'
308 ra.placeholder = "0.0.0.0/0"
311 rp = p:option(Value, "dest_port", translate("Remote ports"))
312 rp.datatype = 'list(neg(or(portrange, string)))'
313 rp.placeholder = "0-65535"
316 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_protocol_column"))
317 if enc and enc ~= 0 then
318 proto = p:option(ListValue, "proto", translate("Protocol"))
319 proto:value("", "AUTO")
322 enc = uci:get_list("vpn-policy-routing", "config", "webui_supported_protocol")
324 for key, value in pairs(enc) do
326 proto:value(value:lower(), value:gsub(" ", "/"):upper())
329 enc = { "tcp", "udp", "tcp udp", "icmp", "all" }
330 for key,value in pairs(enc) do
331 proto:value(value:lower(), value:gsub(" ", "/"):upper())
336 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_chain_column"))
337 if enc and enc ~= 0 then
338 chain = p:option(ListValue, "chain", translate("Chain"))
339 chain:value("", "PREROUTING")
340 chain:value("FORWARD", "FORWARD")
341 chain:value("INPUT", "INPUT")
342 chain:value("OUTPUT", "OUTPUT")
347 gw = p:option(ListValue, "interface", translate("Interface"))
348 gw.datatype = "network"
350 uci:foreach("network", "interface", function(s)
351 local name=s['.name']
353 gw:value(name, name:upper())
354 if not gw.default then gw.default = name end
355 elseif is_supported_interface(s) then
356 gw:value(name, name:upper())
360 dscp = m:section(NamedSection, "config", "vpn-policy-routing", translate("DSCP Tagging"),
361 translatef("Set DSCP tags (in range between 1 and 63) for specific interfaces. See the %sREADME%s for details.", "<a href=\"" .. readmeURL .. "#dscp-tag-based-policies" .. "\" target=\"_blank\">", "</a>"))
362 uci:foreach("network", "interface", function(s)
363 local name=s['.name']
364 if is_supported_interface(s) then
365 local x = dscp:option(Value, name .. "_dscp", name:upper() .. " " .. translate("DSCP Tag"))
367 x.datatype = "range(1,63)"
372 inc = m:section(TypedSection, "include", translate("Custom User File Includes"),
373 translatef("Run the following user files after setting up but before restarting DNSMASQ. See the %sREADME%s for details.", "<a href=\"" .. readmeURL .. "#custom-user-files" .. "\" target=\"_blank\">", "</a>"))
374 inc.template = "cbi/tblsection"
379 finc = inc:option(Flag, "enabled", translate("Enabled"))
380 finc.optional = false
382 inc:option(Value, "path", translate("Path")).optional = false