]> git.proxmox.com Git - pve-network.git/blob - src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
bump version to 0.9.8
[pve-network.git] / src / PVE / Network / SDN / Dhcp / Dnsmasq.pm
1 package PVE::Network::SDN::Dhcp::Dnsmasq;
2
3 use strict;
4 use warnings;
5
6 use base qw(PVE::Network::SDN::Dhcp::Plugin);
7
8 use Net::IP qw(:PROC);
9 use PVE::Tools qw(file_set_contents run_command lock_file);
10
11 use File::Copy;
12 use Net::DBus;
13
14 use PVE::RESTEnvironment qw(log_warn);
15
16 my $DNSMASQ_CONFIG_ROOT = '/etc/dnsmasq.d';
17 my $DNSMASQ_DEFAULT_ROOT = '/etc/default';
18 my $DNSMASQ_LEASE_ROOT = '/var/lib/misc';
19
20 sub type {
21 return 'dnsmasq';
22 }
23
24 my sub assert_dnsmasq_installed {
25 my ($noerr) = @_;
26
27 my $bin_path = "/usr/sbin/dnsmasq";
28 if (!-e $bin_path) {
29 return if $noerr; # just ignore, e.g., in case zone doesn't use DHCP at all
30 log_warn("please install the 'dnsmasq' package in order to use the DHCP feature!");
31 die "cannot reload with missing 'dnsmasq' package\n";
32 }
33 return 1;
34 }
35
36 sub ethers_file {
37 my ($dhcpid) = @_;
38 return "$DNSMASQ_CONFIG_ROOT/$dhcpid/ethers";
39 }
40
41 sub update_lease {
42 my ($dhcpid, $ip4, $mac) = @_;
43 #update lease as ip could still be associated to an old removed mac
44 my $bus = Net::DBus->system();
45 my $dnsmasq = $bus->get_service("uk.org.thekelleys.dnsmasq.$dhcpid");
46 my $manager = $dnsmasq->get_object("/uk/org/thekelleys/dnsmasq","uk.org.thekelleys.dnsmasq.$dhcpid");
47
48 my @hostname = unpack("C*", "*");
49 $manager->AddDhcpLease($ip4, $mac, \@hostname, undef, 0, 0, 0) if $ip4;
50 }
51
52 sub add_ip_mapping {
53 my ($class, $dhcpid, $macdb, $mac, $ip4, $ip6) = @_;
54
55 my $ethers_file = ethers_file($dhcpid);
56 my $ethers_tmp_file = "$ethers_file.tmp";
57
58 my $reload = undef;
59
60 my $appendFn = sub {
61 open(my $in, '<', $ethers_file) or die "Could not open file '$ethers_file' $!\n";
62 open(my $out, '>', $ethers_tmp_file) or die "Could not open file '$ethers_tmp_file' $!\n";
63
64 my $match = undef;
65
66 while (my $line = <$in>) {
67 chomp($line);
68 my $parsed_ip4 = undef;
69 my $parsed_ip6 = undef;
70 my ($parsed_mac, $parsed_ip1, $parsed_ip2) = split(/,/, $line);
71
72 if ($parsed_ip2) {
73 $parsed_ip4 = $parsed_ip1;
74 $parsed_ip6 = $parsed_ip2;
75 } elsif (Net::IP::ip_is_ipv4($parsed_ip1)) {
76 $parsed_ip4 = $parsed_ip1;
77 } else {
78 $parsed_ip6 = $parsed_ip1;
79 }
80 $parsed_ip6 = $1 if $parsed_ip6 && $parsed_ip6 =~ m/\[(\S+)\]/;
81
82 #delete changed
83 if (!defined($macdb->{macs}->{$parsed_mac}) ||
84 ($parsed_ip4 && $macdb->{macs}->{$parsed_mac}->{'ip4'} && $macdb->{macs}->{$parsed_mac}->{'ip4'} ne $parsed_ip4) ||
85 ($parsed_ip6 && $macdb->{macs}->{$parsed_mac}->{'ip6'} && $macdb->{macs}->{$parsed_mac}->{'ip6'} ne $parsed_ip6)) {
86 $reload = 1;
87 next;
88 }
89
90 if ($parsed_mac eq $mac) {
91 $match = 1 if $ip4 && $parsed_ip4 && $ip4;
92 $match = 1 if $ip6 && $parsed_ip6 && $ip6;
93 }
94
95 print $out "$line\n";
96 }
97
98 if(!$match) {
99 my $reservation = $mac;
100 $reservation .= ",$ip4" if $ip4;
101 $reservation .= ",[$ip6]" if $ip6;
102 print $out "$reservation\n";
103 $reload = 1;
104 }
105
106 close $in;
107 close $out;
108 move $ethers_tmp_file, $ethers_file;
109 chmod 0644, $ethers_file;
110 };
111
112 PVE::Tools::lock_file($ethers_file, 10, $appendFn);
113
114 if ($@) {
115 warn "Unable to add $mac to the dnsmasq configuration: $@\n";
116 return;
117 }
118
119 my $service_name = "dnsmasq\@$dhcpid";
120 systemctl_service('reload', $service_name) if $reload;
121 update_lease($dhcpid, $ip4, $mac);
122 }
123
124 sub configure_subnet {
125 my ($class, $config, $dhcpid, $vnetid, $subnet_config) = @_;
126
127 die "No gateway defined for subnet $subnet_config->{id}"
128 if !$subnet_config->{gateway};
129
130 my $tag = $subnet_config->{id};
131
132 my $option_string;
133 if (ip_is_ipv6($subnet_config->{network})) {
134 $option_string = 'option6';
135 } else {
136 $option_string = 'option';
137 push @{$config}, "dhcp-option=tag:$tag,$option_string:router,$subnet_config->{gateway}";
138 }
139
140 push @{$config}, "dhcp-option=tag:$tag,$option_string:dns-server,$subnet_config->{'dhcp-dns-server'}"
141 if $subnet_config->{'dhcp-dns-server'};
142
143 }
144
145 sub configure_range {
146 my ($class, $config, $dhcpid, $vnetid, $subnet_config, $range_config) = @_;
147
148 my $tag = $subnet_config->{id};
149
150 my ($zone, $network, $mask) = split(/-/, $tag);
151
152 if (Net::IP::ip_is_ipv4($network)) {
153 $mask = (2 ** $mask - 1) << (32 - $mask);
154 $mask = join( '.', unpack( "C4", pack( "N", $mask ) ) );
155 }
156
157 push @{$config}, "dhcp-range=set:$tag,$network,static,$mask,infinite";
158 }
159
160 sub configure_vnet {
161 my ($class, $config, $dhcpid, $vnetid, $vnet_config) = @_;
162
163 return if @{$config} < 1;
164
165 push @{$config}, "interface=$vnetid";
166
167 PVE::Tools::file_set_contents(
168 "$DNSMASQ_CONFIG_ROOT/$dhcpid/10-$vnetid.conf",
169 join("\n", @{$config}) . "\n"
170 );
171 }
172
173 sub systemctl_service {
174 my ($action, $service) = @_;
175
176 PVE::Tools::run_command(['systemctl', $action, $service]);
177 }
178
179 sub before_configure {
180 my ($class, $dhcpid, $zone_cfg) = @_;
181
182 my $dbus_config = <<DBUSCFG;
183 <!DOCTYPE busconfig PUBLIC
184 "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
185 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
186 <busconfig>
187 <policy user="root">
188 <allow own="uk.org.thekelleys.dnsmasq.$dhcpid"/>
189 <allow send_destination="uk.org.thekelleys.dnsmasq.$dhcpid"/>
190 </policy>
191 <policy user="dnsmasq">
192 <allow own="uk.org.thekelleys.dnsmasq.$dhcpid"/>
193 <allow send_destination="uk.org.thekelleys.dnsmasq.$dhcpid"/>
194 </policy>
195 <policy context="default">
196 <deny own="uk.org.thekelleys.dnsmasq.$dhcpid"/>
197 <deny send_destination="uk.org.thekelleys.dnsmasq.$dhcpid"/>
198 </policy>
199 </busconfig>
200 DBUSCFG
201
202 PVE::Tools::file_set_contents(
203 "/etc/dbus-1/system.d/dnsmasq.$dhcpid.conf",
204 $dbus_config
205 );
206
207 my $config_directory = "$DNSMASQ_CONFIG_ROOT/$dhcpid";
208
209 mkdir($config_directory, 0755) if !-d $config_directory;
210
211 my $default_config = <<CFG;
212 CONFIG_DIR='$config_directory,\*.conf'
213 DNSMASQ_OPTS="--conf-file=/dev/null --enable-dbus=uk.org.thekelleys.dnsmasq.$dhcpid"
214 CFG
215
216 PVE::Tools::file_set_contents(
217 "$DNSMASQ_DEFAULT_ROOT/dnsmasq.$dhcpid",
218 $default_config
219 );
220
221 my $mtu = PVE::Network::SDN::Zones::get_mtu($zone_cfg);
222
223 my $default_dnsmasq_config = <<CFG;
224 except-interface=lo
225 enable-ra
226 quiet-ra
227 bind-dynamic
228 no-hosts
229 dhcp-leasefile=$DNSMASQ_LEASE_ROOT/dnsmasq.$dhcpid.leases
230 dhcp-hostsfile=$config_directory/ethers
231 dhcp-ignore=tag:!known
232
233 dhcp-option=26,$mtu
234 ra-param=*,mtu:$mtu,0
235
236 # Send an empty WPAD option. This may be REQUIRED to get windows 7 to behave.
237 dhcp-option=252,"\\n"
238
239 # Send microsoft-specific option to tell windows to release the DHCP lease
240 # when it shuts down. Note the "i" flag, to tell dnsmasq to send the
241 # value as a four-byte integer - that's what microsoft wants.
242 dhcp-option=vendor:MSFT,2,1i
243
244 # If a DHCP client claims that its name is "wpad", ignore that.
245 # This fixes a security hole. see CERT Vulnerability VU#598349
246 dhcp-name-match=set:wpad-ignore,wpad
247 dhcp-ignore-names=tag:wpad-ignore
248 CFG
249
250 PVE::Tools::file_set_contents(
251 "$config_directory/00-default.conf",
252 $default_dnsmasq_config
253 );
254
255 my @config_files = ();
256 PVE::Tools::dir_glob_foreach($config_directory, '10-.*\.conf', sub {
257 my ($file) = @_;
258 push @config_files, "$config_directory/$file";
259 });
260
261 unlink @config_files;
262 }
263
264 sub after_configure {
265 my ($class, $dhcpid, $noerr) = @_;
266
267 return if !assert_dnsmasq_installed($noerr);
268
269 my $service_name = "dnsmasq\@$dhcpid";
270
271 systemctl_service('reload', 'dbus');
272 systemctl_service('enable', $service_name);
273 systemctl_service('restart', $service_name);
274 }
275
276 sub before_regenerate {
277 my ($class, $noerr) = @_;
278
279 return if !assert_dnsmasq_installed($noerr);
280
281 systemctl_service('stop', "dnsmasq@*");
282 systemctl_service('disable', 'dnsmasq@');
283 }
284
285 sub after_regenerate {
286 my ($class) = @_;
287 # noop
288 }
289
290 1;