Initial commit
This commit is contained in:
commit
6c391e8c0a
5 changed files with 343 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.*
|
||||
!.gitignore
|
||||
minipam
|
||||
*.yaml
|
||||
*.json
|
8
go.mod
Normal file
8
go.mod
Normal file
|
@ -0,0 +1,8 @@
|
|||
module minipam
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
6
go.sum
Normal file
6
go.sum
Normal file
|
@ -0,0 +1,6 @@
|
|||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
118
index.html
Normal file
118
index.html
Normal file
|
@ -0,0 +1,118 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>MinIPAM</title>
|
||||
<style>
|
||||
.status_online {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
.status_offline {
|
||||
background-color: orangered;
|
||||
}
|
||||
.status_free {
|
||||
background-color: dimgray;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function formatHumanFriendlyTime(timestamp) {
|
||||
const now = new Date();
|
||||
const time = new Date(timestamp);
|
||||
|
||||
// Calculate the difference in milliseconds
|
||||
const diffMilliseconds = now - time;
|
||||
|
||||
// Convert milliseconds to seconds
|
||||
const diffSeconds = Math.floor(diffMilliseconds / 1000);
|
||||
|
||||
// Define time thresholds in seconds
|
||||
const minuteThreshold = 60;
|
||||
const hourThreshold = 3600;
|
||||
const dayThreshold = 86400;
|
||||
|
||||
// Format the time relative to the current moment
|
||||
if (diffSeconds < minuteThreshold) {
|
||||
return `${diffSeconds} seconds ago`;
|
||||
} else if (diffSeconds < hourThreshold) {
|
||||
const minutes = Math.floor(diffSeconds / 60);
|
||||
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||
} else if (diffSeconds < dayThreshold) {
|
||||
const hours = Math.floor(diffSeconds / 3600);
|
||||
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
} else {
|
||||
const days = Math.floor(diffSeconds / 86400);
|
||||
return `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
}
|
||||
var data = {}
|
||||
function start() {
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/data');
|
||||
xhr.onload = function() {
|
||||
if (xhr.status != 200) { // analyze HTTP status of the response
|
||||
alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
|
||||
} else { // show the result
|
||||
data = JSON.parse(xhr.responseText)
|
||||
for (const subnet in data.Subnets) {
|
||||
document.getElementById("subnetscontainer").innerHTML+="<button onClick=viewSubnet(this)>"+subnet+"</button>"
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
function viewSubnet(elem) {
|
||||
subnet = elem.innerText
|
||||
|
||||
usedAddresses = data.Subnets[subnet].used_addresses
|
||||
totalAddresses = data.Subnets[subnet].total_addresses
|
||||
|
||||
document.getElementById("subnetCidrField").innerText=subnet
|
||||
document.getElementById("subnetUsedField").innerText=usedAddresses+" of "+totalAddresses
|
||||
document.getElementById("subnetUsageBar").max=totalAddresses
|
||||
document.getElementById("subnetUsageBar").value=usedAddresses
|
||||
|
||||
|
||||
let tableHTML = '<table border="1"><thead><tr><th>IP Address</th><th>RevDNS</th><th>First seen</th><th>Last seen</th></tr></thead><tbody>';
|
||||
|
||||
for (const subnet in data.Subnets) {
|
||||
const hosts = data.Subnets[subnet].hosts;
|
||||
|
||||
// Iterate over IPs in the subnet
|
||||
data.Subnets[subnet].host_list.forEach(ip => {
|
||||
const hostDetails = hosts[ip] || {}; // Get host details if available
|
||||
|
||||
// Determine the status based on the presence in hosts and online status
|
||||
let revdns="";
|
||||
let ls=""
|
||||
let fs=""
|
||||
let status;
|
||||
if (ip in hosts) {
|
||||
status = hostDetails.online ? 'online' : 'offline';
|
||||
revdns = hostDetails.rev_dns;
|
||||
ls = formatHumanFriendlyTime(hostDetails.last_seen)
|
||||
fs = formatHumanFriendlyTime(hostDetails.first_seen)
|
||||
|
||||
} else {
|
||||
status = 'free';
|
||||
}
|
||||
// Add a row to the HTML table
|
||||
tableHTML += `<tr class="status_${status}"><td>${ip}</td><td>${revdns}</td><td>${fs}</td><td>${ls}</td></tr>`;
|
||||
});
|
||||
}
|
||||
|
||||
tableHTML += '</tbody></table>';
|
||||
document.getElementById("subnetview").innerHTML=tableHTML
|
||||
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onLoad="start()">
|
||||
Subnets:<br>
|
||||
<div id="subnetscontainer"></div>
|
||||
<hr>
|
||||
<p>Subnet <span id="subnetCidrField"></span></p>
|
||||
<p>Space usage <span id="subnetUsedField"></span><progress id="subnetUsageBar"></progress></p>
|
||||
<div id="subnetview"></div>
|
||||
</body>
|
||||
</html>
|
206
minipam.go
Normal file
206
minipam.go
Normal file
|
@ -0,0 +1,206 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"github.com/gorilla/mux"
|
||||
"gopkg.in/yaml.v3"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigT struct {
|
||||
BindAddress string `yaml:"bind_address"`
|
||||
ScanSubnets []string `yaml:"scan_subnets"`
|
||||
DelayBetweenScans time.Duration `yaml:"delay_between_scans"`
|
||||
ExcludeSpecialAddresses bool `yaml:"exclude_special_addresses"`
|
||||
PersistenceLocation string `yaml:"persistence_location"`
|
||||
UseTLS bool `yaml:"use_tls"`
|
||||
TLSKeyFile string `yaml:"tls_key_file"`
|
||||
TLSCertFile string `yaml:"tls_cert_file"`
|
||||
}
|
||||
|
||||
type HostT struct {
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Online bool `json:"online"`
|
||||
RevDNS string `json:"rev_dns"`
|
||||
}
|
||||
|
||||
type SubnetT struct {
|
||||
Hosts map[string]HostT `json:"hosts"`
|
||||
TotalAddresses int `json:"total_addresses"`
|
||||
UsedAddresses int `json:"used_addresses"`
|
||||
HostList []string `json:"host_list"`
|
||||
}
|
||||
|
||||
type PersistenceT struct {
|
||||
Subnets map[string]SubnetT
|
||||
}
|
||||
|
||||
var conf ConfigT
|
||||
var p PersistenceT
|
||||
|
||||
//go:embed index.html
|
||||
var indexhtml []byte
|
||||
|
||||
func main() {
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatalf("Usage: %s <config location>", os.Args[0])
|
||||
}
|
||||
|
||||
f, err := os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open %s: %s", os.Args[1], err)
|
||||
}
|
||||
err = yaml.NewDecoder(f).Decode(&conf)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config file: %s", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
f, err = os.Open(conf.PersistenceLocation)
|
||||
if err != nil {
|
||||
log.Printf("Failed to open persistence file: %s. Will try to create new one later.", err)
|
||||
} else {
|
||||
err = json.NewDecoder(f).Decode(&p)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to decode persistence file: %s", err)
|
||||
}
|
||||
}
|
||||
if p.Subnets == nil {
|
||||
p.Subnets = make(map[string]SubnetT)
|
||||
}
|
||||
go scanner()
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
|
||||
_, _ = writer.Write(indexhtml)
|
||||
}).Methods("GET")
|
||||
|
||||
r.HandleFunc("/data", func(writer http.ResponseWriter, request *http.Request) {
|
||||
_ = json.NewEncoder(writer).Encode(&p)
|
||||
}).Methods("GET")
|
||||
server := &http.Server{
|
||||
Addr: conf.BindAddress,
|
||||
Handler: r,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
},
|
||||
}
|
||||
if conf.UseTLS {
|
||||
err = server.ListenAndServeTLS(conf.TLSCertFile, conf.TLSKeyFile)
|
||||
} else {
|
||||
err = server.ListenAndServe()
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ping(addr string) bool {
|
||||
cmd := exec.Command("ping", "-n", "-W", "0.2", "-c", "1", addr)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scanner() {
|
||||
for {
|
||||
|
||||
for _, v := range conf.ScanSubnets {
|
||||
|
||||
persistenceSubnet, ok := p.Subnets[v]
|
||||
if !ok {
|
||||
persistenceSubnet = SubnetT{}
|
||||
persistenceSubnet.Hosts = make(map[string]HostT)
|
||||
}
|
||||
log.Printf("Scanning subnet %s", v)
|
||||
prefix, err := netip.ParsePrefix(v)
|
||||
if err != nil {
|
||||
log.Printf("Error: %s", err)
|
||||
continue
|
||||
}
|
||||
prefix = prefix.Masked()
|
||||
addr := prefix.Addr()
|
||||
|
||||
if conf.ExcludeSpecialAddresses {
|
||||
addr = addr.Next()
|
||||
}
|
||||
persistenceSubnet.TotalAddresses = 0
|
||||
persistenceSubnet.UsedAddresses = 0
|
||||
persistenceSubnet.HostList = make([]string, 0)
|
||||
for {
|
||||
if !prefix.Contains(addr) {
|
||||
break
|
||||
}
|
||||
//skip broadcast address
|
||||
if conf.ExcludeSpecialAddresses && !prefix.Contains(addr.Next()) {
|
||||
break
|
||||
}
|
||||
persistenceSubnet.TotalAddresses++
|
||||
//fmt.Println(addr.String())
|
||||
persistenceSubnet.HostList = append(persistenceSubnet.HostList, addr.String())
|
||||
pingstate := ping(addr.String())
|
||||
host, ok := persistenceSubnet.Hosts[addr.String()]
|
||||
if pingstate {
|
||||
persistenceSubnet.UsedAddresses++
|
||||
//log.Printf("%s is up", addr.String())
|
||||
rdnsString := ""
|
||||
rdns, err := net.LookupAddr(addr.String())
|
||||
if err == nil {
|
||||
if len(rdns) > 0 {
|
||||
rdnsString = rdns[0]
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
persistenceSubnet.Hosts[addr.String()] = HostT{
|
||||
FirstSeen: time.Now(),
|
||||
LastSeen: time.Now(),
|
||||
Online: true,
|
||||
RevDNS: rdnsString,
|
||||
}
|
||||
} else {
|
||||
host.LastSeen = time.Now()
|
||||
host.Online = true
|
||||
host.RevDNS = rdnsString
|
||||
persistenceSubnet.Hosts[addr.String()] = host
|
||||
}
|
||||
|
||||
} else if ok {
|
||||
host.Online = false
|
||||
persistenceSubnet.Hosts[addr.String()] = host
|
||||
persistenceSubnet.UsedAddresses++
|
||||
}
|
||||
|
||||
addr = addr.Next()
|
||||
}
|
||||
p.Subnets[v] = persistenceSubnet
|
||||
}
|
||||
log.Printf("Scan finished")
|
||||
f, err := os.Create(conf.PersistenceLocation)
|
||||
if err != nil {
|
||||
log.Printf("Failed to save persistence: %s", err)
|
||||
} else {
|
||||
err := json.NewEncoder(f).Encode(&p)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
log.Printf("Failed to encode persistence: %s", err)
|
||||
}
|
||||
}
|
||||
time.Sleep(conf.DelayBetweenScans)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue