По просьбе некоторых комментаторов «что-то написать самому и выложить на обозрение» — ну вот, написал и выкладываю:
Суть задачи: как, не имея установленного современного 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/
Добавить комментарий