This commit is contained in:
Adrien MALINGREY 2025-05-27 02:08:54 +02:00
parent d6da7a6a2e
commit 55ac3e1d44
10 changed files with 441 additions and 116 deletions

18
db.php
View File

@ -1,18 +0,0 @@
<?php
try {
$db = new SQLite3('ipam.db');
} catch (Exception $e) {
echo $e->getMessage();
exit();
}
$db->exec(<<<SQL
CREATE TABLE IF NOT EXISTS networks (
adress INTEGER NOT NULL,
mask INTEGER NOT NULL,
nom TEXT,
description TEXT,
PRIMARY KEY (adress, masque)
);
SQL);

123
edit.php Normal file
View File

@ -0,0 +1,123 @@
<?php
include_once "load-networks.php";
$tag = "";
$network = [];
if (isset($_SERVER["QUERY_STRING"]) && preg_match(
"/^$namePtn$/", $_SERVER["QUERY_STRING"], $matches
)) {
$tag = $matches[0];
if (isset($networks[$tag])) {
$network = $networks[$tag];
}
}
if ($network == []) {
preg_match("/\n\d+: (?P<iface>[a-z0-9]+) inet (?<ipaddr>$ipPtn)\/(?<mask>\d+) brd (?<broadcast>$ipPtn)/", `ip -o -4 a`, $network);
$ip_addr = ip2long($network["ipaddr"]);
$network["cidr_mask"] = $network["mask"];
$mask = $long_mask[(int)$network["mask"]];
$tag = "local";
$network["network-addr"] = $ip_addr & $netmask;
$network["netmask"] = long2ip($mask);
$network["broadcast"] = ip2long($network["broadcast"]);
if (preg_match("/default via ($ipPtn)/", `ip -4 route`, $router)) {
$network["router"] = $router[1];
}
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IPAM</title>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.4/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.4/dist/semantic.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav class="ui inverted teal menu">
<a class="header item" href=".">IP<em>AM</em></a>
</nav>
<main class="ui container">
<h1>Éditer un réseau</h1>
<form action="update-network.php" method="post" class="ui form">
<input type="hidden" name="previous-tag" value="<?=$tag?>"/>
<div class="field">
<label for="tag" class="required">Nom</label>
<input id="tag" name="tag" type="text" pattern="[a-zA-Z_][\w\-]*" placeholder="local" required value="<?=$tag?>" title="une lettre suivie par des lettres, des chiffres ou des tirets"/>
</div>
<div class="two fields">
<div class="field">
<label for="network-addr" class="required">Adresse réseau</label>
<div class="ui labeled input">
<input id="network-addr" name="network-addr" type="text" title="xxx.xxx.xxx.xxx" required
pattern="(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)"
placeholder="<?=long2ip($network["network-addr"]) ?? "192.168.1.0" ?>"
value="<?=long2ip($network["network-addr"]) ?? "" ?>"/>
</div>
</div>
<div class="field">
<label for="netmask" class="required">Masque</label>
<select id="netmask" name="netmask" class="ui search selection dropdown">
<?php foreach ($long_mask as $cidr_mask => $netmask) :
$netmask = long2ip($netmask);
?>
<option value="<?=$netmask?>"<?=$netmask == ($network["netmask"] ?? 24) ? " selected" : ""?>>/<?=$cidr_mask?> <?=$netmask?></option>
<?php endforeach; ?>
</select>
<script>$('#netmask').dropdown()</script>
</div>
</div>
<div class="field">
<label for="router">Passerelle</label>
<input id="router" name="router" type="text" title="xxx.xxx.xxx.xxx"
pattern="(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)"
placeholder="<?=$network["router"] ?? "192.168.1.1"?>"
value="<?=$network["router"] ?? ""?>"/>
</div>
<div class="field">
<label for="dns-server">Serveur(s) DNS (laisser vide pour ce serveur)</label>
<select id="dns-server" name="dns-server" multiple class="ui search multiple selection dropdown" autocomplete="off">
</select>
<script>
const ipRegex = /^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$/;
$('#dns-server').dropdown({
allowAdditions: true,
forceSelection: false,
onAdd: function(value, text, $choice) {
if (!ipRegex.test(value)) {
$(this).dropdown('remove selected', value);
}
}
});
</script>
</div>
<h4 class="ui header">DHCP dynamique</h4>
<div class="two fields">
<div class="field">
<label for="start-addr">Première adresse</label>
<div class="ui labeled input">
<input id="start-addr" name="start-addr" type="text" title="xxx.xxx.xxx.xxx"
pattern="(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)"
placeholder="<?=$network["dhcp-range"]["start_addr"] ?? "192.168.1.100" ?>"
value="<?=$network["dhcp-range"]["start_addr"] ?? "" ?>"/>
</div>
</div>
<div class="field">
<label for="end-addr">Dernière adresse</label>
<div class="ui labeled input">
<input id="end-addr" name="end-addr" type="text" title="xxx.xxx.xxx.xxx"
pattern="(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)"
placeholder="<?=$network["end-addr"] ?? "192.168.1.200" ?>"
value="<?=$network["dhcp-range"]["end"] ?? "" ?>"/>
</div>
</div>
</div>
<button type="submit" class="ui button">Enregistrer</button>
</form>
</main>
</body>
</html>

142
index.php
View File

@ -1,100 +1,46 @@
<?php
require_once "load-networks.php";
if (isset($_SERVER["QUERY_STRING"]) && preg_match(
"/^$namePtn$/", $_SERVER["QUERY_STRING"], $matches
)) {
$tag = $matches[0];
if (isset($networks[$tag])) {
include("show-network.php");
} else {
header("Location: edit.php?$tag");
}
exit();
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IPAM</title>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.4/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.4/dist/semantic.min.js"></script>
<style>
.ui.form input[type="number"].textfield {
appearance: textfield;
}
.ui.form input[type="number"].textfield ::-webkit-inner-spin-button,
.ui.form input[type="number"].textfield ::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.ui.input.address input {
text-align: right;
}
.address :not(:first-child):not(:last-child) {
border-radius: 0 !important;
}
.address :first-child {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.address :last-child {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
</style>
</head>
<body>
<main class="ui container">
<div class="ui segment">
<form action="add-network.php" method="post" class="ui form">
<div class="field">
<label for="name">Nom</label>
<input type="text" id="name" name="name">
</div>
<div class="fields">
<div class="field">
<label for="address">Adresse</label>
<div class="ui labeled address input">
<input type="number" id="octet1Input" min="0" max="255" step="1" required class="textfield">
<div class="ui label">.</div>
<input type="number" id="octet2Input" min="0" max="255" step="1" required class="textfield">
<div class="ui label">.</div>
<input type="number" id="octet3Input" min="0" max="255" step="1" required class="textfield">
<div class="ui label">.</div>
<input type="number" id="octet4Input" min="0" max="255" step="1" required class="textfield">
</div>
</div>
<input type="hidden" id="addressInput" name="address">
<div class="field">
<label>Masque</label>
<div class="ui labeled input">
<div class="ui label">/</div>
<input type="number" id="maskInput" name="mask" min="0" max="32" step="1" required class="textfield" title="Masque au format CIDR">
</div>
</div>
<script>
let otctetInputs = [octet1Input, octet2Input, octet3Input, octet4Input]
console.log(otctetInputs.forEach)
otctetInputs.forEach(input => {
input.oninput = () => {
o1 = octet1Input.valueAsNumber || 0;
o2 = octet2Input.valueAsNumber || 0;
o3 = octet3Input.valueAsNumber || 0;
o4 = octet4Input.valueAsNumber || 0;
addressInput.value = ((o1 << 24) | (o2 << 16) | (o3 << 8) | o4) >>> 0
}
input.onfocus = () => {
input.select();
}
});
[octet1Input, octet2Input, octet3Input].forEach((input, index) => {
input.onkeydown = function (event) {
if (event.key == ".") {
event.preventDefault();
otctetInputs[index + 1].focus()
}
}
})
octet4Input.onkeydown = function (event) {
if (event.key == "/") {
event.preventDefault();
maskInput.focus()
}
}
</script>
</div>
<button type="submit" class="ui button">Ajouter</button>
</form>
</div>
</main>
</body>
</html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IPAM</title>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.4/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.4/dist/semantic.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav class="ui inverted teal menu">
<a class="header item">IP<em>AM</em></a>
</nav>
<main class="ui container">
<h1>Réseaux</h1>
<ul class="ui link list">
<?php foreach ($networks as $tag => $options) : ?>
<li class="item">
<a class="header" href="?<?=$tag?>"><?=$tag?></a>
<div class="description"><?=long2ip($options["network-addr"])?>/<?=$options["cidr_mask"]?></div>
</li>
<?php endforeach; ?>
<li class="item">
<a class='header' href="edit.php">Nouveau réseau...</a>
</li>
</ul>
</main>
</body>
</html>

4
ipam.conf Normal file
View File

@ -0,0 +1,4 @@
dhcp-optsfile=/var/lib/misc/dhcp.options
dhcp-hostsfile=/var/lib/misc/dhcp.hosts
dhcp-leasefile=/var/lib/misc/dnsmasq.leases
conf-file=/var/lib/misc/dhcp.ranges

29
load-conf.php Normal file
View File

@ -0,0 +1,29 @@
<?php
$conf_paths = [
"/etc/dnsmasq.d/ipam.conf",
__DIR__ . "/ipam.conf"
];
$default = [
"dhcp-optsfile" => "/var/lib/misc/dhcp.options",
"dhcp-hostsfile" => "/var/lib/misc/dhcp.hosts",
"dhcp-leasefile" => "/var/lib/misc/dnsmasq.leases",
"conf-file" => "/var/lib/misc/dhcp.ranges",
];
foreach($conf_paths as $path) {
if (file_exists($path)) {
$conf = array_merge($default, parse_ini_file($path, true, INI_SCANNER_TYPED));
break;
}
}
$namePtn = "[a-zA-Z_][\w-]*";
$ipPtn = "\d+\.\d+\.\d+\.\d+";
$macPtn = "[\da-f]{2}(?:[:-][\da-f]{2}){5}";
$domainPtn = "[\w-]+(\.[\w-]+)*";
$ifacePtn = "[\w,]+";
$modesPtn = "static|proxya-only|slaac|ra-names|ra-stateless|ra-advrouter|off-link";
$timePtn = "\d+[mhds]";
$commentPtn = "[ \t]*(?:# ?(?<comment>.*))?";

39
load-hosts.php Normal file
View File

@ -0,0 +1,39 @@
<?php
require_once "load-conf.php";
$hosts = [];
if (isset($conf["dhcp-hostsfile"]) && file_exists($conf["dhcp-hostsfile"])) {
foreach(file($conf["dhcp-hostsfile"]) as $line) {
if (preg_match(
"/^(?<hwaddrs>$macPtn(,$macPtn)*)(id:[^,]+)*(?:,(?:tag|set):(?<tag>$namePtn))?(?:,(?<ipaddr>$ipPtn))?(?:,(?<hostnames>$domainPtn(,$domainPtn)*))?(?:,(?<lease_time>$timePtn|infinite))?([ \t]+#services:(?<services>\w[,\w]*))?$commentPtn$/",
$line, $host, PREG_UNMATCHED_AS_NULL
)) {
$hosts[ip2long($host["ipaddr"])] = [
"hwaddrs" => $host["hwaddrs"] ? explode(',', $host["hwaddrs"]) : [],
"tag" => $host["tag"] ?? "local",
"hostnames" => $host["hostnames"] ? explode(',', $host["hostnames"]) : [],
"lease_time" => $host["lease_time"],
"services" => $host["services"]? explode(',', $host["services"]) : [],
"comment" => $host["comment"],
];
}
}
}
if(isset($conf["dhcp-leasefile"]) && file_exists($conf["dhcp-leasefile"])) {
foreach(file($conf["dhcp-leasefile"]) as $line) {
if (preg_match(
"/^(?<expiry>\d+) (?<hwaddr>$macPtn) (?<ipaddr>$ipPtn) (?:\*|(?<hostname>\$domainPtn)) (?<duid>[^ ]+)(?: (?<tag>.*))?$/",
$line, $lease, PREG_UNMATCHED_AS_NULL
)) {
$hosts[ip2long($lease["ipaddr"])] = [
"hwaddrs" => [$lease["hwaddr"]],
"tag" => $host["tag"] ?? "local",
"hostnames" => [$lease["hostname"]],
"lease_time" => null,
"services" => [],
"comment" => "DHCP dynamique",
];
}
}
}

78
load-networks.php Normal file
View File

@ -0,0 +1,78 @@
<?php
require_once "load-conf.php";
$networks = [];
if (isset($conf["dhcp-optsfile"]) && file_exists($conf["dhcp-optsfile"])) {
foreach(file($conf["dhcp-optsfile"]) as $line) {
if (preg_match(
"/^(?:tag:(?<tag>$namePtn),)?option:(?<option>$namePtn),(?<value>[^#\r\n]*)$commentPtn$/",
$line, $option
)) {
if (!isset($networks[$option["tag"] ?? "default"])) $networks[$option["tag"] ?? "default"] = [];
$networks[$option["tag"] ?? "default"][$option["option"]] = $option["value"];
}
}
}
if (isset($conf["conf-file"]) && file_exists($conf["conf-file"])) {
foreach(file($conf["conf-file"]) as $line) {
if (preg_match(
"/^dhcp-range=(?:(?:tag|set):(?<tag>$namePtn),)?(?<start_addr>$ipPtn),(?:(?<end_addr>$ipPtn)|$modesPtn)(?:,(?<netmask>$ipPtn)(?:,$ipPtn)?)?(?:,(?<lease_time>$timePtn|infinite))?$commentPtn$/",
$line, $range, PREG_UNMATCHED_AS_NULL
)) {
if (!isset($networks[$range["tag"] ?? "default"])) $networks[$range["tag"] ?? "default"] = [];
$networks[$range["tag"] ?? "default"]["dhcp-range"] = [
"start_addr" => $range["start_addr"],
"end_addr" => $range["end_addr"],
"netmask" => $range["netmask"],
"lease_time" => $range["lease_time"],
];
}
}
}
$long_mask = [
0b00000000000000000000000000000000,
0b10000000000000000000000000000000,
0b11000000000000000000000000000000,
0b11100000000000000000000000000000,
0b11110000000000000000000000000000,
0b11111000000000000000000000000000,
0b11111100000000000000000000000000,
0b11111110000000000000000000000000,
0b11111111000000000000000000000000,
0b11111111100000000000000000000000,
0b11111111110000000000000000000000,
0b11111111111000000000000000000000,
0b11111111111100000000000000000000,
0b11111111111110000000000000000000,
0b11111111111111000000000000000000,
0b11111111111111100000000000000000,
0b11111111111111110000000000000000,
0b11111111111111111000000000000000,
0b11111111111111111100000000000000,
0b11111111111111111110000000000000,
0b11111111111111111111000000000000,
0b11111111111111111111100000000000,
0b11111111111111111111110000000000,
0b11111111111111111111111000000000,
0b11111111111111111111111100000000,
0b11111111111111111111111110000000,
0b11111111111111111111111111000000,
0b11111111111111111111111111100000,
0b11111111111111111111111111110000,
0b11111111111111111111111111111000,
0b11111111111111111111111111111100,
0b11111111111111111111111111111110,
0b11111111111111111111111111111111,
];
foreach ($networks as $tag => &$options) {
$ip_addr = ip2long($network["router"] ?? $options["dhcp-range"]["start_addr"]);
$netmask = ip2long($network["netmask"] ?? $options["dhcp-range"]["netmask"]);
$hostmask = 0xFFFFFFFF & ~$netmask;
$options["network-addr"] = $ip_addr & $netmask;
$options["broadcast"] = $ip_addr | $hostmask;
$options["cidr_mask"] = array_search($netmask, $long_mask);
}

34
save.php Normal file
View File

@ -0,0 +1,34 @@
<?php
require_once "load.php";
$dhcp_options = "";
$dhcp_ranges = "";
foreach($networks as $tag => $options) {
foreach($options as $option => $value) {
if ($option == "dhcp-range") {
$dhcp_ranges .= "dhcp-range=set:$tag,{$value["start_addr"]},{$value["end_addr"]}";
if (isset($value["netmask"])) $dhcp_ranges .= ",{$value["netmask"]}";
if (isset($value["lease_time"])) $dhcp_ranges .= ",{$value["lease_time"]}";
$dhcp_ranges .= "\n";
} else {
$dhcp_options .= "tag:$tag,option:$option,$value\n";
}
}
}
try {
file_put_contents($conf["dhcp-optsfile"], $dhcp_options);
} catch (Exception $e) {
http_response_code(500);
die($e->getMessage());
}
try {
file_put_contents($conf["conf-file"], $dhcp_ranges);
} catch (Exception $e) {
http_response_code(500);
die($e->getMessage());
}
echo $dhcp_ranges;

43
show-network.php Normal file
View File

@ -0,0 +1,43 @@
<?php
require_once "load-networks.php";
require_once "load-hosts.php";
$network = $networks[$tag];
?>
<html>
<head>
<style>
table {
border-collapse: collapse;
}
th, td {
border: 1px solid;
}
</style>
</head>
<body>
<h1>
<?=$tag?><br/>
<small><?=long2ip($network["network-addr"])?>/<?=$network["cidr_mask"]?></small>
</h1>
<table>
<tbody>
<tr><th></th><th>Adresse IP</th><th>Adresse MAC</th><th>Nom d'hôte</th><th>Description</th><th>Services</th><th></th></tr>
<?php
for ($ipaddr = $network["network-addr"] + 1; $ipaddr < $network["broadcast"]; $ipaddr++):
$host = $hosts[$ipaddr] ?? [];
?>
<tr>
<td></td>
<td><?=long2ip($ipaddr)?></td>
<td><?=implode("<br/>\n", $host["hwaddrs"] ?? [])?></td>
<td><?=implode("<br/>\n", $host["hostnames"] ?? [])?></td>
<td><?=$host["comment"] ?? "" ?></td>
<td><?=implode(", ", $host["services"] ?? [])?></td>
<td></td>
</tr>
<?php endfor; ?>
</tbody>
</table>
</body>
</html>

47
style.css Normal file
View File

@ -0,0 +1,47 @@
ul {
margin-top: 0 !important;
margin-bottom: 1em !important;
padding-top: 0.7em !important;
padding-left: 0 !important;
padding-bottom: 0 !important;
}
li {
border-left: 2px solid #DDD;
margin: 0 !important;
padding-top: 0.5em !important;
padding-bottom: 0.5em !important;
padding-left: 0.7em !important;
}
li::before {
display: none !important;
}
.ui.form input[type="number"].textfield {
appearance: textfield;
}
.ui.form input[type="number"].textfield ::-webkit-inner-spin-button,
.ui.form input[type="number"].textfield ::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.ui.input.address input {
text-align: right;
}
.address :not(:first-child):not(:last-child) {
border-radius: 0 !important;
}
.address :first-child {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.address :last-child {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}