sync_ccd.pl 11 KB

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