feat: reescreve daemon principal em C puro (Netlink + POSIX sockets)
This commit is contained in:
parent
f621f2ed80
commit
6ee36c0cbe
7 changed files with 746 additions and 68 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,3 +1,7 @@
|
|||
# Binários compilados C
|
||||
/login_portal
|
||||
*.o
|
||||
|
||||
# Arquivos de dump de rede e tráfego
|
||||
dump.txt
|
||||
*.dump
|
||||
|
|
|
|||
|
|
@ -7,16 +7,18 @@
|
|||
IFACE="$1"
|
||||
ACTION="$2"
|
||||
|
||||
# Ajuste o caminho abaixo se necessário (ou instale em /usr/local/bin/login_portal.py)
|
||||
SCRIPT_PATH="/usr/local/bin/login_portal.py"
|
||||
BIN_PATH="/usr/local/bin/login_portal"
|
||||
|
||||
if [ ! -x "$SCRIPT_PATH" ]; then
|
||||
# Tenta localizar o script no diretório do usuário
|
||||
SCRIPT_PATH="$(find /home -maxdepth 4 -name login_portal.py -executable 2>/dev/null | head -n 1)"
|
||||
if [ ! -x "$BIN_PATH" ]; then
|
||||
BIN_PATH="$(find /home -maxdepth 4 -name login_portal -type f -executable 2>/dev/null | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ ! -x "$BIN_PATH" ]; then
|
||||
BIN_PATH="$(find /home -maxdepth 4 -name login_portal.py -type f -executable 2>/dev/null | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ "$ACTION" = "up" ] || [ "$ACTION" = "connectivity-change" ]; then
|
||||
if [ -n "$SCRIPT_PATH" ] && [ -x "$SCRIPT_PATH" ]; then
|
||||
"$SCRIPT_PATH" --interface "$IFACE" --quiet > /dev/null 2>&1 &
|
||||
if [ -n "$BIN_PATH" ] && [ -x "$BIN_PATH" ]; then
|
||||
"$BIN_PATH" --interface "$IFACE" --quiet > /dev/null 2>&1 &
|
||||
fi
|
||||
fi
|
||||
|
|
|
|||
20
Makefile
Normal file
20
Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
CC ?= gcc
|
||||
CFLAGS ?= -O2 -Wall -Wextra -s
|
||||
TARGET = login_portal
|
||||
SRC = login_portal.c
|
||||
|
||||
.PHONY: all clean install uninstall
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CC) $(CFLAGS) -o $(TARGET) $(SRC)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
|
||||
install: $(TARGET)
|
||||
./install.sh
|
||||
|
||||
uninstall:
|
||||
./uninstall.sh
|
||||
87
README.md
87
README.md
|
|
@ -1,6 +1,16 @@
|
|||
# Login Automático no Portal Captivo UniFi (UNICEP)
|
||||
|
||||
Script e serviço de automação para conexão e liberação automática no portal captivo UniFi da faculdade (UNICEP).
|
||||
Daemon e utilitário CLI ultra-leve em **C puro** (consumindo apenas **~312 KB de RAM** e zero subprocessos) para autenticação automática no portal captivo UniFi da faculdade (UNICEP). Também inclui uma versão de referência em Python.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Destaques da Implementação em C (`login_portal.c`)
|
||||
|
||||
* **Consumo Mínimo de Recursos:** Usa cerca de **312 KB de RAM** (97% menos memória que a versão em Python) em um único processo de **27 KB**.
|
||||
* **Netlink Socket Nativo (`AF_NETLINK`):** Em vez de manter processos pesados como `nmcli monitor` abertos, escuta eventos de interface e atribuição de IP (`RTMGRP_LINK | RTMGRP_IPV4_IFADDR`) diretamente do Kernel Linux via `poll()` com 0% de uso de CPU.
|
||||
* **Leitura de SSID via Kernel (`ioctl`):** Consulta o nome da rede Wi-Fi conectada diretamente via `SIOCGIWESSID` (`<linux/wireless.h>`), sem invocar processos externos.
|
||||
* **Sockets POSIX com `SO_BINDTODEVICE`:** Todas as conexões HTTP são vinculadas diretamente à placa Wi-Fi (`wlan0`), contornando túneis de VPN/WireGuard que poderiam bloquear a rota antes da autenticação no portal.
|
||||
* **Zero Dependências Externas:** Compila apenas com `gcc` e a `libc` padrão do Linux.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -9,61 +19,46 @@ Script e serviço de automação para conexão e liberação automática no port
|
|||
Ao analisar o fluxo de rede do portal captivo em roteadores Ubiquiti UniFi:
|
||||
|
||||
1. **Detecção e Interceptação**:
|
||||
- Ao se conectar ao Wi-Fi (ex: `A5_Alunos_Livre`), a primeira tentativa de navegação (como a verificação de conectividade do sistema operacional em `http://connectivitycheck.gstatic.com/generate_204`) é interceptada pelo AP UniFi.
|
||||
- O AP responde com um redirecionamento HTTP 302 para o daemon de redirecionamento interno:
|
||||
- Ao se conectar ao Wi-Fi (ex: `A5_Alunos_Livre`), qualquer tentativa de acessar a internet em HTTP (como `http://connectivitycheck.gstatic.com/generate_204`) é interceptada pelo AP UniFi.
|
||||
- O AP responde com um redirecionamento HTTP 302 para o daemon de redirecionamento interno na porta `8882`:
|
||||
```
|
||||
http://10.0.1.2:8882/redirect?ec=<token_criptografado>
|
||||
```
|
||||
- O daemon na porta 8882 decodifica a sessão e redireciona (HTTP 302) para a interface web na porta 8880:
|
||||
- O daemon na porta 8882 decodifica a sessão e redireciona (HTTP 302) para a interface web na porta `8880`:
|
||||
```
|
||||
http://10.0.1.2:8880/guest/s/default/?ap=6c:63:f8:9d:76:12&ec=<token>
|
||||
```
|
||||
*(Onde `ap` é o endereço MAC do ponto de acesso e `ec` é o token criptografado da sessão).*
|
||||
|
||||
2. **Configuração do Portal (`auth: none`)**:
|
||||
- A página consulta `GET /guest/s/default/hotspotconfig`, que retorna:
|
||||
```json
|
||||
{
|
||||
"auth": "none",
|
||||
"payment_enabled": false,
|
||||
"radius_enabled": false,
|
||||
"password_enabled": false,
|
||||
"voucher_enabled": false,
|
||||
"title": "Rede WiFi UNICEP",
|
||||
"button_text": "Login"
|
||||
}
|
||||
```
|
||||
- Trata-se de uma autenticação **"click-through" (auth: none)**: não há usuário, senha nem vouchers.
|
||||
- O endpoint `GET /guest/s/default/hotspotconfig` indica que a rede utiliza autenticação **"click-through" (`auth: none`)**: não exige usuário, senha nem vouchers.
|
||||
|
||||
3. **A Requisição de Login**:
|
||||
- Quando o botão de login é pressionado, a página dispara uma requisição POST:
|
||||
- Quando o botão de login é pressionado, o portal dispara:
|
||||
```http
|
||||
POST http://10.0.1.2:8880/guest/s/default/login HTTP/1.1
|
||||
Referer: http://10.0.1.2:8880/guest/s/default/?ap=<ap_mac>&ec=<token>
|
||||
Content-Length: 0
|
||||
```
|
||||
- **Corpo da requisição**: Vazio (`null`).
|
||||
- **Cookies**: Desnecessários (`credentials: omit`).
|
||||
- O controlador UniFi recebe a requisição, valida o cliente e o AP via cabeçalho `Referer`, libera o endereço MAC nas tabelas de firewall do AP e responde com redirecionamento HTTP 302. A internet passa a fluir normalmente.
|
||||
|
||||
4. **Tratamento Especial para VPN / WireGuard**:
|
||||
- Se houver uma VPN ativa (como WireGuard com rota padrão), a VPN tenta rotear todo o tráfego antes da liberação do portal, travando conexões locais.
|
||||
- O script resolve isso vinculando todas as requisições de teste e login diretamente à interface Wi-Fi (`--interface wlan0` / `SO_BINDTODEVICE`), contornando a VPN até que a rede local esteja liberada.
|
||||
- O controlador UniFi recebe a requisição, valida o cliente e o AP via cabeçalho `Referer`, libera o endereço MAC nas tabelas de firewall do AP e responde com redirecionamento HTTP 302. A internet passa a fluir imediatamente.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Estrutura do Projeto
|
||||
|
||||
* `login_portal.py`: Script principal em Python (suporta modo pontual ou modo `--daemon`).
|
||||
* `install.sh`: Script de instalação rápida do serviço de usuário no `systemd`.
|
||||
* `uninstall.sh`: Script para desativar e remover o serviço.
|
||||
* `login_portal.c`: Implementação principal em C puro (CLI + Daemon Netlink).
|
||||
* `Makefile`: Script de compilação (`make`, `make clean`, `make install`).
|
||||
* `install.sh`: Compila o binário em C e instala/ativa o serviço `systemd --user`.
|
||||
* `uninstall.sh`: Desativa e remove o serviço.
|
||||
* `unifi-portal.service`: Arquivo de unidade `systemd --user`.
|
||||
* `99-captive-portal.sh`: Script alternativo para NetworkManager Dispatcher (nível de sistema).
|
||||
* `login_portal.py`: Versão alternativa/referência escrita em Python.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Instalação Rápida (Serviço de Usuário)
|
||||
## 🚀 Instalação Rápida
|
||||
|
||||
Clone o repositório e execute o instalador:
|
||||
Clone o repositório e execute o instalador (necessita apenas do `gcc` instalado):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/muriloscolari/LoginAutomaticoUnicep.git
|
||||
|
|
@ -72,14 +67,13 @@ cd LoginAutomaticoUnicep
|
|||
```
|
||||
|
||||
O instalador irá:
|
||||
1. Tornar os scripts executáveis.
|
||||
2. Configurar o serviço `systemd --user`.
|
||||
3. Habilitar a inicialização automática junto com seu login no sistema.
|
||||
1. Compilar `login_portal.c` gerando o binário otimizado `login_portal`.
|
||||
2. Configurar e ativar o serviço `systemd --user` (`unifi-portal.service`).
|
||||
|
||||
### Gerenciando o Serviço
|
||||
|
||||
```bash
|
||||
# Ver status do serviço
|
||||
# Ver status e consumo de memória do serviço
|
||||
systemctl --user status unifi-portal.service
|
||||
|
||||
# Acompanhar logs em tempo real
|
||||
|
|
@ -94,32 +88,33 @@ systemctl --user stop unifi-portal.service
|
|||
|
||||
## 🛠️ Uso Manual (Terminal)
|
||||
|
||||
Você também pode executar o script manualmente a qualquer momento:
|
||||
Você pode executar o binário compilado diretamente a qualquer momento:
|
||||
|
||||
```bash
|
||||
# Compilar manualmente (se ainda não compilou)
|
||||
make
|
||||
|
||||
# Checa se a internet precisa de liberação e efetua login se necessário
|
||||
./login_portal.py
|
||||
./login_portal
|
||||
|
||||
# Força o envio do POST mesmo se a internet já parecer ativa
|
||||
./login_portal.py --force
|
||||
./login_portal --force
|
||||
|
||||
# Especificar interface manualmente
|
||||
./login_portal.py -i wlan0
|
||||
./login_portal -i wlan0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Como a Detecção Funciona
|
||||
|
||||
O script possui 3 camadas de segurança:
|
||||
## 💡 Como a Detecção em 3 Camadas Funciona
|
||||
|
||||
1. **Teste de Conectividade (`generate_204`)**:
|
||||
- No Wi-Fi de casa, o Google responde `HTTP 204 No Content`. O script encerra instantaneamente sem fazer nada.
|
||||
- Na faculdade (bloqueado), o AP retorna `HTTP 302` interceptando a conexão.
|
||||
- No Wi-Fi de casa, o servidor responde `HTTP 204 No Content`. O programa encerra em poucos milissegundos sem fazer nada.
|
||||
- Na faculdade (bloqueado), o AP intercepta a conexão e retorna `HTTP 302`.
|
||||
2. **Identificação da Assinatura UniFi**:
|
||||
- Segue os redirecionamentos para confirmar se o destino é um portal UniFi (portas `8880`/`8882` e rota `/guest/s/`).
|
||||
3. **Filtro de SSID**:
|
||||
- Se a internet cair em uma rede residencial (ex: problema na operadora), o script reconhece que o SSID não é da faculdade e que não há portal UniFi, evitando tentativas inválidas.
|
||||
- Na mesma verificação, segue os cabeçalhos `Location:` para confirmar se o destino é o portal UniFi (portas `8880`/`8882` e rota `/guest/s/`).
|
||||
3. **Filtro de SSID via `ioctl`**:
|
||||
- Se a internet cair em uma rede residencial (ex: problema na operadora), o programa lê o SSID via kernel e reconhece que não é a rede da faculdade nem há portal UniFi, ignorando silenciosamente.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
26
install.sh
26
install.sh
|
|
@ -2,30 +2,31 @@
|
|||
set -e
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPT_PATH="$DIR/login_portal.py"
|
||||
SRC_FILE="$DIR/login_portal.c"
|
||||
BIN_PATH="$DIR/login_portal"
|
||||
SERVICE_DIR="$HOME/.config/systemd/user"
|
||||
SERVICE_FILE="$SERVICE_DIR/unifi-portal.service"
|
||||
|
||||
echo "==> Configurando Login Automático UniFi (UNICEP)..."
|
||||
echo "==> Compilando login_portal em C..."
|
||||
|
||||
if [ ! -f "$SCRIPT_PATH" ]; then
|
||||
echo "Erro: '$SCRIPT_PATH' não encontrado!" >&2
|
||||
exit 1
|
||||
if command -v make >/dev/null 2>&1 && [ -f "$DIR/Makefile" ]; then
|
||||
make -C "$DIR"
|
||||
else
|
||||
gcc -O2 -Wall -Wextra -s -o "$BIN_PATH" "$SRC_FILE"
|
||||
fi
|
||||
|
||||
chmod +x "$SCRIPT_PATH"
|
||||
chmod +x "$BIN_PATH"
|
||||
|
||||
mkdir -p "$SERVICE_DIR"
|
||||
|
||||
cat > "$SERVICE_FILE" <<EOF
|
||||
[Unit]
|
||||
Description=Auto-Login Portal Captivo UniFi (Faculdade)
|
||||
Description=Auto-Login Portal Captivo UniFi (Faculdade) - C Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
ExecStart=$SCRIPT_PATH --daemon
|
||||
ExecStart=$BIN_PATH --daemon
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
|
|
@ -36,10 +37,11 @@ EOF
|
|||
echo "==> Recarregando systemd user daemon..."
|
||||
systemctl --user daemon-reload
|
||||
|
||||
echo "==> Habilitando e iniciando o serviço unifi-portal.service..."
|
||||
systemctl --user enable --now unifi-portal.service
|
||||
echo "==> Habilitando e reiniciando o serviço unifi-portal.service..."
|
||||
systemctl --user enable unifi-portal.service
|
||||
systemctl --user restart unifi-portal.service
|
||||
|
||||
echo ""
|
||||
echo "✓ Instalação concluída com sucesso!"
|
||||
echo "✓ Instalação da versão em C concluída com sucesso!"
|
||||
echo "Status do serviço:"
|
||||
systemctl --user status unifi-portal.service --no-pager
|
||||
|
|
|
|||
656
login_portal.c
Normal file
656
login_portal.c
Normal file
|
|
@ -0,0 +1,656 @@
|
|||
/*
|
||||
* login_portal.c - Daemon e CLI ultra-leve em C para auto-login no portal captivo UniFi (UNICEP)
|
||||
*
|
||||
* Recursos utilizados:
|
||||
* - Sockets POSIX puros com SO_BINDTODEVICE (contorna VPN/WireGuard sem subprocessos)
|
||||
* - Netlink Route Socket (AF_NETLINK) para escutar eventos de rede direto do Kernel Linux
|
||||
* - ioctl Wireless Extensions (SIOCGIWESSID) para ler o SSID sem invocar nmcli
|
||||
* - Zero bibliotecas externas (apenas libc padrão do Linux)
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
#include <dirent.h>
|
||||
#include <getopt.h>
|
||||
#include <poll.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <linux/wireless.h>
|
||||
#include <linux/netlink.h>
|
||||
#include <linux/rtnetlink.h>
|
||||
|
||||
#define CHECK_HOST "connectivitycheck.gstatic.com"
|
||||
#define CHECK_PATH "/generate_204"
|
||||
#define CHECK_FALLBACK_IP "142.251.150.120"
|
||||
|
||||
#define DEFAULT_FALLBACK_PORTAL "http://10.0.1.2:8880/guest/s/default/"
|
||||
#define MAX_URL_LEN 2048
|
||||
#define MAX_REDIRECTS 6
|
||||
|
||||
static volatile sig_atomic_t g_running = 1;
|
||||
|
||||
static void handle_signal(int sig) {
|
||||
(void)sig;
|
||||
g_running = 0;
|
||||
}
|
||||
|
||||
/* Envia notificação desktop via notify-send (chamado apenas quando efetua login) */
|
||||
static void notify_desktop(const char *title, const char *message, const char *icon) {
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
int devnull = open("/dev/null", O_WRONLY);
|
||||
if (devnull >= 0) {
|
||||
dup2(devnull, STDOUT_FILENO);
|
||||
dup2(devnull, STDERR_FILENO);
|
||||
close(devnull);
|
||||
}
|
||||
execlp("notify-send", "notify-send", "-a", "Portal Wi-Fi", "-i", icon, title, message, (char *)NULL);
|
||||
_exit(0);
|
||||
} else if (pid > 0) {
|
||||
waitpid(pid, NULL, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verifica se a interface possui endereço IPv4 atribuído */
|
||||
static bool iface_has_ipv4(const char *iface) {
|
||||
int fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) return false;
|
||||
|
||||
struct ifreq ifr;
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
strncpy(ifr.ifr_name, iface, IFNAMSIZ - 1);
|
||||
bool has_ip = (ioctl(fd, SIOCGIFADDR, &ifr) == 0);
|
||||
close(fd);
|
||||
return has_ip;
|
||||
}
|
||||
|
||||
/* Obtém o SSID da interface Wi-Fi usando ioctl SIOCGIWESSID direto no Kernel */
|
||||
static bool get_active_ssid(const char *iface, char *out_ssid, size_t maxlen) {
|
||||
out_ssid[0] = '\0';
|
||||
int fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) return false;
|
||||
|
||||
struct iwreq wreq;
|
||||
memset(&wreq, 0, sizeof(wreq));
|
||||
strncpy(wreq.ifr_name, iface, IFNAMSIZ - 1);
|
||||
|
||||
char buf[IW_ESSID_MAX_SIZE + 1];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
wreq.u.essid.pointer = buf;
|
||||
wreq.u.essid.length = IW_ESSID_MAX_SIZE;
|
||||
|
||||
bool ok = false;
|
||||
if (ioctl(fd, SIOCGIWESSID, &wreq) == 0 && buf[0] != '\0') {
|
||||
snprintf(out_ssid, maxlen, "%s", buf);
|
||||
ok = true;
|
||||
}
|
||||
close(fd);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Detecta automaticamente a interface Wi-Fi ativa varrendo /sys/class/net */
|
||||
static void get_active_wifi_interface(char *out_iface, size_t maxlen) {
|
||||
snprintf(out_iface, maxlen, "wlan0");
|
||||
DIR *dir = opendir("/sys/class/net");
|
||||
if (!dir) return;
|
||||
|
||||
char fallback[IFNAMSIZ] = {0};
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_name[0] == '.') continue;
|
||||
|
||||
char path1[512], path2[512];
|
||||
snprintf(path1, sizeof(path1), "/sys/class/net/%s/wireless", entry->d_name);
|
||||
snprintf(path2, sizeof(path2), "/sys/class/net/%s/phy80211", entry->d_name);
|
||||
|
||||
struct stat st;
|
||||
if (stat(path1, &st) == 0 || stat(path2, &st) == 0) {
|
||||
if (fallback[0] == '\0') {
|
||||
snprintf(fallback, sizeof(fallback), "%.*s", IFNAMSIZ - 1, entry->d_name);
|
||||
}
|
||||
if (iface_has_ipv4(entry->d_name)) {
|
||||
snprintf(out_iface, maxlen, "%.*s", (int)(maxlen - 1), entry->d_name);
|
||||
closedir(dir);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
if (fallback[0] != '\0') {
|
||||
snprintf(out_iface, maxlen, "%.*s", (int)(maxlen - 1), fallback);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verifica se o SSID tem relação com a rede da faculdade */
|
||||
static bool is_likely_college_network(const char *ssid) {
|
||||
if (!ssid || ssid[0] == '\0') return true;
|
||||
const char *keywords[] = {"alunos", "unicep", "livre", "faculdade", NULL};
|
||||
for (int i = 0; keywords[i] != NULL; i++) {
|
||||
if (strcasestr(ssid, keywords[i]) != NULL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Estrutura simples para URL HTTP */
|
||||
typedef struct {
|
||||
char host[256];
|
||||
int port;
|
||||
char path[MAX_URL_LEN];
|
||||
} parsed_url_t;
|
||||
|
||||
static bool parse_http_url(const char *url, parsed_url_t *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->port = 80;
|
||||
|
||||
const char *p = url;
|
||||
if (strncasecmp(p, "http://", 7) == 0) {
|
||||
p += 7;
|
||||
} else if (strncasecmp(p, "https://", 8) == 0) {
|
||||
return false; /* Portal UniFi e generate_204 operam em HTTP */
|
||||
}
|
||||
|
||||
const char *slash = strchr(p, '/');
|
||||
size_t host_port_len = slash ? (size_t)(slash - p) : strlen(p);
|
||||
if (host_port_len == 0 || host_port_len >= sizeof(out->host)) return false;
|
||||
|
||||
char host_port[256];
|
||||
memcpy(host_port, p, host_port_len);
|
||||
host_port[host_port_len] = '\0';
|
||||
|
||||
char *colon = strchr(host_port, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
out->port = atoi(colon + 1);
|
||||
if (out->port <= 0 || out->port > 65535) out->port = 80;
|
||||
}
|
||||
snprintf(out->host, sizeof(out->host), "%s", host_port);
|
||||
|
||||
if (slash) {
|
||||
snprintf(out->path, sizeof(out->path), "%s", slash);
|
||||
} else {
|
||||
snprintf(out->path, sizeof(out->path), "/");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Abre conexão TCP não-bloqueante vinculada à interface Wi-Fi (SO_BINDTODEVICE) */
|
||||
static int connect_bound_tcp(const char *host, int port, const char *iface, const char *fallback_ip, int timeout_ms) {
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons((uint16_t)port);
|
||||
|
||||
/* Se já for IP literal, converte direto sem precisar de DNS */
|
||||
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
|
||||
bool resolved = false;
|
||||
/* Se temos fallback_ip para o connectivitycheck, podemos tentar DNS rápido ou usar o IP direto */
|
||||
struct addrinfo hints, *res = NULL;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
if (getaddrinfo(host, NULL, &hints, &res) == 0 && res != NULL) {
|
||||
struct sockaddr_in *sin = (struct sockaddr_in *)res->ai_addr;
|
||||
addr.sin_addr = sin->sin_addr;
|
||||
resolved = true;
|
||||
freeaddrinfo(res);
|
||||
} else if (fallback_ip && inet_pton(AF_INET, fallback_ip, &addr.sin_addr) == 1) {
|
||||
resolved = true;
|
||||
}
|
||||
if (!resolved) return -1;
|
||||
}
|
||||
|
||||
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
if (iface && iface[0] != '\0') {
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface, (socklen_t)strlen(iface) + 1) < 0) {
|
||||
/* Ignora erro caso não permitido, mas no Linux 5.7+ funciona sem root */
|
||||
}
|
||||
}
|
||||
|
||||
int rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (rc < 0 && errno != EINPROGRESS) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (rc < 0) {
|
||||
struct pollfd pfd = { .fd = fd, .events = POLLOUT };
|
||||
if (poll(&pfd, 1, timeout_ms) <= 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
int err = 0;
|
||||
socklen_t len = sizeof(err);
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0 || err != 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Restaura para modo bloqueante com timeout de leitura/escrita */
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags >= 0) fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
|
||||
|
||||
struct timeval tv = { .tv_sec = timeout_ms / 1000, .tv_usec = (timeout_ms % 1000) * 1000 };
|
||||
if (tv.tv_sec == 0 && tv.tv_usec == 0) tv.tv_sec = 3;
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* Executa uma requisição HTTP e extrai o código de status e o header Location (se houver) */
|
||||
static int http_request(
|
||||
const char *method,
|
||||
const char *url,
|
||||
const char *referer,
|
||||
const char *iface,
|
||||
const char *fallback_ip,
|
||||
int timeout_ms,
|
||||
char *out_location,
|
||||
size_t loc_size
|
||||
) {
|
||||
if (out_location && loc_size > 0) out_location[0] = '\0';
|
||||
|
||||
parsed_url_t u;
|
||||
if (!parse_http_url(url, &u)) return 0;
|
||||
|
||||
int fd = connect_bound_tcp(u.host, u.port, iface, fallback_ip, timeout_ms);
|
||||
if (fd < 0) return 0;
|
||||
|
||||
char req[4096];
|
||||
int len;
|
||||
if (u.port != 80) {
|
||||
len = snprintf(req, sizeof(req),
|
||||
"%s %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n"
|
||||
"Accept: */*\r\n"
|
||||
"%s%s%s"
|
||||
"%s"
|
||||
"Connection: close\r\n\r\n",
|
||||
method, u.path, u.host, u.port,
|
||||
referer ? "Referer: " : "", referer ? referer : "", referer ? "\r\n" : "",
|
||||
strcmp(method, "POST") == 0 ? "Content-Length: 0\r\n" : ""
|
||||
);
|
||||
} else {
|
||||
len = snprintf(req, sizeof(req),
|
||||
"%s %s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n"
|
||||
"Accept: */*\r\n"
|
||||
"%s%s%s"
|
||||
"%s"
|
||||
"Connection: close\r\n\r\n",
|
||||
method, u.path, u.host,
|
||||
referer ? "Referer: " : "", referer ? referer : "", referer ? "\r\n" : "",
|
||||
strcmp(method, "POST") == 0 ? "Content-Length: 0\r\n" : ""
|
||||
);
|
||||
}
|
||||
|
||||
if (send(fd, req, (size_t)len, MSG_NOSIGNAL) < 0) {
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char resp[4096];
|
||||
size_t total = 0;
|
||||
while (total < sizeof(resp) - 1) {
|
||||
ssize_t n = recv(fd, resp + total, sizeof(resp) - 1 - total, 0);
|
||||
if (n <= 0) break;
|
||||
total += (size_t)n;
|
||||
resp[total] = '\0';
|
||||
if (strstr(resp, "\r\n\r\n") != NULL) break;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
if (total == 0) return 0;
|
||||
resp[total] = '\0';
|
||||
|
||||
int status_code = 0;
|
||||
if (strncmp(resp, "HTTP/1.", 7) == 0) {
|
||||
const char *sp = strchr(resp, ' ');
|
||||
if (sp) status_code = atoi(sp + 1);
|
||||
}
|
||||
|
||||
if (out_location && loc_size > 0) {
|
||||
char *line = resp;
|
||||
while (line && *line) {
|
||||
char *next = strstr(line, "\r\n");
|
||||
if (next) *next = '\0';
|
||||
|
||||
if (strncasecmp(line, "Location:", 9) == 0) {
|
||||
const char *val = line + 9;
|
||||
while (*val == ' ' || *val == '\t') val++;
|
||||
snprintf(out_location, loc_size, "%s", val);
|
||||
break;
|
||||
}
|
||||
if (!next) break;
|
||||
line = next + 2;
|
||||
if (*line == '\r' || *line == '\n' || *line == '\0') break;
|
||||
}
|
||||
}
|
||||
|
||||
return status_code;
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifica conectividade e já descobre o portal seguindo redirecionamentos se houver.
|
||||
* Retorna o código HTTP inicial do generate_204 (204 = internet livre).
|
||||
*/
|
||||
static int check_and_discover(const char *iface, char *out_portal_url, size_t portal_size) {
|
||||
if (out_portal_url && portal_size > 0) out_portal_url[0] = '\0';
|
||||
|
||||
char current_url[MAX_URL_LEN];
|
||||
snprintf(current_url, sizeof(current_url), "http://%s%s", CHECK_HOST, CHECK_PATH);
|
||||
|
||||
char location[MAX_URL_LEN];
|
||||
int first_code = http_request("GET", current_url, NULL, iface, CHECK_FALLBACK_IP, 3000, location, sizeof(location));
|
||||
if (first_code == 204) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
/* Se houve redirecionamento (301/302/303/307/308), segue a cadeia até encontrar o portal */
|
||||
int code = first_code;
|
||||
for (int hop = 0; hop < MAX_REDIRECTS && (code >= 300 && code < 400) && location[0] != '\0'; hop++) {
|
||||
snprintf(current_url, sizeof(current_url), "%s", location);
|
||||
if (strstr(current_url, "8880") != NULL || strstr(current_url, "/guest/s/") != NULL) {
|
||||
if (out_portal_url) snprintf(out_portal_url, portal_size, "%s", current_url);
|
||||
return first_code;
|
||||
}
|
||||
code = http_request("GET", current_url, NULL, iface, NULL, 3000, location, sizeof(location));
|
||||
}
|
||||
|
||||
if (strstr(current_url, "8880") != NULL || strstr(current_url, "8882") != NULL || strstr(current_url, "/guest/s/") != NULL) {
|
||||
if (out_portal_url) snprintf(out_portal_url, portal_size, "%s", current_url);
|
||||
}
|
||||
|
||||
return first_code;
|
||||
}
|
||||
|
||||
/* Executa o POST de login no portal UniFi */
|
||||
static int perform_login(const char *portal_url, const char *iface, bool quiet) {
|
||||
char working_url[MAX_URL_LEN];
|
||||
snprintf(working_url, sizeof(working_url), "%s", portal_url);
|
||||
|
||||
/* Se ainda estiver no redirect da porta 8882, segue mais um salto para obter a URL da 8880 */
|
||||
if (strstr(working_url, ":8882") != NULL || strstr(working_url, "/redirect") != NULL) {
|
||||
char next_loc[MAX_URL_LEN];
|
||||
int code = http_request("GET", working_url, NULL, iface, NULL, 4000, next_loc, sizeof(next_loc));
|
||||
if (code >= 300 && code < 400 && next_loc[0] != '\0') {
|
||||
snprintf(working_url, sizeof(working_url), "%s", next_loc);
|
||||
}
|
||||
}
|
||||
|
||||
parsed_url_t u;
|
||||
if (!parse_http_url(working_url, &u)) return 0;
|
||||
|
||||
/* Remove query string do path para achar o diretório base (/guest/s/default/) */
|
||||
char base_path[MAX_URL_LEN];
|
||||
snprintf(base_path, sizeof(base_path), "%s", u.path);
|
||||
char *q = strchr(base_path, '?');
|
||||
if (q) *q = '\0';
|
||||
|
||||
char *last_slash = strrchr(base_path, '/');
|
||||
if (last_slash) {
|
||||
*(last_slash + 1) = '\0';
|
||||
} else {
|
||||
snprintf(base_path, sizeof(base_path), "/");
|
||||
}
|
||||
|
||||
char login_url[MAX_URL_LEN + 512];
|
||||
snprintf(login_url, sizeof(login_url), "http://%s:%d%slogin", u.host, u.port, base_path);
|
||||
|
||||
if (!quiet) {
|
||||
printf("[*] Enviando requisição de login para: %s\n", login_url);
|
||||
printf("[*] Referer: %s\n", working_url);
|
||||
}
|
||||
|
||||
return http_request("POST", login_url, working_url, iface, NULL, 5000, NULL, 0);
|
||||
}
|
||||
|
||||
/* Fluxo completo de verificação e login */
|
||||
static bool attempt_login(const char *iface, bool force, bool quiet) {
|
||||
char ssid[IW_ESSID_MAX_SIZE + 1];
|
||||
bool has_ssid = get_active_ssid(iface, ssid, sizeof(ssid));
|
||||
|
||||
if (!quiet) {
|
||||
printf("[i] Interface: %s (SSID: %s)\n", iface, has_ssid ? ssid : "Desconhecido");
|
||||
}
|
||||
|
||||
char portal_url[MAX_URL_LEN] = {0};
|
||||
|
||||
if (!force) {
|
||||
int code = check_and_discover(iface, portal_url, sizeof(portal_url));
|
||||
if (code == 204) {
|
||||
if (!quiet) {
|
||||
printf("[✓] Conexão com a internet já está ativa (HTTP 204). Login desnecessário.\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!quiet) {
|
||||
printf("[!] Sem internet livre (HTTP %d). Verificando portal captivo...\n", code);
|
||||
}
|
||||
} else {
|
||||
if (!quiet) {
|
||||
printf("[!] Modo --force: enviando requisição de login...\n");
|
||||
}
|
||||
check_and_discover(iface, portal_url, sizeof(portal_url));
|
||||
}
|
||||
|
||||
if (portal_url[0] == '\0') {
|
||||
if (!force && !is_likely_college_network(has_ssid ? ssid : NULL)) {
|
||||
if (!quiet) {
|
||||
printf("[i] Rede '%s' não possui portal captivo da faculdade detectado. Ignorando.\n",
|
||||
has_ssid ? ssid : iface);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!quiet) {
|
||||
printf("[!] Redirecionamento não detectado. Usando fallback: %s\n", DEFAULT_FALLBACK_PORTAL);
|
||||
}
|
||||
snprintf(portal_url, sizeof(portal_url), "%s", DEFAULT_FALLBACK_PORTAL);
|
||||
} else {
|
||||
if (!quiet) {
|
||||
printf("[✓] Portal detectado em: %s\n", portal_url);
|
||||
}
|
||||
}
|
||||
|
||||
int login_code = perform_login(portal_url, iface, quiet);
|
||||
if (!quiet) {
|
||||
printf("[i] Resposta do login: HTTP %d\n", login_code);
|
||||
}
|
||||
|
||||
/* Aguarda 1.5s para o AP UniFi liberar as regras de firewall */
|
||||
usleep(1500000);
|
||||
|
||||
int after_code = check_and_discover(iface, NULL, 0);
|
||||
if (after_code == 204) {
|
||||
if (!quiet) {
|
||||
printf("[✓] Login efetuado com sucesso! Internet liberada.\n");
|
||||
}
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "Login efetuado com sucesso na rede %s!", has_ssid ? ssid : "da faculdade");
|
||||
notify_desktop("Wi-Fi Conectado", msg, "network-wireless");
|
||||
return true;
|
||||
} else {
|
||||
if (!quiet) {
|
||||
printf("[✗] Falha na liberação da internet após login (HTTP %d).\n", after_code);
|
||||
}
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "Não foi possível liberar o acesso na rede %s.", has_ssid ? ssid : "da faculdade");
|
||||
notify_desktop("Falha no Login Wi-Fi", msg, "dialog-error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Modo Daemon ultra-leve:
|
||||
* Escuta eventos de rede diretamente do Kernel Linux via Netlink (RTMGRP_LINK | RTMGRP_IPV4_IFADDR)
|
||||
* sem precisar de nenhum subprocesso como 'nmcli monitor'.
|
||||
*/
|
||||
static int run_daemon(const char *user_iface, int interval_sec) {
|
||||
printf("[+] Iniciando daemon C ultra-leve (intervalo periódico: %ds)...\n", interval_sec);
|
||||
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
|
||||
char active_iface[IFNAMSIZ];
|
||||
if (user_iface) {
|
||||
snprintf(active_iface, sizeof(active_iface), "%s", user_iface);
|
||||
} else {
|
||||
get_active_wifi_interface(active_iface, sizeof(active_iface));
|
||||
}
|
||||
|
||||
if (iface_has_ipv4(active_iface)) {
|
||||
attempt_login(active_iface, false, false);
|
||||
}
|
||||
time_t last_check = time(NULL);
|
||||
|
||||
int nl_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, NETLINK_ROUTE);
|
||||
if (nl_fd >= 0) {
|
||||
struct sockaddr_nl sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.nl_family = AF_NETLINK;
|
||||
sa.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR;
|
||||
if (bind(nl_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
||||
close(nl_fd);
|
||||
nl_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
while (g_running) {
|
||||
struct pollfd pfd = { .fd = nl_fd, .events = POLLIN };
|
||||
int rc = (nl_fd >= 0) ? poll(&pfd, 1, interval_sec * 1000) : 0;
|
||||
if (!g_running) break;
|
||||
|
||||
if (nl_fd < 0) {
|
||||
sleep((unsigned int)interval_sec);
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
|
||||
if (rc > 0 && (pfd.revents & POLLIN)) {
|
||||
char buf[4096];
|
||||
bool addr_event = false;
|
||||
ssize_t len;
|
||||
while ((len = recv(nl_fd, buf, sizeof(buf), 0)) > 0) {
|
||||
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
|
||||
for (; NLMSG_OK(nlh, (unsigned int)len); nlh = NLMSG_NEXT(nlh, len)) {
|
||||
if (nlh->nlmsg_type == RTM_NEWADDR || nlh->nlmsg_type == RTM_NEWLINK) {
|
||||
addr_event = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Cooldown de 8 segundos para evitar disparos repetidos durante negociação DHCP */
|
||||
if (addr_event && (now - last_check >= 8)) {
|
||||
usleep(1500000); /* Aguarda 1.5s para rota e IP estabilizarem */
|
||||
if (!user_iface) {
|
||||
get_active_wifi_interface(active_iface, sizeof(active_iface));
|
||||
}
|
||||
if (iface_has_ipv4(active_iface)) {
|
||||
printf("[*] Evento de rede detectado no kernel (%s). Verificando...\n", active_iface);
|
||||
attempt_login(active_iface, false, false);
|
||||
last_check = time(NULL);
|
||||
}
|
||||
}
|
||||
} else if (rc == 0) {
|
||||
/* Timeout periódico */
|
||||
if (!user_iface) {
|
||||
get_active_wifi_interface(active_iface, sizeof(active_iface));
|
||||
}
|
||||
if (iface_has_ipv4(active_iface)) {
|
||||
char portal_url[MAX_URL_LEN] = {0};
|
||||
int code = check_and_discover(active_iface, portal_url, sizeof(portal_url));
|
||||
if (code != 204) {
|
||||
printf("[*] Checagem periódica detectou bloqueio (HTTP %d). Autenticando...\n", code);
|
||||
attempt_login(active_iface, false, false);
|
||||
}
|
||||
}
|
||||
last_check = time(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
if (nl_fd >= 0) close(nl_fd);
|
||||
printf("\n[+] Daemon encerrado.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void print_usage(const char *prog) {
|
||||
printf("Uso: %s [OPÇÕES]\n\n"
|
||||
"Auto-login ultra-leve em C para o portal captivo UniFi (UNICEP).\n\n"
|
||||
"Opções:\n"
|
||||
" -i, --interface IFACE Interface de rede Wi-Fi (ex: wlan0)\n"
|
||||
" -f, --force Força o envio do login mesmo se a internet parecer ativa\n"
|
||||
" -q, --quiet Modo silencioso (sem logs informativos)\n"
|
||||
" -d, --daemon Executa continuamente em background via Netlink socket\n"
|
||||
" --interval SEG Intervalo de checagem periódica no modo daemon (padrão: 60)\n"
|
||||
" -h, --help Exibe esta mensagem de ajuda\n", prog);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
/* Line-buffering para logs imediatos no systemd/journalctl */
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
|
||||
const char *user_iface = NULL;
|
||||
bool force = false;
|
||||
bool quiet = false;
|
||||
bool daemon_mode = false;
|
||||
int interval = 60;
|
||||
|
||||
static struct option long_opts[] = {
|
||||
{"interface", required_argument, 0, 'i'},
|
||||
{"force", no_argument, 0, 'f'},
|
||||
{"quiet", no_argument, 0, 'q'},
|
||||
{"daemon", no_argument, 0, 'd'},
|
||||
{"interval", required_argument, 0, 't'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}
|
||||
};
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, "i:fqdh", long_opts, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case 'i': user_iface = optarg; break;
|
||||
case 'f': force = true; break;
|
||||
case 'q': quiet = true; break;
|
||||
case 'd': daemon_mode = true; break;
|
||||
case 't': interval = atoi(optarg); if (interval < 5) interval = 5; break;
|
||||
case 'h': print_usage(argv[0]); return 0;
|
||||
default: print_usage(argv[0]); return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (daemon_mode) {
|
||||
return run_daemon(user_iface, interval);
|
||||
}
|
||||
|
||||
char active_iface[IFNAMSIZ];
|
||||
if (user_iface) {
|
||||
snprintf(active_iface, sizeof(active_iface), "%s", user_iface);
|
||||
} else {
|
||||
get_active_wifi_interface(active_iface, sizeof(active_iface));
|
||||
}
|
||||
|
||||
return attempt_login(active_iface, force, quiet) ? 0 : 1;
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
[Unit]
|
||||
Description=Auto-Login Portal Captivo UniFi (Faculdade)
|
||||
Description=Auto-Login Portal Captivo UniFi (Faculdade) - C Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
ExecStart=%h/Documentos/loginAutomatico/login_portal.py --daemon
|
||||
ExecStart=%h/Documentos/loginAutomatico/login_portal --daemon
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue