wifictl: corrigir trava de conexão com senha/SSID especiais

Vários edge cases no fluxo de conexão WiFi com caracteres não-ASCII /
metacaracteres causavam o sintoma reportado "trava ao conectar":

1. iwd codifica caracteres não ([A-Za-z0-9._-]) no nome do .psk como
   =XX (man iwd.network "Network file names"). wifictl gravava o
   profile como /var/lib/iwd/<SSID literal>.psk — quando o SSID tinha
   espaço, ç, +, /, etc. iwd não encontrava o arquivo, o iwctl
   station connect caía em prompt interativo aguardando passphrase via
   stdin e ficava preso até o autoconnect timeout (30+s). encode_iwd_
   name aplica a transformação canônica byte-a-byte (validado com
   "Café" -> "Caf=C3=A9", "DIRECT-17-HP Douglas" -> "DIRECT-17-HP=20
   Douglas").

2. Senhas fora do intervalo válido (WPA-PSK: 8..63 bytes printable ou
   64 hex chars) também faziam iwd rejeitar o profile e cair no
   mesmo trava-no-prompt-stdin. wifictl agora rejeita cedo com retorno
   2 e mensagem em stderr, dando feedback útil para a UI.

3. Profile órfão de tentativa anterior podia conflitar: ao trocar
   entre rede aberta e WPA, ou ao mudar a senha, o iwd preferia o
   profile antigo (.open / .psk). Agora wifictl remove os dois antes
   de regravar.

4. O check de pós-conexão era frouxo: "state=connected" sozinho
   retornava sucesso quando a estação já estava em outra rede,
   mesmo que a conexão solicitada falhasse silenciosamente. Adicionado
   match contra "Connected network" do iwctl show.

5. O heredoc do create_adhoc usava <<EOF (não-quoted) — um $ literal
   na passphrase disparava expansão de variável bash. Substituído por
   printf %s para preservar a senha byte-a-byte.

6. /var/lib/iwd/*.psk passa a ser criado com umask 077 — a passphrase
   está em texto claro no arquivo e não deveria ser legível por nada
   além do iwd (rodando como root).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Douglas Teles
2026-06-24 11:50:04 -03:00
parent 44dfbad623
commit e48e9675a9
@@ -66,13 +66,57 @@ connect_wifi() {
iwctl device ${WIFI_DEV} set-property Mode station 2>/dev/null
sleep 1
# Validar a passphrase. WPA-PSK exige 8..63 bytes de printable ASCII
# ou exatamente 64 hex chars. Um PSK fora desse intervalo faz iwd
# rejeitar o profile e o iwctl connect ficar esperando passphrase via
# stdin — wifictl não tem TTY, então o iwctl trava até o timeout do
# autoconnect (30+s). Falhar cedo dá feedback útil para a UI no ES.
if [ -n "${PSK}" ]; then
PSK_LEN=$(printf '%s' "${PSK}" | wc -c)
if [ "${PSK_LEN}" -lt 8 ] || [ "${PSK_LEN}" -gt 63 ]; then
if [ "${PSK_LEN}" -ne 64 ] || ! printf '%s' "${PSK}" | grep -qE '^[0-9a-fA-F]{64}$'; then
echo "wifictl: passphrase has ${PSK_LEN} bytes; WPA-PSK accepts 8-63 printable bytes or 64 hex chars" >&2
return 2
fi
fi
fi
# iwd codifica caracteres não-imprimíveis e alguns metacaracteres do
# SSID como =XX no nome do .psk (man iwd.network "Network file names").
# Sem isso, redes com espaços, +, /, etc. no SSID têm o arquivo
# gravado em um nome que iwd não procura, e o iwctl cai em prompt
# interativo aguardando a passphrase — sintoma visível como "trava".
# encode_iwd_name implementa a transformação canônica.
encode_iwd_name() {
printf '%s' "$1" | awk '
BEGIN { for (i = 0; i < 256; i++) byteToChar[sprintf("%c", i)] = sprintf("%02x", i) }
{
out = ""
n = length($0)
for (i = 1; i <= n; i++) {
c = substr($0, i, 1)
# iwd allows: letters, digits, ".", "-", "_". Outros = hex.
if (c ~ /[A-Za-z0-9._-]/) out = out c
else out = out "=" toupper(byteToChar[c])
}
print out
}'
}
IWD_NAME=$(encode_iwd_name "${SSID}")
# Create iwd network profile for persistent connection
IWD_DIR="/var/lib/iwd"
mkdir -p "${IWD_DIR}"
# Limpa qualquer perfil antigo para o mesmo SSID antes de regravar
# (caso usuário mude entre rede aberta e PSK, ou troque a senha — sem
# o rm o iwd pode preferir o profile antigo conflitante).
rm -f "${IWD_DIR}/${IWD_NAME}.psk" "${IWD_DIR}/${IWD_NAME}.open" 2>/dev/null
if [ -n "${PSK}" ]; then
printf '[Security]\nPassphrase=%s\n' "${PSK}" > "${IWD_DIR}/${SSID}.psk"
# umask 077 antes do printf garante que o profile (com a senha em
# texto) não fique world-readable em /var/lib/iwd.
( umask 077; printf '[Security]\nPassphrase=%s\n' "${PSK}" > "${IWD_DIR}/${IWD_NAME}.psk" )
else
printf '[Security]\n' > "${IWD_DIR}/${SSID}.open"
printf '[Security]\n' > "${IWD_DIR}/${IWD_NAME}.open"
fi
# Scan and connect
@@ -80,10 +124,19 @@ connect_wifi() {
sleep 2
iwctl station ${WIFI_DEV} connect "${SSID}" 2>/dev/null
# Wait for connection
# Wait for connection. O "state=connected" sozinho não basta: se a
# estação já estava em outra rede o teste retornaria sucesso falso.
# Comparar também o "Connected network" com o SSID solicitado.
for i in $(seq 1 15); do
STATE=$(iwctl station ${WIFI_DEV} show 2>/dev/null | grep -i "state" | awk '{print $NF}')
[ "${STATE}" = "connected" ] && return 0
SHOW=$(iwctl station ${WIFI_DEV} show 2>/dev/null)
STATE=$(printf '%s\n' "${SHOW}" | awk '/^[[:space:]]*State[[:space:]]/ {print $NF}')
NET=$(printf '%s\n' "${SHOW}" | sed -n 's/^[[:space:]]*Connected network[[:space:]]*//p')
# Trim espaços ao redor do nome da rede
NET="${NET#"${NET%%[![:space:]]*}"}"
NET="${NET%"${NET##*[![:space:]]}"}"
if [ "${STATE}" = "connected" ] && [ "${NET}" = "${SSID}" ]; then
return 0
fi
sleep 1
done
return 1
@@ -92,20 +145,30 @@ connect_wifi() {
create_adhoc() {
wait_for_wifi
# Create an iwd AP profile file
cat <<EOF >"${IWD_AP_CFG_DIR}/${SSID}.ap"
[General]
Channel=${ADHOC_CHAN}
DisableHT=true
# Heredoc com EOF não-quoted permitia que um $ na senha disparasse
# expansão de variável bash; agora montamos o profile com printf %s
# para preservar a passphrase byte-a-byte. AP filename usa o mesmo
# encoding de connect_wifi.
IWD_NAME=$(printf '%s' "${SSID}" | awk '
BEGIN { for (i = 0; i < 256; i++) bc[sprintf("%c", i)] = sprintf("%02x", i) }
{ out=""; n=length($0)
for (i = 1; i <= n; i++) {
c = substr($0, i, 1)
if (c ~ /[A-Za-z0-9._-]/) out = out c
else out = out "=" toupper(bc[c])
}
print out }')
[Security]
Passphrase=${PSK}
[IPv4]
Address=${NETWORK_ADDRESS}.${ADHOC_CLIENT_ID}
Netmask=255.255.255.0
Gateway=${NETWORK_ADDRESS}.${ADHOC_CLIENT_ID}
EOF
mkdir -p "${IWD_AP_CFG_DIR}"
( umask 077
{
printf '[General]\nChannel=%s\nDisableHT=true\n\n' "${ADHOC_CHAN}"
printf '[Security]\nPassphrase=%s\n\n' "${PSK}"
printf '[IPv4]\nAddress=%s.%s\nNetmask=255.255.255.0\nGateway=%s.%s\n' \
"${NETWORK_ADDRESS}" "${ADHOC_CLIENT_ID}" \
"${NETWORK_ADDRESS}" "${ADHOC_CLIENT_ID}"
} > "${IWD_AP_CFG_DIR}/${IWD_NAME}.ap"
)
iwctl device ${WIFI_DEV} set-property Mode ap
# Start the AP profile
@@ -114,7 +177,19 @@ EOF
destroy_adhoc() {
iwctl ap ${WIFI_DEV} stop 2>/dev/null
# Apaga ambos os possíveis nomes (versão antiga sem encoding + nova
# com encoding) para não deixar profile órfão depois do upgrade.
rm -f "${IWD_AP_CFG_DIR}/${SSID}.ap" 2>/dev/null
IWD_NAME=$(printf '%s' "${SSID}" | awk '
BEGIN { for (i = 0; i < 256; i++) bc[sprintf("%c", i)] = sprintf("%02x", i) }
{ out=""; n=length($0)
for (i = 1; i <= n; i++) {
c = substr($0, i, 1)
if (c ~ /[A-Za-z0-9._-]/) out = out c
else out = out "=" toupper(bc[c])
}
print out }')
rm -f "${IWD_AP_CFG_DIR}/${IWD_NAME}.ap" 2>/dev/null
}
has_ap_mode() {