Merge pull request #656 from nlhintz/pull-request
[project/luci.git] / modules / luci-mod-admin-full / luasrc / model / cbi / admin_network / ifaces.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008-2011 Jo-Philipp Wich <jow@openwrt.org>
3 -- Licensed to the public under the Apache License 2.0.
4
5 local fs = require "nixio.fs"
6 local ut = require "luci.util"
7 local pt = require "luci.tools.proto"
8 local nw = require "luci.model.network"
9 local fw = require "luci.model.firewall"
10
11 arg[1] = arg[1] or ""
12
13 local has_dnsmasq  = fs.access("/etc/config/dhcp")
14 local has_firewall = fs.access("/etc/config/firewall")
15
16 m = Map("network", translate("Interfaces") .. " - " .. arg[1]:upper(), translate("On this page you can configure the network interfaces. You can bridge several interfaces by ticking the \"bridge interfaces\" field and enter the names of several network interfaces separated by spaces. You can also use <abbr title=\"Virtual Local Area Network\">VLAN</abbr> notation <samp>INTERFACE.VLANNR</samp> (<abbr title=\"for example\">e.g.</abbr>: <samp>eth0.1</samp>)."))
17 m.redirect = luci.dispatcher.build_url("admin", "network", "network")
18 m:chain("wireless")
19
20 if has_firewall then
21         m:chain("firewall")
22 end
23
24 nw.init(m.uci)
25 fw.init(m.uci)
26
27
28 local net = nw:get_network(arg[1])
29
30 local function backup_ifnames(is_bridge)
31         if not net:is_floating() and not m:get(net:name(), "_orig_ifname") then
32                 local ifcs = net:get_interfaces() or { net:get_interface() }
33                 if ifcs then
34                         local _, ifn
35                         local ifns = { }
36                         for _, ifn in ipairs(ifcs) do
37                                 ifns[#ifns+1] = ifn:name()
38                         end
39                         if #ifns > 0 then
40                                 m:set(net:name(), "_orig_ifname", table.concat(ifns, " "))
41                                 m:set(net:name(), "_orig_bridge", tostring(net:is_bridge()))
42                         end
43                 end
44         end
45 end
46
47
48 -- redirect to overview page if network does not exist anymore (e.g. after a revert)
49 if not net then
50         luci.http.redirect(luci.dispatcher.build_url("admin/network/network"))
51         return
52 end
53
54 -- protocol switch was requested, rebuild interface config and reload page
55 if m:formvalue("cbid.network.%s._switch" % net:name()) then
56         -- get new protocol
57         local ptype = m:formvalue("cbid.network.%s.proto" % net:name()) or "-"
58         local proto = nw:get_protocol(ptype, net:name())
59         if proto then
60                 -- backup default
61                 backup_ifnames()
62
63                 -- if current proto is not floating and target proto is not floating,
64                 -- then attempt to retain the ifnames
65                 --error(net:proto() .. " > " .. proto:proto())
66                 if not net:is_floating() and not proto:is_floating() then
67                         -- if old proto is a bridge and new proto not, then clip the
68                         -- interface list to the first ifname only
69                         if net:is_bridge() and proto:is_virtual() then
70                                 local _, ifn
71                                 local first = true
72                                 for _, ifn in ipairs(net:get_interfaces() or { net:get_interface() }) do
73                                         if first then
74                                                 first = false
75                                         else
76                                                 net:del_interface(ifn)
77                                         end
78                                 end
79                                 m:del(net:name(), "type")
80                         end
81
82                 -- if the current proto is floating, the target proto not floating,
83                 -- then attempt to restore ifnames from backup
84                 elseif net:is_floating() and not proto:is_floating() then
85                         -- if we have backup data, then re-add all orphaned interfaces
86                         -- from it and restore the bridge choice
87                         local br = (m:get(net:name(), "_orig_bridge") == "true")
88                         local ifn
89                         local ifns = { }
90                         for ifn in ut.imatch(m:get(net:name(), "_orig_ifname")) do
91                                 ifn = nw:get_interface(ifn)
92                                 if ifn and not ifn:get_network() then
93                                         proto:add_interface(ifn)
94                                         if not br then
95                                                 break
96                                         end
97                                 end
98                         end
99                         if br then
100                                 m:set(net:name(), "type", "bridge")
101                         end
102
103                 -- in all other cases clear the ifnames
104                 else
105                         local _, ifc
106                         for _, ifc in ipairs(net:get_interfaces() or { net:get_interface() }) do
107                                 net:del_interface(ifc)
108                         end
109                         m:del(net:name(), "type")
110                 end
111
112                 -- clear options
113                 local k, v
114                 for k, v in pairs(m:get(net:name())) do
115                         if k:sub(1,1) ~= "." and
116                            k ~= "type" and
117                            k ~= "ifname" and
118                            k ~= "_orig_ifname" and
119                            k ~= "_orig_bridge"
120                         then
121                                 m:del(net:name(), k)
122                         end
123                 end
124
125                 -- set proto
126                 m:set(net:name(), "proto", proto:proto())
127                 m.uci:save("network")
128                 m.uci:save("wireless")
129
130                 -- reload page
131                 luci.http.redirect(luci.dispatcher.build_url("admin/network/network", arg[1]))
132                 return
133         end
134 end
135
136 -- dhcp setup was requested, create section and reload page
137 if m:formvalue("cbid.dhcp._enable._enable") then
138         m.uci:section("dhcp", "dhcp", arg[1], {
139                 interface = arg[1],
140                 start     = "100",
141                 limit     = "150",
142                 leasetime = "12h"
143         })
144
145         m.uci:save("dhcp")
146         luci.http.redirect(luci.dispatcher.build_url("admin/network/network", arg[1]))
147         return
148 end
149
150 local ifc = net:get_interface()
151
152 s = m:section(NamedSection, arg[1], "interface", translate("Common Configuration"))
153 s.addremove = false
154
155 s:tab("general",  translate("General Setup"))
156 s:tab("advanced", translate("Advanced Settings"))
157 s:tab("physical", translate("Physical Settings"))
158
159 if has_firewall then
160         s:tab("firewall", translate("Firewall Settings"))
161 end
162
163
164 st = s:taboption("general", DummyValue, "__status", translate("Status"))
165
166 local function set_status()
167         -- if current network is empty, print a warning
168         if not net:is_floating() and net:is_empty() then
169                 st.template = "cbi/dvalue"
170                 st.network  = nil
171                 st.value    = translate("There is no device assigned yet, please attach a network device in the \"Physical Settings\" tab")
172         else
173                 st.template = "admin_network/iface_status"
174                 st.network  = arg[1]
175                 st.value    = nil
176         end
177 end
178
179 m.on_init = set_status
180 m.on_after_save = set_status
181
182
183 p = s:taboption("general", ListValue, "proto", translate("Protocol"))
184 p.default = net:proto()
185
186
187 if not net:is_installed() then
188         p_install = s:taboption("general", Button, "_install")
189         p_install.title      = translate("Protocol support is not installed")
190         p_install.inputtitle = translate("Install package %q" % net:opkg_package())
191         p_install.inputstyle = "apply"
192         p_install:depends("proto", net:proto())
193
194         function p_install.write()
195                 return luci.http.redirect(
196                         luci.dispatcher.build_url("admin/system/packages") ..
197                         "?submit=1&install=%s" % net:opkg_package()
198                 )
199         end
200 end
201
202
203 p_switch = s:taboption("general", Button, "_switch")
204 p_switch.title      = translate("Really switch protocol?")
205 p_switch.inputtitle = translate("Switch protocol")
206 p_switch.inputstyle = "apply"
207
208 local _, pr
209 for _, pr in ipairs(nw:get_protocols()) do
210         p:value(pr:proto(), pr:get_i18n())
211         if pr:proto() ~= net:proto() then
212                 p_switch:depends("proto", pr:proto())
213         end
214 end
215
216
217 auto = s:taboption("advanced", Flag, "auto", translate("Bring up on boot"))
218 auto.default = (net:proto() == "none") and auto.disabled or auto.enabled
219
220 delegate = s:taboption("advanced", Flag, "delegate", translate("Use builtin IPv6-management"))
221 delegate.default = delegate.enabled
222
223
224 if not net:is_virtual() then
225         br = s:taboption("physical", Flag, "type", translate("Bridge interfaces"), translate("creates a bridge over specified interface(s)"))
226         br.enabled = "bridge"
227         br.rmempty = true
228         br:depends("proto", "static")
229         br:depends("proto", "dhcp")
230         br:depends("proto", "none")
231
232         stp = s:taboption("physical", Flag, "stp", translate("Enable <abbr title=\"Spanning Tree Protocol\">STP</abbr>"),
233                 translate("Enables the Spanning Tree Protocol on this bridge"))
234         stp:depends("type", "bridge")
235         stp.rmempty = true
236 end
237
238
239 if not net:is_floating() then
240         ifname_single = s:taboption("physical", Value, "ifname_single", translate("Interface"))
241         ifname_single.template = "cbi/network_ifacelist"
242         ifname_single.widget = "radio"
243         ifname_single.nobridges = true
244         ifname_single.rmempty = false
245         ifname_single.network = arg[1]
246         ifname_single:depends("type", "")
247
248         function ifname_single.cfgvalue(self, s)
249                 -- let the template figure out the related ifaces through the network model
250                 return nil
251         end
252
253         function ifname_single.write(self, s, val)
254                 local i
255                 local new_ifs = { }
256                 local old_ifs = { }
257
258                 for _, i in ipairs(net:get_interfaces() or { net:get_interface() }) do
259                         old_ifs[#old_ifs+1] = i:name()
260                 end
261
262                 for i in ut.imatch(val) do
263                         new_ifs[#new_ifs+1] = i
264
265                         -- if this is not a bridge, only assign first interface
266                         if self.option == "ifname_single" then
267                                 break
268                         end
269                 end
270
271                 table.sort(old_ifs)
272                 table.sort(new_ifs)
273
274                 for i = 1, math.max(#old_ifs, #new_ifs) do
275                         if old_ifs[i] ~= new_ifs[i] then
276                                 backup_ifnames()
277                                 for i = 1, #old_ifs do
278                                         net:del_interface(old_ifs[i])
279                                 end
280                                 for i = 1, #new_ifs do
281                                         net:add_interface(new_ifs[i])
282                                 end
283                                 break
284                         end
285                 end
286         end
287 end
288
289
290 if not net:is_virtual() then
291         ifname_multi = s:taboption("physical", Value, "ifname_multi", translate("Interface"))
292         ifname_multi.template = "cbi/network_ifacelist"
293         ifname_multi.nobridges = true
294         ifname_multi.rmempty = false
295         ifname_multi.network = arg[1]
296         ifname_multi.widget = "checkbox"
297         ifname_multi:depends("type", "bridge")
298         ifname_multi.cfgvalue = ifname_single.cfgvalue
299         ifname_multi.write = ifname_single.write
300 end
301
302
303 if has_firewall then
304         fwzone = s:taboption("firewall", Value, "_fwzone",
305                 translate("Create / Assign firewall-zone"),
306                 translate("Choose the firewall zone you want to assign to this interface. Select <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>create</em> field to define a new zone and attach the interface to it."))
307
308         fwzone.template = "cbi/firewall_zonelist"
309         fwzone.network = arg[1]
310         fwzone.rmempty = false
311
312         function fwzone.cfgvalue(self, section)
313                 self.iface = section
314                 local z = fw:get_zone_by_network(section)
315                 return z and z:name()
316         end
317
318         function fwzone.write(self, section, value)
319                 local zone = fw:get_zone(value)
320
321                 if not zone and value == '-' then
322                         value = m:formvalue(self:cbid(section) .. ".newzone")
323                         if value and #value > 0 then
324                                 zone = fw:add_zone(value)
325                         else
326                                 fw:del_network(section)
327                         end
328                 end
329
330                 if zone then
331                         fw:del_network(section)
332                         zone:add_network(section)
333                 end
334         end
335 end
336
337
338 function p.write() end
339 function p.remove() end
340 function p.validate(self, value, section)
341         if value == net:proto() then
342                 if not net:is_floating() and net:is_empty() then
343                         local ifn = ((br and (br:formvalue(section) == "bridge"))
344                                 and ifname_multi:formvalue(section)
345                              or ifname_single:formvalue(section))
346
347                         for ifn in ut.imatch(ifn) do
348                                 return value
349                         end
350                         return nil, translate("The selected protocol needs a device assigned")
351                 end
352         end
353         return value
354 end
355
356
357 local form, ferr = loadfile(
358         ut.libpath() .. "/model/cbi/admin_network/proto_%s.lua" % net:proto()
359 )
360
361 if not form then
362         s:taboption("general", DummyValue, "_error",
363                 translate("Missing protocol extension for proto %q" % net:proto())
364         ).value = ferr
365 else
366         setfenv(form, getfenv(1))(m, s, net)
367 end
368
369
370 local _, field
371 for _, field in ipairs(s.children) do
372         if field ~= st and field ~= p and field ~= p_install and field ~= p_switch then
373                 if next(field.deps) then
374                         local _, dep
375                         for _, dep in ipairs(field.deps) do
376                                 dep.proto = net:proto()
377                         end
378                 else
379                         field:depends("proto", net:proto())
380                 end
381         end
382 end
383
384
385 --
386 -- Display DNS settings if dnsmasq is available
387 --
388
389 if has_dnsmasq and net:proto() == "static" then
390         m2 = Map("dhcp", "", "")
391
392         local has_section = false
393
394         m2.uci:foreach("dhcp", "dhcp", function(s)
395                 if s.interface == arg[1] then
396                         has_section = true
397                         return false
398                 end
399         end)
400
401         if not has_section and has_dnsmasq then
402
403                 s = m2:section(TypedSection, "dhcp", translate("DHCP Server"))
404                 s.anonymous   = true
405                 s.cfgsections = function() return { "_enable" } end
406
407                 x = s:option(Button, "_enable")
408                 x.title      = translate("No DHCP Server configured for this interface")
409                 x.inputtitle = translate("Setup DHCP Server")
410                 x.inputstyle = "apply"
411
412         elseif has_section then
413
414                 s = m2:section(TypedSection, "dhcp", translate("DHCP Server"))
415                 s.addremove = false
416                 s.anonymous = true
417                 s:tab("general",  translate("General Setup"))
418                 s:tab("advanced", translate("Advanced Settings"))
419                 s:tab("ipv6", translate("IPv6 Settings"))
420
421                 function s.filter(self, section)
422                         return m2.uci:get("dhcp", section, "interface") == arg[1]
423                 end
424
425                 local ignore = s:taboption("general", Flag, "ignore",
426                         translate("Ignore interface"),
427                         translate("Disable <abbr title=\"Dynamic Host Configuration Protocol\">DHCP</abbr> for " ..
428                                 "this interface."))
429
430                 local start = s:taboption("general", Value, "start", translate("Start"),
431                         translate("Lowest leased address as offset from the network address."))
432                 start.optional = true
433                 start.datatype = "or(uinteger,ip4addr)"
434                 start.default = "100"
435
436                 local limit = s:taboption("general", Value, "limit", translate("Limit"),
437                         translate("Maximum number of leased addresses."))
438                 limit.optional = true
439                 limit.datatype = "uinteger"
440                 limit.default = "150"
441
442                 local ltime = s:taboption("general", Value, "leasetime", translate("Leasetime"),
443                         translate("Expiry time of leased addresses, minimum is 2 minutes (<code>2m</code>)."))
444                 ltime.rmempty = true
445                 ltime.default = "12h"
446
447                 local dd = s:taboption("advanced", Flag, "dynamicdhcp",
448                         translate("Dynamic <abbr title=\"Dynamic Host Configuration Protocol\">DHCP</abbr>"),
449                         translate("Dynamically allocate DHCP addresses for clients. If disabled, only " ..
450                                 "clients having static leases will be served."))
451                 dd.default = dd.enabled
452
453                 s:taboption("advanced", Flag, "force", translate("Force"),
454                         translate("Force DHCP on this network even if another server is detected."))
455
456                 -- XXX: is this actually useful?
457                 --s:taboption("advanced", Value, "name", translate("Name"),
458                 --      translate("Define a name for this network."))
459
460                 mask = s:taboption("advanced", Value, "netmask",
461                         translate("<abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Netmask"),
462                         translate("Override the netmask sent to clients. Normally it is calculated " ..
463                                 "from the subnet that is served."))
464
465                 mask.optional = true
466                 mask.datatype = "ip4addr"
467
468                 s:taboption("advanced", DynamicList, "dhcp_option", translate("DHCP-Options"),
469                         translate("Define additional DHCP options, for example \"<code>6,192.168.2.1," ..
470                                 "192.168.2.2</code>\" which advertises different DNS servers to clients."))
471
472                 for i, n in ipairs(s.children) do
473                         if n ~= ignore then
474                                 n:depends("ignore", "")
475                         end
476                 end
477
478                 o = s:taboption("ipv6", ListValue, "ra", translate("Router Advertisement-Service"))
479                 o:value("", translate("disabled"))
480                 o:value("server", translate("server mode"))
481                 o:value("relay", translate("relay mode"))
482                 o:value("hybrid", translate("hybrid mode"))
483
484                 o = s:taboption("ipv6", ListValue, "dhcpv6", translate("DHCPv6-Service"))
485                 o:value("", translate("disabled"))
486                 o:value("server", translate("server mode"))
487                 o:value("relay", translate("relay mode"))
488                 o:value("hybrid", translate("hybrid mode"))
489
490                 o = s:taboption("ipv6", ListValue, "ndp", translate("NDP-Proxy"))
491                 o:value("", translate("disabled"))
492                 o:value("relay", translate("relay mode"))
493                 o:value("hybrid", translate("hybrid mode"))
494
495                 o = s:taboption("ipv6", ListValue, "ra_management", translate("DHCPv6-Mode"),
496                         translate("Default is stateless + stateful"))
497                 o:value("0", translate("stateless"))
498                 o:value("1", translate("stateless + stateful"))
499                 o:value("2", translate("stateful-only"))
500                 o:depends("dhcpv6", "server")
501                 o:depends("dhcpv6", "hybrid")
502                 o.default = "1"
503
504                 o = s:taboption("ipv6", Flag, "ra_default", translate("Always announce default router"),
505                         translate("Announce as default router even if no public prefix is available."))
506                 o:depends("ra", "server")
507                 o:depends("ra", "hybrid")
508
509                 s:taboption("ipv6", DynamicList, "dns", translate("Announced DNS servers"))
510                 s:taboption("ipv6", DynamicList, "domain", translate("Announced DNS domains"))
511
512         else
513                 m2 = nil
514         end
515 end
516
517
518 return m, m2