luci-app-vpn-policy-routing: sync with principal app
[project/luci.git] / applications / luci-app-vpn-policy-routing / luasrc / model / cbi / vpn-policy-routing.lua
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")
13 local enc
14
15 function getPackageVersion()
16 local opkgFile = "/usr/lib/opkg/status"
17 local line
18 local flag = false
19 for line in io.lines(opkgFile) do
20 if flag then
21 return line:match('[%d%.$-]+') or ""
22 elseif line:find("Package: " .. packageName:gsub("%-", "%%%-")) then
23 flag = true
24 end
25 end
26 return ""
27 end
28
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"]
46 end
47
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)
52 end
53 if sys.call("iptables -t mangle -L | grep -q VPR_PREROUTING") == 0 then
54 serviceRunning = true
55 statusText = translate("Running")
56 if serviceMode and serviceMode == "strict" then
57 statusText = translatef("%s (strict mode)", statusText)
58 end
59 else
60 statusText = translate("Stopped")
61 if uci:get(packageName, "config", "enabled") ~= "1" then
62 statusText = translatef("%s (disabled)", statusText)
63 end
64 end
65
66 local t = uci:get("vpn-policy-routing", "config", "supported_interface")
67 if not t then
68 supportedIfaces = ""
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
72 supportedIfaces = t
73 end
74
75 t = uci:get("vpn-policy-routing", "config", "ignored_interface")
76 if not t then
77 ignoredIfaces = ""
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
81 ignoredIfaces = t
82 end
83
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
88 first = true
89 for i,line in ipairs(lanIPAddr) do
90 lanIPAddr = lanIPAddr[i]
91 break
92 end
93 lanIPAddr = lanIPAddr:match("[0-9.]+")
94 end
95 if lanIPAddr and lanNetmask then
96 laPlaceholder = ip.new(lanIPAddr .. "/" .. lanNetmask )
97 end
98
99 function is_wan(name)
100 return name:sub(1,3) == "wan" or name:sub(-3) == "wan"
101 end
102
103 function is_supported_interface(arg)
104 local name=arg['.name']
105 local proto=arg['proto']
106 local ifname=arg['ifname']
107
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
117 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
123 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
128 end
129 end
130
131 m = Map("vpn-policy-routing", translate("VPN and WAN Policy-Based Routing"))
132
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
141 end
142 if serviceErrors and serviceErrors ~= "" then
143 errors = h:option(DummyValue, "_dummy", translate("Service Errors"))
144 errors.template = packageName .. "/status"
145 errors.value = serviceErrors
146 end
147 if serviceWarnings and serviceWarnings ~= "" then
148 warnings = h:option(DummyValue, "_dummy", translate("Service Warnings"))
149 warnings.template = packageName .. "/status"
150 warnings.value = serviceWarnings
151 end
152 if packageVersion ~= "" then
153 buttons = h:option(DummyValue, "_dummy")
154 buttons.template = packageName .. "/buttons"
155 end
156
157 -- General Options
158 config = m:section(NamedSection, "config", "vpn-policy-routing", translate("Configuration"))
159 config.override_values = true
160 config.override_depends = true
161
162 -- Basic Options
163 config:tab("basic", translate("Basic Configuration"))
164
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"))
169 verb.default = 2
170
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"))
175 se.default = 1
176
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"
182
183 ipv6 = config:taboption("basic", ListValue, "ipv6_enabled", translate("IPv6 Support"))
184 ipv6:value("0", translate("Disabled"))
185 ipv6:value("1", translate("Enabled"))
186
187 -- Advanced Options
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/>&nbsp;&nbsp;&nbsp;&nbsp;<b>", "</b>", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>", "<br/><br/>"))
190
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
193
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
196
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
200
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"
206
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"
212
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"
217
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
224 end)
225 icmp.rmempty = true
226
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))'
231
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)"
236
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)"
241
242 config:tab("webui", translate("Web UI Configuration"))
243
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"))
247
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"))
251
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
254
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"))
258
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"))
262
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"
267
268 -- Policies
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
273 p.sortable = true
274 end
275 p.anonymous = true
276 p.addremove = true
277
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"))
281 le.default = "1"
282 end
283
284 local comment = uci:get_first("vpn-policy-routing", "policy", "comment")
285 if comment then
286 p:option(Value, "comment", translate("Comment"))
287 else
288 p:option(Value, "name", translate("Name"))
289 end
290
291 la = p:option(Value, "src_addr", translate("Local addresses / devices"))
292 if laPlaceholder then
293 la.placeholder = laPlaceholder
294 end
295 la.rmempty = true
296 la.datatype = 'list(neg(or(host,network,macaddr,string)))'
297
298 lp = p:option(Value, "src_port", translate("Local ports"))
299 lp.datatype = 'list(neg(or(portrange, string)))'
300 lp.placeholder = "0-65535"
301 lp.rmempty = true
302
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"
306 ra.rmempty = true
307
308 rp = p:option(Value, "dest_port", translate("Remote ports"))
309 rp.datatype = 'list(neg(or(portrange, string)))'
310 rp.placeholder = "0-65535"
311 rp.rmempty = true
312
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")
317 proto.default = ""
318 proto.rmempty = true
319 enc = uci:get_list("vpn-policy-routing", "config", "webui_supported_protocol")
320 local count = 0
321 for key, value in pairs(enc) do
322 count = count + 1
323 proto:value(value:lower(), value:gsub(" ", "/"):upper())
324 end
325 if count == 0 then
326 enc = { "tcp", "udp", "tcp udp", "icmp", "all" }
327 for key,value in pairs(enc) do
328 proto:value(value:lower(), value:gsub(" ", "/"):upper())
329 end
330 end
331 end
332
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")
340 chain.default = ""
341 chain.rmempty = true
342 end
343
344 gw = p:option(ListValue, "interface", translate("Interface"))
345 gw.datatype = "network"
346 gw.rmempty = false
347 uci:foreach("network", "interface", function(s)
348 local name=s['.name']
349 if is_wan(name) then
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())
354 end
355 end)
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")
359 end
360
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"))
367 x.rmempty = true
368 x.datatype = "range(1,63)"
369 end
370 end)
371
372 -- Includes
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"
376 inc.sortable = true
377 inc.anonymous = true
378 inc.addremove = true
379
380 finc = inc:option(Flag, "enabled", translate("Enabled"))
381 finc.optional = false
382 finc.default = "1"
383 inc:option(Value, "path", translate("Path")).optional = false
384
385 return m