magicoli / kick-inside
Pure PHP UPnP IGD port mapping (miniupnpc-like) without CLI dependencies.
Requires
- php: ^8.1
- ext-libxml: *
- ext-simplexml: *
Requires (Dev)
- pestphp/pest: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-10 22:17:15 UTC
README
Pure PHP UPnP IGD port mapping, without the miniupnpc CLI. Open or close port mappings on a router for one or several ports, over TCP or UDP.
The library talks to the router directly: SSDP discovery, device description parsing, and SOAP calls are all done in PHP. No external binary, no shell command.
Requirements
- PHP 8.1 or later
ext-simplexmlandext-libxml- A router that supports UPnP IGD (most home routers do)
Installation
Run this from the root of the application that will use the library:
composer require magicoli/kick-inside
Composer adds the package to the application's autoloader. In a standalone PHP entry point, load that autoloader (most frameworks already do this during bootstrap):
require __DIR__ . '/vendor/autoload.php'; $mapper = \Magicoli\KickInside\PortMapper::discover(); $mapper->open(8080, \Magicoli\KickInside\Protocol::Tcp);
Usage
use Magicoli\KickInside\Port; use Magicoli\KickInside\PortMapper; use Magicoli\KickInside\Protocol; // Find the gateway on the local network. $mapper = PortMapper::discover(); // Open a single TCP port. $mapper->open(8080); // Open a list of ports. $mapper->open([8080, 8081, 8082], Protocol::Tcp); // Open a port for both TCP and UDP. $mapper->open(8080, [Protocol::Tcp, Protocol::Udp]); // Map an external port to a different internal port. $mapper->open(new Port(8080, 80, Protocol::Tcp)); // Close mappings. Plain ports close both TCP and UDP by default. $mapper->close(8080); $mapper->close([8080, 8081], Protocol::Udp); // Read the external IP address and the open mappings. echo $mapper->externalIp(); $mappings = $mapper->mappings();
open() accepts an optional description, lease duration, force flag, and rollback flag:
$mapper->open(8080, Protocol::Tcp, 'my app', leaseDuration: 3600, force: true, rollback: true);
A lease duration of 0 (the default) means a permanent mapping.
Internal client
The internal client is the address the router forwards to. By default it is auto-detected from the machine's routing table. You can set it explicitly with an IP address or a network interface name:
// By IP address. $mapper = PortMapper::discover(internalClient: '192.168.1.10'); // By interface name (useful for VPN interfaces like wg0). $mapper = PortMapper::discover(internalClient: 'wg0'); // Or when building the mapper directly. $mapper = new PortMapper($gateway, 'wg0');
LocalAddress::interfaces() lists the available interfaces and their IPv4 address.
Behavior
The library is idempotent, so calling open() or close() twice is safe:
- If the requested mapping is already active and points to the same destination,
open()succeeds. - If the port is already mapped to a different destination,
open()throws aPortConflictExceptiondescribing the existing mapping. Passforce: trueto replace it instead. - If the mapping does not exist,
close()succeeds.
When opening several ports, a failure stops the loop. The mappings opened so far are kept by default; pass rollback: true to close them again and keep the whole set consistent.
Command line
A CLI is available as bin/kick-inside, a self-contained executable. Composer also exposes it as vendor/bin/kick-inside when the package is installed as a dependency:
kick-inside 8002/tcp 8003/udp 8100-8120/tcp --interface=wg0 kick-inside close 8002 8003 kick-inside list kick-inside ip kick-inside interfaces
open is the default command, so kick-inside 8080 opens port 8080.
Port specs:
8080— external and internal port 80808080/udp— protocol udp8080:80— external 8080 to internal 808100-8120/tcp— port range
Options accept either a space or = before their value, for example --description myapp or --description=myapp.
--protocol tcp|udp|both(default:tcpforopen,bothforclose)--interface <name>(default: auto-detected)--description <text>--lease <seconds>--force--rollback(close opened mappings again if one fails)--timeout=<seconds>(discovery timeout)
Building the executable
bin/kick-inside is a compiled phar. Rebuild it after changing the sources:
composer build:phar
The build writes the phar to build/kick-inside.phar, then installs it as the executable bin/kick-inside. The source entry point is bin/kick-inside-cli.php, which runs against the local vendor/ autoloader during development.
Extending
Gateways implement Magicoli\KickInside\Gateway. The UPnP implementation lives in Magicoli\KickInside\Service\Upnp. Other backends (ngrok, Cloudflare Tunnel, ...) can be added as new Gateway implementations and used through the same PortMapper facade.
Testing
composer test
Unit tests cover the SOAP envelope, response parsing, device description parsing, the UpnpGateway conflict handling, LocalAddress, the CLI, and the PortMapper facade. The integration test is skipped unless a real gateway is available:
KICK_INSIDE_GATEWAY=1 composer test