11#!/usr/bin/python3
22
33#
4- # Copyright (C) 2023 Nethesis S.r.l.
4+ # Copyright (C) 2026 Nethesis S.r.l.
55# SPDX-License-Identifier: GPL-2.0-only
66#
77
@@ -52,7 +52,6 @@ def list_custom_dhcp_options():
5252 continue
5353 return ret
5454
55-
5655def list_dhcp_options ():
5756 ret = {}
5857 pd = subprocess .run (["dnsmasq" , "--help" , "dhcp" ], capture_output = True , text = True )
@@ -82,7 +81,7 @@ def list_interfaces():
8281 if not dhcps :
8382 dhcps = []
8483 for i in interfaces :
85- record = {"device" : "" , "start" : "" , "end" : "" , " active" : False , "options" : {}, "force" : False , "ns_binding" : 0 }
84+ record = {"device" : "" , "active" : False , "options" : {}, "force" : False , "ns_binding" : 0 , "ranges" : [] }
8685 # skip loopback, bond devices, wans, aliases and interfaces with no IP address (e.g. DHCP interfaces)
8786 if i == "loopback" or i in wans or not ('ipaddr' in interfaces [i ]) or interfaces [i ].get ('proto' ) == 'bonding' or interfaces [i ].get ('device' , '' ).startswith ('@' ):
8887 continue
@@ -94,7 +93,7 @@ def list_interfaces():
9493 if 'interface' in ds and ds ['interface' ] == i :
9594 if 'ignore' in ds and ds ['ignore' ] == "1" :
9695 continue
97- (record [ ' first' ], record [ ' last' ] ) = conf_to_range (interfaces [i ]['ipaddr' ], interfaces [i ]['netmask' ], ds .get ('start' , 100 ), ds .get ('limit' , 150 ))
96+ (first , last ) = conf_to_range (interfaces [i ]['ipaddr' ], interfaces [i ]['netmask' ], ds .get ('start' , 100 ), ds .get ('limit' , 150 ))
9897 record ['active' ] = ds .get ('dhcpv4' , 'server' ) == 'server'
9998 # handle existing active configurations:
10099 # - old default was force off, as OpenWrt
@@ -111,7 +110,7 @@ def list_interfaces():
111110 tmp = opt .split ("," )
112111 tmp [0 ] = tmp [0 ].removeprefix ('option:' )
113112 record ['options' ][tmp [0 ]] = " " .join (tmp [1 :])
114- break
113+ record [ 'ranges' ]. append ({ "first" : first , "last" : last })
115114 ret [i ] = record
116115
117116 return ret
@@ -125,66 +124,89 @@ def edit_interface(args):
125124 # ns-binding validation, this check is more controller-safe
126125 if 'ns_binding' in args and args ['ns_binding' ] not in [0 , 1 , 2 ]:
127126 return utils .validation_error ("ns_binding" , "invalid" , args ["ns_binding" ])
128- # handle migrated dhcp config: issue #855
129- dhcp_id = args ['interface' ]
127+
128+ ipaddr = u .get ('network' , args ['interface' ], 'ipaddr' )
129+ netmask = u .get ('network' , args ['interface' ], 'netmask' )
130+ network = ipaddress .ip_network (f"{ ipaddr } /{ netmask } " , strict = False )
131+
132+ # ranges validation
133+ for r in args ['ranges' ]:
134+ r ['first' ] = r ['first' ].strip ()
135+ r ['last' ] = r ['last' ].strip ()
136+ if not r ['first' ]:
137+ return utils .validation_error ("first" , "first_is_empty" , r ["first" ])
138+ if not r ['last' ]:
139+ return utils .validation_error ("last" , "last_is_empty" , r ["last" ])
140+ first_ip = ipaddress .ip_address (r ['first' ])
141+ last_ip = ipaddress .ip_address (r ['last' ])
142+ if not (network .network_address < first_ip < network .broadcast_address ):
143+ return utils .validation_error ("first" , "ip_out_of_interface_network" , r ["first" ])
144+ if not (network .network_address < last_ip < network .broadcast_address ):
145+ return utils .validation_error ("last" , "ip_out_of_interface_network" , r ["last" ])
146+ if ip2int (r ['last' ]) <= ip2int (r ['first' ]):
147+ return utils .validation_error ("last" , "last_must_be_greater_than_first" , r ["last" ])
148+
130149 dhcp_servers = utils .get_all_by_type (u , 'dhcp' , 'dhcp' )
131150 for server in dhcp_servers :
132151 if dhcp_servers [server ]['interface' ] == args ['interface' ]:
133- dhcp_id = server
152+ u .delete ('dhcp' , server )
153+
154+ for r in args ['ranges' ]:
155+ dhcp_id = f"{ args ['interface' ]} _{ utils .get_random_id ()} "
156+ (start , limit ) = range_to_conf (ipaddr , netmask , r ["first" ], r ["last" ])
157+ u .set ("dhcp" , dhcp_id , 'dhcp' )
158+ if args ['active' ]:
159+ u .set ("dhcp" , dhcp_id , 'dhcpv4' , 'server' )
160+ else :
161+ u .set ("dhcp" , dhcp_id , 'dhcpv4' , 'disabled' )
162+ u .set ("dhcp" , dhcp_id , "limit" , limit )
163+ u .set ("dhcp" , dhcp_id , "start" , start )
164+ u .set ("dhcp" , dhcp_id , "leasetime" , args ["leasetime" ])
165+ u .set ("dhcp" , dhcp_id , "interface" , args ['interface' ])
166+ u .set ("dhcp" , dhcp_id , "force" , '1' if args .get ('force' , False ) else '0' )
167+ u .set ("dhcp" , dhcp_id , "instance" , "ns_dnsmasq" )
168+ if 'ns_binding' in args :
169+ u .set ("dhcp" , dhcp_id , "ns_binding" , args .get ('ns_binding' ))
170+ u .set ("dhcp" , dhcp_id , "ignore" , '0' )
171+ opts = []
172+ for opt in args ['options' ]:
173+ k = list (opt .keys ())[0 ]
174+ v = opt [k ].strip ().rstrip (',' )
175+ if v :
176+ if k .isdigit (): # custom option, no "option" prefix
177+ opts .append (f"{ k } ,{ v } " )
178+ else :
179+ opts .append (f"option:{ k } ,{ v } " )
180+ u .set ("dhcp" , dhcp_id , "dhcp_option" , opts )
134181
135- u .set ("dhcp" , dhcp_id , 'dhcp' )
136- ipaddr = u .get ('network' , args ['interface' ], 'ipaddr' )
137- netmask = u .get ('network' , args ['interface' ], 'netmask' )
138- args ['first' ] = args ['first' ].strip ()
139- args ['last' ] = args ['last' ].strip ()
140- if ip2int (args ['first' ]) > ip2int (args ['last' ]):
141- return utils .validation_error ("last" , "last_must_be_greater_than_first" , args ["last" ])
142- (start , limit ) = range_to_conf (ipaddr , netmask , args ["first" ].strip (), args ["last" ].strip ())
143- if args ['active' ]:
144- u .set ("dhcp" , dhcp_id , 'dhcpv4' , 'server' )
145- else :
146- u .set ("dhcp" , dhcp_id , 'dhcpv4' , 'disabled' )
147- u .set ("dhcp" , dhcp_id , "limit" , limit )
148- u .set ("dhcp" , dhcp_id , "start" , start )
149- u .set ("dhcp" , dhcp_id , "leasetime" , args ["leasetime" ])
150- u .set ("dhcp" , dhcp_id , "interface" , args ['interface' ])
151- u .set ("dhcp" , dhcp_id , "force" , '1' if args .get ('force' , False ) else '0' )
152- u .set ("dhcp" , dhcp_id , "instance" , "ns_dnsmasq" )
153- if 'ns_binding' in args :
154- u .set ("dhcp" , dhcp_id , "ns_binding" , args .get ('ns_binding' ))
155- opts = []
156- for opt in args ['options' ]:
157- k = list (opt .keys ())[0 ]
158- v = opt [k ].strip ().rstrip (',' )
159- if v :
160- if k .isdigit (): # custom option, no "option" preffix
161- opts .append (f"{ k } ,{ v } " )
162- else :
163- opts .append (f"option:{ k } ,{ v } " )
164- u .set ("dhcp" , dhcp_id , "dhcp_option" , opts )
165- u .set ("dhcp" , dhcp_id , "ignore" , '0' )
166182 u .save ("dhcp" )
167183 return {"interface" : args ["interface" ]}
168184
169185def get_interface (args ):
170186 u = EUci ()
171- ret = {"interface" : args ["interface" ], "options" : [], "first" : "" , "last" : "" , "active" : False }
172- try :
173- items = utils .get_all_by_type (u , "dhcp" , "dhcp" )
174- for i in items :
175- if items [i ].get ('interface' ) == args ['interface' ]:
176- dhcp = u .get_all ('dhcp' , i )
177- if not dhcp :
178- raise Exception # fallback to default
179- except :
180- dhcp = {"start" : 100 , "limit" : 150 , "leasetime" : "12h" , "dhcpv4" : "disabled" , "dhcp_option" : []}
187+ ret = {"interface" : args ["interface" ], "options" : [], "active" : False , "ranges" : []}
181188 try :
182189 interface = u .get_all ("network" , args ["interface" ])
183190 except :
184191 interface = {}
185192
193+ items = utils .get_all_by_type (u , "dhcp" , "dhcp" )
194+ dhcp_sections = []
195+ for i in items :
196+ if items [i ].get ('interface' ) == args ['interface' ] and items [i ].get ('ignore' , '0' ) != '1' :
197+ dhcp_sections .append (u .get_all ('dhcp' , i ))
198+
199+ if not dhcp_sections :
200+ dhcp_sections = [{"start" : 100 , "limit" : 150 , "leasetime" : "12h" , "dhcpv4" : "disabled" , "dhcp_option" : []}]
201+
202+ # get options from the first dhcp section since they are the same for all sections
203+ dhcp = dhcp_sections [0 ]
204+
186205 if 'ipaddr' in interface :
187- (ret ['first' ], ret ['last' ]) = conf_to_range (interface ['ipaddr' ], interface ['netmask' ], dhcp .get ('start' , 100 ), dhcp .get ('limit' , 150 ))
206+ for ds in dhcp_sections :
207+ (first , last ) = conf_to_range (interface ['ipaddr' ], interface ['netmask' ], ds .get ('start' , 100 ), ds .get ('limit' , 150 ))
208+ ret ['ranges' ].append ({"first" : first , "last" : last })
209+
188210 ret ["leasetime" ] = dhcp .get ("leasetime" )
189211 ret ["active" ] = dhcp .get ("dhcpv4" , "server" ) == "server"
190212 # handle existing active configurations:
@@ -201,6 +223,10 @@ def get_interface(args):
201223 tmp [0 ] = tmp [0 ].removeprefix ('option:' )
202224 ret ["options" ].append ({tmp [0 ]: "," .join (tmp [1 :])})
203225 ret ['ns_binding' ] = int (dhcp .get ('ns_binding' , 0 ))
226+ if 'ipaddr' in interface and 'netmask' in interface :
227+ network = ipaddress .ip_network (f"{ interface ['ipaddr' ]} /{ interface ['netmask' ]} " , strict = False )
228+ ret ['class_start_ip' ] = str (network .network_address + 1 )
229+ ret ['class_end_ip' ] = str (network .broadcast_address - 1 )
204230
205231 return ret
206232
@@ -261,7 +287,6 @@ def is_reserved(u, mac, exclude_lease_id=''):
261287
262288 return False
263289
264-
265290def count_ip_occurrences (e_uci , ip ):
266291 occurrences = 0
267292 for item in utils .get_all_by_type (e_uci , 'dhcp' , 'host' ):
@@ -272,7 +297,6 @@ def count_ip_occurrences(e_uci, ip):
272297
273298 return occurrences
274299
275-
276300def add_static_lease (args ):
277301 u = EUci ()
278302 if is_reserved (u , args ["macaddr" ]):
@@ -346,8 +370,10 @@ if cmd == 'list':
346370 "get-interface" : {"interface" : "lan" },
347371 "edit-interface" : {
348372 "interface" : "lan" ,
349- "first" : "192.168.100.2" ,
350- "last" : "192.168.100.150" ,
373+ "ranges" : [
374+ {"first" : "192.168.100.2" , "last" : "192.168.100.150" },
375+ {"first" : "192.168.100.200" , "last" : "192.168.100.220" }
376+ ],
351377 "leasetime" : "12h" ,
352378 "active" : True ,
353379 "force" : True ,
0 commit comments