Как стать автором
Обновить

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

Время на прочтение11 мин
Количество просмотров4K

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

Суть задачи: как, не имея установленного современного 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 $ctl_dir = "/run/wpa_supplicant";

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;
  opendir(my $d, $ctl_dir);
  while(my $str = readdir($d)){
    next if ($str eq '.' || $str eq '..');
    print "($str)\n";
    my $iter = $list_A->get_model->append();
    $list_A->get_model->set($iter, 0 => $str);
  }
  closedir($d);
}

# Получение выбранного интерфейса
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, например? Не знаю. Может быть тем, что тут всё прозрачно, кто что делает, и всегда можно посмотреть, что и где пошло не так.
В конце концов, я для себя делал, меня вроде пока устраивает.

PS: попробовал запустить это всё под Ubuntu (systemd работает):
- программа в принципе запускается через udevd
- при этом wpa_supplicant работать с адаптером не может, потому что "Address family not supported by protocol"
- однако если вместо udevd запустить скрипт руками - всё прекрасно работает и supported, с тем же самым ядром, модулями, и правами.
- шаманства с задержкой запуска и прочими нестандартностями не помогают: то, что запускает udevd, работает в Ubuntu не так, как то же самое, что запускается напрямую.
- в Debian без systemd все работает прекрасно.

То есть лишний раз подтверждение версии, что использование systemd заставляет скрипты работать непредсказуемо, не так как они должны работать.
Допускаю, что если "использовать systemd для запуска" - то всё будет ОК, но это неправильно, это все равно что "карандаши для третьего класса", из древнего анекдота, которые не пишут ни во втором, ни в четвертом.
Магии не должно быть в компьютере.

Теги:
Хабы:
Всего голосов 11: ↑11 и ↓0+15
Комментарии9

Публикации

Ближайшие события