Pchełki Powershell: pobudka!

https://xpil.eu/4FckO

Mam w sieci lokalnej maszynę z Linuksem, której używam od czasu do czasu do różnych eksperymentów podczas pisania kodu. Nie ma tam nic ważnego i nie używam jej zbyt często, ale czasem się jednak przydaje. W czasie kiedy się nie przydaje, komputer ów (pełnowymiarowy pecet swoją drogą) przechodzi w stan spoczynku łamane przez uśpienia, bo po co marnować cenne cykle procesora

... i nie mniej cenne elektrony z elektrowni, za które ostatnio przychodzi płacić coraz więcej...

Ze stanu tego można go wybudzić albo dźgając paluchem w przycisk zasilania, albo wysyłając specjalny sygnał do karty sieciowej.

Dźganie paluchem ma dwie wady: raz, nie wiem, czy w danym momencie komp śpi czy nie (jeżeli nie śpi, to go przyciskiem power wyłączę), a dwa, muszę w tym celu wstać i ruszyć swoją przerośniętą sempiternę o całe półtora metra, no ileż można, nieprawdaż.

Dlatego napisałem sobie ostatnio[1] prościutki skrypt, który "budzi" kompa zdalnie, używając w tym celu starej jak świat technologii WOL, czyli po naszemu Wake On LAN. Skryptem się dzisiaj dzielę z P.T. czytelniactwem z braku lepszego laku.

function Send-WOL {
    param ($MacAddress)

    $MacAddress = $MacAddress.Replace('-', '').Replace(':', '')
    $MacBytes = [byte[]]@()

    for ($i = 0; $i -lt 6; $i++) {
        $MacBytes += 0xFF
    }

    for ($i = 0; $i -lt 16; $i++) {
        for ($j = 0; $j -lt 6; $j++) {
            $MacBytes += [convert]::ToByte($MacAddress.Substring($j * 2, 2), 16)
        }
    }

    $UdpClient = New-Object System.Net.Sockets.UdpClient
    $UdpClient.Connect("255.255.255.255", 9)
    $UdpClient.Send($MacBytes, $MacBytes.Length)
    $UdpClient.Close()
}

$mac = "90-1B-0E-68-E0-B1"
Send-WOL -MacAddress $mac

Kod jest dość prosty. Jedyna tajemnicza część to dwie zagnieżdżone pętle w liniach 11-15. Otóż WOL działa tak, że wysyła do sieci lokalnej - w trybie UDP - specjalny "magiczny" pakiet. Do całej sieci, nie do jakiegoś konkretnego komputera, ponieważ ten "śpiący" z racji swojego spania chwilowo nie ma adresu IP (nawet jeżeli miał tuż przez zaśnięciem i będzie go miał tuż po obudzeniu, w czasie snu adres ten jest nieaktywny). "Magiczny" pakiet odbierają więc wszystkie komputery w sieci lokalnej, ale tylko jeden z nich nań zareaguje - ten, którego adres MAC będzie pasował do zawartości pakietu.

Sam "magiczny" pakiet natomiast ma zawartość całkiem banalną: zaczyna się od sześciu bajtów zapełnionych samymi binarnymi jedynkami (czyli FF:FF:FF:FF:FF:FF - tę kwestię załatwiają linie 7-9), a zaraz po nich następuje szesnaście powtórzeń adresu MAC maszyny, którą chcemy obudzić. Stąd też zewnętrzna pętla wykonuje się 16 razy, a w wewnętrznej czytamy adres MAC literka po literce, przekształcamy na binarne bajty i dopisujemy do "magicznego" pakietu.

Skrypt działa całkiem zgrabnie i budzi mojego śpiącego peceta bezproblemowo i za każdym razem.

P.S. Dopisane tego samego dnia po południu: ten sam skrypt w wersji w Pythonie wygląda dużo prościej:

import socket
import binascii

def send_wol(mac_address):
    mac_address = mac_address.replace('-', '').replace(':', '')
    
    mac_bytes = binascii.unhexlify(mac_address)
    magic_packet = b'\xff' * 6 + mac_bytes * 16
    
    udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_client.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    udp_client.sendto(magic_packet, ('255.255.255.255', 9))
    udp_client.close()

mac = "90-1B-0E-68-E0-B1"
send_wol(mac)

P.S.2. Dopisane późną nocą: z nudów poprosiłem LLM, żeby mi to przerobił na C++. Po parugodzinnej walce z kompilatorem i innymi bibliotekami, udało się. Po drodze łącznie dobrze ponad 6GB do pobrania z Sieci, żeby w ogóle móc skompilować do exe.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

void send_wol_packet(const char *mac_address) {
    unsigned char packet[102];
    unsigned char mac[6];
    struct sockaddr_in dest;
    SOCKET sock;
    WSADATA wsa;

    sscanf(mac_address, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
           &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]);

    memset(packet, 0xFF, 6);
    for (int i = 0; i < 16; i++) {
        memcpy(&packet[i * 6 + 6], mac, 6);
    }

    WSAStartup(MAKEWORD(2, 2), &wsa);

    sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sock == INVALID_SOCKET) {
        printf("Could not create socket: %d\n", WSAGetLastError());
        return;
    }

    int optval = 1;
    setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char *)&optval, sizeof(optval));

    dest.sin_family = AF_INET;
    dest.sin_port = htons(9);
    dest.sin_addr.s_addr = INADDR_BROADCAST;

    sendto(sock, (const char *)packet, sizeof(packet), 0, (struct sockaddr *)&dest, sizeof(dest));

    closesocket(sock);
    WSACleanup();
}

int main() {
    const char *mac_address = "90:1B:0E:68:E0:B1"; // Your hardcoded MAC address
    send_wol_packet(mac_address);
    return 0;
}

Plik wykonywalny to prawie półmegabajtowa binarka (ale działa!). Nie bardzo rozumiem, czemu takie to wielkie. Skończyły się czasy, kiedy malutki programik kompilowało się do malutkiego pliku wykonywalnego...

P.S.3. Za sugestią Roziego zainstalowałem pakiet etherwake, który dostarcza polecenie wakeonlan. Też działa! Skrypt tym razem w Perlu, razem coś z sześć kilobajtów.

[1] Z wydatną pomocą LLM, ale ćśśśś, może się nie wyda.

https://xpil.eu/4FckO

1 Comment

  1. O, ciekawe. Rozwiązanie znam i nawet używałem (czy też: szukałem zastosowania), ale nie sądziłem, że ten pakiet taki długi jest. Wydawało mi się, że dwukrotna długość MAC jakoś, a tu niespodzianka.

    BTW sprawdziłem i wersja z gotowym magic_packet skompresowanym zlib i zakodowanym base64 wygląda dość rozsądnie, jeśli chodzi o wielkość. Ale tracimy czytelność i elastyczność.

    Jeśli masz WSL2 to może po prostu wykorzystać polecenie wol?

Leave a Comment

Komentarze mile widziane.

Jeżeli chcesz do komentarza wstawić kod, użyj składni:
[code]
tutaj wstaw swój kod
[/code]

Jeżeli zrobisz literówkę lub zmienisz zdanie, możesz edytować komentarz po jego zatwierdzeniu.