1
mirror of https://git.burble.com/burble.dn42/dn42regsrv.git synced 2024-02-26 20:28:04 +01:00

Add Free Explorer functionality

This commit is contained in:
Simon Marsh 2021-05-23 13:12:46 +01:00
parent 22234d8918
commit a88cf6d47a
No known key found for this signature in database
GPG Key ID: 0FCCD13AE1CF7ED8
3 changed files with 941 additions and 2 deletions

237
StaticRoot/free Normal file
View File

@ -0,0 +1,237 @@
<!doctype html>
<html lang="en">
<head>
<title>DN42 Free Explorer</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="stylesheet" href="bootstrap.min.css"/>
<link rel="stylesheet" href="material-icons.css"/>
<!-- Style overrides -->
<style>
.material-icons { display:inline-flex;vertical-align:middle }
body { box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh }
</style>
</head>
<body>
<div id="free-app">
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
<router-link to="/4" class="btn btn-secondary mx-2">IPv4</router-link>
<router-link to="/6" class="btn btn-secondary mx-2">IPv6</router-link>
<router-link to="/asn" class="btn btn-secondary mx-2">ASN</router-link>
<div class="collapse navbar-collapse w-100 ml-auto text-nowrap">
<div class="ml-auto"><router-link class="navbar-brand"
to="/">Free&nbsp;Explorer</router-link>&nbsp;<a class="navbar-brand"
href="/">Registry&nbsp;Explorer</a>&nbsp;<a class="pull-right navbar-brand"
href="https://dn42.dev/"><img src="/dn42_logo.png" width="173" height="60"/></a>
</div>
</div>
</nav>
<router-view></router-view>
</div>
<footer class="page-footer font-small">
<div style="margin-top: 20px; padding: 5px">
<a href="https://git.burble.com/burble.dn42/dn42regsrv">Source Code</a>.
Powered by
<a href="https://getbootstrap.com/">Bootstrap</a>,
<a href="https://vuejs.org">Vue.js</a>,
<a href="https://github.com/axios/axios">axios</a>,
<a href="http://alexcorvi.github.io/anchorme.js/">Anchorme</a>.
</div>
</footer>
<script type="text/x-template" id="app-free4-template">
<div class="p-5">
<p>Select prefix length:
<span class="btn-group mx-3" role="group" aria-label="Prefix Length" id="prefixselect">
<button @click="updatePrefixLen" value="24" type="button" class="btn btn-secondary">/24</button>
<button @click="updatePrefixLen" value="25" type="button" class="btn btn-secondary">/25</button>
<button @click="updatePrefixLen" value="26" type="button" class="btn btn-secondary">/26</button>
<button @click="updatePrefixLen" value="27" type="button" class="btn btn-secondary">/27</button>
<button @click="updatePrefixLen" value="28" type="button" class="btn btn-secondary">/28</button>
<button @click="updatePrefixLen" value="29" type="button" class="btn btn-secondary">/29</button>
</span>
</p>
<div class="w-auto d-inline-block px-4">
<section v-if="filter == 29">
<p class="alert alert-danger">A /29 is a very small allocation,
providing only 8 IP addresses.<br/>
Whilst you will likely only need one IPv4 address per host a more
typical allocation in DN42 is a /27.</p>
</section>
<section v-else-if="filter == 28">
<p class="alert alert-success">/28 provides only 16 IPv4 addresses, but may be suitable
for small networks.</p>
</section>
<section v-else-if="filter == 27">
<p class="alert alert-success">A /27 is the typical allocation in DN42 providing
you with 32 IPv4 addresses.</p>
</section>
<section v-else-if="filter == 26">
<p class="alert alert-info">A /26 would provide 64 IPv4 addresses
and would be enough for most large networks.</p>
</section>
<section v-else-if="filter == 25">
<p class="alert alert-warning">A /25 is considered a very
large allocation in DN42.<br/>Typically you only need one
IPv4 per host and should consider carefully before requesting such a big allocation.</p>
</section>
<section v-else-if="filter == 24">
<p class="alert alert-danger">/24 is a huge allocation in DN42
and is likely unecessary unless you have special requirements or are an organisation.
You may request a /24 prefix, but expect to provide justification to the registry maintainers.
DN42 also has address ranges specifically reserved for organisations or large
allocations. See the
<a href="https://dn42.dev/howto/Address-Space">DN42 Wiki</a> for more details.</p>
</section>
</div>
<p>
{{ ftotal }} free /{{ filter }} prefixes found, showing up to 10 random results<br/>
Select the prefix size again to get more networks.
</p>
<div class="m-5">
<pre v-for="prefix in filtered" class="text-center my-1"
style="font-size: 1.2rem">{{ prefix }}</pre>
</div>
<div class="container d-flex flex-column w-75">
<section v-if="state == 'error'">
<div class="alert alert-warning clearfix" role="alert">
An error recurred whilst retrieving data from the API
<button type="button" class="float-right btn btn-primary"
v-on:click="reload"><span class="material-icons">refresh</span>
Refresh</button>
</div>
</section>
<section v-else-if="state == 'loading'">
<div class="alert alert-info" role="alert">Loading data ...</div>
</section>
<section v-else>
<div class="text-center w-100"><span
class="mx-3"><b>IPv4</b><span><span
class="mx-3">Inetnum:&nbsp;{{ stats.alloc }}</span><span
class="mx-3">Free&nbsp;net&nbsp;blocks:&nbsp;{{ stats.nets }}</span><span
class="mx-3">Available&nbsp;IPs:&nbsp;{{ stats.addr }}&nbsp;({{
Math.round((stats.addr/262144)*100) }}%)</span>
</div>
</section>
</div>
</div>
</script>
<script type="text/x-template" id="app-free6-template">
<div class="p-5">
<p>
The recommended IPv6 prefix size in DN42 is a /48 which provides plenty of address
space for a multi-site, global network.<br/>Smaller networks ranges are not advised
as they are likely to limit the future growth of your network.
</p>
<p>
The DN42 registry is not authoritative across the whole fd00::/8 range.<br/>It is
recommended to generate an RFC4193 compliant random prefix to reduce the risk of
clashing with another network.
<p>
Creating 10 random IPv6 /48 prefixes
<button type="button" class="mx-3 btn btn-primary" @click="generate">More !</button>
</p>
<div class="m-5">
<pre v-for="prefix in plist" class="text-center my-1"
style="font-size: 1.2rem">{{ prefix }}</pre>
</div>
<div class="container d-flex flex-column w-75">
<section v-if="state == 'error'">
<div class="alert alert-warning clearfix" role="alert">
An error recurred whilst retrieving data from the API
<button type="button" class="float-right btn btn-primary"
v-on:click="reload"><span class="material-icons">refresh</span>
Refresh</button>
</div>
</section>
<section v-else-if="state == 'loading'">
<div class="alert alert-info" role="alert">Loading data ...</div>
</section>
<section v-else>
<div class="text-center w-100"><span
class="mx-3"><b>IPv6</b><span><span
class="mx-3">Inet6num: {{ stats.alloc }}</span><span
class="mx-3">Assigned /64s: ~{{ stats.nets }}M&nbsp;({{
Math.round(stats.nets*10000/72057594)/100000 }}%)</span><span
class="mx-3">Free /64s: ~{{ freenets }}T</span>
</div>
</section>
</div>
</div>
</script>
<script type="text/x-template" id="app-asn-template">
<div class="p-5">
<p>
Showing 10 random free ASNs
<button type="button" class="mx-3 btn btn-primary" @click="generate">More !</button>
</p>
<div class="m-5">
<pre v-for="prefix in alist" class="text-center my-1"
style="font-size: 1.2rem">{{ prefix }}</pre>
</div>
<div class="container d-flex flex-column w-75">
<section v-if="state == 'error'">
<div class="alert alert-warning clearfix" role="alert">
An error recurred whilst retrieving data from the API
<button type="button" class="float-right btn btn-primary"
v-on:click="reload"><span class="material-icons">refresh</span>
Refresh</button>
</div>
</section>
<section v-else-if="state == 'loading'">
<div class="alert alert-info" role="alert">Loading data ...</div>
</section>
<section v-else>
<div class="text-center w-100"><span
class="mx-3"><b>ASN</b><span><span
class="mx-3">Allocated: {{ stats.alloc }}</span><span
class="mx-3">Available: {{ 4000 - stats.alloc }}
({{ Math.round(stats.alloc / 40) }}%)</span>
</div>
</section>
</div>
</div>
</script>
<script type="text/x-template" id="app-root-template">
<div class="p-3">
<div class="jumbotron">
<h1>DN42 IP Explorer</h1>
<p class="lead">Select IPv4, IPv6 or ASN to find free blocks and ASNs</p>
</div>
</div>
</script>
<script src="jquery.slim.min.js"></script>
<script src="bootstrap.bundle.min.js"></script>
<script src="vue.min.js"></script>
<script src="vue-router.min.js"></script>
<script src="axios.min.js"></script>
<script src="anchorme.min.js"></script>
<script src="anchorme.min.js"></script>
<script src="free.js"></script>
</body>
</html>

701
StaticRoot/free.js Normal file
View File

@ -0,0 +1,701 @@
//////////////////////////////////////////////////////////////////////////
// DN42 IP Explorer
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// root component
Vue.component('app-root', {
template: '#app-root-template',
methods: {
}
})
//////////////////////////////////////////////////////////////////////////
// free IPv4 view
Vue.component('app-free4', {
template: '#app-free4-template',
data() {
return {
state: 'invalid',
error: '',
inetnum: null,
p4: [ ],
free4: [ ],
stats: {
alloc: 0,
addr: 0,
nets: 0
},
filter: 27,
filtered: [ ],
ftotal: 0,
asn: '4242423999',
mntner: 'FOO',
person: 'FOO',
example: '0.0.0.0/0'
}
},
methods: {
// not used
pretty4(msg, subnet, plen) {
var octets = [ ]
octets[0] = (subnet >> 24) & 0xFF
octets[1] = (subnet >> 16) & 0xFF
octets[2] = (subnet >> 8) & 0xFF
octets[3] = subnet & 0xFF
console.log(msg, ': ', octets.join("."),'/',plen)
},
// reload prefix data
reload() {
// reset current data
this.inetnum = null
this.p4.splice(0,this.p4.length)
this.state = "loading"
// IPv4 prefixes
axios
.get('/api/registry/inetnum/*')
.then(response => {
this.inetnum = response.data
this.processIPv4()
})
.catch(error => {
this.error = error
this.state = 'error'
console.log(error)
})
},
// parse an IPv4 string in to an object
parse4(p) {
p = p.replace('_','/')
// split out prefix
var s1 = p.split('/')
// and octets
var s2 = s1[0].split('.')
if (s1.length !=2 || s2.length != 4) {
console.log("Failed to IPv4 parse ", p)
return null
}
// convert to a 32bit integer
var num =
((parseInt(s2[0]) << 24) +
(parseInt(s2[1]) << 16) +
(parseInt(s2[2]) << 8) +
parseInt(s2[3]))>>>0
var plen = parseInt(s1[1])
var mask = (0xFFFFFFFF - ((2**(32 - plen))-1))>>>0
// finally return the ipv4 object
return {
plen: plen,
mask: mask,
subnet: num
}
},
// process IPv4 prefixes
processIPv4() {
var inets = [ ]
Object.values(this.inetnum).forEach(rp => {
// map the attributes in to a hash
// no need to worry about duplicated attribs
var attrib = { }
rp.Attributes.forEach(a => {
attrib[a[0]] = a[1]
})
// convert the cidr to an object
obj = this.parse4(attrib.cidr)
if (obj != null) {
if (attrib.policy != null) {
obj.policy = attrib.policy
}
inets.push(obj)
}
})
// sort the prefixes in ascending order
var sorted = inets.sort((a,b) => {
if (a.subnet == b.subnet) {
return a.plen - b.plen
}
else {
return a.subnet - b.subnet
}
})
// splice in sorted array
this.p4.splice(0, this.p4.length, ...sorted)
// update the free list
this.updateFree4()
this.updatePrefixLen({ target: { value: this.filter } })
// all done
this.state = 'ready'
},
// update ipv4 stats
stats4(subnet, plen) {
// check DN42 range 172.20.0.0/14
var masked = (subnet & 0xFFFC0000)>>>0
if (masked == 0xAC140000) {
// add em up
this.stats.nets += 1
this.stats.addr += 2**(32-plen)
}
},
// recursively check a subnet for free blocks
scanSubnets(subnet, mask, plen) {
// check for the last defined prefix
if (this.ix >= this.p4.length) {
this.stats4(subnet, plen)
this.free4.push({
subnet: subnet,
mask: mask,
plen: plen
})
return
}
var prefix = this.p4[this.ix]
// does the next prefix match this subnet ?
if ((prefix.subnet == subnet) && (prefix.plen == plen)) {
var policy = 'assigned'
if (prefix.policy != null) {
policy = prefix.policy
}
if (policy != 'open') {
// optimisation to reduce recursion by
// scanning forward for open children
this.ix += 1
while(this.ix < this.p4.length) {
prefix = this.p4[this.ix]
var masked = (prefix.subnet & mask)>>>0
if (subnet != masked) {
// no longer a child, we're done
return
}
// found a subnet that is open
if (prefix.policy == 'open') {
this.scanSubnets(prefix.subnet,
prefix.mask,
prefix.plen,
prefix.policy)
}
else {
// don't recurse closed subnets
var tmppolicy = 'assigned'
if (prefix.policy != null) {
tmppolicy = prefix.policy
}
this.ix += 1
}
}
return
}
// move on to next index
this.ix += 1
if (this.ix >= this.p4.length) {
this.stats4(subnet, plen)
this.free4.push({
subnet: subnet,
mask: mask,
plen: plen
})
return
}
prefix = this.p4[this.ix]
}
// is the next prefix a subnet of the current search ?
var masked = (prefix.subnet & mask)>>>0
if (subnet == masked) {
// split the subnet and try again
plen += 1
var bit = (2**(32 - plen))>>>0
mask = (mask | bit)>>>0
this.scanSubnets((subnet & (~bit))>>>0, mask, plen)
this.scanSubnets((subnet | bit)>>>0, mask, plen)
}
else {
// found an open block
this.stats4(subnet, plen)
this.free4.push({
subnet: subnet,
mask: mask,
plen: plen
})
}
},
// update the free blocks list
updateFree4() {
// reset stats
this.stats.alloc = this.p4.length
this.stats.addr = 0
this.stats.nets = 0
// recursively scan for free nets
this.ix = 0
this.scanSubnets(0, 0, 0, 'undefined')
},
// filter subnets based on prefix length
filterFree() {
var tlist = [ ]
// filter the free list
this.free4.forEach(free => {
if (free.plen == this.filter) {
tlist.push(free)
}
})
this.ftotal = tlist.length
// pick up to ten random prefixes
var result = [ ]
for(var i = 0; ((tlist.length > 0) && (i < 10)); i++) {
var ix = Math.round(Math.random() * tlist.length)
var obj = tlist[ix]
// push to result
var octets = [ ]
octets[0] = (obj.subnet >> 24) & 0xFF
octets[1] = (obj.subnet >> 16) & 0xFF
octets[2] = (obj.subnet >> 8) & 0xFF
octets[3] = obj.subnet & 0xFF
result.push(octets.join(".")+'/'+obj.plen)
// remove from the list
tlist.splice(ix, 1)
}
this.filtered.splice(0, this.filtered.length, ...result)
},
// update function when selecting prefixes
updatePrefixLen(e) {
this.filter = e.target.value
// update buttons
var group = document.getElementById("prefixselect")
group.childNodes.forEach(button => {
if (button.nodeName == "BUTTON") {
button.classList.remove('btn-primary')
button.classList.remove('btn-secondary')
if (button.value == this.filter) {
button.classList.add('btn-primary')
}
else {
button.classList.add('btn-secondary')
}
}
})
// select available prefixes
this.filterFree()
},
// update the example templates
updateExample(e) {
this.example = e.text
}
},
mounted() {
// reload data if required
if (this.p4.length == 0) {
this.reload()
}
// always update the prefix selection
this.updatePrefixLen({ target: { value: this.filter } })
}
})
//////////////////////////////////////////////////////////////////////////
// free IPv6 view
Vue.component('app-free6', {
template: '#app-free6-template',
data() {
return {
state: 'invalid',
error: '',
inet6num: null,
p6: [ ],
stats: {
alloc: 0,
nets: 0
},
plist: [ ]
}
},
methods: {
// reload prefix data
reload() {
// reset current data
this.inet6num = null
this.p6.splice(0,this.p6.length)
this.state = "loading"
// IPv6 prefixes
axios
.get('/api/registry/inet6num/*')
.then(response => {
this.inet6num = response.data
this.processIPv6()
})
.catch(error => {
this.error = error
this.state = 'error'
console.log(error)
})
},
// parse an IPv6 string in to an object
parse6(cidr) {
cidr = cidr.replace('_','/')
// split prefix and length
var s1 = cidr.split('/')
var plen = parseInt(s1[1])
// ignore networks longer than /64 as they aren't valid
if (plen <= 64) {
// split out the quads
var quads = s1[0].split(':')
// and canonicalise '::'
var ix=quads.indexOf('')
if (ix != -1) {
while(quads.length < 8) {
quads.splice(ix, 0, '0')
}
}
// calculate 64bit network part of prefix
var num = BigInt(0)
var quad
for(var i = 0; i < 4; i++) {
num *= BigInt(65536)
quad = (quads[i] == '' ? 0 : parseInt('0x' + quads[i]))
num += BigInt(quad)
}
var plen = parseInt(s1[1])
var mask = BigInt(0xFFFFFFFFFFFFFFFF) - ((2n**BigInt(64-plen))-1n)
// return object
return {
plen: plen,
mask: mask,
subnet: num
}
}
},
// process IPv6 prefixes
processIPv6() {
var netspace = BigInt(0)
var nets = [ ]
Object.values(this.inet6num).forEach(rp => {
// extract attributes
var attrib = { }
rp.Attributes.forEach(a => {
attrib[a[0]] = a[1]
})
// turn in to object
obj = this.parse6(attrib.cidr)
if (obj != null) {
if (attrib.policy != null) {
obj.policy = attrib.policy
}
// ignore ::/0 and fd00::/8 when calculating netspace
if ((obj.plen > 8) && (obj.policy != 'open')) {
netspace += 2n**BigInt(64 - obj.plen)
}
nets.push(obj)
}
})
netspace /= 1000000n
this.stats.nets = Number(netspace)
// sort by subnet
var sorted = nets.sort((a,b) => {
if (a.subnet == b.subnet) {
return a.plen - b.plen
}
else {
if (a.subnet > b.subnet) { return 1 }
return -1
}
})
// update
this.p6.splice(0, this.p6.length, ...nets)
this.stats.alloc = this.p6.length
this.generate()
// all done
this.state = 'ready'
},
// check if a network is already allocated
check6(n) {
var match = null
this.p6.forEach(obj => {
// only check longer prefixes than current match
if ((match == null) || (obj.plen >= match.plen)) {
var masked = n & obj.mask
if (obj.subnet == masked) {
// new, more precise prefix found
match = obj
}
}
})
if ((match != null) && (match.policy != 'open')) {
return false
}
return true
},
// create new random prefixes
generate() {
var nlist = [ ]
// generate 10 new prefixes
for(var i = 0; i < 10; i++) {
var valid = false
var prefix = ''
while(!valid) {
// create 48bits of random address
var quads = [ ]
for(var j = 0; j < 3; j++) {
quads[j] = Math.round(Math.random()*65536)
}
// fix the first byte to be in fd00::/8
quads[0] = 0xFD00 + (quads[0] & 0xFF)
// convert to hex
var hex = [ ]
for (j = 0; j < 3; j++) {
hex[j] = quads[j].toString(16)
}
prefix = `${hex[0]}:${hex[1]}:${hex[2]}::/48`
// convert to a BigInt
var num = BigInt(0)
for(var j = 0; j < 3; j++) {
num *= BigInt(65536)
num += BigInt(quads[j])
}
num *= 65536n
// now check that the random prefix is not a subnet of another allocation
valid = this.check6(num)
}
nlist.push(prefix)
}
// swap in the new prefix list
this.plist.splice(0, this.plist.length, ...nlist)
}
},
computed: {
freenets() {
return Math.round(72057.594 - (this.stats.nets/1000000))
}
},
mounted() {
if (this.p6.length == 0) {
this.reload()
}
}
})
//////////////////////////////////////////////////////////////////////////
// free IPv6 view
Vue.component('app-asn', {
template: '#app-asn-template',
data() {
return {
state: 'invalid',
error: '',
autnum: null,
free: [ ],
stats: {
alloc: 0,
},
alist: [ ]
}
},
methods: {
// reload prefix data
reload() {
// reset current data
this.asn = null
this.free.splice(0,this.free.length)
this.state = "loading"
// fetch ASN list
axios
.get('/api/registry/aut-num')
.then(response => {
this.autnum = response.data['aut-num']
this.processASN()
})
.catch(error => {
this.error = error
this.state = 'error'
console.log(error)
})
},
// process ASN list
processASN() {
var asn = [ ]
// extract used ASN
this.autnum.forEach(a => {
// only interested in DN42 ASN
if (a.startsWith('AS424242')) {
var num = parseInt(a.substr(8))
asn[num] = true
this.stats.alloc += 1
}
})
// now work out which ASN are free
for(var a = 0; a < 4000; a++) {
if (!asn[a]) {
var str = a.toString(10)
// zero pad
while(str.length < 4) {
str = '0' + str
}
this.free.push('AS424242' + str)
}
}
this.generate()
// all done
this.state = 'ready'
},
// create new random prefixes
generate() {
var nlist = [ ]
for(var i = 0; i < 10; i++) {
// pick a random free ASN
var rand = Math.round(Math.random() * this.free.length)
nlist.push(this.free[rand])
}
// swap in the new prefix list
this.alist.splice(0, this.alist.length, ...nlist)
}
},
mounted() {
if (this.free.length == 0) {
this.reload()
}
}
})
//////////////////////////////////////////////////////////////////////////
// main vue application starts here
// initialise the Vue Router
const router = new VueRouter({
routes: [
{ path: '/', component: Vue.component('app-root') },
{ path: '/4', component: Vue.component('app-free4') },
{ path: '/6', component: Vue.component('app-free6') },
{ path: '/asn', component: Vue.component('app-asn') }
]
})
// and the main app instance
const vm = new Vue({
el: '#free-app',
data: {
},
router
})
//////////////////////////////////////////////////////////////////////////
// end of code

View File

@ -20,8 +20,9 @@ body { box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh }
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
<search-input></search-input>
<div class="collapse navbar-collapse w-100 ml-auto text-nowrap">
<div class="ml-auto"><router-link class="navbar-brand"
to="/">Registry Explorer</router-link>&nbsp;<a class="pull-right navbar-brand"
<div class="ml-auto"><a class="navbar-brand"
href="/free">Free&nbsp;Explorer</a>&nbsp;<router-link class="navbar-brand"
to="/">Registry&nbsp;Explorer</router-link>&nbsp;<a class="pull-right navbar-brand"
href="https://dn42.dev/"><img src="/dn42_logo.png" width="173" height="60"/></a>
</div>
</div>