]>
Commit | Line | Data |
---|---|---|
0854fb22 DM |
1 | package PMG::Cluster; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
45e68618 | 5 | use Data::Dumper; |
0854fb22 | 6 | use Socket; |
cfdf6608 | 7 | use File::Path; |
45e68618 | 8 | |
0854fb22 DM |
9 | use PVE::Tools; |
10 | use PVE::INotify; | |
11 | ||
9f67f5b3 | 12 | use PMG::ClusterConfig; |
0854fb22 | 13 | |
cfdf6608 DM |
14 | our $spooldir = "/var/spool/proxmox"; |
15 | ||
16 | sub create_needed_dirs { | |
17 | my ($lcid, $cleanup) = @_; | |
18 | ||
19 | # if requested, remove any stale date | |
d8782874 DM |
20 | rmtree("$spooldir/cluster", "$spooldir/virus" "$spooldir/spam") if $cleanup; |
21 | ||
22 | mkdir "$spooldir/spam"; | |
23 | mkdir "$spooldir/virus"; | |
cfdf6608 DM |
24 | |
25 | if ($lcid) { | |
26 | mkpath "$spooldir/cluster/$lcid/virus"; | |
27 | mkpath "$spooldir/cluster/$lcid/spam"; | |
28 | } | |
29 | } | |
30 | ||
8737f93a DM |
31 | sub remote_node_ip { |
32 | my ($nodename, $noerr) = @_; | |
33 | ||
d8782874 | 34 | my $cinfo = PMG::ClusterConfig->new(); |
8737f93a | 35 | |
45e68618 | 36 | foreach my $entry (values %{$cinfo->{ids}}) { |
8737f93a DM |
37 | if ($entry->{name} eq $nodename) { |
38 | my $ip = $entry->{ip}; | |
39 | return $ip if !wantarray; | |
40 | my $family = PVE::Tools::get_host_address_family($ip); | |
41 | return ($ip, $family); | |
42 | } | |
43 | } | |
44 | ||
45 | # fallback: try to get IP by other means | |
9f67f5b3 | 46 | return PMG::Utils::lookup_node_ip($nodename, $noerr); |
8737f93a DM |
47 | } |
48 | ||
d2e43f9e DM |
49 | sub get_master_node { |
50 | my ($cinfo) = @_; | |
51 | ||
d8782874 | 52 | $cinfo = PMG::ClusterConfig->new() if !$cinfo; |
d2e43f9e DM |
53 | |
54 | return $cinfo->{master}->{name} if defined($cinfo->{master}); | |
55 | ||
56 | return 'localhost'; | |
57 | } | |
58 | ||
cba17aeb DM |
59 | sub read_local_ssl_cert_fingerprint { |
60 | my $cert_path = "/etc/pmg/pmg-api.pem"; | |
0854fb22 | 61 | |
cba17aeb DM |
62 | my $cert; |
63 | eval { | |
64 | my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r'); | |
65 | $cert = Net::SSLeay::PEM_read_bio_X509($bio); | |
66 | Net::SSLeay::BIO_free($bio); | |
67 | }; | |
68 | if (my $err = $@) { | |
69 | die "unable to read certificate '$cert_path' - $err\n"; | |
70 | } | |
0854fb22 | 71 | |
cba17aeb DM |
72 | if (!defined($cert)) { |
73 | die "unable to read certificate '$cert_path' - got empty value\n"; | |
74 | } | |
75 | ||
76 | my $fp; | |
77 | eval { | |
78 | $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256'); | |
79 | }; | |
80 | if (my $err = $@) { | |
81 | die "unable to get fingerprint for '$cert_path' - $err\n"; | |
82 | } | |
0854fb22 | 83 | |
cba17aeb DM |
84 | if (!defined($fp) || $fp eq '') { |
85 | die "unable to get fingerprint for '$cert_path' - got empty value\n"; | |
86 | } | |
0854fb22 | 87 | |
cba17aeb DM |
88 | return $fp; |
89 | } | |
0854fb22 | 90 | |
cba17aeb DM |
91 | my $hostrsapubkey_fn = '/etc/ssh/ssh_host_rsa_key.pub'; |
92 | my $rootrsakey_fn = '/root/.ssh/id_rsa'; | |
93 | my $rootrsapubkey_fn = '/root/.ssh/id_rsa.pub'; | |
0854fb22 | 94 | |
cba17aeb DM |
95 | sub read_local_cluster_info { |
96 | ||
97 | my $res = {}; | |
98 | ||
99 | my $hostrsapubkey = PVE::Tools::file_read_firstline($hostrsapubkey_fn); | |
100 | $hostrsapubkey =~ s/^.*ssh-rsa\s+//i; | |
101 | $hostrsapubkey =~ s/\s+root\@\S+\s*$//i; | |
102 | ||
103 | die "unable to parse ${hostrsapubkey_fn}\n" | |
104 | if $hostrsapubkey !~ m/^[A-Za-z0-9\.\/\+]{200,}$/; | |
0854fb22 DM |
105 | |
106 | my $nodename = PVE::INotify::nodename(); | |
107 | ||
cba17aeb | 108 | $res->{name} = $nodename; |
0854fb22 | 109 | |
cba17aeb | 110 | $res->{ip} = PMG::Utils::lookup_node_ip($nodename); |
0854fb22 | 111 | |
cba17aeb | 112 | $res->{hostrsapubkey} = $hostrsapubkey; |
0854fb22 | 113 | |
cba17aeb DM |
114 | if (! -f $rootrsapubkey_fn) { |
115 | unlink $rootrsakey_fn; | |
116 | my $cmd = ['ssh-keygen', '-t', 'rsa', '-N', '', '-b', '2048', | |
117 | '-f', $rootrsakey_fn]; | |
118 | PVE::Tools::run_command($cmd); | |
119 | } | |
120 | ||
121 | my $rootrsapubkey = PVE::Tools::file_read_firstline($rootrsapubkey_fn); | |
122 | $rootrsapubkey =~ s/^.*ssh-rsa\s+//i; | |
123 | $rootrsapubkey =~ s/\s+root\@\S+\s*$//i; | |
124 | ||
125 | die "unable to parse ${rootrsapubkey_fn}\n" | |
126 | if $rootrsapubkey !~ m/^[A-Za-z0-9\.\/\+]{200,}$/; | |
127 | ||
128 | $res->{rootrsapubkey} = $rootrsapubkey; | |
129 | ||
130 | $res->{fingerprint} = read_local_ssl_cert_fingerprint(); | |
131 | ||
132 | return $res; | |
133 | } | |
134 | ||
135 | # X509 Certificate cache helper | |
136 | ||
137 | my $cert_cache_nodes = {}; | |
138 | my $cert_cache_timestamp = time(); | |
139 | my $cert_cache_fingerprints = {}; | |
0854fb22 | 140 | |
cba17aeb | 141 | sub update_cert_cache { |
0854fb22 | 142 | |
cba17aeb DM |
143 | $cert_cache_timestamp = time(); |
144 | ||
145 | $cert_cache_fingerprints = {}; | |
146 | $cert_cache_nodes = {}; | |
147 | ||
d8782874 | 148 | my $cinfo = PMG::ClusterConfig->new(); |
cba17aeb DM |
149 | |
150 | foreach my $entry (values %{$cinfo->{ids}}) { | |
151 | my $node = $entry->{name}; | |
152 | my $fp = $entry->{fingerprint}; | |
153 | if ($node && $fp) { | |
154 | $cert_cache_fingerprints->{$fp} = 1; | |
155 | $cert_cache_nodes->{$node} = $fp; | |
0854fb22 DM |
156 | } |
157 | } | |
158 | } | |
159 | ||
160 | # load and cache cert fingerprint once | |
161 | sub initialize_cert_cache { | |
162 | my ($node) = @_; | |
163 | ||
cba17aeb | 164 | update_cert_cache() |
0854fb22 DM |
165 | if defined($node) && !defined($cert_cache_nodes->{$node}); |
166 | } | |
167 | ||
168 | sub check_cert_fingerprint { | |
169 | my ($cert) = @_; | |
170 | ||
171 | # clear cache every 30 minutes at least | |
cba17aeb | 172 | update_cert_cache() if time() - $cert_cache_timestamp >= 60*30; |
0854fb22 DM |
173 | |
174 | # get fingerprint of server certificate | |
175 | my $fp; | |
176 | eval { | |
177 | $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256'); | |
178 | }; | |
179 | return 0 if $@ || !defined($fp) || $fp eq ''; # error | |
180 | ||
181 | my $check = sub { | |
182 | for my $expected (keys %$cert_cache_fingerprints) { | |
183 | return 1 if $fp eq $expected; | |
184 | } | |
185 | return 0; | |
186 | }; | |
187 | ||
cba17aeb | 188 | return 1 if $check->(); |
0854fb22 DM |
189 | |
190 | # clear cache and retry at most once every minute | |
191 | if (time() - $cert_cache_timestamp >= 60) { | |
192 | syslog ('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache"); | |
193 | update_cert_cache(); | |
cba17aeb | 194 | return $check->(); |
0854fb22 DM |
195 | } |
196 | ||
197 | return 0; | |
198 | } | |
199 | ||
200 | 1; |