当前位置: 首页 > news >正文

DIY Linux 桌面:WiFi 管理器

大家好!我是大聪明-PLUS

任务的本质是:在没有安装现代桌面环境(包括网络管理器和 systemd)的情况下,如何顺利管理 Wi-Fi 网络连接?
复杂之处在于:假设我们有多个 Wi-Fi 适配器,可以同时连接多个网络。

简单!
不过首先,我们来简单了解一下“它的内部工作原理”(如果你不感兴趣,那就直接跳过)

手捧日出

为了不涵盖所有可能的情况,我将立即概述框架:
今天,WPA/WPA2 几乎随处可见,甚至在某些 ESP8266 中也是如此,因此我们正在讨论通过 WPA 进行连接。

在 Linux 中,著名的 wpa_supplicant 程序可以实现这一点。它不仅拥有易读易懂的配置,还能通过命令行(wpa_cli)进行控制,因此非常出色。

通常,它的设置存储在 /etc/wpa_supplicant 的某个位置,启动时它会读取包含已连接网络记录的配置文件,作为守护进程挂起,跟踪网络的出现和消失并在必要时连接到它们。

同时,它的运行可以通过“控制接口套接字”来控制,默认在/var/run/wpa_supplicant中,这就是wpa_cli所做的。

在最简单的情况下,您至少需要一个包含以下内容的配置文件:
/etc/wpa_supplicant.conf

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

(此处的国家/地区决定了允许的 WiFi 范围)

之后,运行它:

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

现在 wlan0 接口应该已经连接到 MyNet 网络了。
剩下的就是通过 DHCP 获取 IP 地址……

但这只适用于台式计算机,当预先知道网络、知道 Wi-Fi 适配器时,只需注册一次然后从 root 运行它即可。

实际上,情况要复杂一些。
首先,现在通常会给 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 必须以 root 身份启动。

好吧,最后,还有一个问题 - 适配器可能只是电源关闭(省电模式开启) - 那么您需要先将其打开。

因此,算法如下:
1 - 找到适配器

2 - 打开电源
3 - 作为网络接口打开(这有点不同)
4 - 为其运行 wpa_supplicant
5 - 如果网络是新的 - 注册并保存设置
6 - 连接到 AP 后 - 获取 IP 地址。

这就是我们实现自动化的。

让电脑工作吧,它是铁做的

首先,脚本将搜索适配器、为其创建配置文件并启动守护进程:

#!/bin/sh -x# /etc/wpa_supplicant/start_wpa_supplicant.shPATH=/sbin:$PATH; export PATHCONFIG_DIR=/etc/wpa_supplicant
CTL_DIR=/run/wpa_supplicantexec >> /var/log/wpa_auto.log
exec 2>&1check_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} ] ; thenIFACES=$(iwconfig 2>&1 | grep IEEE | awk '{ print $1 }')
elseIFACES=$(iw dev | grep Interface | awk '{ print $2 }')
fifor i in ${IFACES} ; doecho ${i}CONFIG_FILE=${CONFIG_DIR}/iface_${i}.confx=$(ps ax | grep -v grep | grep "${CONFIG_FILE}" | wc -l)if [ "$x" -eq "0" ] ; thenecho "No wpa_supplicant for ${i} found"# Включим питание на всякий случайif [ ${NO_IW} ] ; theniwconfig ${i} power onelseiw dev ${i} set power_save offficheck_config_fileip link set "${i}" upwpa_supplicant -i "${i}" -c "${CONFIG_FILE}" -B -C ${CTL_DIR}chown -R root:netdev ${CTL_DIR}wpa_cli -a /etc/wpa_supplicant/wpa_dhcp.sh -i ${i} -Bfidoneexit

是的,这是一个非常普通的 Shell 脚本。它使用 iw 或 iwconfig 检查系统中有哪些适配器,如果配置文件尚不存在,则为其创建配置文件,并启动守护进程。
此外,它还会启动一个后台进程来监视连接状态,以便自动启动 dhclient:

#!/bin/sh# /etc/wpa_supplicant/wpa_dhcp.shPATH=/sbin:$PATH; export PATHexec >> /var/log/wpa_dhcp.log
exec 2>&1INTERFACE="$1"
EVENT="$2"echo "Event received: $EVENT on interface $INTERFACE" if [ "$EVENT" = "CONNECTED" ]; thenecho "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; });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);#
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);
}# =========================================
#
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;
}#
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);
}#
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);#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);# -------------------------------------------#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;

该脚本仅在 Windows 中显示已找到的网络,允许您选择网络并设置密码。
为了避免需要以 root 身份运行,首先,在脚本 start_wpa_supplicant.sh 中重新分配控制接口的权限,添加 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 无法与适配器一起使用,因为“协议不支持地址系列”
- 但是,如果您手动运行脚本而不是 udevd - 一切都会正常工作并且受到支持,具有相同的内核、模块和权限。
- 启动延迟和其他非标准的萨满教没有帮助:udevd 在 Ubuntu 中启动的工作方式与直接启动的相同内容不同。
- 在没有 systemd 的 Debian 中一切正常。

也就是说,再次确认了版本号,使用 systemd 会导致脚本运行异常,不符合预期。
我承认,如果“使用 systemd 启动”——那么一切都会正常,但这是错误的,就像一个老笑话里说的“三年级的铅笔”,既不是二年级的,也不是四年级的。
电脑里不应该有魔法。


文章转载自:

http://BQnSlJS9.dfkby.cn
http://yEDW7wu0.dfkby.cn
http://FvKd6C49.dfkby.cn
http://cPSFPdNO.dfkby.cn
http://B5IvcERv.dfkby.cn
http://C9x1Oobl.dfkby.cn
http://5ZkfI8wY.dfkby.cn
http://Wdw5OAnX.dfkby.cn
http://AQOGh4nD.dfkby.cn
http://MkwSSIx4.dfkby.cn
http://i0J4ymzc.dfkby.cn
http://GsUfVqx1.dfkby.cn
http://GFI3Hfpk.dfkby.cn
http://kzfD5wd2.dfkby.cn
http://btsBxuEk.dfkby.cn
http://ggtCgODU.dfkby.cn
http://tnHsPwAz.dfkby.cn
http://187uwhSM.dfkby.cn
http://bgRkGsOu.dfkby.cn
http://mB8FNicL.dfkby.cn
http://XJtfLDsI.dfkby.cn
http://FQ43Pcn3.dfkby.cn
http://9uOEIw7i.dfkby.cn
http://7OvGiBQ5.dfkby.cn
http://0M3gc3oT.dfkby.cn
http://KkNblRd7.dfkby.cn
http://X8JGqZMD.dfkby.cn
http://Q75NUAwj.dfkby.cn
http://lzBIccxv.dfkby.cn
http://enBY6gaW.dfkby.cn
http://www.dtcms.com/a/385921.html

相关文章:

  • 从 Pump.fun「直播」看热点币的生与死
  • 《算法闯关指南:优选算法-双指针》--05有效三角形的个数,06查找总价值为目标值的两个商品
  • Java List 详解:从基础到进阶的全面指南
  • 【问题】自启动的容器在开机重启后 都退出了,未能正常启动
  • 苹果手机上有没有可以定时提醒做事的工具
  • blender多个动作导入到unity
  • 通过adb dump activity的configChanges配置
  • 智能语音机器人如何提升语音交互机器人的交互能力?
  • 一文读懂Docker:从入门到实践
  • 控制IP端口访问的方法
  • VS2017 下openssl-1.1.1+ libwebsockets-4.0.0 编译
  • 从 “无感服务” 到 “情感连接”:智慧园区如何用科技重构企业归属感
  • 封装形成用助焊剂:电子制造“隐形桥梁”的技术突围与全球产业重构
  • 3dsMax 2026 .NET Core 8 转型下的Maxscript脚本开发:动态编译模块的重构策略与兼容性升级路径
  • 高并发异步处理实战指南与性能优化策略
  • React18学习笔记(二) React的状态管理工具--Redux,案例--移动端外卖平台
  • ReactJS + DynamoDB 性能优化方案
  • Next.js与React服务端渲染演进全解析
  • C++ `std::future` 与 `std::promise` 超全解析笔记
  • VScode插件Remote-SSH
  • 挣脱网络桎梏:CapsWriter-Offline+cpolar,让高效输入不受网络牵绊
  • Qt地图软件开发/GIS软件开发组件/修改天地图支持21级别/离线瓦片地图
  • Kafka 跨集群地理复制(Geo-Replication)
  • ​​[硬件电路-235]:双极型三极管、MOS场效应管、IGBT管异同比较
  • Spark专题-第二部分:Spark SQL 入门(1)-Spark SQL 简介
  • Spark源码学习分享之submit提交流程(1)
  • 5、二叉树-小堆
  • 技术奇点爆发周:2025 年 9 月科技突破全景扫描
  • 从Dubbo到SpringCloud Alibaba:大型项目迁移的实战手册(含成本分析与踩坑全记录)(一)
  • 【算法】C语言多组输入输出模板