Cómo usar Wake On LAN desde Internet si no puedes usar Wake On WAN

En un artículo anterior te conté cómo podías arrancar tu ordenador desde la red local. Quizás, ahora que ya sabes que es posible, lo siguiente que te preguntes es: «Ok, ¿y desde Internet?». La respuesta es sí pero… a veces no. En este post te cuento el por qué y qué he hecho yo para poder utilizar Wake On LAN desde Internet.

Existe una variante de Wake On LAN llamada Wake On WAN, la cual permite justamente esto. La configuración no suele ser demasiado compleja, ya que simplemente necesitarías abrir el puerto 9 en tu router y asignar una entrada estática en la tabla ARP por cada asociación IP/MAC de los equipos que quieras arrancar. El problema que suele surgir es que muchos de los routers, sobre todo los que facilitan los proveedores de Internet, no permiten la creación de entradas estáticas en la tabla ARP, por lo que todas las entradas en dicha tabla son dinámicas y tu router perderá la cuenta de qué IP va con qué MAC, por lo que no funcionará, al menos siempre. Después de perder horas y horas buscando la forma de modificar el mío lo que he optado es por una opción intermedia y es crear una web interna en mi LAN accesible desde Internet y arrancarlos desde allí 🙂

Antes de continuar es importante que tengas en cuenta de que haciendo uso de este código y publicándolo tal cual en Internet estás exponiendo información sobre tu red interna e incluso permitiendo que nosotros apaguemos tus ordenadores 🙂 Te recomiendo que pruebes el mismo en local y si quieres publicarlo tomes las medidas necesarias para que tu sitio sea seguro. Este artículo pretende simplemente demostrar que es posible, pero por ahora el proteger tu red y equipos es cosa tuya 🙂

Para esta prueba he creado dos aplicaciones: el back end que es una API con Node.js y Express y un front end con React.js.

Back-end: API con Node.js y Express

const express = require('express'),
    bodyParser = require('body-parser'),
    cors = require('cors'),
    app = express(),
    httpServer = require('http').Server(app),
    find = require('local-devices'),
    wol = require('wol');
require('dotenv').config();
httpServer.listen(9090, function () {
    console.log('listening on *:9090');
});
const corsOptions = {
    origin: process.env.URL_ORIGIN
};
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors(corsOptions));
app.get('/devices', function (req, res) {
    find().then(function (devices) {
        devices = devices.filter(device => device.mac != '<incomplete>');
        console.dir(devices);
        res.setHeader('Content-Type', 'application/json');
        res.send(JSON.stringify(devices));
    });
});
app.post('/check-if-it-is-awake', function (req, res) {
    find().then(function (devices) {
        let result = devices.filter(device => device.mac == req.body.mac);
        console.log(result);
        if (result.length > 0)
            res.send(true);
        else
            res.send(false);
    });
});
app.post('/wakeup', function (req, res) {
    console.log(req.body);
    wol.wake(req.body.mac, function (error, result) {
        if (error) {
            res.send(JSON.stringify({ error: error }));
        }
        console.log('wok.wake resutl: ' + result);
        res.send(result);
    });
});

Como puedes ver en el código anterior, he utilizado dos librerías que se ejecutarán en mi red local: local-devices la utilizo para ver todos los dispositivos que están encendidos en mi red (/devices), además de comprobar aquel que estoy intentando arrancar (/check-if-it-is-awake), y wol para mandar el paquete mágico a la MAC recibida en la petición a mi acción /wakeup.

Font-end: Web app con React.js

Para consumir la API anterior he creado un par de componentes que me permiten dos cosas:
Por un lado, Devices.js muestra los dispositivos encendidos en mi red:

import React, { Component } from 'react';
import { Header, Message, Table } from "semantic-ui-react";
export default class Devices extends Component {
    constructor(props) {
        super(props);
        this.state = {
            devices: null,
            isLoading: false
        };
    }
    componentWillMount() {
        this.findDevices();
    }
    async findDevices() {
        this.setState({ isLoading: true });
        //Find all local network devices
        const response = await fetch(process.env.REACT_APP_WOL_API_URL + '/devices', {
            method: 'GET'
        });
        const data = await response.json();
        this.setState({
            devices: data,
            isLoading: false
        });
    }
    render() {
        return (
            <div>
                <Header as="h1">Network Devices</Header>
                {this.state.isLoading && <Message info header="Finding devices..." />}
                {this.state.devices && (
                    <div>
                        <Table>
                            <thead>
                                <tr>
                                    <th>Name</th>
                                    <th>IP</th>
                                    <th>MAC</th>
                                </tr>
                            </thead>
                            <tbody>
                                {this.state.devices.map(device => (
                                    <tr id={device.ip} key={device.ip}>
                                        <td>{device.name}</td>
                                        <td>{device.ip}</td>
                                        <td>{device.mac}</td>
                                    </tr>
                                ))}
                            </tbody>
                        </Table>
                    </div>
                )}
            </div>
        );
    }
}

Y Wol.js me permite enviar la orden de arranque escribiendo la MAC del equipo:

import React, { Component } from 'react';
import { Button, Form, Message, Icon } from "semantic-ui-react";
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import delay from 'delay';

export default class Wol extends Component {
    constructor(props) {
        super(props);
        this.state = {
            isWaiting: false,
            isChecking: false,
            mac: window.localStorage.getItem('last_mac')
        };
        this.handleChangeMac = this.handleChangeMac.bind(this);
        this.onSubmit = this.onSubmit.bind(this);
    }
    handleChangeMac(e) {
        window.localStorage.setItem('last_mac', e.target.value);
        this.setState({
            mac: e.target.value
        });
    }
    async onSubmit(e) {
        e.preventDefault();
        this.setState({
            isWaiting: true
        });
        console.log('waking up: ' + this.state.mac);
        const response = await fetch(process.env.REACT_APP_WOL_API_URL + '/wakeup', {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ mac: this.state.mac })
        });
        const data = await response.json();
        if (data) {
            this.setState({
                isChecking: true
            });
            let awake = false;
            while (!awake) {
                await delay(3000);
                const response = await fetch(process.env.REACT_APP_WOL_API_URL + '/check-if-it-is-awake', {
                    method: 'POST',
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ mac: this.state.mac })
                });
                awake = await response.json();
            }
            this.setState({
                isChecking: false
            });
            toast.success("Your device is awake ?!", {
                position: toast.POSITION.TOP_CENTER
            });
        }
        else {
            toast.error("Something happend with your device", {
                position: toast.POSITION.TOP_CENTER
            });
        }
        this.setState({
            isWaiting: false
        });
    }
    render() {
        return (
            <div>
                <Form onSubmit={this.onSubmit}>
                    <Form.Field>
                        <label>Type a MAC address</label>
                        <input
                            placeholder="Enter PC MAC"
                            value={this.state.mac}
                            onChange={this.handleChangeMac}
                        />
                    </Form.Field>
                    <Button type="submit" loading={this.state.isWaiting}>
                        Wake up
                </Button>
                </Form>
                {this.state.isChecking && (<Message icon>
                    <Icon name='circle notched' loading />
                    <Message.Content>
                        <Message.Header>Just one second</Message.Header>
                        We are waiting for your device.
                    </Message.Content>
                </Message>)}
                <ToastContainer />
            </div>
        );
    }
};

Ambos proyectos están en mi GitHub:

0GiS0/wake-on-lan-apis
0GiS0/wake-on-lan-client

¡Saludos!