1 local packageName = "vpn-policy-routing"
2 local readmeURL = "https://docs.openwrt.melmac.net/" .. packageName .. "/"
3 local uci = require "luci.model.uci".cursor()
4 local sys = require "luci.sys"
5 local util = require "luci.util"
6 local ip = require "luci.ip"
7 local fs = require "nixio.fs"
8 local jsonc = require "luci.jsonc"
9 local http = require "luci.http"
10 local nutil = require "nixio.util"
11 local dispatcher = require "luci.dispatcher"
12 local enabledFlag = uci:get(packageName, "config", "enabled")
15 function getPackageVersion()
16 local opkgFile = "/usr/lib/opkg/status"
19 for line in io.lines(opkgFile) do
21 return line:match('[%d%.$-]+') or ""
22 elseif line:find("Package: " .. packageName:gsub("%-", "%%%-")) then
29 local ubusStatus = util.ubus("service", "list", { name = packageName })
30 if ubusStatus and ubusStatus[packageName] and
31 ubusStatus[packageName]["instances"] and
32 ubusStatus[packageName]["instances"]["main"] and
33 ubusStatus[packageName]["instances"]["main"]["data"] and
34 ubusStatus[packageName]["instances"]["main"]["data"]["status"] and
35 ubusStatus[packageName]["instances"]["main"]["data"]["status"][1] then
36 serviceGateways = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["gateway"]
37 serviceGateways = serviceGateways and serviceGateways:gsub('\\n', '\n')
38 serviceGateways = serviceGateways and serviceGateways:gsub('\\033%[0;32m%[\\xe2\\x9c\\x93%]\\033%[0m', '✓')
39 serviceErrors = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["error"]
40 serviceErrors = serviceErrors and serviceErrors:gsub('\\n', '\n')
41 serviceErrors = serviceErrors and serviceErrors:gsub('\\033%[0;31mERROR\\033%[0m: ', '')
42 serviceWarnings = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["warning"]
43 serviceWarnings = serviceWarnings and serviceWarnings:gsub('\\n', '\n')
44 serviceWarnings = serviceWarnings and serviceWarnings:gsub('\\033%[0;33mWARNING\\033%[0m: ', '')
45 serviceMode = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["mode"]
48 local serviceRunning, statusText = false, nil
49 local packageVersion = getPackageVersion()
50 if packageVersion == "" then
51 statusText = translatef("%s is not installed or not found", packageName)
53 if sys.call("iptables -t mangle -L | grep -q VPR_PREROUTING") == 0 then
55 statusText = translate("Running")
56 if serviceMode and serviceMode == "strict" then
57 statusText = translatef("%s (strict mode)", statusText)
60 statusText = translate("Stopped")
61 if uci:get(packageName, "config", "enabled") ~= "1" then
62 statusText = translatef("%s (disabled)", statusText)
66 local t = uci:get("vpn-policy-routing", "config", "supported_interface")
69 elseif type(t) == "table" then
70 for key,value in pairs(t) do supportedIfaces = supportedIfaces and supportedIfaces .. ' ' .. value or value end
71 elseif type(t) == "string" then
75 t = uci:get("vpn-policy-routing", "config", "ignored_interface")
78 elseif type(t) == "table" then
79 for key,value in pairs(t) do ignoredIfaces = ignoredIfaces and ignoredIfaces .. ' ' .. value or value end
80 elseif type(t) == "string" then
84 local lanIPAddr = uci:get("network", "lan", "ipaddr")
85 local lanNetmask = uci:get("network", "lan", "netmask")
86 -- 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"}
87 if (type(lanIPAddr) == "table") then
89 for i,line in ipairs(lanIPAddr) do
90 lanIPAddr = lanIPAddr[i]
93 lanIPAddr = lanIPAddr:match("[0-9.]+")
95 if lanIPAddr and lanNetmask then
96 laPlaceholder = ip.new(lanIPAddr .. "/" .. lanNetmask )
100 return name:sub(1,3) == "wan" or name:sub(-3) == "wan"
103 function is_supported_interface(arg)
104 local name=arg['.name']
105 local proto=arg['proto']
106 local ifname=arg['ifname']
108 if name and is_wan(name) then return true end
109 if name and supportedIfaces:match('%f[%w]' .. name .. '%f[%W]') then return true end
110 if name and not ignoredIfaces:match('%f[%w]' .. name .. '%f[%W]') then
111 if type(ifname) == "table" then
112 for key,value in pairs(ifname) do
113 if value and value:sub(1,3) == "tun" then return true end
114 if value and value:sub(1,3) == "tap" then return true end
115 if value and value:sub(1,3) == "tor" then return true end
116 if value and fs.access("/sys/devices/virtual/net/" .. value .. "/tun_flags") then return true end
118 elseif type(ifname) == "string" then
119 if ifname and ifname:sub(1,3) == "tun" then return true end
120 if ifname and ifname:sub(1,3) == "tap" then return true end
121 if ifname and ifname:sub(1,3) == "tor" then return true end
122 if ifname and fs.access("/sys/devices/virtual/net/" .. ifname .. "/tun_flags") then return true end
124 if proto and proto:sub(1,11) == "openconnect" then return true end
125 if proto and proto:sub(1,4) == "pptp" then return true end
126 if proto and proto:sub(1,4) == "l2tp" then return true end
127 if proto and proto:sub(1,9) == "wireguard" then return true end
131 m = Map("vpn-policy-routing", translate("VPN and WAN Policy-Based Routing"))
133 h = m:section(NamedSection, "config", packageName, translatef("Service Status [%s %s]", packageName, packageVersion))
134 status = h:option(DummyValue, "_dummy", translate("Service Status"))
135 status.template = "vpn-policy-routing/status-service"
136 status.value = statusText
137 if serviceRunning and serviceGateways and serviceGateways ~= "" then
138 gateways = h:option(DummyValue, "_dummy", translate("Service Gateways"))
139 gateways.template = packageName .. "/status-gateways"
140 gateways.value = serviceGateways
142 if serviceErrors and serviceErrors ~= "" then
143 errors = h:option(DummyValue, "_dummy", translate("Service Errors"))
144 errors.template = packageName .. "/status"
145 errors.value = serviceErrors
147 if serviceWarnings and serviceWarnings ~= "" then
148 warnings = h:option(DummyValue, "_dummy", translate("Service Warnings"))
149 warnings.template = packageName .. "/status"
150 warnings.value = serviceWarnings
152 if packageVersion ~= "" then
153 buttons = h:option(DummyValue, "_dummy")
154 buttons.template = packageName .. "/buttons"
158 config = m:section(NamedSection, "config", "vpn-policy-routing", translate("Configuration"))
159 config.override_values = true
160 config.override_depends = true
163 config:tab("basic", translate("Basic Configuration"))
165 verb = config:taboption("basic", ListValue, "verbosity", translate("Output verbosity"), translate("Controls both system log and console output verbosity."))
166 verb:value("0", translate("Suppress/No output"))
167 verb:value("1", translate("Condensed output"))
168 verb:value("2", translate("Verbose output"))
171 se = config:taboption("basic", ListValue, "strict_enforcement", translate("Strict enforcement"),
172 translatef("See the %sREADME%s for details.", "<a href=\"" .. readmeURL .. "#strict-enforcement" .. "\" target=\"_blank\">", "</a>"))
173 se:value("0", translate("Do not enforce policies when their gateway is down"))
174 se:value("1", translate("Strictly enforce policies when their gateway is down"))
177 resolver_ipset = config:taboption("basic", ListValue, "resolver_ipset", translate("Use resolver's ipset for domains"),
178 translatef("Please check the %sREADME%s before changing this option.", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>"))
179 resolver_ipset:value("none", translate("Disabled"))
180 resolver_ipset:value("dnsmasq.ipset", translate("DNSMASQ ipset"))
181 resolver_ipset.default = "dnsmasq.ipset"
183 ipv6 = config:taboption("basic", ListValue, "ipv6_enabled", translate("IPv6 Support"))
184 ipv6:value("0", translate("Disabled"))
185 ipv6:value("1", translate("Enabled"))
188 config:tab("advanced", translate("Advanced Configuration"),
189 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/>"))
191 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*."))
192 supportedIface.optional = false
194 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."))
195 ignoredIface.optional = false
197 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."))
198 timeout.optional = false
199 timeout.rmempty = true
201 dest_ipset = config:taboption("advanced", ListValue, "dest_ipset", translate("The ipset option for remote policies"),
202 translatef("Please check the %sREADME%s before changing this option.", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>"))
203 dest_ipset:value("0", translate("Disabled"))
204 dest_ipset:value("1", translate("Use ipset command"))
205 dest_ipset.default = "0"
207 src_ipset = config:taboption("advanced", ListValue, "src_ipset", translate("The ipset option for local policies"),
208 translatef("Please check the %sREADME%s before changing this option.", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>"))
209 src_ipset:value("0", translate("Disabled"))
210 src_ipset:value("1", translate("Use ipset command"))
211 src_ipset.default = "0"
213 insert = config:taboption("advanced", ListValue, "iptables_rule_option", translate("IPTables rule option"), translate("Select Append for -A and Insert for -I."))
214 insert:value("append", translate("Append"))
215 insert:value("insert", translate("Insert"))
216 insert.default = "append"
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 wantid = config:taboption("advanced", Value, "wan_tid", translate("WAN Table ID"), translate("Starting (WAN) Table ID number for tables created by the service."))
228 wantid.rmempty = true
229 wantid.placeholder = "201"
230 wantid.datatype = 'and(uinteger, min(201))'
232 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") .. ".")
233 wanmark.rmempty = true
234 wanmark.placeholder = "0x010000"
235 wanmark.datatype = "hex(8)"
237 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") .. ".")
238 fwmask.rmempty = true
239 fwmask.placeholder = "0xff0000"
240 fwmask.datatype = "hex(8)"
242 config:tab("webui", translate("Web UI Configuration"))
244 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."))
245 webui_enable_column:value("0", translate("Disabled"))
246 webui_enable_column:value("1", translate("Enabled"))
248 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."))
249 webui_protocol_column:value("0", translate("Disabled"))
250 webui_protocol_column:value("1", translate("Enabled"))
252 webui_supported_protocol = config:taboption("webui", DynamicList, "webui_supported_protocol", translate("Supported Protocols"), translate("Display these protocols in protocol column in Web UI."))
253 webui_supported_protocol.optional = false
255 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."))
256 webui_chain_column:value("0", translate("Disabled"))
257 webui_chain_column:value("1", translate("Enabled"))
259 webui_show_ignore_target = config:taboption("webui", ListValue, "webui_show_ignore_target", translate("Add IGNORE Target"), translate("Adds `IGNORE` to the list of interfaces for policies, allowing you to skip further processing by VPN Policy Routing."))
260 webui_show_ignore_target:value("0", translate("Disabled"))
261 webui_show_ignore_target:value("1", translate("Enabled"))
263 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."))
264 webui_sorting:value("0", translate("Disabled"))
265 webui_sorting:value("1", translate("Enabled"))
266 webui_sorting.default = "1"
269 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."))
270 p.template = "cbi/tblsection"
271 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_sorting"))
272 if not enc or enc ~= 0 then
278 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_enable_column"))
279 if enc and enc ~= 0 then
280 le = p:option(Flag, "enabled", translate("Enabled"))
284 local comment = uci:get_first("vpn-policy-routing", "policy", "comment")
286 p:option(Value, "comment", translate("Comment"))
288 p:option(Value, "name", translate("Name"))
291 la = p:option(Value, "src_addr", translate("Local addresses / devices"))
292 if laPlaceholder then
293 la.placeholder = laPlaceholder
296 la.datatype = 'list(neg(or(host,network,macaddr,string)))'
298 lp = p:option(Value, "src_port", translate("Local ports"))
299 lp.datatype = 'list(neg(or(portrange, string)))'
300 lp.placeholder = "0-65535"
303 ra = p:option(Value, "dest_addr", translate("Remote addresses / domains"))
304 ra.datatype = 'list(neg(host))'
305 ra.placeholder = "0.0.0.0/0"
308 rp = p:option(Value, "dest_port", translate("Remote ports"))
309 rp.datatype = 'list(neg(or(portrange, string)))'
310 rp.placeholder = "0-65535"
313 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_protocol_column"))
314 if enc and enc ~= 0 then
315 proto = p:option(ListValue, "proto", translate("Protocol"))
316 proto:value("", "AUTO")
319 enc = uci:get_list("vpn-policy-routing", "config", "webui_supported_protocol")
321 for key, value in pairs(enc) do
323 proto:value(value:lower(), value:gsub(" ", "/"):upper())
326 enc = { "tcp", "udp", "tcp udp", "icmp", "all" }
327 for key,value in pairs(enc) do
328 proto:value(value:lower(), value:gsub(" ", "/"):upper())
333 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_chain_column"))
334 if enc and enc ~= 0 then
335 chain = p:option(ListValue, "chain", translate("Chain"))
336 chain:value("", "PREROUTING")
337 chain:value("FORWARD", "FORWARD")
338 chain:value("INPUT", "INPUT")
339 chain:value("OUTPUT", "OUTPUT")
344 gw = p:option(ListValue, "interface", translate("Interface"))
345 gw.datatype = "network"
347 uci:foreach("network", "interface", function(s)
348 local name=s['.name']
350 gw:value(name, name:upper())
351 if not gw.default then gw.default = name end
352 elseif is_supported_interface(s) then
353 gw:value(name, name:upper())
356 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_show_ignore_target"))
357 if enc and enc ~= 0 then
358 gw:value("ignore", "IGNORE")
361 dscp = m:section(NamedSection, "config", "vpn-policy-routing", translate("DSCP Tagging"),
362 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>"))
363 uci:foreach("network", "interface", function(s)
364 local name=s['.name']
365 if is_supported_interface(s) then
366 local x = dscp:option(Value, name .. "_dscp", name:upper() .. " " .. translate("DSCP Tag"))
368 x.datatype = "range(1,63)"
373 inc = m:section(TypedSection, "include", translate("Custom User File Includes"),
374 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>"))
375 inc.template = "cbi/tblsection"
380 finc = inc:option(Flag, "enabled", translate("Enabled"))
381 finc.optional = false
383 inc:option(Value, "path", translate("Path")).optional = false