Linux-десктоп своими руками: WiFi-manager

от автора

По просьбе некоторых комментаторов «что-то написать самому и выложить на обозрение» — ну вот, написал и выкладываю:

Суть задачи: как, не имея установленного современного Desktop Environment, с Network Manager и systemd, управлять подключением к Wi-Fi сетям без особых проблем?
Усложнение: допустим, у нас к тому же несколько Wi-Fi адаптеров, для одновременного подключения к нескольким сетям.

Легко!
Но для начала — немного о том, «как это работает под капотом» (кому неинтересно — проскакиваем)

Восход Солнца вручную

Сразу, чтобы не пытаться охватить все возможные ситуации, обозначу рамки:
на сегодняшний день почти везде используется WPA/WPA2, даже в каких-нибудь ESP8266, поэтому речь идет о подключении именно через WPA.

За это дело в Linux отвечает уже давно известная прекрасная программа wpa_supplicant. Прекрасна она еще и тем, что кроме работы с вполне читаемыми и понятными конфигами ей можно управлять и через командную строку, через wpa_cli.

Как правило, ее настройки хранятся где-то в /etc/wpa_supplicant, при запуске она читает конфигурационные файлы, содержащие записи о подключенных сетях, висит демоном, отслеживая появление-пропадание сетей и подключается к ним, когда надо.

При этом её работой можно управлять через «control interface socket», по умолчанию в /var/run/wpa_supplicant, чем и занимается wpa_cli.

В простейшем случае нужен хотя бы один конфигурационнный файл примерно такого содержания:
/etc/wpa_supplicant.conf

ctrl_interface=/run/wpa_supplicant update_config=1 country=US  network={   ssid = "MyNet"   psk = "MyPassword" }

(country тут определяет разрешенные диапазоны работы WiFi)

После чего достаточно запустить её:

wpa_supplicant -i wlan0 -c /etc/wpa_supplicant.conf

Теперь интерфейс wlan0 должен быть подключен к сети MyNet.
Останется только получить IP-адрес по DHCP…

Но это хорошо только для стационарного компьютера, когда сеть известна заранее, Wi-Fi адаптер известен, достаточно один раз прописать и потом запускать из-под рута.

В реальности бывает немного сложнее.
Во-первых, сейчас принято давать Wi-Fi адаптерам «предсказуемые имена» (предсказуемые в том смысле, что они соответствуют конкретному адаптеру, и при перезагрузке не перескочат случайно wlan0 <-> wlan1).
Но это означает, что вместо wlan0 там может быть что-то типа wl557hwej24, и вот это-то вы не предскажете заранее, подключая USB-донгл.

Если адаптеров несколько, и у них вот такие имена — надо делать для них разные конфигурационные файлы, и вызывать wpa_supplicant для каждого отдельно.

И если у вас ноутбук — вы не сможете заранее знать все нужные вам сети, а править всё это вручную, убивая и перезапуская процессы — удовольствие то еще.
Как раз эту проблему решает wpa_cli: с его помощью можно подключиться к запущенному wpa_supplicant, создать новую сеть или удалить старую:

scan — запускает сканирование
scan_results — покажет, что найдено
list_networks — покажет, что сохранено

add_network — создаст новую сеть (сохранение) с номером N
set_network N ssid «MyNet»
set_network N psk «MyPass»
save_config

select_network N — выбрать ее текущей

remove_network N — удалить ее

И это только малая часть команд. Уже лучше, но всё равно «вручную».
К тому же, по умолчанию wpa_cli нужно запускать от рута.

Ну и наконец еще одна проблема — у адаптера может быть просто отключено питание (включен режим Power Save) — тогда надо его сначала включить.

Итого, алгоритм получается такой:
1 — найти адаптер(ы)

2 — включить питание
3 — включить как сетевой интерфейс (это немного другое)
4 — запустить для него wpa_supplicant
5 — если сеть новая — прописать ее и сохранить настройку
6 — после подключения к AP — получить IP-адрес.

Вот это и автоматизируем

Пусть работает компьютер, он железный

Для начала — скрипт, который будет искать адаптеры, создавать для них конфигурационные файлы и запускать демонов:

#!/bin/sh -x  # /etc/wpa_supplicant/start_wpa_supplicant.sh  PATH=/sbin:$PATH; export PATH  CONFIG_DIR=/etc/wpa_supplicant CTL_DIR=/run/wpa_supplicant  exec >> /var/log/wpa_auto.log exec 2>&1  check_config_file(){   if [ -n "${CONFIG_FILE}" ] && [ ! -f "${CONFIG_FILE}" ] ; then     (       echo "ctrl_interface=${CTL_DIR}"       echo "update_config=1"       echo "country=US"     ) > "${CONFIG_FILE}"   fi }  which iw NO_IW=$?  if [ ${NO_IW} ] ; then   IFACES=$(iwconfig 2>&1 | grep IEEE | awk '{ print $1 }') else   IFACES=$(iw dev | grep Interface | awk '{ print $2 }') fi  for i in ${IFACES} ; do   echo ${i}    CONFIG_FILE=${CONFIG_DIR}/iface_${i}.conf    x=$(ps ax | grep -v grep | grep "${CONFIG_FILE}" | wc -l)   if [ "$x" -eq "0" ] ; then     echo "No wpa_supplicant for ${i} found"      # Включим питание на всякий случай     if [ ${NO_IW} ] ; then       iwconfig ${i} power on     else       iw dev ${i} set power_save off     fi      check_config_file      # Поднимаем интерфейс     ip link set "${i}" up      # Запускаем wpa_supplicant     wpa_supplicant -i "${i}" -c "${CONFIG_FILE}" -B -C ${CTL_DIR}     chown -R root:netdev ${CTL_DIR}      # Запускаем wpa_cli, чтобы следить за событиями и запускать DHCP при подключении     wpa_cli -a /etc/wpa_supplicant/wpa_dhcp.sh -i ${i} -B    fi  done  exit  

Да, это самый обыкновенный shell-скрипт. Он проверяет какие адаптеры есть в системе, используя iw или iwconfig, создает для них конфигурационный файл, если его еще нет, и запускает демона.
Дополнительно запускает в фоне процесс, контролирующий момент подключения, чтобы автоматически запустить dhclient:

#!/bin/sh  # /etc/wpa_supplicant/wpa_dhcp.sh  PATH=/sbin:$PATH; export PATH  exec >> /var/log/wpa_dhcp.log exec 2>&1  INTERFACE="$1" EVENT="$2"  echo "Event received: $EVENT on interface $INTERFACE"   if [ "$EVENT" = "CONNECTED" ]; then   echo "Wi-Fi connected on $INTERFACE, requesting DHCP..."   dhclient -r "$INTERFACE"   dhclient "$INTERFACE" fi 

Запускать этот скрипт удобно автоматически, через udevd:

/etc/udev/rules.d/99-wifi-autostart.rules

ACTION=="add", SUBSYSTEM=="net", KERNEL=="wl*", RUN+="/etc/wpa_supplicant/start_wpa_supplicant.sh %k"

При появлении в системе устройства типа wlan скрипт атоматически запустится.
Почему нельзя просто использовать передаваемый ему параметр — имя интерфейса?
Потому что тут он как раз еще не переименован, тут будет wlan0, но после запуска это будет уже что-то «предсказуемое».

В общем, уже значительная часть работы автоматизирована: если в соответствующем конфиг-файле сохранена какая-то сеть — wpa_supplicant подключится к ней, wpa_cli, ждущий соединения, запустит dhclient и интерфейс получит адрес.
Остается автоматизировать внесение сетей.

Для этого — несложный скрипт, с графическими окошками, традиционно — perl.

#!/usr/bin/perl use strict; use warnings; use Gtk3 '-init'; use IPC::Open2;  use Data::Dumper;  my $wpa_cli = "/sbin/wpa_cli";  my %networks;  # Создание основного окна my $window = Gtk3::Window->new('toplevel'); $window->set_title("Wi-Fi Manager"); $window->set_default_size(400, 400); $window->signal_connect(delete_event => sub { Gtk3->main_quit; });  # Создание списка интерфейсов (A) my $store_A = Gtk3::ListStore->new('Glib::String'); my $list_A = Gtk3::TreeView->new($store_A); my $colA1 = Gtk3::TreeViewColumn->new_with_attributes('Interface', Gtk3::CellRendererText->new, text => 0); $list_A->append_column($colA1);  # Создание списка сетей (B) my $store_B = Gtk3::ListStore->new('Glib::String','Glib::String','Glib::String'); my $list_B = Gtk3::TreeView->new($store_B); my $colB1 = Gtk3::TreeViewColumn->new_with_attributes('#', Gtk3::CellRendererText->new, text => 0); my $colB2 = Gtk3::TreeViewColumn->new_with_attributes('SSID', Gtk3::CellRendererText->new, text => 1); my $colB3 = Gtk3::TreeViewColumn->new_with_attributes('Status', Gtk3::CellRendererText->new, text => 2); $list_B->append_column($colB1); $list_B->append_column($colB2); $list_B->append_column($colB3);  # Кнопки управления my $scan_button = Gtk3::Button->new_with_label("Add network"); my $select_button = Gtk3::Button->new_with_label("Select"); my $delete_button = Gtk3::Button->new_with_label("Remove"); my $quit_button = Gtk3::Button->new_with_label("Quit"); $_->set_sensitive(0) for ($scan_button, $select_button, $delete_button);  # Формирование окна {   my $vbox = Gtk3::Box->new('vertical', 5);   $window->add($vbox);    $vbox->pack_start($list_A, 1, 1, 5);   $vbox->pack_start($list_B, 1, 1, 5);    my $button_box = Gtk3::ButtonBox->new('horizontal');   $button_box->pack_start($_, 1, 1, 5) for ($scan_button, $select_button, $delete_button, $quit_button);   $vbox->pack_start($button_box, 1, 1, 20); }  # ========================================= # Функция для выполнения команд wpa_cli sub run_wpa_cli {   my ($cmd) = @_;   print STDERR "cmd: ==$cmd==\n";   open2(my $out, my $in, "$wpa_cli $cmd");   my @result = <$out>;   print STDERR Dumper(@result);   return @result; }  # Заполнение списка интерфейсов (A) sub load_interfaces {   $list_A->get_model->clear;   my @interfaces = run_wpa_cli('interface');   foreach my $iface (@interfaces) {     next if($iface =~ /\w+\s+\w/);     $iface =~ s/\s+$//;     my $iter = $list_A->get_model->append();     $list_A->get_model->set($iter, 0 => $iface);   } }  # Получение выбранного интерфейса sub get_selected_iface {   my $selection = $list_A->get_selection;   my ($model, $iter) = $selection->get_selected;   return $model->get($iter, 0); }  # Получение выбранной сети sub get_selected_network {   my $selection = $list_B->get_selection;   my ($model, $iter) = $selection->get_selected;   return $model->get($iter, 0); }  # Заполнение списка сетей (B) для выбранного интерфейса sub load_networks {   my ($iface) = @_;   $list_B->get_model->clear;   my @networks = run_wpa_cli("-i $iface list_network");   shift @networks;   foreach my $net (@networks) {     my ($id, $ssid, $bssid, $flags) = split(/\s/, $net);     my $iter = $list_B->get_model->append();     $list_B->get_model->set($iter, 0 => $id, 1 => $ssid, 2 => $flags);     $networks{$ssid} = $id;   }   $select_button->set_sensitive(0);   $delete_button->set_sensitive(0); }  # Выбор сети sub select_network {   my $iface = get_selected_iface();   my $id = get_selected_network();   run_wpa_cli("-i $iface select_network $id");   run_wpa_cli("-i $iface save_config");   load_networks($iface); }  # Удаление сети sub delete_network {   my $iface = get_selected_iface();   my $id = get_selected_network();   run_wpa_cli("-i $iface remove_network $id");   run_wpa_cli("-i $iface save_config");   %networks=();   load_networks($iface); }  # Ввод пароля sub show_password_dialog {   my ($iface, $ssid, $parent_window) = @_;    my $dialog = Gtk3::Dialog->new("Password", $parent_window,     [ 'modal' ], 'gtk-ok', 'accept', 'gtk-cancel', 'cancel');   $dialog->set_default_size(300, 100);    my $entry = Gtk3::Entry->new();   $entry->set_visibility(0);   $dialog->get_content_area()->pack_start($entry, 1, 1, 5);    $dialog->signal_connect(response => sub {     my ($dialog, $response) = @_;     if ($response eq 'accept') {       my $pass= $entry->get_text();       if ($pass) {         my $id = $networks{ $ssid };         if(!defined $id){           my @output = run_wpa_cli("-i $iface add_network");           if(defined $output[0] && $output[0] =~ /(\d+)/){             $id = $1;           }           else{             $dialog->destroy;             return;           }         }         run_wpa_cli("-i $iface set_network $id ssid '\"$ssid\"'");         run_wpa_cli("-i $iface set_network $id psk '\"$pass\"'");         run_wpa_cli("-i $iface enable_network $id");         run_wpa_cli("-i $iface save_config");         sleep(1);       }     }     load_networks($iface);     $dialog->destroy;     $parent_window->destroy;   });    $dialog->show_all; }  # Выбор сетей из отсканированных sub scan_networks {   my $iface = get_selected_iface();   return unless $iface;    # Создаем новое окно сканирования   my $scan_window = Gtk3::Dialog->new(     "Scan Wi-Fi",     $window,     [ 'modal' ]   );   $scan_window->set_default_size(400, 300);    my $vbox_scan = Gtk3::Box->new('vertical', 5);   my $content_area = $scan_window->get_content_area();   $content_area->add($vbox_scan);    my $scrolled_window = Gtk3::ScrolledWindow->new();   $scrolled_window->set_policy('automatic', 'automatic');   $scrolled_window->set_min_content_height(400);    # Список доступных сетей (C)  my $store_C = Gtk3::ListStore->new('Glib::String','Glib::String');   my $list_C = Gtk3::TreeView->new($store_C);   $scrolled_window->add($list_C);   $vbox_scan->pack_start($scrolled_window, 1, 1, 5);    my $colC1 = Gtk3::TreeViewColumn->new_with_attributes('SSID', Gtk3::CellRendererText->new, text => 0);   my $colC2 = Gtk3::TreeViewColumn->new_with_attributes(' ', Gtk3::CellRendererText->new, text => 1);   $list_C->append_column($colC1);   $list_C->append_column($colC2);    # Кнопки   my $start_button = Gtk3::Button->new_with_label("Scan");   my $close_button = Gtk3::Button->new_with_label("Close");    my $button_box = Gtk3::ButtonBox->new('horizontal');   $button_box->pack_start($_, 1, 1, 5) for ($start_button, $close_button);   $vbox_scan->pack_start($button_box, 1, 1, 5);    $scan_window->show_all;    run_wpa_cli("-i $iface scan");   sleep(1);    # -------------------------------------------   # Обработчик выбора сети в списке C (делает кнопку "Добавить" активной)   my $selection_C = $list_C->get_selection;   $selection_C->signal_connect(changed => sub {     my ($model, $iter) = $selection_C->get_selected;     my $sel_ssid = $list_C->get_model->get($iter, 0);     my $iface = get_selected_iface();      show_password_dialog($iface, $sel_ssid, $scan_window);    });      # -------------------------------------------   $start_button->signal_connect(clicked => sub {     my %scan_results;     $list_C->get_model->clear;      my @results = run_wpa_cli("-i $iface scan_results");     shift @results;      my $list = {};      foreach my $line (@results) {       $line =~ s/[\n\r]+//gm;       print STDERR "$line\n";       my ($bssid, $freq, $signal, $flags, $ssid) = split(/\s+/, $line, 5);       next unless $ssid;        $list->{$ssid} = 1;     }          foreach my $ssid (keys(%$list)){       my $iter = $list_C->get_model->append();       my $fl = (defined $networks{ $ssid }) ? '*':'';       $list_C->get_model->set($iter, 0 => $ssid, 1 => $fl);     }   });    # -------------------------------------------   # Закрытие окна   $close_button->signal_connect(clicked => sub {     $scan_window->destroy;   });  }  # ========================================= my $selection_A = $list_A->get_selection; $selection_A->signal_connect(changed => sub {   my $iface = get_selected_iface();   if ($iface) {     %networks=();     load_networks($iface);     $scan_button->set_sensitive(1);   } });  my $selection_B = $list_B->get_selection; $selection_B->signal_connect(changed => sub {   $select_button->set_sensitive(1);   $delete_button->set_sensitive(1); });  $scan_button->signal_connect('clicked',sub {   scan_networks(); });  $select_button->signal_connect('clicked',sub {   select_network(); });  $delete_button->signal_connect('clicked',sub {   delete_network(); });  $quit_button->signal_connect(clicked => sub {   Gtk3->main_quit; });  ######################################################### # Загрузка интерфейсов при старте load_interfaces();  $window->show_all; Gtk3->main;  

Скрипт просто показывает в окошках найденные сети, позволяет что-то выбрать и установить пароль.
А для того, чтобы не требовалось запускать его от рута — во-первых, в скрипте start_wpa_supplicant.sh переназначаются права на control interface, добавляются права для группы netdev, а во-вторых, достаточно внести пользователя в группу netdev.

Таким образом, вся установка сводится к тому, чтобы установить необходимые пакеты:

sudo apt install wpasupplicant libgtk3-perl isc-dhcp-client iw

любым удобным способом прописать пользователя в группу netdev, и разложить файлы по своим местам

/etc/udev/rules.d/99-wifi-autostart.rules
/etc/wpa_supplicant/start_wpa_supplicant.sh
/etc/wpa_supplicant/start_wpa_dhcp.sh
/usr/local/bin/wifi_ctl.pl

Чем это лучше NetworkManager и компании? Тем, что не требует установки кучи всего, только самое необходимое.
Чем это лучше iwd, например? Не знаю. Может быть тем, что тут всё прозрачно, кто что делает, и всегда можно посмотреть, что и где пошло не так.
В конце концов, я для себя делал, меня вроде пока устраивает.


ссылка на оригинал статьи https://habr.com/ru/articles/893854/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *