Search by

magicoli / kick-inside

magicoli

Pure PHP UPnP IGD port mapping (miniupnpc-like) without CLI dependencies.

0.1.0 2026-10-10 21:55 UTC

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-simplexml and ext-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 a PortConflictException describing the existing mapping. Pass force: true to 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 8080
  • 8080/udp — protocol udp
  • 8080:80 — external 8080 to internal 80
  • 8100-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: tcp for open, both for close)
  • --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