sync_ccd.pl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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 ($sec,$min,$hour,$mday,$mon,$year) = (localtime())[0,1,2,3,4,5];
  60. $mon += 1; $year += 1900;
  61. open my $fh, '>>', $log_file or do {
  62. warn "Не могу открыть лог файл $log_file: $!\n";
  63. return;
  64. };
  65. printf $fh "%04d%02d%02d-%02d%02d%02d [%d] [%s] %s\n",$year,$mon,$mday,$hour,$min,$sec,$$,$level,$message;
  66. close $fh;
  67. }
  68. sub ip_and_mask_to_cidr {
  69. my ($ip, $mask) = @_;
  70. # Преобразуем IP и маску в 32-битные числа (в сетевом порядке)
  71. my $ip_n = unpack("N", inet_aton($ip));
  72. my $mask_n = unpack("N", inet_aton($mask));
  73. # Проверка: маска должна быть корректной (последовательность 1, затем 0)
  74. my $binary_mask = sprintf("%032b", $mask_n);
  75. if ($binary_mask !~ /^1*0*$/) {
  76. log_message("ERROR", "Некорректная маска подсети: $mask");
  77. die "Некорректная маска подсети: $mask\n";
  78. }
  79. # Считаем количество единиц в маске — это /24, /16 и т.д.
  80. my $prefix_len = ($binary_mask =~ tr/1/1/);
  81. # Применяем маску к IP, чтобы получить сетевой адрес
  82. my $network_n = $ip_n & $mask_n;
  83. # Преобразуем обратно в dotted-decimal
  84. my $network_ip = join '.', map { ($network_n >> (24 - 8 * $_)) & 255 } 0..3;
  85. # Возвращаем в формате CIDR
  86. return "$network_ip/$prefix_len";
  87. }
  88. sub fetch_json {
  89. my $url = shift;
  90. my $ua = LWP::UserAgent->new;
  91. $ua->timeout(30);
  92. $ua->env_proxy;
  93. log_message("DEBUG", "Выполняем запрос к API: $url");
  94. my $response = $ua->get($url);
  95. if ($response->is_success) {
  96. log_message("DEBUG", "API запрос успешен");
  97. return $response->decoded_content;
  98. } else {
  99. my $error = "Ошибка запроса: " . $response->status_line;
  100. log_message("ERROR", $error);
  101. warn "$error\n";
  102. return undef;
  103. }
  104. }
  105. sub parse_auth_data {
  106. my $json_str = shift;
  107. my @result;
  108. eval {
  109. my $json = decode_json($json_str);
  110. @result = @{$json->{auth}} if $json && ref $json eq 'HASH' && $json->{auth};
  111. };
  112. if ($@) {
  113. my $error = "Ошибка парсинга JSON: $@";
  114. log_message("ERROR", $error);
  115. warn "$error\n";
  116. return undef;
  117. }
  118. log_message("DEBUG", "Найдено " . scalar(@result) . " записей auth");
  119. return \@result;
  120. }
  121. sub find_ovpn_configs {
  122. my $dir = shift;
  123. my @configs;
  124. find(sub {
  125. return unless -f && /\.conf$/;
  126. push @configs, $File::Find::name;
  127. }, $dir);
  128. log_message("DEBUG", "Найдено " . scalar(@configs) . " конфигурационных файлов OpenVPN");
  129. return @configs;
  130. }
  131. sub process_ovpn_config {
  132. my $config_file = shift;
  133. my $ip_list = shift;
  134. log_message("INFO", "Обрабатываем конфиг: $config_file");
  135. print "Обрабатываем конфиг: $config_file\n";
  136. # Читаем конфиг и находим ccd directory и сеть
  137. my ($ccd_dir, $network, $network_mask) = parse_ovpn_config($config_file);
  138. unless ($ccd_dir && $network) {
  139. log_message("WARN", "Не найдены ccd directory или network в $config_file");
  140. return;
  141. }
  142. my $ServerNet = new Net::Patricia;
  143. $ServerNet->add_string($network);
  144. log_message("INFO", "Found server network: $network (mask: $network_mask) and ccd: $ccd_dir");
  145. print "Found server network: $network (mask: $network_mask) and ccd: $ccd_dir\n";
  146. # Преобразуем относительный пути в абсолютный
  147. unless ($ccd_dir =~ m{^/}) {
  148. my $config_dir = dirname($config_file);
  149. $ccd_dir = "$config_dir/$ccd_dir";
  150. }
  151. # Создаем CCD директорию если не существует
  152. make_path($ccd_dir) unless -d $ccd_dir;
  153. # Обрабатываем каждый auth record
  154. foreach my $auth (@$ip_list) {
  155. next unless $auth->{comments} && $auth->{ip};
  156. next if (!$ServerNet->match_string($auth->{ip}));
  157. my $username = $auth->{comments};
  158. my $ip = $auth->{ip};
  159. process_ccd_file($ccd_dir, $username, $ip, $network_mask);
  160. }
  161. }
  162. sub parse_ovpn_config {
  163. my $config_file = shift;
  164. my ($ccd_dir, $network, $network_mask);
  165. open my $fh, '<', $config_file or do {
  166. my $error = "Не могу открыть $config_file: $!";
  167. log_message("ERROR", $error);
  168. warn "$error\n";
  169. return (undef, undef);
  170. };
  171. while (my $line = <$fh>) {
  172. chomp $line;
  173. # Ищем client-config-dir
  174. if ($line =~ /^\s*client-config-dir\s+(\S+)/i) {
  175. $ccd_dir = $1;
  176. }
  177. # Ищем server directive
  178. if ($line =~ /^\s*server\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)/i) {
  179. $network = ip_and_mask_to_cidr($1,$2) if ($1 and $2);
  180. $network_mask = $2 if ($2);
  181. }
  182. # Ищем server directive
  183. if ($line =~ /^\s*ifconfig\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)/i) {
  184. $network = ip_and_mask_to_cidr($1,$2) if ($1 and $2);
  185. $network_mask = $2 if ($2);
  186. }
  187. last if $ccd_dir && $network;
  188. }
  189. close $fh;
  190. return ($ccd_dir, $network, $network_mask);
  191. }
  192. sub process_ccd_file {
  193. my $ccd_dir = shift;
  194. my $username = shift;
  195. my $ip = shift;
  196. my $network_mask = shift;
  197. return if (!$username or !$ip or !$network_mask);
  198. my $ccd_file = "$ccd_dir/$username";
  199. my $log_msg = "Обрабатываем пользователя: $username, IP: $ip";
  200. log_message("INFO", $log_msg);
  201. print "$log_msg ...";
  202. # Читаем существующий файл или создаем новый
  203. my @lines;
  204. if (-f $ccd_file) {
  205. open my $fh, '<', $ccd_file or do {
  206. my $error = "Не могу открыть $ccd_file: $!";
  207. log_message("ERROR", $error);
  208. warn "$error\n";
  209. return;
  210. };
  211. @lines = <$fh>;
  212. close $fh;
  213. }
  214. my $changed = 0;
  215. # Ищем или добавляем ifconfig-push
  216. my $found = 0;
  217. my $new_ifconfig = "ifconfig-push $ip $network_mask";
  218. for my $i (0..$#lines) {
  219. if ($lines[$i] =~ /^ifconfig-push/) {
  220. if ($lines[$i] !~ /^\s*$new_ifconfig\s*$/) {
  221. my $replace_msg = "Заменяем: $lines[$i] на: $new_ifconfig";
  222. log_message("INFO", $replace_msg);
  223. print "$replace_msg\n";
  224. $lines[$i] = "$new_ifconfig\n";
  225. $changed = 1;
  226. } else {
  227. my $current_msg = "ifconfig-push уже актуален для $username";
  228. log_message("INFO", $current_msg);
  229. print "$current_msg\n";
  230. }
  231. $found = 1;
  232. last;
  233. }
  234. }
  235. # Если не нашли, добавляем новую строку
  236. unless ($found) {
  237. my $add_msg = "Добавляем ifconfig-push для $username";
  238. log_message("INFO", $add_msg);
  239. print "$add_msg\n";
  240. $changed = 1;
  241. push @lines, "$new_ifconfig\n";
  242. }
  243. if ($changed) {
  244. # Записываем обратно в файл
  245. open my $fh, '>', $ccd_file or do {
  246. my $error = "Не могу записать в $ccd_file: $!";
  247. log_message("ERROR", $error);
  248. warn "$error\n";
  249. return;
  250. };
  251. print $fh @lines;
  252. close $fh;
  253. # Ставим права на конфиг
  254. chmod 0664, $ccd_file or do {
  255. my $error = "chmod failed: $!";
  256. log_message("ERROR", $error);
  257. die "$error\n";
  258. };
  259. my $ccd_user = 'nobody';
  260. my $ccd_group = 'www-data';
  261. my $uid = getpwnam($ccd_user) or do {
  262. my $error = "Пользователь $ccd_user не найден";
  263. log_message("ERROR", $error);
  264. die "$error\n";
  265. };
  266. my $gid = getgrnam($ccd_group) or do {
  267. my $error = "Группа $ccd_group не найдена";
  268. log_message("ERROR", $error);
  269. die "$error\n";
  270. };
  271. chown $uid, $gid, $ccd_file or do {
  272. my $error = "chown failed: $!";
  273. log_message("ERROR", $error);
  274. die "$error\n";
  275. };
  276. my $success_msg = "Файл $ccd_file успешно обновлен";
  277. log_message("INFO", $success_msg);
  278. print "$success_msg\n";
  279. }
  280. }
  281. # Обработка ошибок
  282. $SIG{__DIE__} = sub {
  283. my $error = shift;
  284. warn "Критическая ошибка: $error\n";
  285. exit 1;
  286. };