]> git.proxmox.com Git - pve-installer.git/blob - Proxmox/Sys/Net.pm
proxinstall: avoid open-coding FQDN sanity check
[pve-installer.git] / Proxmox / Sys / Net.pm
1 package Proxmox::Sys::Net;
2
3 use strict;
4 use warnings;
5
6 use base qw(Exporter);
7 our @EXPORT_OK = qw(parse_ip_address parse_ip_mask parse_fqdn);
8
9 our $HOSTNAME_RE = "(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
10 our $FQDN_RE = "(?:${HOSTNAME_RE}\.)*${HOSTNAME_RE}";
11
12 my $IPV4OCTET = "(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])";
13 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
14 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
15 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
16
17 my $IPV6RE = "(?:" .
18 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
19 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
20 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
21 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
22 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
23 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
24 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
25 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
26 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
27
28 my $IPRE = "(?:$IPV4RE|$IPV6RE)";
29
30
31 my $ipv4_mask_hash = {
32 '128.0.0.0' => 1,
33 '192.0.0.0' => 2,
34 '224.0.0.0' => 3,
35 '240.0.0.0' => 4,
36 '248.0.0.0' => 5,
37 '252.0.0.0' => 6,
38 '254.0.0.0' => 7,
39 '255.0.0.0' => 8,
40 '255.128.0.0' => 9,
41 '255.192.0.0' => 10,
42 '255.224.0.0' => 11,
43 '255.240.0.0' => 12,
44 '255.248.0.0' => 13,
45 '255.252.0.0' => 14,
46 '255.254.0.0' => 15,
47 '255.255.0.0' => 16,
48 '255.255.128.0' => 17,
49 '255.255.192.0' => 18,
50 '255.255.224.0' => 19,
51 '255.255.240.0' => 20,
52 '255.255.248.0' => 21,
53 '255.255.252.0' => 22,
54 '255.255.254.0' => 23,
55 '255.255.255.0' => 24,
56 '255.255.255.128' => 25,
57 '255.255.255.192' => 26,
58 '255.255.255.224' => 27,
59 '255.255.255.240' => 28,
60 '255.255.255.248' => 29,
61 '255.255.255.252' => 30,
62 '255.255.255.254' => 31,
63 '255.255.255.255' => 32
64 };
65
66 my $ipv4_reverse_mask = [
67 '0.0.0.0',
68 '128.0.0.0',
69 '192.0.0.0',
70 '224.0.0.0',
71 '240.0.0.0',
72 '248.0.0.0',
73 '252.0.0.0',
74 '254.0.0.0',
75 '255.0.0.0',
76 '255.128.0.0',
77 '255.192.0.0',
78 '255.224.0.0',
79 '255.240.0.0',
80 '255.248.0.0',
81 '255.252.0.0',
82 '255.254.0.0',
83 '255.255.0.0',
84 '255.255.128.0',
85 '255.255.192.0',
86 '255.255.224.0',
87 '255.255.240.0',
88 '255.255.248.0',
89 '255.255.252.0',
90 '255.255.254.0',
91 '255.255.255.0',
92 '255.255.255.128',
93 '255.255.255.192',
94 '255.255.255.224',
95 '255.255.255.240',
96 '255.255.255.248',
97 '255.255.255.252',
98 '255.255.255.254',
99 '255.255.255.255',
100 ];
101
102 # returns (addr, version) tuple
103 sub parse_ip_address {
104 my ($text) = @_;
105
106 if ($text =~ m!^\s*($IPV4RE)\s*$!) {
107 return ($1, 4);
108 } elsif ($text =~ m!^\s*($IPV6RE)\s*$!) {
109 return ($1, 6);
110 }
111 return (undef, undef);
112 }
113
114 sub parse_ip_mask {
115 my ($text, $ip_version) = @_;
116 $text =~ s/^\s+//;
117 $text =~ s/\s+$//;
118 if ($ip_version == 6 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 126) {
119 return $text;
120 } elsif ($ip_version == 4 && ($text =~ m/^(\d+)$/) && $1 >= 8 && $1 <= 32) {
121 return $text;
122 } elsif ($ip_version == 4 && defined($ipv4_mask_hash->{$text})) {
123 # costs nothing to handle 255.x.y.z style masks, so continue to allow it
124 return $ipv4_mask_hash->{$text};
125 }
126 return;
127 }
128
129 sub get_ip_config {
130 my $ifaces = {};
131 my $default;
132
133 my $links = `ip -o l`;
134 foreach my $l (split /\n/,$links) {
135 my ($index, $name, $flags, $state, $mac) = $l =~ m/^(\d+):\s+(\S+):\s+<(\S+)>.*\s+state\s+(\S+)\s+.*\s+link\/ether\s+(\S+)\s+/;
136 next if !$name || $name eq 'lo';
137
138 my $driver = readlink "/sys/class/net/$name/device/driver" || 'unknown';
139 $driver =~ s!^.*/!!;
140
141 $ifaces->{"$index"} = {
142 name => $name,
143 driver => $driver,
144 flags => $flags,
145 state => $state,
146 mac => $mac,
147 };
148
149 my $addresses = `ip -o a s $name`;
150 for my $addr_line (split /\n/,$addresses) {
151 my ($family, $ip, $prefix) = $addr_line =~ m/^\Q$index\E:\s+\Q$name\E\s+(inet|inet6)\s+($IPRE)\/(\d+)\s+/;
152 next if !$ip;
153 next if $addr_line =~ /scope\s+link/; # ignore link local
154
155 my $mask = $prefix;
156
157 if ($family eq 'inet') {
158 next if !$ip =~ /$IPV4RE/;
159 next if $prefix < 8 || $prefix > 32;
160 $mask = @$ipv4_reverse_mask[$prefix];
161 } else {
162 next if !$ip =~ /$IPV6RE/;
163 }
164
165 $default = $index if !$default;
166
167 $ifaces->{"$index"}->{"$family"} = {
168 prefix => $prefix,
169 mask => $mask,
170 addr => $ip,
171 };
172 }
173 }
174
175
176 my $route = `ip route`;
177 my ($gateway) = $route =~ m/^default\s+via\s+(\S+)\s+/m;
178
179 my $resolvconf = `cat /etc/resolv.conf`;
180 my ($dnsserver) = $resolvconf =~ m/^nameserver\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/m;
181 my ($domain) = $resolvconf =~ m/^domain\s+(\S+)$/m;
182
183 return {
184 default => $default,
185 ifaces => $ifaces,
186 gateway => $gateway,
187 dnsserver => $dnsserver,
188 domain => $domain,
189 }
190 }
191
192 # Tries to detect the FQDN hostname for this system via DHCP, if available.
193 #
194 # DHCP server can set option 12 to inform the client about it's hostname [0]. dhclient dumps all
195 # options set by the DHCP server it in lease file, so just read it from there.
196 # [0] RFC 2132, section 3.14
197 sub get_dhcp_fqdn : prototype() {
198 my $leasefile = '/var/lib/dhcp/dhclient.leases';
199 return if ! -f $leasefile;
200
201 open(my $fh, '<', $leasefile) or return;
202
203 my $name = undef;
204 while (my $line = <$fh>) {
205 # "The name may or may not be qualified with the local domain name"
206 # Thus, only match the first part.
207 if ($line =~ m/^\s+option host-name \"(${FQDN_RE})\";$/) {
208 $name = $1;
209 last;
210 }
211 }
212
213 close($fh);
214 return $name if defined($name) && $name =~ m/^([^\.]+)(?:\.(?:\S+))?$/;
215 }
216
217 # Follows the rules as laid out by proxmox_installer_common::utils::Fqdn
218 sub parse_fqdn : prototype($) {
219 my ($text) = @_;
220
221 die "FQDN cannot be empty\n"
222 if !$text || length($text) == 0;
223
224 die "Purely numeric hostnames are not allowed\n"
225 if $text =~ /^[0-9]+(?:\.|$)/;
226
227 die "FQDN must only consist of alphanumeric characters and dashes\n"
228 if $text !~ m/^${Proxmox::Sys::Net::FQDN_RE}$/;
229
230 if ($text =~ m/^([^\.]+)\.(\S+)$/) {
231 return ($1, $2);
232 }
233
234 die "Hostname does not look like a fully qualified domain name\n";
235 }
236
237 1;