1
0

sync_ccd.pl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use Encode;
  5. no warnings 'utf8';
  6. use English;
  7. use LWP::UserAgent;
  8. use JSON;
  9. use File::Find;
  10. use File::Path qw(make_path);
  11. use File::Basename;
  12. use Socket qw(inet_aton);
  13. use Net::Patricia;
  14. #use Data::Dumper;
  15. my $api_user='USER';
  16. my $api_key ='PASSWORD';
  17. my $host ='HOST';
  18. my $user_id = $ARGV[0];
  19. exit if (!$user_id);
  20. # Конфигурация логгирования
  21. my $log_file = '/var/log/openvpn/sync_ccd_'.$user_id.'.log';
  22. my $log_level = 'INFO'; # DEBUG, INFO, WARN, ERROR
  23. # Конфигурационные параметры
  24. my $api_url = 'https://'.$host.'/api.php?login='.$api_user.'&api_key='.$api_key.'&get=user&id='.$user_id;
  25. my $ovpn_dir = '/etc/openvpn/server';
  26. # Создаем backup и log директории если не существуют
  27. make_path(dirname($log_file)) unless -d dirname($log_file);
  28. # 1. Выполняем API запрос и получаем JSON
  29. my $json_data = fetch_json($api_url);
  30. unless ($json_data) {
  31. log_message("ERROR", "Не удалось получить данные от API");
  32. die "Не удалось получить данные от API\n";
  33. }
  34. # 2. Парсим JSON и извлекаем массив auth
  35. my $auth_data = parse_auth_data($json_data);
  36. if (!$auth_data and !scalar @{$auth_data}) {
  37. log_message("ERROR", "Не найдены данные auth в JSON ответе");
  38. die "Не найдены данные auth в JSON ответе\n";
  39. }
  40. # 3. Ищем конфиги OpenVPN и анализируем их
  41. my @configs = find_ovpn_configs($ovpn_dir);
  42. # 4. Обрабатываем каждый конфиг
  43. foreach my $config (@configs) {
  44. process_ovpn_config($config, $auth_data);
  45. }
  46. log_message("INFO", "Обработка завершена успешно!");
  47. print "Обработка завершена успешно!\n";
  48. # Функция логирования
  49. sub log_message {
  50. my ($level, $message) = @_;
  51. # Уровни логирования: DEBUG < INFO < WARN < ERROR
  52. my %levels = (
  53. 'DEBUG' => 1,
  54. 'INFO' => 2,
  55. 'WARN' => 3,
  56. 'ERROR' => 4
  57. );
  58. return unless $levels{$level} >= $levels{$log_level};
  59. my $timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime);
  60. my $log_entry = "[$timestamp] [$level] $message\n";
  61. open my $fh, '>>', $log_file or do {
  62. warn "Не могу открыть лог файл $log_file: $!\n";
  63. return;
  64. };
  65. print $fh $log_entry;
  66. close $fh;
  67. }
  68. # Функция для форматирования времени
  69. sub strftime {
  70. my ($format, @time) = @_;
  71. my @tm = localtime($time[0] || time);
  72. my %formats = (
  73. '%Y' => sprintf("%04d", $tm[5] + 1900),
  74. '%m' => sprintf("%02d", $tm[4] + 1),
  75. '%d' => sprintf("%02d", $tm[3]),
  76. '%H' => sprintf("%02d", $tm[2]),
  77. '%M' => sprintf("%02d", $tm[1]),
  78. '%S' => sprintf("%02d", $tm[0]),
  79. );
  80. $format =~ s/(%\w)/$formats{$1} || $1/eg;
  81. return $format;
  82. }
  83. sub ip_and_mask_to_cidr {
  84. my ($ip, $mask) = @_;
  85. # Преобразуем IP и маску в 32-битные числа (в сетевом порядке)
  86. my $ip_n = unpack("N", inet_aton($ip));
  87. my $mask_n = unpack("N", inet_aton($mask));
  88. # Проверка: маска должна быть корректной (последовательность 1, затем 0)
  89. my $binary_mask = sprintf("%032b", $mask_n);
  90. if ($binary_mask !~ /^1*0*$/) {
  91. log_message("ERROR", "Некорректная маска подсети: $mask");
  92. die "Некорректная маска подсети: $mask\n";
  93. }
  94. # Считаем количество единиц в маске — это /24, /16 и т.д.
  95. my $prefix_len = ($binary_mask =~ tr/1/1/);
  96. # Применяем маску к IP, чтобы получить сетевой адрес
  97. my $network_n = $ip_n & $mask_n;
  98. # Преобразуем обратно в dotted-decimal
  99. my $network_ip = join '.', map { ($network_n >> (24 - 8 * $_)) & 255 } 0..3;
  100. # Возвращаем в формате CIDR
  101. return "$network_ip/$prefix_len";
  102. }
  103. sub fetch_json {
  104. my $url = shift;
  105. my $ua = LWP::UserAgent->new;
  106. $ua->timeout(30);
  107. $ua->env_proxy;
  108. log_message("DEBUG", "Выполняем запрос к API: $url");
  109. my $response = $ua->get($url);
  110. if ($response->is_success) {
  111. log_message("DEBUG", "API запрос успешен");
  112. return $response->decoded_content;
  113. } else {
  114. my $error = "Ошибка запроса: " . $response->status_line;
  115. log_message("ERROR", $error);
  116. warn "$error\n";
  117. return undef;
  118. }
  119. }
  120. sub parse_auth_data {
  121. my $json_str = shift;
  122. my @result;
  123. eval {
  124. my $json = decode_json($json_str);
  125. @result = @{$json->{auth}} if $json && ref $json eq 'HASH' && $json->{auth};
  126. };
  127. if ($@) {
  128. my $error = "Ошибка парсинга JSON: $@";
  129. log_message("ERROR", $error);
  130. warn "$error\n";
  131. return undef;
  132. }
  133. log_message("DEBUG", "Найдено " . scalar(@result) . " записей auth");
  134. return \@result;
  135. }
  136. sub find_ovpn_configs {
  137. my $dir = shift;
  138. my @configs;
  139. find(sub {
  140. return unless -f && /\.conf$/;
  141. push @configs, $File::Find::name;
  142. }, $dir);
  143. log_message("DEBUG", "Найдено " . scalar(@configs) . " конфигурационных файлов OpenVPN");
  144. return @configs;
  145. }
  146. sub process_ovpn_config {
  147. my $config_file = shift;
  148. my $ip_list = shift;
  149. log_message("INFO", "Обрабатываем конфиг: $config_file");
  150. print "Обрабатываем конфиг: $config_file\n";
  151. # Читаем конфиг и находим ccd directory и сеть
  152. my ($ccd_dir, $network, $network_mask) = parse_ovpn_config($config_file);
  153. unless ($ccd_dir && $network) {
  154. log_message("WARN", "Не найдены ccd directory или network в $config_file");
  155. return;
  156. }
  157. my $ServerNet = new Net::Patricia;
  158. $ServerNet->add_string($network);
  159. log_message("INFO", "Found server network: $network (mask: $network_mask) and ccd: $ccd_dir");
  160. print "Found server network: $network (mask: $network_mask) and ccd: $ccd_dir\n";
  161. # Преобразуем относительный пути в абсолютный
  162. unless ($ccd_dir =~ m{^/}) {
  163. my $config_dir = dirname($config_file);
  164. $ccd_dir = "$config_dir/$ccd_dir";
  165. }
  166. # Создаем CCD директорию если не существует
  167. make_path($ccd_dir) unless -d $ccd_dir;
  168. # Обрабатываем каждый auth record
  169. foreach my $auth (@$ip_list) {
  170. next unless $auth->{comments} && $auth->{ip};
  171. next if (!$ServerNet->match_string($auth->{ip}));
  172. my $username = $auth->{comments};
  173. my $ip = $auth->{ip};
  174. process_ccd_file($ccd_dir, $username, $ip, $network_mask);
  175. }
  176. }
  177. sub parse_ovpn_config {
  178. my $config_file = shift;
  179. my ($ccd_dir, $network, $network_mask);
  180. open my $fh, '<', $config_file or do {
  181. my $error = "Не могу открыть $config_file: $!";
  182. log_message("ERROR", $error);
  183. warn "$error\n";
  184. return (undef, undef);
  185. };
  186. while (my $line = <$fh>) {
  187. chomp $line;
  188. # Ищем client-config-dir
  189. if ($line =~ /^\s*client-config-dir\s+(\S+)/i) {
  190. $ccd_dir = $1;
  191. }
  192. # Ищем server directive
  193. if ($line =~ /^\s*server\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)/i) {
  194. $network = ip_and_mask_to_cidr($1,$2) if ($1 and $2);
  195. $network_mask = $2 if ($2);
  196. }
  197. # Ищем server directive
  198. if ($line =~ /^\s*ifconfig\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)/i) {
  199. $network = ip_and_mask_to_cidr($1,$2) if ($1 and $2);
  200. $network_mask = $2 if ($2);
  201. }
  202. last if $ccd_dir && $network;
  203. }
  204. close $fh;
  205. return ($ccd_dir, $network, $network_mask);
  206. }
  207. sub process_ccd_file {
  208. my $ccd_dir = shift;
  209. my $username = shift;
  210. my $ip = shift;
  211. my $network_mask = shift;
  212. return if (!$username or !$ip or !$network_mask);
  213. my $ccd_file = "$ccd_dir/$username";
  214. my $log_msg = "Обрабатываем пользователя: $username, IP: $ip";
  215. log_message("INFO", $log_msg);
  216. print "$log_msg ...";
  217. # Читаем существующий файл или создаем новый
  218. my @lines;
  219. if (-f $ccd_file) {
  220. open my $fh, '<', $ccd_file or do {
  221. my $error = "Не могу открыть $ccd_file: $!";
  222. log_message("ERROR", $error);
  223. warn "$error\n";
  224. return;
  225. };
  226. @lines = <$fh>;
  227. close $fh;
  228. }
  229. # Ищем или добавляем ifconfig-push
  230. my $found = 0;
  231. my $new_ifconfig = "ifconfig-push $ip $network_mask";
  232. my $changed = 0;
  233. for my $i (0..$#lines) {
  234. if ($lines[$i] =~ /^ifconfig-push/) {
  235. if ($lines[$i] !~ /^\s*$new_ifconfig\s*$/) {
  236. my $replace_msg = "Заменяем: $lines[$i] на: $new_ifconfig";
  237. log_message("INFO", $replace_msg);
  238. print "$replace_msg\n";
  239. $lines[$i] = "$new_ifconfig\n";
  240. $changed = 1;
  241. } else {
  242. my $current_msg = "ifconfig-push уже актуален для $username";
  243. log_message("INFO", $current_msg);
  244. print "$current_msg\n";
  245. }
  246. $found = 1;
  247. last;
  248. }
  249. }
  250. # Если не нашли, добавляем новую строку
  251. unless ($found) {
  252. my $add_msg = "Добавляем ifconfig-push для $username";
  253. log_message("INFO", $add_msg);
  254. print "$add_msg\n";
  255. $changed = 1;
  256. push @lines, "$new_ifconfig\n";
  257. }
  258. if ($changed) {
  259. # Записываем обратно в файл
  260. open my $fh, '>', $ccd_file or do {
  261. my $error = "Не могу записать в $ccd_file: $!";
  262. log_message("ERROR", $error);
  263. warn "$error\n";
  264. return;
  265. };
  266. print $fh @lines;
  267. close $fh;
  268. # Ставим права на конфиг
  269. chmod 0644, $ccd_file or do {
  270. my $error = "chmod failed: $!";
  271. log_message("ERROR", $error);
  272. die "$error\n";
  273. };
  274. my $ccd_user = 'nobody';
  275. my $ccd_group = 'www-data';
  276. my $uid = getpwnam($ccd_user) or do {
  277. my $error = "Пользователь $ccd_user не найден";
  278. log_message("ERROR", $error);
  279. die "$error\n";
  280. };
  281. my $gid = getgrnam($ccd_group) or do {
  282. my $error = "Группа $ccd_group не найдена";
  283. log_message("ERROR", $error);
  284. die "$error\n";
  285. };
  286. chown $uid, $gid, $ccd_file or do {
  287. my $error = "chown failed: $!";
  288. log_message("ERROR", $error);
  289. die "$error\n";
  290. };
  291. my $success_msg = "Файл $ccd_file успешно обновлен";
  292. log_message("INFO", $success_msg);
  293. print "$success_msg\n";
  294. }
  295. }
  296. # Обработка ошибок
  297. $SIG{__DIE__} = sub {
  298. my $error = shift;
  299. warn "Критическая ошибка: $error\n";
  300. exit 1;
  301. };