This commit is contained in:
Aiden McClelland
2025-11-05 14:51:11 -07:00
parent 2c05e6129c
commit 7e888b825c
8 changed files with 217 additions and 75 deletions

View File

@@ -34,7 +34,13 @@ import { TuiForm } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { ApiService } from 'src/app/services/api/api.service'
import { getIp, DeviceData, MappedSubnet, subnetValidator } from './utils'
import {
getIp,
DeviceData,
MappedSubnet,
subnetValidator,
ipInSubnetValidator,
} from './utils'
@Component({
template: `
@@ -132,6 +138,11 @@ export class DevicesAdd {
range ? `${name} (${range})` : ''
protected onSubnet(subnet: MappedSubnet) {
this.form.controls.ip.clearValidators()
this.form.controls.ip.addValidators([
Validators.required,
ipInSubnetValidator(subnet.range),
])
const ip = getIp(subnet)
if (ip) {

View File

@@ -30,25 +30,31 @@ export function subnetValidator({ value }: AbstractControl<MappedSubnet>) {
: { noHosts: 'No hosts available' }
}
export function getIp({ clients, range }: MappedSubnet) {
const { prefix, octets } = new IpNet(range)
const used = Object.keys(clients).map(ip =>
new utils.IpAddress(ip).octets.at(3),
)
export const ipInSubnetValidator = (subnet: string | null = null) => {
const ipnet = subnet && utils.IpNet.parse(subnet)
return ({ value }: AbstractControl<string>) => {
let ip: utils.IpAddress
try {
ip = utils.IpAddress.parse(value)
} catch (e) {
return { invalidIp: 'Not a valid IP Address' }
}
if (!ipnet) return null
return ipnet.contains(ip)
? null
: { notInSubnet: `Address is not part of ${subnet}` }
}
}
for (let i = 2; i < totalHosts(prefix); i++) {
if (!used.includes(i)) {
return [...octets.slice(0, 3), i].join('.')
export function getIp({ clients, range }: MappedSubnet) {
const net = IpNet.parse(range)
const last = net.last()
for (let ip = net.add(1); ip.cmp(last) === -1; ip.add(1)) {
if (!clients[ip.address]) {
return ip.address
}
}
return ''
}
function totalHosts(prefix: number) {
// Handle special cases per RFC 3021
if (prefix === 31) return 4 // point-to-point, 2 usable addresses
if (prefix === 32) return 3 // single host, 1 usable address
return Math.pow(2, 32 - prefix)
}

View File

@@ -73,7 +73,7 @@ export default class PortForwards {
map(g =>
Object.values(g)
.flatMap(
val => val.ipInfo?.subnets.map(s => new utils.IpNet(s)) || [],
val => val.ipInfo?.subnets.map(s => utils.IpNet.parse(s)) || [],
)
.filter(s => s.isIpv4() && s.isPublic())
.map(s => s.address),

View File

@@ -131,12 +131,13 @@ export default class Subnets {
}
private getNext(): string {
const used = this.subnets().map(s => new utils.IpNet(s.range).octets.at(2))
for (let i = 0; i < 256; i++) {
if (!used.includes(i)) {
return `10.59.${i}.0/24`
}
const last =
this.subnets()
.map(s => utils.IpNet.parse(s.range))
.sort((a, b) => -1 * a.cmp(b))[0] ?? utils.IpNet.parse('10.58.255.0/24')
const next = utils.IpNet.fromIpPrefix(last.last().add(2), 24)
if (!next.isPublic()) {
return next.ipnet
}
// No recommendation if /24 subnets are used